From 8d8ef7e2f1b84257384cf5165fdbe4b1300d39ad Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:30:59 +0200 Subject: [PATCH] fix: unreadable native 's option list itself, so the bg-muted / text-foreground classes on the control never reach it. Themes whose --color-muted is translucent (Aurora Glass dark uses rgba(255,255,255,0.05)) composited to a light popup with near-white text, leaving every unselected option almost invisible. Pin option background/colour to the popover tokens, which every built-in theme defines as an opaque surface, and set color-scheme: dark on selects under .dark so Safari (which ignores option colours) renders a dark popup too. Fixes all ~40 native selects at once. Adds a stylesheet test that locks the rules and asserts every built-in theme's --color-popover stays opaque in each variant. --- app/globals.css | 16 +++++ lib/__tests__/native-select-options.test.ts | 77 +++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 lib/__tests__/native-select-options.test.ts diff --git a/app/globals.css b/app/globals.css index 76d6f1522..a3a4d3ab3 100644 --- a/app/globals.css +++ b/app/globals.css @@ -187,6 +187,22 @@ body { "calt" 1; } +/* Native 's option list itself; the Tailwind + * classes on the control (bg-muted / text-foreground) never reach it. It only + * honours `color-scheme` and an explicit option background/colour. Themes with + * a translucent --color-muted (Aurora Glass dark) therefore composited to a + * light popup with near-white text (#999). globals.css pins options to the + * popover tokens, so every theme must define those as an opaque surface. + * + * jsdom does not render native popups, so lock the stylesheet rules. + */ +const css = readFileSync(path.join(process.cwd(), 'app', 'globals.css'), 'utf8'); + +/** Return the body of the first top-level `selector { ... }` rule. */ +function ruleBody(selector: string): string { + const idx = css.indexOf('\n' + selector + ' {'); + expect(idx, 'no `' + selector + '` rule in globals.css').toBeGreaterThan(-1); + const open = css.indexOf('{', idx); + return css.slice(open + 1, css.indexOf('}', open)); +} + +/** True when a CSS colour value has no alpha channel below 1. */ +function isOpaque(value: string): boolean { + const v = value.trim().toLowerCase(); + if (v === 'transparent') return false; + if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/.test(v)) return true; + if (/^#[0-9a-f]{4}$/.test(v)) return v.endsWith('f'); + if (/^#[0-9a-f]{8}$/.test(v)) return v.endsWith('ff'); + const fn = v.match(/^(rgb|hsl)a?\((.*)\)$/); + if (fn) { + const parts = fn[2].split(/[\s,/]+/).filter(Boolean); + if (parts.length < 4) return true; + const alpha = parts[3]; + return alpha.endsWith('%') ? parseFloat(alpha) >= 100 : parseFloat(alpha) >= 1; + } + return false; +} + +/** Concrete `--color-popover` values, skipping the `@theme` alias `var(--color-popover)`. */ +function popoverValues(source: string): string[] { + return [...source.matchAll(/--color-popover:\s*([^;]+);/g)] + .map((m) => m[1].trim()) + .filter((v) => !v.startsWith('var(')); +} + +describe('native