mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
9ccbbf83f2
* perf(control-ui): load built-in theme palettes on demand
Every built-in theme's tokens shipped in the startup stylesheet, so the default
path downloaded six palettes it never painted and each new theme taxed everyone.
That is what pushed the startup CSS ceiling from 45 to 47 KiB when Tide, Beacon,
and Phosphor landed.
Moves the twelve non-default palettes into public/themes/<family>.css, one file
per family covering both modes. Claw stays inline because its tokens are the
:root defaults, so the default path loses nothing and gains the bytes back.
The first-paint story this needed: index.html's boot script now links the active
family's palette during head parsing, which makes it render-blocking exactly
like the app stylesheet, so a persisted theme paints its own colours on the
first frame instead of flashing the default. The href is built from the mount
prefix the gateway already stamps on <html>, so it follows a configured Control
UI base path without the script having to know one. theme.ts keeps the link
correct when the theme changes at runtime, reusing the helper the webfont
stylesheets already use.
The nested resolve-theme ternary became a family table in the same script, since
it now picks an asset as well as a data-theme value.
startup CSS 45.8 -> 42.2 KiB gzip, below the 44.3 KiB it measured before the
three themes landed; ceiling restored 47 -> 45 KiB
base.css 64.0 -> 35.8 KiB raw
Adds a regression test that blocks every bundle script and asserts the palette
still applies, so moving this back into the app bundle fails instead of silently
reintroducing the flash. Verified it catches that: with the boot-script link
removed the assertion reports `expected null to be '/themes/tide.css'`.
* fix(control-ui): publish themes after their palettes load
* fix(control-ui): clean up palette completion listeners
* refactor(control-ui): consolidate theme name resolution
206 lines
6.4 KiB
CSS
206 lines
6.4 KiB
CSS
/* Beacon palette; selected by index.html and app/theme.ts. */
|
|
|
|
/*
|
|
* Beacon targets WCAG AAA (7:1), not the AA 4.5:1 floor the other themes hold,
|
|
* for low vision, direct sunlight, projectors, and poor panels.
|
|
*
|
|
* That forces one deliberate break from the status-token convention in :root:
|
|
* elsewhere each --x-subtle is --x at 8%, but a label sitting on a tint of its
|
|
* own hue caps the achievable contrast, because the tint pulls the background
|
|
* toward the text. Beacon tints neutrally instead and lets the label carry the
|
|
* contrast, which is what buys AAA on the status pairs.
|
|
*/
|
|
:root[data-theme="beacon"] {
|
|
/*
|
|
* Accent — high-visibility amber on pure black
|
|
*
|
|
* WCAG 2.1 AAA audit (bg = #000000):
|
|
* --accent #ffc233 contrast ≈ 13.0:1 AAA ✓
|
|
* --primary-foreground #000000 on #ffc233 button: 13.0:1 ✓
|
|
* worst text pairing --muted on --bg-muted: 10.29:1 ✓
|
|
* worst status label on its neutral subtle tint: 7.05:1 ✓
|
|
*/
|
|
--ring: #ffc233;
|
|
--accent: #ffc233;
|
|
--accent-hover: #ffd45e;
|
|
--accent-muted: #ffc233;
|
|
--accent-subtle: rgba(255, 255, 255, 0.1);
|
|
--accent-glow: rgba(255, 194, 51, 0.3);
|
|
--primary: #ffc233;
|
|
--primary-hover: #ffd45e;
|
|
--primary-foreground: #000000;
|
|
|
|
/* Focus is 3px and fully opaque: a translucent ring is the first thing to
|
|
disappear on a poor panel, which is exactly this theme's audience. */
|
|
--focus: rgba(255, 194, 51, 0.35);
|
|
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 5px var(--ring);
|
|
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 5px var(--ring);
|
|
|
|
--bg: #000000;
|
|
--bg-accent: #0a0a0a;
|
|
--bg-elevated: #141414;
|
|
--bg-hover: #1c1c1c;
|
|
--bg-muted: #1c1c1c;
|
|
|
|
--card: #0a0a0a;
|
|
--card-foreground: #ffffff;
|
|
--card-highlight: rgba(255, 255, 255, 0.06);
|
|
--popover: #141414;
|
|
--popover-foreground: #ffffff;
|
|
|
|
--panel: #000000;
|
|
--panel-strong: #141414;
|
|
--panel-hover: #1c1c1c;
|
|
--chrome: rgba(0, 0, 0, 0.98);
|
|
--chrome-strong: #000000;
|
|
|
|
--text: #ffffff;
|
|
--text-strong: #ffffff;
|
|
--chat-text: #ffffff;
|
|
--muted: #c9c9c9;
|
|
--muted-strong: #dcdcdc;
|
|
--muted-foreground: #c9c9c9;
|
|
|
|
/* Borders are structure here, not decoration: hairlines vanish first. */
|
|
--border: #4a4a4a;
|
|
--border-strong: #6e6e6e;
|
|
--border-hover: #9a9a9a;
|
|
--input: #4a4a4a;
|
|
|
|
--secondary: #141414;
|
|
--secondary-foreground: #ffffff;
|
|
--accent-2: #8ecdff;
|
|
--accent-2-muted: rgba(142, 205, 255, 0.85);
|
|
--accent-2-subtle: rgba(255, 255, 255, 0.1);
|
|
|
|
/* Neutral status tints — see the block comment above. */
|
|
--ok: #5ee88a;
|
|
--ok-muted: rgba(94, 232, 138, 0.85);
|
|
--ok-subtle: rgba(255, 255, 255, 0.1);
|
|
--destructive: #ffabab;
|
|
--destructive-hover: #ffc4c4;
|
|
--destructive-foreground: #000000;
|
|
--warn: #ffc233;
|
|
--warn-muted: rgba(255, 194, 51, 0.85);
|
|
--warn-subtle: rgba(255, 255, 255, 0.1);
|
|
--danger: #ffabab;
|
|
--danger-muted: rgba(255, 171, 171, 0.85);
|
|
--danger-subtle: rgba(255, 255, 255, 0.1);
|
|
--info: #8ecdff;
|
|
--info-subtle: rgba(255, 255, 255, 0.1);
|
|
--run: #5ee88a;
|
|
|
|
/* Diff ink is a status pair too: it sits on the same neutral tints. */
|
|
--hljs-deletion: #ffaba3;
|
|
--hljs-addition: #7ee787;
|
|
|
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.9);
|
|
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.9);
|
|
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.95);
|
|
--overlay-shadow: var(--shadow-md);
|
|
--overlay-border: var(--border-strong);
|
|
|
|
--grid-line: rgba(255, 255, 255, 0.08);
|
|
/* Atkinson Hyperlegible Next everywhere, prose included: the face is part of
|
|
the accessibility contract, not an accent (see styles fonts/beacon.css). */
|
|
--font-body:
|
|
"Atkinson Hyperlegible Next", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
--font-chat: var(--font-body);
|
|
}
|
|
|
|
:root[data-theme="beacon-light"] {
|
|
/*
|
|
* Accent — dark amber on pure white
|
|
*
|
|
* WCAG 2.1 AAA audit (bg = #ffffff):
|
|
* --accent #6e4a00 contrast ≈ 8.0:1 AAA ✓
|
|
* --primary-foreground #ffffff on #6e4a00 button: 8.0:1 ✓
|
|
* worst text pairing --muted on --bg-muted: 9.72:1 ✓
|
|
* worst status label on its neutral subtle tint: 7.02:1 ✓
|
|
*/
|
|
--ring: #6e4a00;
|
|
--accent: #6e4a00;
|
|
--accent-hover: #573a00;
|
|
--accent-muted: #6e4a00;
|
|
--accent-subtle: rgba(0, 0, 0, 0.07);
|
|
--accent-glow: rgba(110, 74, 0, 0.2);
|
|
--primary: #6e4a00;
|
|
--primary-foreground: #ffffff;
|
|
|
|
--focus: rgba(110, 74, 0, 0.35);
|
|
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 5px var(--ring);
|
|
--focus-glow: 0 0 0 2px var(--bg), 0 0 0 5px var(--ring);
|
|
|
|
--bg: #ffffff;
|
|
--bg-accent: #f4f4f4;
|
|
--bg-elevated: #ffffff;
|
|
--bg-hover: #ededed;
|
|
--bg-muted: #ededed;
|
|
--bg-content: #f4f4f4;
|
|
|
|
--card: #ffffff;
|
|
--card-foreground: #000000;
|
|
--card-highlight: rgba(0, 0, 0, 0.04);
|
|
--popover: #ffffff;
|
|
--popover-foreground: #000000;
|
|
--session-hovercard-surface: var(--card);
|
|
--session-hovercard-progress-surface: var(--bg-accent);
|
|
|
|
--panel: #ffffff;
|
|
--panel-strong: #f4f4f4;
|
|
--panel-hover: #e4e4e4;
|
|
--chrome: rgba(255, 255, 255, 0.98);
|
|
--chrome-strong: #ffffff;
|
|
|
|
--text: #000000;
|
|
--text-strong: #000000;
|
|
--chat-text: #000000;
|
|
--muted: #3a3a3a;
|
|
--muted-strong: #262626;
|
|
--muted-foreground: #3a3a3a;
|
|
|
|
--border: #8a8a8a;
|
|
--border-strong: #5a5a5a;
|
|
--border-hover: #2e2e2e;
|
|
--input: #8a8a8a;
|
|
|
|
--secondary: #f4f4f4;
|
|
--secondary-foreground: #000000;
|
|
--accent-2: #09428d;
|
|
--accent-2-muted: rgba(9, 66, 141, 0.85);
|
|
--accent-2-subtle: rgba(0, 0, 0, 0.07);
|
|
|
|
/* Neutral status tints — see the block comment above. */
|
|
--ok: #0c5024;
|
|
--ok-muted: rgba(12, 80, 36, 0.85);
|
|
--ok-subtle: rgba(0, 0, 0, 0.07);
|
|
--destructive: #8c0f0f;
|
|
--destructive-foreground: #ffffff;
|
|
--warn: #613d00;
|
|
--warn-muted: rgba(97, 61, 0, 0.85);
|
|
--warn-subtle: rgba(0, 0, 0, 0.07);
|
|
--danger: #8c0f0f;
|
|
--danger-muted: rgba(140, 15, 15, 0.85);
|
|
--danger-subtle: rgba(0, 0, 0, 0.07);
|
|
--info: #09428d;
|
|
--info-subtle: rgba(0, 0, 0, 0.07);
|
|
--run: #0c5024;
|
|
|
|
/* Diff ink is a status pair too: it sits on the same neutral tints. */
|
|
--hljs-deletion: #8b0c20;
|
|
--hljs-addition: #0e5021;
|
|
|
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.28);
|
|
--shadow-lg: 0 12px 28px rgba(0, 0, 0, 0.3);
|
|
--overlay-shadow: var(--shadow-md);
|
|
--overlay-border: var(--border-strong);
|
|
|
|
--grid-line: rgba(0, 0, 0, 0.12);
|
|
/* Atkinson Hyperlegible Next everywhere, prose included: the face is part of
|
|
the accessibility contract, not an accent (see styles fonts/beacon.css). */
|
|
--font-body:
|
|
"Atkinson Hyperlegible Next", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
--font-chat: var(--font-body);
|
|
}
|