170 lines
11 KiB
Markdown
170 lines
11 KiB
Markdown
---
|
||
phase: 10-ux-interaction
|
||
plan: 03
|
||
type: execute
|
||
wave: 0
|
||
depends_on: []
|
||
files_modified:
|
||
- frontend/src/components/ui/BreadcrumbBar.vue
|
||
- frontend/src/components/ui/__tests__/BreadcrumbBar.test.js
|
||
autonomous: true
|
||
requirements: [UX-12]
|
||
must_haves:
|
||
truths:
|
||
- "BreadcrumbBar renders the rootLabel as the first clickable segment when showRoot is true"
|
||
- "BreadcrumbBar does NOT render the root segment when showRoot is false (admin/settings views)"
|
||
- "The last segment is always rendered as plain non-clickable text"
|
||
- "Segments without an id render as plain text (admin static segments)"
|
||
- "Clicking an intermediate segment emits navigate(segment.id); clicking root emits navigate(null)"
|
||
- ">4 segments collapse to first + ellipsis + last two"
|
||
artifacts:
|
||
- path: "frontend/src/components/ui/BreadcrumbBar.vue"
|
||
provides: "Shared breadcrumb component used by file manager, cloud, admin, settings views"
|
||
- path: "frontend/src/components/ui/__tests__/BreadcrumbBar.test.js"
|
||
provides: "BreadcrumbBar unit tests"
|
||
key_links:
|
||
- from: "BreadcrumbBar.vue"
|
||
to: "AppIcon.vue"
|
||
via: "import for chevronRight separator"
|
||
pattern: "<AppIcon name=\"chevronRight\""
|
||
---
|
||
|
||
<objective>
|
||
Create `frontend/src/components/ui/BreadcrumbBar.vue` — a shared breadcrumb component that generalizes `FolderBreadcrumb.vue` to also serve admin, settings, and topics views. The existing `FolderBreadcrumb.vue` will be deleted in Wave 1 (plan 10-06) after BreadcrumbBar is wired everywhere.
|
||
|
||
Purpose: Per D-11/D-12/D-13, every view computes its own `segments` array. Admin views (e.g. `Admin › Users`) and settings (`Settings › Account`) need static segments without a "Home" root. File manager and cloud views use folder store breadcrumb data with a "Home" or "Cloud" root.
|
||
|
||
Output: `BreadcrumbBar.vue` (Options API, props segments/rootLabel/showRoot, emits navigate) + Vitest unit tests adapted from the existing FolderBreadcrumb tests.
|
||
</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/folders/FolderBreadcrumb.vue
|
||
@frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js
|
||
|
||
<interfaces>
|
||
Per D-11, D-12, D-13 (CONTEXT.md):
|
||
- Props:
|
||
- `segments` (Array, default []) — array of `{ id?, label }` objects
|
||
- Items without `id` render as plain non-clickable text (admin static segments)
|
||
- Last item is ALWAYS plain non-clickable text regardless of `id`
|
||
- `rootLabel` (String, default 'Home') — text for the root button when showRoot=true
|
||
- `showRoot` (Boolean, default true) — when false, omit the root button entirely
|
||
- Emits: `navigate` with `segment.id` (intermediate click) or `null` (root click)
|
||
- Last segment: plain `<span>`, no click handler
|
||
- >4 segments: collapse pattern `[first, {id:'ellipsis', label:'…'}, ...last_two]`
|
||
|
||
Reference implementation (FolderBreadcrumb.vue):
|
||
- Uses `<script setup>` (we will use Options API per CLAUDE.md for THIS new component — exception in PATTERNS.md says BreadcrumbBar may use script setup since it replaces a setup component; pick Options API anyway for consistency with EmptyState/AppIcon)
|
||
- Renders `<nav aria-label>` > `<ol>` > `<li>` segments
|
||
|
||
Segment shape change:
|
||
- FolderBreadcrumb uses `{ id, name }` — BreadcrumbBar uses `{ id?, label }`
|
||
- Wave 1 plans will map `{id, name}` → `{id, label: name}` at the call sites
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 1: Create BreadcrumbBar.test.js with failing tests</name>
|
||
<files>frontend/src/components/ui/__tests__/BreadcrumbBar.test.js</files>
|
||
<read_first>
|
||
- frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js (reference test patterns)
|
||
- frontend/src/components/folders/FolderBreadcrumb.vue (current behavior reference)
|
||
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"BreadcrumbBar.vue"
|
||
</read_first>
|
||
<behavior>
|
||
- Test 1: `renders rootLabel button when showRoot=true` — mount with rootLabel='Home', segments=[], assert wrapper.find('button').text() === 'Home'
|
||
- Test 2: `does NOT render root button when showRoot=false` — mount with showRoot=false, segments=[{label: 'Users'}], assert wrapper.findAll('button').length === 0
|
||
- Test 3: `clicking root button emits navigate(null)` — mount default with empty segments, click first button, assert emitted.navigate[0] === [null]
|
||
- Test 4: `last segment renders as a non-clickable <span>` — mount with segments=[{id:'a', label:'A'}, {id:'b', label:'B'}], assert text contains 'B' AND wrapper.findAll('button').filter(b => b.text() === 'B').length === 0
|
||
- Test 5: `clicking intermediate segment emits navigate(segment.id)` — mount with segments=[{id:'r1', label:'Root'}, {id:'f1', label:'Test'}], click the 'Root' button, assert emitted.navigate[0] === ['r1']
|
||
- Test 6: `segments without id render as plain text even when intermediate` — mount with segments=[{label:'Admin'}, {label:'Users'}], showRoot=false, assert NO buttons exist (both render as spans)
|
||
- Test 7: `>4 segments collapse to first + ellipsis + last two` — mount with 5 segments, assert text contains the first segment label, '…', and the last 2 labels but NOT segments 2-3
|
||
- Test 8: `rootLabel defaults to "Home"` — mount with no rootLabel prop, assert first button text === 'Home'
|
||
- Test 9: `custom rootLabel "Cloud" renders correctly` — mount with rootLabel='Cloud', assert first button text === 'Cloud'
|
||
- Test 10: `renders chevronRight separator between segments` — mount with segments=[{id:'a', label:'A'}, {id:'b', label:'B'}] and stubs: { AppIcon: true }, assert AppIcon stubs are present (count >= 1)
|
||
</behavior>
|
||
<action>
|
||
Create `frontend/src/components/ui/__tests__/BreadcrumbBar.test.js`. Adapt the test style from `frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js`. Stub AppIcon via `global: { stubs: { AppIcon: true } }`. Use the segment shape `{ id, label }` (NOT `{ id, name }`). Write all 10 tests above. Tests fail until Task 2.
|
||
</action>
|
||
<verify>
|
||
<automated>cd frontend && npm run test -- --run BreadcrumbBar</automated>
|
||
Expected: 10 tests collected; all fail (RED).
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- File `frontend/src/components/ui/__tests__/BreadcrumbBar.test.js` exists with 10 `it(...)` blocks
|
||
- Tests use segment shape `{ id, label }` exclusively
|
||
- Running `cd frontend && npm run test -- --run BreadcrumbBar` shows 10 failing tests
|
||
</acceptance_criteria>
|
||
<done>10 failing tests in place describing the BreadcrumbBar contract.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 2: Implement BreadcrumbBar.vue</name>
|
||
<files>frontend/src/components/ui/BreadcrumbBar.vue</files>
|
||
<read_first>
|
||
- frontend/src/components/ui/__tests__/BreadcrumbBar.test.js (failing tests)
|
||
- frontend/src/components/folders/FolderBreadcrumb.vue (reference template — lines 1-44)
|
||
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"BreadcrumbBar.vue"
|
||
</read_first>
|
||
<behavior>
|
||
- Options API component `BreadcrumbBar`, registers `AppIcon` from `./AppIcon.vue`
|
||
- Props: `segments` (Array, default []), `rootLabel` (String, default 'Home'), `showRoot` (Boolean, default true)
|
||
- Emits: `['navigate']`
|
||
- Computed `visibleSegments`: if `segments.length > 4` returns `[segments[0], { id: 'ellipsis', label: '…' }, ...segments.slice(-2)]`; else returns `segments`
|
||
- Template renders `<nav aria-label="Navigation"><ol class="flex items-center gap-1 text-sm flex-wrap">`
|
||
- When `showRoot`: render the root `<li><button @click="$emit('navigate', null)">{{ rootLabel }}</button></li>`
|
||
- For each segment in visibleSegments with index:
|
||
- Separator `<li aria-hidden><AppIcon name="chevronRight" class="w-3 h-3 text-gray-400" /></li>` shown only when there is something to the left (showRoot OR idx > 0)
|
||
- If `segment.id === 'ellipsis'`: `<li><span>…</span></li>`
|
||
- If `idx === visibleSegments.length - 1`: `<li><span class="text-gray-900 font-medium">{{ segment.label }}</span></li>`
|
||
- Else if `segment.id`: `<li><button @click="$emit('navigate', segment.id)" class="text-indigo-600 hover:underline font-medium">{{ segment.label }}</button></li>`
|
||
- Else: `<li><span class="text-gray-500">{{ segment.label }}</span></li>` (no id, no click)
|
||
</behavior>
|
||
<action>
|
||
Create `frontend/src/components/ui/BreadcrumbBar.vue` using Options API. Import AppIcon from `./AppIcon.vue`. Implement exactly the template described in the behavior block. Match the FolderBreadcrumb visual style (text-indigo-600, hover:underline for clickable; text-gray-900 font-medium for the last segment; text-gray-400 for separator). Use `<AppIcon name="chevronRight" class="w-3 h-3 text-gray-400" />` for separators (NO inline svg — this is the new icon centralization paradigm). Skip the separator before the first segment when `!showRoot`. No comments inside the file.
|
||
</action>
|
||
<verify>
|
||
<automated>cd frontend && npm run test -- --run BreadcrumbBar</automated>
|
||
Expected: all 10 tests PASS.
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- File `frontend/src/components/ui/BreadcrumbBar.vue` exists
|
||
- File imports AppIcon from `./AppIcon.vue`
|
||
- File contains `name: 'BreadcrumbBar'`
|
||
- Template contains `<AppIcon name="chevronRight"` (separator uses AppIcon, not inline svg)
|
||
- Template contains `aria-label="Navigation"` (or similar) on the `<nav>`
|
||
- All 10 tests pass
|
||
- File uses Options API
|
||
</acceptance_criteria>
|
||
<done>BreadcrumbBar.vue implemented; 10 tests green.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
- `cd frontend && npm run test -- --run BreadcrumbBar` exits 0 with 10 passing tests
|
||
- `grep -E "name:\\s*'BreadcrumbBar'" frontend/src/components/ui/BreadcrumbBar.vue` returns 1 match
|
||
- `grep -E "rootLabel" frontend/src/components/ui/BreadcrumbBar.vue` returns ≥ 2 matches (prop + template)
|
||
- `grep -E "showRoot" frontend/src/components/ui/BreadcrumbBar.vue` returns ≥ 2 matches
|
||
- `grep -E "<AppIcon name=\"chevronRight\"" frontend/src/components/ui/BreadcrumbBar.vue` returns ≥ 1 match
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
BreadcrumbBar.vue is ready to replace FolderBreadcrumb.vue everywhere. Wave 1 plan 10-06 will swap the import in StorageBrowser.vue, update FileManagerView/CloudFolderView to map breadcrumb to `{id, label}`, wire admin/settings views to pass static segments, and delete `FolderBreadcrumb.vue`.
|
||
</success_criteria>
|
||
|
||
<output>
|
||
Create `.planning/phases/10-ux-interaction/10-03-SUMMARY.md` when done.
|
||
</output>
|