feat: auto OS-driven dark mode (prefers-color-scheme) #8

Merged
brendan merged 2 commits from feat/auto-dark-mode into main 2026-05-11 16:56:44 +00:00
Owner

Implements the cross-project automatic dark-mode feature for nanodrop (item 2026-05-11 in ~/features.md).

What changed

  • src/views/layout.ts — added <meta name="color-scheme" content="light dark"> to <head> so browser UA chrome (scrollbars, form-control popups, initial backdrop) matches the user's system theme before the stylesheet parses (prevents the white-flash on dark OS).
  • public/style.css — full refactor to CSS custom properties. Light palette under :root, dark palette under @media (prefers-color-scheme: dark). 17 semantic tokens (--bg, --bg-elevated, --bg-hover, --fg, --fg-muted, --fg-on-accent, --border-color, --border-subtle, --accent, --accent-hover, --danger, --danger-bg, --danger-fg, --input-bg, --input-bg-focus, --input-fg, --input-border). Removed the floating #fff5f5 literal — now var(--danger-bg).
  • tests/integration/style.test.ts (+3 assertions) — locks in the @media (prefers-color-scheme: dark) block, the presence of core --bg/--fg/--accent tokens, and the absence of hex literals outside :root/@media.
  • tests/integration/pages.test.ts (+1 assertion) — verifies the meta tag is emitted in served HTML.

No toggle UI. No localStorage. No cookie. No per-account preference. Pure media-query driven, per the user's explicit spec.

Second commit (refactor:) restores the var(--border) shorthand in 3 rules for consistency — byte-equivalent at the CSS cascade.

Verification

  • 130/130 vitest pass (+4 new dark-mode assertions).
  • npx tsc --noEmit clean.
  • npm run build clean.
  • iOS font-size rule preservedfont-size: 16px still on input[type=text|password|file] at public/style.css:184.
  • WCAG AA spot check — all key pairs clear ≥4.5:1 (tightest is --danger on --danger-bg at ≈4.85:1).

Security

None. Presentation-only diff: meta tag is a static string literal (no user-data interpolation), escHtml(title) preserved, no auth/IDOR/injection/XSS surface change, zero new process.env reads.

Environment

None. No new/renamed/removed env vars. Pure client-side prefers-color-scheme — no server-side configuration.

Implements the cross-project automatic dark-mode feature for nanodrop (item 2026-05-11 in ~/features.md). ## What changed - `src/views/layout.ts` — added `<meta name="color-scheme" content="light dark">` to <head> so browser UA chrome (scrollbars, form-control popups, initial backdrop) matches the user's system theme before the stylesheet parses (prevents the white-flash on dark OS). - `public/style.css` — full refactor to CSS custom properties. Light palette under `:root`, dark palette under `@media (prefers-color-scheme: dark)`. 17 semantic tokens (`--bg`, `--bg-elevated`, `--bg-hover`, `--fg`, `--fg-muted`, `--fg-on-accent`, `--border-color`, `--border-subtle`, `--accent`, `--accent-hover`, `--danger`, `--danger-bg`, `--danger-fg`, `--input-bg`, `--input-bg-focus`, `--input-fg`, `--input-border`). Removed the floating `#fff5f5` literal — now `var(--danger-bg)`. - `tests/integration/style.test.ts` (+3 assertions) — locks in the `@media (prefers-color-scheme: dark)` block, the presence of core `--bg/--fg/--accent` tokens, and the absence of hex literals outside `:root`/`@media`. - `tests/integration/pages.test.ts` (+1 assertion) — verifies the meta tag is emitted in served HTML. No toggle UI. No localStorage. No cookie. No per-account preference. Pure media-query driven, per the user's explicit spec. Second commit (`refactor:`) restores the `var(--border)` shorthand in 3 rules for consistency — byte-equivalent at the CSS cascade. ## Verification - **130/130 vitest** pass (+4 new dark-mode assertions). - **`npx tsc --noEmit`** clean. - **`npm run build`** clean. - **iOS font-size rule preserved** — `font-size: 16px` still on `input[type=text|password|file]` at `public/style.css:184`. - **WCAG AA spot check** — all key pairs clear ≥4.5:1 (tightest is `--danger` on `--danger-bg` at ≈4.85:1). ## Security None. Presentation-only diff: meta tag is a static string literal (no user-data interpolation), `escHtml(title)` preserved, no auth/IDOR/injection/XSS surface change, zero new `process.env` reads. ## Environment None. No new/renamed/removed env vars. Pure client-side `prefers-color-scheme` — no server-side configuration.
brendan added 2 commits 2026-05-11 16:56:28 +00:00
Refactor public/style.css to semantic CSS custom properties under :root
(light palette by default) and add a @media (prefers-color-scheme: dark)
override block that flips background/foreground/accent/danger tokens to
a soft near-black palette. The original --black/--white/--gray-*/--red
literal tokens and the lone #fff5f5 floating literal are gone — every
color now derives from a semantic token, so the dark override is the
only place that re-declares them.

Add <meta name="color-scheme" content="light dark"> to the shared
layout <head> so the browser renders its UA backdrop, native scrollbars,
and form-control popups in the user's preferred scheme before the
stylesheet parses — this is what avoids a white flash on initial paint
in dark mode.

No toggle, no JS, no preference persistence — the page follows the OS
setting directly via the media query. The 16px font-size rule on
text-entry inputs is preserved (still asserted by tests/integration
/style.test.ts).
The dark-mode refactor expanded `border: var(--border);` to
`1px solid var(--border-color);` in three rules (.share-box button,
th, button.copy-link). The rest of the file still uses the
var(--border) shorthand for the same value, so restore it for
consistency.

`--border` is defined as `1px solid var(--border-color)`, so the
substitution is byte-equivalent at the cascade level: same border
in light mode, same border in dark mode, identical to the prior
commit.

Behavior unchanged. 130/130 tests pass; tsc --noEmit clean.
brendan merged commit 3f8da8c12c into main 2026-05-11 16:56:44 +00:00
Sign in to join this conversation.