Dark / Light / System theme with selector for marketing/ and docs-site/ #73
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Add a theme system — dark, light, and system (follow OS) — plus a visible theme selector to
the two static surfaces:
marketing/(repo root)docs-site/(Next.js + fumadocs, static export)The main dashboard already has its own theme handling (system/dark/light + accent) — this issue
is about marketing and docs-site only.
Scope
CSS; check whether a toggle exists). Record current state in the plan before designing.
state), applied before first paint (no flash-of-wrong-theme / FOUC), defaults to
systemfor a new visitor.
marked; keyboard accessible;
prefers-reduced-motionrespected for any transition.docs-siteisoutput: export— no server routes, no dynamicrendering hooks that break the export. Verify with the site's build command.
genuinely reusable keep it simple — two small self-contained implementations are acceptable
if sharing would add build machinery (say which you chose and why).
Gates
docs-site: its build (bun build→ static export) must succeed, plus whatever checks existin that package.
marketing: its own build/lint command (readmarketing/package.jsonand run what exists).persists across reload;
systemfollows an OS change.Refs #<N>, no closing keywords.Claimed — branch
issue/73-theme-selector. Doing: dark/light/system theme with a selector for marketing/ and docs-site/ (audit done: marketing ships a dark token ladder with no toggle; docs-site has fumadocs+next-themes but a 2-state toggle).Plan — issue #73: dark/light/system theme + selector for marketing/ and docs-site/
Branch
issue/73-theme-selector, worktree/home/eduardez/workspace/gotoi-wt/issue-73.Restated goal
Give both static GTM surfaces a three-state theme — light, dark, system (follow OS) — with the
choice persisted in
localStorage, applied before first paint so there is no flash of the wrong theme,and exposed through a small visible, keyboard-accessible selector in each site's header/nav. "Done" for the
operator: open either site on a fresh browser in dark OS mode → the page is dark on the first frame; click
Light → it turns light and stays light after a reload; pick System → it follows a later OS theme change.
No server state (both sites are static exports), no new dependencies, one commit per surface.
Audit results
marketing/— tokens ship, no toggle, no initmarketing/src/app.css:163-213— a complete dark token ladder under.dark, html[data-theme="dark"],and it already sets
color-scheme: dark;@custom-variant dark (&:is(.dark *, [data-theme="dark"] *))atline 14.
@layer base html { color-scheme: light }(line 217) is the light default; the unlayered.darkrule outranks it, so
color-schemeis already theme-correct in both directions. Reduced-motion block atlines 618-647.
marketing/src/app.html:8-9—<meta name="theme-color" content="#fbfcfe">and<meta name="color-scheme" content="light">, both light-only. The only script is a.jshydrationwatchdog (lines 10-17). No pre-paint theme script, no storage key, no toggle.
marketing/src/lib/sections/Nav.svelte— the floating nav pill: desktop section links,ml-autogroupwith the "Try Gotoi" CTA and a
md:hiddenmenu button, plus a mobile panel (#mobile-nav).theme.ts/theme store inmarketing/src/lib. Hardcoded light hexes: none found inmarketing/src/**/*.svelte(theclass="dark"product mocks invisuals/ConsoleWindow.svelteandsections/AiDeskDemo.svelteare intentional dark islands inside a light page — left alone).dashboard/src/lib/theme.ts(ThemePreference = 'system'|'light'|'dark',THEME_STORAGE_KEY = 'gotoi-theme',parseThemePreference,resolveTheme,applyResolvedTheme) andthe pre-paint block in
dashboard/src/app.html(readsgotoi-theme, defaults tosystem, computes darkfrom
matchMedia('(prefers-color-scheme: dark)'), toggles.dark+data-theme, wrapped in try/catch).Labels Light / Dark / System.
docs-site/— full dark support, but only a 2-state toggledocs-site/app/globals.css:71-90— complete dark ladder (.dark:root, .dark) mapping--gotoi-*ontofumadocs'
--color-fd-*. Nothing missing.docs-site/app/providers.tsx—RootProvider(fumadocs) with search options only. fumadocs'RootProvideralready wrapsnext-themeswithattribute:"class",defaultTheme:"system",enableSystem:true,disableTransitionOnChange:true(fumadocs-ui/dist/provider/base.js). Sopersistence, the pre-paint inline script, and the "system" default are already in place —
docs-site/app/layout.tsxcarriessuppressHydrationWarningfor exactly that script.docs-site/lib/layout.shared.tsx— exportsbaseOptions: BaseLayoutProps(spread intoDocsLayout),without
themeSwitch, so fumadocs defaults tomode: "light-dark"→ the toggle rendered in the docssidebar and mobile header is a two-state Sun/Moon button (
aria-label="Toggle Theme", verified present inout/docs/*/index.html).fumadocs-ui/dist/layouts/shared/slots/theme-switch.jsswitches to thethree-button Light/Dark/System variant when
mode === 'light-dark-system', with per-buttonaria-labels.docs-site/app/page.tsx:28-47— the landing page has its own<header>; it is not a fumadocs layout,so it currently has no theme control at all.
output: "export"(docs-site/next.config.mjs) — no server routes; nothing planned needs one.Conclusion: docs-site needs configuration + one placement (reuse the installed control). marketing needs
a real, small implementation (pre-paint init + selector + the pure resolve/apply helpers).
UC decision
No UC is authored. The catalog is three domains —
middleware(which also hosts the OP cases),frontend,agent— anddocs/use-cases/domains.mdcontains no marketing/docs-site domain. The only casesthat mention
docs-site/(UC-OP-26compose deploy,UC-OP-27CI publish) citedocs-site/Dockerfileandtreat the site as a deployed artifact; theming a static GTM page is not their story. Authoring a new UC
here would invent a domain and a structure the repo does not have, which AGENTS.md forbids. Per the issue
prompt's own instruction ("If no UC domain covers marketing/docs-site and authoring one would be inventing
structure, say so explicitly"), I will state this in the plan and the report, cite
UC-OP-26/UC-OP-27inthe report only as the nearest existing references, and change no file under
docs/use-cases/.python3 docs/use-cases/_check.pyis therefore expected to stay green as a no-op gate.Scope
Files to change — exactly these:
src/app.htmlgotoi-theme, defaultsystem,.dark+data-theme); theme-color metas split byprefers-color-schemesrc/lib/theme.tsThemePreference,THEME_STORAGE_KEY,parseThemePreference,resolveTheme,applyResolvedThemesrc/lib/theme.test.tssrc/lib/components/ThemeToggle.svelteButton+@lucide/svelteicons,aria-pressed, storage +matchMedialistenersrc/lib/sections/Nav.svelteLastlib/layout.shared.tsxthemeSwitch: { mode: "light-dark-system" }inbaseOptions, bumpLastapp/providers.tsxLastapp/page.tsxThemeSwitch(3-state) in the landing header, bumpLastapp/globals.cssprefers-reduced-motionguard for::view-transition-*(fumadocs' switch usesstartViewTransition), bumpLastExplicitly not touched:
dashboard/,middleware/,agent/,plans/,deploy/,infra/,scripts/,docs/use-cases/,docs-site/content/,marketing/src/app.css(its dark ladder andcolor-schemeare already correct in both directions), and the intentionalclass="dark"product mocks inmarketing/src/lib/visuals/andmarketing/src/lib/sections/.Sharing decision: two self-contained per-site implementations, as the issue permits. docs-site reuses
fumadocs' own control (zero new code beyond config); marketing gets one ~60-line component. A shared package
would add build machinery for ~60 lines.
Ordered tasks
src/lib/theme.tsandsrc/lib/theme.test.ts.Verify:
cd marketing && bun run test.src/lib/components/ThemeToggle.svelte; mount it insrc/lib/sections/Nav.svelte(desktop row before the CTA, plus a row in the mobile panel).Verify:
cd marketing && bun run check.src/app.html.Verify:
cd marketing && bun run build && bun run check, thenrg -c 'gotoi-theme' marketing/build/index.html(the string must survive into the exported HTML).
Commit:
marketing: dark/light/system theme with a nav selector and pre-paint init Refs #73themeSwitchmode inlib/layout.shared.tsx, comment fix inapp/providers.tsx, switch inapp/page.tsx's header, view-transition reduced-motion guard inapp/globals.css.Verify:
cd docs-site && bun run build, thenrg -o 'aria-label="(Light|Dark|System)"' docs-site/out/docs/index.html | sort -u(three buttons present)and
rg -o 'aria-label="(Light|Dark|System)"' docs-site/out/index.html | sort -u(landing too).Commit:
docs-site: three-state theme switch, system-following, reduced-motion safe Refs #73python3 docs/use-cases/_check.py,python3 scripts/check-ai-headers.py,rg -n 'Showing lines' --glob '!node_modules' --glob '!.git'(must be empty), plus a finalcd marketing && bun run build && bun run checkandcd docs-site && bun run build./home/eduardez/workspace/gotoi-wt/meta/issue-73.report.mdand issue #73.Open questions, with the assumption I proceed on
ml-autogroup) and as a row in the mobile panel. A separate floating button would compete with thepill's chrome.
aria-pressedeach, all tabbable,native Enter/Space). It matches fumadocs' three-button variant and the dashboard's Light/Dark/System
names, and it needs no roving-tabindex code that a
role="radiogroup"would.own header and no fumadocs layout, so without it the site root would have no theme control.
theme-colorin marketing. The dark chrome colour is wired with a secondmedia-scoped meta tag(follows the OS) rather than JS, so no extra code path can drift from the buttons.
parse,resolve,apply) insrc/lib/theme.tssobun run testcan cover them in the node environment;storage read/write and the
matchMedialistener live in the component, guarded byonMount.Risk and boring fallback
systemas active beforeonMountreads storage. Svelte 5 hydrates beforeonMountruns, so the correction is a post-hydrationupdate, not a mismatch; the page colours are already correct pre-paint from
app.html. Fallback: ifbun run checkor the build complains, move the "active" mark to adata-attribute applied in thesame
onMountand leave the button labels static.themeSwitchprop shape drift — ifmode: "light-dark-system"does not produce threebuttons after the build, fall back to
slots.themeSwitchwith an explicit<ThemeSwitch mode=... />from
fumadocs-ui/layouts/shared/slots/theme-switch, or, at worst, render the same imported componentin the landing header only and leave the sidebar at its default.
links + switch + CTA in one
flex items-center gap-4div (keepsjustify-between's two groups).RootProviderandThemeSwitchare"use client"and already ship in the current export.marketing/src/lib/components/ui/is out of scope but every new marketing fileI author is in scope, so all four new/edited marketing files and the four docs-site files carry the
five-field header with a bumped
Last.DONE
Commits:
1fdc243marketing: dark/light/system theme with a nav selector and pre-paint init1033abddocs-site: three-state theme switch with system followingGates:
Manual check (headless Chromium over both local static artifacts): fresh visitor with OS dark paints dark before first paint on both sites; clicking Light persists across reload on both; with System selected a synthetic OS colour-scheme flip re-renders both sites with no reload. Keyboard: marketing's three buttons are tabbable native buttons and Enter selects. Reduced motion: docs-site's view-transition cross-fade is pinned off under prefers-reduced-motion (rule present in the export).
Findings worth noting: docs-site already had a two-state toggle — the change there is mode light-dark-system plus the landing page, which had no control at all. No UC authored: the catalog has no marketing/docs-site domain and UC-OP-26/27 cover docs-site only as a deployed artifact. Storage keys differ by stack (next-themes 'theme' vs 'gotoi-theme'); say the word if you want them unified.
Report: /home/eduardez/workspace/gotoi-wt/meta/issue-73.report.md
REVIEW: CLEAN — findings: (1) plan scope fidelity exact — 9 files, none in/out of scope, 2 commits both 'Refs #73', no closing keywords; (2) repo-law checkers all green on re-run (_check.py, check-ai-headers.py 576 ok, check-type-mirror.py, 'Showing lines' hits are only the rule's own docs); (3) report claims reproduced — marketing check 451 FILES 0 ERRORS, bun test 17/17, exports contain gotoi-theme pre-paint + Light/Dark/System aria-labels + reduced-motion view-transition rule; (4) behavior verified live in headless Chromium against marketing/build and docs-site/out: fresh load OS dark -> dark, Light persists across reload, System follows OS flips on both sites, active state marked (aria-pressed / bg-fd-accent); (5) no test deleted/skipped/weakened; observation: docs-site storage key is next-themes 'theme' vs marketing/dashboard 'gotoi-theme' — semantics identical, implementer flagged it for a human, issue did not require key parity; UNFIXED: none
DONE — PR #77 opened against agent-develop: #77 | review: CLEAN (independent reviewer, 9router/reviewer) | gates (orchestrator re-run on final tree): docs-site build PASS, marketing build PASS, marketing check PASS, marketing test PASS (17/17), _check.py PASS, ai-headers 576 ok, type-mirror PASS, 'Showing lines' clean. Issue stays open — operator merges.