panel chrome stops being black
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -14,7 +14,7 @@ export const AppPicker = ({ registry, onSelect }: AppPickerProps) => {
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 rounded-full border border-duck-teal/25 bg-background/80 backdrop-blur-sm px-3 py-1.5 text-duck-teal hover:border-duck-teal/40 hover:bg-background/90 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-1.5 rounded-full border border-[color-mix(in_srgb,var(--panel-accent)_25%,transparent)] bg-background/80 backdrop-blur-sm px-3 py-1.5 text-[var(--panel-accent)] hover:border-[color-mix(in_srgb,var(--panel-accent)_40%,transparent)] hover:bg-background/90 transition-colors cursor-pointer"
|
||||
onClick={() => onSelect(key)}
|
||||
>
|
||||
<entry.icon className="h-3.5 w-3.5 shrink-0" />
|
||||
|
||||
@@ -89,12 +89,12 @@ const SwapOverlay = ({ panelId }: { panelId: string }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-20 flex items-center justify-center bg-duck-teal/10 hover:bg-duck-teal/20 transition-colors cursor-pointer"
|
||||
className="absolute inset-0 z-20 flex items-center justify-center bg-[color-mix(in_srgb,var(--panel-accent)_10%,transparent)] hover:bg-[color-mix(in_srgb,var(--panel-accent)_20%,transparent)] transition-colors cursor-pointer"
|
||||
onClick={() => onSwap(swapSourceId, panelId)}
|
||||
>
|
||||
<div className="flex items-center gap-2 rounded-lg bg-background/90 backdrop-blur-sm px-3 py-2 shadow-lg border border-duck-teal/30">
|
||||
<ArrowLeftRight className="h-4 w-4 text-duck-teal" />
|
||||
<span className="text-sm font-medium text-duck-teal">Swap here</span>
|
||||
<div className="flex items-center gap-2 rounded-lg bg-background/90 backdrop-blur-sm px-3 py-2 shadow-lg border border-[color-mix(in_srgb,var(--panel-accent)_30%,transparent)]">
|
||||
<ArrowLeftRight className="h-4 w-4 text-[var(--panel-accent)]" />
|
||||
<span className="text-sm font-medium text-[var(--panel-accent)]">Swap here</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -106,12 +106,12 @@ const SwapSourceIndicator = ({ panelId }: { panelId: string }) => {
|
||||
if (swapSourceId !== panelId) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-20 pointer-events-none ring-2 ring-inset ring-duck-teal/50 rounded-lg">
|
||||
<div className="absolute inset-0 z-20 pointer-events-none ring-2 ring-inset ring-[color-mix(in_srgb,var(--panel-accent)_50%,transparent)] rounded-lg">
|
||||
<div className="absolute top-2 left-2 pointer-events-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSwapSourceId(null)}
|
||||
className="flex items-center gap-1.5 rounded-md bg-duck-teal/90 px-2.5 py-1 text-xs font-medium text-white shadow cursor-pointer hover:bg-duck-teal"
|
||||
className="flex items-center gap-1.5 rounded-md bg-[color-mix(in_srgb,var(--panel-accent)_90%,transparent)] px-2.5 py-1 text-xs font-medium text-[var(--panel-accent-fg)] shadow cursor-pointer hover:bg-[var(--panel-accent)]"
|
||||
>
|
||||
<ArrowLeftRight className="h-3 w-3" />
|
||||
Swapping... click to cancel
|
||||
@@ -183,7 +183,7 @@ const FullscreenToggle = ({ panelId }: { panelId: string }) => {
|
||||
type="button"
|
||||
onClick={() => setFullscreenPanelId(isFullscreen ? null : panelId)}
|
||||
title={isFullscreen ? 'Exit full screen' : 'Full screen'}
|
||||
className={`cursor-pointer rounded p-1 transition-all hover:bg-black/10 focus-visible:opacity-100 ${isFullscreen ? '' : 'opacity-0 group-hover/header:opacity-100'}`}
|
||||
className={`cursor-pointer rounded p-1 transition-all hover:bg-[var(--panel-control-hover)] focus-visible:opacity-100 ${isFullscreen ? '' : 'opacity-0 group-hover/header:opacity-100'}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -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"
|
||||
>
|
||||
<ZoomOut className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -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)}%
|
||||
</button>
|
||||
@@ -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"
|
||||
>
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@@ -383,7 +383,7 @@ export const PanelSlot = ({
|
||||
if (!interactive || locked) {
|
||||
return (
|
||||
<div className="h-full w-full p-1">
|
||||
<div className="h-full w-full rounded-lg border-3 border-duck-teal/50" />
|
||||
<div className="h-full w-full rounded-lg border-3 border-[color-mix(in_srgb,var(--panel-accent)_50%,transparent)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -440,7 +440,7 @@ export const PanelSlot = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onMobileBack}
|
||||
className="flex items-center gap-0.5 text-xs font-medium text-black/70 hover:text-black transition-colors cursor-pointer shrink-0"
|
||||
className="flex items-center gap-0.5 text-xs font-medium text-[var(--panel-header-fg-dim)] hover:text-[var(--panel-header-fg)] transition-colors cursor-pointer shrink-0"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Back
|
||||
@@ -454,11 +454,14 @@ export const PanelSlot = ({
|
||||
const fullscreenToggle = interactive && !isMobile ? <FullscreenToggle panelId={panel.id} /> : null;
|
||||
|
||||
const headerContent = (
|
||||
<div className="group/header shrink-0 flex items-center gap-2 px-3 py-1.5 border-b border-black/10 text-black font-semibold">
|
||||
<div className="group/header shrink-0 flex items-center gap-2 px-3 py-1.5 border-b border-[var(--panel-header-border)] text-[var(--panel-header-fg)] font-semibold">
|
||||
{mobileBackButton}
|
||||
{header}
|
||||
{!mobileBackButton && onClose && (
|
||||
<button onClick={onClose} className="p-1 rounded hover:bg-black/10 transition-colors cursor-pointer">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-[var(--panel-control-hover)] transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user