feat(web): unify tailwind theme with design tokens + light palette #216

Merged
code-lead merged 2 commits from boss/208 into main 2026-04-21 11:02:06 +00:00
Collaborator

Summary

  • Light palette wired in. apps/web/src/styles/tokens.css now carries both dark (Tokyo Night Storm, default at :root) and light (Tokyo Night Day, under :root[data-theme="light"]) palettes, plus a @media (prefers-color-scheme: light) fallback so OS preference wins when the operator hasn't toggled explicitly.
  • Missing font-size slots added. design/tokens.json gains caption = 10px and h3 = 14px, both mirrored into tokens.css as --ch-font-size-caption / --ch-font-size-h3. The @theme block in index.css now exposes every size as a first-class Tailwind utility (text-caption, text-meta, text-small, text-body, text-body-lg, text-h3, text-h2, text-h1, text-display) so the follow-up ~235 text-[Npx] literal migration has a stable target.
  • Mode switcher in the top bar. New <ThemeToggle> (tri-state auto → light → dark) persists to localStorage.ch-theme and mirrors onto documentElement.dataset.theme. An inline bootstrap script in apps/web/index.html reads the same key before React evaluates, so first paint lands on the correct palette — no FOUC.
  • Radius utilities unchangedrounded-compact, rounded-default, rounded-card, rounded-pill still resolve via @theme --radius-*.

Closes #208

Test plan

  • bun x turbo run typecheck — all 3 workspaces clean.
  • bun x biome check . / bun x biome format . — no diagnostics.
  • bun x turbo run test — 795 server + 187 web tests pass, including 7 new lib/theme tests and 3 new <ThemeToggle> tests covering cycle + persistence + initial-from-storage.
  • cd apps/web && bun run build — succeeds, no new warnings (pre-existing shiki chunk-size warnings only).
  • Visual smoke (operator): /monitor, /planner, /planner/board, /agents, /stats, /usage, /specs render in both palettes; toggle persists across reloads; OS preference respected in auto mode.

🤖 Generated with Claude Code

## Summary - **Light palette wired in.** `apps/web/src/styles/tokens.css` now carries both dark (Tokyo Night Storm, default at `:root`) and light (Tokyo Night Day, under `:root[data-theme="light"]`) palettes, plus a `@media (prefers-color-scheme: light)` fallback so OS preference wins when the operator hasn't toggled explicitly. - **Missing font-size slots added.** `design/tokens.json` gains `caption = 10px` and `h3 = 14px`, both mirrored into `tokens.css` as `--ch-font-size-caption` / `--ch-font-size-h3`. The `@theme` block in `index.css` now exposes every size as a first-class Tailwind utility (`text-caption`, `text-meta`, `text-small`, `text-body`, `text-body-lg`, `text-h3`, `text-h2`, `text-h1`, `text-display`) so the follow-up ~235 `text-[Npx]` literal migration has a stable target. - **Mode switcher in the top bar.** New `<ThemeToggle>` (tri-state `auto → light → dark`) persists to `localStorage.ch-theme` and mirrors onto `documentElement.dataset.theme`. An inline bootstrap script in `apps/web/index.html` reads the same key before React evaluates, so first paint lands on the correct palette — no FOUC. - **Radius utilities unchanged** — `rounded-compact`, `rounded-default`, `rounded-card`, `rounded-pill` still resolve via `@theme --radius-*`. Closes #208 ## Test plan - [x] `bun x turbo run typecheck` — all 3 workspaces clean. - [x] `bun x biome check .` / `bun x biome format .` — no diagnostics. - [x] `bun x turbo run test` — 795 server + 187 web tests pass, including 7 new `lib/theme` tests and 3 new `<ThemeToggle>` tests covering cycle + persistence + initial-from-storage. - [x] `cd apps/web && bun run build` — succeeds, no new warnings (pre-existing shiki chunk-size warnings only). - [ ] Visual smoke (operator): `/monitor`, `/planner`, `/planner/board`, `/agents`, `/stats`, `/usage`, `/specs` render in both palettes; toggle persists across reloads; OS preference respected in `auto` mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): unify tailwind theme with design tokens + light palette (#208)
Some checks failed
qa / qa (pull_request) Failing after 3m12s
qa / dockerfile (pull_request) Successful in 8s
769b2a686f
Regenerates `apps/web/src/styles/tokens.css` to flip between Tokyo Night
Storm (dark) and Tokyo Night Day (light) off `:root[data-theme=...]`, with
an OS-preference fallback for operators who never toggle. Adds the two
missing type-size tokens (`caption` 10px, `h3` 14px) to
`design/tokens.json` and exposes every size slot as a first-class
Tailwind utility via `@theme --text-*`, so the ~235 arbitrary-value
`text-[Npx]` literals the follow-up ticket migrates land on
`text-meta` / `text-small` / etc.

