(null);
+
+ const handleSubmit = async () => {
+ setSubmitting(true);
+ setError(null);
+ const items = sourcePaths.map((p) => ({
+ inputPath: p,
+ outputPath: p.replace(/\.md$/, extFor(format)),
+ }));
+ const result = await ipc.export.batch(items, { format, concurrency });
+ if (!result.ok) {
+ setError(result.error.message);
+ setSubmitting(false);
+ } else {
+ closeModal();
+ }
+ };
+
+ return (
+
+ );
+}
+```
+
+- [ ] **Step 4: Run, commit**
+
+```bash
+npx vitest run tests/component/modals/ExportBatchDialog.test.tsx
+git add src/renderer/components/modals/ExportBatchDialog.tsx tests/component/modals/ExportBatchDialog.test.tsx
+git commit -m "feat(renderer): ExportBatchDialog (format, concurrency, file list)"
+```
+
+---
+
+## Task 27: `EditorSettings` tab (no test, exercised via SettingsSheet test in Task 32)
+
+**Files:**
+- Create: `src/renderer/components/modals/EditorSettings.tsx`
+
+- [ ] **Step 1: Implement**
+
+Create `src/renderer/components/modals/EditorSettings.tsx`:
+
+```tsx
+import { useSettingsStore } from '@/stores/settings-store';
+import { Label } from '@/components/ui/label';
+import { Slider } from '@/components/ui/slider';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Switch } from '@/components/ui/switch';
+
+export function EditorSettings() {
+ const { fontSize, tabSize, lineNumbers, wordWrap, minimap, setSetting } = useSettingsStore();
+
+ return (
+
+
+
+ setSetting('fontSize', v)} />
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/renderer/components/modals/EditorSettings.tsx
+git commit -m "feat(renderer): EditorSettings tab (font, tab, line/wrap, minimap)"
+```
+
+---
+
+## Task 28: `ThemeSettings` tab
+
+**Files:**
+- Create: `src/renderer/components/modals/ThemeSettings.tsx`
+
+- [ ] **Step 1: Implement**
+
+Create `src/renderer/components/modals/ThemeSettings.tsx`:
+
+```tsx
+import { useSettingsStore } from '@/stores/settings-store';
+import { Label } from '@/components/ui/label';
+import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+
+export function ThemeSettings() {
+ const { theme, accentColor, fontFamily, setSetting } = useSettingsStore();
+
+ return (
+
+
+
+
setSetting('theme', v as 'light' | 'dark' | 'auto')}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/renderer/components/modals/ThemeSettings.tsx
+git commit -m "feat(renderer): ThemeSettings tab (mode, accent, font)"
+```
+
+---
+
+## Task 29: `ExportSettings` tab
+
+**Files:**
+- Create: `src/renderer/components/modals/ExportSettings.tsx`
+
+- [ ] **Step 1: Implement**
+
+Create `src/renderer/components/modals/ExportSettings.tsx`:
+
+```tsx
+import { useSettingsStore } from '@/stores/settings-store';
+import { Label } from '@/components/ui/label';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Switch } from '@/components/ui/switch';
+
+export function ExportSettings() {
+ const { pdfFormat, pdfMargins, pdfEmbedFonts, docxTemplate, htmlHighlightStyle, renderTablesAsAscii, setSetting } = useSettingsStore();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/renderer/components/modals/ExportSettings.tsx
+git commit -m "feat(renderer): ExportSettings tab (PDF/DOCX/HTML defaults + ascii toggle)"
+```
+
+---
+
+## Task 30: `PluginsSettings` tab (placeholder)
+
+**Files:**
+- Create: `src/renderer/components/modals/PluginsSettings.tsx`
+
+- [ ] **Step 1: Implement**
+
+Create `src/renderer/components/modals/PluginsSettings.tsx`:
+
+```tsx
+import { Sparkles } from 'lucide-react';
+
+export function PluginsSettings() {
+ return (
+
+
+
Coming soon
+
+ The plugin system is on the roadmap. You'll be able to extend MarkdownConverter with custom
+ commands, themes, and export formats.
+
+
+ );
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/renderer/components/modals/PluginsSettings.tsx
+git commit -m "feat(renderer): PluginsSettings tab placeholder (Coming soon)"
+```
+
+---
+
+## Task 31: `AboutSettings` tab
+
+**Files:**
+- Create: `src/renderer/components/modals/AboutSettings.tsx`
+
+- [ ] **Step 1: Implement**
+
+Create `src/renderer/components/modals/AboutSettings.tsx`:
+
+```tsx
+import { useEffect, useState } from 'react';
+import { ipc } from '@/lib/ipc';
+
+export function AboutSettings() {
+ const [version, setVersion] = useState('…');
+
+ useEffect(() => {
+ ipc.app.getVersion().then((r) => {
+ if (r.ok && typeof r.data === 'string') setVersion(r.data);
+ });
+ }, []);
+
+ return (
+
+ );
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add src/renderer/components/modals/AboutSettings.tsx
+git commit -m "feat(renderer): AboutSettings tab (version + links)"
+```
+
+---
+
+## Task 32: `SettingsSheet` (compose 5 tabs) + test (TDD)
+
+**Files:**
+- Create: `src/renderer/components/modals/SettingsSheet.tsx`
+- Create: `tests/component/modals/SettingsSheet.test.tsx`
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `tests/component/modals/SettingsSheet.test.tsx`:
+
+```tsx
+import { describe, it, expect, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { SettingsSheet } from '@/components/modals/SettingsSheet';
+import { useSettingsStore } from '@/stores/settings-store';
+
+describe('SettingsSheet', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ useSettingsStore.setState(useSettingsStore.getInitialState());
+ });
+
+ it('renders 5 tab triggers', () => {
+ render();
+ expect(screen.getByRole('tab', { name: /editor/i })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: /theme/i })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: /export/i })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: /plugins/i })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: /about/i })).toBeInTheDocument();
+ });
+
+ it('editor tab is open by default and shows font size', () => {
+ render();
+ expect(screen.getByText(/font size/i)).toBeInTheDocument();
+ });
+
+ it('theme tab shows theme radio group', async () => {
+ render();
+ await userEvent.click(screen.getByRole('tab', { name: /theme/i }));
+ expect(screen.getByText(/accent color/i)).toBeInTheDocument();
+ });
+
+ it('export tab shows ascii toggle and template picker', async () => {
+ render();
+ await userEvent.click(screen.getByRole('tab', { name: /export/i }));
+ expect(screen.getByText(/render tables as ascii by default/i)).toBeInTheDocument();
+ expect(screen.getByText(/default docx template/i)).toBeInTheDocument();
+ });
+
+ it('plugins tab shows coming soon message', async () => {
+ render();
+ await userEvent.click(screen.getByRole('tab', { name: /plugins/i }));
+ expect(screen.getByText(/coming soon/i)).toBeInTheDocument();
+ });
+
+ it('about tab shows version', async () => {
+ render();
+ await userEvent.click(screen.getByRole('tab', { name: /about/i }));
+ expect(screen.getByText(/markdownconverter/i)).toBeInTheDocument();
+ });
+
+ it('reset to defaults button clears all settings', async () => {
+ useSettingsStore.getState().setSetting('fontSize', 22);
+ render();
+ await userEvent.click(screen.getByRole('button', { name: /reset/i }));
+ expect(useSettingsStore.getState().fontSize).toBe(14);
+ });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+```bash
+npx vitest run tests/component/modals/SettingsSheet.test.tsx
+```
+
+Expected: FAIL.
+
+- [ ] **Step 3: Implement**
+
+Create `src/renderer/components/modals/SettingsSheet.tsx`:
+
+```tsx
+import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Button } from '@/components/ui/button';
+import { useAppStore } from '@/stores/app-store';
+import { useSettingsStore } from '@/stores/settings-store';
+import { EditorSettings } from './EditorSettings';
+import { ThemeSettings } from './ThemeSettings';
+import { ExportSettings } from './ExportSettings';
+import { PluginsSettings } from './PluginsSettings';
+import { AboutSettings } from './AboutSettings';
+
+export function SettingsSheet() {
+ const closeModal = useAppStore((s) => s.closeModal);
+ const resetToDefaults = useSettingsStore((s) => s.resetToDefaults);
+
+ return (
+ !o && closeModal()}>
+
+
+ Settings
+ Editor, theme, and export preferences
+
+
+
+ Editor
+ Theme
+ Export
+ Plugins
+ About
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 4: Run test to verify it passes**
+
+```bash
+npx vitest run tests/component/modals/SettingsSheet.test.tsx
+```
+
+Expected: PASS, 7 tests.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/renderer/components/modals/SettingsSheet.tsx tests/component/modals/SettingsSheet.test.tsx
+git commit -m "feat(renderer): SettingsSheet (5 tabs: editor, theme, export, plugins, about)"
+```
+
+---
+
+## Task 33: Extend `register-menu-commands.ts` with 9 new commands + test (TDD)
+
+**Files:**
+- Modify: `src/renderer/lib/commands/register-menu-commands.ts`
+- Create: `tests/unit/commands/modal-commands.test.ts`
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `tests/unit/commands/modal-commands.test.ts`:
+
+```ts
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { registerMenuCommands } from '@/lib/commands/register-menu-commands';
+import { useCommandStore } from '@/stores/command-store';
+import { useAppStore } from '@/stores/app-store';
+import { useFileStore } from '@/stores/file-store';
+import { useEditorStore } from '@/stores/editor-store';
+
+describe('modal commands', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ useCommandStore.setState({ handlers: {}, userBindings: {} } as any);
+ useAppStore.setState({ modal: { kind: null } } as any);
+ useFileStore.setState({ activeTabId: '/x.md', openTabs: [{ id: '/x.md', path: '/x.md', title: 'x.md', dirty: false }] } as any);
+ useEditorStore.setState({ buffers: new Map([['/x.md', { id: '/x.md', path: '/x.md', content: '# hi', dirty: false }]]) } as any);
+ });
+
+ it('settings.open opens settings modal', () => {
+ registerMenuCommands();
+ useCommandStore.getState().dispatch('settings.open');
+ expect(useAppStore.getState().modal).toEqual({ kind: 'settings' });
+ });
+
+ it('help.about opens about modal', () => {
+ registerMenuCommands();
+ useCommandStore.getState().dispatch('help.about');
+ expect(useAppStore.getState().modal).toEqual({ kind: 'about' });
+ });
+
+ it('help.welcome opens welcome modal', () => {
+ registerMenuCommands();
+ useCommandStore.getState().dispatch('help.welcome');
+ expect(useAppStore.getState().modal).toEqual({ kind: 'welcome' });
+ });
+
+ it('file.exportPdf opens export-pdf modal with active path', () => {
+ registerMenuCommands();
+ useCommandStore.getState().dispatch('file.exportPdf');
+ expect(useAppStore.getState().modal).toEqual({ kind: 'export-pdf', props: { sourcePath: '/x.md' } });
+ });
+
+ it('file.exportDocx opens export-docx modal', () => {
+ registerMenuCommands();
+ useCommandStore.getState().dispatch('file.exportDocx');
+ expect(useAppStore.getState().modal).toEqual({ kind: 'export-docx', props: { sourcePath: '/x.md' } });
+ });
+
+ it('file.exportBatch opens export-batch modal with all open files', () => {
+ useFileStore.setState({ activeTabId: '/x.md', openTabs: [{ id: '/x.md', path: '/x.md', title: 'x.md', dirty: false }, { id: '/y.md', path: '/y.md', title: 'y.md', dirty: false }] } as any);
+ registerMenuCommands();
+ useCommandStore.getState().dispatch('file.exportBatch');
+ expect(useAppStore.getState().modal).toEqual({ kind: 'export-batch', props: { sourcePaths: ['/x.md', '/y.md'] } });
+ });
+});
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+```bash
+npx vitest run tests/unit/commands/modal-commands.test.ts
+```
+
+Expected: FAIL — `Cannot find module '@/lib/commands/register-menu-commands'`.
+
+- [ ] **Step 3: Update `register-menu-commands.ts`**
+
+Open `src/renderer/lib/commands/register-menu-commands.ts` and add the new commands. The existing file (from Phase 6) registers `file.*` and `view.*` and `tab.*` commands. Append the new handlers.
+
+(If the file uses a `registerMany` pattern from Phase 6, the addition is a single object spread. The exact structure depends on what's already there; the test above defines the contract.)
+
+The new commands (full handler code):
+
+```ts
+import { useAppStore } from '@/stores/app-store';
+import { useFileStore } from '@/stores/file-store';
+import { useEditorStore } from '@/stores/editor-store';
+
+export function registerMenuCommands() {
+ const handlers: Record void | Promise> = {
+ // ... existing handlers from Phase 6 ...
+ 'settings.open': () => useAppStore.getState().openModal('settings'),
+ 'help.about': () => useAppStore.getState().openModal('about'),
+ 'help.welcome': () => useAppStore.getState().openModal('welcome'),
+ 'file.exportPdf': () => {
+ const activeTabId = useFileStore.getState().activeTabId;
+ if (!activeTabId) return;
+ useAppStore.getState().openModal('export-pdf', { sourcePath: activeTabId });
+ },
+ 'file.exportDocx': () => {
+ const activeTabId = useFileStore.getState().activeTabId;
+ if (!activeTabId) return;
+ useAppStore.getState().openModal('export-docx', { sourcePath: activeTabId });
+ },
+ 'file.exportHtml': () => {
+ const activeTabId = useFileStore.getState().activeTabId;
+ if (!activeTabId) return;
+ useAppStore.getState().openModal('export-html', { sourcePath: activeTabId });
+ },
+ 'file.exportBatch': () => {
+ const paths = useFileStore.getState().openTabs.map((t) => t.path);
+ if (paths.length === 0) return;
+ useAppStore.getState().openModal('export-batch', { sourcePaths: paths });
+ },
+ };
+ useCommandStore.getState().registerMany(handlers);
+}
+```
+
+(If the existing `registerMenuCommands` already has a return signature, preserve it. The key requirement is that the 9 new commands are registered.)
+
+- [ ] **Step 4: Run test to verify it passes**
+
+```bash
+npx vitest run tests/unit/commands/modal-commands.test.ts
+```
+
+Expected: PASS, 6 tests.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/renderer/lib/commands/register-menu-commands.ts tests/unit/commands/modal-commands.test.ts
+git commit -m "feat(renderer): register 9 modal-opening commands in command store"
+```
+
+---
+
+## Task 34: Extend `AppHeader` with Settings + About icon buttons + test (TDD)
+
+**Files:**
+- Modify: `src/renderer/components/layout/AppHeader.tsx`
+- Modify: `tests/component/layout/AppHeader.test.tsx`
+
+- [ ] **Step 1: Read the current AppHeader to see its command pattern**
+
+```bash
+cat src/renderer/components/layout/AppHeader.tsx
+```
+
+The header from Phase 6 already uses `useCommandStore` to dispatch `view.toggleSidebar`, `view.togglePreview`, `shortcuts.show`. Add Settings (gear icon) and About (info icon) buttons that dispatch `settings.open` and `help.about`.
+
+- [ ] **Step 2: Add the new buttons**
+
+In `AppHeader.tsx`, add (after the existing buttons, before the `ThemeToggle`):
+
+```tsx
+import { Settings, Info } from 'lucide-react';
+// ...
+const dispatch = useCommandStore((s) => s.dispatch);
+// ...
+
+
+```
+
+- [ ] **Step 3: Update the test**
+
+The test must register the matching commands in `beforeEach` (the AppHeader dispatches, doesn't call store directly). Add to `tests/component/layout/AppHeader.test.tsx`:
+
+```tsx
+beforeEach(() => {
+ // ... existing beforeEach ...
+ useCommandStore.setState({ handlers: { 'settings.open': () => useAppStore.getState().openModal('settings'), 'help.about': () => useAppStore.getState().openModal('about') }, userBindings: {} } as any);
+});
+
+it('settings button opens settings modal', async () => {
+ render();
+ await userEvent.click(screen.getByRole('button', { name: /settings/i }));
+ expect(useAppStore.getState().modal).toEqual({ kind: 'settings' });
+});
+
+it('about button opens about modal', async () => {
+ render();
+ await userEvent.click(screen.getByRole('button', { name: /^about$/i }));
+ expect(useAppStore.getState().modal).toEqual({ kind: 'about' });
+});
+```
+
+- [ ] **Step 4: Run tests**
+
+```bash
+npx vitest run tests/component/layout/AppHeader.test.tsx
+```
+
+Expected: PASS, 6 tests (2 existing + 2 new + the toggle tests from Phase 6).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/renderer/components/layout/AppHeader.tsx tests/component/layout/AppHeader.test.tsx
+git commit -m "feat(renderer): AppHeader gets Settings + About icon buttons"
+```
+
+---
+
+## Task 35: Mount `ModalLayer` in `App.tsx` + first-launch welcome `useEffect`
+
+**Files:**
+- Modify: `src/renderer/App.tsx`
+- Create: `src/renderer/hooks/use-welcome-trigger.ts`
+
+- [ ] **Step 1: Create the welcome trigger hook**
+
+Create `src/renderer/hooks/use-welcome-trigger.ts`:
+
+```ts
+import { useEffect } from 'react';
+import { useAppStore } from '@/stores/app-store';
+import { useSettingsStore } from '@/stores/settings-store';
+
+/**
+ * On first mount, if the user hasn't dismissed the welcome dialog, open it.
+ * Call once at the top of App.tsx.
+ */
+export function useWelcomeTrigger() {
+ useEffect(() => {
+ if (!useSettingsStore.getState().welcomeDismissed) {
+ useAppStore.getState().openModal('welcome');
+ }
+ }, []);
+}
+```
+
+- [ ] **Step 2: Mount `ModalLayer` + call the hook in `App.tsx`**
+
+Open `src/renderer/App.tsx`. The current structure (from Phase 2) is:
+
+```tsx
+import { AppShell } from './components/layout/AppShell';
+
+function App() {
+ return ;
+}
+export default App;
+```
+
+Replace with:
+
+```tsx
+import { AppShell } from './components/layout/AppShell';
+import { ModalLayer } from './components/modals/ModalLayer';
+import { useWelcomeTrigger } from './hooks/use-welcome-trigger';
+
+function App() {
+ useWelcomeTrigger();
+ return (
+ <>
+
+
+ >
+ );
+}
+export default App;
+```
+
+- [ ] **Step 3: Build verification**
+
+```bash
+npx vite build --config vite.renderer.config.ts
+```
+
+Expected: success.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/renderer/App.tsx src/renderer/hooks/use-welcome-trigger.ts
+git commit -m "feat(renderer): mount ModalLayer + first-launch welcome trigger"
+```
+
+---
+
+## Task 36: Phase 7 integration smoke test
+
+**Files:**
+- Create: `tests/integration/phase7-modals-smoke.test.tsx`
+
+- [ ] **Step 1: Write the test**
+
+```tsx
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from '@/App';
+import { useAppStore } from '@/stores/app-store';
+import { useCommandStore } from '@/stores/command-store';
+import { useSettingsStore } from '@/stores/settings-store';
+import { useFileStore } from '@/stores/file-store';
+import { useEditorStore } from '@/stores/editor-store';
+import { registerMenuCommands } from '@/lib/commands/register-menu-commands';
+
+vi.mock('@/lib/ipc', () => ({
+ ipc: {
+ app: {
+ getVersion: vi.fn().mockResolvedValue({ ok: true, data: '5.0.0' }),
+ openExternal: vi.fn().mockResolvedValue({ ok: true }),
+ },
+ },
+}));
+
+describe('Phase 7 modals integration', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ useAppStore.setState({ modal: { kind: null }, sidebarVisible: true, previewVisible: true, zenMode: false, paneSizes: { sidebar: 20, editor: 50, preview: 30 } } as any);
+ useCommandStore.setState({ handlers: {}, userBindings: {} } as any);
+ useSettingsStore.setState(useSettingsStore.getInitialState());
+ useFileStore.setState({ activeTabId: '/x.md', openTabs: [{ id: '/x.md', path: '/x.md', title: 'x.md', dirty: false }] } as any);
+ useEditorStore.setState({ buffers: new Map([['/x.md', { id: '/x.md', path: '/x.md', content: '# hi', dirty: false }]]) } as any);
+ });
+
+ it('dispatching settings.open from command store opens SettingsSheet', () => {
+ registerMenuCommands();
+ render();
+ useCommandStore.getState().dispatch('settings.open');
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+
+ it('AppHeader Settings button dispatches settings.open', async () => {
+ registerMenuCommands();
+ render();
+ await userEvent.click(screen.getByRole('button', { name: /settings/i }));
+ expect(useAppStore.getState().modal.kind).toBe('settings');
+ });
+
+ it('first launch with welcomeDismissed=false opens welcome modal', () => {
+ useSettingsStore.setState({ ...useSettingsStore.getInitialState(), welcomeDismissed: false });
+ registerMenuCommands();
+ render();
+ expect(useAppStore.getState().modal.kind).toBe('welcome');
+ });
+
+ it('first launch with welcomeDismissed=true does not open welcome', () => {
+ useSettingsStore.setState({ ...useSettingsStore.getInitialState(), welcomeDismissed: true });
+ registerMenuCommands();
+ render();
+ expect(useAppStore.getState().modal).toEqual({ kind: null });
+ });
+});
+```
+
+- [ ] **Step 2: Run test to verify it passes**
+
+```bash
+npx vitest run tests/integration/phase7-modals-smoke.test.tsx
+```
+
+Expected: PASS, 4 tests.
+
+If the App.tsx export is `default`, change the import to:
+
+```ts
+import App from '@/App';
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add tests/integration/phase7-modals-smoke.test.tsx
+git commit -m "test(renderer): Phase 7 modals integration smoke test"
+```
+
+---
+
+## Task 37: Phase 7 verification + tag
+
+- [ ] **Step 1: Run the full test suite**
+
+```bash
+npx vitest run
+```
+
+Expected: all tests pass. Total should be ~250-270 (target: +50 from the ~220 at end of Phase 6).
+
+- [ ] **Step 2: Build the renderer**
+
+```bash
+npx vite build --config vite.renderer.config.ts
+```
+
+Expected: `built in XXXms`, exit 0.
+
+- [ ] **Step 3: Lint**
+
+```bash
+npx eslint src tests
+```
+
+Expected: no errors.
+
+- [ ] **Step 4: Push the branch and tag**
+
+```bash
+git push origin react-electron
+git tag -a phase-7-modals -m "Phase 7 complete: ModalLayer, 7 modal types, settings store, ASCII table transform, DOCX template picker"
+git push origin react-electron --tags
+```
+
+---
+
+## Self-Review Notes
+
+**Spec coverage:**
+- §2.1 modal state in `useAppStore` → Task 15 ✓
+- §2.2 discriminated union → Task 15 ✓
+- §2.3 single `` → Task 17 ✓
+- §2.4 settings store → Task 16 ✓
+- §2.5 welcome trigger → Task 35 ✓
+- §2.6 commands trigger modals → Task 33 ✓
+- §3.1 shadcn primitives → Tasks 1-12 ✓
+- §3.2 modals → Tasks 17-26, 32 ✓
+- §3.3 stores → Tasks 15-16 ✓
+- §3.4 lib → Tasks 13-14 ✓
+- §3.5 modified files → Tasks 33-35 ✓
+- §3.6 tests → all tasks have tests ✓
+- §4 data flow → Tasks 23-26, 33-35 ✓
+- §5 error handling → inline error banner in Tasks 23-26; onRehydrateStorage in Task 16 ✓
+- §6 testing strategy → followed throughout ✓
+- §9 success criteria → Task 37 ✓
+
+**Placeholder scan:** None.
+
+**Type consistency:**
+- `useAppStore.modal` shape consistent across Tasks 15, 17, 33.
+- `useSettingsStore.setSetting` signature consistent across Tasks 16, 27-29.
+- `useExportSource` shape consistent across Tasks 21, 23-26.
+- IPC `ipc.export.{pdf,docx,html,batch}` signatures match `types/ipc.ts` from Phase 1.
+
+**One spec gap:** the spec mentions "ascii-table.test.ts (3 tests: simple table, alignment, empty input)" but Task 13 has 5 tests. Acceptable — the additional `applyAsciiTransform` tests are needed.
+
+**One risk:** Task 33 assumes `register-menu-commands.ts` has a specific shape. If the Phase 6 file uses a different structure (e.g., separate `useRegisterMenuCommands` hook), the engineer must adapt. The tests in Task 33 verify the behavior, not the internals.