mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
feat(web): GPUI role tokens from set_theme via the palette pipeline
Add a separate GPUI_* / GPUI_LIGHT_* const set to crates/palette (mirrored from codewhale-app set_theme; Shoreline and the TUI are unchanged, per founder decision D2 = separate set), export it as --gpui-dark-* / --gpui-light-* through scripts/export-design-tokens.py, and regenerate web/app/tokens.css. web/app/styles/tokens-roles.css: - New role layer (--bg --surface --panel --text --muted --line --accent --on-accent --hover --selected --selection --ring) that follows the OS: dark under prefers-color-scheme guarded by :root:not([data-theme="light"]), repeated for [data-theme="dark"]. Not yet consumed; S2 moves the site onto it. - The folio's --gpui-* aliases and --c-* triples now resolve to the generated GPUI values instead of Shoreline, with no literal hexes in role positions: hover is primary @ 0.9 and selection primary @ 0.28, as set_theme derives them. Dark ink-mute moves from #7e7583 (3.70:1) to muted_foreground #b1b1ad (UX-05). - Removed the false "same constants the GPUI client..." comments (tokens-roles.css, tokens.rs Shoreline header). tailwind indigo-deep now reads --indigo-deep (the dropped literal --c-indigo-deep hover triple). Checks (web/): - npm run check:tokens: OK (142 tokens) - npm run lint: 0 errors, 2 pre-existing warnings - npx tsc --noEmit: pass - npx vitest run lib/gpui-role-tokens lib/blue-stage-contract lib/docs-theme-contract: 3 files, 11/11 passed - npm run build: pass - cargo test -p codewhale-palette: 81 passed, 0 failed Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
28754c4c2b
commit
3cd160e9b1
8 files changed
+347
-102
No files matched your search
@@ -222,13 +222,14 @@ pub const MATRIX_TEXT_SOFT_RGB: (u8, u8, u8) = (221, 255, 221); // #DDFFDD
|
||||
pub const MATRIX_TEXT_DIM_RGB: (u8, u8, u8) = (0, 108, 0); // #006C00, lifted for 3:1
|
||||
pub const MATRIX_BORDER_RGB: (u8, u8, u8) = (0, 204, 0); // #00CC00
|
||||
|
||||
// Shoreline — the product-client palette, shared with the GPUI desktop.
|
||||
// Shoreline — the TUI's charcoal theme.
|
||||
//
|
||||
// Warm charcoal ground and warm paper sheet, one restrained blue, and the
|
||||
// whale's ivory ink on both sides. This is the charcoal alternative to the
|
||||
// terminal's navy "Underwater" default in 0.10.0. The
|
||||
// 0.10.0 action pair uses glacial blue on charcoal and deep ocean blue on
|
||||
// paper. Shared tokens keep the terminal, desktop, and web in one system.
|
||||
// paper. The GPUI desktop does not paint these values: its theme is the
|
||||
// separate `GPUI_*` / `GPUI_LIGHT_*` set below.
|
||||
//
|
||||
// Every pair audited by `contrast::theme_contrast_violations` clears its
|
||||
// floor: body roles clear 4.5:1 on all four surfaces, hint/dim and the
|
||||
@@ -299,6 +300,36 @@ pub const SHORELINE_LIGHT_DIFF_ADDED_BG_RGB: (u8, u8, u8) = (226, 242, 230); //
|
||||
pub const SHORELINE_LIGHT_DIFF_DELETED_FG_RGB: (u8, u8, u8) = (168, 40, 80); // #A82850
|
||||
pub const SHORELINE_LIGHT_DIFF_DELETED_BG_RGB: (u8, u8, u8) = (251, 230, 236); // #FBE6EC
|
||||
|
||||
// GPUI — the desktop client's theme, mirrored from `set_theme` in
|
||||
// codewhale-app/src/workspace/mod.rs (the GPUI app has no dependency on this
|
||||
// crate, so these are kept in step by hand). The website's role tokens are
|
||||
// generated from this set; Shoreline above stays the TUI's theme. Each const
|
||||
// names the `gpui_kit` theme field it mirrors. Derived roles are not
|
||||
// duplicated here: hover is `PRIMARY` at 0.9 opacity and selection is
|
||||
// `PRIMARY` at 0.28, exactly as `set_theme` derives them.
|
||||
pub const GPUI_BG_RGB: (u8, u8, u8) = (32, 33, 35); // #202123 background
|
||||
pub const GPUI_PANEL_RGB: (u8, u8, u8) = (42, 43, 46); // #2A2B2E muted — raised inputs, popovers
|
||||
pub const GPUI_SIDEBAR_RGB: (u8, u8, u8) = (25, 26, 28); // #191A1C sidebar — recessed navigation
|
||||
pub const GPUI_TEXT_RGB: (u8, u8, u8) = (239, 238, 235); // #EFEEEB foreground
|
||||
pub const GPUI_TEXT_MUTED_RGB: (u8, u8, u8) = (177, 177, 173); // #B1B1AD muted_foreground
|
||||
pub const GPUI_BORDER_RGB: (u8, u8, u8) = (59, 60, 63); // #3B3C3F border
|
||||
pub const GPUI_PRIMARY_RGB: (u8, u8, u8) = (144, 185, 255); // #90B9FF primary — whale blue
|
||||
pub const GPUI_ON_PRIMARY_RGB: (u8, u8, u8) = (21, 36, 62); // #15243E primary_foreground
|
||||
pub const GPUI_ACCENT_RGB: (u8, u8, u8) = (48, 49, 52); // #303134 accent — hover
|
||||
pub const GPUI_LIST_ACTIVE_RGB: (u8, u8, u8) = (55, 57, 61); // #37393D list_active — selected row
|
||||
|
||||
// GPUI Light — the same `set_theme` on warm paper.
|
||||
pub const GPUI_LIGHT_BG_RGB: (u8, u8, u8) = (250, 248, 245); // #FAF8F5 background
|
||||
pub const GPUI_LIGHT_PANEL_RGB: (u8, u8, u8) = (255, 255, 255); // #FFFFFF muted — raised inputs, popovers
|
||||
pub const GPUI_LIGHT_SIDEBAR_RGB: (u8, u8, u8) = (240, 237, 232); // #F0EDE8 sidebar
|
||||
pub const GPUI_LIGHT_TEXT_RGB: (u8, u8, u8) = (40, 41, 43); // #28292B foreground
|
||||
pub const GPUI_LIGHT_TEXT_MUTED_RGB: (u8, u8, u8) = (95, 96, 93); // #5F605D muted_foreground
|
||||
pub const GPUI_LIGHT_BORDER_RGB: (u8, u8, u8) = (217, 213, 207); // #D9D5CF border
|
||||
pub const GPUI_LIGHT_PRIMARY_RGB: (u8, u8, u8) = (36, 91, 199); // #245BC7 primary
|
||||
pub const GPUI_LIGHT_ON_PRIMARY_RGB: (u8, u8, u8) = (251, 245, 238); // #FBF5EE primary_foreground
|
||||
pub const GPUI_LIGHT_ACCENT_RGB: (u8, u8, u8) = (232, 229, 224); // #E8E5E0 accent — hover
|
||||
pub const GPUI_LIGHT_LIST_ACTIVE_RGB: (u8, u8, u8) = (223, 220, 214); // #DFDCD6 list_active
|
||||
|
||||
// Semantic colors
|
||||
pub const BORDER_COLOR_RGB: (u8, u8, u8) = WHALE_BORDER_RGB;
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"""Export the Codewhale palettes to the other Codewhale clients.
|
||||
|
||||
`crates/palette/src/tokens.rs` is the single source for the product colors.
|
||||
This script parses its `WHALE_*_RGB`, `LIGHT_*_RGB`, `SHORELINE_*_RGB`, and
|
||||
`SHORELINE_LIGHT_*_RGB` consts (aliases included) and writes the same values
|
||||
as CSS custom properties so the web app stops hand-copying hexes. The
|
||||
Shoreline set is the Shoreline redesign's dark + light pair; `--whale-*` and
|
||||
`--light-*` stay until the components that use them migrate.
|
||||
This script parses its `WHALE_*_RGB`, `LIGHT_*_RGB`, `SHORELINE_*_RGB`,
|
||||
`SHORELINE_LIGHT_*_RGB`, `GPUI_*_RGB`, and `GPUI_LIGHT_*_RGB` consts (aliases
|
||||
included) and writes the same values as CSS custom properties so the web app
|
||||
stops hand-copying hexes. The GPUI pair mirrors the desktop client's
|
||||
`set_theme` and backs the website's role tokens; Shoreline is the TUI's
|
||||
charcoal theme; `--whale-*` and `--light-*` stay until the components that
|
||||
use them migrate.
|
||||
|
||||
Target: <repo>/web/app/tokens.css. This script writes nothing outside this
|
||||
repository.
|
||||
@@ -28,8 +30,8 @@ TOKENS_RS = REPO / "crates/palette/src/tokens.rs"
|
||||
SOURCE_LABEL = "crates/palette/src/tokens.rs"
|
||||
|
||||
CONST_RE = re.compile(
|
||||
r"^pub const ((?:SHORELINE_LIGHT|SHORELINE|WHALE|LIGHT)_[A-Z0-9_]+)_RGB: \(u8, u8, u8\) = "
|
||||
r"(?:\((\d+), (\d+), (\d+)\)|((?:SHORELINE_LIGHT|SHORELINE|WHALE|LIGHT)_[A-Z0-9_]+)_RGB);",
|
||||
r"^pub const ((?:GPUI_LIGHT|GPUI|SHORELINE_LIGHT|SHORELINE|WHALE|LIGHT)_[A-Z0-9_]+)_RGB: \(u8, u8, u8\) = "
|
||||
r"(?:\((\d+), (\d+), (\d+)\)|((?:GPUI_LIGHT|GPUI|SHORELINE_LIGHT|SHORELINE|WHALE|LIGHT)_[A-Z0-9_]+)_RGB);",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
@@ -59,8 +61,14 @@ def css_name(name: str) -> str:
|
||||
`LIGHT_*` consts export as `--light-*` so the website's paper surface can
|
||||
reference the same light-mode ink and border values the TUI ships. The
|
||||
Shoreline dark pair exports as `--shoreline-*` and its light pair as
|
||||
`--shoreline-light-*`, matching the theme the TUI and GPUI clients open
|
||||
on."""
|
||||
`--shoreline-light-*`, the TUI's charcoal theme. The GPUI desktop's
|
||||
`set_theme` pair exports as `--gpui-dark-*` and `--gpui-light-*`; the
|
||||
`dark` infix keeps the generated names clear of the hand-kept `--gpui-*`
|
||||
aliases in web/app/styles/tokens-roles.css."""
|
||||
if name.startswith("GPUI_LIGHT_"):
|
||||
return "--gpui-light-" + name.removeprefix("GPUI_LIGHT_").lower().replace("_", "-")
|
||||
if name.startswith("GPUI_"):
|
||||
return "--gpui-dark-" + name.removeprefix("GPUI_").lower().replace("_", "-")
|
||||
if name.startswith("SHORELINE_LIGHT_"):
|
||||
return "--shoreline-light-" + name.removeprefix("SHORELINE_LIGHT_").lower().replace(
|
||||
"_", "-"
|
||||
|
||||
+130
-59
@@ -1,52 +1,48 @@
|
||||
/* ---------- root tokens — GPUI theme ---------- */
|
||||
/* ---------- site tokens — the folio ---------- */
|
||||
/*
|
||||
* The site is a folio read in the product client's light: warm paper at the
|
||||
* surface, warm charcoal at depth. Above the waterline the field is the GPUI
|
||||
* light theme's paper and the ink is its plum-charcoal foreground. Below the
|
||||
* waterline (`.ocean-column`, the footer, every terminal plate) the same
|
||||
* names resolve to the dark charcoal tokens, so a component written once
|
||||
* reads correctly on either side.
|
||||
* The site is still a folio read in the product client's light: warm paper
|
||||
* at the surface, warm charcoal at depth. Above the waterline the field is
|
||||
* the GPUI light theme; below it (`.ocean-column`, the footer, every
|
||||
* terminal plate) the same names resolve to the GPUI dark theme, so a
|
||||
* component written once reads correctly on either side. (The site-wide
|
||||
* theme contract that moves these onto the role layer above is a separate
|
||||
* slice.)
|
||||
*
|
||||
* The token NAMES keep their historical shape (`--paper`, `--ink`,
|
||||
* `--indigo`) so every component rule stays diffable; the VALUES resolve
|
||||
* through the `--gpui-*` block below, which now references the generated
|
||||
* Shoreline tokens in ./tokens.css (crates/palette/src/tokens.rs is the
|
||||
* single source — the same constants the GPUI client and the TUI open on).
|
||||
* Only values with no Shoreline slot stay literal: the hover tones, the
|
||||
* accent washes, and the AA-adapted state hues. `--whale-*`/`--light-*`
|
||||
* remain available to anything that genuinely wants the terminal palette.
|
||||
* through the `--gpui-*` aliases below, which reference the generated GPUI
|
||||
* tokens. Only the warm state hues (tan, moss, rust and their AA-adapted
|
||||
* inks) stay literal: `set_theme` has no slot for them.
|
||||
*/
|
||||
:root {
|
||||
/* --- The GPUI source palette (generated Shoreline slots) --- */
|
||||
--gpui-paper: var(--shoreline-light-surface); /* light background — warm paper */
|
||||
--gpui-paper-raised: var(--shoreline-light-elevated); /* light card/popover */
|
||||
--gpui-paper-muted: var(--shoreline-light-panel); /* light muted surface */
|
||||
--gpui-ink: var(--shoreline-light-text-body); /* light foreground */
|
||||
--gpui-ink-soft: var(--shoreline-light-text-soft);
|
||||
--gpui-ink-mute: var(--shoreline-light-text-muted); /* light muted_foreground */
|
||||
--gpui-edge: var(--shoreline-light-border); /* light border */
|
||||
--gpui-primary: var(--shoreline-light-action); /* light primary */
|
||||
--gpui-primary-hover: #00536d; /* light button_primary_hover — no Shoreline slot */
|
||||
--gpui-accent: #dcebee; /* light accent — no Shoreline slot */
|
||||
--gpui-selection: var(--shoreline-light-selection); /* light selection */
|
||||
/* --- GPUI light (above the waterline) --- */
|
||||
--gpui-paper: var(--gpui-light-bg); /* background — warm paper */
|
||||
--gpui-paper-raised: var(--gpui-light-panel); /* muted — card/popover */
|
||||
--gpui-paper-muted: var(--gpui-light-sidebar); /* sidebar — recessed */
|
||||
--gpui-ink: var(--gpui-light-text); /* foreground */
|
||||
--gpui-ink-soft: var(--gpui-light-text-muted); /* muted_foreground */
|
||||
--gpui-ink-mute: var(--gpui-light-text-muted); /* muted_foreground */
|
||||
--gpui-edge: var(--gpui-light-border); /* border */
|
||||
--gpui-primary: var(--gpui-light-primary); /* primary */
|
||||
--gpui-primary-hover: rgb(var(--gpui-light-primary-rgb) / 0.9); /* button_primary_hover */
|
||||
|
||||
--gpui-stage: var(--shoreline-surface); /* dark background — warm charcoal */
|
||||
--gpui-stage-deep: var(--shoreline-chrome); /* deepest dark */
|
||||
--gpui-stage-muted: var(--shoreline-panel); /* dark muted surface */
|
||||
--gpui-stage-raised: var(--shoreline-elevated); /* raised dark plate */
|
||||
--gpui-stage-ink: var(--shoreline-text-body); /* dark foreground */
|
||||
--gpui-stage-ink-soft: var(--shoreline-text-muted); /* dark muted_foreground */
|
||||
--gpui-stage-ink-dim: var(--shoreline-text-dim);
|
||||
--gpui-stage-edge: var(--shoreline-border); /* dark border */
|
||||
--gpui-primary-dark: var(--shoreline-action); /* dark primary */
|
||||
--gpui-primary-hover-dark: #93cde1; /* dark button_primary_hover — no Shoreline slot */
|
||||
--gpui-on-primary: #102d38; /* dark primary_foreground — no Shoreline slot */
|
||||
--gpui-accent-dark: #303f48; /* dark accent — no Shoreline slot */
|
||||
--gpui-selection-dark: var(--shoreline-selection); /* dark selection */
|
||||
/* --- GPUI dark (below the waterline) --- */
|
||||
--gpui-stage: var(--gpui-dark-bg); /* background — warm charcoal */
|
||||
--gpui-stage-deep: var(--gpui-dark-sidebar); /* sidebar — deepest dark */
|
||||
--gpui-stage-muted: var(--gpui-dark-panel); /* muted — raised input plate */
|
||||
--gpui-stage-raised: var(--gpui-dark-list-active); /* list_active — top plate */
|
||||
--gpui-stage-ink: var(--gpui-dark-text); /* foreground */
|
||||
--gpui-stage-ink-soft: var(--gpui-dark-text-muted); /* muted_foreground */
|
||||
--gpui-stage-ink-dim: var(--gpui-dark-text-muted); /* muted_foreground (AA, UX-05) */
|
||||
--gpui-stage-edge: var(--gpui-dark-border); /* border */
|
||||
--gpui-primary-dark: var(--gpui-dark-primary); /* primary */
|
||||
--gpui-primary-hover-dark: rgb(var(--gpui-dark-primary-rgb) / 0.9); /* button_primary_hover */
|
||||
--gpui-accent-dark: var(--gpui-dark-accent); /* accent — hover wash */
|
||||
--gpui-selection-dark: rgb(var(--gpui-dark-primary-rgb) / 0.28); /* selection */
|
||||
|
||||
/* Warm-muted state hues from the GPUI mockups, darkened or lifted per
|
||||
side of the waterline so text uses of them still clear AA. No Shoreline
|
||||
slots: these are site-specific adaptations. */
|
||||
side of the waterline so text uses of them still clear AA. No
|
||||
set_theme slots: these are site-specific adaptations. */
|
||||
--gpui-tan: #b1a17a; /* human mark */
|
||||
--gpui-moss: #739686; /* outcome */
|
||||
--gpui-rust: #aa6f6c; /* attention */
|
||||
@@ -54,17 +50,16 @@
|
||||
/* RGB channel triples backing the Tailwind surface/ink tokens (see
|
||||
tailwind.config.ts). `.ocean-column` and the opt-in docs dark sheet
|
||||
override them for their dark subtree. */
|
||||
--c-paper: var(--shoreline-light-surface-rgb);
|
||||
--c-paper-deep: var(--shoreline-light-panel-rgb);
|
||||
--c-paper-edge: var(--shoreline-light-border-rgb);
|
||||
--c-ink: var(--shoreline-light-text-body-rgb);
|
||||
--c-ink-soft: var(--shoreline-light-text-soft-rgb);
|
||||
--c-ink-mute: var(--shoreline-light-text-muted-rgb);
|
||||
--c-indigo: var(--shoreline-light-action-rgb);
|
||||
--c-indigo-deep: 0 83 109; /* light primary hover — no Shoreline slot */
|
||||
--c-stage-soft: var(--shoreline-text-muted-rgb);
|
||||
--c-stage-deep: var(--shoreline-chrome-rgb);
|
||||
--c-primary-dark: var(--shoreline-action-rgb);
|
||||
--c-paper: var(--gpui-light-bg-rgb);
|
||||
--c-paper-deep: var(--gpui-light-sidebar-rgb);
|
||||
--c-paper-edge: var(--gpui-light-border-rgb);
|
||||
--c-ink: var(--gpui-light-text-rgb);
|
||||
--c-ink-soft: var(--gpui-light-text-muted-rgb);
|
||||
--c-ink-mute: var(--gpui-light-text-muted-rgb);
|
||||
--c-indigo: var(--gpui-light-primary-rgb);
|
||||
--c-stage-soft: var(--gpui-dark-text-muted-rgb);
|
||||
--c-stage-deep: var(--gpui-dark-sidebar-rgb);
|
||||
--c-primary-dark: var(--gpui-dark-primary-rgb);
|
||||
|
||||
/* The sheet. */
|
||||
--paper: var(--gpui-paper);
|
||||
@@ -145,14 +140,13 @@
|
||||
.ocean-column,
|
||||
.site-footer,
|
||||
html[data-theme="dark"] .docs-portal {
|
||||
--c-paper: var(--shoreline-surface-rgb);
|
||||
--c-paper-deep: var(--shoreline-panel-rgb);
|
||||
--c-paper-edge: var(--shoreline-border-rgb);
|
||||
--c-ink: var(--shoreline-text-body-rgb);
|
||||
--c-ink-soft: var(--shoreline-text-muted-rgb);
|
||||
--c-ink-mute: var(--shoreline-text-dim-rgb);
|
||||
--c-indigo: var(--shoreline-action-rgb);
|
||||
--c-indigo-deep: 147 205 225; /* lifted primary — hover on dark, no Shoreline slot */
|
||||
--c-paper: var(--gpui-dark-bg-rgb);
|
||||
--c-paper-deep: var(--gpui-dark-panel-rgb);
|
||||
--c-paper-edge: var(--gpui-dark-border-rgb);
|
||||
--c-ink: var(--gpui-dark-text-rgb);
|
||||
--c-ink-soft: var(--gpui-dark-text-muted-rgb);
|
||||
--c-ink-mute: var(--gpui-dark-text-muted-rgb);
|
||||
--c-indigo: var(--gpui-dark-primary-rgb);
|
||||
--paper: var(--gpui-stage);
|
||||
--paper-deep: var(--gpui-stage-muted);
|
||||
--paper-edge: var(--gpui-stage-edge);
|
||||
@@ -174,3 +168,80 @@ html[data-theme="dark"] .docs-portal {
|
||||
--hairline: rgb(var(--c-stage-soft) / 0.2);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ---------- role tokens — GPUI set_theme ---------- */
|
||||
/*
|
||||
* One token authority: the GPUI desktop client's `set_theme`
|
||||
* (codewhale-app/src/workspace/mod.rs), mirrored as the `GPUI_*` /
|
||||
* `GPUI_LIGHT_*` consts in crates/palette/src/tokens.rs and generated into
|
||||
* ./tokens.css as `--gpui-dark-*` / `--gpui-light-*`. Nothing below repeats a
|
||||
* hex for a role: every role value is a generated token, or the primary at
|
||||
* the opacity `set_theme` derives (hover 0.9, selection 0.28).
|
||||
*
|
||||
* The role layer follows the OS: light by default, dark under
|
||||
* `prefers-color-scheme: dark` unless the page pins `data-theme="light"`,
|
||||
* and dark whenever it pins `data-theme="dark"`.
|
||||
*
|
||||
* --bg background the reading canvas
|
||||
* --surface sidebar recessed navigation and chrome
|
||||
* --panel muted raised inputs, cards, popovers
|
||||
* --text foreground
|
||||
* --muted muted_foreground secondary text (AA on --bg and --panel)
|
||||
* --line border
|
||||
* --accent primary action, links, whale blue
|
||||
* --on-accent primary_foreground
|
||||
* --hover accent hover wash, distinct from --panel
|
||||
* --selected list_active the current row
|
||||
* --selection primary @ 0.28 text selection
|
||||
* --ring primary focus ring
|
||||
*
|
||||
* Shoreline (`--shoreline-*`) is the TUI's charcoal theme and is not the
|
||||
* GPUI palette; `--whale-*`/`--light-*` remain available to anything that
|
||||
* genuinely wants the terminal palette.
|
||||
*/
|
||||
:root {
|
||||
--bg: var(--gpui-light-bg);
|
||||
--surface: var(--gpui-light-sidebar);
|
||||
--panel: var(--gpui-light-panel);
|
||||
--text: var(--gpui-light-text);
|
||||
--muted: var(--gpui-light-text-muted);
|
||||
--line: var(--gpui-light-border);
|
||||
--accent: var(--gpui-light-primary);
|
||||
--on-accent: var(--gpui-light-on-primary);
|
||||
--hover: var(--gpui-light-accent);
|
||||
--selected: var(--gpui-light-list-active);
|
||||
--selection: rgb(var(--gpui-light-primary-rgb) / 0.28);
|
||||
--ring: var(--gpui-light-primary);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: var(--gpui-dark-bg);
|
||||
--surface: var(--gpui-dark-sidebar);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--accent: var(--gpui-dark-primary);
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-accent);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--selection: rgb(var(--gpui-dark-primary-rgb) / 0.28);
|
||||
--ring: var(--gpui-dark-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg: var(--gpui-dark-bg);
|
||||
--surface: var(--gpui-dark-sidebar);
|
||||
--panel: var(--gpui-dark-panel);
|
||||
--text: var(--gpui-dark-text);
|
||||
--muted: var(--gpui-dark-text-muted);
|
||||
--line: var(--gpui-dark-border);
|
||||
--accent: var(--gpui-dark-primary);
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-accent);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--selection: rgb(var(--gpui-dark-primary-rgb) / 0.28);
|
||||
--ring: var(--gpui-dark-primary);
|
||||
}
|
||||
@@ -245,4 +245,44 @@
|
||||
--shoreline-light-diff-deleted-fg-rgb: 168 40 80;
|
||||
--shoreline-light-diff-deleted-bg: #fbe6ec;
|
||||
--shoreline-light-diff-deleted-bg-rgb: 251 230 236;
|
||||
--gpui-dark-bg: #202123;
|
||||
--gpui-dark-bg-rgb: 32 33 35;
|
||||
--gpui-dark-panel: #2a2b2e;
|
||||
--gpui-dark-panel-rgb: 42 43 46;
|
||||
--gpui-dark-sidebar: #191a1c;
|
||||
--gpui-dark-sidebar-rgb: 25 26 28;
|
||||
--gpui-dark-text: #efeeeb;
|
||||
--gpui-dark-text-rgb: 239 238 235;
|
||||
--gpui-dark-text-muted: #b1b1ad;
|
||||
--gpui-dark-text-muted-rgb: 177 177 173;
|
||||
--gpui-dark-border: #3b3c3f;
|
||||
--gpui-dark-border-rgb: 59 60 63;
|
||||
--gpui-dark-primary: #90b9ff;
|
||||
--gpui-dark-primary-rgb: 144 185 255;
|
||||
--gpui-dark-on-primary: #15243e;
|
||||
--gpui-dark-on-primary-rgb: 21 36 62;
|
||||
--gpui-dark-accent: #303134;
|
||||
--gpui-dark-accent-rgb: 48 49 52;
|
||||
--gpui-dark-list-active: #37393d;
|
||||
--gpui-dark-list-active-rgb: 55 57 61;
|
||||
--gpui-light-bg: #faf8f5;
|
||||
--gpui-light-bg-rgb: 250 248 245;
|
||||
--gpui-light-panel: #ffffff;
|
||||
--gpui-light-panel-rgb: 255 255 255;
|
||||
--gpui-light-sidebar: #f0ede8;
|
||||
--gpui-light-sidebar-rgb: 240 237 232;
|
||||
--gpui-light-text: #28292b;
|
||||
--gpui-light-text-rgb: 40 41 43;
|
||||
--gpui-light-text-muted: #5f605d;
|
||||
--gpui-light-text-muted-rgb: 95 96 93;
|
||||
--gpui-light-border: #d9d5cf;
|
||||
--gpui-light-border-rgb: 217 213 207;
|
||||
--gpui-light-primary: #245bc7;
|
||||
--gpui-light-primary-rgb: 36 91 199;
|
||||
--gpui-light-on-primary: #fbf5ee;
|
||||
--gpui-light-on-primary-rgb: 251 245 238;
|
||||
--gpui-light-accent: #e8e5e0;
|
||||
--gpui-light-accent-rgb: 232 229 224;
|
||||
--gpui-light-list-active: #dfdcd6;
|
||||
--gpui-light-list-active-rgb: 223 220 214;
|
||||
}
|
||||
@@ -34,26 +34,28 @@ const BELOW_WATERLINE = selectorBlock(
|
||||
describe("GPUI public-surface contract", () => {
|
||||
it("grounds the paper sheet in the GPUI light theme's warm paper and inks", () => {
|
||||
// Above the waterline the field is the GPUI light background — warm
|
||||
// paper — and the ink is its plum-charcoal foreground. The literals are
|
||||
// the Shoreline theme constants in crates/palette/src/tokens.rs, reached
|
||||
// through the generated tokens in app/tokens.css.
|
||||
expect(cssHexIn(ROOT, "paper")).toBe("#f5f0e9");
|
||||
expect(cssHexIn(ROOT, "paper-deep")).toBe("#ece5e0");
|
||||
expect(cssHexIn(ROOT, "paper-edge")).toBe("#d7ced5");
|
||||
expect(cssHexIn(ROOT, "paper-card")).toBe("#fffcf7");
|
||||
expect(cssHexIn(ROOT, "ink")).toBe("#302832");
|
||||
expect(cssHexIn(ROOT, "ink-soft")).toBe("#4a414c");
|
||||
expect(cssHexIn(ROOT, "ink-mute")).toBe("#6b606e");
|
||||
// Action on paper is the GPUI light primary; hover sinks to its hover.
|
||||
expect(cssHexIn(ROOT, "indigo")).toBe("#006684");
|
||||
expect(cssHexIn(ROOT, "indigo-deep")).toBe("#00536d");
|
||||
expect(cssHexIn(ROOT, "mark-ink")).toBe("#302832");
|
||||
// paper — and the ink is its foreground. The literals are the GPUI_LIGHT_*
|
||||
// consts in crates/palette/src/tokens.rs (mirrored from set_theme),
|
||||
// reached through the generated tokens in app/tokens.css.
|
||||
expect(cssHexIn(ROOT, "paper")).toBe("#faf8f5");
|
||||
expect(cssHexIn(ROOT, "paper-deep")).toBe("#f0ede8");
|
||||
expect(cssHexIn(ROOT, "paper-edge")).toBe("#d9d5cf");
|
||||
expect(cssHexIn(ROOT, "paper-card")).toBe("#ffffff");
|
||||
expect(cssHexIn(ROOT, "ink")).toBe("#28292b");
|
||||
expect(cssHexIn(ROOT, "ink-soft")).toBe("#5f605d");
|
||||
expect(cssHexIn(ROOT, "ink-mute")).toBe("#5f605d");
|
||||
// Action on paper is the GPUI light primary; hover is the same hue at
|
||||
// 0.9 opacity (`button_primary_hover`), not a second blue.
|
||||
expect(cssHexIn(ROOT, "indigo")).toBe("#245bc7");
|
||||
expect(resolveWhale("var(--gpui-primary-hover)")).toBe("rgb(var(--gpui-light-primary-rgb) / 0.9)");
|
||||
expect(ROOT).toMatch(/--indigo-deep:\s*var\(--gpui-primary-hover\);/);
|
||||
expect(cssHexIn(ROOT, "mark-ink")).toBe("#28292b");
|
||||
// The deep field is always the stage's darkest, and code plates keep the
|
||||
// stage deep on either side of the waterline.
|
||||
expect(cssHexIn(ROOT, "ocean-deep")).toBe("#1a181c");
|
||||
expect(cssHexIn(ROOT, "action-on-dark")).toBe("#67b8d6");
|
||||
expect(cssHexIn(ROOT, "ocean-current")).toBe("#67b8d6");
|
||||
expect(cssHexIn(ROOT, "code-bg")).toBe("#1a181c");
|
||||
expect(cssHexIn(ROOT, "ocean-deep")).toBe("#191a1c");
|
||||
expect(cssHexIn(ROOT, "action-on-dark")).toBe("#90b9ff");
|
||||
expect(cssHexIn(ROOT, "ocean-current")).toBe("#90b9ff");
|
||||
expect(cssHexIn(ROOT, "code-bg")).toBe("#191a1c");
|
||||
});
|
||||
|
||||
it("re-inks every dark subtree with the GPUI charcoal tokens through one rule", () => {
|
||||
@@ -61,13 +63,13 @@ describe("GPUI public-surface contract", () => {
|
||||
// share one below-the-waterline rule, so a component never needs to know
|
||||
// which side of the surface it is on.
|
||||
expect(CSS).toMatch(/\.ocean-column,\s*\.site-footer,\s*html\[data-theme="dark"\] \.docs-portal\s*\{/);
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper")).toBe("#211f23");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper-deep")).toBe("#2b282e");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper-edge")).toBe("#49424d");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink")).toBe("#f2ece5");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink-soft")).toBe("#b0a7b2");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink-mute")).toBe("#7e7583");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "indigo")).toBe("#67b8d6");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper")).toBe("#202123");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper-deep")).toBe("#2a2b2e");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "paper-edge")).toBe("#3b3c3f");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink")).toBe("#efeeeb");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink-soft")).toBe("#b1b1ad");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "ink-mute")).toBe("#b1b1ad");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "indigo")).toBe("#90b9ff");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "jade")).toBe("#9ec7b2");
|
||||
expect(cssHexIn(BELOW_WATERLINE, "signal-gold")).toBe("#d6c78f");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveWhale } from "./whale-tokens";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
const CSS = siteCss();
|
||||
const ROLES = ["bg", "surface", "panel", "text", "muted", "line", "accent", "on-accent", "hover", "selected", "selection", "ring"];
|
||||
|
||||
function declarations(block: string): Record<string, string> {
|
||||
const vars: Record<string, string> = {};
|
||||
for (const match of block.matchAll(/--([\w-]+):\s*([^;]+);/g)) vars[match[1]] = match[2].trim();
|
||||
return vars;
|
||||
}
|
||||
|
||||
/** The first block for `selector` (at any indent) that declares `--bg`. */
|
||||
function roleBlock(selector: string): Record<string, string> {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
for (const match of CSS.matchAll(new RegExp(`(?:^|\\n)\\s*${escaped}\\s*\\{([^}]*)\\}`, "g"))) {
|
||||
const vars = declarations(match[1]);
|
||||
if ("bg" in vars) return vars;
|
||||
}
|
||||
throw new Error(`No role block for ${selector}`);
|
||||
}
|
||||
|
||||
function hex(value: string): string {
|
||||
const resolved = resolveWhale(value);
|
||||
if (!/^#[0-9a-f]{6}$/i.test(resolved)) throw new Error(`not a hex color: ${value} -> ${resolved}`);
|
||||
return resolved.toLowerCase();
|
||||
}
|
||||
|
||||
function luminance(color: string): number {
|
||||
const [r, g, b] = color
|
||||
.slice(1)
|
||||
.match(/.{2}/g)!
|
||||
.map((v) => Number.parseInt(v, 16) / 255)
|
||||
.map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
function contrast(a: string, b: string): number {
|
||||
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
const light = () => roleBlock(":root");
|
||||
const osDark = () => roleBlock(':root:not([data-theme="light"])');
|
||||
const pinnedDark = () => roleBlock(':root[data-theme="dark"]');
|
||||
|
||||
describe("GPUI role tokens", () => {
|
||||
it("defines every role in light, OS-dark, and pinned-dark schemes", () => {
|
||||
for (const scheme of [light(), osDark(), pinnedDark()]) {
|
||||
for (const role of ROLES) expect(scheme, role).toHaveProperty(role);
|
||||
}
|
||||
// The OS-dark scheme is guarded so a pinned light page stays light, and
|
||||
// the pinned dark block repeats it exactly.
|
||||
expect(CSS).toMatch(/@media \(prefers-color-scheme: dark\)\s*\{\s*:root:not\(\[data-theme="light"\]\)\s*\{/);
|
||||
expect(pinnedDark()).toEqual(osDark());
|
||||
});
|
||||
|
||||
it("uses generated GPUI tokens only, never a literal hex, in role positions", () => {
|
||||
for (const scheme of [light(), osDark()]) {
|
||||
for (const role of ROLES) {
|
||||
expect(scheme[role], role).not.toMatch(/#[0-9a-f]{3,8}\b/i);
|
||||
expect(scheme[role], role).toMatch(/var\(--gpui-(light|dark)-[\w-]+\)/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("paints the GPUI set_theme values", () => {
|
||||
expect(hex(light().bg)).toBe("#faf8f5");
|
||||
expect(hex(light().accent)).toBe("#245bc7");
|
||||
expect(hex(light().hover)).toBe("#e8e5e0");
|
||||
expect(hex(light().selected)).toBe("#dfdcd6");
|
||||
expect(hex(osDark().bg)).toBe("#202123");
|
||||
expect(hex(osDark().accent)).toBe("#90b9ff");
|
||||
expect(hex(osDark().hover)).toBe("#303134");
|
||||
expect(hex(osDark().selected)).toBe("#37393d");
|
||||
expect(light().selection).toBe("rgb(var(--gpui-light-primary-rgb) / 0.28)");
|
||||
expect(osDark().selection).toBe("rgb(var(--gpui-dark-primary-rgb) / 0.28)");
|
||||
});
|
||||
|
||||
it("keeps text, muted text, links, and button text at WCAG AA in both schemes", () => {
|
||||
for (const scheme of [light(), osDark()]) {
|
||||
const bg = hex(scheme.bg);
|
||||
expect(contrast(hex(scheme.muted), bg)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme.muted), hex(scheme.panel))).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme.text), bg)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme.accent), bg)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(scheme["on-accent"]), hex(scheme.accent))).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,14 @@ import { siteCss } from "./site-css";
|
||||
* The design palette as the site actually resolves it.
|
||||
*
|
||||
* `app/tokens.css` is generated from `crates/palette/src/tokens.rs` by
|
||||
* `scripts/export-design-tokens.py`, and `app/styles/tokens-roles.css` carries the hand-kept
|
||||
* `--gpui-*` block that mirrors the GPUI client's theme. Site variables state
|
||||
* `scripts/export-design-tokens.py` (including the GPUI desktop's `set_theme`
|
||||
* pair as `--gpui-dark-*` / `--gpui-light-*`), and `app/styles/tokens-roles.css`
|
||||
* carries the hand-kept `--gpui-*` aliases over them. Site variables state
|
||||
* which token each uses (`--paper: var(--gpui-paper)`) instead of repeating
|
||||
* the hex. The contract tests still need the literal color to check parity
|
||||
* and contrast, so this reads both files and flattens the alias chains
|
||||
* (`--whale-success` -> `--whale-working-green` -> `#9bd66f`,
|
||||
* `--paper` -> `--gpui-paper` -> `#f5f0e9`). The Blue Stage light preset's
|
||||
* `--paper` -> `--gpui-paper` -> `--gpui-light-bg` -> `#faf8f5`). The Blue Stage light preset's
|
||||
* `LIGHT_*` consts export as `--light-*` beside them, and the Shoreline
|
||||
* redesign's dark/light pair exports as `--shoreline-*` /
|
||||
* `--shoreline-light-*`.
|
||||
@@ -22,7 +23,7 @@ const RAW: Record<string, string> = (() => {
|
||||
const generated = readFileSync(new URL("../app/tokens.css", import.meta.url), "utf8");
|
||||
const globals = siteCss();
|
||||
const raw: Record<string, string> = {};
|
||||
for (const match of generated.matchAll(/--((?:whale|light|shoreline-light|shoreline)-[\w-]+):\s*([^;]+);/g)) {
|
||||
for (const match of generated.matchAll(/--((?:whale|light|shoreline-light|shoreline|gpui-dark|gpui-light)-[\w-]+):\s*([^;]+);/g)) {
|
||||
raw[match[1]] = match[2].trim();
|
||||
}
|
||||
for (const match of globals.matchAll(/--(gpui-[\w-]+):\s*([^;]+);/g)) {
|
||||
|
||||
@@ -6,8 +6,9 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
// The surface, ink, and accent tokens all resolve through CSS custom
|
||||
// properties so the docs light sheet can re-theme the subtree, while
|
||||
// the default values stay the Tideline dark whale palette.
|
||||
// properties (app/styles/tokens-roles.css) so the dark subtrees can
|
||||
// re-theme themselves; the values are the generated GPUI set_theme
|
||||
// tokens. Hover is the primary at 0.9 opacity, as in set_theme.
|
||||
paper: "rgb(var(--c-paper) / <alpha-value>)",
|
||||
"paper-deep": "rgb(var(--c-paper-deep) / <alpha-value>)",
|
||||
"paper-edge": "rgb(var(--c-paper-edge) / <alpha-value>)",
|
||||
@@ -18,7 +19,7 @@ export default {
|
||||
"ink-soft": "rgb(var(--c-ink-soft) / <alpha-value>)",
|
||||
"ink-mute": "rgb(var(--c-ink-mute) / <alpha-value>)",
|
||||
indigo: "rgb(var(--c-indigo) / <alpha-value>)",
|
||||
"indigo-deep": "rgb(var(--c-indigo-deep) / <alpha-value>)",
|
||||
"indigo-deep": "var(--indigo-deep)",
|
||||
"indigo-pale": "var(--indigo-pale)",
|
||||
ochre: "var(--ochre)",
|
||||
jade: "var(--jade)",
|
||||
|
||||
Reference in new issue
Block a user