fix(theme): make sheets and dialogs track the active theme (#1315)

* fix(theme): make sheets and dialogs track the active theme

Portaled panels (scan-history and other sheets, dialogs, confirm modals)
only partially followed the theme: their popover surface was a flat per-theme
literal, so OLED mode never reached true black inside them and the contrast
control did not move their fill. The accent stopped at the thin rail because
the ambient glow was painted only on the main app shell, never inside a
portaled panel, and the overlay scrim was a hardcoded black.

- Derive the popover surface from per-theme lightness and alpha plus the
  contrast spread, the same way the page background is derived, so every
  floating surface tones with the active theme (including OLED true black) and
  deepens with contrast. Dim and Light are unchanged at the default contrast.
- Add an accent wash to sheets and large dialogs so the chosen accent reads
  inside them, matching the page. Small dropdowns and tooltips keep the themed
  tone without the wash.
- Replace the hardcoded modal and sheet scrim with a theme-aware token that
  softens to a light dim in the Light theme.

Tested across Dim, OLED, and Light with multiple accents and contrast levels.

* fix(theme): keep the command palette token-only (no accent wash)

The command palette renders through DialogContent, so it picked up the accent
wash meant for content sheets and dialogs. It is a command menu, so it should
stay token-only like dropdowns and tooltips. Add an optional panelGlow flag to
DialogContent (on by default, so every other dialog is unchanged) and turn it
off for the palette.
This commit is contained in:
Anso
2026-06-05 17:34:38 -04:00
committed by GitHub
parent 5d508b9416
commit 622af7e0b3
5 changed files with 39 additions and 10 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'fixed inset-0 z-50 bg-[var(--scrim)] backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
@@ -32,7 +32,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-glass-border bg-popover p-6 shadow-lg backdrop-blur-[10px] backdrop-saturate-[1.15] sm:rounded-lg',
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-glass-border bg-popover panel-glow p-6 shadow-lg backdrop-blur-[10px] backdrop-saturate-[1.15] sm:rounded-lg',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
className
)}
+3 -1
View File
@@ -28,7 +28,9 @@ Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, shouldFilter, ...props }: DialogProps & { shouldFilter?: boolean }) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
{/* The command palette is a menu/list surface, so it stays token-only like
dropdowns and tooltips: no accent wash behind the results. */}
<DialogContent panelGlow={false} className="overflow-hidden p-0">
<Command shouldFilter={shouldFilter} className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
+4 -3
View File
@@ -20,14 +20,15 @@ import {
// while delegating animation to animate-ui's spring-based dialog
const DialogContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof AnimateDialogContent> & { showClose?: boolean }
>(({ className, children, showClose = true, ...props }, ref) => (
React.ComponentProps<typeof AnimateDialogContent> & { showClose?: boolean; panelGlow?: boolean }
>(({ className, children, showClose = true, panelGlow = true, ...props }, ref) => (
<DialogPortal>
<AnimateDialogOverlay className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm" />
<AnimateDialogOverlay className="fixed inset-0 z-50 bg-[var(--scrim)] backdrop-blur-sm" />
<AnimateDialogContent
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-glass-border bg-popover p-6 shadow-lg backdrop-blur-[10px] backdrop-saturate-[1.15] sm:rounded-lg',
panelGlow && 'panel-glow',
className
)}
{...props}
+2 -2
View File
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-[var(--scrim)] backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-popover p-6 shadow-lg backdrop-blur-[10px] backdrop-saturate-[1.15] border-glass-border transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
"fixed z-50 gap-4 bg-popover panel-glow p-6 shadow-lg backdrop-blur-[10px] backdrop-saturate-[1.15] border-glass-border transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
+28 -2
View File
@@ -38,6 +38,16 @@
--band: oklch(var(--band-l) 0 0);
--well: oklch(var(--well-l) 0 0);
/* ---- Computed popover / floating-panel fill. Mirrors --background's clamp +
--ink-dir + contrast spread off a per-theme raw lightness + alpha, so every
floating surface (sheets, dialogs, dropdowns, menus) tones with the active
theme (incl. OLED true-black) and deepens with --contrast, instead of being
a flat per-theme literal that stranded OLED at the Dim value. ---- */
--popover: oklch(clamp(0, var(--pop-l) - var(--ink-dir) * var(--contrast) * 0.03, 1) 0 0 / var(--pop-a));
/* Overlay scrim behind modals & sheets (theme-aware; softened in Light). */
--scrim: oklch(0 0 0 / 0.45);
/* ---- Computed borders (opaque grey, directionally lit). --border-boost and
--contrast both lift every hairline through one expression; clamp keeps
them in gamut and below pure white so the lit edge never merges into a
@@ -88,11 +98,11 @@
--val: 0.970; --title: 0.800; --sub: 0.605; --icon: 0.480; --ink-dir: 1;
--accent-l: 0.745; --brand-fg: oklch(0.15 0 0);
--glow-l: 0.55; --glow-c: 0.085;
--pop-l: 0.14; --pop-a: 0.82;
/* Dark non-derivable tokens (shared by Dim & OLED) */
--foreground: oklch(0.94 0 0);
--card-foreground: oklch(0.94 0 0);
--popover: oklch(0.14 0 0 / 0.82);
--popover-foreground: oklch(0.94 0 0);
--primary: oklch(0.98 0 0);
--primary-foreground: oklch(0.10 0 0);
@@ -264,6 +274,7 @@
--val: 0.950; --title: 0.780; --sub: 0.575; --icon: 0.445; --ink-dir: 1;
--accent-l: 0.745; --brand-fg: oklch(0.11 0 0);
--glow-l: 0.46; --glow-c: 0.085;
--pop-l: 0.06; --pop-a: 0.86;
}
/* ─────────────────────────────────────────────────────────────
@@ -280,11 +291,12 @@
--val: 0.190; --title: 0.400; --sub: 0.500; --icon: 0.620; --ink-dir: -1;
--accent-l: 0.525; --brand-fg: oklch(0.99 0 0);
--glow-l: 0.90; --glow-c: 0.040;
--pop-l: 1.0; --pop-a: 0.92;
--scrim: oklch(0.22 0 0 / 0.32);
/* Light non-derivable tokens */
--foreground: oklch(0.15 0 0);
--card-foreground: oklch(0.15 0 0);
--popover: oklch(1 0 0 / 0.92);
--popover-foreground: oklch(0.15 0 0);
--primary: oklch(0.15 0 0);
--primary-foreground: oklch(1 0 0);
@@ -507,6 +519,20 @@
background-attachment: fixed;
}
/* Accent wash for floating panels (sheets & large dialogs). Layers the ambient
accent glow as a background image over the panel's own --popover fill, so a
portaled surface carries the same accent personality .app-canvas gives the
page. Image-only (the popover color stays the base) to keep the glass
translucency and backdrop blur; anchored to the panel top so the translucent
header bands let it bleed through. */
.panel-glow {
background-image: radial-gradient(
ellipse 130% 42% at 50% 0%,
var(--glow-color),
transparent 64%
);
}
/* Density: compact mode compresses row/cell padding by ~50%. */
body.density-compact {
--density-scale: 0.75;