Tasks: Dark Mode Toggle
Implementation task list for the Notes dark-mode feature, derived from design.md and prd.md. Produced by design-to-tasks; statuses updated by tasks-to-code. Status markers follow the shared conventions.
1. Overview
Section titled “1. Overview”Project Summary
Section titled “Project Summary”Notes needs a light/dark theme with a persisted, OS-aware preference and no flash of the wrong theme on load. The work decomposes along the design’s component boundaries: token palettes, a storage module, a pure resolver, a React provider, the toggle, and the inline pre-paint script — then accessibility and contrast.
Scope Reference
Section titled “Scope Reference”- PRD: prd.md · Design: design.md
- Phases decomposed: Phase 1 (MVP), Phase 2 (Accessibility & polish)
- Open question affecting tasks: PRD Q1 (two- vs three-state toggle) — does not block; see Future Considerations.
Task Statistics
Section titled “Task Statistics”| Metric | Count |
|---|---|
| Total Tasks | 11 |
| Completed | 11 |
| In Progress | 0 |
| Blocked | 0 |
| Not Started | 0 |
Phase 1: MVP (7/7 tasks complete)
Section titled “Phase 1: MVP (7/7 tasks complete)”Token palettes, storage, resolver, provider, toggle, and the pre-paint script. Goal: a working, persisted light/dark toggle with no flash — US-1, US-2, FR-1–FR-5.
Data Layer / Styling
Section titled “Data Layer / Styling”- Add a dark palette to the design tokens
[P0][S]- Depends on: None
- Requirements: FR-2, NFR-2
- Acceptance Criteria:
-
tokens.cssdefines color custom properties for[data-theme="dark"]alongside the existing light:rootvalues - No existing light-theme token value changes
-
- Notes: Completed 2026-06-17. Reused the existing
--color-*token names; only added a dark block.
Backend / Logic
Section titled “Backend / Logic”-
Add the
theme-storagemodule with a safe fallback[P0][S]- Depends on: None
- Requirements: FR-3, NFR-3
- Acceptance Criteria:
-
getPreference()/setPreference()read/writelocalStoragekeynotes.theme - Both wrap access in
try/catch;getPreference()returns'system'andsetPreference()is a no-op when storage is unavailable
-
- Notes: Completed 2026-06-17. Verified the fallback by stubbing
localStorageto throw — see decisions.md.
-
Add the pure
resolveTheme()helper[P0][S]- Depends on: None
- Requirements: FR-4
- Acceptance Criteria:
-
resolveTheme('system', prefersDark)returnsdark/lightper the media query -
resolveTheme('light'|'dark', …)returns the explicit value - Pure and total (unit-tested)
-
- Notes: Completed 2026-06-17. Shared by the inline script and the provider so the logic exists once.
-
Add the
ThemeProvidercontext[P0][M]- Depends on: Task 1.2, Task 1.3
- Requirements: FR-2
- Acceptance Criteria:
- Provides
{ theme, setTheme }to the tree - Initial theme matches the value the inline script applied
-
setThemeupdatesdata-themeand persists viatheme-storage
- Provides
- Notes: Completed 2026-06-18. Followed the existing
src/components/context pattern.
-
Add the inline pre-paint
theme-scripttoindex.html[P0][S]- Depends on: Task 1.3
- Requirements: FR-5, NFR-1
- Acceptance Criteria:
- Inline
<script>in<head>, before the stylesheet, setsdata-themefromresolveTheme(getPreference(), …) - Adds < 1 KB to the HTML
- Inline
- Notes: Completed 2026-06-18. First placed in
<body>→ flash; moved into<head>before the stylesheet after the code review flagged it. See decisions.md and review.md.
Frontend / UI
Section titled “Frontend / UI”- Add the
ThemeTogglecomponent[P0][M]- Depends on: Task 1.4
- Requirements: FR-1
- Acceptance Criteria:
- Rendered in the app shell so it appears on every page
- Activating it calls
setThemeand repaints immediately, no reload
- Notes: Completed 2026-06-18. Keyboard/SR semantics handled in Task 2.1.
Testing
Section titled “Testing”- Phase 1 verification: integration test and quality gates
[P0][M]- Depends on: All prior tasks in Phase 1
- Requirements: PRD Section 7 (Testing Strategy), QG-1–QG-5
- Acceptance Criteria:
- Integration test: toggling flips
data-themeand persists across a remount -
bun run checkpasses -
bun run formatpasses -
bun run lintpasses -
bun run testpasses -
bun run buildpasses - Phase 1 is independently deployable
- Integration test: toggling flips
- Notes: Completed 2026-06-18.
Phase 2: Accessibility & polish (4/4 tasks complete)
Section titled “Phase 2: Accessibility & polish (4/4 tasks complete)”Keyboard/screen-reader support and a contrast audit. Goal: the toggle is fully accessible and both palettes meet AA — FR-6, NFR-2.
Frontend / UI
Section titled “Frontend / UI”-
Make the toggle keyboard- and screen-reader-operable
[P1][S]- Depends on: Task 1.6
- Requirements: FR-6, NFR-2
- Acceptance Criteria:
- Focusable, activatable with Enter/Space, visible focus ring
- Exposes the current state via
aria-pressedand an accessible label
- Notes: Completed 2026-06-18. Added after the review suggestion to expose
aria-pressed.
-
Audit and fix dark-palette contrast to WCAG AA
[P1][M]- Depends on: Task 1.1
- Requirements: NFR-2
- Acceptance Criteria:
- Every text/background pair in dark theme measured ≥ 4.5:1 (≥ 3:1 large)
- Any failing token adjusted; re-measured
- Notes: Completed 2026-06-19. Two muted-text tokens were below AA and were darkened — see decisions.md.
Testing
Section titled “Testing”-
Regression test: initial
data-themeset before mount (no FOUC)[P0][S]- Depends on: Task 1.5
- Requirements: FR-5
- Acceptance Criteria:
- Test asserts
data-themeis present on the document root before the app mounts - Fails if the inline script is removed or moved after the stylesheet
- Test asserts
- Notes: Completed 2026-06-19. Added as the ADR-0001 follow-up to lock in the FOUC fix.
-
Phase 2 verification: integration test and quality gates
[P0][S]- Depends on: All prior tasks in Phase 2
- Requirements: PRD Section 7, QG-1–QG-5
- Acceptance Criteria:
-
bun run check/format/lint/test/buildall pass - Manual check: toggle persists across reload; OS default honored on a fresh profile
-
- Notes: Completed 2026-06-19.
Dependency Graph
Section titled “Dependency Graph”Task 1.1 (tokens) ─────────────── Task 2.2 (contrast audit)Task 1.2 (storage) ┐Task 1.3 (resolver)┼─ Task 1.4 (provider) ── Task 1.6 (toggle) ── Task 2.1 (a11y) └─ Task 1.5 (inline script) ── Task 2.3 (FOUC test)Task 1.7 (phase-1 verify) ── after 1.1–1.6Task 2.4 (phase-2 verify) ── after 2.1–2.3Requirements Coverage
Section titled “Requirements Coverage”| Requirement | Task(s) | Status |
|---|---|---|
| FR-1: Toggle on every page | 1.6 | ✅ Covered |
| FR-2: Apply selected theme | 1.1, 1.4 | ✅ Covered |
| FR-3: Persist choice | 1.2 | ✅ Covered |
| FR-4: OS default when unset | 1.3 | ✅ Covered |
| FR-5: No flash before paint | 1.5, 2.3 | ✅ Covered |
| FR-6: Keyboard/SR operable | 2.1 | ✅ Covered |
| NFR-1: < 5 ms / < 1 KB | 1.5 | ✅ Covered |
| NFR-2: WCAG AA contrast | 1.1, 2.1, 2.2 | ✅ Covered |
| NFR-3: Storage-unavailable fallback | 1.2 | ✅ Covered |
| US-1: Switch + persist | 1.6, 1.2 | ✅ Covered |
| US-2: OS default | 1.3, 1.5 | ✅ Covered |
| QG-1…QG-5: gates | 1.7, 2.4 | ✅ Covered |
| QG-6: Code review | review.md | ✅ Covered |
Future Considerations
Section titled “Future Considerations”- Explicit “System” toggle state (PRD Q1): the storage contract already supports
system; exposing it is a UI-only follow-up if users request it. Not tracked here. - Cross-device sync of the preference: would require the backend store the PRD rules out as a non-goal.