mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
feat(web): build the website on design tokens 2.3.0, the desktop app's system
The site painted with the Moda brand kit (navy grounds, signal-blue actions) over tokens 1.1.2, while the desktop app, web app and mobile run tokens 2.3.0 (pearl and charcoal grounds, ink filled actions, one slate accent). Founder direction 2026-10-27: one design system on every surface. This supersedes the 2026-10-11 Moda-kit color decision (web/docs/BRAND_COLOR_MAPPING.md is rewritten to say so). - vendor/codewhale-design is synced to 2.3.0 with codewhale-design's scripts/sync-to.sh (generate.py --check passes). Only scripts/export-design-tokens.py reads it in this repository. - The exporter now emits the 2.x roles (accent distinct from hover, ocean_depth, diff_add, diff_remove) and the type roles, motion durations and loops, easing curves, popover and dialog elevation, breakpoints and the logo ombre as --gpui-* custom properties. - tokens-roles.css names every site role as a var(--gpui-*): light, pinned dark and the stage (footer and install band, the dark set in both appearances). Type, spacing, radius and motion roles bind to the tokens; the fluid editorial display sizes stay as the site's own layout. The logo blues remain only on the whale and the mark. - The home proof strip keeps the release, source marker, licence and live contributor count; the source-tree tool and provider-route counts are removed, because a source count is not a released total (lib/public-copy.test.ts) and marketing asked for no provider count. - The locale layout comment no longer claims the site follows the OS appearance; it is light unless dark is pinned. - Contract tests that pinned the Moda kit inks, the blue sea and tide ramps and literal easing/prose sizes now assert the token roles instead; the WCAG AA check over every ink and ground in every scheme is unchanged and passes. Verified: check:latest-release, prebuild, check:docs PASS, check:tokens up to date (158 tokens), eslint clean, tsc --noEmit clean, check:locales PASS, next build compiled with 1011/1011 static pages. vitest: 770 passed, 1 failed: docs-ia sitemap count (112 vs 110) comes from the unpushed local /enterprise commit a6b26b5c3e, not from this change. check:facts fails in this checkout because its tree is behind origin/main's regenerated feature registry; not touched here. Screenshots (1440 and 390, light and pinned dark, home, product, docs, install): artifacts/design-20261027/after-tokens-2.3.0/. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bdf71d6df4
commit
e9201f5d38
38 files changed
+725
-479
No files matched your search
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the Codewhale palettes to the other Codewhale clients.
|
||||
|
||||
The versioned `vendor/codewhale-design/tokens.json` snapshot owns the GPUI
|
||||
colors, radii, typography fallbacks and focus geometry. The adapter preserves
|
||||
this site's existing light/OS-dark/pinned-dark roles. The portable artifact's
|
||||
The versioned `vendor/codewhale-design/tokens.json` snapshot owns the
|
||||
website's colors, type roles, spacing, radii, motion, easing, elevation,
|
||||
breakpoints and focus geometry; `web/app/styles/tokens-roles.css` names the
|
||||
site's roles over them (web/docs/BRAND_COLOR_MAPPING.md). The portable artifact's
|
||||
own generator checks its digest before exporting. Update the entire vendored
|
||||
folder from the app's design package; never edit its JSON here.
|
||||
|
||||
@@ -101,9 +102,10 @@ def gpui_tokens(data):
|
||||
aliases = {
|
||||
"BG": "background", "TEXT": "foreground", "PANEL": "surface",
|
||||
"TEXT_MUTED": "muted_foreground", "BORDER": "border", "SIDEBAR": "sidebar",
|
||||
"PRIMARY": "primary", "ON_PRIMARY": "primary_foreground", "ACCENT": "hover",
|
||||
"LIST_ACTIVE": "selected", "ATTENTION": "attention", "LIVE": "live",
|
||||
"DANGER": "danger", "BORDER_STRONG": "border_strong",
|
||||
"PRIMARY": "primary", "ON_PRIMARY": "primary_foreground", "ACCENT": "accent",
|
||||
"HOVER": "hover", "LIST_ACTIVE": "selected", "ATTENTION": "attention", "LIVE": "live",
|
||||
"DANGER": "danger", "BORDER_STRONG": "border_strong", "OCEAN_DEPTH": "ocean_depth",
|
||||
"DIFF_ADD": "diff_add", "DIFF_REMOVE": "diff_remove",
|
||||
}
|
||||
return [
|
||||
(f"{prefix}_{name}", tuple(bytes.fromhex(palette[key])))
|
||||
@@ -120,6 +122,25 @@ def gpui_geometry(data, digest):
|
||||
lines.append(f" --gpui-{section}-{name}: {value}px;")
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
lines.append(f" --gpui-{name.replace('_', '-')}: {data[name]};")
|
||||
for role, spec in data["type_roles"].items():
|
||||
lines += [f" --gpui-type-{role}-size: {spec['size_px'] / 16:g}rem;",
|
||||
f" --gpui-type-{role}-line: {spec['line_px'] / spec['size_px']:.4g};",
|
||||
f" --gpui-type-{role}-weight: {spec['weight']};"]
|
||||
for name, value in data["motion"].items():
|
||||
if name.startswith("duration_") or name.startswith("loop_") and name.endswith("_ms"):
|
||||
lines.append(f" --gpui-motion-{name.removesuffix('_ms').replace('_', '-')}: {value}ms;")
|
||||
lines.append(f" --gpui-motion-arrive-rise: {data['motion']['arrive_rise_px']}px;")
|
||||
for name, curve in data["easing"].items():
|
||||
lines.append(f" --gpui-easing-{name}: cubic-bezier({', '.join(f'{v:g}' for v in curve)});")
|
||||
e = data["elevation"]
|
||||
for kind in ["popover", "dialog"]:
|
||||
for mode in ["light", "dark"]:
|
||||
lines.append(f" --gpui-elevation-{kind}-{mode}: 0 {e[kind + '_y_px']}px "
|
||||
f"{e[kind + '_blur_px']}px rgb(0 0 0 / {e[kind + '_alpha_' + mode]:g});")
|
||||
for name, value in data["breakpoints"].items():
|
||||
lines.append(f" --gpui-breakpoint-{name.removesuffix('_px')}: {value}px;")
|
||||
rise, deep = data["ombre"]["rise"], data["ombre"]["deep"]
|
||||
lines.append(f" --gpui-ombre: linear-gradient(135deg, #{rise}, #{deep});")
|
||||
fallbacks = ", ".join(json.dumps(v) for v in data["typography"]["fallbacks"])
|
||||
lines += [f" --gpui-font-fallbacks: {fallbacks};",
|
||||
f" --gpui-mono-size: {data['typography']['mono_px'] / 16:g}rem;", "}"]
|
||||
|
||||
Vendored
+44
-5
@@ -2,6 +2,9 @@
|
||||
|
||||
Keep every Codewhale surface on one set of design tokens.
|
||||
|
||||
[SYSTEM.md](SYSTEM.md) is the design system: the product model, how tokens are used, components per
|
||||
medium, patterns and the current audit. [DIRECTION.md](DIRECTION.md) is how it should feel.
|
||||
|
||||
This repository is the source of truth for Codewhale's colours, type, spacing,
|
||||
radii and motion constants. The desktop app, the website, the terminal UI
|
||||
(`codewhale-ratatui`) and the mobile app each vendor a copy and verify it at
|
||||
@@ -18,20 +21,56 @@ web-next vendors this folder and imports `tokens.css`. No network access or
|
||||
sibling checkout is required to build a consumer.
|
||||
|
||||
Change the JSON here, increment its version, run `python3 generate.py`,
|
||||
and copy this entire folder into each consumer's `vendor/codewhale-design`.
|
||||
and use `scripts/sync-to.sh` to copy the runtime token files into each consumer's
|
||||
`vendor/codewhale-design`. Design studies and visual proofs stay in this repository.
|
||||
Run the generator with `--check` in each repository. Generated files include the
|
||||
source digest; consumer tests reject drift. Layout remains native to each
|
||||
surface. Focus and reduced-motion settings must remain accessible on each host.
|
||||
|
||||
Typography and palette come from the shipping desktop theme. Icons retain the
|
||||
Typography and palette come from the shipping desktop theme. `primary` fills
|
||||
actions and switches in neutral ink; `accent` is the one quiet blue, for focus
|
||||
rings, links, text selection and the caret; the logo ombre stays on the whale
|
||||
and the sign-in page. Icons retain the
|
||||
24-unit, 1.7-pixel rounded stroke family; semantic names and accessible labels
|
||||
stay with the host controls. Desktop spring constants and reduced-motion poll
|
||||
cadence are shared without introducing decorative animation on web or phones.
|
||||
stay with the host controls. Motion is shared too: durations (state, arrive,
|
||||
exit, panel, linger), the arrive rise, the two loop periods (`beat` for
|
||||
something that needs you, `breathe` for work in progress), the loop frame cap,
|
||||
three easing curves (`out`, `in`, `panel`), state glows, the logo ombre and the
|
||||
popover elevation reach Rust as constants and CSS as `--motion-*`, `--glow-*`,
|
||||
`--ombre-*` and `--elevation-popover`. Desktop spring constants and the
|
||||
reduced-motion poll cadence stay Rust-only. Under reduced motion every host
|
||||
runs durations at zero, stops loops and keeps glows static.
|
||||
|
||||
`stream` paces how received reply text is uncovered, the same way on every
|
||||
client: it follows the stream's arrival rate averaged over `rate_window_ms`,
|
||||
uncovers the first receipt within `reveal_first_ms`, never lets hidden text
|
||||
trail the received text by more than `reveal_lag_ms`, shows a receipt of at
|
||||
most `instant_graphemes` at once when nothing is waiting, and shows a backlog
|
||||
over `max_pending_bytes` (UTF-16 units on the web) at once. Rust reads them as
|
||||
`STREAM_*`; the web reads `tokens.json`. Completion, corrections and reduced
|
||||
motion always show the exact received text immediately.
|
||||
|
||||
`icons/` holds the sea glyphs DIRECTION.md names, on the same 24-unit grid at a
|
||||
1.7-pixel round stroke, each with its word as `aria-label`: sonar (searching,
|
||||
thinking), tide (real progress, waking), shell (receipt) and kelp (divider).
|
||||
A consumer copies the file it uses beside its own icons; a glyph never stands
|
||||
without its word on screen.
|
||||
|
||||
CSS consumers use `font-family: var(--font-family), var(--font-fallbacks), sans-serif`
|
||||
to keep the shared family and CJK fallbacks together. The generator rejects
|
||||
selection and primary-hover opacity values outside the inclusive 0–1 range,
|
||||
and any palette that misses WCAG AA contrast in either mode: every text role
|
||||
at 4.5:1 (`muted_foreground` at 5.5:1) and `border_strong` at 3:1 on
|
||||
`background`, `surface`, `sidebar`, `hover` and `selected`, and `primary_foreground` on `primary` at 4.5:1. A new
|
||||
`background`, `surface`, `sidebar`, `hover`, `selected`, `ocean_depth` and the diff rows `diff_add` and `diff_remove`, `primary_foreground` on `primary` and on `accent` at 4.5:1, and dark
|
||||
`foreground` on the ombre's deep stop at 4.5:1 (its rising stop is never a text ground). It also rejects
|
||||
glow and elevation alphas outside 0–1 and any easing curve whose control points leave [0, 1], so nothing
|
||||
overshoots. A new
|
||||
colour, such as a named blue, enters the tokens only once it passes.
|
||||
|
||||
## Design studies and implementation handoffs
|
||||
|
||||
[Claude desktop → Codewhale, October 9, 2026](studies/claude-parity-20261009/README.md)
|
||||
contains the editable Figma screen map, reusable component inventory, visual
|
||||
proofs and frame-specific implementation packets. Its observed reference
|
||||
behavior and proposed states are recorded separately. The study uses the
|
||||
existing tokens; it does not introduce another palette or change shipped defaults.
|
||||
Vendored
+68
-13
@@ -11,14 +11,19 @@ ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# WCAG 2.x contrast every palette must hold, in both modes, before anything is
|
||||
# generated: text roles at 4.5:1 and control edges at 3:1 on each ground a
|
||||
# role can sit on, including a hovered or selected row.
|
||||
GROUNDS = ["background", "surface", "sidebar", "hover", "selected"]
|
||||
TEXT = ["foreground", "muted_foreground", "primary", "live", "attention", "danger"]
|
||||
# role can sit on, including a hovered or selected row and the deepest point
|
||||
# of the ocean field behind the working area, and the added and removed rows
|
||||
# of a diff, which carry code and their +/− marks.
|
||||
GROUNDS = ["background", "surface", "sidebar", "hover", "selected", "ocean_depth", "diff_add", "diff_remove"]
|
||||
TEXT = ["foreground", "muted_foreground", "primary", "accent", "live", "attention", "danger"]
|
||||
# Secondary text carries labels, metadata and status words people must read at
|
||||
# small sizes, so it holds 5.5:1 on every ground, above the AA floor.
|
||||
TEXT_FLOORS = {"muted_foreground": 5.5}
|
||||
EDGES = ["border_strong"]
|
||||
FILLED = [("primary_foreground", "primary")]
|
||||
FILLED = [("primary_foreground", "primary"), ("primary_foreground", "accent")]
|
||||
# The ombre's deep stop can carry body text on the dark ground; its rising
|
||||
# stop is a brand colour only and never a text ground.
|
||||
OMBRE_TEXT = [("foreground", "deep", 4.5)]
|
||||
|
||||
def luminance(hex6):
|
||||
channels = [int(hex6[i:i + 2], 16) / 255 for i in (0, 2, 4)]
|
||||
@@ -29,7 +34,7 @@ def contrast(a, b):
|
||||
hi, lo = sorted([luminance(a), luminance(b)], reverse=True)
|
||||
return (hi + 0.05) / (lo + 0.05)
|
||||
|
||||
def contrast_violations(colors):
|
||||
def contrast_violations(colors, ombre):
|
||||
pairs = [(fg, bg, TEXT_FLOORS.get(fg, 4.5)) for fg in TEXT for bg in GROUNDS]
|
||||
pairs += [(edge, bg, 3.0) for edge in EDGES for bg in GROUNDS]
|
||||
pairs += [(fg, bg, 4.5) for fg, bg in FILLED]
|
||||
@@ -39,22 +44,35 @@ def contrast_violations(colors):
|
||||
ratio = contrast(palette[fg], palette[bg])
|
||||
if ratio < floor:
|
||||
found.append(f"{mode} {fg} on {bg} is {ratio:.2f}:1, needs {floor}:1")
|
||||
for fg, stop, floor in OMBRE_TEXT:
|
||||
ratio = contrast(colors["dark"][fg], ombre[stop])
|
||||
if ratio < floor:
|
||||
found.append(f"dark {fg} on ombre {stop} is {ratio:.2f}:1, needs {floor}:1")
|
||||
return found
|
||||
|
||||
def render():
|
||||
source = (ROOT / "tokens.json").read_bytes()
|
||||
data = json.loads(source)
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
value = data[name]
|
||||
unit = {name: data[name] for name in ["selection_opacity", "primary_hover_opacity"]}
|
||||
unit |= {f"glow.{name}": v for name, v in data["glow"].items() if name.endswith("_alpha")}
|
||||
unit |= {f"elevation.{name}": v for name, v in data["elevation"].items() if "_alpha" in name}
|
||||
for name, value in unit.items():
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value <= 1:
|
||||
raise ValueError(f"{name} must be a number between 0 and 1")
|
||||
# No overshoot or undershoot: a curve's x and y control points stay in [0, 1].
|
||||
for name, curve in data["easing"].items():
|
||||
if len(curve) != 4 or not all(isinstance(v, (int, float)) and 0 <= v <= 1 for v in curve):
|
||||
raise ValueError(f"easing.{name} must be four control points between 0 and 1")
|
||||
stream = data["stream"]
|
||||
if not all(isinstance(v, int) and v > 0 for v in stream.values()) or stream["reveal_first_ms"] > stream["reveal_lag_ms"]:
|
||||
raise ValueError("stream values must be positive integers, with reveal_first_ms no longer than reveal_lag_ms")
|
||||
digest = hashlib.sha256(source).hexdigest()
|
||||
header = f"Generated from Codewhale GPUI design {data['version']}; sha256 {digest}. Do not edit."
|
||||
keys = list(data["colors"]["dark"])
|
||||
assert set(keys) == set(data["colors"]["light"])
|
||||
for palette in data["colors"].values():
|
||||
for palette in [*data["colors"].values(), data["ombre"]]:
|
||||
assert all(len(v) == 6 and all(c in "0123456789abcdef" for c in v) for v in palette.values())
|
||||
violations = contrast_violations(data["colors"])
|
||||
violations = contrast_violations(data["colors"], data["ombre"])
|
||||
if violations:
|
||||
raise ValueError("Contrast below WCAG AA:\n " + "\n ".join(violations))
|
||||
rust = [f"// {header}", "#![allow(dead_code)]", "", f'pub const VERSION: &str = "{data["version"]}";',
|
||||
@@ -70,29 +88,61 @@ def render():
|
||||
"pub const FONT_FALLBACKS: &[&str] = &["]
|
||||
rust += [f" {json.dumps(v)}," for v in data["typography"]["fallbacks"]]
|
||||
rust += ["];"]
|
||||
for section in ["spacing", "radius", "focus", "icons", "motion"]:
|
||||
for section in ["spacing", "radius", "focus", "icons", "motion", "stream", "glow", "elevation"]:
|
||||
for name, value in data[section].items():
|
||||
rust += [f"pub const {section.upper()}_{name.upper()}: f32 = {float(value)};"]
|
||||
for name, curve in data["easing"].items():
|
||||
rust += [f"pub const EASING_{name.upper()}: [f32; 4] = [{', '.join(str(float(v)) for v in curve)}];"]
|
||||
for name, value in data["ombre"].items():
|
||||
rust += [f"pub const OMBRE_{name.upper()}: u32 = 0x{value};"]
|
||||
for name in ["selection_opacity", "primary_hover_opacity"]:
|
||||
rust += [f"pub const {name.upper()}: f32 = {float(data[name])};"]
|
||||
rust += [f'pub const MONO_PX: f32 = {float(data["typography"]["mono_px"])};']
|
||||
roles = data["type_roles"]
|
||||
for name, role in roles.items():
|
||||
if not (isinstance(role["size_px"], (int, float)) and role["line_px"] >= role["size_px"] and role["weight"] in range(100, 1000)):
|
||||
raise ValueError(f"type_roles.{name} needs size_px, a line_px no smaller than it, and a weight from 100 to 900")
|
||||
for key in ["size_px", "line_px", "weight"]:
|
||||
rust += [f"pub const TYPE_ROLE_{name.upper()}_{key.upper()}: f32 = {float(role[key])};"]
|
||||
for section in ["size", "breakpoints"]:
|
||||
for name, value in data[section].items():
|
||||
rust += [f"pub const {section.upper()}_{name.upper()}: f32 = {float(value)};"]
|
||||
if data["size"]["touch_target_px"] < 44:
|
||||
raise ValueError("size.touch_target_px must be at least 44")
|
||||
for name in ["body_px", "caption_px", "prose_px", "heading_px", "title_px"]:
|
||||
if name in data["typography"]:
|
||||
rust += [f'pub const TYPE_{name.upper()}: f32 = {float(data["typography"][name])};']
|
||||
# Existing web names remain aliases, so every current component migrates together.
|
||||
aliases = {"outside":"sidebar","surface":"background","panel":"surface","text":"foreground",
|
||||
"muted":"muted_foreground","line":"border","accent":"primary","button":"primary",
|
||||
"muted":"muted_foreground","line":"border","accent":"accent","button":"primary",
|
||||
"button-text":"primary_foreground","attention":"attention","hover":"hover","selected":"selected",
|
||||
"live":"live","danger":"danger","border-strong":"border_strong"}
|
||||
"live":"live","danger":"danger","border-strong":"border_strong","ocean-depth":"ocean_depth",
|
||||
"diff-add":"diff_add","diff-remove":"diff_remove"}
|
||||
css = [f"/* {header} */"]
|
||||
for mode, palette in data["colors"].items():
|
||||
css += [":root {" if mode == "dark" else '[data-theme="light"] {']
|
||||
css += [f" --{alias}: #{palette[key]};" for alias, key in aliases.items()]
|
||||
css += [f' --selection: #{palette["primary"]}{round(data["selection_opacity"] * 255):02x};',
|
||||
elevation = data["elevation"]
|
||||
css += [f' --selection: #{palette["accent"]}{round(data["selection_opacity"] * 255):02x};',
|
||||
f' --elevation-popover: 0 {elevation["popover_y_px"]}px {elevation["popover_blur_px"]}px rgb(0 0 0 / {elevation[f"popover_alpha_{mode}"]});',
|
||||
f' --elevation-dialog: 0 {elevation["dialog_y_px"]}px {elevation["dialog_blur_px"]}px rgb(0 0 0 / {elevation[f"dialog_alpha_{mode}"]});',
|
||||
f" color-scheme: {mode};", "}"]
|
||||
css += [":root {"]
|
||||
for section in ["spacing", "radius", "focus"]:
|
||||
css += [f" --{section}-{name}: {value}px;" for name, value in data[section].items()]
|
||||
motion = data["motion"]
|
||||
css += [f" --motion-duration-{name[9:-3]}: {value}ms;" for name, value in motion.items()
|
||||
if name.startswith("duration_")]
|
||||
css += [f" --motion-{name[:-3].replace('_', '-')}: {value}ms;" for name, value in motion.items()
|
||||
if name.startswith("loop_") and name.endswith("_ms")]
|
||||
css += [f' --motion-arrive-rise: {motion["arrive_rise_px"]}px;']
|
||||
css += [f" --motion-ease-{name}: cubic-bezier({', '.join(str(v) for v in curve)});"
|
||||
for name, curve in data["easing"].items()]
|
||||
glow = data["glow"]
|
||||
css_alias = {"primary": "accent", "live": "live", "attention": "attention"}
|
||||
css += [f' --glow-{name[:-6]}: 0 0 {glow["blur_px"]}px color-mix(in srgb, var(--{css_alias[name[:-6]]}) {round(value * 100)}%, transparent);'
|
||||
for name, value in glow.items() if name.endswith("_alpha")]
|
||||
css += [f" --ombre-{name}: #{value};" for name, value in data["ombre"].items()]
|
||||
css += [f' --icon-stroke: {data["icons"]["stroke"]};',
|
||||
f' --font-family: {json.dumps(data["typography"]["family"], ensure_ascii=False)};',
|
||||
f' --font-fallbacks: {", ".join(json.dumps(v, ensure_ascii=False) for v in data["typography"]["fallbacks"])};',
|
||||
@@ -101,6 +151,11 @@ def render():
|
||||
for name in ["caption_px", "prose_px", "heading_px", "title_px"]:
|
||||
if name in data["typography"]:
|
||||
css += [f' --font-{name[:-3]}-size: {data["typography"][name]}px;']
|
||||
for name, role in data["type_roles"].items():
|
||||
css += [f' --type-{name}-size: {role["size_px"]}px;', f' --type-{name}-line: {role["line_px"]}px;',
|
||||
f' --type-{name}-weight: {role["weight"]};']
|
||||
css += [f' --size-{name[:-3].replace("_", "-")}: {value}px;' for name, value in data["size"].items()]
|
||||
css += [f' --breakpoint-{name[:-3]}: {value}px;' for name, value in data["breakpoints"].items()]
|
||||
css += ["}"]
|
||||
return {"tokens.rs":"\n".join(rust) + "\n", "tokens.css":"\n".join(css) + "\n"}
|
||||
|
||||
|
||||
Vendored
+71
-11
@@ -1,21 +1,26 @@
|
||||
/* Generated from Codewhale GPUI design 1.1.2; sha256 433b18a41c526da0b5a1bbaa461c37feac987c7688fbe7de660acfbbdafa9bd5. Do not edit. */
|
||||
/* Generated from Codewhale GPUI design 2.3.0; sha256 9e74fb8f8fbfebb6b90a9740b324c56405277886e61273e2a0cb682797a8a6f9. Do not edit. */
|
||||
:root {
|
||||
--outside: #191a1c;
|
||||
--surface: #202123;
|
||||
--panel: #2a2b2e;
|
||||
--text: #efeeeb;
|
||||
--muted: #c2c1bd;
|
||||
--muted: #b8b8b4;
|
||||
--line: #3b3c3f;
|
||||
--accent: #90b9ff;
|
||||
--button: #90b9ff;
|
||||
--button-text: #15243e;
|
||||
--accent: #a9c0de;
|
||||
--button: #e6e4df;
|
||||
--button-text: #202123;
|
||||
--attention: #e8b077;
|
||||
--hover: #303134;
|
||||
--selected: #37393d;
|
||||
--live: #9ec7b2;
|
||||
--danger: #e39a90;
|
||||
--border-strong: #828386;
|
||||
--selection: #90b9ff47;
|
||||
--ocean-depth: #202123;
|
||||
--diff-add: #22392f;
|
||||
--diff-remove: #45302f;
|
||||
--selection: #a9c0de47;
|
||||
--elevation-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--elevation-dialog: 0 12px 32px rgb(0 0 0 / 0.5);
|
||||
color-scheme: dark;
|
||||
}
|
||||
[data-theme="light"] {
|
||||
@@ -23,18 +28,23 @@
|
||||
--surface: #faf8f5;
|
||||
--panel: #ffffff;
|
||||
--text: #28292b;
|
||||
--muted: #535451;
|
||||
--muted: #4f504d;
|
||||
--line: #d9d5cf;
|
||||
--accent: #245bc7;
|
||||
--button: #245bc7;
|
||||
--button-text: #fbf5ee;
|
||||
--accent: #425c82;
|
||||
--button: #2e2f31;
|
||||
--button-text: #faf8f5;
|
||||
--attention: #86520d;
|
||||
--hover: #e8e5e0;
|
||||
--selected: #dfdcd6;
|
||||
--live: #3a6352;
|
||||
--danger: #9e3f36;
|
||||
--border-strong: #807c76;
|
||||
--selection: #245bc747;
|
||||
--ocean-depth: #faf8f5;
|
||||
--diff-add: #d2eadd;
|
||||
--diff-remove: #f8e1dd;
|
||||
--selection: #425c8247;
|
||||
--elevation-popover: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
--elevation-dialog: 0 12px 32px rgb(0 0 0 / 0.2);
|
||||
color-scheme: light;
|
||||
}
|
||||
:root {
|
||||
@@ -49,6 +59,22 @@
|
||||
--radius-sheet: 14px;
|
||||
--focus-width: 2px;
|
||||
--focus-offset: 3px;
|
||||
--motion-duration-state: 120ms;
|
||||
--motion-duration-arrive: 180ms;
|
||||
--motion-duration-exit: 120ms;
|
||||
--motion-duration-panel: 340ms;
|
||||
--motion-duration-linger: 1200ms;
|
||||
--motion-loop-beat: 1200ms;
|
||||
--motion-loop-breathe: 2400ms;
|
||||
--motion-arrive-rise: 4px;
|
||||
--motion-ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--motion-ease-panel: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
--glow-primary: 0 0 6px color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
--glow-live: 0 0 6px color-mix(in srgb, var(--live) 35%, transparent);
|
||||
--glow-attention: 0 0 6px color-mix(in srgb, var(--attention) 40%, transparent);
|
||||
--ombre-rise: #1e8fd8;
|
||||
--ombre-deep: #0b48bb;
|
||||
--icon-stroke: 1.7;
|
||||
--font-family: "Shannon Sans";
|
||||
--font-fallbacks: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC";
|
||||
@@ -58,4 +84,38 @@
|
||||
--font-prose-size: 17px;
|
||||
--font-heading-size: 17px;
|
||||
--font-title-size: 22px;
|
||||
--type-caption-size: 12px;
|
||||
--type-caption-line: 16px;
|
||||
--type-caption-weight: 400;
|
||||
--type-label-size: 13px;
|
||||
--type-label-line: 18px;
|
||||
--type-label-weight: 400;
|
||||
--type-body-size: 14px;
|
||||
--type-body-line: 22px;
|
||||
--type-body-weight: 400;
|
||||
--type-prose-size: 16px;
|
||||
--type-prose-line: 26px;
|
||||
--type-prose-weight: 400;
|
||||
--type-heading-size: 17px;
|
||||
--type-heading-line: 24px;
|
||||
--type-heading-weight: 550;
|
||||
--type-title-size: 22px;
|
||||
--type-title-line: 28px;
|
||||
--type-title-weight: 550;
|
||||
--type-display-size: 28px;
|
||||
--type-display-line: 34px;
|
||||
--type-display-weight: 550;
|
||||
--type-code-size: 13px;
|
||||
--type-code-line: 20px;
|
||||
--type-code-weight: 400;
|
||||
--size-control: 28px;
|
||||
--size-row: 30px;
|
||||
--size-touch-target: 44px;
|
||||
--size-icon: 16px;
|
||||
--size-icon-large: 20px;
|
||||
--size-sidebar: 248px;
|
||||
--size-panel: 360px;
|
||||
--breakpoint-compact: 720px;
|
||||
--breakpoint-medium: 1024px;
|
||||
--breakpoint-wide: 1440px;
|
||||
}
|
||||
Vendored
+128
-11
@@ -1,37 +1,45 @@
|
||||
{
|
||||
"version": "1.1.2",
|
||||
"version": "2.3.0",
|
||||
"colors": {
|
||||
"dark": {
|
||||
"background": "202123",
|
||||
"foreground": "efeeeb",
|
||||
"surface": "2a2b2e",
|
||||
"muted_foreground": "c2c1bd",
|
||||
"muted_foreground": "b8b8b4",
|
||||
"border": "3b3c3f",
|
||||
"sidebar": "191a1c",
|
||||
"primary": "90b9ff",
|
||||
"primary_foreground": "15243e",
|
||||
"primary": "e6e4df",
|
||||
"primary_foreground": "202123",
|
||||
"accent": "a9c0de",
|
||||
"hover": "303134",
|
||||
"selected": "37393d",
|
||||
"attention": "e8b077",
|
||||
"live": "9ec7b2",
|
||||
"danger": "e39a90",
|
||||
"border_strong": "828386"
|
||||
"border_strong": "828386",
|
||||
"ocean_depth": "202123",
|
||||
"diff_add": "22392f",
|
||||
"diff_remove": "45302f"
|
||||
},
|
||||
"light": {
|
||||
"background": "faf8f5",
|
||||
"foreground": "28292b",
|
||||
"surface": "ffffff",
|
||||
"muted_foreground": "535451",
|
||||
"muted_foreground": "4f504d",
|
||||
"border": "d9d5cf",
|
||||
"sidebar": "f0ede8",
|
||||
"primary": "245bc7",
|
||||
"primary_foreground": "fbf5ee",
|
||||
"primary": "2e2f31",
|
||||
"primary_foreground": "faf8f5",
|
||||
"accent": "425c82",
|
||||
"hover": "e8e5e0",
|
||||
"selected": "dfdcd6",
|
||||
"attention": "86520d",
|
||||
"live": "3a6352",
|
||||
"danger": "9e3f36",
|
||||
"border_strong": "807c76"
|
||||
"border_strong": "807c76",
|
||||
"ocean_depth": "faf8f5",
|
||||
"diff_add": "d2eadd",
|
||||
"diff_remove": "f8e1dd"
|
||||
}
|
||||
},
|
||||
"typography": {
|
||||
@@ -81,8 +89,117 @@
|
||||
"reduced_poll_ms": 500,
|
||||
"duration_state_ms": 120,
|
||||
"duration_arrive_ms": 180,
|
||||
"duration_panel_ms": 340
|
||||
"duration_exit_ms": 120,
|
||||
"duration_panel_ms": 340,
|
||||
"duration_linger_ms": 1200,
|
||||
"arrive_rise_px": 4,
|
||||
"loop_beat_ms": 1200,
|
||||
"loop_breathe_ms": 2400,
|
||||
"loop_fps_cap": 15
|
||||
},
|
||||
"easing": {
|
||||
"out": [
|
||||
0,
|
||||
0,
|
||||
0.2,
|
||||
1
|
||||
],
|
||||
"in": [
|
||||
0.4,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"panel": [
|
||||
0.2,
|
||||
0.1,
|
||||
0.16,
|
||||
1
|
||||
]
|
||||
},
|
||||
"stream": {
|
||||
"reveal_first_ms": 100,
|
||||
"reveal_lag_ms": 500,
|
||||
"rate_window_ms": 1000,
|
||||
"instant_graphemes": 8,
|
||||
"max_pending_bytes": 16384
|
||||
},
|
||||
"glow": {
|
||||
"blur_px": 6,
|
||||
"primary_alpha": 0.12,
|
||||
"live_alpha": 0.35,
|
||||
"attention_alpha": 0.4
|
||||
},
|
||||
"ombre": {
|
||||
"rise": "1e8fd8",
|
||||
"deep": "0b48bb"
|
||||
},
|
||||
"elevation": {
|
||||
"popover_y_px": 4,
|
||||
"popover_blur_px": 8,
|
||||
"popover_alpha_dark": 0.4,
|
||||
"popover_alpha_light": 0.16,
|
||||
"dialog_y_px": 12,
|
||||
"dialog_blur_px": 32,
|
||||
"dialog_alpha_dark": 0.5,
|
||||
"dialog_alpha_light": 0.2
|
||||
},
|
||||
"selection_opacity": 0.28,
|
||||
"primary_hover_opacity": 0.9
|
||||
"primary_hover_opacity": 0.9,
|
||||
"type_roles": {
|
||||
"caption": {
|
||||
"size_px": 12,
|
||||
"line_px": 16,
|
||||
"weight": 400
|
||||
},
|
||||
"label": {
|
||||
"size_px": 13,
|
||||
"line_px": 18,
|
||||
"weight": 400
|
||||
},
|
||||
"body": {
|
||||
"size_px": 14,
|
||||
"line_px": 22,
|
||||
"weight": 400
|
||||
},
|
||||
"prose": {
|
||||
"size_px": 16,
|
||||
"line_px": 26,
|
||||
"weight": 400
|
||||
},
|
||||
"heading": {
|
||||
"size_px": 17,
|
||||
"line_px": 24,
|
||||
"weight": 550
|
||||
},
|
||||
"title": {
|
||||
"size_px": 22,
|
||||
"line_px": 28,
|
||||
"weight": 550
|
||||
},
|
||||
"display": {
|
||||
"size_px": 28,
|
||||
"line_px": 34,
|
||||
"weight": 550
|
||||
},
|
||||
"code": {
|
||||
"size_px": 13,
|
||||
"line_px": 20,
|
||||
"weight": 400
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"control_px": 28,
|
||||
"row_px": 30,
|
||||
"touch_target_px": 44,
|
||||
"icon_px": 16,
|
||||
"icon_large_px": 20,
|
||||
"sidebar_px": 248,
|
||||
"panel_px": 360
|
||||
},
|
||||
"breakpoints": {
|
||||
"compact_px": 720,
|
||||
"medium_px": 1024,
|
||||
"wide_px": 1440
|
||||
}
|
||||
}
|
||||
Vendored
+82
-8
@@ -1,7 +1,7 @@
|
||||
// Generated from Codewhale GPUI design 1.1.2; sha256 433b18a41c526da0b5a1bbaa461c37feac987c7688fbe7de660acfbbdafa9bd5. Do not edit.
|
||||
// Generated from Codewhale GPUI design 2.3.0; sha256 9e74fb8f8fbfebb6b90a9740b324c56405277886e61273e2a0cb682797a8a6f9. Do not edit.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub const VERSION: &str = "1.1.2";
|
||||
pub const VERSION: &str = "2.3.0";
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Colors {
|
||||
pub background: u32,
|
||||
@@ -12,44 +12,56 @@ pub struct Colors {
|
||||
pub sidebar: u32,
|
||||
pub primary: u32,
|
||||
pub primary_foreground: u32,
|
||||
pub accent: u32,
|
||||
pub hover: u32,
|
||||
pub selected: u32,
|
||||
pub attention: u32,
|
||||
pub live: u32,
|
||||
pub danger: u32,
|
||||
pub border_strong: u32,
|
||||
pub ocean_depth: u32,
|
||||
pub diff_add: u32,
|
||||
pub diff_remove: u32,
|
||||
}
|
||||
pub const DARK: Colors = Colors {
|
||||
background: 0x202123,
|
||||
foreground: 0xefeeeb,
|
||||
surface: 0x2a2b2e,
|
||||
muted_foreground: 0xc2c1bd,
|
||||
muted_foreground: 0xb8b8b4,
|
||||
border: 0x3b3c3f,
|
||||
sidebar: 0x191a1c,
|
||||
primary: 0x90b9ff,
|
||||
primary_foreground: 0x15243e,
|
||||
primary: 0xe6e4df,
|
||||
primary_foreground: 0x202123,
|
||||
accent: 0xa9c0de,
|
||||
hover: 0x303134,
|
||||
selected: 0x37393d,
|
||||
attention: 0xe8b077,
|
||||
live: 0x9ec7b2,
|
||||
danger: 0xe39a90,
|
||||
border_strong: 0x828386,
|
||||
ocean_depth: 0x202123,
|
||||
diff_add: 0x22392f,
|
||||
diff_remove: 0x45302f,
|
||||
};
|
||||
pub const LIGHT: Colors = Colors {
|
||||
background: 0xfaf8f5,
|
||||
foreground: 0x28292b,
|
||||
surface: 0xffffff,
|
||||
muted_foreground: 0x535451,
|
||||
muted_foreground: 0x4f504d,
|
||||
border: 0xd9d5cf,
|
||||
sidebar: 0xf0ede8,
|
||||
primary: 0x245bc7,
|
||||
primary_foreground: 0xfbf5ee,
|
||||
primary: 0x2e2f31,
|
||||
primary_foreground: 0xfaf8f5,
|
||||
accent: 0x425c82,
|
||||
hover: 0xe8e5e0,
|
||||
selected: 0xdfdcd6,
|
||||
attention: 0x86520d,
|
||||
live: 0x3a6352,
|
||||
danger: 0x9e3f36,
|
||||
border_strong: 0x807c76,
|
||||
ocean_depth: 0xfaf8f5,
|
||||
diff_add: 0xd2eadd,
|
||||
diff_remove: 0xf8e1dd,
|
||||
};
|
||||
pub fn colors(dark: bool) -> Colors {
|
||||
if dark { DARK } else { LIGHT }
|
||||
@@ -84,10 +96,72 @@ pub const MOTION_PET_POLL_MS: f32 = 30.0;
|
||||
pub const MOTION_REDUCED_POLL_MS: f32 = 500.0;
|
||||
pub const MOTION_DURATION_STATE_MS: f32 = 120.0;
|
||||
pub const MOTION_DURATION_ARRIVE_MS: f32 = 180.0;
|
||||
pub const MOTION_DURATION_EXIT_MS: f32 = 120.0;
|
||||
pub const MOTION_DURATION_PANEL_MS: f32 = 340.0;
|
||||
pub const MOTION_DURATION_LINGER_MS: f32 = 1200.0;
|
||||
pub const MOTION_ARRIVE_RISE_PX: f32 = 4.0;
|
||||
pub const MOTION_LOOP_BEAT_MS: f32 = 1200.0;
|
||||
pub const MOTION_LOOP_BREATHE_MS: f32 = 2400.0;
|
||||
pub const MOTION_LOOP_FPS_CAP: f32 = 15.0;
|
||||
pub const STREAM_REVEAL_FIRST_MS: f32 = 100.0;
|
||||
pub const STREAM_REVEAL_LAG_MS: f32 = 500.0;
|
||||
pub const STREAM_RATE_WINDOW_MS: f32 = 1000.0;
|
||||
pub const STREAM_INSTANT_GRAPHEMES: f32 = 8.0;
|
||||
pub const STREAM_MAX_PENDING_BYTES: f32 = 16384.0;
|
||||
pub const GLOW_BLUR_PX: f32 = 6.0;
|
||||
pub const GLOW_PRIMARY_ALPHA: f32 = 0.12;
|
||||
pub const GLOW_LIVE_ALPHA: f32 = 0.35;
|
||||
pub const GLOW_ATTENTION_ALPHA: f32 = 0.4;
|
||||
pub const ELEVATION_POPOVER_Y_PX: f32 = 4.0;
|
||||
pub const ELEVATION_POPOVER_BLUR_PX: f32 = 8.0;
|
||||
pub const ELEVATION_POPOVER_ALPHA_DARK: f32 = 0.4;
|
||||
pub const ELEVATION_POPOVER_ALPHA_LIGHT: f32 = 0.16;
|
||||
pub const ELEVATION_DIALOG_Y_PX: f32 = 12.0;
|
||||
pub const ELEVATION_DIALOG_BLUR_PX: f32 = 32.0;
|
||||
pub const ELEVATION_DIALOG_ALPHA_DARK: f32 = 0.5;
|
||||
pub const ELEVATION_DIALOG_ALPHA_LIGHT: f32 = 0.2;
|
||||
pub const EASING_OUT: [f32; 4] = [0.0, 0.0, 0.2, 1.0];
|
||||
pub const EASING_IN: [f32; 4] = [0.4, 0.0, 1.0, 1.0];
|
||||
pub const EASING_PANEL: [f32; 4] = [0.2, 0.1, 0.16, 1.0];
|
||||
pub const OMBRE_RISE: u32 = 0x1e8fd8;
|
||||
pub const OMBRE_DEEP: u32 = 0x0b48bb;
|
||||
pub const SELECTION_OPACITY: f32 = 0.28;
|
||||
pub const PRIMARY_HOVER_OPACITY: f32 = 0.9;
|
||||
pub const MONO_PX: f32 = 13.0;
|
||||
pub const TYPE_ROLE_CAPTION_SIZE_PX: f32 = 12.0;
|
||||
pub const TYPE_ROLE_CAPTION_LINE_PX: f32 = 16.0;
|
||||
pub const TYPE_ROLE_CAPTION_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_LABEL_SIZE_PX: f32 = 13.0;
|
||||
pub const TYPE_ROLE_LABEL_LINE_PX: f32 = 18.0;
|
||||
pub const TYPE_ROLE_LABEL_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_BODY_SIZE_PX: f32 = 14.0;
|
||||
pub const TYPE_ROLE_BODY_LINE_PX: f32 = 22.0;
|
||||
pub const TYPE_ROLE_BODY_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_PROSE_SIZE_PX: f32 = 16.0;
|
||||
pub const TYPE_ROLE_PROSE_LINE_PX: f32 = 26.0;
|
||||
pub const TYPE_ROLE_PROSE_WEIGHT: f32 = 400.0;
|
||||
pub const TYPE_ROLE_HEADING_SIZE_PX: f32 = 17.0;
|
||||
pub const TYPE_ROLE_HEADING_LINE_PX: f32 = 24.0;
|
||||
pub const TYPE_ROLE_HEADING_WEIGHT: f32 = 550.0;
|
||||
pub const TYPE_ROLE_TITLE_SIZE_PX: f32 = 22.0;
|
||||
pub const TYPE_ROLE_TITLE_LINE_PX: f32 = 28.0;
|
||||
pub const TYPE_ROLE_TITLE_WEIGHT: f32 = 550.0;
|
||||
pub const TYPE_ROLE_DISPLAY_SIZE_PX: f32 = 28.0;
|
||||
pub const TYPE_ROLE_DISPLAY_LINE_PX: f32 = 34.0;
|
||||
pub const TYPE_ROLE_DISPLAY_WEIGHT: f32 = 550.0;
|
||||
pub const TYPE_ROLE_CODE_SIZE_PX: f32 = 13.0;
|
||||
pub const TYPE_ROLE_CODE_LINE_PX: f32 = 20.0;
|
||||
pub const TYPE_ROLE_CODE_WEIGHT: f32 = 400.0;
|
||||
pub const SIZE_CONTROL_PX: f32 = 28.0;
|
||||
pub const SIZE_ROW_PX: f32 = 30.0;
|
||||
pub const SIZE_TOUCH_TARGET_PX: f32 = 44.0;
|
||||
pub const SIZE_ICON_PX: f32 = 16.0;
|
||||
pub const SIZE_ICON_LARGE_PX: f32 = 20.0;
|
||||
pub const SIZE_SIDEBAR_PX: f32 = 248.0;
|
||||
pub const SIZE_PANEL_PX: f32 = 360.0;
|
||||
pub const BREAKPOINTS_COMPACT_PX: f32 = 720.0;
|
||||
pub const BREAKPOINTS_MEDIUM_PX: f32 = 1024.0;
|
||||
pub const BREAKPOINTS_WIDE_PX: f32 = 1440.0;
|
||||
pub const TYPE_BODY_PX: f32 = 14.0;
|
||||
pub const TYPE_CAPTION_PX: f32 = 12.0;
|
||||
pub const TYPE_PROSE_PX: f32 = 17.0;
|
||||
|
||||
@@ -103,11 +103,9 @@ export default async function LocaleLayout({
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(siteJsonLd) }}
|
||||
/>
|
||||
{/* Site-wide theme, applied before paint so there is no flash. With
|
||||
no pin, the stylesheet's `prefers-color-scheme` rules resolve the
|
||||
OS appearance (and track it live); a stored "light" or "dark"
|
||||
(`cw-theme`, see components/theme-toggle.tsx) pins the scheme.
|
||||
"system", a legacy "auto", or no choice leaves it to the OS. */}
|
||||
{/* Site-wide theme, applied before paint so there is no flash. The
|
||||
site is light unless a stored "dark" (`cw-theme`, see
|
||||
components/theme-toggle.tsx) pins the dark set. */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
|
||||
@@ -131,11 +131,9 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
|
||||
? fill(d.latestRelease, { tag: publishedRelease.tag })
|
||||
: d.releaseUnavailable}
|
||||
</Status>
|
||||
{!sourceIsPublished && <span>{`${d.sourceCandidate} v${sourceVersion}`}</span>}
|
||||
<span>{`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`}</span>
|
||||
<span>{facts.license ?? "MIT"}</span>
|
||||
<span>{fill(d.proofContributors, { count: facts.contributorCount })}</span>
|
||||
{facts.toolCount ? <span>{fill(d.proofTools, { count: facts.toolCount })}</span> : null}
|
||||
{facts.providers.length > 0 && <span>{fill(d.proofProviders, { count: facts.providers.length })}</span>}
|
||||
</div>
|
||||
|
||||
{/* PAPER — models, connections, and the getting-started path. */}
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
box-shadow: 0 1px 0 var(--line), 0 32px 64px -32px rgb(10 30 51 / 0.5);
|
||||
}
|
||||
|
||||
/* Release, licence and counts read from facts, never typed into copy. */
|
||||
/* Release, licence and the contributor count read from facts, never typed into copy. */
|
||||
.home-proof {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
+149
-185
@@ -2,92 +2,47 @@
|
||||
/* Tokens and roles — the whale-road */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/*
|
||||
* Direction: codewhale-design/DIRECTION.md. Colour authority: the Moda brand
|
||||
* kit "Codewhale" (--kit-* below, mapping in docs/BRAND_COLOR_MAPPING.md).
|
||||
* Geometry, radii and focus metrics still come from the generated
|
||||
* `--gpui-*` tokens in ../tokens.css. This file names the roles every page
|
||||
* paints with.
|
||||
* Direction: codewhale-design/DIRECTION.md. Authority: codewhale-design
|
||||
* SYSTEM.md and tokens.json, vendored at ../../vendor/codewhale-design and
|
||||
* exported to ../tokens.css (`--gpui-*`) by scripts/export-design-tokens.py.
|
||||
* This file only names the roles every page paints with; every value is a
|
||||
* generated token, so the site, the desktop app and the web app share one
|
||||
* palette, type scale, radius set and motion.
|
||||
*
|
||||
* The ocean. Dark is the preferred look: deep navy grounds that rise into
|
||||
* the logo's gradient (#1E8FD8 -> #0B48BB). Light is warm paper above one
|
||||
* calm horizon. Below the horizon (the footer, the home sea) is always
|
||||
* the deep water, in both appearances (`.stage`).
|
||||
* Grounds are pearl (light) and charcoal (dark), the desktop shell's own.
|
||||
* Filled actions are neutral ink (`primary`); one quiet slate (`accent`)
|
||||
* marks links, focus and selection. The logo ombre lives on the whale.
|
||||
*
|
||||
* Depth is elevation. --panel is raised (nearer the surface, lighter);
|
||||
* --bg is the floor; --surface is recessed (code, rails, fields).
|
||||
*
|
||||
* Light means something. Focus, live work and "needs you" carry a small
|
||||
* point of light (--glow). Nothing else glows.
|
||||
*
|
||||
* State is a mark and a word: --live (working, done, ready), --attention
|
||||
* (needs you), --danger (failed), --muted (idle). Never colour alone.
|
||||
*
|
||||
* Text, accent and state inks are kit colours in both appearances, and the
|
||||
* navy grounds are kit colours too; they are measured against every ink
|
||||
* here (gpui-role-tokens.test.ts checks WCAG AA on every ground).
|
||||
*
|
||||
* NAMING TRAP: tokens.json `surface` is the RAISED colour (web --panel);
|
||||
* tokens.json `sidebar` is the RECESSED one (web --surface).
|
||||
*/
|
||||
|
||||
/* ---------- scheme-independent tokens ---------- */
|
||||
:root {
|
||||
/* The ocean: the navy grounds and the logo gradient. */
|
||||
--kit-whale-blue: #0b48bb;
|
||||
--kit-current-blue: #1e8fd8;
|
||||
--kit-mark-navy: #142352;
|
||||
--kit-abyss: #0f1e2d;
|
||||
--kit-deep-navy: #142638;
|
||||
--kit-ocean-depth: #173b50;
|
||||
--kit-night-surface: #20374e;
|
||||
--kit-night-border: #31495e;
|
||||
--kit-foam: #e6eef7;
|
||||
--kit-mist: #bbc9d7;
|
||||
--kit-bioluminescence: #8fbdff;
|
||||
--kit-warm-paper: #faf9f5;
|
||||
--kit-white: #ffffff;
|
||||
--kit-seaglass: #edf1ef;
|
||||
--kit-shallows: #dcefeb;
|
||||
--kit-tideline: #d2dedd;
|
||||
--kit-ink: #263844;
|
||||
--kit-slate: #495a63;
|
||||
--kit-signal-blue: #245bc7;
|
||||
--kit-kelp: #9ecbbd;
|
||||
--kit-kelp-deep: #396554;
|
||||
--kit-lantern: #eab982;
|
||||
--kit-amber: #84530d;
|
||||
--kit-coral: #eea59d;
|
||||
--kit-rust: #9e3f36;
|
||||
--ocean-abyss: var(--kit-abyss);
|
||||
--ocean-floor: var(--kit-deep-navy);
|
||||
--ocean-raised: var(--kit-ocean-depth);
|
||||
--ocean-hover: var(--kit-night-surface);
|
||||
--ocean-selected: var(--kit-night-border);
|
||||
--ocean-line: var(--kit-night-border);
|
||||
--ocean-line-strong: var(--kit-mist);
|
||||
--ocean-sky: var(--kit-mark-navy);
|
||||
--brand-light: var(--kit-current-blue);
|
||||
--brand-deep: var(--kit-whale-blue);
|
||||
--brand-mid: var(--kit-signal-blue);
|
||||
/* The primary fill: Signal Blue into Whale Blue; white text clears 6:1. */
|
||||
--brand-fill: linear-gradient(135deg, var(--brand-mid), var(--brand-deep));
|
||||
--brand-fill-hover: linear-gradient(135deg, var(--brand-deep), var(--kit-mark-navy));
|
||||
--on-brand: var(--kit-white);
|
||||
/* The sea, one ramp from the logo's deep blue at the surface to the
|
||||
floor. It settles within a few rows so text from 3rem down sits on
|
||||
>= 4.5:1 muted contrast; it is never a bright wall. */
|
||||
--sea-surface: var(--brand-deep);
|
||||
--sea-shallow: #0f3991;
|
||||
--sea-mid: var(--kit-mark-navy);
|
||||
--sea: linear-gradient(180deg, var(--sea-shallow) 0, var(--sea-mid) 5rem, var(--ocean-floor) 22rem);
|
||||
--sea-band: linear-gradient(180deg, var(--sea-shallow) 0, var(--sea-mid) 40%, var(--ocean-floor) 100%);
|
||||
/* The tide (the merch's Tide ombré): paper, light aqua, cobalt, then the
|
||||
deep. Headings start past the logo-deep stop (7rem), where text ink
|
||||
clears 4.5:1; muted ink only appears past the mid stop (11rem). */
|
||||
--tide-aqua: var(--kit-shallows);
|
||||
--tide: linear-gradient(180deg, var(--kit-warm-paper) 0, var(--tide-aqua) 2rem, var(--brand-light) 4.5rem, var(--brand-deep) 7rem, var(--sea-mid) 11rem, var(--ocean-floor) 26rem);
|
||||
/* The horizon: one line of light where the paper meets the sea. */
|
||||
--horizon-light: rgb(143 189 255 / 0.85);
|
||||
/* Below the horizon (the footer, the home install band) is the dark set
|
||||
in both appearances. */
|
||||
--ocean-abyss: var(--gpui-dark-sidebar);
|
||||
--ocean-floor: var(--gpui-dark-ocean-depth);
|
||||
--ocean-raised: var(--gpui-dark-panel);
|
||||
--ocean-hover: var(--gpui-dark-hover);
|
||||
--ocean-selected: var(--gpui-dark-list-active);
|
||||
--ocean-line: var(--gpui-dark-border);
|
||||
--ocean-line-strong: var(--gpui-dark-border-strong);
|
||||
--ocean-sky: var(--gpui-dark-ocean-depth);
|
||||
/* The logo ombre, for the whale and the mark only. */
|
||||
--brand-light: #1e8fd8;
|
||||
--brand-deep: #0b48bb;
|
||||
--ombre: var(--gpui-ombre);
|
||||
--sea: linear-gradient(var(--ocean-floor), var(--ocean-floor));
|
||||
--sea-band: var(--sea);
|
||||
/* The horizon: one quiet line of accent where the page meets the floor. */
|
||||
--horizon-light: rgb(var(--gpui-dark-accent-rgb) / 0.6);
|
||||
--horizon-glow: var(--horizon-light);
|
||||
|
||||
/* Type: Shannon Sans for every role, the system monospace stack for
|
||||
@@ -99,29 +54,32 @@
|
||||
"Liberation Mono", monospace;
|
||||
--font-cjk: var(--gpui-font-fallbacks), sans-serif;
|
||||
|
||||
/* Type scale. 12px is the floor. Display sizes are one per page. */
|
||||
--text-caption: 0.75rem; /* 12 meta, timestamps, labels */
|
||||
--text-mono: var(--gpui-mono-size); /* 13 code, commands */
|
||||
--text-ui: 0.875rem; /* 14 controls, rows, panel body */
|
||||
--text-body: 1rem; /* 16 short copy, form inputs (no iOS zoom) */
|
||||
--text-prose: 1.0625rem; /* 17 reading: docs, legal, answers */
|
||||
--leading-prose: 1.6;
|
||||
--text-heading: 1.0625rem; /* 17 600 group heading */
|
||||
--text-title: 1.375rem; /* 22 600 group and prose title */
|
||||
--text-section: clamp(1.75rem, 1.2rem + 1.7vw, 2.5rem); /* 28-40 editorial section title */
|
||||
--text-display: clamp(2rem, 1.3rem + 2.6vw, 3rem); /* inner-page title */
|
||||
--text-display-lg: clamp(2.5rem, 1.4rem + 4.4vw, 4.5rem); /* the home hero */
|
||||
/* Type roles from tokens.json. The editorial section and display sizes
|
||||
are the website's own layout: fluid steps above the token display. */
|
||||
--text-caption: var(--gpui-type-caption-size);
|
||||
--text-mono: var(--gpui-type-code-size);
|
||||
--text-label: var(--gpui-type-label-size);
|
||||
--text-ui: var(--gpui-type-body-size);
|
||||
--text-body: var(--gpui-type-prose-size);
|
||||
--text-prose: var(--gpui-type-prose-size);
|
||||
--leading-prose: var(--gpui-type-prose-line);
|
||||
--text-heading: var(--gpui-type-heading-size);
|
||||
--text-title: var(--gpui-type-title-size);
|
||||
--text-section: clamp(var(--gpui-type-display-size), 1.2rem + 1.7vw, 2.5rem);
|
||||
--text-display: clamp(2rem, 1.3rem + 2.6vw, 3rem);
|
||||
--text-display-lg: clamp(2.5rem, 1.4rem + 4.4vw, 4.5rem);
|
||||
--weight-heading: var(--gpui-type-heading-weight);
|
||||
--text-xs: var(--text-caption);
|
||||
--text-sm: var(--text-ui);
|
||||
--text-base: var(--text-body);
|
||||
|
||||
/* Space: a 4px program. */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.5rem;
|
||||
--space-6: 2rem;
|
||||
/* Space: the token steps (4, 8, 12, 16, 24, 32), then page rhythm. */
|
||||
--space-1: var(--gpui-spacing-compact);
|
||||
--space-2: var(--gpui-spacing-control);
|
||||
--space-3: var(--gpui-spacing-group);
|
||||
--space-4: var(--gpui-spacing-section);
|
||||
--space-5: var(--gpui-spacing-large);
|
||||
--space-6: var(--gpui-spacing-page);
|
||||
--space-7: 3rem;
|
||||
--space-8: 4rem;
|
||||
--section-gap: clamp(var(--space-7), 7vw, 5.5rem);
|
||||
@@ -133,21 +91,21 @@
|
||||
--hero-pad: clamp(3rem, 7vw, 5.5rem);
|
||||
--section-pad: clamp(2.75rem, 5vw, 4rem);
|
||||
|
||||
/* Radius: 6 controls, 10 surfaces, 14 sheets, pill for dots. */
|
||||
/* Radius: control, panel, sheet; pill for dots. */
|
||||
--radius-control: var(--gpui-radius-control);
|
||||
--radius-surface: var(--gpui-radius-panel);
|
||||
--radius-sheet: var(--gpui-radius-sheet);
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* Motion. Nothing bounces: every curve stays inside [0, 1]. */
|
||||
--dur-state: 120ms; /* colour, border, opacity on hover and press */
|
||||
--dur-arrive: 180ms; /* something new arrives, rise <= 4px */
|
||||
--dur-exit: 120ms; /* something closes or leaves */
|
||||
--dur-spring: 340ms; /* panes and sheets */
|
||||
--dur-spin: 1.2s; /* a working spinner: motion that shows work */
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-spring: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
/* Motion from tokens.json. Nothing bounces: every curve stays in [0, 1]. */
|
||||
--dur-state: var(--gpui-motion-duration-state);
|
||||
--dur-arrive: var(--gpui-motion-duration-arrive);
|
||||
--dur-exit: var(--gpui-motion-duration-exit);
|
||||
--dur-spring: var(--gpui-motion-duration-panel);
|
||||
--dur-spin: var(--gpui-motion-loop-beat);
|
||||
--ease-out: var(--gpui-easing-out);
|
||||
--ease-in: var(--gpui-easing-in);
|
||||
--ease-spring: var(--gpui-easing-panel);
|
||||
--ease-exit: var(--ease-in);
|
||||
}
|
||||
|
||||
@@ -163,104 +121,110 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- light — warm paper above the horizon ---------- */
|
||||
/* ---------- light — pearl ---------- */
|
||||
:root {
|
||||
--bg: var(--kit-warm-paper);
|
||||
--surface: var(--kit-seaglass);
|
||||
--panel: var(--kit-white);
|
||||
--text: var(--kit-ink);
|
||||
--muted: var(--kit-slate);
|
||||
--line: var(--kit-tideline);
|
||||
--line-strong: var(--kit-slate);
|
||||
--accent: var(--kit-signal-blue);
|
||||
--accent-hover: rgb(36 91 199 / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--kit-white);
|
||||
--hover: var(--kit-seaglass);
|
||||
--selected: var(--kit-shallows);
|
||||
--selection: rgb(36 91 199 / var(--gpui-selection-opacity));
|
||||
--ring: var(--kit-signal-blue);
|
||||
--live: var(--kit-kelp-deep);
|
||||
--success: var(--kit-kelp-deep);
|
||||
--attention: var(--kit-amber);
|
||||
--danger: var(--kit-rust);
|
||||
--mark-ink: var(--kit-ink);
|
||||
--ombre-ink: linear-gradient(135deg, var(--brand-light), var(--brand-deep));
|
||||
--diff-add: rgb(57 101 84 / 0.14);
|
||||
--diff-del: rgb(158 63 54 / 0.11);
|
||||
/* Paper: the plain ground above the horizon. */
|
||||
--ground: var(--kit-warm-paper);
|
||||
/* On paper, light is a quiet ring. */
|
||||
--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);
|
||||
--line-strong: var(--gpui-light-border-strong);
|
||||
--accent: var(--gpui-light-accent);
|
||||
--accent-hover: rgb(var(--gpui-light-accent-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-light-on-primary);
|
||||
--brand-fill: var(--gpui-light-primary);
|
||||
--brand-fill-hover: rgb(var(--gpui-light-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-brand: var(--gpui-light-on-primary);
|
||||
--hover: var(--gpui-light-hover);
|
||||
--selected: var(--gpui-light-list-active);
|
||||
--selection: rgb(var(--gpui-light-accent-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-light-accent);
|
||||
--live: var(--gpui-light-live);
|
||||
--success: var(--gpui-light-live);
|
||||
--attention: var(--gpui-light-attention);
|
||||
--danger: var(--gpui-light-danger);
|
||||
--mark-ink: var(--gpui-light-text);
|
||||
--ombre-ink: var(--ombre);
|
||||
--diff-add: var(--gpui-light-diff-add);
|
||||
--diff-del: var(--gpui-light-diff-remove);
|
||||
--ground: var(--gpui-light-bg);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 16%, transparent);
|
||||
--glow-focus: 0 0 0 6px rgb(36 91 199 / 0.12);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
--glow-focus: 0 0 0 4px rgb(var(--gpui-light-accent-rgb) / 0.16);
|
||||
--shadow-popover: var(--gpui-elevation-popover-light);
|
||||
--shadow-dialog: var(--gpui-elevation-dialog-light);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ---------- dark — the navy ocean, only when pinned ---------- */
|
||||
/* Paper is the site's appearance; the sea lives below the horizon in both.
|
||||
A reader who pins dark from the theme control gets the navy ground. */
|
||||
/* ---------- dark — charcoal, only when pinned ---------- */
|
||||
:root[data-theme="dark"] {
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--kit-foam);
|
||||
--muted: var(--kit-mist);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--kit-bioluminescence);
|
||||
--accent-hover: rgb(143 189 255 / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--kit-abyss);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(143 189 255 / var(--gpui-selection-opacity));
|
||||
--ring: var(--kit-bioluminescence);
|
||||
--live: var(--kit-kelp);
|
||||
--success: var(--kit-kelp);
|
||||
--attention: var(--kit-lantern);
|
||||
--danger: var(--kit-coral);
|
||||
--mark-ink: var(--kit-foam);
|
||||
--ombre-ink: linear-gradient(135deg, var(--kit-bioluminescence), var(--brand-light));
|
||||
--diff-add: rgb(158 203 189 / 0.16);
|
||||
--diff-del: rgb(238 165 157 / 0.12);
|
||||
--ground: radial-gradient(90rem 38rem at 50% -14rem, rgb(30 143 216 / 0.3), transparent 70%), linear-gradient(180deg, var(--ocean-sky) 0, var(--ocean-floor) 34rem);
|
||||
--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);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--accent: var(--gpui-dark-accent);
|
||||
--accent-hover: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--brand-fill: var(--gpui-dark-primary);
|
||||
--brand-fill-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-brand: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-hover);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--selection: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-accent);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--ombre-ink: var(--ombre);
|
||||
--diff-add: var(--gpui-dark-diff-add);
|
||||
--diff-del: var(--gpui-dark-diff-remove);
|
||||
--ground: var(--gpui-dark-bg);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(143 189 255 / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--glow-focus: 0 0 0 4px rgb(var(--gpui-dark-accent-rgb) / 0.3);
|
||||
--shadow-popover: var(--gpui-elevation-popover-dark);
|
||||
--shadow-dialog: var(--gpui-elevation-dialog-dark);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* ---------- the stage — below the horizon, in both appearances ---------- */
|
||||
/* The footer and the home sea. The ground is the deep water; raised and
|
||||
recessed plates step up and down from it. Legacy aliases are repeated
|
||||
here because a custom property that references another is computed where
|
||||
it is declared. */
|
||||
/* The footer and the home install band. Legacy aliases are repeated here
|
||||
because a custom property that references another is computed where it is
|
||||
declared. */
|
||||
.stage,
|
||||
.site-footer {
|
||||
--bg: var(--ocean-floor);
|
||||
--surface: var(--ocean-abyss);
|
||||
--panel: var(--ocean-raised);
|
||||
--text: var(--kit-foam);
|
||||
--muted: var(--kit-mist);
|
||||
--line: var(--ocean-line);
|
||||
--line-strong: var(--ocean-line-strong);
|
||||
--accent: var(--kit-bioluminescence);
|
||||
--accent-hover: rgb(143 189 255 / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--kit-abyss);
|
||||
--hover: var(--ocean-hover);
|
||||
--selected: var(--ocean-selected);
|
||||
--selection: rgb(143 189 255 / var(--gpui-selection-opacity));
|
||||
--ring: var(--kit-bioluminescence);
|
||||
--live: var(--kit-kelp);
|
||||
--success: var(--kit-kelp);
|
||||
--attention: var(--kit-lantern);
|
||||
--danger: var(--kit-coral);
|
||||
--mark-ink: var(--kit-foam);
|
||||
--ombre-ink: linear-gradient(135deg, var(--kit-bioluminescence), var(--brand-light));
|
||||
--diff-add: rgb(158 203 189 / 0.16);
|
||||
--diff-del: rgb(238 165 157 / 0.12);
|
||||
--bg: var(--gpui-dark-ocean-depth);
|
||||
--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);
|
||||
--line-strong: var(--gpui-dark-border-strong);
|
||||
--accent: var(--gpui-dark-accent);
|
||||
--accent-hover: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-accent: var(--gpui-dark-on-primary);
|
||||
--brand-fill: var(--gpui-dark-primary);
|
||||
--brand-fill-hover: rgb(var(--gpui-dark-primary-rgb) / var(--gpui-primary-hover-opacity));
|
||||
--on-brand: var(--gpui-dark-on-primary);
|
||||
--hover: var(--gpui-dark-hover);
|
||||
--selected: var(--gpui-dark-list-active);
|
||||
--selection: rgb(var(--gpui-dark-accent-rgb) / var(--gpui-selection-opacity));
|
||||
--ring: var(--gpui-dark-accent);
|
||||
--live: var(--gpui-dark-live);
|
||||
--success: var(--gpui-dark-live);
|
||||
--attention: var(--gpui-dark-attention);
|
||||
--danger: var(--gpui-dark-danger);
|
||||
--mark-ink: var(--gpui-dark-text);
|
||||
--ombre-ink: var(--ombre);
|
||||
--diff-add: var(--gpui-dark-diff-add);
|
||||
--diff-del: var(--gpui-dark-diff-remove);
|
||||
--glow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent), 0 0 12px 1px color-mix(in srgb, currentColor 45%, transparent);
|
||||
--glow-focus: 0 0 14px 2px rgb(143 189 255 / 0.35);
|
||||
--shadow-popover: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--glow-focus: 0 0 0 4px rgb(var(--gpui-dark-accent-rgb) / 0.3);
|
||||
--shadow-popover: var(--gpui-elevation-popover-dark);
|
||||
|
||||
--paper: var(--bg);
|
||||
--paper-deep: var(--surface);
|
||||
|
||||
+76
-17
@@ -251,18 +251,20 @@
|
||||
--gpui-dark-text-rgb: 239 238 235;
|
||||
--gpui-dark-panel: #2a2b2e;
|
||||
--gpui-dark-panel-rgb: 42 43 46;
|
||||
--gpui-dark-text-muted: #c2c1bd;
|
||||
--gpui-dark-text-muted-rgb: 194 193 189;
|
||||
--gpui-dark-text-muted: #b8b8b4;
|
||||
--gpui-dark-text-muted-rgb: 184 184 180;
|
||||
--gpui-dark-border: #3b3c3f;
|
||||
--gpui-dark-border-rgb: 59 60 63;
|
||||
--gpui-dark-sidebar: #191a1c;
|
||||
--gpui-dark-sidebar-rgb: 25 26 28;
|
||||
--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-primary: #e6e4df;
|
||||
--gpui-dark-primary-rgb: 230 228 223;
|
||||
--gpui-dark-on-primary: #202123;
|
||||
--gpui-dark-on-primary-rgb: 32 33 35;
|
||||
--gpui-dark-accent: #a9c0de;
|
||||
--gpui-dark-accent-rgb: 169 192 222;
|
||||
--gpui-dark-hover: #303134;
|
||||
--gpui-dark-hover-rgb: 48 49 52;
|
||||
--gpui-dark-list-active: #37393d;
|
||||
--gpui-dark-list-active-rgb: 55 57 61;
|
||||
--gpui-dark-attention: #e8b077;
|
||||
@@ -273,24 +275,32 @@
|
||||
--gpui-dark-danger-rgb: 227 154 144;
|
||||
--gpui-dark-border-strong: #828386;
|
||||
--gpui-dark-border-strong-rgb: 130 131 134;
|
||||
--gpui-dark-ocean-depth: #202123;
|
||||
--gpui-dark-ocean-depth-rgb: 32 33 35;
|
||||
--gpui-dark-diff-add: #22392f;
|
||||
--gpui-dark-diff-add-rgb: 34 57 47;
|
||||
--gpui-dark-diff-remove: #45302f;
|
||||
--gpui-dark-diff-remove-rgb: 69 48 47;
|
||||
--gpui-light-bg: #faf8f5;
|
||||
--gpui-light-bg-rgb: 250 248 245;
|
||||
--gpui-light-text: #28292b;
|
||||
--gpui-light-text-rgb: 40 41 43;
|
||||
--gpui-light-panel: #ffffff;
|
||||
--gpui-light-panel-rgb: 255 255 255;
|
||||
--gpui-light-text-muted: #535451;
|
||||
--gpui-light-text-muted-rgb: 83 84 81;
|
||||
--gpui-light-text-muted: #4f504d;
|
||||
--gpui-light-text-muted-rgb: 79 80 77;
|
||||
--gpui-light-border: #d9d5cf;
|
||||
--gpui-light-border-rgb: 217 213 207;
|
||||
--gpui-light-sidebar: #f0ede8;
|
||||
--gpui-light-sidebar-rgb: 240 237 232;
|
||||
--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-primary: #2e2f31;
|
||||
--gpui-light-primary-rgb: 46 47 49;
|
||||
--gpui-light-on-primary: #faf8f5;
|
||||
--gpui-light-on-primary-rgb: 250 248 245;
|
||||
--gpui-light-accent: #425c82;
|
||||
--gpui-light-accent-rgb: 66 92 130;
|
||||
--gpui-light-hover: #e8e5e0;
|
||||
--gpui-light-hover-rgb: 232 229 224;
|
||||
--gpui-light-list-active: #dfdcd6;
|
||||
--gpui-light-list-active-rgb: 223 220 214;
|
||||
--gpui-light-attention: #86520d;
|
||||
@@ -301,8 +311,14 @@
|
||||
--gpui-light-danger-rgb: 158 63 54;
|
||||
--gpui-light-border-strong: #807c76;
|
||||
--gpui-light-border-strong-rgb: 128 124 118;
|
||||
--gpui-light-ocean-depth: #faf8f5;
|
||||
--gpui-light-ocean-depth-rgb: 250 248 245;
|
||||
--gpui-light-diff-add: #d2eadd;
|
||||
--gpui-light-diff-add-rgb: 210 234 221;
|
||||
--gpui-light-diff-remove: #f8e1dd;
|
||||
--gpui-light-diff-remove-rgb: 248 225 221;
|
||||
}
|
||||
/* GPUI design 1.1.2; sha256 433b18a41c526da0b5a1bbaa461c37feac987c7688fbe7de660acfbbdafa9bd5. */
|
||||
/* GPUI design 2.3.0; sha256 9e74fb8f8fbfebb6b90a9740b324c56405277886e61273e2a0cb682797a8a6f9. */
|
||||
:root {
|
||||
--gpui-radius-control: 6px;
|
||||
--gpui-radius-panel: 10px;
|
||||
@@ -317,6 +333,49 @@
|
||||
--gpui-spacing-page: 32px;
|
||||
--gpui-selection-opacity: 0.28;
|
||||
--gpui-primary-hover-opacity: 0.9;
|
||||
--gpui-type-caption-size: 0.75rem;
|
||||
--gpui-type-caption-line: 1.333;
|
||||
--gpui-type-caption-weight: 400;
|
||||
--gpui-type-label-size: 0.8125rem;
|
||||
--gpui-type-label-line: 1.385;
|
||||
--gpui-type-label-weight: 400;
|
||||
--gpui-type-body-size: 0.875rem;
|
||||
--gpui-type-body-line: 1.571;
|
||||
--gpui-type-body-weight: 400;
|
||||
--gpui-type-prose-size: 1rem;
|
||||
--gpui-type-prose-line: 1.625;
|
||||
--gpui-type-prose-weight: 400;
|
||||
--gpui-type-heading-size: 1.0625rem;
|
||||
--gpui-type-heading-line: 1.412;
|
||||
--gpui-type-heading-weight: 550;
|
||||
--gpui-type-title-size: 1.375rem;
|
||||
--gpui-type-title-line: 1.273;
|
||||
--gpui-type-title-weight: 550;
|
||||
--gpui-type-display-size: 1.75rem;
|
||||
--gpui-type-display-line: 1.214;
|
||||
--gpui-type-display-weight: 550;
|
||||
--gpui-type-code-size: 0.8125rem;
|
||||
--gpui-type-code-line: 1.538;
|
||||
--gpui-type-code-weight: 400;
|
||||
--gpui-motion-duration-state: 120ms;
|
||||
--gpui-motion-duration-arrive: 180ms;
|
||||
--gpui-motion-duration-exit: 120ms;
|
||||
--gpui-motion-duration-panel: 340ms;
|
||||
--gpui-motion-duration-linger: 1200ms;
|
||||
--gpui-motion-loop-beat: 1200ms;
|
||||
--gpui-motion-loop-breathe: 2400ms;
|
||||
--gpui-motion-arrive-rise: 4px;
|
||||
--gpui-easing-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--gpui-easing-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--gpui-easing-panel: cubic-bezier(0.2, 0.1, 0.16, 1);
|
||||
--gpui-elevation-popover-light: 0 4px 8px rgb(0 0 0 / 0.16);
|
||||
--gpui-elevation-popover-dark: 0 4px 8px rgb(0 0 0 / 0.4);
|
||||
--gpui-elevation-dialog-light: 0 12px 32px rgb(0 0 0 / 0.2);
|
||||
--gpui-elevation-dialog-dark: 0 12px 32px rgb(0 0 0 / 0.5);
|
||||
--gpui-breakpoint-compact: 720px;
|
||||
--gpui-breakpoint-medium: 1024px;
|
||||
--gpui-breakpoint-wide: 1440px;
|
||||
--gpui-ombre: linear-gradient(135deg, #1e8fd8, #0b48bb);
|
||||
--gpui-font-fallbacks: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC";
|
||||
--gpui-mono-size: 0.8125rem;
|
||||
}
|
||||
@@ -12,11 +12,11 @@
|
||||
.pair label { margin-block-end: .7rem; }
|
||||
.aside { min-width: 0; }
|
||||
.print { display: flex; flex-direction: column; align-items: center; gap: .8rem; padding: 2rem 1.5rem; background: var(--ocean-floor); color: white; border: 1px solid var(--ocean-line); border-radius: var(--radius-sheet); text-align: center; }
|
||||
.caption { font-size: var(--text-ui); color: var(--kit-mist); }
|
||||
.caption { font-size: var(--text-ui); color: var(--ocean-line-strong); }
|
||||
.print img { filter: brightness(0) invert(1); }
|
||||
.printPhrase { margin: .5rem 0; font-size: clamp(1.35rem, 2.8vw, 2rem); font-weight: 650; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.brand { font-weight: 600; }
|
||||
.site { font-size: var(--text-caption); color: var(--kit-mist); }
|
||||
.site { font-size: var(--text-caption); color: var(--ocean-line-strong); }
|
||||
.cost { margin-block-start: 2rem; }
|
||||
.cost h2, .draft h2 { font-size: var(--text-title); margin-block-end: .5rem; }
|
||||
.cost p, .draft p { color: var(--muted); line-height: 1.6; }
|
||||
|
||||
+47
-117
@@ -1,129 +1,59 @@
|
||||
# Site colors and the Moda brand kit
|
||||
# Site colors and the design tokens
|
||||
|
||||
Decision (founder, 2026-10-11): the Moda brand kit "Codewhale"
|
||||
(`bk_3S5VAJCR69901RXWE5S5DFR3B3`) is the source of truth for color. The site's
|
||||
role tokens in `app/styles/tokens-roles.css` paint with `--kit-*` (the kit's
|
||||
original 25 colors) and nothing else. Geometry (radii, focus, spacing) still
|
||||
comes from the generated `--gpui-*` tokens.
|
||||
Decision (founder, 2026-10-27): the website uses the same design system as the
|
||||
desktop app. `codewhale-design` `tokens.json` (2.3.0, vendored at
|
||||
`vendor/codewhale-design`) is the source of truth for color, type roles,
|
||||
spacing, radius, motion and elevation. This supersedes the 2026-10-11 decision
|
||||
that made the Moda brand kit "Codewhale" the site's color authority.
|
||||
|
||||
## What was not changed, and why
|
||||
## How the values reach a page
|
||||
|
||||
`app/tokens.css` is generated by `scripts/export-design-tokens.py` from
|
||||
`crates/palette/src/rgb.rs` (the terminal palettes) and
|
||||
`vendor/codewhale-design/tokens.json`. Its 131 off-kit colors (`--whale-*`,
|
||||
`--light-*`, `--shoreline-*`) are the terminal's palettes. No page, component
|
||||
or stylesheet reads them (grep of `app`, `components`, `lib`); they only feed
|
||||
the palette contract tests. They are not site colors and are left to the
|
||||
palette crate. The `--gpui-*` colors in that file are no longer read by any
|
||||
role; they remain generated for parity checks.
|
||||
1. `scripts/export-design-tokens.py` reads the vendored `tokens.json` and
|
||||
writes `app/tokens.css`: `--gpui-light-*` / `--gpui-dark-*` colors,
|
||||
`--gpui-type-*`, `--gpui-motion-*`, `--gpui-easing-*`,
|
||||
`--gpui-elevation-*`, `--gpui-radius-*`, `--gpui-spacing-*`,
|
||||
`--gpui-breakpoint-*` and `--gpui-ombre`. `npm run check:tokens` fails when
|
||||
it is stale.
|
||||
2. `app/styles/tokens-roles.css` names the roles (`--bg`, `--text`,
|
||||
`--accent`, `--brand-fill`, `--text-ui`, `--dur-state` and so on). Every
|
||||
role is a `var(--gpui-*)`, never a literal.
|
||||
3. Pages and components paint only with roles.
|
||||
|
||||
The `#0a1e33` / `#f6f2e8` pair in `primitives.css` is the captured terminal
|
||||
frame's real colors and stays as captured.
|
||||
To change a color, change `tokens.json` in `codewhale-design`, bump its
|
||||
version, run `generate.py`, sync it here with `scripts/sync-to.sh`, then run
|
||||
`scripts/export-design-tokens.py`.
|
||||
|
||||
## Role mapping (tokens-roles.css)
|
||||
## Role mapping
|
||||
|
||||
| Role | Old | Kit color (new) |
|
||||
| Role | Light | Dark (pinned) and the stage |
|
||||
| --- | --- | --- |
|
||||
| light bg | gpui #faf8f5 | Warm Paper #FAF9F5 |
|
||||
| light surface (recessed) | gpui sidebar #f0ede8 | Seaglass #EDF1EF |
|
||||
| light panel (raised) | #ffffff | White #FFFFFF |
|
||||
| light text | #28292b | Ink #263844 |
|
||||
| light muted | #535451 | Slate #495A63 |
|
||||
| light line | #d9d5cf | Tideline #D2DEDD |
|
||||
| light line-strong | #807c76 | Slate #495A63 (see gap 1) |
|
||||
| light accent / ring | #245bc7 | Signal Blue #245BC7 |
|
||||
| light on-accent | #fbf5ee | White #FFFFFF |
|
||||
| light hover / selected | #e8e5e0 / #dfdcd6 | Seaglass / Shallows #DCEFEB |
|
||||
| light live | #3a6352 | Kelp Deep #396554 |
|
||||
| light attention | #86520d | Amber #84530D |
|
||||
| light danger | #9e3f36 | Rust #9E3F36 |
|
||||
| dark bg (ocean-floor) | #061431 | Deep Navy #142638 |
|
||||
| dark surface (ocean-abyss) | #030a1c | Abyss #0F1E2D |
|
||||
| dark panel (ocean-raised) | #0c2152 | Ocean Depth #173B50 |
|
||||
| dark hover | #11295e | Night Surface #20374E |
|
||||
| dark selected | #16316b | Night Border #31495E |
|
||||
| dark line | #1f3a78 | Night Border #31495E |
|
||||
| dark line-strong | #6f86b8 | Mist #BBC9D7 (see gap 1) |
|
||||
| ocean-sky (dark ground top) | #0a1d48 | Mark Navy #142352 |
|
||||
| dark text / muted | #efeeeb / #c2c1bd | Foam #E6EEF7 / Mist #BBC9D7 |
|
||||
| dark accent / ring | #90b9ff | Bioluminescence #8FBDFF |
|
||||
| dark on-accent | #15243e | Abyss #0F1E2D |
|
||||
| dark live | #9ec7b2 | Kelp #9ECBBD |
|
||||
| dark attention | #e8b077 | Lantern #EAB982 |
|
||||
| dark danger | #e39a90 | Coral #EEA59D |
|
||||
| brand-fill (primary button) | #1877c9 -> #0b48bb | Signal Blue -> Whale Blue |
|
||||
| brand-fill-hover | #1670c2 -> #0a3fa6 | Whale Blue -> Mark Navy |
|
||||
| sea ramp | #0a3a94, #072a6e, #061431 | Whale Blue, Mark Navy, Deep Navy |
|
||||
| tide-aqua | #d3ebf7 | Shallows #DCEFEB |
|
||||
| dark ombre text | #7cc2f2 -> #1e8fd8 | Bioluminescence -> Current Blue |
|
||||
| horizon light | rgb(144 185 255) | Bioluminescence rgb(143 189 255) |
|
||||
| diff add / del (light) | rgb(104 152 119) / rgb(189 119 116) | Kelp Deep / Rust, same alpha |
|
||||
| diff add / del (dark) | rgb(80 170 140) / rgb(227 154 144) | Kelp / Coral, same alpha |
|
||||
| OG image gradient | #0a1d48, #061431, #0b48bb | Mark Navy, Deep Navy, Whale Blue |
|
||||
| OG phrase | #efeeeb | Foam |
|
||||
| manifest theme/background | #061431 | Deep Navy #142638 |
|
||||
| merch caption on navy | #b3c5e8 | Mist #BBC9D7 |
|
||||
| whale belly in SVG and icon PNGs | #F4F8FF | White #FFFFFF |
|
||||
| `--bg` | `background` #faf8f5 | `background` #202123 (stage: `ocean_depth` #202123) |
|
||||
| `--surface` (recessed) | `sidebar` #f0ede8 | `sidebar` #191a1c |
|
||||
| `--panel` (raised) | `surface` #ffffff | `surface` #2a2b2e |
|
||||
| `--text` | `foreground` #28292b | `foreground` #efeeeb |
|
||||
| `--muted` | `muted_foreground` #4f504d | `muted_foreground` #b8b8b4 |
|
||||
| `--line` / `--line-strong` | `border` / `border_strong` | `border` / `border_strong` |
|
||||
| `--accent`, `--ring` (links, focus, selection) | `accent` #425c82 | `accent` #a9c0de |
|
||||
| `--brand-fill` / `--on-brand` (filled actions) | `primary` #2e2f31 / `primary_foreground` | `primary` #e6e4df / `primary_foreground` |
|
||||
| `--hover` / `--selected` | `hover` / `selected` | `hover` / `selected` |
|
||||
| `--live` / `--attention` / `--danger` | token roles | token roles |
|
||||
| `--diff-add` / `--diff-del` | `diff_add` / `diff_remove` | `diff_add` / `diff_remove` |
|
||||
|
||||
The mono mark's `#000000` is the kit's "Mark - mono black" logo and stays.
|
||||
The logo ombre (`#1e8fd8` → `#0b48bb`, `--ombre`, `--brand-light`,
|
||||
`--brand-deep`) is used only on the whale and the mark.
|
||||
|
||||
## Contrast (WCAG 2.x, computed from the kit hexes)
|
||||
The footer and the home install band (`.stage`, `.site-footer`) are painted
|
||||
with the dark set in both appearances.
|
||||
|
||||
Every text ink on every ground (bg, surface, panel, hover, selected):
|
||||
## Not from tokens
|
||||
|
||||
| Scheme | Lowest text ratio | Where |
|
||||
| --- | --- | --- |
|
||||
| light | 5.18 | Signal Blue on Shallows |
|
||||
| dark and stage | 4.68 | Coral on Night Border |
|
||||
- `--whale-*`, `--light-*` and `--shoreline-*` in `app/tokens.css` are the
|
||||
terminal's palettes from `crates/palette/src/rgb.rs`. No page reads them.
|
||||
- `#0a1e33` / `#f6f2e8` in `primitives.css` are the captured terminal frame's
|
||||
real colors and stay as captured.
|
||||
- The fluid editorial sizes (`--text-section`, `--text-display`,
|
||||
`--text-display-lg`) are the website's own layout steps above the token
|
||||
`display` role.
|
||||
|
||||
Other pairs: light line-strong 6.81 on Warm Paper, 7.18 on White; dark
|
||||
line-strong 9.13 on Deep Navy, 7.00 on Ocean Depth; white on the primary fill
|
||||
6.18 (Signal Blue) and 7.90 (Whale Blue); on-accent 6.18 light, 8.76 dark;
|
||||
Foam on the sea ramp 6.75 / 12.89 / 13.15; Mist on the sea ramp 4.68 / 8.94 /
|
||||
9.13 (the lowest is Mist on Whale Blue). `lib/gpui-role-tokens.test.ts` asserts
|
||||
these floors in CI.
|
||||
|
||||
Non-text edges: the kit's hairlines are faint by design (Tideline on Warm
|
||||
Paper 1.31:1, Night Border on Deep Navy 1.65:1). Control edges use
|
||||
`--line-strong`, never `--line`.
|
||||
|
||||
## Gaps in the kit (recorded, not edited)
|
||||
|
||||
1. A control-edge color for each scheme. Slate and Mist pass 3:1 but are
|
||||
heavier than a border should be. Proposal: light `#7A8B95` (3.35:1 on Warm
|
||||
Paper, 3.53:1 on White, 3.10:1 on Seaglass); dark `#6F8CA3` (4.37:1 on Deep
|
||||
Navy, 3.35:1 on Ocean Depth, 3.47:1 on Night Surface).
|
||||
2. The kit has no ordered ramp of five dark grounds; Night Border doubles as
|
||||
the selected-row fill and the hairline.
|
||||
3. Fonts: the kit names Noto Sans; the site loads Shannon Sans subsets. Not a
|
||||
color, not changed here.
|
||||
|
||||
## Where codewhale-design differs
|
||||
|
||||
`vendor/codewhale-design/tokens.json` (GPUI 1.1.2) uses neutral warm grays
|
||||
(#28292b text, #faf8f5 paper, #202123 dark background) and the kit uses
|
||||
blue-tinted inks and navy grounds. The kit now leads on the site; the GPUI
|
||||
tokens differ on text, muted, lines, sidebar, dark grounds, live, attention and
|
||||
danger by a few units of lightness. Primary light (#245bc7) and danger light
|
||||
(#9e3f36) are identical. That repo was not edited.
|
||||
|
||||
## Imagery decisions
|
||||
|
||||
| Asset | Decision |
|
||||
| --- | --- |
|
||||
| `app/opengraph-image.tsx` | recolored to kit stops (generated in code) |
|
||||
| `public/whale/*.svg`, `app/icon.svg` | recolored belly to White; gradient stops were already kit |
|
||||
| `icon-192.png`, `icon-512.png`, `app/apple-icon.png` | belly pixels #F4F8FF recolored to White, no regeneration |
|
||||
| `codwhale-404.webp` | keep: the founder's restored 404 poster (see `ocean-contract.test.ts`), photographic parody art, name kept |
|
||||
| `codewhale-tui-*.png` | not touched: referenced only by README files, outside this slice |
|
||||
| `public/merch/` | keep: product renders |
|
||||
|
||||
## Two marks, two jobs (founder, 2026-10-11)
|
||||
|
||||
| Mark | Use for |
|
||||
| --- | --- |
|
||||
| Smooth aurora whale (`public/brand/mark-gradient.svg` family) | identity: app icon, favicon, header, OG image, print and merch |
|
||||
| Pixel-dither whale (`public/brand/pixel-whale.webp`, 192-cell dither, same aurora gradient and silhouette) | the terminal side: pet mode page, docs and README heroes, release art, TUI splash |
|
||||
|
||||
The pixel mark is a lossless raster (7.6 KB) shown with `image-rendering:
|
||||
pixelated`. The 404 poster stays as the founder restored it.
|
||||
`lib/gpui-role-tokens.test.ts` checks that every role resolves to its token
|
||||
and that every ink clears WCAG AA on every ground in every scheme.
|
||||
@@ -204,8 +204,6 @@
|
||||
"latestRelease": "Latest release {tag}",
|
||||
"releaseUnavailable": "Release status unavailable",
|
||||
"proofContributors": "{count} contributors",
|
||||
"proofTools": "{count} built-in tools",
|
||||
"proofProviders": "{count} provider routes",
|
||||
"currentSource": "Source",
|
||||
"sourceCandidate": "Unreleased",
|
||||
"publishedRelease": "released",
|
||||
|
||||
@@ -204,8 +204,6 @@
|
||||
"latestRelease": "最新发布 {tag}",
|
||||
"releaseUnavailable": "发布状态暂不可用",
|
||||
"proofContributors": "{count} 位贡献者",
|
||||
"proofTools": "{count} 个内置工具",
|
||||
"proofProviders": "{count} 条服务商路由",
|
||||
"currentSource": "源码",
|
||||
"sourceCandidate": "未发布",
|
||||
"publishedRelease": "已发布",
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("design grammar contract", () => {
|
||||
expect(CSS).toMatch(/::selection \{ background: var\(--selection\); \}/);
|
||||
// The light ring on the dark stage would fall under 3:1, so the stage
|
||||
// re-declares it.
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--kit-bioluminescence\);/);
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--gpui-dark-accent\);/);
|
||||
});
|
||||
|
||||
it("paints every primary fill with the logo gradient and white text", () => {
|
||||
@@ -67,7 +67,8 @@ describe("design grammar contract", () => {
|
||||
it("times motion with the spring tokens and stills it under reduced motion", () => {
|
||||
expect(CSS).not.toMatch(/\d+ms ease[,;]/);
|
||||
expect(CSS).not.toMatch(/cubic-bezier\(0\.25, 0\.46/);
|
||||
expect(CSS).toMatch(/--ease-spring: cubic-bezier\(/);
|
||||
expect(CSS).toMatch(/--ease-spring: var\(--gpui-easing-panel\);/);
|
||||
expect(resolveWhale("var(--gpui-easing-panel)")).toMatch(/^cubic-bezier\(/);
|
||||
expect(CSS).toMatch(
|
||||
/@media \(prefers-reduced-motion: reduce\) \{\s*:root \{\s*--dur-state: 0ms;\s*--dur-spring: 0ms;/,
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ import { resolveWhale } from "./whale-tokens";
|
||||
import { siteCss } from "./site-css";
|
||||
|
||||
// The role layer (app/styles/tokens-roles.css): every role in every scheme,
|
||||
// inks from the Moda brand kit, the navy ocean grounds measured
|
||||
// against every ink at WCAG AA.
|
||||
// every value a tokens.json role, every ink measured against every
|
||||
// ground at WCAG AA.
|
||||
const CSS = siteCss();
|
||||
const ROLES = [
|
||||
"bg", "surface", "panel", "text", "muted", "line", "line-strong", "accent", "on-accent",
|
||||
@@ -62,21 +62,20 @@ describe("role tokens", () => {
|
||||
expect(CSS).not.toMatch(/@media \(prefers-color-scheme: dark\)/);
|
||||
});
|
||||
|
||||
it("inks with the Moda brand kit in both appearances", () => {
|
||||
const kit = (name: string) => hex(`var(--kit-${name})`);
|
||||
const inks: Record<string, Record<string, string>> = {
|
||||
light: { text: "ink", muted: "slate", accent: "signal-blue", ring: "signal-blue", live: "kelp-deep", attention: "amber", danger: "rust", "on-accent": "white" },
|
||||
dark: { text: "foam", muted: "mist", accent: "bioluminescence", ring: "bioluminescence", live: "kelp", attention: "lantern", danger: "coral", "on-accent": "abyss" },
|
||||
it("inks every role from the tokens.json set in both appearances", () => {
|
||||
const inks: Record<string, string> = {
|
||||
text: "text", muted: "text-muted", accent: "accent", ring: "accent", live: "live",
|
||||
attention: "attention", danger: "danger", "on-accent": "on-primary",
|
||||
bg: "bg", panel: "panel", surface: "sidebar", hover: "hover", selected: "list-active",
|
||||
"brand-fill": "primary", "on-brand": "on-primary",
|
||||
};
|
||||
for (const [mode, scheme] of [["light", light()], ["dark", pinnedDark()], ["dark", stage()]] as const) {
|
||||
for (const [role, name] of Object.entries(inks[mode])) {
|
||||
expect(scheme[role], `${mode}.${role}`).toBe(`var(--kit-${name})`);
|
||||
expect(hex(scheme[role]), `${mode}.${role}`).toBe(kit(name));
|
||||
for (const [mode, scheme, isStage] of [["light", light(), false], ["dark", pinnedDark(), false], ["dark", stage(), true]] as const) {
|
||||
for (const [role, token] of Object.entries(inks)) {
|
||||
if (isStage && role === "bg") continue;
|
||||
expect(scheme[role], `${mode}.${role}`).toBe(`var(--gpui-${mode}-${token})`);
|
||||
}
|
||||
}
|
||||
expect(hex(light().bg)).toBe(kit("warm-paper"));
|
||||
expect(hex(light().panel)).toBe(kit("white"));
|
||||
for (const role of GROUNDS) expect(pinnedDark()[role], role).toMatch(/^var\(--ocean-[\w-]+\)$/);
|
||||
expect(stage().bg).toBe("var(--gpui-dark-ocean-depth)");
|
||||
});
|
||||
|
||||
it("keeps every text ink at WCAG AA on every ground, in every scheme", () => {
|
||||
@@ -94,44 +93,17 @@ describe("role tokens", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("puts white text on the logo gradient only where it clears 4.5:1", () => {
|
||||
const fill = CSS.match(/--brand-fill:\s*linear-gradient\(([^;]+)\);/)?.[1] ?? "";
|
||||
const stops = [...fill.matchAll(/#[0-9a-f]{6}|var\(--brand-[\w-]+\)/gi)].map((m) => hex(m[0]));
|
||||
expect(stops.length).toBeGreaterThanOrEqual(2);
|
||||
for (const stop of stops) expect(contrast("#ffffff", stop), stop).toBeGreaterThanOrEqual(4.5);
|
||||
it("puts primary-foreground ink on every filled action at AA", () => {
|
||||
for (const [name, scheme] of [["light", light()], ["dark", pinnedDark()], ["stage", stage()]] as const) {
|
||||
expect(contrast(hex(scheme["on-brand"]), hex(scheme["brand-fill"])), `${name}: on-brand`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
expect(hex("var(--brand-deep)")).toBe("#0b48bb");
|
||||
expect(hex("var(--brand-light)")).toBe("#1e8fd8");
|
||||
});
|
||||
|
||||
function stops(token: string): string[] {
|
||||
const ramp = CSS.match(new RegExp(`--${token}:\\s*linear-gradient\\(([^;]+)\\);`))?.[1] ?? "";
|
||||
return [...ramp.matchAll(/#[0-9a-f]{6}|var\(--[\w-]+\)/gi)].map((m) => hex(m[0]));
|
||||
}
|
||||
|
||||
it("keeps sea text readable on every stop of the sea ramps", () => {
|
||||
for (const token of ["sea", "sea-band"]) {
|
||||
const ramp = stops(token);
|
||||
expect(ramp.length, token).toBeGreaterThanOrEqual(3);
|
||||
for (const stop of ramp) {
|
||||
expect(contrast(hex(stage().muted), stop), `${token}: muted on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(stage().text), stop), `${token}: text on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps stage text readable over the deep tide stops", () => {
|
||||
// The tide runs paper -> aqua -> logo light -> logo deep -> mid -> navy.
|
||||
// Only its deep stops can carry stage text; the pale stops cannot.
|
||||
const ramp = stops("tide");
|
||||
const deep = ramp.indexOf(hex("var(--brand-deep)"));
|
||||
const mid = ramp.indexOf(hex("var(--sea-mid)"));
|
||||
expect(deep).toBeGreaterThan(0);
|
||||
expect(mid).toBeGreaterThan(deep);
|
||||
for (const stop of ramp.slice(deep)) {
|
||||
expect(contrast(hex(stage().text), stop), `text on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
for (const stop of ramp.slice(mid)) {
|
||||
expect(contrast(hex(stage().muted), stop), `muted on ${stop}`).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
it("keeps stage text readable on the floor below the horizon", () => {
|
||||
const floor = hex("var(--ocean-floor)");
|
||||
expect(contrast(hex(stage().text), floor)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrast(hex(stage().muted), floor)).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "أحدث إصدار {tag}",
|
||||
releaseUnavailable: "حالة الإصدار غير متاحة",
|
||||
proofContributors: "{count} مساهمًا",
|
||||
proofTools: "{count} أداة مدمجة",
|
||||
proofProviders: "{count} مسار مزوّد",
|
||||
currentSource: "المصدر",
|
||||
sourceCandidate: "غير منشور",
|
||||
publishedRelease: "منشور",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Última versió {tag}",
|
||||
releaseUnavailable: "Estat de la versió no disponible",
|
||||
proofContributors: "{count} col·laboradors",
|
||||
proofTools: "{count} eines integrades",
|
||||
proofProviders: "{count} rutes de proveïdor",
|
||||
currentSource: "Font",
|
||||
sourceCandidate: "Sense publicar",
|
||||
publishedRelease: "publicada",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Aktuellstes Release {tag}",
|
||||
releaseUnavailable: "Release-Status nicht verfügbar",
|
||||
proofContributors: "{count} Mitwirkende",
|
||||
proofTools: "{count} integrierte Werkzeuge",
|
||||
proofProviders: "{count} Anbieter-Routen",
|
||||
currentSource: "Quelle",
|
||||
sourceCandidate: "Unveröffentlicht",
|
||||
publishedRelease: "veröffentlicht",
|
||||
|
||||
@@ -18,8 +18,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Latest release {tag}",
|
||||
releaseUnavailable: "Release status unavailable",
|
||||
proofContributors: "{count} contributors",
|
||||
proofTools: "{count} built-in tools",
|
||||
proofProviders: "{count} provider routes",
|
||||
currentSource: "Source",
|
||||
sourceCandidate: "Unreleased",
|
||||
publishedRelease: "released",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Último lanzamiento {tag}",
|
||||
releaseUnavailable: "Estado del lanzamiento no disponible",
|
||||
proofContributors: "{count} colaboradores",
|
||||
proofTools: "{count} herramientas integradas",
|
||||
proofProviders: "{count} rutas de proveedor",
|
||||
currentSource: "Fuente",
|
||||
sourceCandidate: "Sin publicar",
|
||||
publishedRelease: "publicado",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Dernière version {tag}",
|
||||
releaseUnavailable: "État des versions indisponible",
|
||||
proofContributors: "{count} contributeurs",
|
||||
proofTools: "{count} outils intégrés",
|
||||
proofProviders: "{count} routes de fournisseur",
|
||||
currentSource: "Source",
|
||||
sourceCandidate: "Non publiée",
|
||||
publishedRelease: "publiée",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "नवीनतम रिलीज़ {tag}",
|
||||
releaseUnavailable: "रिलीज़ स्थिति उपलब्ध नहीं",
|
||||
proofContributors: "{count} योगदानकर्ता",
|
||||
proofTools: "{count} अंतर्निहित टूल",
|
||||
proofProviders: "{count} प्रदाता रूट",
|
||||
currentSource: "सोर्स",
|
||||
sourceCandidate: "अप्रकाशित",
|
||||
publishedRelease: "प्रकाशित",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Rilis terbaru {tag}",
|
||||
releaseUnavailable: "Status rilis tidak tersedia",
|
||||
proofContributors: "{count} kontributor",
|
||||
proofTools: "{count} alat bawaan",
|
||||
proofProviders: "{count} rute penyedia",
|
||||
currentSource: "Sumber",
|
||||
sourceCandidate: "Belum dirilis",
|
||||
publishedRelease: "dirilis",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Ultima release {tag}",
|
||||
releaseUnavailable: "Stato delle release non disponibile",
|
||||
proofContributors: "{count} contributori",
|
||||
proofTools: "{count} strumenti integrati",
|
||||
proofProviders: "{count} percorsi dei provider",
|
||||
currentSource: "Sorgente",
|
||||
sourceCandidate: "Non rilasciata",
|
||||
publishedRelease: "rilasciata",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "最新リリース {tag}",
|
||||
releaseUnavailable: "リリース情報を取得できません",
|
||||
proofContributors: "コントリビューター {count} 人",
|
||||
proofTools: "組み込みツール {count} 個",
|
||||
proofProviders: "プロバイダールート {count} 件",
|
||||
currentSource: "ソース",
|
||||
sourceCandidate: "未リリース",
|
||||
publishedRelease: "リリース済み",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "최신 릴리스 {tag}",
|
||||
releaseUnavailable: "릴리스 상태를 확인할 수 없음",
|
||||
proofContributors: "기여자 {count}명",
|
||||
proofTools: "내장 도구 {count}개",
|
||||
proofProviders: "제공자 경로 {count}개",
|
||||
currentSource: "소스",
|
||||
sourceCandidate: "미공개",
|
||||
publishedRelease: "공개됨",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Najnowsze wydanie {tag}",
|
||||
releaseUnavailable: "Status wydania niedostępny",
|
||||
proofContributors: "Współtwórcy: {count}",
|
||||
proofTools: "Wbudowane narzędzia: {count}",
|
||||
proofProviders: "Trasy dostawców: {count}",
|
||||
currentSource: "Źródło",
|
||||
sourceCandidate: "Niewydane",
|
||||
publishedRelease: "wydane",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Último lançamento {tag}",
|
||||
releaseUnavailable: "Status do lançamento indisponível",
|
||||
proofContributors: "{count} colaboradores",
|
||||
proofTools: "{count} ferramentas integradas",
|
||||
proofProviders: "{count} rotas de provedor",
|
||||
currentSource: "Código-fonte",
|
||||
sourceCandidate: "Não publicado",
|
||||
publishedRelease: "publicado",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Последний релиз {tag}",
|
||||
releaseUnavailable: "Статус релиза недоступен",
|
||||
proofContributors: "Участников: {count}",
|
||||
proofTools: "Встроенных инструментов: {count}",
|
||||
proofProviders: "Маршрутов провайдеров: {count}",
|
||||
currentSource: "Исходники",
|
||||
sourceCandidate: "Не выпущено",
|
||||
publishedRelease: "выпущено",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "En yeni sürüm {tag}",
|
||||
releaseUnavailable: "Sürüm durumu kullanılamıyor",
|
||||
proofContributors: "{count} katkıda bulunan",
|
||||
proofTools: "{count} yerleşik araç",
|
||||
proofProviders: "{count} sağlayıcı rotası",
|
||||
currentSource: "Kaynak",
|
||||
sourceCandidate: "Yayımlanmadı",
|
||||
publishedRelease: "yayımlandı",
|
||||
|
||||
@@ -163,8 +163,6 @@ export interface HomeDict {
|
||||
latestRelease: string;
|
||||
releaseUnavailable: string;
|
||||
proofContributors: string;
|
||||
proofTools: string;
|
||||
proofProviders: string;
|
||||
/** "Source" / "Unreleased" — prepended to `v{version}`. */
|
||||
currentSource: string;
|
||||
sourceCandidate: string;
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Останній реліз {tag}",
|
||||
releaseUnavailable: "Статус релізу недоступний",
|
||||
proofContributors: "Учасників: {count}",
|
||||
proofTools: "Вбудованих інструментів: {count}",
|
||||
proofProviders: "Маршрутів провайдерів: {count}",
|
||||
currentSource: "Джерело",
|
||||
sourceCandidate: "Не випущено",
|
||||
publishedRelease: "випущено",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "Bản phát hành mới nhất {tag}",
|
||||
releaseUnavailable: "Không có trạng thái phát hành",
|
||||
proofContributors: "{count} người đóng góp",
|
||||
proofTools: "{count} công cụ tích hợp",
|
||||
proofProviders: "{count} tuyến nhà cung cấp",
|
||||
currentSource: "Mã nguồn",
|
||||
sourceCandidate: "Chưa phát hành",
|
||||
publishedRelease: "đã phát hành",
|
||||
|
||||
@@ -16,8 +16,6 @@ export const home: HomeDict = {
|
||||
latestRelease: "最新发布 {tag}",
|
||||
releaseUnavailable: "发布状态暂不可用",
|
||||
proofContributors: "{count} 位贡献者",
|
||||
proofTools: "{count} 个内置工具",
|
||||
proofProviders: "{count} 条服务商路由",
|
||||
currentSource: "源码",
|
||||
sourceCandidate: "未发布",
|
||||
publishedRelease: "已发布",
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("the horizon", () => {
|
||||
});
|
||||
|
||||
it("paints the stage with the dark set in both appearances", () => {
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--kit-bioluminescence\);[^}]*color-scheme: dark;/);
|
||||
expect(CSS).toMatch(/\.stage,\s*\.site-footer\s*\{[^}]*--ring: var\(--gpui-dark-accent\);[^}]*color-scheme: dark;/);
|
||||
});
|
||||
|
||||
it("keeps the horizon visible in forced colours", () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("typography contract", () => {
|
||||
expect(rootVar("font-mono")).toMatch(/^ui-monospace,/);
|
||||
expect(rootVar("font-cjk")).not.toMatch(/Serif|(?<!sans-)serif/);
|
||||
expect(resolveWhale(rootVar("text-mono"))).toBe("0.8125rem");
|
||||
expect(rootVar("text-prose")).toBe("1.0625rem");
|
||||
expect(resolveWhale(rootVar("text-prose"))).toBe("1rem");
|
||||
});
|
||||
|
||||
it("preloads only the Latin subset of Shannon Sans", () => {
|
||||
|
||||
Reference in new issue
Block a user