mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* feat(themes): public tokens for diffs, git status, tabs, dividers, rows, links Adds 29 public app theme tokens (diff, git decorations, tab bar, split divider, current workspace row, host badge, chat bubble, text link), each falling back to today's built-in color or CSS chain so existing themes and built-ins render the same. Monaco's diff editor now paints from the diff tokens, with sharper default diff tints (from #34698) and a readable dark ignored-file color. The contrast gate learns one-sided drops, translucent surfaces checked over their base, and main.css chains for half-set pairs. Co-Authored-By: Claude <noreply@anthropic.com> * fix(themes): check omitted diff tints, chat bubbles and dark host badges as painted The contrast gate now derives an omitted diff tint from the theme's hue (and drops the hue when its tint hides code), checks chat bubbles against chat's mixed foreground on the chat canvas, and models the dark host badge at 80% accent. The main.css fallback table moves to plugin-theme-css-fallbacks.ts. Co-Authored-By: Claude <noreply@anthropic.com> * fix(themes): blame a diff hue only for tints it painted, and check chat diff rows A diff tint the theme set itself no longer drops the theme's hue when it fails, and chat diff rows (hue-mixed text on the line tint over the chat canvas) are now checked too. Co-Authored-By: Claude <noreply@anthropic.com> * fix(themes): new theme tokens only expose colors; unthemed surfaces paint exactly as before Every new token now falls back to the exact value its surface used before: - Diff hues default to the git decoration colors at the previous per-side percentages, and chat diff text keeps its 70% mix. - Monaco's diff view, overview ruler and links keep Monaco's stock colors unless the theme painted a diff or link token. The colors go through the derived Monaco theme instead of a global CSS override. - --text-link has no :root default; each link rule keeps its old literal as the fallback, including the markdown-light/-dark preview split and the always-#58a6ff Monaco doc link. - Tab bar, tab text and indicator fall back per element (bg-card, text-foreground, ...) instead of through :root aliases. - Dark --git-decoration-ignored goes back to #6e6e6e (#35910 ships separately). The contrast gate skips checks that have no theme token to drop, so themes that set only older tokens pay no extra probes. * perf(themes): contrast check skips ring checks the theme didn't set and precomputes drop lists --------- Co-authored-by: Claude <noreply@anthropic.com>
142 lines
6.6 KiB
TypeScript
142 lines
6.6 KiB
TypeScript
import {
|
|
completePluginAppThemeTokens,
|
|
PLUGIN_APP_THEME_REQUIRED_TOKENS,
|
|
PLUGIN_APP_THEME_TOKENS
|
|
} from './plugin-theme-artifact'
|
|
import { pluginAppThemeCssFallback } from './plugin-theme-css-fallbacks'
|
|
|
|
const REQUIRED = new Set<string>(PLUGIN_APP_THEME_REQUIRED_TOKENS)
|
|
|
|
type PluginAppThemeToken = (typeof PLUGIN_APP_THEME_TOKENS)[number]
|
|
|
|
// Why a full record: a public token added without saying what it paints fails typecheck.
|
|
const TOKEN_ROLES: Record<PluginAppThemeToken, string> = {
|
|
'--background': 'App canvas',
|
|
'--editor-surface': 'Code editor and notebook background',
|
|
'--foreground': 'Body text',
|
|
'--card': 'Cards and panels',
|
|
'--card-foreground': 'Text on `--card`',
|
|
'--popover': 'Menus, popovers and dialogs',
|
|
'--popover-foreground': 'Text on `--popover`',
|
|
'--primary': 'Primary buttons',
|
|
'--primary-foreground': 'Text on `--primary`',
|
|
'--secondary': 'Secondary buttons',
|
|
'--secondary-foreground': 'Text on `--secondary`',
|
|
'--muted': 'Quiet fills',
|
|
'--muted-foreground': 'Secondary text',
|
|
'--accent': 'Hover and selected fills',
|
|
'--accent-foreground': 'Text on `--accent`',
|
|
'--border': 'Borders and separators',
|
|
'--input': 'Input borders',
|
|
'--ring': 'Focus rings',
|
|
'--chart-1': 'Chart series 1',
|
|
'--chart-2': 'Chart series 2',
|
|
'--chart-3': 'Chart series 3',
|
|
'--chart-4': 'Chart series 4',
|
|
'--chart-5': 'Chart series 5',
|
|
'--sidebar': 'Right sidebar and settings navigation',
|
|
'--sidebar-foreground': 'Text on `--sidebar`',
|
|
'--sidebar-primary': 'Primary controls in sidebars',
|
|
'--sidebar-primary-foreground': 'Text on `--sidebar-primary`',
|
|
'--sidebar-accent': 'Hover and selected rows in sidebars',
|
|
'--sidebar-accent-foreground': 'Text on `--sidebar-accent`',
|
|
'--sidebar-border': 'Sidebar borders',
|
|
'--sidebar-ring': 'Focus rings in sidebars',
|
|
'--worktree-sidebar': 'Left workspace sidebar',
|
|
'--worktree-sidebar-foreground': 'Text on `--worktree-sidebar`',
|
|
'--worktree-sidebar-accent': 'Hover and multi-selected rows in the workspace sidebar',
|
|
'--worktree-sidebar-accent-foreground': 'Text on `--worktree-sidebar-accent`',
|
|
'--worktree-sidebar-border': 'Workspace sidebar borders',
|
|
'--worktree-sidebar-ring': 'Focus rings in the workspace sidebar',
|
|
'--worktree-sidebar-active':
|
|
'The current workspace row; may be translucent, it paints over `--worktree-sidebar`',
|
|
'--worktree-sidebar-active-border': 'Border of the current workspace row',
|
|
'--host-badge': 'Execution-host badge on workspaces (for example `Local`, an SSH host)',
|
|
'--host-badge-foreground': 'Text on `--host-badge`',
|
|
'--tab-bar': 'Strip behind tabs, including the title bar when tabs sit in it',
|
|
'--tab-active': 'Active tab',
|
|
'--tab-active-foreground': 'Active tab text, and inactive tab text on hover',
|
|
'--tab-active-indicator': 'Line under the active tab',
|
|
'--tab-inactive': 'Inactive tabs',
|
|
'--tab-inactive-foreground': 'Inactive tab text',
|
|
'--split-divider': 'Divider between split tab groups',
|
|
'--split-divider-hover': 'Split divider while hovered or dragged',
|
|
'--chat-user-message': 'Your messages in chat; may be translucent, it paints over the chat',
|
|
'--text-link': 'Links in Markdown previews and editors',
|
|
'--diff-added': 'Added-line hue: Monaco overview ruler and chat diff text',
|
|
'--diff-added-background': 'Added lines in diffs',
|
|
'--diff-added-emphasis': 'Added characters within a changed line',
|
|
'--diff-added-gutter': 'Gutter beside added lines',
|
|
'--diff-removed': 'Removed-line hue: Monaco overview ruler and chat diff text',
|
|
'--diff-removed-background': 'Removed lines in diffs',
|
|
'--diff-removed-emphasis': 'Removed characters within a changed line',
|
|
'--diff-removed-gutter': 'Gutter beside removed lines',
|
|
'--git-decoration-added': 'Added files in the file explorer and source control',
|
|
'--git-decoration-modified': 'Modified files',
|
|
'--git-decoration-deleted': 'Deleted files',
|
|
'--git-decoration-renamed': 'Renamed files',
|
|
'--git-decoration-untracked': 'Untracked files',
|
|
'--git-decoration-copied': 'Copied files',
|
|
'--git-decoration-ignored': 'Git-ignored files'
|
|
}
|
|
|
|
// Why: these mix per mode in main.css, which the derivation rules cannot express.
|
|
const MODE_MIXES: Partial<Record<PluginAppThemeToken, string>> = {
|
|
'--worktree-sidebar-active': 'a translucent wash of `--worktree-sidebar-foreground`',
|
|
'--worktree-sidebar-active-border': 'a mix of `--worktree-sidebar-border`',
|
|
'--chat-user-message': '`--foreground` mixed lightly into the chat background'
|
|
}
|
|
|
|
/** What an omitted token becomes, read back from the real derivation with every other token
|
|
* standing in as `var(<name>)`, so the table cannot drift from what Orca applies. */
|
|
function omittedValueFor(token: PluginAppThemeToken, base: 'light' | 'dark'): string {
|
|
const others = Object.fromEntries(
|
|
PLUGIN_APP_THEME_TOKENS.filter((other) => other !== token).map((other) => [
|
|
other,
|
|
`var(${other})`
|
|
])
|
|
)
|
|
const cssFallback = pluginAppThemeCssFallback(token, base)
|
|
if (cssFallback && 'tint' in cssFallback) {
|
|
return `\`${cssFallback.tint}\` at ${cssFallback.percent}% opacity`
|
|
}
|
|
const derived = cssFallback
|
|
? 'mixForeground' in cssFallback
|
|
? `color-mix(in srgb, var(--foreground) ${cssFallback.mixForeground}%, var(${cssFallback.into ?? '--background'}))`
|
|
: `var(${cssFallback[0]})`
|
|
: completePluginAppThemeTokens(others)[token]
|
|
if (derived === undefined) {
|
|
return MODE_MIXES[token] ?? "Orca's built-in color"
|
|
}
|
|
const mix = /^color-mix\(in srgb, var\((--[\w-]+)\) (\d+)%, var\((--[\w-]+)\)\)$/.exec(derived)
|
|
if (mix) {
|
|
return `\`${mix[1]}\` mixed ${mix[2]}% into \`${mix[3]}\``
|
|
}
|
|
return derived.replace(/^var\((--[\w-]+)\)$/, 'same as `$1`')
|
|
}
|
|
|
|
function omittedValue(token: PluginAppThemeToken): string {
|
|
const light = omittedValueFor(token, 'light')
|
|
const dark = omittedValueFor(token, 'dark')
|
|
return light === dark ? light : `light: ${light}; dark: ${dark}`
|
|
}
|
|
|
|
/** Colour token list for theme authors, rendered from the public token set. */
|
|
export function renderPluginThemeTokenReference(): string {
|
|
const lines = [
|
|
'# Orca app theme tokens',
|
|
'',
|
|
'<!-- Generated by `pnpm generate:plugin-schemas` from src/shared/plugins. Do not edit. -->',
|
|
'',
|
|
'Every key an app theme may set in `tokens` (see [plugin themes](plugin-themes.md)). Any other key is refused. An omitted token falls back as listed; when that source is omitted too, it falls back the same way in turn.',
|
|
'',
|
|
'| Token | Paints | Required | When omitted |',
|
|
'| --- | --- | --- | --- |',
|
|
...PLUGIN_APP_THEME_TOKENS.map(
|
|
(token) =>
|
|
`| \`${token}\` | ${TOKEN_ROLES[token]} | ${REQUIRED.has(token) ? 'yes' : 'no'} | ${REQUIRED.has(token) ? '' : omittedValue(token)} |`
|
|
)
|
|
]
|
|
return `${lines.join('\n')}\n`
|
|
}
|