From b5ea82333a8e5b9ed35b7a7e32cd49b19135446a Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 20:48:10 -0500 Subject: [PATCH 01/41] docs(specs): create dark mode spec --- .../001-dark-mode/checklists/requirements.md | 35 ++++++ specs/001-dark-mode/spec.md | 114 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 specs/001-dark-mode/checklists/requirements.md create mode 100644 specs/001-dark-mode/spec.md diff --git a/specs/001-dark-mode/checklists/requirements.md b/specs/001-dark-mode/checklists/requirements.md new file mode 100644 index 0000000..e07845e --- /dev/null +++ b/specs/001-dark-mode/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Dark Mode + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-06-17 +**Feature**: [Dark Mode](spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [ ] No [NEEDS CLARIFICATION] markers remain (1 found: FR-007) +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` +- **ISSUE FOUND**: FR-007 contains [NEEDS CLARIFICATION] marker requiring user input diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md new file mode 100644 index 0000000..2f8b8cd --- /dev/null +++ b/specs/001-dark-mode/spec.md @@ -0,0 +1,114 @@ +# Feature Specification: Dark Mode + +**Feature Branch**: `001-dark-mode` +**Created**: 2025-06-17 +**Status**: Draft +**Input**: User description: "dark mode" + +## User Scenarios & Testing _(mandatory)_ + + + +### User Story 1 - Toggle Dark Mode (Priority: P1) + +User wants to switch between light and dark themes to reduce eye strain during reading in low-light conditions. + +**Why this priority**: Core functionality that provides immediate user value and accessibility benefits. + +**Independent Test**: Can be fully tested by toggling the theme switch and verifying the UI changes between light and dark modes. + +**Acceptance Scenarios**: + +1. **Given** the application is in light mode, **When** user clicks the dark mode toggle, **Then** the interface switches to dark theme with appropriate colors +2. **Given** the application is in dark mode, **When** user clicks the dark mode toggle, **Then** the interface switches to light theme + +--- + +### User Story 2 - Persistent Theme Preference (Priority: P2) + +User wants their theme preference to be remembered across sessions so they don't have to manually switch each time. + +**Why this priority**: Improves user experience by maintaining consistency and reducing friction. + +**Independent Test**: Can be tested by setting a theme, closing/reopening the application, and verifying the theme persists. + +**Acceptance Scenarios**: + +1. **Given** user has selected dark mode, **When** they close and reopen the application, **Then** dark mode is automatically applied +2. **Given** user has selected light mode, **When** they close and reopen the application, **Then** light mode is automatically applied + +--- + +### User Story 3 - System Theme Detection (Priority: P3) + +User wants the application to automatically match their operating system's theme preference. + +**Why this priority**: Provides seamless integration with user's system preferences for better UX. + +**Independent Test**: Can be tested by changing OS theme settings and verifying the application responds accordingly. + +**Acceptance Scenarios**: + +1. **Given** user's OS is set to dark mode, **When** they first visit the application, **Then** dark mode is automatically selected +2. **Given** user's OS is set to light mode, **When** they first visit the application, **Then** light mode is automatically selected + +### Edge Cases + +- What happens when localStorage is disabled or full? +- How does system handle theme switching during page load? +- What happens when system theme changes while application is open? +- How does system handle high contrast mode accessibility settings? + +## Requirements _(mandatory)_ + + + +### Constitution Alignment _(mandatory)_ + +- **Comprehension Outcome**: Dark mode reduces eye strain in low-light conditions, improving reading comfort and potentially extending reading sessions without fatigue. +- **Deterministic Behavior**: Theme changes must apply instantly and consistently across all UI elements, with no flickering or partial updates. +- **Accessibility Coverage**: Theme toggle must be keyboard accessible, properly labeled for screen readers, and maintain sufficient color contrast ratios in both modes. + +### Functional Requirements + +- **FR-001**: System MUST provide a toggle control to switch between light and dark themes +- **FR-002**: System MUST apply theme changes immediately to all UI elements +- **FR-003**: System MUST persist user's theme preference across sessions +- **FR-004**: System MUST detect and respect user's operating system theme preference on first visit +- **FR-005**: System MUST maintain proper color contrast ratios for accessibility in both themes +- **FR-006**: System MUST provide smooth transitions between theme changes without flickering +- **FR-007**: System MUST handle localStorage unavailability gracefully [NEEDS CLARIFICATION: fallback behavior when storage unavailable] + +### Key Entities _(include if feature involves data)_ + +- **Theme Preference**: User's selected theme (light/dark/system) with persistence across sessions +- **System Theme**: Operating system's current theme preference for automatic detection + +## Success Criteria _(mandatory)_ + + + +### Measurable Outcomes + +- **SC-001**: Users can toggle between themes in under 1 second with immediate visual feedback +- **SC-002**: Theme preference persists across 100% of browser sessions when localStorage is available +- **SC-003**: Both light and dark themes maintain WCAG AA contrast ratios (4.5:1 for normal text) +- **SC-004**: 95% of users successfully find and use the theme toggle without assistance +- **SC-005**: System theme detection works correctly on 90% of supported operating systems and browsers From 4538d7c98a4dcc9a507d0e956eceb7a272d4df64 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 20:49:13 -0500 Subject: [PATCH 02/41] docs(specs): clarify localStorage fallback --- specs/001-dark-mode/checklists/requirements.md | 5 ++--- specs/001-dark-mode/spec.md | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/specs/001-dark-mode/checklists/requirements.md b/specs/001-dark-mode/checklists/requirements.md index e07845e..7da8439 100644 --- a/specs/001-dark-mode/checklists/requirements.md +++ b/specs/001-dark-mode/checklists/requirements.md @@ -13,7 +13,7 @@ ## Requirement Completeness -- [ ] No [NEEDS CLARIFICATION] markers remain (1 found: FR-007) +- [x] No [NEEDS CLARIFICATION] markers remain - [x] Requirements are testable and unambiguous - [x] Success criteria are measurable - [x] Success criteria are technology-agnostic (no implementation details) @@ -31,5 +31,4 @@ ## Notes -- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` -- **ISSUE FOUND**: FR-007 contains [NEEDS CLARIFICATION] marker requiring user input +- All checklist items completed - specification is ready for `/speckit.plan` diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md index 2f8b8cd..7a06b60 100644 --- a/specs/001-dark-mode/spec.md +++ b/specs/001-dark-mode/spec.md @@ -91,7 +91,7 @@ User wants the application to automatically match their operating system's theme - **FR-004**: System MUST detect and respect user's operating system theme preference on first visit - **FR-005**: System MUST maintain proper color contrast ratios for accessibility in both themes - **FR-006**: System MUST provide smooth transitions between theme changes without flickering -- **FR-007**: System MUST handle localStorage unavailability gracefully [NEEDS CLARIFICATION: fallback behavior when storage unavailable] +- **FR-007**: System MUST handle localStorage unavailability gracefully by defaulting to system theme preference ### Key Entities _(include if feature involves data)_ From 61639d53bcbc6c2401159691b1e5287662a78c03 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 20:50:44 -0500 Subject: [PATCH 03/41] docs(specs): fix date --- specs/001-dark-mode/checklists/requirements.md | 2 +- specs/001-dark-mode/spec.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/001-dark-mode/checklists/requirements.md b/specs/001-dark-mode/checklists/requirements.md index 7da8439..6f91f98 100644 --- a/specs/001-dark-mode/checklists/requirements.md +++ b/specs/001-dark-mode/checklists/requirements.md @@ -1,7 +1,7 @@ # Specification Quality Checklist: Dark Mode **Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2025-06-17 +**Created**: 2026-02-15 **Feature**: [Dark Mode](spec.md) ## Content Quality diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md index 7a06b60..261557c 100644 --- a/specs/001-dark-mode/spec.md +++ b/specs/001-dark-mode/spec.md @@ -1,7 +1,7 @@ # Feature Specification: Dark Mode **Feature Branch**: `001-dark-mode` -**Created**: 2025-06-17 +**Created**: 2026-02-15 **Status**: Draft **Input**: User description: "dark mode" From 65b40741be74f3c90773444e1ef19c16e7f372a8 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 20:52:18 -0500 Subject: [PATCH 04/41] docs(specs): fix more dates --- .specify/templates/constitution-template.md | 2 +- specs/001-multiple-words/checklists/requirements.md | 2 +- specs/001-multiple-words/contracts/component-apis.md | 2 +- specs/001-multiple-words/data-model.md | 2 +- specs/001-multiple-words/plan.md | 2 +- specs/001-multiple-words/quickstart.md | 2 +- specs/001-multiple-words/research.md | 2 +- specs/001-multiple-words/spec.md | 4 ++-- specs/001-multiple-words/tasks.md | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.specify/templates/constitution-template.md b/.specify/templates/constitution-template.md index 4e9354e..377e0ce 100644 --- a/.specify/templates/constitution-template.md +++ b/.specify/templates/constitution-template.md @@ -70,4 +70,4 @@ **Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] - + diff --git a/specs/001-multiple-words/checklists/requirements.md b/specs/001-multiple-words/checklists/requirements.md index 9ec227d..3a48d25 100644 --- a/specs/001-multiple-words/checklists/requirements.md +++ b/specs/001-multiple-words/checklists/requirements.md @@ -1,7 +1,7 @@ # Specification Quality Checklist: Multiple Words Display **Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2025-02-15 +**Created**: 2026-02-15 **Feature**: [Multiple Words Display](../spec.md) ## Content Quality diff --git a/specs/001-multiple-words/contracts/component-apis.md b/specs/001-multiple-words/contracts/component-apis.md index dae7ea2..b47bc08 100644 --- a/specs/001-multiple-words/contracts/component-apis.md +++ b/specs/001-multiple-words/contracts/component-apis.md @@ -1,6 +1,6 @@ # Component API Contracts: Multiple Words Display -**Date**: 2025-02-15 +**Date**: 2026-02-15 **Feature**: Multiple Words Display ## ControlPanel Component API diff --git a/specs/001-multiple-words/data-model.md b/specs/001-multiple-words/data-model.md index 2a48d46..a4c7899 100644 --- a/specs/001-multiple-words/data-model.md +++ b/specs/001-multiple-words/data-model.md @@ -1,6 +1,6 @@ # Data Model: Multiple Words Display -**Date**: 2025-02-15 +**Date**: 2026-02-15 **Feature**: Multiple Words Display **Status**: Complete diff --git a/specs/001-multiple-words/plan.md b/specs/001-multiple-words/plan.md index 884f033..5484a35 100644 --- a/specs/001-multiple-words/plan.md +++ b/specs/001-multiple-words/plan.md @@ -1,6 +1,6 @@ # Implementation Plan: Multiple Words Display -**Branch**: `001-multiple-words` | **Date**: 2025-02-15 | **Spec**: [spec.md](spec.md) +**Branch**: `001-multiple-words` | **Date**: 2026-02-15 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `/specs/001-multiple-words/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. diff --git a/specs/001-multiple-words/quickstart.md b/specs/001-multiple-words/quickstart.md index 7985dc0..484014a 100644 --- a/specs/001-multiple-words/quickstart.md +++ b/specs/001-multiple-words/quickstart.md @@ -1,6 +1,6 @@ # Quickstart Guide: Multiple Words Display -**Date**: 2025-02-15 +**Date**: 2026-02-15 **Feature**: Multiple Words Display ## Overview diff --git a/specs/001-multiple-words/research.md b/specs/001-multiple-words/research.md index 26be080..08756bb 100644 --- a/specs/001-multiple-words/research.md +++ b/specs/001-multiple-words/research.md @@ -1,6 +1,6 @@ # Research: Multiple Words Display -**Date**: 2025-02-15 +**Date**: 2026-02-15 **Feature**: Multiple Words Display **Status**: Complete diff --git a/specs/001-multiple-words/spec.md b/specs/001-multiple-words/spec.md index 7cbf310..dff8a68 100644 --- a/specs/001-multiple-words/spec.md +++ b/specs/001-multiple-words/spec.md @@ -1,7 +1,7 @@ # Feature Specification: Multiple Words Display **Feature Branch**: `001-multiple-words` -**Created**: 2025-02-15 +**Created**: 2026-02-15 **Status**: **Fully Implemented - All Phases Complete** **Input**: User description: "multiple words" @@ -34,7 +34,7 @@ ## Clarifications -### Session 2025-02-15 +### Session 2026-02-15 - Q: What type of UI control should be used for selecting the word count per chunk? → A: Dropdown/select menu with numbered options, min 1 and max 5 words - Q: What should be the default word count when users first enable multiple words display? → A: 1 word (same as current single-word mode) diff --git a/specs/001-multiple-words/tasks.md b/specs/001-multiple-words/tasks.md index 45fc868..59a052b 100644 --- a/specs/001-multiple-words/tasks.md +++ b/specs/001-multiple-words/tasks.md @@ -1,6 +1,6 @@ # Implementation Tasks: Multiple Words Display -**Branch**: `001-multiple-words` | **Date**: 2025-02-15 +**Branch**: `001-multiple-words` | **Date**: 2026-02-15 **Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md) ## Summary From 00f3abf31583cc1c7b8966d3132c4e07020ddb11 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 20:53:33 -0500 Subject: [PATCH 05/41] docs(specs): clarify toggle icons --- specs/001-dark-mode/spec.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md index 261557c..5ca60c3 100644 --- a/specs/001-dark-mode/spec.md +++ b/specs/001-dark-mode/spec.md @@ -5,6 +5,12 @@ **Status**: Draft **Input**: User description: "dark mode" +## Clarifications + +### Session 2026-02-15 + +- Q: Toggle Control Location and Type → A: Header toggle switch with sun/moon icons + ## User Scenarios & Testing _(mandatory)_ - ### User Story 1 - Toggle Dark Mode (Priority: P1) User wants to switch between light and dark themes to reduce eye strain during reading in low-light conditions. @@ -82,11 +69,6 @@ User wants the application to automatically match their operating system's theme ## Requirements _(mandatory)_ - - ### Constitution Alignment _(mandatory)_ - **Comprehension Outcome**: Dark mode reduces eye strain in low-light conditions, improving reading comfort and potentially extending reading sessions without fatigue. @@ -112,11 +94,6 @@ User wants the application to automatically match their operating system's theme ## Success Criteria _(mandatory)_ - - ### Measurable Outcomes - **SC-001**: Users can toggle between themes in under 1 second with immediate visual feedback From 3ef7c06b6835d8ca21b5a47a9cea414050d92340 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:31:15 -0500 Subject: [PATCH 13/41] docs(specs): create plan --- specs/001-dark-mode/data-model.md | 142 ++++++++++++++++++++++++++++++ specs/001-dark-mode/plan.md | 100 +++++++++++++++++++++ specs/001-dark-mode/research.md | 92 +++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 specs/001-dark-mode/data-model.md create mode 100644 specs/001-dark-mode/plan.md create mode 100644 specs/001-dark-mode/research.md diff --git a/specs/001-dark-mode/data-model.md b/specs/001-dark-mode/data-model.md new file mode 100644 index 0000000..a616ead --- /dev/null +++ b/specs/001-dark-mode/data-model.md @@ -0,0 +1,142 @@ +# Data Model: Dark Mode + +**Feature**: Dark Mode +**Date**: 2026-02-15 +**Phase**: 1 - Design + +## Core Entities + +### ThemePreference + +Represents the user's theme selection and system behavior. + +```typescript +interface ThemePreference { + /** Current theme state */ + theme: 'light' | 'dark' | 'system'; + /** Whether theme preference should persist across sessions */ + persist: boolean; + /** Timestamp of last theme change */ + lastChanged: number; +} +``` + +### SystemTheme + +Represents the operating system's current theme preference. + +```typescript +interface SystemTheme { + /** System's current color scheme preference */ + prefersColorScheme: 'light' | 'dark' | 'no-preference'; + /** Whether system supports color scheme detection */ + supported: boolean; +} +``` + +### ThemeState + +Combined state representing the effective theme applied to the UI. + +```typescript +interface ThemeState { + /** The theme currently applied to the UI */ + effectiveTheme: 'light' | 'dark'; + /** User's preference setting */ + userPreference: ThemePreference; + /** System's current preference */ + systemPreference: SystemTheme; + /** Whether high contrast mode is active */ + highContrastMode: boolean; +} +``` + +## State Transitions + +### Theme Toggle Flow + +```mermaid +stateDiagram-v2 + [*] --> Light + Light --> Dark: User clicks toggle + Dark --> Light: User clicks toggle + + Light --> System: User selects system preference + Dark --> System: User selects system preference + System --> Light: System changes to light + System --> Dark: System changes to dark +``` + +### Initialization Flow + +```mermaid +flowchart TD + A[App Start] --> B{localStorage available?} + B -->|Yes| C[Load stored preference] + B -->|No| D[Detect system preference] + C --> E{Valid preference?} + E -->|Yes| F[Apply stored theme] + E -->|No| D + D --> G[Apply system theme] + F --> H[Render UI] + G --> H +``` + +## Validation Rules + +### Theme Preference Validation + +- `theme` must be one of: 'light', 'dark', 'system' +- `persist` must be boolean +- `lastChanged` must be valid Unix timestamp +- Invalid localStorage data should be ignored and fall back to system preference + +### System Theme Detection + +- `prefersColorScheme` detection uses `window.matchMedia('(prefers-color-scheme: dark)')` +- `supported` is true if `matchMedia` is available and responds +- `no-preference` used when system doesn't express a preference + +### High Contrast Mode Detection + +- Uses `window.matchMedia('(prefers-contrast: high)')` +- Takes precedence over dark mode when active +- Falls back gracefully if not supported + +## Storage Schema + +### localStorage Key + +```typescript +const THEME_STORAGE_KEY = 'speedreader-theme-preference'; +``` + +### Stored Data Format + +```json +{ + "theme": "dark", + "persist": true, + "lastChanged": 1739644800000 +} +``` + +### Storage Validation + +- Parse JSON safely, handle corruption gracefully +- Validate structure against ThemePreference interface +- Default to system preference on validation failure + +## Performance Considerations + +- Theme state updates should be batched to prevent re-renders +- System theme listeners should be properly cleaned up +- localStorage reads/writes minimized to essential operations +- Theme transitions use CSS for optimal performance + +## Error Handling + +- localStorage quota exceeded: fall back to system preference +- JSON parsing errors: clear corrupted data, use system preference +- Media query not supported: assume 'no-preference' +- Theme toggle errors: maintain current theme, log error diff --git a/specs/001-dark-mode/plan.md b/specs/001-dark-mode/plan.md new file mode 100644 index 0000000..90b9b03 --- /dev/null +++ b/specs/001-dark-mode/plan.md @@ -0,0 +1,100 @@ +# Implementation Plan: Dark Mode + +**Branch**: `001-dark-mode` | **Date**: 2026-02-15 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-dark-mode/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. + +## Summary + +Add dark mode functionality to the speed reader application with a floating toggle button, theme persistence, and system theme detection. The feature will provide immediate visual feedback with smooth transitions and maintain accessibility standards. + +## Technical Context + + + +**Language/Version**: TypeScript 5 (React 19) +**Primary Dependencies**: React 19, Tailwind CSS 4 +**Storage**: localStorage for theme persistence +**Testing**: Vitest 4, React Testing Library +**Target Platform**: Web browser +**Project Type**: Web application +**Performance Goals**: <1s theme toggle, 300ms transitions, no layout shifts +**Constraints**: Must work with localStorage disabled, respect high contrast mode +**Scale/Scope**: Single page application with theme-aware components + +## Constitution Check + +_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._ + +- [x] Reader comprehension impact is defined and measurable for the feature. +- [x] Deterministic behavior is specified for timing/state transitions and has regression guardrails. +- [x] Accessibility requirements cover keyboard navigation, semantics, focus, and responsive parity. +- [x] Test strategy includes regression coverage and required quality gates (`lint`, `lint:tsc`, `test:ci`). +- [x] Scope is minimal and dependency changes are justified. + +## Project Structure + +### Documentation (this feature) + +```text +specs/[###-feature]/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + + + +```text +src/ +├── components/ +│ ├── App/ +│ │ ├── App.tsx +│ │ ├── App.types.ts +│ │ └── App.test.tsx +│ ├── ThemeToggle/ +│ │ ├── ThemeToggle.tsx +│ │ ├── ThemeToggle.types.ts +│ │ └── ThemeToggle.test.tsx +│ └── ... +├── hooks/ +│ ├── useTheme.ts +│ └── useTheme.test.ts +├── utils/ +│ ├── theme.ts +│ └── theme.test.ts +└── types/ + └── index.ts + +tests/ +├── integration/ +│ └── theme.integration.test.tsx +└── unit/ + └── theme.test.tsx +``` + +**Structure Decision**: Single web application using React 19 with TypeScript. Theme functionality will be encapsulated in custom hooks and utility functions, with a dedicated ThemeToggle component. The existing component structure will be extended rather than restructured. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +| -------------------------- | ------------------ | ------------------------------------ | +| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | +| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | diff --git a/specs/001-dark-mode/research.md b/specs/001-dark-mode/research.md new file mode 100644 index 0000000..d310d97 --- /dev/null +++ b/specs/001-dark-mode/research.md @@ -0,0 +1,92 @@ +# Research: Dark Mode Implementation + +**Feature**: Dark Mode +**Date**: 2026-02-15 +**Phase**: 0 - Research Complete + +## Technology Decisions + +### Theme Management Approach + +**Decision**: Use React Context + localStorage + system preference detection +**Rationale**: + +- React Context provides global theme state management +- localStorage ensures persistence across sessions +- `prefers-color-scheme` media query enables system theme detection +- No additional dependencies required beyond existing React/Tailwind + +**Alternatives considered**: + +- CSS-only solution with `prefers-color-scheme` (rejected: lacks user preference persistence) +- Third-party theme libraries (rejected: adds unnecessary dependencies for simple use case) + +### Theme Toggle Implementation + +**Decision**: Custom SVG toggle button with sun/moon icons +**Rationale**: + +- Complete control over styling and animations +- Lightweight - no external icon dependencies +- Can be positioned as floating button per requirements +- Better accessibility control with custom ARIA labels + +**Alternatives considered**: + +- Icon library (react-icons) (rejected: adds dependency for just 2 icons) +- Emoji toggle (rejected: inconsistent rendering across platforms) + +### Tailwind CSS Dark Mode Strategy + +**Decision**: Use Tailwind's `dark:` variant prefix with `class` strategy +**Rationale**: + +- Leverages existing Tailwind setup +- Provides explicit control over theme application +- Works well with React Context state management +- Maintains design system consistency + +**Alternatives considered**: + +- Tailwind `media` strategy (rejected: doesn't allow user preference override) +- Custom CSS variables (rejected: more complex, loses Tailwind utility benefits) + +### Storage Strategy + +**Decision**: localStorage with fallback to system preference +**Rationale**: + +- Native browser API, no dependencies +- Sufficient for simple theme preference storage +- Graceful degradation when localStorage unavailable +- Meets persistence requirements + +**Alternatives considered**: + +- IndexedDB (rejected: overkill for simple boolean preference) +- Cookies (rejected: sent with every request, unnecessary overhead) + +## Performance Considerations + +- Theme transitions will use CSS transitions (300ms) for optimal performance +- No layout shifts expected - theme changes only affect colors, not layout +- Theme detection happens once on app initialization +- localStorage access is synchronous and fast + +## Accessibility Strategy + +- Toggle button will have proper ARIA labels and keyboard support +- Theme changes will respect `prefers-reduced-motion` for users who disable animations +- Color contrast ratios will be validated for both light and dark themes +- High contrast mode detection will override dark mode when detected + +## Edge Cases Handled + +- localStorage unavailable: fallback to system preference +- System theme changes during session: automatic detection and update +- High contrast mode: takes precedence over dark mode +- Page load timing: wait for stored theme before rendering to prevent flash + +## Research Complete + +All technical decisions have been documented. No further clarification needed for Phase 1 design. From 1f60e046ece4f55a28494bc8802849da6c8ab269 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:33:03 -0500 Subject: [PATCH 14/41] docs(specs): update contracts --- .../contracts/component-contracts.md | 201 ++++++++ specs/001-dark-mode/quickstart.md | 479 ++++++++++++++++++ 2 files changed, 680 insertions(+) create mode 100644 specs/001-dark-mode/contracts/component-contracts.md create mode 100644 specs/001-dark-mode/quickstart.md diff --git a/specs/001-dark-mode/contracts/component-contracts.md b/specs/001-dark-mode/contracts/component-contracts.md new file mode 100644 index 0000000..bd933f5 --- /dev/null +++ b/specs/001-dark-mode/contracts/component-contracts.md @@ -0,0 +1,201 @@ +# Component Contracts: Dark Mode + +**Feature**: Dark Mode +**Date**: 2026-02-15 +**Phase**: 1 - Design + +## ThemeToggle Component Contract + +### Interface Definition + +```typescript +interface ThemeToggleProps { + /** Current theme state */ + currentTheme: 'light' | 'dark' | 'system'; + /** Callback when theme is toggled */ + onThemeToggle: () => void; + /** Optional CSS class name */ + className?: string; + /** Whether the toggle should be disabled */ + disabled?: boolean; +} +``` + +### Behavioral Contract + +#### User Interactions + +1. **Click Action** + - **Trigger**: User clicks the toggle button + - **Action**: Calls `onThemeToggle()` callback + - **Visual Feedback**: Shows sun/moon icon transition with 300ms animation + +2. **Keyboard Navigation** + - **Tab Order**: Toggle must be focusable and included in tab sequence + - **Enter/Space**: Activates toggle when focused + - **Focus Indicator**: Visible focus state with 2px outline + +3. **Screen Reader Support** + - **ARIA Label**: "Toggle dark mode, currently {light/dark} mode" + - **ARIA Role**: `button` + - **State Announcement**: Theme change announced to screen readers + +#### Visual Requirements + +1. **Position**: Fixed position, bottom-right corner +2. **Size**: 48px × 48px minimum touch target +3. **Icons**: Sun icon for light mode, moon icon for dark mode +4. **Animation**: 300ms smooth rotation and color transition +5. **Hover State**: Slight scale increase (1.05) and background color change + +### Accessibility Contract + +```typescript +interface AccessibilityRequirements { + /** Minimum touch target size in pixels */ + minTouchTarget: 48; + /** Minimum color contrast ratio for normal text */ + minContrastRatio: 4.5; + /** Animation duration in milliseconds */ + maxAnimationDuration: 300; + /** Keyboard support required */ + keyboardSupport: true; + /** Screen reader support required */ + screenReaderSupport: true; +} +``` + +## useTheme Hook Contract + +### Interface Definition + +```typescript +interface UseThemeReturn { + /** Currently applied theme */ + theme: 'light' | 'dark'; + /** User's preference setting */ + preference: 'light' | 'dark' | 'system'; + /** Whether system preference is being followed */ + followingSystem: boolean; + /** Toggle between light and dark themes */ + toggleTheme: () => void; + /** Set specific theme preference */ + setTheme: (theme: 'light' | 'dark' | 'system') => void; + /** Whether high contrast mode is active */ + highContrastMode: boolean; +} +``` + +### Behavioral Contract + +#### Theme Management + +1. **Initialization** + - Load preference from localStorage if available + - Detect system theme as fallback + - Apply theme before rendering to prevent flash + +2. **Persistence** + - Save user preference to localStorage on change + - Include timestamp for debugging + - Handle storage errors gracefully + +3. **System Detection** + - Listen for system theme changes + - Update theme automatically when following system + - Clean up listeners on unmount + +#### Error Handling + +```typescript +interface ErrorHandling { + /** Fallback behavior when localStorage fails */ + localStorageFallback: 'system-preference'; + /** Behavior when JSON parsing fails */ + parseErrorFallback: 'clear-storage-and-use-system'; + /** Behavior when media queries not supported */ + mediaQueryFallback: 'assume-no-preference'; +} +``` + +## Theme Utility Contract + +### Interface Definition + +```typescript +interface ThemeUtils { + /** Get system theme preference */ + getSystemTheme: () => 'light' | 'dark' | 'no-preference'; + /** Check if high contrast mode is active */ + getHighContrastMode: () => boolean; + /** Save theme preference to localStorage */ + saveThemePreference: (preference: ThemePreference) => boolean; + /** Load theme preference from localStorage */ + loadThemePreference: () => ThemePreference | null; + /** Validate theme preference object */ + validateThemePreference: (data: unknown) => data is ThemePreference; +} +``` + +### Storage Contract + +#### localStorage Schema + +```typescript +interface StorageContract { + /** Storage key for theme preference */ + key: 'speedreader-theme-preference'; + /** Data format stored */ + format: { + theme: 'light' | 'dark' | 'system'; + persist: boolean; + lastChanged: number; + }; + /** Maximum storage size */ + maxSize: 1024; // bytes +} +``` + +#### Error Recovery + +1. **Quota Exceeded** + - Clear existing theme data + - Fall back to system preference + - Log error for debugging + +2. **Data Corruption** + - Detect invalid JSON structure + - Remove corrupted data + - Reset to system preference + +## Integration Contract + +### App Component Integration + +```typescript +interface AppThemeIntegration { + /** Theme provider must wrap entire app */ + providerLocation: 'root'; + /** Theme must be applied before first render */ + initializationTiming: 'before-render'; + /** Theme changes must not cause layout shifts */ + layoutStability: 'no-shift'; + /** Theme transitions must respect motion preferences */ + motionRespect: 'honor-prefers-reduced-motion'; +} +``` + +### CSS Integration + +```typescript +interface CSSIntegration { + /** Tailwind class strategy */ + tailwindStrategy: 'class'; + /** Custom properties for theme colors */ + cssVariables: boolean; + /** Transition duration */ + transitionDuration: '300ms'; + /** Transition easing */ + transitionEasing: 'ease-in-out'; +} +``` diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md new file mode 100644 index 0000000..f41a2f8 --- /dev/null +++ b/specs/001-dark-mode/quickstart.md @@ -0,0 +1,479 @@ +# Quickstart: Dark Mode Implementation + +**Feature**: Dark Mode +**Date**: 2026-02-15 +**Phase**: 1 - Design + +## Implementation Overview + +This quickstart guide provides the step-by-step approach to implement dark mode functionality in the speed reader application. The implementation follows the React 19 + TypeScript 5 + Tailwind CSS 4 stack. + +## Prerequisites + +- React 19 with TypeScript 5 strict mode +- Tailwind CSS 4 configured with `class` dark mode strategy +- Vitest 4 for testing +- Existing component structure in `src/components/` + +## Step 1: Configure Tailwind CSS Dark Mode + +Update `tailwind.config.js` to enable dark mode with class strategy: + +```javascript +module.exports = { + darkMode: 'class', // Enable class-based dark mode + // ... existing config +}; +``` + +## Step 2: Create Theme Types + +Create `src/types/theme.ts`: + +```typescript +export type Theme = 'light' | 'dark' | 'system'; + +export interface ThemePreference { + theme: Theme; + persist: boolean; + lastChanged: number; +} + +export interface ThemeState { + effectiveTheme: 'light' | 'dark'; + userPreference: ThemePreference; + systemPreference: 'light' | 'dark' | 'no-preference'; + highContrastMode: boolean; +} +``` + +## Step 3: Implement Theme Utilities + +Create `src/utils/theme.ts`: + +```typescript +import type { ThemePreference } from 'src/types/theme'; + +const THEME_STORAGE_KEY = 'speedreader-theme-preference'; + +export const getSystemTheme = (): 'light' | 'dark' | 'no-preference' => { + if (!window.matchMedia) return 'no-preference'; + + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; +}; + +export const getHighContrastMode = (): boolean => { + if (!window.matchMedia) return false; + + return window.matchMedia('(prefers-contrast: high)').matches; +}; + +export const saveThemePreference = (preference: ThemePreference): boolean => { + try { + localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(preference)); + return true; + } catch { + return false; + } +}; + +export const loadThemePreference = (): ThemePreference | null => { + try { + const stored = localStorage.getItem(THEME_STORAGE_KEY); + if (!stored) return null; + + const parsed = JSON.parse(stored); + return validateThemePreference(parsed) ? parsed : null; + } catch { + return null; + } +}; + +export const validateThemePreference = ( + data: unknown, +): data is ThemePreference => { + return ( + typeof data === 'object' && + data !== null && + 'theme' in data && + ['light', 'dark', 'system'].includes((data as any).theme) && + 'persist' in data && + typeof (data as any).persist === 'boolean' && + 'lastChanged' in data && + typeof (data as any).lastChanged === 'number' + ); +}; +``` + +## Step 4: Create useTheme Hook + +Create `src/hooks/useTheme.ts`: + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import type { Theme, ThemeState } from 'src/types/theme'; +import { + getSystemTheme, + getHighContrastMode, + saveThemePreference, + loadThemePreference, +} from 'src/utils/theme'; + +const DEFAULT_PREFERENCE = { + theme: 'system' as Theme, + persist: true, + lastChanged: Date.now(), +}; + +export const useTheme = () => { + const [themeState, setThemeState] = useState(() => { + const stored = loadThemePreference(); + const systemTheme = getSystemTheme(); + const highContrast = getHighContrastMode(); + + const preference = stored || DEFAULT_PREFERENCE; + const effectiveTheme = highContrast + ? 'light' + : preference.theme === 'system' + ? systemTheme + : preference.theme; + + return { + effectiveTheme, + userPreference: preference, + systemPreference: systemTheme, + highContrastMode: highContrast, + }; + }); + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = () => { + setThemeState((prev) => { + if (prev.userPreference.theme !== 'system') return prev; + + const newSystemTheme = mediaQuery.matches ? 'dark' : 'light'; + const effectiveTheme = prev.highContrastMode ? 'light' : newSystemTheme; + + return { + ...prev, + systemPreference: newSystemTheme, + effectiveTheme, + }; + }); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + // Listen for high contrast mode changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-contrast: high)'); + + const handleChange = () => { + setThemeState((prev) => { + const highContrast = mediaQuery.matches; + const effectiveTheme = highContrast + ? 'light' + : prev.userPreference.theme === 'system' + ? prev.systemPreference + : prev.userPreference.theme; + + return { + ...prev, + highContrastMode: highContrast, + effectiveTheme, + }; + }); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + const toggleTheme = useCallback(() => { + setThemeState((prev) => { + const newTheme = prev.effectiveTheme === 'light' ? 'dark' : 'light'; + const newPreference = { + theme: newTheme, + persist: true, + lastChanged: Date.now(), + }; + + saveThemePreference(newPreference); + + return { + ...prev, + effectiveTheme: newTheme, + userPreference: newPreference, + }; + }); + }, []); + + const setTheme = useCallback((theme: Theme) => { + setThemeState((prev) => { + const newPreference = { + theme, + persist: true, + lastChanged: Date.now(), + }; + + saveThemePreference(newPreference); + + const effectiveTheme = prev.highContrastMode + ? 'light' + : theme === 'system' + ? prev.systemPreference + : theme; + + return { + ...prev, + effectiveTheme, + userPreference: newPreference, + }; + }); + }, []); + + return { + theme: themeState.effectiveTheme, + preference: themeState.userPreference.theme, + followingSystem: themeState.userPreference.theme === 'system', + toggleTheme, + setTheme, + highContrastMode: themeState.highContrastMode, + }; +}; +``` + +## Step 5: Create ThemeToggle Component + +Create `src/components/ThemeToggle/ThemeToggle.tsx`: + +```typescript +import { buttonVariants } from 'src/components/Button'; +import type { ThemeToggleProps } from './ThemeToggle.types'; + +export const ThemeToggle = ({ + currentTheme, + onThemeToggle, + className, + disabled = false +}: ThemeToggleProps) => { + const isDark = currentTheme === 'dark'; + const isSystem = currentTheme === 'system'; + + const ariaLabel = `Toggle dark mode, currently ${ + isDark ? 'dark' : isSystem ? 'system' : 'light' + } mode`; + + return ( + + ); +}; +``` + +Create `src/components/ThemeToggle/ThemeToggle.types.ts`: + +```typescript +export interface ThemeToggleProps { + currentTheme: 'light' | 'dark' | 'system'; + onThemeToggle: () => void; + className?: string; + disabled?: boolean; +} +``` + +## Step 6: Integrate with App Component + +Update `src/components/App/App.tsx`: + +```typescript +import { useTheme } from 'src/hooks/useTheme'; +import { ThemeToggle } from 'src/components/ThemeToggle'; +import './App.css'; + +export const App = () => { + const { theme, toggleTheme } = useTheme(); + + // Apply theme to document root + useEffect(() => { + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [theme]); + + return ( +
+ {/* Your existing app content */} + + +
+ ); +}; +``` + +## Step 7: Add Tests + +Create `src/hooks/useTheme.test.ts`: + +```typescript +import { renderHook, act } from '@testing-library/react'; +import { useTheme } from './useTheme'; + +// Mock localStorage +const localStorageMock = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), +}; +Object.defineProperty(window, 'localStorage', { value: localStorageMock }); + +// Mock matchMedia +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +describe('useTheme', () => { + beforeEach(() => { + localStorageMock.getItem.mockReturnValue(null); + }); + + it('should initialize with system preference', () => { + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe('light'); + expect(result.current.preference).toBe('system'); + }); + + it('should toggle theme', () => { + const { result } = renderHook(() => useTheme()); + + act(() => { + result.current.toggleTheme(); + }); + + expect(result.current.theme).toBe('dark'); + expect(result.current.preference).toBe('dark'); + }); + + it('should load saved preference from localStorage', () => { + const savedPreference = { + theme: 'dark', + persist: true, + lastChanged: Date.now(), + }; + + localStorageMock.getItem.mockReturnValue(JSON.stringify(savedPreference)); + + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe('dark'); + expect(result.current.preference).toBe('dark'); + }); +}); +``` + +## Step 8: Update CSS for Theme Transitions + +Add to `src/index.css`: + +```css +/* Prevent flash of incorrect theme */ +html { + color-scheme: light dark; +} + +/* Smooth theme transitions */ +* { + transition-property: background-color, border-color, color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} + +/* Respect reduced motion preferences */ +@media (prefers-reduced-motion: reduce) { + * { + transition-duration: 0ms !important; + } +} +``` + +## Verification Steps + +1. **Build and Test**: Run `npm run build` and `npm run test:ci` +2. **Manual Testing**: + - Toggle between themes + - Refresh browser to verify persistence + - Change system theme to verify automatic detection +3. **Accessibility Testing**: + - Test keyboard navigation + - Verify screen reader announcements + - Check color contrast ratios + +## Next Steps + +After implementation, run `/speckit.tasks` to generate the detailed task breakdown for development. From ffb770e19677e3ac692d941c72856d1a1a25b56c Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:33:55 -0500 Subject: [PATCH 15/41] docs(windsurf): update agent context --- .windsurf/rules/specify-rules.md | 7 +++-- specs/001-dark-mode/plan.md | 44 +++++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/.windsurf/rules/specify-rules.md b/.windsurf/rules/specify-rules.md index a134e2a..c1cdb6e 100644 --- a/.windsurf/rules/specify-rules.md +++ b/.windsurf/rules/specify-rules.md @@ -4,6 +4,9 @@ Auto-generated from all feature plans. Last updated: 2026-02-14 ## Active Technologies +- TypeScript 5 (React 19) + React 19, Tailwind CSS 4 (001-dark-mode) +- localStorage for theme persistence (001-dark-mode) + - TypeScript 5 (React 19) + React 19, Vite 7, Vitest 4, Tailwind CSS 4 (001-multiple-words) - localStorage for user preferences (001-multiple-words) @@ -30,11 +33,11 @@ TypeScript 5 (strict) with React 19: Follow standard conventions ## Recent Changes +- 001-dark-mode: Added TypeScript 5 (React 19) + React 19, Tailwind CSS 4 + - 001-multiple-words: Added TypeScript 5 (React 19) + React 19, Vite 7, Vitest 4, Tailwind CSS 4 - 001-component-refactor: Added TypeScript 5 (strict mode) with React 19 + React 19, Vite 7, Vitest 4, Tailwind CSS 4 -- 001-speed-reading-app: Added TypeScript 5 (strict) with React 19 + React 19, React DOM 19, Vite 7, Tailwind CSS 4 - diff --git a/specs/001-dark-mode/plan.md b/specs/001-dark-mode/plan.md index 90b9b03..ca815ea 100644 --- a/specs/001-dark-mode/plan.md +++ b/specs/001-dark-mode/plan.md @@ -31,12 +31,22 @@ Add dark mode functionality to the speed reader application with a floating togg _GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._ +### Pre-Design Evaluation ✅ + - [x] Reader comprehension impact is defined and measurable for the feature. - [x] Deterministic behavior is specified for timing/state transitions and has regression guardrails. - [x] Accessibility requirements cover keyboard navigation, semantics, focus, and responsive parity. - [x] Test strategy includes regression coverage and required quality gates (`lint`, `lint:tsc`, `test:ci`). - [x] Scope is minimal and dependency changes are justified. +### Post-Design Evaluation ✅ + +- [x] **Reader Comprehension**: Dark mode reduces eye strain in low-light conditions, improving reading comfort and potentially extending reading sessions without fatigue (measurable through user session duration). +- [x] **Deterministic Behavior**: Theme changes apply instantly with 300ms CSS transitions, state management through React Context ensures predictable behavior, localStorage persistence provides reliable session-to-session consistency. +- [x] **Accessibility**: ThemeToggle component includes keyboard navigation, ARIA labels, proper focus management, respects high contrast mode and reduced motion preferences. +- [x] **Test Strategy**: Comprehensive test coverage including unit tests for hooks, component tests for ThemeToggle, integration tests for theme persistence, and required quality gates. +- [x] **Scope Minimal**: Uses existing React/Tailwind stack, no new dependencies required, leverages browser native APIs (localStorage, matchMedia). + ## Project Structure ### Documentation (this feature) @@ -94,7 +104,33 @@ tests/ > **Fill ONLY if Constitution Check has violations that must be justified** -| Violation | Why Needed | Simpler Alternative Rejected Because | -| -------------------------- | ------------------ | ------------------------------------ | -| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | -| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | +| Violation | Why Needed | Simpler Alternative Rejected Because | +| --------- | ---------- | -------------------------------------------- | +| None | N/A | All requirements met with minimal complexity | + +## Phase Completion Status + +### Phase 0: Research ✅ COMPLETED + +- [x] Technology decisions documented +- [x] Performance considerations analyzed +- [x] Accessibility strategy defined +- [x] Edge cases identified and solutions planned + +### Phase 1: Design ✅ COMPLETED + +- [x] Data model with entities and state transitions defined +- [x] Component contracts created with interface definitions +- [x] Implementation quickstart guide generated +- [x] Agent context updated with new technology information + +### Phase 2: Tasks ⏸️ PENDING + +- [ ] Run `/speckit.tasks` to generate actionable implementation tasks +- [ ] Execute tasks following dependency order + +## Ready for Implementation + +The dark mode feature is fully planned and ready for implementation. All constitution requirements have been met, technical decisions have been documented, and comprehensive design artifacts have been created. + +**Next Step**: Execute `/speckit.tasks` to generate the detailed task breakdown for development. From 3acc874720a25bb0a07a87459ef3194431572c65 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:35:32 -0500 Subject: [PATCH 16/41] docs(specs): fix incorrect project structure in plan --- specs/001-dark-mode/plan.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/specs/001-dark-mode/plan.md b/specs/001-dark-mode/plan.md index ca815ea..63ed4fd 100644 --- a/specs/001-dark-mode/plan.md +++ b/specs/001-dark-mode/plan.md @@ -11,12 +11,6 @@ Add dark mode functionality to the speed reader application with a floating togg ## Technical Context - - **Language/Version**: TypeScript 5 (React 19) **Primary Dependencies**: React 19, Tailwind CSS 4 **Storage**: localStorage for theme persistence @@ -63,13 +57,6 @@ specs/[###-feature]/ ### Source Code (repository root) - - ```text src/ ├── components/ @@ -90,12 +77,6 @@ src/ │ └── theme.test.ts └── types/ └── index.ts - -tests/ -├── integration/ -│ └── theme.integration.test.tsx -└── unit/ - └── theme.test.tsx ``` **Structure Decision**: Single web application using React 19 with TypeScript. Theme functionality will be encapsulated in custom hooks and utility functions, with a dedicated ThemeToggle component. The existing component structure will be extended rather than restructured. From 5d593a2f0ee48051c7bd9ecd2acbb9b32ed4b5ac Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:37:32 -0500 Subject: [PATCH 17/41] docs(specs): replace React Context with hook --- specs/001-dark-mode/plan.md | 2 +- specs/001-dark-mode/research.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/specs/001-dark-mode/plan.md b/specs/001-dark-mode/plan.md index 63ed4fd..a7aa901 100644 --- a/specs/001-dark-mode/plan.md +++ b/specs/001-dark-mode/plan.md @@ -36,7 +36,7 @@ _GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._ ### Post-Design Evaluation ✅ - [x] **Reader Comprehension**: Dark mode reduces eye strain in low-light conditions, improving reading comfort and potentially extending reading sessions without fatigue (measurable through user session duration). -- [x] **Deterministic Behavior**: Theme changes apply instantly with 300ms CSS transitions, state management through React Context ensures predictable behavior, localStorage persistence provides reliable session-to-session consistency. +- [x] **Deterministic Behavior**: Theme changes apply instantly with 300ms CSS transitions, state management through custom useTheme hook ensures predictable behavior, localStorage persistence provides reliable session-to-session consistency. - [x] **Accessibility**: ThemeToggle component includes keyboard navigation, ARIA labels, proper focus management, respects high contrast mode and reduced motion preferences. - [x] **Test Strategy**: Comprehensive test coverage including unit tests for hooks, component tests for ThemeToggle, integration tests for theme persistence, and required quality gates. - [x] **Scope Minimal**: Uses existing React/Tailwind stack, no new dependencies required, leverages browser native APIs (localStorage, matchMedia). diff --git a/specs/001-dark-mode/research.md b/specs/001-dark-mode/research.md index d310d97..c8cc9da 100644 --- a/specs/001-dark-mode/research.md +++ b/specs/001-dark-mode/research.md @@ -8,16 +8,18 @@ ### Theme Management Approach -**Decision**: Use React Context + localStorage + system preference detection +**Decision**: Use custom hook + localStorage + system preference detection **Rationale**: -- React Context provides global theme state management +- Custom `useTheme` hook follows existing codebase pattern (like `useReadingSession`) - localStorage ensures persistence across sessions - `prefers-color-scheme` media query enables system theme detection +- Simple prop passing to ThemeToggle component (no deep component tree) - No additional dependencies required beyond existing React/Tailwind **Alternatives considered**: +- React Context (rejected: over-engineering for simple use case with shallow component tree) - CSS-only solution with `prefers-color-scheme` (rejected: lacks user preference persistence) - Third-party theme libraries (rejected: adds unnecessary dependencies for simple use case) @@ -43,7 +45,7 @@ - Leverages existing Tailwind setup - Provides explicit control over theme application -- Works well with React Context state management +- Works well with custom hook state management - Maintains design system consistency **Alternatives considered**: From f16cfa3e271824274f2312e77aa4e29fde6e3a21 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:39:14 -0500 Subject: [PATCH 18/41] docs(specs): update error handling --- .../contracts/component-contracts.md | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/specs/001-dark-mode/contracts/component-contracts.md b/specs/001-dark-mode/contracts/component-contracts.md index bd933f5..a1ae225 100644 --- a/specs/001-dark-mode/contracts/component-contracts.md +++ b/specs/001-dark-mode/contracts/component-contracts.md @@ -112,12 +112,14 @@ interface ErrorHandling { /** Fallback behavior when localStorage fails */ localStorageFallback: 'system-preference'; /** Behavior when JSON parsing fails */ - parseErrorFallback: 'clear-storage-and-use-system'; + parseErrorFallback: 'use-system-preference'; /** Behavior when media queries not supported */ mediaQueryFallback: 'assume-no-preference'; } ``` +**Approach**: Simple try-catch blocks following existing `storage.ts` pattern with silent failure and system preference fallback. + ## Theme Utility Contract ### Interface Definition @@ -156,17 +158,25 @@ interface StorageContract { } ``` -#### Error Recovery +#### Error Handling + +Follow existing codebase pattern with simple try-catch blocks: + +```typescript +interface ErrorHandling { + /** Fallback behavior when localStorage fails */ + localStorageFallback: 'system-preference'; + /** Error handling approach */ + errorStrategy: 'silent-failure-with-default'; +} +``` -1. **Quota Exceeded** - - Clear existing theme data - - Fall back to system preference - - Log error for debugging +**Storage Operations**: -2. **Data Corruption** - - Detect invalid JSON structure - - Remove corrupted data - - Reset to system preference +- All localStorage operations wrapped in try-catch blocks +- On any error, fall back to system preference +- No explicit error logging or data corruption detection +- Follows same pattern as existing `storage.ts` utility ## Integration Contract From 55bb7c486734af7c0504cb28f46448f549ed0413 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:42:51 -0500 Subject: [PATCH 19/41] docs(specs): update localStorage key and value --- .../contracts/component-contracts.md | 22 +++-- specs/001-dark-mode/data-model.md | 20 ++--- specs/001-dark-mode/quickstart.md | 86 ++++++------------- 3 files changed, 43 insertions(+), 85 deletions(-) diff --git a/specs/001-dark-mode/contracts/component-contracts.md b/specs/001-dark-mode/contracts/component-contracts.md index a1ae225..9a5ad98 100644 --- a/specs/001-dark-mode/contracts/component-contracts.md +++ b/specs/001-dark-mode/contracts/component-contracts.md @@ -111,7 +111,7 @@ interface UseThemeReturn { interface ErrorHandling { /** Fallback behavior when localStorage fails */ localStorageFallback: 'system-preference'; - /** Behavior when JSON parsing fails */ + /** Behavior when string validation fails */ parseErrorFallback: 'use-system-preference'; /** Behavior when media queries not supported */ mediaQueryFallback: 'assume-no-preference'; @@ -131,11 +131,13 @@ interface ThemeUtils { /** Check if high contrast mode is active */ getHighContrastMode: () => boolean; /** Save theme preference to localStorage */ - saveThemePreference: (preference: ThemePreference) => boolean; + saveThemePreference: (theme: 'light' | 'dark' | 'system') => boolean; /** Load theme preference from localStorage */ - loadThemePreference: () => ThemePreference | null; - /** Validate theme preference object */ - validateThemePreference: (data: unknown) => data is ThemePreference; + loadThemePreference: () => 'light' | 'dark' | 'system' | null; + /** Validate theme preference string */ + validateThemePreference: ( + data: unknown, + ) => data is 'light' | 'dark' | 'system'; } ``` @@ -146,15 +148,11 @@ interface ThemeUtils { ```typescript interface StorageContract { /** Storage key for theme preference */ - key: 'speedreader-theme-preference'; + key: 'speedreader.theme'; /** Data format stored */ - format: { - theme: 'light' | 'dark' | 'system'; - persist: boolean; - lastChanged: number; - }; + format: 'light' | 'dark' | 'system'; /** Maximum storage size */ - maxSize: 1024; // bytes + maxSize: 32; // bytes (string) } ``` diff --git a/specs/001-dark-mode/data-model.md b/specs/001-dark-mode/data-model.md index a616ead..bd4e927 100644 --- a/specs/001-dark-mode/data-model.md +++ b/specs/001-dark-mode/data-model.md @@ -108,24 +108,20 @@ flowchart TD ### localStorage Key ```typescript -const THEME_STORAGE_KEY = 'speedreader-theme-preference'; +const THEME_STORAGE_KEY = 'speedreader.theme'; ``` ### Stored Data Format -```json -{ - "theme": "dark", - "persist": true, - "lastChanged": 1739644800000 -} +```typescript +// Simple string value +'light' | 'dark' | 'system'; ``` ### Storage Validation -- Parse JSON safely, handle corruption gracefully -- Validate structure against ThemePreference interface -- Default to system preference on validation failure +- Validate string against allowed theme values +- Handle invalid values gracefully by using system preference ## Performance Considerations @@ -137,6 +133,6 @@ const THEME_STORAGE_KEY = 'speedreader-theme-preference'; ## Error Handling - localStorage quota exceeded: fall back to system preference -- JSON parsing errors: clear corrupted data, use system preference +- Invalid string values: fall back to system preference - Media query not supported: assume 'no-preference' -- Theme toggle errors: maintain current theme, log error +- Theme toggle errors: maintain current theme, use simple try-catch diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md index f41a2f8..3505161 100644 --- a/specs/001-dark-mode/quickstart.md +++ b/specs/001-dark-mode/quickstart.md @@ -33,15 +33,9 @@ Create `src/types/theme.ts`: ```typescript export type Theme = 'light' | 'dark' | 'system'; -export interface ThemePreference { - theme: Theme; - persist: boolean; - lastChanged: number; -} - export interface ThemeState { effectiveTheme: 'light' | 'dark'; - userPreference: ThemePreference; + userPreference: Theme; systemPreference: 'light' | 'dark' | 'no-preference'; highContrastMode: boolean; } @@ -52,9 +46,9 @@ export interface ThemeState { Create `src/utils/theme.ts`: ```typescript -import type { ThemePreference } from 'src/types/theme'; +import type { Theme } from 'src/types/theme'; -const THEME_STORAGE_KEY = 'speedreader-theme-preference'; +const THEME_STORAGE_KEY = 'speedreader.theme'; export const getSystemTheme = (): 'light' | 'dark' | 'no-preference' => { if (!window.matchMedia) return 'no-preference'; @@ -70,40 +64,28 @@ export const getHighContrastMode = (): boolean => { return window.matchMedia('(prefers-contrast: high)').matches; }; -export const saveThemePreference = (preference: ThemePreference): boolean => { +export const saveThemePreference = (theme: Theme): boolean => { try { - localStorage.setItem(THEME_STORAGE_KEY, JSON.stringify(preference)); + localStorage.setItem(THEME_STORAGE_KEY, theme); return true; } catch { return false; } }; -export const loadThemePreference = (): ThemePreference | null => { +export const loadThemePreference = (): Theme | null => { try { const stored = localStorage.getItem(THEME_STORAGE_KEY); if (!stored) return null; - const parsed = JSON.parse(stored); - return validateThemePreference(parsed) ? parsed : null; + return validateThemePreference(stored) ? stored : null; } catch { return null; } }; -export const validateThemePreference = ( - data: unknown, -): data is ThemePreference => { - return ( - typeof data === 'object' && - data !== null && - 'theme' in data && - ['light', 'dark', 'system'].includes((data as any).theme) && - 'persist' in data && - typeof (data as any).persist === 'boolean' && - 'lastChanged' in data && - typeof (data as any).lastChanged === 'number' - ); +export const validateThemePreference = (data: unknown): data is Theme => { + return typeof data === 'string' && ['light', 'dark', 'system'].includes(data); }; ``` @@ -121,11 +103,7 @@ import { loadThemePreference, } from 'src/utils/theme'; -const DEFAULT_PREFERENCE = { - theme: 'system' as Theme, - persist: true, - lastChanged: Date.now(), -}; +const DEFAULT_PREFERENCE: Theme = 'system'; export const useTheme = () => { const [themeState, setThemeState] = useState(() => { @@ -136,9 +114,9 @@ export const useTheme = () => { const preference = stored || DEFAULT_PREFERENCE; const effectiveTheme = highContrast ? 'light' - : preference.theme === 'system' + : preference === 'system' ? systemTheme - : preference.theme; + : preference; return { effectiveTheme, @@ -154,7 +132,7 @@ export const useTheme = () => { const handleChange = () => { setThemeState((prev) => { - if (prev.userPreference.theme !== 'system') return prev; + if (prev.userPreference !== 'system') return prev; const newSystemTheme = mediaQuery.matches ? 'dark' : 'light'; const effectiveTheme = prev.highContrastMode ? 'light' : newSystemTheme; @@ -180,9 +158,9 @@ export const useTheme = () => { const highContrast = mediaQuery.matches; const effectiveTheme = highContrast ? 'light' - : prev.userPreference.theme === 'system' + : prev.userPreference === 'system' ? prev.systemPreference - : prev.userPreference.theme; + : prev.userPreference; return { ...prev, @@ -199,31 +177,20 @@ export const useTheme = () => { const toggleTheme = useCallback(() => { setThemeState((prev) => { const newTheme = prev.effectiveTheme === 'light' ? 'dark' : 'light'; - const newPreference = { - theme: newTheme, - persist: true, - lastChanged: Date.now(), - }; - saveThemePreference(newPreference); + saveThemePreference(newTheme); return { ...prev, effectiveTheme: newTheme, - userPreference: newPreference, + userPreference: newTheme, }; }); }, []); const setTheme = useCallback((theme: Theme) => { setThemeState((prev) => { - const newPreference = { - theme, - persist: true, - lastChanged: Date.now(), - }; - - saveThemePreference(newPreference); + saveThemePreference(theme); const effectiveTheme = prev.highContrastMode ? 'light' @@ -234,15 +201,15 @@ export const useTheme = () => { return { ...prev, effectiveTheme, - userPreference: newPreference, + userPreference: theme, }; }); }, []); return { theme: themeState.effectiveTheme, - preference: themeState.userPreference.theme, - followingSystem: themeState.userPreference.theme === 'system', + preference: themeState.userPreference, + followingSystem: themeState.userPreference === 'system', toggleTheme, setTheme, highContrastMode: themeState.highContrastMode, @@ -369,7 +336,7 @@ export const App = () => { Create `src/hooks/useTheme.test.ts`: -```typescript +````typescript import { renderHook, act } from '@testing-library/react'; import { useTheme } from './useTheme'; @@ -421,13 +388,9 @@ describe('useTheme', () => { }); it('should load saved preference from localStorage', () => { - const savedPreference = { - theme: 'dark', - persist: true, - lastChanged: Date.now(), - }; + const savedPreference = 'dark'; - localStorageMock.getItem.mockReturnValue(JSON.stringify(savedPreference)); + localStorageMock.getItem.mockReturnValue(savedPreference); const { result } = renderHook(() => useTheme()); @@ -477,3 +440,4 @@ html { ## Next Steps After implementation, run `/speckit.tasks` to generate the detailed task breakdown for development. +```` From 4a08666b9af360d2d3abfd59d4651e4c1bfadd4e Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:45:00 -0500 Subject: [PATCH 20/41] docs(specs): add Tailwind alternative --- specs/001-dark-mode/quickstart.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md index 3505161..b99f699 100644 --- a/specs/001-dark-mode/quickstart.md +++ b/specs/001-dark-mode/quickstart.md @@ -336,7 +336,7 @@ export const App = () => { Create `src/hooks/useTheme.test.ts`: -````typescript +```typescript import { renderHook, act } from '@testing-library/react'; import { useTheme } from './useTheme'; @@ -400,21 +400,21 @@ describe('useTheme', () => { }); ``` -## Step 8: Update CSS for Theme Transitions +## Step 8: Configure Tailwind for Theme Transitions -Add to `src/index.css`: +Since this project uses Tailwind CSS 4, add theme transition support to your CSS: ```css +/* Add to src/index.css after @import 'tailwindcss' */ + /* Prevent flash of incorrect theme */ html { color-scheme: light dark; } -/* Smooth theme transitions */ +/* Tailwind CSS 4: Enable smooth transitions for theme changes */ * { - transition-property: background-color, border-color, color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 300ms; + transition-colors duration-300 ease-in-out; } /* Respect reduced motion preferences */ @@ -425,6 +425,15 @@ html { } ``` +**Alternative**: You can also apply transitions per-component using Tailwind classes: + +```tsx +// In your components, use transition classes +
+ {/* Content */} +
+``` + ## Verification Steps 1. **Build and Test**: Run `npm run build` and `npm run test:ci` @@ -440,4 +449,3 @@ html { ## Next Steps After implementation, run `/speckit.tasks` to generate the detailed task breakdown for development. -```` From d83db6fa70c01dd3e555e2b9bd34b44c4493ef34 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:51:11 -0500 Subject: [PATCH 21/41] docs(specs): remove theme transition --- .../contracts/component-contracts.md | 15 ++----- specs/001-dark-mode/plan.md | 6 +-- specs/001-dark-mode/quickstart.md | 42 ++++++++++--------- specs/001-dark-mode/research.md | 4 +- specs/001-dark-mode/spec.md | 10 ++--- 5 files changed, 34 insertions(+), 43 deletions(-) diff --git a/specs/001-dark-mode/contracts/component-contracts.md b/specs/001-dark-mode/contracts/component-contracts.md index 9a5ad98..c790b29 100644 --- a/specs/001-dark-mode/contracts/component-contracts.md +++ b/specs/001-dark-mode/contracts/component-contracts.md @@ -28,7 +28,7 @@ interface ThemeToggleProps { 1. **Click Action** - **Trigger**: User clicks the toggle button - **Action**: Calls `onThemeToggle()` callback - - **Visual Feedback**: Shows sun/moon icon transition with 300ms animation + - **Visual Feedback**: Shows sun/moon icon change 2. **Keyboard Navigation** - **Tab Order**: Toggle must be focusable and included in tab sequence @@ -45,8 +45,7 @@ interface ThemeToggleProps { 1. **Position**: Fixed position, bottom-right corner 2. **Size**: 48px × 48px minimum touch target 3. **Icons**: Sun icon for light mode, moon icon for dark mode -4. **Animation**: 300ms smooth rotation and color transition -5. **Hover State**: Slight scale increase (1.05) and background color change +4. **Hover State**: Slight scale increase and background color change ### Accessibility Contract @@ -56,8 +55,6 @@ interface AccessibilityRequirements { minTouchTarget: 48; /** Minimum color contrast ratio for normal text */ minContrastRatio: 4.5; - /** Animation duration in milliseconds */ - maxAnimationDuration: 300; /** Keyboard support required */ keyboardSupport: true; /** Screen reader support required */ @@ -182,14 +179,12 @@ interface ErrorHandling { ```typescript interface AppThemeIntegration { - /** Theme provider must wrap entire app */ + /** Theme must be applied to entire app */ providerLocation: 'root'; /** Theme must be applied before first render */ initializationTiming: 'before-render'; /** Theme changes must not cause layout shifts */ layoutStability: 'no-shift'; - /** Theme transitions must respect motion preferences */ - motionRespect: 'honor-prefers-reduced-motion'; } ``` @@ -201,9 +196,5 @@ interface CSSIntegration { tailwindStrategy: 'class'; /** Custom properties for theme colors */ cssVariables: boolean; - /** Transition duration */ - transitionDuration: '300ms'; - /** Transition easing */ - transitionEasing: 'ease-in-out'; } ``` diff --git a/specs/001-dark-mode/plan.md b/specs/001-dark-mode/plan.md index a7aa901..4774f5a 100644 --- a/specs/001-dark-mode/plan.md +++ b/specs/001-dark-mode/plan.md @@ -17,8 +17,8 @@ Add dark mode functionality to the speed reader application with a floating togg **Testing**: Vitest 4, React Testing Library **Target Platform**: Web browser **Project Type**: Web application -**Performance Goals**: <1s theme toggle, 300ms transitions, no layout shifts -**Constraints**: Must work with localStorage disabled, respect high contrast mode +**Performance Goals**: Instant theme toggle, no layout shifts +**Constraints**: Must work with localStorage disabled, respect high contrast mode **Scale/Scope**: Single page application with theme-aware components ## Constitution Check @@ -36,7 +36,7 @@ _GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._ ### Post-Design Evaluation ✅ - [x] **Reader Comprehension**: Dark mode reduces eye strain in low-light conditions, improving reading comfort and potentially extending reading sessions without fatigue (measurable through user session duration). -- [x] **Deterministic Behavior**: Theme changes apply instantly with 300ms CSS transitions, state management through custom useTheme hook ensures predictable behavior, localStorage persistence provides reliable session-to-session consistency. +- [x] **Deterministic Behavior**: Theme changes apply instantly, state management through custom useTheme hook ensures predictable behavior, localStorage persistence provides reliable session-to-session consistency. - [x] **Accessibility**: ThemeToggle component includes keyboard navigation, ARIA labels, proper focus management, respects high contrast mode and reduced motion preferences. - [x] **Test Strategy**: Comprehensive test coverage including unit tests for hooks, component tests for ThemeToggle, integration tests for theme persistence, and required quality gates. - [x] **Scope Minimal**: Uses existing React/Tailwind stack, no new dependencies required, leverages browser native APIs (localStorage, matchMedia). diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md index b99f699..ba25887 100644 --- a/specs/001-dark-mode/quickstart.md +++ b/specs/001-dark-mode/quickstart.md @@ -400,9 +400,9 @@ describe('useTheme', () => { }); ``` -## Step 8: Configure Tailwind for Theme Transitions +## Step 8: Configure Tailwind for Theme Support -Since this project uses Tailwind CSS 4, add theme transition support to your CSS: +Add theme support to your CSS: ```css /* Add to src/index.css after @import 'tailwindcss' */ @@ -411,27 +411,31 @@ Since this project uses Tailwind CSS 4, add theme transition support to your CSS html { color-scheme: light dark; } - -/* Tailwind CSS 4: Enable smooth transitions for theme changes */ -* { - transition-colors duration-300 ease-in-out; -} - -/* Respect reduced motion preferences */ -@media (prefers-reduced-motion: reduce) { - * { - transition-duration: 0ms !important; - } -} ``` -**Alternative**: You can also apply transitions per-component using Tailwind classes: +**Apply theme classes to components**: ```tsx -// In your components, use transition classes -
- {/* Content */} -
+// Main App container +
+ {/* Header text */} +
+

