From 89590a349a105a9a5f93fc7a60f9d7a2b86084bc Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 16:54:07 +0530 Subject: [PATCH] docs(plan): Phase 7 modals implementation plan (37 tasks, TDD, ~+50 tests) --- .../plans/2026-06-05-phase-7-modals.md | 3327 +++++++++++++++++ 1 file changed, 3327 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-05-phase-7-modals.md diff --git a/docs/superpowers/plans/2026-06-05-phase-7-modals.md b/docs/superpowers/plans/2026-06-05-phase-7-modals.md new file mode 100644 index 0000000..e9ac710 --- /dev/null +++ b/docs/superpowers/plans/2026-06-05-phase-7-modals.md @@ -0,0 +1,3327 @@ +# Phase 7 — Modals Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a layered modal system to the React renderer — a single `` driven by a discriminated-union `useAppStore.modal` field, hosting 7 modal types (SettingsSheet + 4 export dialogs + About + Welcome + Confirm) plus a persisted `useSettingsStore` for user preferences, with all modals opened via the Phase 6 command store. + +**Architecture:** Modal state is a discriminated union in `useAppStore` (runtime-only, excluded from `partialize`). A new `useSettingsStore` persists user preferences via `zustand persist` with `partialize` (matching the `useFileStore` and `useCommandStore` precedent). All 7 modal components are small, focused, and individually testable. A `useExportSource` hook and `ExportDialogFooter` component are shared across the 4 export dialogs. Commands registered in `register-menu-commands.ts` open the modals — no component opens a modal directly. + +**Tech Stack:** React 19, zustand (with persist + immer middleware), shadcn/ui new-york style (radix-ui primitives), react-hook-form + zod, lucide-react icons, motion (for transitions). + +**Spec:** `docs/superpowers/specs/2026-06-05-phase-7-modals-design.md` +**Tag:** `phase-7-modals` + +--- + +## Conventions + +- **shadcn primitives** are pasted manually from canonical sources (the shadcn CLI is broken on Node v24 in this project — see memory `shadcn-cli-blocked-manual-primitives`). For each primitive, the task cites the canonical URL and provides a smoke test. +- **All stores** use zustand's `persist` middleware with `partialize` to selectively serialize (matching the `useFileStore` and `useCommandStore` precedent). +- **All commands** are registered in `src/renderer/lib/commands/register-menu-commands.ts` (Phase 6 pattern). Components never call `openModal` directly — they `dispatch` a command. +- **All IPC** goes through `ipc.*` wrappers (`src/renderer/lib/ipc.ts`). +- **No `tsconfig.json`** exists in this project. Validate TypeScript with `npx vite build --config vite.renderer.config.ts` after each task that adds/changes code. +- **Test commands:** `npx vitest run tests/` (one file) or `npx vitest run` (full suite). Tests live in `tests/unit/`, `tests/component/`, `tests/integration/`. +- **Commit style:** `feat(renderer): ...`, `test(renderer): ...`, `feat(renderer): ... + tests` for combined commits. +- **Style:** new-york (set in `components.json`). All primitives use `cn()` from `@/lib/utils` for class merging. + +--- + +## Task 1: Add shadcn `label` primitive + smoke test + +**Files:** +- Create: `src/renderer/components/ui/label.tsx` +- Create: `tests/component/ui/label.test.tsx` + +- [ ] **Step 1: Create the primitive** + +Canonical source: https://ui.shadcn.com/docs/components/label + +Create `src/renderer/components/ui/label.tsx` by pasting the canonical new-york style source. It is a thin wrapper around `@radix-ui/react-label` using `forwardRef` and `cn()`. The file should be ~25 lines. + +Verify the file imports `@radix-ui/react-label` (if not installed, run `npm install @radix-ui/react-label`). + +- [ ] **Step 2: Write the smoke test** + +Create `tests/component/ui/label.test.tsx`: + +```tsx +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Label } from '@/components/ui/label'; + +describe('Label', () => { + it('renders children and associates with a control', () => { + render( + <> + + + + ); + const label = screen.getByText(/username/i); + expect(label).toBeInTheDocument(); + expect(label.tagName).toBe('LABEL'); + }); +}); +``` + +- [ ] **Step 3: Run the test** + +```bash +npx vitest run tests/component/ui/label.test.tsx +``` + +Expected: PASS, 1 test. + +- [ ] **Step 4: Commit** + +```bash +git add src/renderer/components/ui/label.tsx tests/component/ui/label.test.tsx package.json package-lock.json +git commit -m "feat(renderer): add shadcn label primitive" +``` + +--- + +## Task 2: Add shadcn `input` primitive + smoke test + +**Files:** +- Create: `src/renderer/components/ui/input.tsx` +- Create: `tests/component/ui/input.test.tsx` + +- [ ] **Step 1: Create the primitive** + +Canonical source: https://ui.shadcn.com/docs/components/input + +Create `src/renderer/components/ui/input.tsx` by pasting the canonical new-york source. ~30 lines. Uses `React.forwardRef`, `cn()`, no radix dep. + +- [ ] **Step 2: Write the smoke test** + +Create `tests/component/ui/input.test.tsx`: + +```tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Input } from '@/components/ui/input'; + +describe('Input', () => { + it('renders and accepts typing', async () => { + const onChange = vi.fn(); + render(); + const input = screen.getByLabelText(/name/i); + await userEvent.type(input, 'a'); + expect(onChange).toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 3: Run, commit** + +```bash +npx vitest run tests/component/ui/input.test.tsx +git add src/renderer/components/ui/input.tsx tests/component/ui/input.test.tsx +git commit -m "feat(renderer): add shadcn input primitive" +``` + +--- + +## Task 3: Add shadcn `textarea` primitive + smoke test + +**Files:** +- Create: `src/renderer/components/ui/textarea.tsx` +- Create: `tests/component/ui/textarea.test.tsx` + +- [ ] **Step 1: Create the primitive** + +Canonical source: https://ui.shadcn.com/docs/components/textarea + +Create `src/renderer/components/ui/textarea.tsx` (~25 lines, `forwardRef` + `cn()`). + +- [ ] **Step 2: Smoke test** + +```tsx +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { Textarea } from '@/components/ui/textarea'; + +describe('Textarea', () => { + it('renders a multi-line input', () => { + render(