diff --git a/.agent/plans/issue-323-dark-light-mode.md b/.agent/plans/issue-323-dark-light-mode.md new file mode 100644 index 00000000..32ad67da --- /dev/null +++ b/.agent/plans/issue-323-dark-light-mode.md @@ -0,0 +1,193 @@ +# Issue 323: Dark Mode / Light Mode + +## Issue + +GitHub issue: https://github.com/PerryHighCS/ActiveBits/issues/323 + +> The whole interface (including headers and borders) should respond to user's preference. + +## Current State + +Dark mode is not a real, app-wide system today. Tailwind v4 is used with the +default `dark:` variant, which only follows `prefers-color-scheme` — there is +no theme provider, no toggle, no persisted preference, and no design-token +layer. The only place `dark:` classes exist is `activities/resonance/*` plus +two shared components it happened to touch +(`activities/shared/client/components/ReactionSummary.tsx`, +`InstructorFeedbackControls.tsx`). Everything else — the root app shell, +`SessionHeader`, `ManageDashboard`, `StatusDashboard`, and ~10 other +activities — is light-only with hardcoded `bg-white` / `border-gray-*` / +inline hex colors. `StatusDashboard.tsx` is the worst offender, with ~80 +inline `style={{ color: '#...' }}` literals that bypass Tailwind entirely. + +## Product Decisions + +- Three theme states: `system` (default) → `light` → `dark`, user-selectable. +- Default behavior with no stored preference: follow OS/browser + `prefers-color-scheme`, and keep following it live if the OS setting + changes, for as long as the user has never explicitly toggled. +- The moment the user explicitly picks Light or Dark, that choice is + persisted (localStorage) and overrides OS preference from then on, across + reloads and sessions. Picking "System" again clears the override and + resumes following the OS. +- **No flash of the wrong theme on load is a hard requirement** — this is + not a nice-to-have. The correct theme must be applied to the document + before first paint, not after React mounts and an effect runs. This means + the initial theme resolution (read localStorage → fall back to + `matchMedia('(prefers-color-scheme: dark)')` → set an attribute on + ``) must happen in a small **synchronous, non-module inline + `