From 8fa3f7da3e417309267162772c47f99ab8d0de87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 17 Aug 2026 01:08:54 +0000 Subject: [PATCH] hex everywhere, because the palettes were half broken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported as "the ellipsis doesn't show in the streaming bubble". It was the visible corner of something much larger: every Tailwind semantic utility was broken under all eleven ported palettes. `@theme` mapped `--color-muted-foreground: hsl(var(--muted-foreground))`, which needs an unwrapped triplet. A ported palette publishes `#6e6a86`, so the mapping produced `hsl(#6e6a86)` — invalid — and the utility fell back to white or transparent: duck-dark text-muted-foreground=rgb(148,163,184) bg-muted=rgb(30,41,59) rose-pine text-muted-foreground=rgb(255,255,255) bg-muted=rgba(0,0,0,0) The themes looked convincing because panels, the page and the header read their variables DIRECTLY rather than through a utility class. Everything that went through Tailwind was quietly wrong, and the thinking dots — `bg-muted-foreground/60` — were simply invisible. So: 123 HSL triplets converted to hex across the three hand-written themes, and 37 `hsl(var(--x))` mappings unwrapped. One format everywhere, which is also the format every published palette ships in. The triplets only ever existed so `bg-card/50` could get opacity. That was tested earlier tonight and turned out unnecessary — Tailwind v4 emits `color-mix(in oklab, var(--card) 50%, transparent)` for any colour format. Having proved it, I then left the mappings wrapped, which is what broke the palettes. Verified across six themes: `bg-muted` and `bg-muted-foreground/60` resolve in every one of them, and Duck's values are unchanged through the conversion — `bg-muted` is `rgb(30,41,59)` before and after. Co-Authored-By: Claude Opus 5 --- src/workspaces/theme/css/base.css | 87 +++++++++++-------- src/workspaces/theme/css/themes/duck-dark.css | 86 +++++++++--------- .../theme/css/themes/duck-light.css | 86 +++++++++--------- src/workspaces/theme/css/themes/plain.css | 82 ++++++++--------- 4 files changed, 177 insertions(+), 164 deletions(-) diff --git a/src/workspaces/theme/css/base.css b/src/workspaces/theme/css/base.css index 3e5b8bb5..7944abc8 100644 --- a/src/workspaces/theme/css/base.css +++ b/src/workspaces/theme/css/base.css @@ -14,7 +14,7 @@ ── How a value reaches a component ── component .bg-surface - @theme (here) --color-surface: hsl(var(--surface)) + @theme (here) --color-surface: var(--surface) theme file --surface: 0 0% 100% The indirection through `var()` is load-bearing: Tailwind resolves `@theme` at BUILD time, so a theme @@ -57,64 +57,77 @@ } } +/* + Values are HEX, not unwrapped HSL triplets. + ============================================================================================ + + This block used to read `--color-card: hsl(var(--card))`, which requires every theme to publish an + unwrapped triplet (`0 0% 100%`). That was done so `bg-card/50` could get opacity — and it turned out to + be unnecessary: Tailwind v4 emits `color-mix(in oklab, var(--card) 50%, transparent)` for any colour + format, with a `@supports` fallback. Tested before changing it. + + It was also actively wrong the moment a theme arrived in hex: `hsl(#6e6a86)` is invalid, so every + semantic utility silently fell back to white or transparent. Eleven ported palettes looked correct only + because panels and the page read their variables directly rather than through a utility class. +*/ @theme { /* ── Semantic colour, the whole vocabulary a component is allowed to use ──────────────────────── */ - --color-border: hsl(var(--border)); - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-background: var(--background); + --color-foreground: var(--foreground); - --color-brand: hsl(var(--brand)); - --color-brand-foreground: hsl(var(--brand-foreground)); - --color-brand-muted: hsl(var(--brand-muted)); + --color-brand: var(--brand); + --color-brand-foreground: var(--brand-foreground); + --color-brand-muted: var(--brand-muted); - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); - --color-secondary: hsl(var(--secondary)); - --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); - --color-destructive: hsl(var(--destructive)); - --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); - --color-success: hsl(var(--success)); - --color-success-foreground: hsl(var(--success-foreground)); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); - --color-warning: hsl(var(--warning)); - --color-warning-foreground: hsl(var(--warning-foreground)); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); - --color-info: hsl(var(--info)); - --color-info-foreground: hsl(var(--info-foreground)); + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); /* The sixth state, and the reason it exists: `running` is neither good news nor a warning. The chat tray, the send button and the queue all wore amber for it, and the token set had nothing to offer — `--status-in-progress` is blue, and `warning` would make a working job look like a failing one. `-ink` is the readable-on-a-tint variant: the fill is bright enough to sit behind black text, which makes it far too light to BE text. Every tone will want this eventually; busy needed it first. */ - --color-busy: hsl(var(--busy)); - --color-busy-foreground: hsl(var(--busy-foreground)); - --color-busy-ink: hsl(var(--busy-ink)); + --color-busy: var(--busy); + --color-busy-foreground: var(--busy-foreground); + --color-busy-ink: var(--busy-ink); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); - --color-accent: hsl(var(--accent)); - --color-accent-foreground: hsl(var(--accent-foreground)); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); - --color-popover: hsl(var(--popover)); - --color-popover-foreground: hsl(var(--popover-foreground)); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); - --color-card: hsl(var(--card)); - --color-card-foreground: hsl(var(--card-foreground)); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); - --color-cta: hsl(var(--cta)); - --color-cta-foreground: hsl(var(--cta-foreground)); - --color-ctahover: hsl(var(--ctahover)); + --color-cta: var(--cta); + --color-cta-foreground: var(--cta-foreground); + --color-ctahover: var(--ctahover); - --color-naturegreen: hsl(var(--naturegreen)); - --color-naturegreen-foreground: hsl(var(--naturegreen-foreground)); + --color-naturegreen: var(--naturegreen); + --color-naturegreen-foreground: var(--naturegreen-foreground); /* ── The brand slots ──────────────────────────────────────────────────────────────────────────── diff --git a/src/workspaces/theme/css/themes/duck-dark.css b/src/workspaces/theme/css/themes/duck-dark.css index 30d59d75..1ac65c02 100644 --- a/src/workspaces/theme/css/themes/duck-dark.css +++ b/src/workspaces/theme/css/themes/duck-dark.css @@ -44,7 +44,7 @@ --duck-orange: var(--brand-warm); /* ── The page ─────────────────────────────────────────────────────────────────────────────────── */ - --page-bg: hsl(222.2 84% 4.9%); + --page-bg: #020817; --pixel-grid-color: rgba(200, 230, 210, 0.08); /* ── The wallpaper, the header and the mark ─────────────────────────────────────────────────── @@ -75,7 +75,7 @@ --panel-blur: 24px; --panel-radius: 0.75rem; --panel-header-bg: rgba(0, 0, 0, 0.15); - --panel-header-fg: hsl(var(--foreground)); + --panel-header-fg: var(--foreground); --panel-accent: var(--brand-primary); --panel-grid-color: rgba(200, 230, 210, 0.06); @@ -87,71 +87,71 @@ --card-grid-color: rgba(200, 230, 210, 0.06); /* ── Semantic colour ──────────────────────────────────────────────────────────────────────────── */ - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; + --background: #020817; + --foreground: #f8fafc; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; + --card: #020817; + --card-foreground: #f8fafc; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; + --popover: #020817; + --popover-foreground: #f8fafc; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: #f8fafc; + --primary-foreground: #0f172a; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: #1e293b; + --secondary-foreground: #f8fafc; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: #1e293b; + --muted-foreground: #94a3b8; - --accent: 211 82% 64%; - --accent-foreground: 0 0% 100%; + --accent: #58a1ee; + --accent-foreground: #ffffff; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: #7f1d1d; + --destructive-foreground: #f8fafc; - --success: 142.1 76.2% 36.3%; - --success-foreground: 355.7 100% 97.3%; + --success: #16a34a; + --success-foreground: #fff1f2; - --brand: 145 60% 45%; - --brand-foreground: 0 0% 100%; - --brand-muted: 145 30% 25%; + --brand: #2eb867; + --brand-foreground: #ffffff; + --brand-muted: #2d533d; - --warning: 43 96% 56%; - --warning-foreground: 0 0% 0%; + --warning: #fbbd23; + --warning-foreground: #000000; /* Lifted well above the light value — 50% lightness violet on a near-black background reads as muddy grey at 12px, which is exactly the size it is used at. */ - --info: 263 90% 70%; - --info-foreground: 0 0% 100%; + --info: #a26ef7; + --info-foreground: #ffffff; /* Busy: running, queued, in flight. See base.css. */ - --busy: 43 96% 56%; - --busy-foreground: 20 14% 8%; - --busy-ink: 43 96% 70%; + --busy: #fbbd23; + --busy-foreground: #171312; + --busy-ink: #fcd269; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --border: #1e293b; + --input: #1e293b; + --ring: #cbd5e1; /* The original dark block never overrode these, so the light values were in force. Written out because a theme that silently inherits from another is the thing this file exists to stop. */ - --cta: 211 74.77% 45.58%; - --cta-foreground: 0 0% 100%; - --ctahover: 211 91.53% 34.82%; + --cta: #1d71cb; + --cta-foreground: #ffffff; + --ctahover: #0856aa; - --naturegreen: 145 79% 38%; - --naturegreen-foreground: 0 0% 100%; + --naturegreen: #14ad54; + --naturegreen-foreground: #ffffff; --radius: 0.5rem; /* ── Data visualisation and status ────────────────────────────────────────────────────────────── */ - --chart-primary: 211 82% 64%; - --chart-secondary: 211 82% 84%; - --status-success: 142 76% 36%; - --status-warning: 43 96% 56%; - --status-in-progress: 211 82% 64%; + --chart-primary: #58a1ee; + --chart-secondary: #b5d5f8; + --status-success: #16a249; + --status-warning: #fbbd23; + --status-in-progress: #58a1ee; /* ── Chrome ───────────────────────────────────────────────────────────────────────────────────── */ --scrollbar-thumb: #444; diff --git a/src/workspaces/theme/css/themes/duck-light.css b/src/workspaces/theme/css/themes/duck-light.css index 3cf8b288..7f067d6d 100644 --- a/src/workspaces/theme/css/themes/duck-light.css +++ b/src/workspaces/theme/css/themes/duck-light.css @@ -46,7 +46,7 @@ /* ── The page ───────────────────────────────────────────────────────────────────────────────── A complete `background` value, so a theme can be an image or a gradient rather than a colour. */ - --page-bg: hsl(0 0% 100%); + --page-bg: #ffffff; --pixel-grid-color: rgba(20, 83, 45, 0.1); /* ── The wallpaper, the header and the mark ─────────────────────────────────────────────────── @@ -82,7 +82,7 @@ --panel-blur: 24px; --panel-radius: 0.75rem; --panel-header-bg: rgba(0, 0, 0, 0.05); - --panel-header-fg: hsl(var(--foreground)); + --panel-header-fg: var(--foreground); --panel-accent: var(--brand-primary); --panel-grid-color: rgba(20, 83, 45, 0.08); @@ -94,70 +94,70 @@ --card-grid-color: rgba(20, 83, 45, 0.08); /* ── Semantic colour (HSL triplets — see base.css for why they are unwrapped) ─────────────────── */ - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --background: #ffffff; + --foreground: #020817; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: #ffffff; + --card-foreground: #020817; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover: #ffffff; + --popover-foreground: #020817; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: #0f172a; + --primary-foreground: #f8fafc; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: #f1f5f9; + --secondary-foreground: #0f172a; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: #f1f5f9; + --muted-foreground: #64748b; - --accent: 211 74.77% 45.58%; - --accent-foreground: 0 0% 100%; + --accent: #1d71cb; + --accent-foreground: #ffffff; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: #ef4444; + --destructive-foreground: #f8fafc; - --success: 142.1 76.2% 36.3%; - --success-foreground: 355.7 100% 97.3%; + --success: #16a34a; + --success-foreground: #fff1f2; - --brand: 145 79% 38%; - --brand-foreground: 0 0% 100%; - --brand-muted: 145 40% 90%; + --brand: #14ad54; + --brand-foreground: #ffffff; + --brand-muted: #dbf0e4; - --warning: 43 96% 56%; - --warning-foreground: 0 0% 0%; + --warning: #fbbd23; + --warning-foreground: #000000; /* The fifth state tone. "In progress" and "merged" are neither good news nor bad, and painting them with --success makes a merged PR and an open one look like the same thing. Violet because it has to stay legible next to success green and destructive red at pill size. */ - --info: 263 70% 50%; - --info-foreground: 0 0% 100%; + --info: #6b26d9; + --info-foreground: #ffffff; /* Busy: running, queued, in flight. See base.css. */ - --busy: 43 96% 50%; - --busy-foreground: 20 14% 8%; - --busy-ink: 32 81% 29%; + --busy: #fab505; + --busy-foreground: #171312; + --busy-ink: #864e0e; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: #e2e8f0; + --input: #e2e8f0; + --ring: #020817; - --cta: 211 74.77% 45.58%; - --cta-foreground: 0 0% 100%; - --ctahover: 211 91.53% 34.82%; + --cta: #1d71cb; + --cta-foreground: #ffffff; + --ctahover: #0856aa; - --naturegreen: 145 79% 38%; - --naturegreen-foreground: 0 0% 100%; + --naturegreen: #14ad54; + --naturegreen-foreground: #ffffff; --radius: 0.5rem; /* ── Data visualisation and status ────────────────────────────────────────────────────────────── */ - --chart-primary: 211 82% 64%; - --chart-secondary: 211 82% 84%; - --status-success: 142 76% 36%; - --status-warning: 43 96% 56%; - --status-in-progress: 211 82% 64%; + --chart-primary: #58a1ee; + --chart-secondary: #b5d5f8; + --status-success: #16a249; + --status-warning: #fbbd23; + --status-in-progress: #58a1ee; /* ── Chrome that used hardcoded greys ───────────────────────────────────────────────────────── Scrollbars were `#444`/`#555`/`rgb(128 128 128 / .3)` in the stylesheet, which meant the code editor diff --git a/src/workspaces/theme/css/themes/plain.css b/src/workspaces/theme/css/themes/plain.css index 4232848b..d7a81e75 100644 --- a/src/workspaces/theme/css/themes/plain.css +++ b/src/workspaces/theme/css/themes/plain.css @@ -93,69 +93,69 @@ Greys only. The state tones (success/warning/destructive/info) are greys too, which is wrong for a real theme and right for this one: it proves the app is asking for a token rather than reaching for a green it likes. A theme that wanted real state colour would simply set them. */ - --background: 0 0% 100%; - --foreground: 0 0% 0%; + --background: #ffffff; + --foreground: #000000; - --card: 0 0% 100%; - --card-foreground: 0 0% 0%; + --card: #ffffff; + --card-foreground: #000000; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 0%; + --popover: #ffffff; + --popover-foreground: #000000; - --primary: 0 0% 0%; - --primary-foreground: 0 0% 100%; + --primary: #000000; + --primary-foreground: #ffffff; - --secondary: 0 0% 95%; - --secondary-foreground: 0 0% 0%; + --secondary: #f2f2f2; + --secondary-foreground: #000000; - --muted: 0 0% 95%; - --muted-foreground: 0 0% 35%; + --muted: #f2f2f2; + --muted-foreground: #595959; - --accent: 0 0% 90%; - --accent-foreground: 0 0% 0%; + --accent: #e6e6e6; + --accent-foreground: #000000; - --destructive: 0 0% 25%; - --destructive-foreground: 0 0% 100%; + --destructive: #404040; + --destructive-foreground: #ffffff; - --success: 0 0% 25%; - --success-foreground: 0 0% 100%; + --success: #404040; + --success-foreground: #ffffff; - --brand: 0 0% 0%; - --brand-foreground: 0 0% 100%; - --brand-muted: 0 0% 90%; + --brand: #000000; + --brand-foreground: #ffffff; + --brand-muted: #e6e6e6; - --warning: 0 0% 45%; - --warning-foreground: 0 0% 100%; + --warning: #737373; + --warning-foreground: #ffffff; - --info: 0 0% 35%; - --info-foreground: 0 0% 100%; + --info: #595959; + --info-foreground: #ffffff; /* Busy: running, queued, in flight. See base.css. */ - --busy: 0 0% 45%; - --busy-foreground: 0 0% 100%; - --busy-ink: 0 0% 25%; + --busy: #737373; + --busy-foreground: #ffffff; + --busy-ink: #404040; - --border: 0 0% 0%; - --input: 0 0% 0%; - --ring: 0 0% 0%; + --border: #000000; + --input: #000000; + --ring: #000000; - --cta: 0 0% 0%; - --cta-foreground: 0 0% 100%; - --ctahover: 0 0% 20%; + --cta: #000000; + --cta-foreground: #ffffff; + --ctahover: #333333; - --naturegreen: 0 0% 0%; - --naturegreen-foreground: 0 0% 100%; + --naturegreen: #000000; + --naturegreen-foreground: #ffffff; /* Square everything. Rounded corners surviving under Plain means a hardcoded `rounded-xl`, which is a shape leak rather than a colour one — worth seeing too. */ --radius: 0; /* ── Data visualisation and status ────────────────────────────────────────────────────────────── */ - --chart-primary: 0 0% 20%; - --chart-secondary: 0 0% 60%; - --status-success: 0 0% 25%; - --status-warning: 0 0% 45%; - --status-in-progress: 0 0% 35%; + --chart-primary: #333333; + --chart-secondary: #999999; + --status-success: #404040; + --status-warning: #737373; + --status-in-progress: #595959; /* ── Chrome ───────────────────────────────────────────────────────────────────────────────────── */ --scrollbar-thumb: #000000;