Skip to content

feat(frontend): light theme (Settings → Display)#128

Merged
vidit-admin merged 4 commits into
mainfrom
claude/focused-kirch-ec8028
Jul 4, 2026
Merged

feat(frontend): light theme (Settings → Display)#128
vidit-admin merged 4 commits into
mainfrom
claude/focused-kirch-ec8028

Conversation

@vidit-admin

Copy link
Copy Markdown
Member

What

A light / dark toggle as a second display axis, alongside the existing accent palette. Dark stays the default; a manual Switch in Settings → Display flips it. Browser-local (localStorage key vidit:theme) like the palette, with no OS prefers-color-scheme follow (per the chosen scope).

It touches exactly the two surfaces asked for: the base neutral palette and the map background.

How

The mechanism mirrors the accent system, so there is no per-component churn:

  • Base palette: data-theme="light" on <html> remaps the Tailwind neutral-* scale to an inverted light ramp (mirrored about the neutral-500 midpoint) in globals.css. Every neutral-* utility, the scrollbar, and the MapLibre control chrome flip through the CSS variables. Dark carries no attribute (inert fallback to Tailwind defaults). The handful of hardcoded bg-[#0a0a0a] canvases move onto bg-neutral-950 so they theme too.
  • Map background: maplibre paint can't read CSS variables, so Map.tsx swaps the CARTO dark-matter basemap for its matched light counterpart positron off useTheme. The zoom-control glyphs stop inverting in light mode so they stay legible on the light chrome.
  • Accent untouched: the accent hue is a separate axis; the two compose (light + any of the 5 palettes).

Factored preference plumbing

Theme would have been a third near-duplicate of the palette / help-tooltip stores, which jscpd (1% cap) and the single-source rule reject. So the shared plumbing is factored:

Palette and help-hidden are refactored onto these; behaviour is unchanged (existing tests stay green). jscpd reports 0% for the typescript set.

A pre-paint inline script in the root layout applies the saved theme before first paint, so there is no dark flash on load.

Verification

  • tsc / eslint (0 errors) / knip / jscpd (typescript 0%) / palette:check (36/36) / vitest (160, incl. new theme.test.ts) all green.
  • Verified in the browser preview: the palette styleguide, login, and settings surfaces flip cohesively (backgrounds, text, cards, inputs, sidebar), the map loads the positron light basemap (confirmed in the network log), and the zoom controls render dark-on-light.

Docs

  • docs/design.md: Theme section documents the dark-default / light axis; Foundation notes the mirrored ramp; Map notes the positron swap.
  • planning/next.md: the "persist display prefs server-side" refactor row now reflects three browser-local prefs + the factored plumbing.
  • CHANGELOG.md: Unreleased → Added.

Note

The Switch knob inverts with the neutral scale, so it reads dark on the light track. Contrast stays good in both themes and states, but if you would rather it stay a fixed light knob, that is a one-line follow-up.

🤖 Generated with Claude Code

vidit-admin and others added 2 commits July 5, 2026 00:17
…e plumbing

Add a light/dark toggle as a second display axis alongside the accent
palette. Dark stays the default; a manual Switch flips it, browser-local
(localStorage `vidit:theme`) like the palette, with no OS prefers-color
-scheme follow.

The mechanism mirrors the accent system: `data-theme="light"` on <html>
remaps the Tailwind neutral-* scale to an inverted light ramp (mirrored
about the neutral-500 midpoint) in globals.css, so every neutral-* util,
the scrollbar, and the MapLibre control chrome flip with no per-component
change. Map.tsx swaps the CARTO dark-matter basemap for its matched light
counterpart positron off useTheme (maplibre paint can't read CSS vars).
The accent hue is untouched; the two axes compose.

Factor the shared client-preference plumbing so theme is not a third
near-duplicate store: attributePreference.ts owns the localStorage to
<html data-*> reflection (palette + theme), useClientPreference.ts the
useSyncExternalStore wiring (palette + theme + help-hidden). Keeps jscpd
at 0% for the typescript set.

A pre-paint inline script in the root layout applies the saved theme
before first paint to avoid a dark flash.

tsc / eslint / knip / jscpd / palette:check / vitest (160) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
The logged-out Sign-in rail item never took the active-row treatment, so
it stayed plain grey on the login page while every other nav entry lights
with ACCENT_SURFACE when its route is active. Give it the same active
branch (ACCENT_SURFACE on `pathname === "/login"`) and align its inactive
hover to neutral-100 like the rest of the rail, so it reads as selected
exactly like the other icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
@vidit-admin vidit-admin force-pushed the claude/focused-kirch-ec8028 branch from af7cbf2 to 62c9940 Compare July 4, 2026 22:20
The first light ramp was a mechanical inversion of the dark scale, so the
canvas came out near-white (#fafafa) with near-black text: a flat, harsh
glare on a large surface. Re-point the light `neutral-*` scale to a curated
soft ramp carrying a faint warmth instead: a soft warm grey canvas
(`neutral-950` = #eae8e4) with warm off-white cards (`neutral-900` =
#f7f5f1) floating on it, and dark-grey text (`neutral-100` = #232323, not
near-black), so the sidebar, cards, and panels read comfortably. The light
basemap (Positron ships a cool grey) gets a faint `sepia` on its canvas to
match the warm surfaces. This mirrors how the dark scale already avoids
pure black (#0a0a0a) and pure white text (#f5f5f5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
@vidit-admin vidit-admin force-pushed the claude/focused-kirch-ec8028 branch from efb9a3e to 0bc69e7 Compare July 4, 2026 22:48
- Semantic red / amber scales now flip in light mode (mirrored about their 500
  stop), so FORM_ERROR_BANNER / WARNING_CALLOUT / danger surfaces keep their
  meaning with dark text on a light tint instead of pale-on-pale.
- Set `color-scheme` per theme so native widgets (scrollbars, date / select
  popups) track the theme instead of the OS.
- Flip the selected map point's white halo to a dark ring on the light
  Positron basemap, where a white ring vanished.
- Memoise `useClientPreference`'s subscribe on `event` (useCallback) so
  useSyncExternalStore stops detaching and reattaching listeners every render.
- Replace two em dashes in globals.css comments with colons (house style).
- Clarify `attributePreference`'s `attribute` is the camelCase dataset key;
  correct theme.ts's stale "inverted ramp" wording and the sepia comment scope
  (it covers the whole canvas, markers included).

Known limitation, left as-is: the orange accent is owned by the palette switch,
so accent text (links, the success banner) reads a touch lighter in light mode;
theme-adjusting it needs a theme x palette reconciliation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
@vidit-admin vidit-admin merged commit 4f204e8 into main Jul 4, 2026
11 checks passed
@vidit-admin vidit-admin deleted the claude/focused-kirch-ec8028 branch July 4, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant