---
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 <svg> path for any name in the icon map"
    - "AppIcon forwards the consumer's class attribute to the outer <svg> 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\\]"
---

<objective>
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 `<svg>` 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.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@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

<interfaces>
Per D-08, D-09, D-10 (CONTEXT.md):
- Props: `name` (String, required)
- inheritAttrs: false
- $attrs.class forwarded to outer <svg>
- 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`
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create AppIcon.test.js with failing tests for the contract</name>
  <files>frontend/src/components/ui/__tests__/AppIcon.test.js</files>
  <read_first>
    - 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)
  </read_first>
  <behavior>
    - Test 1: `renders <svg> 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 <svg> 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 <path> 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
  </behavior>
  <action>
    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.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run AppIcon</automated>
    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.
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>Test file exists with 6 failing tests describing the AppIcon contract.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement AppIcon.vue with the full ICON_PATHS map</name>
  <files>frontend/src/components/ui/AppIcon.vue</files>
  <read_first>
    - 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)
  </read_first>
  <behavior>
    - 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: `<svg v-if="resolvedPaths" :class="$attrs.class" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">` containing either a `<template v-if="Array.isArray(resolvedPaths)">` rendering `<path v-for>` (for cog), else a single `<path>`
    - All `<path>` elements MUST include `stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="..."`
  </behavior>
  <action>
    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.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run AppIcon</automated>
    Expected: All 6 tests from Task 1 PASS (GREEN).
  </verify>
  <acceptance_criteria>
    - 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 `<script setup>`
    - `cog` key value is an Array of length 2
  </acceptance_criteria>
  <done>AppIcon.vue exists with full icon map; all 6 tests pass.</done>
</task>

</tasks>

<verification>
- `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`)
</verification>

<success_criteria>
AppIcon.vue is the single source of truth for icon paths. Wave 5's SVG migration (10-12) can replace every inline `<svg>` block with `<AppIcon name="..." class="..." />` and the visual result matches the existing rendering pixel-for-pixel (same fill/stroke/viewBox/path conventions).
</success_criteria>

<output>
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.
</output>
