# 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(