Skip to content
Merged
5 changes: 5 additions & 0 deletions .windsurf/rules/specify-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Auto-generated from all feature plans. Last updated: 2026-02-07

## Active Technologies

- TypeScript 5 (strict mode) + React 19, `diff` npm package (already installed — exports `diffChars`, `diffWords`, `diffLines`) (002-toggle-diff-options)
- localStorage (browser-native, no new dependencies) (002-toggle-diff-options)

- N/A (client-side only, no persistence) (001-text-diff)

- TypeScript 5.9.3 (strict mode) + React 19.2.4, `diff` (npm — to be added as runtime dependency) (001-text-diff)
Expand All @@ -26,6 +29,8 @@ TypeScript 5.9.3 (strict mode): Follow standard conventions

## Recent Changes

- 002-toggle-diff-options: Added TypeScript 5 (strict mode) + React 19, `diff` npm package (already installed — exports `diffChars`, `diffWords`, `diffLines`)

- 001-text-diff: Added TypeScript 5.9.3 (strict mode) + React 19.2.4, `diff` (npm — to be added as runtime dependency)

- 001-text-diff: Added TypeScript 5.9.3 (strict mode) + React 19.2.4, `diff` (npm — to be added as runtime dependency)
Expand Down
36 changes: 36 additions & 0 deletions specs/002-toggle-diff-options/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Specification Quality Checklist: Toggle Diff Options

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-08
**Feature**: [spec.md](../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 items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
- Assumption: the `diff` npm library already supports character-level (`diffChars`), word-level (`diffWords`), and line-level (`diffLines`) methods, so no new dependencies are needed.
- Whitespace trimming and case normalization are pre-processing steps applied to input text before passing to the diff function.
68 changes: 68 additions & 0 deletions specs/002-toggle-diff-options/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Data Model: Toggle Diff Options

**Feature Branch**: `002-toggle-diff-options`
**Date**: 2026-02-08

## Overview

This feature adds one new type (`DiffMethod`) and one new hook (`useLocalStorage`) to the existing client-side data model. All data remains in-memory React state with optional localStorage persistence. No databases, APIs, or external storage.

## New Entities

### DiffMethod

The granularity of comparison. A string union type with three possible values.

- `'characters'` — character-level diff (`diffChars`)
- `'words'` — word-level diff (`diffWords`, current default)
- `'lines'` — line-level diff (`diffLines`)

**Default**: `'words'` (preserves current behavior)
**Persistence**: localStorage key `'diffMethod'`

## Modified Entities

### DiffResult (no structural change)

The `DiffResult` interface (`segments` + `hasChanges`) remains unchanged. The diff method only affects which library function produces the segments — the output shape is identical across `diffChars`, `diffWords`, and `diffLines`.

### ViewMode (no structural change)

The `ViewMode` type (`'unified' | 'side-by-side'`) remains unchanged. It gains localStorage persistence via the same `useLocalStorage` hook.

**Persistence**: localStorage key `'viewMode'`

## Hooks

### useLocalStorage<T>(key, defaultValue) → [T, (value: T) => void]

A generic hook that mirrors `useState` but reads the initial value from localStorage and writes on every update.

- **key**: localStorage key string
- **defaultValue**: fallback when key is missing or value is invalid
- **Returns**: `[currentValue, setValue]` tuple (same shape as `useState`)
- **Error handling**: Falls back to `defaultValue` if localStorage read fails or JSON parsing throws

### useDiff(originalText, modifiedText, method) → DiffResult | null

Modified to accept a third parameter `method: DiffMethod` that selects which diff function to call.

- `'characters'` → `diffChars(original, modified)`
- `'words'` → `diffWords(original, modified)` (current behavior)
- `'lines'` → `diffLines(original, modified)`

## Component Props

### DiffMethodToggleProps

```
activeMethod: DiffMethod — the currently selected diff method
onMethodChange: (method: DiffMethod) => void — callback when user selects a method
```

## localStorage Schema

| Key | Type | Default | Description |
| ------------ | ------------ | ----------- | ------------------------------------ |
| `diffMethod` | `DiffMethod` | `'words'` | Selected diff comparison granularity |
| `viewMode` | `ViewMode` | `'unified'` | Selected diff display mode |
72 changes: 72 additions & 0 deletions specs/002-toggle-diff-options/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Implementation Plan: Toggle Diff Options

**Branch**: `002-toggle-diff-options` | **Date**: 2026-02-08 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/002-toggle-diff-options/spec.md`

## Summary

Add a segmented button group (Characters | Words | Lines) to the diff header that lets users switch between `diffChars`, `diffWords`, and `diffLines` from the `diff` npm library. The selected method and view mode are persisted to localStorage. The diff method toggle is placed on the left side of the diff header; the existing view mode toggle stays on the right. State is lifted in the `App` component following the existing `viewMode` pattern. A new `useLocalStorage` hook provides persistence with type-safe fallback defaults.

## Technical Context

**Language/Version**: TypeScript 5 (strict mode)
**Primary Dependencies**: React 19, `diff` npm package (already installed — exports `diffChars`, `diffWords`, `diffLines`)
**Storage**: localStorage (browser-native, no new dependencies)
**Testing**: Vitest 4 with @testing-library/react and @testing-library/user-event
**Target Platform**: Browser (static SPA, any modern browser)
**Project Type**: Single-page web application
**Performance Goals**: Instant recomputation on method change (no debounce)
**Constraints**: Client-side only, 100% test coverage, no new runtime dependencies
**Scale/Scope**: 3 new/modified components, 1 new hook, 1 type addition

## Constitution Check

_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._

| Principle | Status | Notes |
| ----------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| I. Client-Side Only | ✅ PASS | localStorage is browser-native, no server calls |
| II. Full Test Coverage | ✅ PASS | All new code will have 100% coverage |
| III. Accessibility First | ✅ PASS | Segmented button group uses `role="group"`, `aria-label`, native `<button>` elements |
| IV. Type Safety | ✅ PASS | `DiffMethod` type added to `src/types/diff.ts`, strict props interfaces |
| V. Simplicity and Performance | ✅ PASS | No new runtime dependencies; `useLocalStorage` is a thin wrapper around `useState` + `localStorage`; no state management libraries |

No violations. No complexity tracking needed.

## Project Structure

### Documentation (this feature)

```text
specs/002-toggle-diff-options/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
└── tasks.md # Phase 2 output (/speckit.tasks)
```

### Source Code (repository root)

```text
src/
├── types/
│ └── diff.ts # Add DiffMethod type
├── hooks/
│ ├── useLocalStorage.ts # NEW: generic localStorage persistence hook
│ ├── useLocalStorage.test.ts # NEW: tests
│ └── useDiff.ts # MODIFY: accept DiffMethod param, dispatch to diffChars/diffWords/diffLines
├── components/
│ ├── App/
│ │ ├── App.tsx # MODIFY: add diffMethod state with useLocalStorage, wire to DiffMethodToggle and useDiff; migrate viewMode to useLocalStorage
│ │ └── App.test.tsx # MODIFY: add tests for diff method switching and localStorage persistence
│ ├── DiffMethodToggle/ # NEW component
│ │ ├── DiffMethodToggle.tsx
│ │ ├── DiffMethodToggle.types.ts
│ │ ├── DiffMethodToggle.test.tsx
│ │ └── index.ts
│ └── ViewToggle/
│ └── (no changes)
```

**Structure Decision**: Single-project structure following existing `src/components/` and `src/hooks/` conventions. New `DiffMethodToggle` component follows the established `ComponentName/` directory pattern. No contracts directory needed (no APIs).
87 changes: 87 additions & 0 deletions specs/002-toggle-diff-options/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Quickstart: Toggle Diff Options

**Feature Branch**: `002-toggle-diff-options`
**Date**: 2026-02-08

## Prerequisites

- Node.js 24 (see `.nvmrc`)
- npm (bundled with Node.js)

## Setup

```bash
# Switch to feature branch
git checkout 002-toggle-diff-options

# Install dependencies (no new packages needed)
npm install

# Start dev server
npm start
```

## Development Commands

```bash
# Lint
npm run lint

# Lint with auto-fix
npm run lint:fix

# Type check
npm run lint:tsc

# Run a specific test file
npm test -- src/hooks/useLocalStorage.test.ts

# Run tests with coverage report
npm run test:ci

# Build for production
npm run build
```

## Quality Gates

All must pass before merge:

```bash
npm run lint # Zero errors, zero warnings
npm run lint:tsc # Zero type errors
npm run test:ci # All tests pass, 100% coverage
npm run build # Clean production build
```

## Key Files to Implement

### New Files

| File | Purpose |
| ----------------------------------------------------------- | ------------------------------------------------ |
| `src/hooks/useLocalStorage.ts` | Generic localStorage persistence hook |
| `src/hooks/useLocalStorage.test.ts` | Tests for useLocalStorage |
| `src/components/DiffMethodToggle/DiffMethodToggle.tsx` | Segmented button group for diff method selection |
| `src/components/DiffMethodToggle/DiffMethodToggle.types.ts` | Props interface |
| `src/components/DiffMethodToggle/DiffMethodToggle.test.tsx` | Component tests |
| `src/components/DiffMethodToggle/index.ts` | Barrel export |

### Modified Files

| File | Change |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `src/types/diff.ts` | Add `DiffMethod` type |
| `src/hooks/useDiff.ts` | Accept `DiffMethod` param, dispatch to correct diff function |
| `src/hooks/useDiff.test.ts` | Add tests for character and line diff methods |
| `src/components/App/App.tsx` | Add `diffMethod` state via `useLocalStorage`, wire to `DiffMethodToggle` and `useDiff`; migrate `viewMode` to `useLocalStorage` |
| `src/components/App/App.test.tsx` | Add tests for diff method switching and localStorage persistence |

## Implementation Order

1. Add `DiffMethod` type to `src/types/diff.ts`
2. Implement `useLocalStorage` hook + tests
3. Update `useDiff` hook to accept `DiffMethod` + tests
4. Implement `DiffMethodToggle` component + tests
5. Update `App` to wire everything together + tests
6. Run all quality gates
72 changes: 72 additions & 0 deletions specs/002-toggle-diff-options/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Research: Toggle Diff Options

**Feature Branch**: `002-toggle-diff-options`
**Date**: 2026-02-08

## Research Topic 1: `diff` Library — Multiple Diff Methods

### Decision

Use `diffChars`, `diffWords`, and `diffLines` from the `diff` npm package. All three are already available as named exports from the installed `diff` dependency.

### Rationale

- The `diff` library (jsdiff) provides a consistent API across all three methods: each returns an array of `Change` objects with `added`, `removed`, and `value` properties.
- No signature differences — all three accept `(oldStr, newStr)` and return `Change[]`.
- No new dependencies needed; the library is already installed.

### Alternatives Considered

- **Custom diff implementation**: Rejected — unnecessary complexity when the library already provides all three methods.
- **Separate libraries for each method**: Rejected — the `diff` package covers all granularities with a unified API.

## Research Topic 2: localStorage Persistence Hook

### Decision

Implement a custom `useLocalStorage<T>` hook that wraps `useState` with `localStorage` read/write. The hook reads the initial value from localStorage on mount (falling back to a provided default), and writes to localStorage on every state update.

### Rationale

- No new runtime dependencies needed — `localStorage` is a browser-native API.
- A generic hook (`useLocalStorage<T>`) can be reused for both `DiffMethod` and `ViewMode` persistence, and any future settings.
- The hook signature `useLocalStorage<T>(key: string, defaultValue: T): [T, (value: T) => void]` mirrors `useState` for drop-in replacement.
- JSON serialization/deserialization handles string union types cleanly.

### Alternatives Considered

- **Direct localStorage calls in App component**: Rejected — duplicates read/write logic for each persisted value; violates DRY.
- **Third-party hook library (e.g., usehooks-ts)**: Rejected — adds a runtime dependency; constitution principle V (Simplicity) favors no new dependencies for a thin wrapper.
- **URL query parameters**: Rejected — adds URL management complexity; localStorage is simpler for user preferences that don't need to be shareable.

### Implementation Notes

- The hook must handle invalid/corrupted localStorage values gracefully by falling back to the default.
- `localStorage` is synchronous and available in all target browsers.
- In tests, `localStorage` is available in jsdom — no special mocking needed beyond `localStorage.clear()` in cleanup.

## Research Topic 3: Segmented Button Group Pattern

### Decision

Create a new `DiffMethodToggle` component following the exact same pattern as the existing `ViewToggle` component: a `role="group"` container with `aria-label`, containing 3 native `<button>` elements with active/inactive styling via Tailwind classes.

### Rationale

- Reusing the proven pattern ensures UI consistency and reduces design decisions.
- Native `<button>` elements provide keyboard accessibility out of the box (Tab, Enter, Space).
- The 3-button variant only differs from the 2-button `ViewToggle` in having a middle button (no rounded corners on left or right).

### Alternatives Considered

- **Extending ViewToggle to be generic**: Rejected — the two toggles have different types (`ViewMode` vs `DiffMethod`) and different option counts. A generic component adds abstraction complexity without clear benefit for just 2 instances.
- **Dropdown/select**: Rejected per clarification — less discoverable than buttons.
- **Radio buttons**: Rejected per clarification — takes more vertical space.

### Styling Notes

- Left button: `rounded-l-md`
- Middle button: no border radius
- Right button: `rounded-r-md`
- Active state: `bg-blue-500 text-white dark:bg-blue-600`
- Inactive state: `bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600`
Loading