+ Speed Reader +

+

+ Paste text, choose your pace, and read one word at a time. +

+
+ + {/* Card section */} +
+ {/* Content */} +
+ + {/* ThemeToggle */} + +
``` ## Verification Steps diff --git a/specs/001-dark-mode/research.md b/specs/001-dark-mode/research.md index c8cc9da..3ceddbb 100644 --- a/specs/001-dark-mode/research.md +++ b/specs/001-dark-mode/research.md @@ -70,15 +70,13 @@ ## Performance Considerations -- Theme transitions will use CSS transitions (300ms) for optimal performance -- No layout shifts expected - theme changes only affect colors, not layout +- Theme changes apply instantly, no layout shifts expected - theme changes only affect colors, not layout - Theme detection happens once on app initialization - localStorage access is synchronous and fast ## Accessibility Strategy - Toggle button will have proper ARIA labels and keyboard support -- Theme changes will respect `prefers-reduced-motion` for users who disable animations - Color contrast ratios will be validated for both light and dark themes - High contrast mode detection will override dark mode when detected diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md index a9e554b..f7c7675 100644 --- a/specs/001-dark-mode/spec.md +++ b/specs/001-dark-mode/spec.md @@ -10,7 +10,6 @@ ### Session 2026-02-15 - Q: Toggle Control Location and Type → A: Bottom right floating SVG toggle with sun/moon icons -- Q: Theme Transition Animation Duration → A: 300ms smooth transition - Q: System Theme Change Behavior → A: Automatically follow system changes - Q: High Contrast Mode Interaction → A: Respect high contrast over dark mode - Q: Theme Loading State Behavior → A: Wait for stored theme before showing content @@ -82,10 +81,9 @@ User wants the application to automatically match their operating system's theme - **FR-003**: System MUST persist user's theme preference across sessions - **FR-004**: System MUST detect and respect user's operating system theme preference on first visit and automatically follow system changes - **FR-005**: System MUST maintain proper color contrast ratios for accessibility in both themes -- **FR-006**: System MUST provide 300ms smooth transitions between theme changes without flickering -- **FR-007**: System MUST handle localStorage unavailability gracefully by defaulting to system theme preference -- **FR-008**: System MUST respect high contrast mode over dark mode when detected -- **FR-009**: System MUST wait for stored theme before showing content during page load +- **FR-006**: System MUST handle localStorage unavailability gracefully by defaulting to system theme preference +- **FR-007**: System MUST respect high contrast mode over dark mode when detected +- **FR-008**: System MUST wait for stored theme before showing content during page load ### Key Entities _(include if feature involves data)_ @@ -96,7 +94,7 @@ User wants the application to automatically match their operating system's theme ### Measurable Outcomes -- **SC-001**: Users can toggle between themes in under 1 second with immediate visual feedback +- **SC-001**: Users can toggle between themes instantly with immediate visual feedback - **SC-002**: Theme preference persists across 100% of browser sessions when localStorage is available - **SC-003**: Both light and dark themes maintain WCAG AA contrast ratios (4.5:1 for normal text) - **SC-004**: 95% of users successfully find and use the theme toggle without assistance From 4ddd8a2c9d90b3f7a41d3afdf033cce59f773181 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:52:23 -0500 Subject: [PATCH 22/41] docs(specs): remove unnecessary maxSize --- specs/001-dark-mode/contracts/component-contracts.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/specs/001-dark-mode/contracts/component-contracts.md b/specs/001-dark-mode/contracts/component-contracts.md index c790b29..2065fe0 100644 --- a/specs/001-dark-mode/contracts/component-contracts.md +++ b/specs/001-dark-mode/contracts/component-contracts.md @@ -148,8 +148,6 @@ interface StorageContract { key: 'speedreader.theme'; /** Data format stored */ format: 'light' | 'dark' | 'system'; - /** Maximum storage size */ - maxSize: 32; // bytes (string) } ``` From aa80de1411449cda675fc52155599d46a385495e Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:54:56 -0500 Subject: [PATCH 23/41] docs(specs): tidy quickstart --- specs/001-dark-mode/quickstart.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md index ba25887..16843fe 100644 --- a/specs/001-dark-mode/quickstart.md +++ b/specs/001-dark-mode/quickstart.md @@ -400,18 +400,7 @@ describe('useTheme', () => { }); ``` -## Step 8: Configure Tailwind for Theme Support - -Add theme support to your CSS: - -```css -/* Add to src/index.css after @import 'tailwindcss' */ - -/* Prevent flash of incorrect theme */ -html { - color-scheme: light dark; -} -``` +## Step 8: Apply Theme Classes to Components **Apply theme classes to components**: From c6730f9fddc8622cc4f950c4a4583d75312330ef Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 21:57:20 -0500 Subject: [PATCH 24/41] docs(specs): generate tasks --- specs/001-dark-mode/tasks.md | 226 +++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 specs/001-dark-mode/tasks.md diff --git a/specs/001-dark-mode/tasks.md b/specs/001-dark-mode/tasks.md new file mode 100644 index 0000000..381e2bf --- /dev/null +++ b/specs/001-dark-mode/tasks.md @@ -0,0 +1,226 @@ +--- +description: 'Task list template for feature implementation' +--- + +# Tasks: Dark Mode + +**Input**: Design documents from `/specs/001-dark-mode/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), data-model.md, contracts/ + +**Tests**: Include test tasks for behavior changes and bug fixes. Tests may be omitted only for +documentation-only or non-functional chores, and the omission MUST be justified in tasks.md. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +- **Single project**: `src/`, `tests/` at repository root +- **Web app**: `backend/src/`, `frontend/src/` +- **Mobile**: `api/src/`, `ios/src/` or `android/src/` +- Paths shown below assume single project - adjust based on plan.md structure + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Project initialization and basic structure + +- [ ] T001 Configure Tailwind CSS dark mode with class strategy in tailwind.config.js +- [ ] T002 [P] Create theme types in src/types/theme.ts +- [ ] T003 [P] Establish accessibility and responsive test checklist for dark mode feature + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +- [ ] T004 [P] Implement theme utility functions in src/utils/theme.ts +- [ ] T005 [P] Create useTheme hook in src/hooks/useTheme.ts +- [ ] T006 Create ThemeToggle component structure in src/components/ThemeToggle/ +- [ ] T007 [P] Create ThemeToggle types in src/components/ThemeToggle/ThemeToggle.types.ts + +**Checkpoint**: Foundation ready - user story implementation can now begin in parallel + +--- + +## Phase 3: User Story 1 - Toggle Dark Mode (Priority: P1) 🎯 MVP + +**Goal**: User wants to switch between light and dark themes to reduce eye strain during reading in low-light conditions. + +**Independent Test**: Can be fully tested by toggling the theme switch and verifying the UI changes between light and dark modes. + +### Tests for User Story 1 + +- [ ] T008 [P] [US1] Create useTheme hook tests in src/hooks/useTheme.test.ts +- [ ] T009 [P] [US1] Create theme utility tests in src/utils/theme.test.ts +- [ ] T010 [P] [US1] Create ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx + +### Implementation for User Story 1 + +- [ ] T011 [US1] Implement ThemeToggle component in src/components/ThemeToggle/ThemeToggle.tsx +- [ ] T012 [US1] Create ThemeToggle barrel export in src/components/ThemeToggle/index.ts +- [ ] T013 [US1] Integrate useTheme hook in src/components/App/App.tsx +- [ ] T014 [US1] Apply theme classes to existing components in src/components/App/App.tsx +- [ ] T015 [US1] Add theme transition styles to src/index.css + +**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently + +--- + +## Phase 4: User Story 2 - Persistent Theme Preference (Priority: P2) + +**Goal**: User wants their theme preference to be remembered across sessions so they don't have to manually switch each time. + +**Independent Test**: Can be tested by setting a theme, closing/reopening the application, and verifying the theme persists. + +### Tests for User Story 2 + +- [ ] T016 [P] [US2] Test localStorage persistence in src/hooks/useTheme.test.ts +- [ ] T017 [P] [US2] Test localStorage error handling in src/utils/theme.test.ts + +### Implementation for User Story 2 + +- [ ] T018 [US2] Implement localStorage save functionality in src/utils/theme.ts +- [ ] T019 [US2] Implement localStorage load functionality in src/utils/theme.ts +- [ ] T020 [US2] Add localStorage error handling in src/hooks/useTheme.ts +- [ ] T021 [US2] Test theme persistence across browser sessions + +**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently + +--- + +## Phase 5: User Story 3 - System Theme Detection (Priority: P3) + +**Goal**: User wants the application to automatically match their operating system's theme preference. + +**Independent Test**: Can be tested by changing OS theme settings and verifying the application responds accordingly. + +### Tests for User Story 3 + +- [ ] T022 [P] [US3] Test system theme detection in src/utils/theme.test.ts +- [ ] T023 [P] [US3] Test system theme change listeners in src/hooks/useTheme.test.ts + +### Implementation for User Story 3 + +- [ ] T024 [US3] Implement system theme detection in src/utils/theme.ts +- [ ] T025 [US3] Add system theme change listeners in src/hooks/useTheme.ts +- [ ] T026 [US3] Implement high contrast mode detection in src/utils/theme.ts +- [ ] T027 [US3] Add high contrast mode handling in src/hooks/useTheme.ts +- [ ] T028 [US3] Update ThemeToggle to show system preference state + +**Checkpoint**: All user stories should now be independently functional + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Improvements that affect multiple user stories + +- [ ] T029 [P] Update README.md with dark mode feature documentation +- [ ] T030 Code cleanup and refactoring for theme implementation +- [ ] T031 Performance optimization for theme transitions +- [ ] T032 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints +- [ ] T033 [P] Additional regression tests for theme functionality +- [ ] T034 Security hardening for localStorage usage +- [ ] T035 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies - can start immediately +- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories +- **User Stories (Phase 3+)**: All depend on Foundational phase completion + - User stories can then proceed in parallel (if staffed) + - Or sequentially in priority order (P1 → P2 → P3) +- **Polish (Final Phase)**: Depends on all desired user stories being complete + +### User Story Dependencies + +- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories +- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - Extends US1 with persistence +- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - Extends US1/US2 with system detection + +### Within Each User Story + +- Tests MUST be written and FAIL before implementation +- Utilities before hooks +- Hooks before components +- Components before integration +- Story complete before moving to next priority + +### Parallel Opportunities + +- All Setup tasks marked [P] can run in parallel +- All Foundational tasks marked [P] can run in parallel (within Phase 2) +- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows) +- All tests for a user story marked [P] can run in parallel +- Different user stories can be worked on in parallel by different team members + +--- + +## Parallel Example: User Story 1 + +```bash +# Launch all tests for User Story 1 together: +Task: "Create useTheme hook tests in src/hooks/useTheme.test.ts" +Task: "Create theme utility tests in src/utils/theme.test.ts" +Task: "Create ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx" + +# Launch all utilities for User Story 1 together: +Task: "Implement ThemeToggle component in src/components/ThemeToggle/ThemeToggle.tsx" +Task: "Create ThemeToggle barrel export in src/components/ThemeToggle/index.ts" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup +2. Complete Phase 2: Foundational (CRITICAL - blocks all stories) +3. Complete Phase 3: User Story 1 +4. **STOP and VALIDATE**: Test User Story 1 independently +5. Deploy/demo if ready + +### Incremental Delivery + +1. Complete Setup + Foundational → Foundation ready +2. Add User Story 1 → Test independently → Deploy/Demo (MVP!) +3. Add User Story 2 → Test independently → Deploy/Demo +4. Add User Story 3 → Test independently → Deploy/Demo +5. Each story adds value without breaking previous stories + +### Parallel Team Strategy + +With multiple developers: + +1. Team completes Setup + Foundational together +2. Once Foundational is done: + - Developer A: User Story 1 + - Developer B: User Story 2 + - Developer C: User Story 3 +3. Stories complete and integrate independently + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story should be independently completable and testable +- Verify tests fail before implementing behavior changes +- Document justification for any omitted tests +- Commit after each task or logical group +- Stop at any checkpoint to validate story independently +- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence From 44bb8595682507f36a2a42a802b1239660610fd8 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 22:01:31 -0500 Subject: [PATCH 25/41] docs(specs): enforce TDD --- AGENTS.md | 3 ++- specs/001-dark-mode/tasks.md | 47 ++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 91465ea..c3002eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ You're an expert engineer for this React app. - Vite 7 (build tool) - Vitest 4 (testing framework) - Node.js 24 - - Tailwind CSS 4 + - Tailwind CSS 4 (with dark mode support) - ESLint 9 with TypeScript support - Prettier with Tailwind plugin - React Compiler (babel-plugin-react-compiler) @@ -108,6 +108,7 @@ import type { User } from './types'; ### Testing Standards +- **TDD ENFORCED** - tests MUST be written FIRST and verified to FAIL before implementation for all behavior changes (red, green, refactor) - **100% coverage required** - all statements, branches, functions, and lines (except for barrel exports) - **Do not test barrel exports** - index.ts files are barrel exports and should not have dedicated tests - **Testing Library** - use @testing-library/react for component testing diff --git a/specs/001-dark-mode/tasks.md b/specs/001-dark-mode/tasks.md index 381e2bf..67a48fb 100644 --- a/specs/001-dark-mode/tasks.md +++ b/specs/001-dark-mode/tasks.md @@ -7,8 +7,7 @@ description: 'Task list template for feature implementation' **Input**: Design documents from `/specs/001-dark-mode/` **Prerequisites**: plan.md (required), spec.md (required for user stories), data-model.md, contracts/ -**Tests**: Include test tasks for behavior changes and bug fixes. Tests may be omitted only for -documentation-only or non-functional chores, and the omission MUST be justified in tasks.md. +**Tests**: TDD approach enforced - tests MUST be written and FAIL before implementation for all behavior changes. **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. @@ -56,11 +55,13 @@ documentation-only or non-functional chores, and the omission MUST be justified **Independent Test**: Can be fully tested by toggling the theme switch and verifying the UI changes between light and dark modes. -### Tests for User Story 1 +### Tests for User Story 1 (TDD REQUIRED) -- [ ] T008 [P] [US1] Create useTheme hook tests in src/hooks/useTheme.test.ts -- [ ] T009 [P] [US1] Create theme utility tests in src/utils/theme.test.ts -- [ ] T010 [P] [US1] Create ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx +> **⚠️ TDD ENFORCED**: Write these tests FIRST, ensure they FAIL before implementation + +- [ ] T008 [P] [US1] Write FAILING useTheme hook tests in src/hooks/useTheme.test.ts +- [ ] T009 [P] [US1] Write FAILING theme utility tests in src/utils/theme.test.ts +- [ ] T010 [P] [US1] Write FAILING ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx ### Implementation for User Story 1 @@ -80,10 +81,12 @@ documentation-only or non-functional chores, and the omission MUST be justified **Independent Test**: Can be tested by setting a theme, closing/reopening the application, and verifying the theme persists. -### Tests for User Story 2 +### Tests for User Story 2 (TDD REQUIRED) + +> **⚠️ TDD ENFORCED**: Write these tests FIRST, ensure they FAIL before implementation -- [ ] T016 [P] [US2] Test localStorage persistence in src/hooks/useTheme.test.ts -- [ ] T017 [P] [US2] Test localStorage error handling in src/utils/theme.test.ts +- [ ] T016 [P] [US2] Write FAILING localStorage persistence tests in src/hooks/useTheme.test.ts +- [ ] T017 [P] [US2] Write FAILING localStorage error handling tests in src/utils/theme.test.ts ### Implementation for User Story 2 @@ -102,10 +105,12 @@ documentation-only or non-functional chores, and the omission MUST be justified **Independent Test**: Can be tested by changing OS theme settings and verifying the application responds accordingly. -### Tests for User Story 3 +### Tests for User Story 3 (TDD REQUIRED) + +> **⚠️ TDD ENFORCED**: Write these tests FIRST, ensure they FAIL before implementation -- [ ] T022 [P] [US3] Test system theme detection in src/utils/theme.test.ts -- [ ] T023 [P] [US3] Test system theme change listeners in src/hooks/useTheme.test.ts +- [ ] T022 [P] [US3] Write FAILING system theme detection tests in src/utils/theme.test.ts +- [ ] T023 [P] [US3] Write FAILING system theme change listener tests in src/hooks/useTheme.test.ts ### Implementation for User Story 3 @@ -152,7 +157,7 @@ documentation-only or non-functional chores, and the omission MUST be justified ### Within Each User Story -- Tests MUST be written and FAIL before implementation +- **TDD ENFORCED**: Tests MUST be written and FAIL before implementation (no exceptions) - Utilities before hooks - Hooks before components - Components before integration @@ -168,15 +173,15 @@ documentation-only or non-functional chores, and the omission MUST be justified --- -## Parallel Example: User Story 1 +## Parallel Example: User Story 1 (TDD APPROACH) ```bash -# Launch all tests for User Story 1 together: -Task: "Create useTheme hook tests in src/hooks/useTheme.test.ts" -Task: "Create theme utility tests in src/utils/theme.test.ts" -Task: "Create ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx" +# Step 1: Write all FAILING tests for User Story 1: +Task: "Write FAILING useTheme hook tests in src/hooks/useTheme.test.ts" +Task: "Write FAILING theme utility tests in src/utils/theme.test.ts" +Task: "Write FAILING ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx" -# Launch all utilities for User Story 1 together: +# Step 2: Verify all tests FAIL, then implement to make them pass Task: "Implement ThemeToggle component in src/components/ThemeToggle/ThemeToggle.tsx" Task: "Create ThemeToggle barrel export in src/components/ThemeToggle/index.ts" ``` @@ -218,9 +223,9 @@ With multiple developers: - [P] tasks = different files, no dependencies - [Story] label maps task to specific user story for traceability +- **TDD ENFORCED**: Tests MUST be written first and verified to FAIL before implementation - Each user story should be independently completable and testable -- Verify tests fail before implementing behavior changes -- Document justification for any omitted tests +- No exceptions to TDD rule for behavior changes - Commit after each task or logical group - Stop at any checkpoint to validate story independently - Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence From c814c8c7b4208f2e7bc39a2d1796d4f323d7009a Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 22:07:47 -0500 Subject: [PATCH 26/41] docs(specs): remediate issues about test-first --- AGENTS.md | 2 +- specs/001-dark-mode/spec.md | 15 +++++----- specs/001-dark-mode/tasks.md | 53 ++++++++++++++++++------------------ 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c3002eb..b5c7c1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ import type { User } from './types'; ### Testing Standards -- **TDD ENFORCED** - tests MUST be written FIRST and verified to FAIL before implementation for all behavior changes (red, green, refactor) +- **TDD** - tests MUST be written first and validated before implementation (red, green, refactor) - **100% coverage required** - all statements, branches, functions, and lines (except for barrel exports) - **Do not test barrel exports** - index.ts files are barrel exports and should not have dedicated tests - **Testing Library** - use @testing-library/react for component testing diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md index f7c7675..cc431c7 100644 --- a/specs/001-dark-mode/spec.md +++ b/specs/001-dark-mode/spec.md @@ -61,23 +61,24 @@ User wants the application to automatically match their operating system's theme ### Edge Cases -- What happens when localStorage is disabled or full? -- How does system handle theme switching during page load? System must wait for stored theme before showing content -- What happens when system theme changes while application is open? -- How does system handle high contrast mode accessibility settings? System must respect high contrast over dark mode +- **localStorage disabled**: System MUST default to system theme preference and continue functioning +- **localStorage quota exceeded**: System MUST gracefully fall back to system theme preference +- **Theme switching during page load**: System MUST wait for stored theme before showing content (FR-008) +- **System theme changes while app open**: System MUST automatically update theme when following system preference +- **High contrast mode activation**: System MUST respect high contrast over dark mode when detected ## Requirements _(mandatory)_ ### Constitution Alignment _(mandatory)_ - **Comprehension Outcome**: Dark mode reduces eye strain in low-light conditions, improving reading comfort and potentially extending reading sessions without fatigue. -- **Deterministic Behavior**: Theme changes must apply instantly and consistently across all UI elements, with no flickering or partial updates. +- **Deterministic Behavior**: Theme changes must apply within 100ms consistently across all UI elements, with no flickering or partial updates. - **Accessibility Coverage**: Theme toggle must be keyboard accessible, properly labeled for screen readers, and maintain sufficient color contrast ratios in both modes. ### Functional Requirements - **FR-001**: System MUST provide a bottom right floating SVG toggle with sun/moon icons to switch between light and dark themes -- **FR-002**: System MUST apply theme changes immediately to all UI elements +- **FR-002**: System MUST apply theme changes within 100ms to all UI elements with no flickering or partial updates - **FR-003**: System MUST persist user's theme preference across sessions - **FR-004**: System MUST detect and respect user's operating system theme preference on first visit and automatically follow system changes - **FR-005**: System MUST maintain proper color contrast ratios for accessibility in both themes @@ -94,7 +95,7 @@ User wants the application to automatically match their operating system's theme ### Measurable Outcomes -- **SC-001**: Users can toggle between themes instantly with immediate visual feedback +- **SC-001**: Users can toggle between themes within 100ms with immediate visual feedback and no layout shifts - **SC-002**: Theme preference persists across 100% of browser sessions when localStorage is available - **SC-003**: Both light and dark themes maintain WCAG AA contrast ratios (4.5:1 for normal text) - **SC-004**: 95% of users successfully find and use the theme toggle without assistance diff --git a/specs/001-dark-mode/tasks.md b/specs/001-dark-mode/tasks.md index 67a48fb..9b0757c 100644 --- a/specs/001-dark-mode/tasks.md +++ b/specs/001-dark-mode/tasks.md @@ -7,7 +7,7 @@ description: 'Task list template for feature implementation' **Input**: Design documents from `/specs/001-dark-mode/` **Prerequisites**: plan.md (required), spec.md (required for user stories), data-model.md, contracts/ -**Tests**: TDD approach enforced - tests MUST be written and FAIL before implementation for all behavior changes. +**Tests**: Test-First Quality Gates enforced - tests MUST be written and validated before implementation for behavior changes, following constitution principle IV. **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. @@ -57,7 +57,7 @@ description: 'Task list template for feature implementation' ### Tests for User Story 1 (TDD REQUIRED) -> **⚠️ TDD ENFORCED**: Write these tests FIRST, ensure they FAIL before implementation +> **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation - [ ] T008 [P] [US1] Write FAILING useTheme hook tests in src/hooks/useTheme.test.ts - [ ] T009 [P] [US1] Write FAILING theme utility tests in src/utils/theme.test.ts @@ -70,6 +70,7 @@ description: 'Task list template for feature implementation' - [ ] T013 [US1] Integrate useTheme hook in src/components/App/App.tsx - [ ] T014 [US1] Apply theme classes to existing components in src/components/App/App.tsx - [ ] T015 [US1] Add theme transition styles to src/index.css +- [ ] T016 [US1] Implement theme loading state management in src/components/App/App.tsx to wait for stored theme before showing content **Checkpoint**: At this point, User Story 1 should be fully functional and testable independently @@ -83,17 +84,17 @@ description: 'Task list template for feature implementation' ### Tests for User Story 2 (TDD REQUIRED) -> **⚠️ TDD ENFORCED**: Write these tests FIRST, ensure they FAIL before implementation +> **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation -- [ ] T016 [P] [US2] Write FAILING localStorage persistence tests in src/hooks/useTheme.test.ts -- [ ] T017 [P] [US2] Write FAILING localStorage error handling tests in src/utils/theme.test.ts +- [ ] T017 [P] [US2] Write FAILING localStorage persistence tests in src/hooks/useTheme.test.ts +- [ ] T018 [P] [US2] Write FAILING localStorage error handling tests in src/utils/theme.test.ts ### Implementation for User Story 2 -- [ ] T018 [US2] Implement localStorage save functionality in src/utils/theme.ts -- [ ] T019 [US2] Implement localStorage load functionality in src/utils/theme.ts -- [ ] T020 [US2] Add localStorage error handling in src/hooks/useTheme.ts -- [ ] T021 [US2] Test theme persistence across browser sessions +- [ ] T019 [US2] Implement localStorage save functionality in src/utils/theme.ts +- [ ] T020 [US2] Implement localStorage load functionality in src/utils/theme.ts +- [ ] T021 [US2] Add localStorage error handling in src/hooks/useTheme.ts +- [ ] T022 [US2] Test theme persistence across browser sessions **Checkpoint**: At this point, User Stories 1 AND 2 should both work independently @@ -107,18 +108,18 @@ description: 'Task list template for feature implementation' ### Tests for User Story 3 (TDD REQUIRED) -> **⚠️ TDD ENFORCED**: Write these tests FIRST, ensure they FAIL before implementation +> **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation -- [ ] T022 [P] [US3] Write FAILING system theme detection tests in src/utils/theme.test.ts -- [ ] T023 [P] [US3] Write FAILING system theme change listener tests in src/hooks/useTheme.test.ts +- [ ] T023 [P] [US3] Write FAILING system theme detection tests in src/utils/theme.test.ts +- [ ] T024 [P] [US3] Write FAILING system theme change listener tests in src/hooks/useTheme.test.ts ### Implementation for User Story 3 -- [ ] T024 [US3] Implement system theme detection in src/utils/theme.ts -- [ ] T025 [US3] Add system theme change listeners in src/hooks/useTheme.ts -- [ ] T026 [US3] Implement high contrast mode detection in src/utils/theme.ts -- [ ] T027 [US3] Add high contrast mode handling in src/hooks/useTheme.ts -- [ ] T028 [US3] Update ThemeToggle to show system preference state +- [ ] T025 [US3] Implement system theme detection in src/utils/theme.ts +- [ ] T026 [US3] Add system theme change listeners in src/hooks/useTheme.ts +- [ ] T027 [US3] Implement high contrast mode detection in src/utils/theme.ts +- [ ] T028 [US3] Add high contrast mode handling in src/hooks/useTheme.ts +- [ ] T029 [US3] Update ThemeToggle to show system preference state **Checkpoint**: All user stories should now be independently functional @@ -128,13 +129,13 @@ description: 'Task list template for feature implementation' **Purpose**: Improvements that affect multiple user stories -- [ ] T029 [P] Update README.md with dark mode feature documentation -- [ ] T030 Code cleanup and refactoring for theme implementation -- [ ] T031 Performance optimization for theme transitions -- [ ] T032 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints -- [ ] T033 [P] Additional regression tests for theme functionality -- [ ] T034 Security hardening for localStorage usage -- [ ] T035 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` +- [ ] T030 [P] Update README.md with dark mode feature documentation +- [ ] T031 Code cleanup and refactoring for theme implementation +- [ ] T032 Performance optimization for theme transitions +- [ ] T033 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints +- [ ] T034 [P] Additional regression tests for theme functionality +- [ ] T035 Security hardening for localStorage usage +- [ ] T036 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` --- @@ -157,7 +158,7 @@ description: 'Task list template for feature implementation' ### Within Each User Story -- **TDD ENFORCED**: Tests MUST be written and FAIL before implementation (no exceptions) +- **TEST-FIRST ENFORCED**: Tests MUST be written and validated before implementation (no exceptions) - Utilities before hooks - Hooks before components - Components before integration @@ -223,7 +224,7 @@ With multiple developers: - [P] tasks = different files, no dependencies - [Story] label maps task to specific user story for traceability -- **TDD ENFORCED**: Tests MUST be written first and verified to FAIL before implementation +- **Test-First ENFORCED**: Tests MUST be written first and validated before implementation - Each user story should be independently completable and testable - No exceptions to TDD rule for behavior changes - Commit after each task or logical group From c6a525a64e1afe452af6fdf878065880a7a42bac Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 22:11:17 -0500 Subject: [PATCH 27/41] docs(specs): remove README task --- specs/001-dark-mode/tasks.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/specs/001-dark-mode/tasks.md b/specs/001-dark-mode/tasks.md index 9b0757c..b4998a7 100644 --- a/specs/001-dark-mode/tasks.md +++ b/specs/001-dark-mode/tasks.md @@ -129,13 +129,12 @@ description: 'Task list template for feature implementation' **Purpose**: Improvements that affect multiple user stories -- [ ] T030 [P] Update README.md with dark mode feature documentation -- [ ] T031 Code cleanup and refactoring for theme implementation -- [ ] T032 Performance optimization for theme transitions -- [ ] T033 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints -- [ ] T034 [P] Additional regression tests for theme functionality -- [ ] T035 Security hardening for localStorage usage -- [ ] T036 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` +- [ ] T030 Code cleanup and refactoring for theme implementation +- [ ] T031 Performance optimization for theme transitions +- [ ] T032 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints +- [ ] T033 [P] Additional regression tests for theme functionality +- [ ] T034 Security hardening for localStorage usage +- [ ] T035 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` --- From 419380a8e63a12c97a703ab35735c00082845592 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 22:40:03 -0500 Subject: [PATCH 28/41] feat: add dark mode --- index.html | 20 ++ .../001-dark-mode/checklists/accessibility.md | 52 +++++ specs/001-dark-mode/quickstart.md | 32 ++- specs/001-dark-mode/tasks.md | 70 +++--- src/components/App/App.tsx | 13 +- .../ThemeToggle/ThemeToggle.test.tsx | 137 ++++++++++++ src/components/ThemeToggle/ThemeToggle.tsx | 55 +++++ .../ThemeToggle/ThemeToggle.types.ts | 5 + src/components/ThemeToggle/index.ts | 2 + src/hooks/useTheme.test.ts | 193 ++++++++++++++++ src/hooks/useTheme.ts | 136 ++++++++++++ src/index.css | 10 + src/setupTests.ts | 16 ++ src/types/theme.ts | 8 + src/utils/theme.test.ts | 207 ++++++++++++++++++ src/utils/theme.ts | 37 ++++ 16 files changed, 948 insertions(+), 45 deletions(-) create mode 100644 specs/001-dark-mode/checklists/accessibility.md create mode 100644 src/components/ThemeToggle/ThemeToggle.test.tsx create mode 100644 src/components/ThemeToggle/ThemeToggle.tsx create mode 100644 src/components/ThemeToggle/ThemeToggle.types.ts create mode 100644 src/components/ThemeToggle/index.ts create mode 100644 src/hooks/useTheme.test.ts create mode 100644 src/hooks/useTheme.ts create mode 100644 src/types/theme.ts create mode 100644 src/utils/theme.test.ts create mode 100644 src/utils/theme.ts diff --git a/index.html b/index.html index fae2401..8c18e5c 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,26 @@
+ diff --git a/specs/001-dark-mode/checklists/accessibility.md b/specs/001-dark-mode/checklists/accessibility.md new file mode 100644 index 0000000..7b7df82 --- /dev/null +++ b/specs/001-dark-mode/checklists/accessibility.md @@ -0,0 +1,52 @@ +# Accessibility & Responsive Test Checklist: Dark Mode + +**Purpose**: Validate accessibility and responsive design for dark mode feature +**Created**: 2026-02-15 +**Feature**: Dark Mode + +## Keyboard Navigation + +- [ ] ThemeToggle is focusable via Tab key +- [ ] ThemeToggle activates with Enter key +- [ ] ThemeToggle activates with Space key +- [ ] Focus indicator is visible (2px outline) +- [ ] Focus order is logical and predictable + +## Semantics & ARIA + +- [ ] ThemeToggle has proper ARIA label describing current state +- [ ] ThemeToggle role is "button" +- [ ] Theme changes are announced to screen readers +- [ ] No ARIA violations detected + +## Responsive Design + +- [ ] ThemeToggle is visible and functional on mobile (320px+) +- [ ] ThemeToggle is visible and functional on tablet (768px+) +- [ ] ThemeToggle is visible and functional on desktop (1024px+) +- [ ] Touch target meets minimum 48px × 48px requirement +- [ ] No layout shifts occur during theme transitions + +## Color Contrast + +- [ ] Light mode text meets WCAG AA contrast ratio (4.5:1) +- [ ] Dark mode text meets WCAG AA contrast ratio (4.5:1) +- [ ] Focus indicators meet contrast requirements +- [ ] Interactive elements meet contrast requirements + +## High Contrast Mode + +- [ ] High contrast mode is detected correctly +- [ ] High contrast mode overrides dark mode when active +- [ ] Theme remains functional in high contrast mode + +## Reduced Motion + +- [ ] Theme transitions respect prefers-reduced-motion +- [ ] No animations when reduced motion is preferred +- [ ] Functionality remains intact without animations + +## Notes + +- All items must be checked before feature is considered complete +- Use browser DevTools and accessibility testing tools for validation diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md index 16843fe..377368b 100644 --- a/specs/001-dark-mode/quickstart.md +++ b/specs/001-dark-mode/quickstart.md @@ -17,15 +17,35 @@ This quickstart guide provides the step-by-step approach to implement dark mode ## Step 1: Configure Tailwind CSS Dark Mode -Update `tailwind.config.js` to enable dark mode with class strategy: +**Tailwind CSS v4** uses CSS-based configuration. Add dark mode variant to `src/index.css`: + +```css +@import 'tailwindcss'; + +@theme { + --color-*: initial; + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + --color-slate-100: #f1f5f9; + --color-slate-200: #e2e8f0; + --color-slate-300: #cbd5e1; + --color-slate-700: #334155; + --color-slate-900: #0f172a; +} -```javascript -module.exports = { - darkMode: 'class', // Enable class-based dark mode - // ... existing config -}; +@variant dark (&:where(.dark, .dark *)); ``` +This enables class-based dark mode where styles are applied when `.dark` class is present on an element or its ancestors. + ## Step 2: Create Theme Types Create `src/types/theme.ts`: diff --git a/specs/001-dark-mode/tasks.md b/specs/001-dark-mode/tasks.md index b4998a7..1fd2f94 100644 --- a/specs/001-dark-mode/tasks.md +++ b/specs/001-dark-mode/tasks.md @@ -28,9 +28,9 @@ description: 'Task list template for feature implementation' **Purpose**: Project initialization and basic structure -- [ ] T001 Configure Tailwind CSS dark mode with class strategy in tailwind.config.js -- [ ] T002 [P] Create theme types in src/types/theme.ts -- [ ] T003 [P] Establish accessibility and responsive test checklist for dark mode feature +- [x] T001 Configure Tailwind CSS v4 dark mode with @variant directive in src/index.css +- [x] T002 [P] Create theme types in src/types/theme.ts +- [x] T003 [P] Establish accessibility and responsive test checklist for dark mode feature --- @@ -40,10 +40,10 @@ description: 'Task list template for feature implementation' **⚠️ CRITICAL**: No user story work can begin until this phase is complete -- [ ] T004 [P] Implement theme utility functions in src/utils/theme.ts -- [ ] T005 [P] Create useTheme hook in src/hooks/useTheme.ts -- [ ] T006 Create ThemeToggle component structure in src/components/ThemeToggle/ -- [ ] T007 [P] Create ThemeToggle types in src/components/ThemeToggle/ThemeToggle.types.ts +- [x] T004 [P] Implement theme utility functions in src/utils/theme.ts +- [x] T005 [P] Create useTheme hook in src/hooks/useTheme.ts +- [x] T006 Create ThemeToggle component structure in src/components/ThemeToggle/ +- [x] T007 [P] Create ThemeToggle types in src/components/ThemeToggle/ThemeToggle.types.ts **Checkpoint**: Foundation ready - user story implementation can now begin in parallel @@ -59,18 +59,18 @@ description: 'Task list template for feature implementation' > **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation -- [ ] T008 [P] [US1] Write FAILING useTheme hook tests in src/hooks/useTheme.test.ts -- [ ] T009 [P] [US1] Write FAILING theme utility tests in src/utils/theme.test.ts -- [ ] T010 [P] [US1] Write FAILING ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx +- [x] T008 [P] [US1] Write FAILING useTheme hook tests in src/hooks/useTheme.test.ts +- [x] T009 [P] [US1] Write FAILING theme utility tests in src/utils/theme.test.ts +- [x] T010 [P] [US1] Write FAILING ThemeToggle component tests in src/components/ThemeToggle/ThemeToggle.test.tsx ### Implementation for User Story 1 -- [ ] T011 [US1] Implement ThemeToggle component in src/components/ThemeToggle/ThemeToggle.tsx -- [ ] T012 [US1] Create ThemeToggle barrel export in src/components/ThemeToggle/index.ts -- [ ] T013 [US1] Integrate useTheme hook in src/components/App/App.tsx -- [ ] T014 [US1] Apply theme classes to existing components in src/components/App/App.tsx -- [ ] T015 [US1] Add theme transition styles to src/index.css -- [ ] T016 [US1] Implement theme loading state management in src/components/App/App.tsx to wait for stored theme before showing content +- [x] T011 [US1] Implement ThemeToggle component in src/components/ThemeToggle/ThemeToggle.tsx +- [x] T012 [US1] Create ThemeToggle barrel export in src/components/ThemeToggle/index.ts +- [x] T013 [US1] Integrate useTheme hook in src/components/App/App.tsx +- [x] T014 [US1] Apply theme classes to existing components in src/components/App/App.tsx +- [x] T015 [US1] Add theme transition styles to src/index.css +- [x] T016 [US1] Implement theme loading state management in src/components/App/App.tsx to wait for stored theme before showing content **Checkpoint**: At this point, User Story 1 should be fully functional and testable independently @@ -86,15 +86,15 @@ description: 'Task list template for feature implementation' > **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation -- [ ] T017 [P] [US2] Write FAILING localStorage persistence tests in src/hooks/useTheme.test.ts -- [ ] T018 [P] [US2] Write FAILING localStorage error handling tests in src/utils/theme.test.ts +- [x] T017 [P] [US2] Write FAILING localStorage persistence tests in src/hooks/useTheme.test.ts +- [x] T018 [P] [US2] Write FAILING localStorage error handling tests in src/utils/theme.test.ts ### Implementation for User Story 2 -- [ ] T019 [US2] Implement localStorage save functionality in src/utils/theme.ts -- [ ] T020 [US2] Implement localStorage load functionality in src/utils/theme.ts -- [ ] T021 [US2] Add localStorage error handling in src/hooks/useTheme.ts -- [ ] T022 [US2] Test theme persistence across browser sessions +- [x] T019 [US2] Implement localStorage save functionality in src/utils/theme.ts +- [x] T020 [US2] Implement localStorage load functionality in src/utils/theme.ts +- [x] T021 [US2] Add localStorage error handling in src/hooks/useTheme.ts +- [x] T022 [US2] Test theme persistence across browser sessions **Checkpoint**: At this point, User Stories 1 AND 2 should both work independently @@ -110,16 +110,16 @@ description: 'Task list template for feature implementation' > **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation -- [ ] T023 [P] [US3] Write FAILING system theme detection tests in src/utils/theme.test.ts -- [ ] T024 [P] [US3] Write FAILING system theme change listener tests in src/hooks/useTheme.test.ts +- [x] T023 [P] [US3] Write FAILING system theme detection tests in src/utils/theme.test.ts +- [x] T024 [P] [US3] Write FAILING system theme change listener tests in src/hooks/useTheme.test.ts ### Implementation for User Story 3 -- [ ] T025 [US3] Implement system theme detection in src/utils/theme.ts -- [ ] T026 [US3] Add system theme change listeners in src/hooks/useTheme.ts -- [ ] T027 [US3] Implement high contrast mode detection in src/utils/theme.ts -- [ ] T028 [US3] Add high contrast mode handling in src/hooks/useTheme.ts -- [ ] T029 [US3] Update ThemeToggle to show system preference state +- [x] T025 [US3] Implement system theme detection in src/utils/theme.ts +- [x] T026 [US3] Add system theme change listeners in src/hooks/useTheme.ts +- [x] T027 [US3] Implement high contrast mode detection in src/utils/theme.ts +- [x] T028 [US3] Add high contrast mode handling in src/hooks/useTheme.ts +- [x] T029 [US3] Update ThemeToggle to show system preference state **Checkpoint**: All user stories should now be independently functional @@ -129,12 +129,12 @@ description: 'Task list template for feature implementation' **Purpose**: Improvements that affect multiple user stories -- [ ] T030 Code cleanup and refactoring for theme implementation -- [ ] T031 Performance optimization for theme transitions -- [ ] T032 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints -- [ ] T033 [P] Additional regression tests for theme functionality -- [ ] T034 Security hardening for localStorage usage -- [ ] T035 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` +- [x] T030 Code cleanup and refactoring for theme implementation +- [x] T031 Performance optimization for theme transitions +- [x] T032 [P] Accessibility verification across keyboard, semantics, and responsive breakpoints +- [x] T033 [P] Additional regression tests for theme functionality +- [x] T034 Security hardening for localStorage usage +- [x] T035 Execute quality gates: `npm run lint`, `npm run lint:tsc`, `npm run test:ci` --- diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 139563e..aa781ac 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -7,12 +7,15 @@ import { TextInput, tokenizeContent, } from 'src/components/TextInput'; +import { ThemeToggle } from 'src/components/ThemeToggle'; +import { useTheme } from 'src/hooks/useTheme'; import { SessionCompletion } from '../SessionCompletion'; import { useReadingSession } from './useReadingSession'; export default function App() { const [rawText, setRawText] = useState(''); + const { theme, toggleTheme } = useTheme(); const { currentWordIndex, @@ -53,17 +56,17 @@ export default function App() { }; return ( -
+
-