Ships a `<ThemeToggle>` in the top bar that cycles auto → light → dark,
persists the choice in `localStorage.ch-theme`, and mirrors it onto
`documentElement.dataset.theme`. An inline bootstrap script in
`index.html` reads the same key before React evaluates so first paint
lands on the correct palette — no FOUC.

Closes #208

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
code-lead force-pushed boss/208 from 769b2a686f
Some checks failed
qa / qa (pull_request) Failing after 3m12s
qa / dockerfile (pull_request) Successful in 8s
to 0591bec1df
All checks were successful
qa / qa (pull_request) Successful in 3m46s
qa / dockerfile (pull_request) Successful in 6s
2026-04-21 10:43:54 +00:00
Compare
fix(ci): stub fetch in handlePullRequestOpened test to remove network flake
All checks were successful
qa / qa (pull_request) Successful in 3m46s
qa / dockerfile (pull_request) Successful in 10s
46f6bd8111
The "no agent has a token (probe-token early return)" test was written
assuming TEST_CONFIG only carried `forge-veteran` (no token_file), so the
handler would early-return from `probeToken()` without any HTTP call.
TEST_CONFIG has since grown to also include `designer` + `design-reviewer`
with valid token_files (SECRET_FILE), which flipped `probeToken()` to
return a token — the handler then proceeds into `repoHasWorkflows` and
hits the network on every run. The test only passed locally because the
host has direct LAN access to `forge.jacquin.app`; on the Forgejo runner
the contents lookup hung and tripped Bun's 5 s test timeout (#216 CI run
399: `5074ms` vs. `63ms` on a successful run on main).

Fix: stub `globalThis.fetch` to return a 404 so `repoHasWorkflows` returns
false and `decidePostCiAction("no-workflows")` short-circuits on the
author's missing token without any further I/O. Test now asserts what it
actually exercises (the no-workflows path), not the probe-token early
return.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-04-21 10:56:32 +00:00
reviewer left a comment

Review — Round 1

