--- phase: 10-ux-interaction plan: 01 type: execute wave: 0 depends_on: [] files_modified: - frontend/src/components/ui/AppIcon.vue - frontend/src/components/ui/__tests__/AppIcon.test.js autonomous: true requirements: [CODE-05] must_haves: truths: - "AppIcon renders the correct path for any name in the icon map" - "AppIcon forwards the consumer's class attribute to the outer element" - "AppIcon supports both single-path and dual-path (Array) icons (cog uses two paths)" - "Unknown icon names trigger console.warn in dev mode and render nothing" artifacts: - path: "frontend/src/components/ui/AppIcon.vue" provides: "Centralized icon registry component" contains: "ICON_PATHS" - path: "frontend/src/components/ui/__tests__/AppIcon.test.js" provides: "AppIcon unit tests" key_links: - from: "AppIcon.vue script" to: "ICON_PATHS map" via: "computed resolvedPaths" pattern: "ICON_PATHS\\[this\\.name\\]" --- Create `frontend/src/components/ui/AppIcon.vue` — the single source of truth for all SVG icon paths used in DocuVault. This is the foundation for the CODE-05 SVG migration that happens in Wave 5. Purpose: Eliminate ~66 inline `` blocks across 29 files by centralizing the icon paths into one map. All existing SVGs use the same outline/stroke style (`fill="none" stroke="currentColor" viewBox="0 0 24 24"` with `stroke-linecap="round" stroke-linejoin="round" stroke-width="2"`) — AppIcon matches this exactly so the migration is a drop-in replacement. Output: `AppIcon.vue` (Options API, ~30 named icons including the dual-path `cog`) + a Vitest unit test covering name→path rendering, class forwarding, dual-path handling, and unknown-name warning. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @CLAUDE.md @.planning/phases/10-ux-interaction/10-CONTEXT.md @.planning/phases/10-ux-interaction/10-RESEARCH.md @.planning/phases/10-ux-interaction/10-PATTERNS.md @frontend/src/components/ui/AppSpinner.vue @frontend/src/components/layout/AppSidebar.vue @frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js Per D-08, D-09, D-10 (CONTEXT.md): - Props: `name` (String, required) - inheritAttrs: false - $attrs.class forwarded to outer - SVG attrs: `fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"` - Path attrs: `stroke-linecap="round" stroke-linejoin="round" stroke-width="2"` - Array path support for `cog` (dual path) - Unknown name: console.warn in import.meta.env.DEV; render nothing (v-if guard) Project Vitest layout (verified from existing tests at frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js): - Tests live in __tests__/ alongside the component - Uses `@vue/test-utils` `mount` - Run command: `cd frontend && npm run test` Task 1: Create AppIcon.test.js with failing tests for the contract frontend/src/components/ui/__tests__/AppIcon.test.js - frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js (test style — vitest + @vue/test-utils mount) - .planning/phases/10-ux-interaction/10-PATTERNS.md (AppIcon target structure) - .planning/phases/10-ux-interaction/10-RESEARCH.md §"Component Inventory §1: SVG Audit" (full icon name list) - Test 1: `renders with the correct d attribute for a known single-path icon (folder)` — mount with name="folder", assert svg.path has d starting with "M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9" - Test 2: `forwards consumer class to the element` — mount with name="folder" and attrs class "w-4 h-4 text-amber-500", assert wrapper.find('svg').classes() contains those classes - Test 3: `renders TWO elements for a dual-path icon (cog)` — mount with name="cog", assert wrapper.findAll('path').length === 2 - Test 4: `renders the standard SVG attrs (fill=none, stroke=currentColor, viewBox=0 0 24 24)` — mount any name, assert wrapper.find('svg').attributes('fill') === 'none' and stroke === 'currentColor' and viewBox === '0 0 24 24' - Test 5: `renders no svg and calls console.warn when name is unknown` — spy on console.warn, mount name="bogus-name", assert wrapper.find('svg').exists() === false and console.warn called once with a message containing "bogus-name" - Test 6: `path elements use stroke-linecap=round, stroke-linejoin=round, stroke-width=2` — mount name="folder", read path attributes Create `frontend/src/components/ui/__tests__/AppIcon.test.js`. Import AppIcon from `../AppIcon.vue` (file does NOT exist yet — tests will fail on import, that is correct). Use `import { describe, it, expect, vi } from 'vitest'` and `import { mount } from '@vue/test-utils'`. Follow the test-file style in `frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js`. Implement all 6 tests above as concrete assertions. For console.warn, set `import.meta.env.DEV = true` if needed via a `vi.stubGlobal` or rely on Vitest's default DEV=true environment. Do NOT create AppIcon.vue yet — this is the RED phase. cd frontend && npm run test -- --run AppIcon Expected: test file is collected; all 6 tests FAIL with "Cannot resolve module ../AppIcon.vue" or "Failed to resolve component". This is the RED state. - File `frontend/src/components/ui/__tests__/AppIcon.test.js` exists - File contains exactly 6 `it(...)` blocks inside one `describe('AppIcon', ...)` block - Running `cd frontend && npm run test -- --run AppIcon` shows 6 failing tests (RED — AppIcon.vue does not exist yet) - Tests use `mount` from `@vue/test-utils` (not `shallowMount`) - Tests use `vi.spyOn(console, 'warn')` or `vi.fn()` to assert the dev warning Test file exists with 6 failing tests describing the AppIcon contract. Task 2: Implement AppIcon.vue with the full ICON_PATHS map frontend/src/components/ui/AppIcon.vue - frontend/src/components/ui/__tests__/AppIcon.test.js (the failing tests from Task 1) - frontend/src/components/ui/AppSpinner.vue (single-purpose SVG analog) - .planning/phases/10-ux-interaction/10-PATTERNS.md §"AppIcon.vue" (full target structure) - .planning/phases/10-ux-interaction/10-RESEARCH.md §"Component Inventory §1: SVG Audit" (complete d-attr values) - Component is Options API per CLAUDE.md (project convention) - Component name: `AppIcon` - `inheritAttrs: false` - Props: `name: { type: String, required: true }` - Computed `resolvedPaths()` returns `ICON_PATHS[this.name] ?? null`; if null AND `import.meta.env.DEV`, calls `console.warn('[AppIcon] Unknown icon name: "' + this.name + '"')` - Template: ` Create `frontend/src/components/ui/AppIcon.vue` using the Options API template from `10-PATTERNS.md §"AppIcon.vue"`. Declare a module-scope `const ICON_PATHS = { ... }` with EXACTLY these 31 keys (28 single + 1 dual + 2 added): Single-path keys (use d-values exactly from 10-RESEARCH.md §Component Inventory §1): `plus`, `folder`, `folderMove`, `pencil`, `trash`, `share`, `document`, `fileDoc`, `chevronRight`, `chevronDown`, `tag`, `inbox`, `cloud`, `shield`, `logout`, `home`, `users`, `chartBar`, `clipboardList`, `upload`, `x`, `checkCircle`, `exclamationCircle`, `warning`, `copy`, `check`, `checkMark`, `refresh`, `pencilEdit`, `lightBulb`, `search`, `dots`. Dual-path key (Array value, two strings — see RESEARCH.md): `cog: ['M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z', 'M15 12a3 3 0 11-6 0 3 3 0 016 0z']` For the `search` key, use d = `'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0'` (per D-05 Heroicons outline search). For `dots`, use the stroke replacement d = `'M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z'` (Pattern: per D-09 outline-only convention — replaces FolderRow fill-based dots). Use the exact template structure from 10-PATTERNS.md AppIcon.vue section (lines 47-73 of the analog block). NO comments inside the file describing what the code does (CLAUDE.md non-negotiable). Do NOT add a `default` prop value for name; required is the contract. cd frontend && npm run test -- --run AppIcon Expected: All 6 tests from Task 1 PASS (GREEN). - File `frontend/src/components/ui/AppIcon.vue` exists - File contains module-scope `const ICON_PATHS` with at least 30 keys (verify: `grep -c "':" frontend/src/components/ui/AppIcon.vue` returns ≥ 30 or count keys via parse) - File contains `inheritAttrs: false` - File contains `import.meta.env.DEV` reference - File contains `Array.isArray(resolvedPaths)` check - Running `cd frontend && npm run test -- --run AppIcon` exits 0 with 6 passing tests (GREEN) - File uses Options API (`export default { name: 'AppIcon', ... }`) per CLAUDE.md, NOT ` AppIcon.vue exists with full icon map; all 6 tests pass. - `cd frontend && npm run test -- --run AppIcon` exits 0 - `grep -E "ICON_PATHS\\s*=" frontend/src/components/ui/AppIcon.vue` returns 1 match - `grep -E "inheritAttrs:\\s*false" frontend/src/components/ui/AppIcon.vue` returns 1 match - `grep -E "Array\\.isArray\\(resolvedPaths\\)" frontend/src/components/ui/AppIcon.vue` returns 1 match - `grep -c "'.*':.*'M" frontend/src/components/ui/AppIcon.vue` returns ≥ 28 (single-path icon count, excluding the array `cog`) AppIcon.vue is the single source of truth for icon paths. Wave 5's SVG migration (10-12) can replace every inline `` block with `` and the visual result matches the existing rendering pixel-for-pixel (same fill/stroke/viewBox/path conventions). Create `.planning/phases/10-ux-interaction/10-01-SUMMARY.md` when done with: what was built, the final ICON_PATHS key count, and any deviations from the planned key list.