+

Speed Reader

-

+

Paste text, choose your pace, and read one word at a time.

-
+
{isSetupMode ? ( )}
+ +
); } diff --git a/src/components/ThemeToggle/ThemeToggle.test.tsx b/src/components/ThemeToggle/ThemeToggle.test.tsx new file mode 100644 index 0000000..6d9566b --- /dev/null +++ b/src/components/ThemeToggle/ThemeToggle.test.tsx @@ -0,0 +1,137 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; + +import { ThemeToggle } from './ThemeToggle'; + +describe('ThemeToggle', () => { + it('should render with light theme icon', () => { + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button', { + name: /toggle dark mode, currently light mode/i, + }); + + expect(button).toBeInTheDocument(); + }); + + it('should render with dark theme icon', () => { + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button', { + name: /toggle dark mode, currently dark mode/i, + }); + + expect(button).toBeInTheDocument(); + }); + + it('should render with system theme state', () => { + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button', { + name: /toggle dark mode, currently system mode/i, + }); + + expect(button).toBeInTheDocument(); + }); + + it('should call onThemeToggle when clicked', async () => { + const user = userEvent.setup(); + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button'); + + await user.click(button); + + expect(onThemeToggle).toHaveBeenCalledTimes(1); + }); + + it('should call onThemeToggle when activated with Enter key', async () => { + const user = userEvent.setup(); + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button'); + + button.focus(); + await user.keyboard('{Enter}'); + + expect(onThemeToggle).toHaveBeenCalledTimes(1); + }); + + it('should call onThemeToggle when activated with Space key', async () => { + const user = userEvent.setup(); + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button'); + + button.focus(); + await user.keyboard(' '); + + expect(onThemeToggle).toHaveBeenCalledTimes(1); + }); + + it('should not call onThemeToggle when disabled', async () => { + const user = userEvent.setup(); + const onThemeToggle = vi.fn(); + + render( + , + ); + + const button = screen.getByRole('button'); + + await user.click(button); + + expect(onThemeToggle).not.toHaveBeenCalled(); + }); + + it('should be focusable via keyboard', () => { + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button'); + + button.focus(); + + expect(button).toHaveFocus(); + }); + + it('should have proper ARIA label for accessibility', () => { + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button', { + name: /toggle dark mode, currently light mode/i, + }); + + expect(button).toHaveAttribute('aria-label'); + }); + + it('should have type="button" to prevent form submission', () => { + const onThemeToggle = vi.fn(); + + render(); + + const button = screen.getByRole('button'); + + expect(button).toHaveAttribute('type', 'button'); + }); +}); diff --git a/src/components/ThemeToggle/ThemeToggle.tsx b/src/components/ThemeToggle/ThemeToggle.tsx new file mode 100644 index 0000000..4a5d009 --- /dev/null +++ b/src/components/ThemeToggle/ThemeToggle.tsx @@ -0,0 +1,55 @@ +import type { ThemeToggleProps } from './ThemeToggle.types'; + +export const ThemeToggle = ({ + currentTheme, + onThemeToggle, + disabled = false, +}: ThemeToggleProps) => { + const isDark = currentTheme === 'dark'; + const isSystem = currentTheme === 'system'; + + const ariaLabel = `Toggle dark mode, currently ${ + isDark ? 'dark' : isSystem ? 'system' : 'light' + } mode`; + + return ( + + ); +}; diff --git a/src/components/ThemeToggle/ThemeToggle.types.ts b/src/components/ThemeToggle/ThemeToggle.types.ts new file mode 100644 index 0000000..d9fbb0c --- /dev/null +++ b/src/components/ThemeToggle/ThemeToggle.types.ts @@ -0,0 +1,5 @@ +export interface ThemeToggleProps { + currentTheme: 'light' | 'dark' | 'system'; + onThemeToggle: () => void; + disabled?: boolean; +} diff --git a/src/components/ThemeToggle/index.ts b/src/components/ThemeToggle/index.ts new file mode 100644 index 0000000..9ed116e --- /dev/null +++ b/src/components/ThemeToggle/index.ts @@ -0,0 +1,2 @@ +export { ThemeToggle } from './ThemeToggle'; +export type { ThemeToggleProps } from './ThemeToggle.types'; diff --git a/src/hooks/useTheme.test.ts b/src/hooks/useTheme.test.ts new file mode 100644 index 0000000..061c5a1 --- /dev/null +++ b/src/hooks/useTheme.test.ts @@ -0,0 +1,193 @@ +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useTheme } from './useTheme'; + +const localStorageMock = (() => { + let store: Record = {}; + + return { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + store = Object.keys(store).reduce>((acc, k) => { + if (k !== key) { + acc[k] = store[k]; + } + return acc; + }, {}); + }), + clear: vi.fn(() => { + store = {}; + }), + }; +})(); + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true, +}); + +const createMatchMediaMock = (matches: boolean) => ({ + matches, + media: '', + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), +}); + +describe('useTheme', () => { + beforeEach(() => { + localStorageMock.clear(); + vi.clearAllMocks(); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => { + if (query === '(prefers-color-scheme: dark)') { + return createMatchMediaMock(false); + } + if (query === '(prefers-contrast: high)') { + return createMatchMediaMock(false); + } + return createMatchMediaMock(false); + }), + }); + }); + + it('should initialize with system preference when no stored preference exists', () => { + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe('light'); + expect(result.current.preference).toBe('system'); + expect(result.current.followingSystem).toBe(true); + }); + + it('should initialize with dark theme when system prefers dark', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => { + if (query === '(prefers-color-scheme: dark)') { + return createMatchMediaMock(true); + } + return createMatchMediaMock(false); + }), + }); + + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe('dark'); + expect(result.current.preference).toBe('system'); + }); + + it('should toggle theme from light to dark', () => { + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe('light'); + + act(() => { + result.current.toggleTheme(); + }); + + expect(result.current.theme).toBe('dark'); + expect(result.current.preference).toBe('dark'); + expect(result.current.followingSystem).toBe(false); + }); + + it('should toggle theme from dark to light', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => { + if (query === '(prefers-color-scheme: dark)') { + return createMatchMediaMock(true); + } + return createMatchMediaMock(false); + }), + }); + + const { result } = renderHook(() => useTheme()); + + act(() => { + result.current.toggleTheme(); + }); + + expect(result.current.theme).toBe('light'); + expect(result.current.preference).toBe('light'); + }); + + it('should set specific theme preference', () => { + const { result } = renderHook(() => useTheme()); + + act(() => { + result.current.setTheme('dark'); + }); + + expect(result.current.theme).toBe('dark'); + expect(result.current.preference).toBe('dark'); + expect(result.current.followingSystem).toBe(false); + }); + + it('should set theme to system preference', () => { + const { result } = renderHook(() => useTheme()); + + act(() => { + result.current.setTheme('dark'); + }); + + expect(result.current.theme).toBe('dark'); + + act(() => { + result.current.setTheme('system'); + }); + + expect(result.current.theme).toBe('light'); + expect(result.current.preference).toBe('system'); + expect(result.current.followingSystem).toBe(true); + }); + + it('should detect high contrast mode', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => { + if (query === '(prefers-contrast: high)') { + return createMatchMediaMock(true); + } + return createMatchMediaMock(false); + }), + }); + + const { result } = renderHook(() => useTheme()); + + expect(result.current.highContrastMode).toBe(true); + expect(result.current.theme).toBe('light'); + }); + + it('should cleanup event listeners on unmount', () => { + const removeEventListenerMock = vi.fn(); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(() => ({ + matches: false, + media: '', + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: removeEventListenerMock, + dispatchEvent: vi.fn(), + })), + }); + + const { unmount } = renderHook(() => useTheme()); + + unmount(); + + expect(removeEventListenerMock).toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..de1bed9 --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,136 @@ +import { useCallback, useEffect, useState } from 'react'; +import type { Theme, ThemeState } from 'src/types/theme'; +import { + getHighContrastMode, + getSystemTheme, + loadThemePreference, + saveThemePreference, +} from 'src/utils/theme'; + +const DEFAULT_PREFERENCE: Theme = 'system'; + +const resolveEffectiveTheme = ( + systemTheme: 'light' | 'dark' | 'no-preference', +): 'light' | 'dark' => { + return systemTheme === 'dark' ? 'dark' : 'light'; +}; + +export const useTheme = () => { + const [themeState, setThemeState] = useState(() => { + const stored = loadThemePreference(); + const systemTheme = getSystemTheme(); + const highContrast = getHighContrastMode(); + + const preference = stored ?? DEFAULT_PREFERENCE; + const effectiveTheme = + highContrast || preference === 'system' + ? resolveEffectiveTheme(systemTheme) + : preference; + + return { + effectiveTheme, + userPreference: preference, + systemPreference: systemTheme, + highContrastMode: highContrast, + }; + }); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = () => { + setThemeState((prev) => { + if (prev.userPreference !== 'system') return prev; + + const newSystemTheme = mediaQuery.matches ? 'dark' : 'light'; + const effectiveTheme = prev.highContrastMode ? 'light' : newSystemTheme; + + return { + ...prev, + systemPreference: newSystemTheme, + effectiveTheme, + }; + }); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => { + mediaQuery.removeEventListener('change', handleChange); + }; + }, []); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-contrast: high)'); + + const handleChange = () => { + setThemeState((prev) => { + const highContrast = mediaQuery.matches; + const effectiveTheme = + highContrast || prev.userPreference === 'system' + ? resolveEffectiveTheme(prev.systemPreference) + : prev.userPreference; + + return { + ...prev, + highContrastMode: highContrast, + effectiveTheme, + }; + }); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => { + mediaQuery.removeEventListener('change', handleChange); + }; + }, []); + + useEffect(() => { + const root = document.documentElement; + if (themeState.effectiveTheme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + root.className = ''; // Ensure all classes are removed when switching to light + } + }, [themeState.effectiveTheme]); + + const toggleTheme = useCallback(() => { + setThemeState((prev) => { + const newTheme = prev.effectiveTheme === 'light' ? 'dark' : 'light'; + + saveThemePreference(newTheme); + + return { + ...prev, + effectiveTheme: newTheme, + userPreference: newTheme, + }; + }); + }, []); + + const setTheme = useCallback((theme: Theme) => { + setThemeState((prev) => { + saveThemePreference(theme); + + const effectiveTheme = + prev.highContrastMode || theme === 'system' + ? resolveEffectiveTheme(prev.systemPreference) + : theme; + + return { + ...prev, + effectiveTheme, + userPreference: theme, + }; + }); + }, []); + + return { + theme: themeState.effectiveTheme, + preference: themeState.userPreference, + followingSystem: themeState.userPreference === 'system', + toggleTheme, + setTheme, + highContrastMode: themeState.highContrastMode, + }; +}; diff --git a/src/index.css b/src/index.css index d4b5078..705a096 100644 --- a/src/index.css +++ b/src/index.css @@ -1 +1,11 @@ @import 'tailwindcss'; + +@variant dark (&:where(.dark, .dark *)); + +@media (prefers-reduced-motion: no-preference) { + * { + transition-property: color, background-color, border-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; + } +} diff --git a/src/setupTests.ts b/src/setupTests.ts index a37f87c..9b7f9c7 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -3,3 +3,19 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/vitest'; + +import { vi } from 'vitest'; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); diff --git a/src/types/theme.ts b/src/types/theme.ts new file mode 100644 index 0000000..c9ac7e4 --- /dev/null +++ b/src/types/theme.ts @@ -0,0 +1,8 @@ +export type Theme = 'light' | 'dark' | 'system'; + +export interface ThemeState { + effectiveTheme: 'light' | 'dark'; + userPreference: Theme; + systemPreference: 'light' | 'dark' | 'no-preference'; + highContrastMode: boolean; +} diff --git a/src/utils/theme.test.ts b/src/utils/theme.test.ts new file mode 100644 index 0000000..47dea92 --- /dev/null +++ b/src/utils/theme.test.ts @@ -0,0 +1,207 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + getHighContrastMode, + getSystemTheme, + loadThemePreference, + saveThemePreference, + validateThemePreference, +} from './theme'; + +const localStorageMock = (() => { + let store: Record = {}; + + return { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + store = Object.keys(store).reduce>((acc, k) => { + if (k !== key) { + acc[k] = store[k]; + } + return acc; + }, {}); + }), + clear: vi.fn(() => { + store = {}; + }), + }; +})(); + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + writable: true, +}); + +const createMatchMediaMock = (matches: boolean) => ({ + matches, + media: '', + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), +}); + +describe('getSystemTheme', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return "light" when system prefers light mode', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(() => createMatchMediaMock(false)), + }); + + expect(getSystemTheme()).toBe('light'); + }); + + it('should return "dark" when system prefers dark mode', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(() => createMatchMediaMock(true)), + }); + + expect(getSystemTheme()).toBe('dark'); + }); +}); + +describe('getHighContrastMode', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return false when high contrast mode is not active', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(() => createMatchMediaMock(false)), + }); + + expect(getHighContrastMode()).toBe(false); + }); + + it('should return true when high contrast mode is active', () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(() => createMatchMediaMock(true)), + }); + + expect(getHighContrastMode()).toBe(true); + }); +}); + +describe('saveThemePreference', () => { + beforeEach(() => { + localStorageMock.clear(); + vi.clearAllMocks(); + }); + + it('should save theme preference to localStorage', () => { + const result = saveThemePreference('dark'); + + expect(result).toBe(true); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'speedreader.theme', + 'dark', + ); + }); + + it('should save light theme preference', () => { + const result = saveThemePreference('light'); + + expect(result).toBe(true); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'speedreader.theme', + 'light', + ); + }); + + it('should save system theme preference', () => { + const result = saveThemePreference('system'); + + expect(result).toBe(true); + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'speedreader.theme', + 'system', + ); + }); + + it('should return false when localStorage throws an error', () => { + localStorageMock.setItem.mockImplementationOnce(() => { + throw new Error('Storage quota exceeded'); + }); + + const result = saveThemePreference('dark'); + + expect(result).toBe(false); + }); +}); + +describe('loadThemePreference', () => { + beforeEach(() => { + localStorageMock.clear(); + vi.clearAllMocks(); + }); + + it('should load saved theme preference from localStorage', () => { + localStorageMock.setItem('speedreader.theme', 'dark'); + + const result = loadThemePreference(); + + expect(result).toBe('dark'); + }); + + it('should return null when no preference is stored', () => { + const result = loadThemePreference(); + + expect(result).toBeNull(); + }); + + it('should return null when stored value is invalid', () => { + localStorageMock.setItem('speedreader.theme', 'invalid'); + + const result = loadThemePreference(); + + expect(result).toBeNull(); + }); + + it('should return null when localStorage throws an error', () => { + localStorageMock.getItem.mockImplementationOnce(() => { + throw new Error('Storage access denied'); + }); + + const result = loadThemePreference(); + + expect(result).toBeNull(); + }); +}); + +describe('validateThemePreference', () => { + it('should return true for valid "light" theme', () => { + expect(validateThemePreference('light')).toBe(true); + }); + + it('should return true for valid "dark" theme', () => { + expect(validateThemePreference('dark')).toBe(true); + }); + + it('should return true for valid "system" theme', () => { + expect(validateThemePreference('system')).toBe(true); + }); + + it('should return false for invalid string', () => { + expect(validateThemePreference('invalid')).toBe(false); + }); + + it('should return false for non-string values', () => { + expect(validateThemePreference(123)).toBe(false); + expect(validateThemePreference(null)).toBe(false); + expect(validateThemePreference(undefined)).toBe(false); + expect(validateThemePreference({})).toBe(false); + expect(validateThemePreference([])).toBe(false); + }); +}); diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..a08cb95 --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,37 @@ +import type { Theme } from 'src/types/theme'; + +const THEME_STORAGE_KEY = 'speedreader.theme'; + +export const getSystemTheme = (): 'light' | 'dark' | 'no-preference' => { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; +}; + +export const getHighContrastMode = (): boolean => { + return window.matchMedia('(prefers-contrast: high)').matches; +}; + +export const saveThemePreference = (theme: Theme): boolean => { + try { + localStorage.setItem(THEME_STORAGE_KEY, theme); + return true; + } catch { + return false; + } +}; + +export const loadThemePreference = (): Theme | null => { + try { + const stored = localStorage.getItem(THEME_STORAGE_KEY); + if (!stored) return null; + + return validateThemePreference(stored) ? stored : null; + } catch { + return null; + } +}; + +export const validateThemePreference = (data: unknown): data is Theme => { + return typeof data === 'string' && ['light', 'dark', 'system'].includes(data); +}; From dae418b9d33fa5a4e1a351fdd0eaf7f164f3abea Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 15 Feb 2026 23:12:11 -0500 Subject: [PATCH 29/41] fix: remove bg-white class and get test coverage to 100% --- index.html | 2 +- specs/001-dark-mode/quickstart.md | 6 +- specs/001-multiple-words/quickstart.md | 2 +- src/components/App/App.tsx | 4 +- src/components/Button/Button.test.tsx | 6 +- src/components/Button/Button.tsx | 2 +- src/components/ControlPanel/ControlPanel.tsx | 2 +- src/components/TextInput/TextInput.tsx | 2 +- src/components/ThemeToggle/ThemeToggle.tsx | 2 +- src/hooks/useTheme.test.ts | 275 +++++++++++++++++++ src/hooks/useTheme.ts | 2 + src/utils/theme.test.ts | 9 + 12 files changed, 298 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 8c18e5c..74abe5d 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,7 @@ } - +
+ + @@ -30,28 +54,8 @@ } - +
- diff --git a/src/components/ControlPanel/ControlPanel.tsx b/src/components/ControlPanel/ControlPanel.tsx index a76d0f2..29c3635 100644 --- a/src/components/ControlPanel/ControlPanel.tsx +++ b/src/components/ControlPanel/ControlPanel.tsx @@ -82,7 +82,7 @@ export function ControlPanel({ id={wordCountInputId} value={wordsPerChunk} onChange={handleWordsPerChunkChange} - className="block w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none sm:text-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100" + className="block w-full rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none sm:text-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100" > diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx index 54dcb49..bf0c3fa 100644 --- a/src/components/TextInput/TextInput.tsx +++ b/src/components/TextInput/TextInput.tsx @@ -45,7 +45,7 @@ export const TextInput = ({ onChange(event.target.value); }} disabled={disabled} - className="min-h-56 w-full rounded-md border border-slate-300 bg-white p-3 text-sm text-slate-900 shadow-sm transition outline-none focus:border-sky-500 focus:ring-2 focus:ring-sky-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder-slate-400" + className="min-h-56 w-full rounded-md border border-slate-300 p-3 text-sm text-slate-900 shadow-sm transition outline-none focus:border-sky-500 focus:ring-2 focus:ring-sky-200 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder-slate-400" placeholder="Paste text to begin your speed reading session." rows={10} /> diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index 0a60ec5..de4e87e 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -92,7 +92,6 @@ export const useTheme = () => { root.classList.add('dark'); } else { root.classList.remove('dark'); - root.className = ''; // Ensure all classes are removed when switching to light } }, [themeState.effectiveTheme]); diff --git a/src/index.css b/src/index.css index 705a096..5b73331 100644 --- a/src/index.css +++ b/src/index.css @@ -1,11 +1,3 @@ @import 'tailwindcss'; -@variant dark (&:where(.dark, .dark *)); - -@media (prefers-reduced-motion: no-preference) { - * { - transition-property: color, background-color, border-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 300ms; - } -} +@custom-variant dark (&:where(.dark, .dark *)); diff --git a/src/main.tsx b/src/main.tsx index 62cad15..93ff204 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,24 @@ import { createRoot } from 'react-dom/client'; import App from './components/App'; +// Add transitions after page load to prevent flash +/* v8 ignore next -- @preserve */ +document.addEventListener('DOMContentLoaded', () => { + setTimeout(() => { + const style = document.createElement('style'); + style.textContent = ` + @media (prefers-reduced-motion: no-preference) { + * { + transition-property: color, background-color, border-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; + } + } + `; + document.head.appendChild(style); + }, 300); +}); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion createRoot(document.getElementById('root')!).render( From 6c5a70c08d57bdeb486fa18fc07995488bdaaf1e Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 16 Feb 2026 00:14:36 -0500 Subject: [PATCH 40/41] refactor(hook): tidy constant in useTheme --- src/hooks/useTheme.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index de4e87e..ce6c500 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -15,6 +15,8 @@ const resolveEffectiveTheme = ( return systemTheme === 'dark' ? 'dark' : 'light'; }; +const ROOT = document.documentElement; + export const useTheme = () => { const [themeState, setThemeState] = useState(() => { const stored = loadThemePreference(); @@ -87,11 +89,10 @@ export const useTheme = () => { }, []); useEffect(() => { - const root = document.documentElement; if (themeState.effectiveTheme === 'dark') { - root.classList.add('dark'); + ROOT.classList.add('dark'); } else { - root.classList.remove('dark'); + ROOT.classList.remove('dark'); } }, [themeState.effectiveTheme]); From 8b7424adeef51219c12b34b37750617db32d3148 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 16 Feb 2026 00:16:46 -0500 Subject: [PATCH 41/41] docs(specs): mark accessibility checklist as complete --- .../001-dark-mode/checklists/accessibility.md | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/specs/001-dark-mode/checklists/accessibility.md b/specs/001-dark-mode/checklists/accessibility.md index 7b7df82..8a898e5 100644 --- a/specs/001-dark-mode/checklists/accessibility.md +++ b/specs/001-dark-mode/checklists/accessibility.md @@ -6,45 +6,45 @@ ## Keyboard Navigation -- [ ] ThemeToggle is focusable via Tab key -- [ ] ThemeToggle activates with Enter key -- [ ] ThemeToggle activates with Space key -- [ ] Focus indicator is visible (2px outline) -- [ ] Focus order is logical and predictable +- [x] ThemeToggle is focusable via Tab key +- [x] ThemeToggle activates with Enter key +- [x] ThemeToggle activates with Space key +- [x] Focus indicator is visible (2px outline) +- [x] Focus order is logical and predictable ## Semantics & ARIA -- [ ] ThemeToggle has proper ARIA label describing current state -- [ ] ThemeToggle role is "button" -- [ ] Theme changes are announced to screen readers -- [ ] No ARIA violations detected +- [x] ThemeToggle has proper ARIA label describing current state +- [x] ThemeToggle role is "button" +- [x] Theme changes are announced to screen readers +- [x] No ARIA violations detected ## Responsive Design -- [ ] ThemeToggle is visible and functional on mobile (320px+) -- [ ] ThemeToggle is visible and functional on tablet (768px+) -- [ ] ThemeToggle is visible and functional on desktop (1024px+) -- [ ] Touch target meets minimum 48px × 48px requirement -- [ ] No layout shifts occur during theme transitions +- [x] ThemeToggle is visible and functional on mobile (320px+) +- [x] ThemeToggle is visible and functional on tablet (768px+) +- [x] ThemeToggle is visible and functional on desktop (1024px+) +- [x] Touch target meets minimum 48px × 48px requirement +- [x] No layout shifts occur during theme transitions ## Color Contrast -- [ ] Light mode text meets WCAG AA contrast ratio (4.5:1) -- [ ] Dark mode text meets WCAG AA contrast ratio (4.5:1) -- [ ] Focus indicators meet contrast requirements -- [ ] Interactive elements meet contrast requirements +- [x] Light mode text meets WCAG AA contrast ratio (4.5:1) +- [x] Dark mode text meets WCAG AA contrast ratio (4.5:1) +- [x] Focus indicators meet contrast requirements +- [x] Interactive elements meet contrast requirements ## High Contrast Mode -- [ ] High contrast mode is detected correctly -- [ ] High contrast mode overrides dark mode when active -- [ ] Theme remains functional in high contrast mode +- [x] High contrast mode is detected correctly +- [x] High contrast mode overrides dark mode when active +- [x] Theme remains functional in high contrast mode ## Reduced Motion -- [ ] Theme transitions respect prefers-reduced-motion -- [ ] No animations when reduced motion is preferred -- [ ] Functionality remains intact without animations +- [x] Theme transitions respect prefers-reduced-motion +- [x] No animations when reduced motion is preferred +- [x] Functionality remains intact without animations ## Notes