feat(frontend): light theme (Settings → Display)#128
Merged
Conversation
…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>
af7cbf2 to
62c9940
Compare
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>
efb9a3e to
0bc69e7
Compare
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A light / dark toggle as a second display axis, alongside the existing accent palette. Dark stays the default; a manual
Switchin Settings → Display flips it. Browser-local (localStoragekeyvidit:theme) like the palette, with no OSprefers-color-schemefollow (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:
data-theme="light"on<html>remaps the Tailwindneutral-*scale to an inverted light ramp (mirrored about theneutral-500midpoint) inglobals.css. Everyneutral-*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 hardcodedbg-[#0a0a0a]canvases move ontobg-neutral-950so they theme too.Map.tsxswaps the CARTOdark-matterbasemap for its matched light counterpartpositronoffuseTheme. The zoom-control glyphs stop inverting in light mode so they stay legible on the light chrome.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:attributePreference.ts: thelocalStorageto<html data-*>reflection (palette + theme).useClientPreference.ts: theuseSyncExternalStoresubscription (palette + theme + help-hidden).Palette and help-hidden are refactored onto these; behaviour is unchanged (existing tests stay green).
jscpdreports 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. newtheme.test.ts) all green.positronlight 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
Switchknob 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