From 94a55fe67995427a7e902847f92b8eebe600859b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 17 Aug 2026 01:04:08 +0000 Subject: [PATCH] panel chrome stops being black MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported against a dark theme: the panel header's title, zoom readout and controls were hardcoded `text-black`, `text-black/60`, `hover:bg-black/10` and `border-black/10`. Legible on Duck's pale glass, invisible on every palette theme — which is the whole class of bug the theme work exists to remove, sitting in the one component I moved between packages and did not re-read. Three derived tokens, so no theme has to think about them: --panel-header-fg-dim the header's ink at 60% (the zoom readout) --panel-header-border the header's ink at 15% (the rule under it) --panel-control-hover the header's ink at 12% (control hover) The swap, drop-target and app-picker chrome used `duck-teal` and `text-white` directly. Those now use `--panel-accent` and a new `--panel-accent-fg`, so a chip on the accent stays readable whatever the accent is. Verified across five themes rather than by reading: duck-dark ink=rgb(248,250,252) duck-light ink=rgb(2,8,23) rose-pine ink=rgb(224,222,244) gruvbox ink=rgb(235,219,178) catppuccin-latte ink=rgb(76,79,105) Two comments corrected while there: both asserted the chrome "is black throughout", which was true when written and is now the opposite of true. Co-Authored-By: Claude Opus 5 --- src/workspaces/theme/css/base.css | 10 ++++++ .../workspaces/src/react/AppPicker.tsx | 2 +- .../workspaces/src/react/PanelSlot.tsx | 35 ++++++++++--------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/workspaces/theme/css/base.css b/src/workspaces/theme/css/base.css index 18bdb1fd..3e5b8bb5 100644 --- a/src/workspaces/theme/css/base.css +++ b/src/workspaces/theme/css/base.css @@ -251,7 +251,17 @@ --panel-blur: 0px; --panel-header-bg: color-mix(in srgb, var(--muted) 60%, transparent); --panel-header-fg: var(--foreground); + /* + Panel chrome states. These were literal blacks in the component — `text-black/60` on the zoom + readout, `hover:bg-black/10` on every control, `border-black/10` under the header — which is legible + on Duck's pale glass and invisible on any dark theme. Derived from the header's own ink so they + follow whatever it is. + */ + --panel-header-fg-dim: color-mix(in srgb, var(--panel-header-fg) 60%, transparent); + --panel-header-border: color-mix(in srgb, var(--panel-header-fg) 15%, transparent); + --panel-control-hover: color-mix(in srgb, var(--panel-header-fg) 12%, transparent); --panel-accent: var(--primary); + --panel-accent-fg: var(--primary-foreground); --panel-grid-color: color-mix(in srgb, var(--border) 25%, transparent); /* The header */ diff --git a/src/workspaces/workspaces/src/react/AppPicker.tsx b/src/workspaces/workspaces/src/react/AppPicker.tsx index 8d350943..664fc0f0 100644 --- a/src/workspaces/workspaces/src/react/AppPicker.tsx +++ b/src/workspaces/workspaces/src/react/AppPicker.tsx @@ -14,7 +14,7 @@ export const AppPicker = ({ registry, onSelect }: AppPickerProps) => { @@ -217,7 +217,7 @@ const ZoomControls = ({ panelId, zoom }: { panelId: string; zoom: number }) => { onClick={() => onSetZoom(panelId, zoom - ZOOM_STEP)} disabled={zoom <= ZOOM_MIN} title="Zoom out" - className="cursor-pointer rounded p-1 opacity-0 transition-all group-hover/header:opacity-100 hover:bg-black/10 focus-visible:opacity-100 disabled:cursor-default disabled:opacity-0 disabled:group-hover/header:opacity-30" + className="cursor-pointer rounded p-1 opacity-0 transition-all group-hover/header:opacity-100 hover:bg-[var(--panel-control-hover)] focus-visible:opacity-100 disabled:cursor-default disabled:opacity-0 disabled:group-hover/header:opacity-30" > @@ -225,7 +225,7 @@ const ZoomControls = ({ panelId, zoom }: { panelId: string; zoom: number }) => { type="button" onClick={() => onSetZoom(panelId, 1)} title="Reset zoom" - className={`w-9 cursor-pointer rounded px-1 text-center text-[11px] tabular-nums text-black/60 transition-all hover:bg-black/10 hover:text-black ${atDefault ? 'opacity-0 group-hover/header:opacity-100' : ''}`} + className={`w-9 cursor-pointer rounded px-1 text-center text-[11px] tabular-nums text-[var(--panel-header-fg-dim)] transition-all hover:bg-[var(--panel-control-hover)] hover:text-[var(--panel-header-fg)] ${atDefault ? 'opacity-0 group-hover/header:opacity-100' : ''}`} > {Math.round(zoom * 100)}% @@ -234,7 +234,7 @@ const ZoomControls = ({ panelId, zoom }: { panelId: string; zoom: number }) => { onClick={() => onSetZoom(panelId, zoom + ZOOM_STEP)} disabled={zoom >= ZOOM_MAX} title="Zoom in" - className="cursor-pointer rounded p-1 opacity-0 transition-all group-hover/header:opacity-100 hover:bg-black/10 focus-visible:opacity-100 disabled:cursor-default disabled:opacity-0 disabled:group-hover/header:opacity-30" + className="cursor-pointer rounded p-1 opacity-0 transition-all group-hover/header:opacity-100 hover:bg-[var(--panel-control-hover)] focus-visible:opacity-100 disabled:cursor-default disabled:opacity-0 disabled:group-hover/header:opacity-30" > @@ -383,7 +383,7 @@ export const PanelSlot = ({ if (!interactive || locked) { return (
-
+
); } @@ -440,7 +440,7 @@ export const PanelSlot = ({ )} @@ -543,9 +546,9 @@ export const PanelSlot = ({ // is no app left to float above. // // Denser glass than a tiled panel, not a darker one. Panel chrome — header text, icons, the zoom - // control — is black throughout, so the old near-opaque dark fill left every one of them unreadable. + // control — takes the header's own ink, so a near-opaque dark fill left every one of them unreadable. // This is the nav Header's exact recipe (`rgba(255,255,255,0.25)` over a 0.35 border), which is the - // app's established "raised above the rest" surface and is already proven to carry black text. + // app's established "raised above the rest" surface. Its ink follows `--panel-header-fg` like the rest. // Fullscreen is DENSER glass, not darker: panel chrome is dark text throughout, and a heavy dark fill // made every icon unreadable. A panel with no header takes a harder edge, having nothing else to // separate it from what it sits on. Both were literal rgba pairs and are now the theme's to decide.