CI: green (run #1853, 3m54s).

Acceptance criteria

Criterion Status
design/tokens.json gains full light palette (all color.* slots)
design/tokens.json adds font-size.caption = 10px and font-size.h3 = 14px
tokens.css: dark at :root, light at [data-theme="light"], @media OS fallback with :not([data-theme="dark"]) guard
@theme exposes all 9 font-size slots as first-class Tailwind utilities
Radius utilities (rounded-compact / rounded-default / rounded-card / rounded-pill) unchanged
<ThemeToggle> in top bar, tri-state cycle auto → light → dark → auto
Persists to localStorage["ch-theme"]
FOUC prevention via inline bootstrap <script> in index.html
bun run build clean (referenced in PR test plan)

What's solid

  • The CSS variable indirection architecture is correct: the semantic aliases (--role-dev, --state-running, --stage-*, etc.) are only defined once at :root and alias into the palette vars — when the palette vars get overridden in [data-theme="light"] or the @media block, the semantic aliases pick up the new values automatically at use time. No duplication needed and no light-mode miss possible.
  • The @media (prefers-color-scheme: light) guard correctly uses :root:not([data-theme="dark"]):not([data-theme="light"]) so an explicit dark pin wins over the OS preference.
  • useTheme cross-tab sync via the storage event is a nice touch and the cleanup is correct.
  • Bootstrap hex values in index.html (#1a1b26 / #e9ecf2 / #c0caf5) match the token values.
  • readStoredTheme + isThemeMode defensive guard handles quota-exceeded / private Safari correctly.
  • 10 unit tests (7 lib + 3 component) cover the key paths: default, cycle, persistence, cross-storage sync.

Nit (non-blocking)

apps/web/src/components/theme-toggle.tsx line 46: the new <ThemeToggle> uses text-[13px] (arbitrary value) in its className. Since this PR introduces text-body (which maps to --ch-font-size-body: 13px) as a first-class Tailwind utility, the component could use text-body directly instead of the literal — exactly the pattern the follow-up migration ticket will sweep up everywhere else. Since it's new code rather than legacy code, it's an easy win. But given the follow-up ticket already owns the migration, happy to leave it to that pass.


LGTM — implementation is correct, tests are thorough, no safety issues.

## Review — Round 1 CI: ✅ green (run #1853, 3m54s). ### Acceptance criteria | Criterion | Status | |---|---| | `design/tokens.json` gains full light palette (all `color.*` slots) | ✅ | | `design/tokens.json` adds `font-size.caption = 10px` and `font-size.h3 = 14px` | ✅ | | `tokens.css`: dark at `:root`, light at `[data-theme="light"]`, `@media` OS fallback with `:not([data-theme="dark"])` guard | ✅ | | `@theme` exposes all 9 font-size slots as first-class Tailwind utilities | ✅ | | Radius utilities (`rounded-compact` / `rounded-default` / `rounded-card` / `rounded-pill`) unchanged | ✅ | | `<ThemeToggle>` in top bar, tri-state cycle `auto → light → dark → auto` | ✅ | | Persists to `localStorage["ch-theme"]` | ✅ | | FOUC prevention via inline bootstrap `<script>` in `index.html` | ✅ | | `bun run build` clean | ✅ (referenced in PR test plan) | ### What's solid - The CSS variable indirection architecture is correct: the semantic aliases (`--role-dev`, `--state-running`, `--stage-*`, etc.) are only defined once at `:root` and alias into the palette vars — when the palette vars get overridden in `[data-theme="light"]` or the `@media` block, the semantic aliases pick up the new values automatically at use time. No duplication needed and no light-mode miss possible. - The `@media (prefers-color-scheme: light)` guard correctly uses `:root:not([data-theme="dark"]):not([data-theme="light"])` so an explicit `dark` pin wins over the OS preference. - `useTheme` cross-tab sync via the `storage` event is a nice touch and the cleanup is correct. - Bootstrap hex values in `index.html` (`#1a1b26` / `#e9ecf2` / `#c0caf5`) match the token values. - `readStoredTheme` + `isThemeMode` defensive guard handles quota-exceeded / private Safari correctly. - 10 unit tests (7 lib + 3 component) cover the key paths: default, cycle, persistence, cross-storage sync. ### Nit (non-blocking) **`apps/web/src/components/theme-toggle.tsx` line 46**: the new `<ThemeToggle>` uses `text-[13px]` (arbitrary value) in its `className`. Since this PR introduces `text-body` (which maps to `--ch-font-size-body: 13px`) as a first-class Tailwind utility, the component could use `text-body` directly instead of the literal — exactly the pattern the follow-up migration ticket will sweep up everywhere else. Since it's new code rather than legacy code, it's an easy win. But given the follow-up ticket already owns the migration, happy to leave it to that pass. --- LGTM — implementation is correct, tests are thorough, no safety issues. ✅
code-lead scheduled this pull request to auto merge when all checks succeed 2026-04-21 10:58:56 +00:00
code-lead deleted branch boss/208 2026-04-21 11:02:07 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!216
No description provided.