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/.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/AGENTS.md b/AGENTS.md index 91465ea..b5c7c1b 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** - 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/index.html b/index.html index fae2401..5ec0e6a 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,30 @@ Speed Reader + + + + diff --git a/specs/001-dark-mode/checklists/accessibility.md b/specs/001-dark-mode/checklists/accessibility.md new file mode 100644 index 0000000..8a898e5 --- /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 + +- [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 + +- [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 + +- [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 + +- [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 + +- [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 + +- [x] Theme transitions respect prefers-reduced-motion +- [x] No animations when reduced motion is preferred +- [x] 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/checklists/requirements.md b/specs/001-dark-mode/checklists/requirements.md new file mode 100644 index 0000000..6f91f98 --- /dev/null +++ b/specs/001-dark-mode/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Dark Mode + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-02-15 +**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 + +- [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) +- [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 + +- All checklist items completed - specification is ready for `/speckit.plan` 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..2065fe0 --- /dev/null +++ b/specs/001-dark-mode/contracts/component-contracts.md @@ -0,0 +1,198 @@ +# 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 change + +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. **Hover State**: Slight scale increase 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; + /** 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 string validation fails */ + 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 + +```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: (theme: 'light' | 'dark' | 'system') => boolean; + /** Load theme preference from localStorage */ + loadThemePreference: () => 'light' | 'dark' | 'system' | null; + /** Validate theme preference string */ + validateThemePreference: ( + data: unknown, + ) => data is 'light' | 'dark' | 'system'; +} +``` + +### Storage Contract + +#### localStorage Schema + +```typescript +interface StorageContract { + /** Storage key for theme preference */ + key: 'speedreader.theme'; + /** Data format stored */ + format: 'light' | 'dark' | 'system'; +} +``` + +#### 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'; +} +``` + +**Storage Operations**: + +- 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 + +### App Component Integration + +```typescript +interface AppThemeIntegration { + /** 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'; +} +``` + +### CSS Integration + +```typescript +interface CSSIntegration { + /** Tailwind class strategy */ + tailwindStrategy: 'class'; + /** Custom properties for theme colors */ + cssVariables: boolean; +} +``` diff --git a/specs/001-dark-mode/data-model.md b/specs/001-dark-mode/data-model.md new file mode 100644 index 0000000..bd4e927 --- /dev/null +++ b/specs/001-dark-mode/data-model.md @@ -0,0 +1,138 @@ +# 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'; +``` + +### Stored Data Format + +```typescript +// Simple string value +'light' | 'dark' | 'system'; +``` + +### Storage Validation + +- Validate string against allowed theme values +- Handle invalid values gracefully by using system preference + +## 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 +- Invalid string values: fall back to system preference +- Media query not supported: assume 'no-preference' +- Theme toggle errors: maintain current theme, use simple try-catch diff --git a/specs/001-dark-mode/plan.md b/specs/001-dark-mode/plan.md new file mode 100644 index 0000000..4774f5a --- /dev/null +++ b/specs/001-dark-mode/plan.md @@ -0,0 +1,117 @@ +# 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**: 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 + +_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, 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). + +## 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 +``` + +**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 | +| --------- | ---------- | -------------------------------------------- | +| 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. diff --git a/specs/001-dark-mode/quickstart.md b/specs/001-dark-mode/quickstart.md new file mode 100644 index 0000000..4efee31 --- /dev/null +++ b/specs/001-dark-mode/quickstart.md @@ -0,0 +1,488 @@ +# 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 + +**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; +} + +@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`: + +```typescript +export type Theme = 'light' | 'dark' | 'system'; + +export interface ThemeState { + effectiveTheme: 'light' | 'dark'; + userPreference: Theme; + systemPreference: 'light' | 'dark' | 'no-preference'; + highContrastMode: boolean; +} +``` + +## Step 3: Implement Theme Utilities + +Create `src/utils/theme.ts`: + +```typescript +import type { Theme } from 'src/types/theme'; + +const THEME_STORAGE_KEY = 'speedreader.theme'; + +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 = (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); +}; +``` + +## 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'; + +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 === 'system' + ? systemTheme + : preference; + + 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 !== '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 === 'system' + ? prev.systemPreference + : prev.userPreference; + + return { + ...prev, + highContrastMode: highContrast, + effectiveTheme, + }; + }); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + const toggleTheme = useCallback(() => { + setThemeState((prev) => { + // Cycle through: light → dark → system → light + const cycleMap: Record = { + light: 'dark', + dark: 'system', + system: 'light', + }; + const newPreference = cycleMap[prev.userPreference]; + + saveThemePreference(newPreference); + + const effectiveTheme = + prev.highContrastMode || newPreference === 'system' + ? resolveEffectiveTheme(prev.systemPreference) + : newPreference; + + return { + ...prev, + effectiveTheme, + userPreference: newPreference, + }; + }); + }, []); + + const setTheme = useCallback((theme: Theme) => { + setThemeState((prev) => { + saveThemePreference(theme); + + const effectiveTheme = prev.highContrastMode + ? 'light' + : theme === 'system' + ? prev.systemPreference + : theme; + + return { + ...prev, + effectiveTheme, + userPreference: theme, + }; + }); + }, []); + + return { + theme: themeState.effectiveTheme, + preference: themeState.userPreference, + followingSystem: themeState.userPreference === 'system', + toggleTheme, + setTheme, + highContrastMode: themeState.highContrastMode, + }; +}; +``` + +## Step 5: Create ThemeToggle Component + +Create `src/components/ThemeToggle/ThemeToggle.tsx`: + +```typescript +import type { ThemeToggleProps } from './ThemeToggle.types'; + +export const ThemeToggle = ({ + currentTheme, + onThemeToggle, + disabled = false, +}: ThemeToggleProps) => { + const ariaLabel = `Toggle theme, currently ${currentTheme} mode. Click to cycle to ${ + currentTheme === 'light' ? 'dark' : currentTheme === 'dark' ? 'system' : 'light' + } mode`; + + const renderIcon = () => { + switch (currentTheme) { + case 'dark': + return ( + + ); + case 'system': + return ( + + ); + case 'light': + default: + return ( + + ); + } + }; + + 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 { preference, toggleTheme } = useTheme(); + + // Theme is automatically applied to document root by useTheme hook + + return ( +
+ {/* Your existing app content */} + + +
+ ); +}; +``` + +**Note**: The `useTheme` hook now handles applying the theme class to `document.documentElement` internally, so you don't need a separate `useEffect`. Pass `preference` (not `theme`) to `ThemeToggle` to show the user's preference (light/dark/system) rather than the effective theme. + +## 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 = 'dark'; + + localStorageMock.getItem.mockReturnValue(savedPreference); + + const { result } = renderHook(() => useTheme()); + + expect(result.current.theme).toBe('dark'); + expect(result.current.preference).toBe('dark'); + }); +}); +``` + +## Step 8: Apply Theme Classes to Components + +**Apply theme classes to components**: + +```tsx +// 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 + +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. diff --git a/specs/001-dark-mode/research.md b/specs/001-dark-mode/research.md new file mode 100644 index 0000000..3ceddbb --- /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 custom hook + localStorage + system preference detection +**Rationale**: + +- 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) + +### 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 custom hook 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 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 +- 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. diff --git a/specs/001-dark-mode/spec.md b/specs/001-dark-mode/spec.md new file mode 100644 index 0000000..86cae0e --- /dev/null +++ b/specs/001-dark-mode/spec.md @@ -0,0 +1,104 @@ +# Feature Specification: Dark Mode + +**Feature Branch**: `001-dark-mode` +**Created**: 2026-02-15 +**Status**: Draft +**Input**: User description: "dark mode" + +## Clarifications + +### Session 2026-02-15 + +- Q: Toggle Control Location and Type → A: Bottom right floating SVG toggle with sun/moon/monitor icons cycling through light/dark/system +- 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 + +## User Scenarios & Testing _(mandatory)_ + +### User Story 1 - Toggle Dark Mode (Priority: P1) + +User wants to cycle between light, dark, and system 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 cycling through the theme states and verifying the UI changes between light, dark, and system modes. + +**Acceptance Scenarios**: + +1. **Given** the application is in light mode, **When** user clicks the theme toggle, **Then** the interface switches to dark theme with appropriate colors +2. **Given** the application is in dark mode, **When** user clicks the theme toggle, **Then** the interface switches to system theme (following OS preference) +3. **Given** the application is in system mode, **When** user clicks the theme toggle, **Then** the interface switches to light theme +4. **Given** the application is in system mode following dark OS preference, **When** OS theme changes to light, **Then** the interface automatically updates 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 + +- **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 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/monitor icons to cycle between light, dark, and system themes +- **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 +- **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)_ + +- **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 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 +- **SC-005**: System theme detection works correctly on 90% of supported operating systems and browsers diff --git a/specs/001-dark-mode/tasks.md b/specs/001-dark-mode/tasks.md new file mode 100644 index 0000000..1fd2f94 --- /dev/null +++ b/specs/001-dark-mode/tasks.md @@ -0,0 +1,231 @@ +--- +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**: 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. + +## 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 + +- [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 + +--- + +## 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 + +- [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 + +--- + +## 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 (TDD REQUIRED) + +> **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation + +- [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 + +- [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 + +--- + +## 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 (TDD REQUIRED) + +> **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation + +- [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 + +- [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 + +--- + +## 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 (TDD REQUIRED) + +> **⚠️ TEST-FIRST ENFORCED**: Write tests FIRST, ensure they PASS validation before implementation + +- [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 + +- [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 + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Improvements that affect multiple user stories + +- [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` + +--- + +## 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 + +- **TEST-FIRST ENFORCED**: Tests MUST be written and validated before implementation (no exceptions) +- 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 (TDD APPROACH) + +```bash +# 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" + +# 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" +``` + +--- + +## 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 +- **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 +- Stop at any checkpoint to validate story independently +- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence 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..8bfa994 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 @@ -52,7 +52,7 @@ export function ControlPanel({ id="word-count" value={wordCount} onChange={(e) => handleWordCountChange(parseInt(e.target.value, 10))} - className="w-full rounded-md border border-slate-300 bg-white p-2" + className="w-full rounded-md border border-slate-300 p-2" > 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 diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 139563e..3d96cdc 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 { preference, 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/App/readerTypes.ts b/src/components/App/readerTypes.ts deleted file mode 100644 index 6c06cae..0000000 --- a/src/components/App/readerTypes.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { ReadingSessionStatus } from 'src/types/readerTypes'; - -export interface ReadingContent { - rawText: string; - words: string[]; - totalWords: number; -} - -export interface ReadingSessionState { - status: ReadingSessionStatus; - currentWordIndex: number; - selectedWpm: number; - elapsedMs: number; - // Multiple words display support - currentChunkIndex: number; - totalChunks: number; - wordsPerChunk: number; - // Store the actual words for chunk generation - words: string[]; -} - -export interface ReadingSessionMetrics { - wordsRead: number; - totalWords: number; - progressPercent: number; - chunksRead: number; - totalChunks: number; -} diff --git a/src/components/App/sessionReducer.ts b/src/components/App/sessionReducer.ts index 153946e..f1f8978 100644 --- a/src/components/App/sessionReducer.ts +++ b/src/components/App/sessionReducer.ts @@ -1,4 +1,4 @@ -import type { ReadingSessionState } from './readerTypes'; +import type { ReadingSessionState } from 'src/types/readerTypes'; export interface SessionReducerState extends ReadingSessionState { startCount: number; diff --git a/src/components/App/tokenizeContent.test.ts b/src/components/App/tokenizeContent.test.ts index b5f18fd..04f8c08 100644 --- a/src/components/App/tokenizeContent.test.ts +++ b/src/components/App/tokenizeContent.test.ts @@ -1,5 +1,5 @@ +import { hasReadableText, tokenizeContent } from '../TextInput/tokenizeContent'; import { SESSION_TEXT } from './__fixtures__/sessionText'; -import { hasReadableText, tokenizeContent } from './tokenizeContent'; describe('tokenizeContent', () => { it('returns false for empty or whitespace text', () => { diff --git a/src/components/App/tokenizeContent.ts b/src/components/App/tokenizeContent.ts deleted file mode 100644 index 16989de..0000000 --- a/src/components/App/tokenizeContent.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export from TextInput for consistency -export type { TokenizedContent } from '../TextInput/tokenizeContent'; -export { hasReadableText, tokenizeContent } from '../TextInput/tokenizeContent'; diff --git a/src/components/Button/Button.test.tsx b/src/components/Button/Button.test.tsx index a4cb3bc..a8ba9d2 100644 --- a/src/components/Button/Button.test.tsx +++ b/src/components/Button/Button.test.tsx @@ -16,11 +16,7 @@ describe('Button', () => { render(); const button = screen.getByRole('button', { name: 'Click me' }); - expect(button).toHaveClass( - 'border-slate-300', - 'bg-white', - 'text-slate-800', - ); + expect(button).toHaveClass('border-slate-300', 'text-slate-800'); }); it('handles click events', async () => { diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 69cb253..54a132e 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -19,9 +19,9 @@ export const Button = ({ const variantClasses: Record<'primary' | 'secondary', string> = { primary: - 'border-sky-600 bg-sky-600 text-white hover:border-sky-700 hover:bg-sky-700', + 'border-sky-600 bg-sky-600 text-white hover:border-sky-700 hover:bg-sky-700 dark:border-sky-500 dark:bg-sky-500 dark:hover:border-sky-600 dark:hover:bg-sky-600', secondary: - 'border-slate-300 bg-white text-slate-800 hover:border-slate-400 hover:bg-slate-50', + 'border-slate-300 text-slate-800 hover:border-slate-400 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-200 dark:hover:border-slate-500 dark:hover:bg-slate-700', }; const classes = `${baseClasses} ${variantClasses[variant]} ${className}`; diff --git a/src/components/ControlPanel/ControlPanel.tsx b/src/components/ControlPanel/ControlPanel.tsx index 5473c87..29c3635 100644 --- a/src/components/ControlPanel/ControlPanel.tsx +++ b/src/components/ControlPanel/ControlPanel.tsx @@ -51,7 +51,7 @@ export function ControlPanel({ >