Skip to content

refactor(web): conventional class-based dark mode + decoupled theme module#97

Open
ajianaz wants to merge 1 commit into
developfrom
feat/theme-system-refactor
Open

refactor(web): conventional class-based dark mode + decoupled theme module#97
ajianaz wants to merge 1 commit into
developfrom
feat/theme-system-refactor

Conversation

@ajianaz

@ajianaz ajianaz commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactors the frontend theme system to fix a desync bug between CSS variables and Tailwind dark: utilities, and makes the architecture framework-agnostic for the future.

Problem

The previous design was inverted: dark was the default, light was opt-in via a .light class, and there was no @custom-variant dark. As a result:

  • System A (CSS variables) followed the .light class via ThemeToggle
  • System B (dark: utilities — 21 usages across badge/button/input/textarea) followed the OS prefers-color-scheme media query (Tailwind v4 default)

These two systems were independent. Manual light/dark toggling produced broken component styling — only system mode was correct (by accident). Additional issues: FOUC flash for system + light-OS users, missing color-scheme, no live OS-change reaction.

Changes

  • app.css@custom-variant dark (&:is(.dark *)) binds both the variable swap and dark: utilities to the same .dark class. Light is now default; dark is opt-in. Added color-scheme: light/dark so native UI (scrollbars, <select>, autofill) follows the theme.
  • theme.ts (new) — Pure TS, SSR-safe, framework-agnostic theme module: getStoredTheme, resolveTheme, applyTheme, setTheme, cycleTheme, watchSystemTheme (live OS-change listener). Logic is reusable/testable independent of Svelte or shadcn.
  • ThemeToggle.svelte — Now a thin UI shell over theme.ts.
  • app.html — Fixed FOUC script: system + light-OS no longer flashes dark→light (mirrors theme.ts logic exactly).
  • theme.test.ts (new) — 16 unit tests (vitest).

Why this approach

  • .dark class = universal Tailwind convention → any future component (shadcn, hand-written, or a third-party kit) works out of the box
  • CSS variables (--color-*) are the contract in @theme, not tied to shadcn → survives moving away from shadcn
  • theme.ts is decoupled → reusable across UI layers

Trade-off: default flips from dark → light (web/shadcn convention). Mode system still follows OS preference, so most users are unaffected.

Validation

  • vitest — 62 passed | 1 skipped (theme: 16/16)
  • svelte-check — 0 errors (1 pre-existing @types/node warning)
  • vite build — OK
  • ✅ Compiled CSS verified: no @media prefers-color-scheme remains; dark: variants now compile to :is(.dark *)
  • ✅ localStorage key unchanged (rungu-theme) — backward compatible

Notes

  • Cora local review failed: provider zai/glm-5.2 returns empty responses (downstream infra issue, not code). CI will run Cora on GitHub as backup.

…odule

- Bind dark: variant to .dark class via @custom-variant dark (&:is(.dark *))
  in app.css. Previously dark: followed the OS media query while CSS vars
  followed a .light opt-in class, so manual light/dark toggling desynced
  the 21 dark: usages in badge/button/input/textarea from the variable swap.
- Light is now the default; dark is opt-in via .dark on <html>. Add
  color-scheme: light/dark so native UI (scrollbars, select, autofill)
  follows the theme.
- Extract theme logic into src/lib/theme.ts (pure TS, SSR-safe,
  framework-agnostic): getStoredTheme, resolveTheme, applyTheme, setTheme,
  cycleTheme, watchSystemTheme (live OS change listener).
  ThemeToggle.svelte is now a thin UI shell over it.
- Fix FOUC script in app.html: handle stored 'system' correctly (was treated
  as concrete dark -> dark->light flash for system + light-OS users).
- Add 16 unit tests in theme.test.ts (vitest). localStorage key unchanged
  (backward compatible).
@github-actions

Copy link
Copy Markdown

🔍 Cora AI Code Review

⚠️ Review recommended — warnings found.

🟡 Warning (1)

  • unknown:? — Cora AI review could not complete: LLM API returned status 401: {"error":"Insufficient balance"}
    . Review was skipped; no code quality issues were found by the automated check.

Review powered by cora-cli · BYOK · MIT

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