Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
9.4 KiB
Markdown
172 lines
9.4 KiB
Markdown
---
|
|
phase: 10-ux-interaction
|
|
plan: 02
|
|
type: execute
|
|
wave: 0
|
|
depends_on: []
|
|
files_modified:
|
|
- frontend/src/components/ui/EmptyState.vue
|
|
- frontend/src/components/ui/__tests__/EmptyState.test.js
|
|
autonomous: true
|
|
requirements: [UX-01]
|
|
must_haves:
|
|
truths:
|
|
- "EmptyState renders the headline, subtext, and optional icon based on props"
|
|
- "EmptyState renders nothing in the CTA area when the #cta slot is empty"
|
|
- "EmptyState supports size='sm' for sidebar micro states and size='md' (default) for full centered layout"
|
|
artifacts:
|
|
- path: "frontend/src/components/ui/EmptyState.vue"
|
|
provides: "Shared empty-state component used in 7+ contexts"
|
|
- path: "frontend/src/components/ui/__tests__/EmptyState.test.js"
|
|
provides: "EmptyState unit tests"
|
|
key_links:
|
|
- from: "EmptyState.vue template"
|
|
to: "AppIcon.vue"
|
|
via: "import + <AppIcon :name=\"icon\" />"
|
|
pattern: "import AppIcon"
|
|
---
|
|
|
|
<objective>
|
|
Create `frontend/src/components/ui/EmptyState.vue` — a shared, props-driven empty-state component that replaces every inline "No items yet" / "Nothing here" pattern across StorageBrowser, SharedView, CloudStorageView, AppSidebar, AdminAuditView.
|
|
|
|
Purpose: Per D-06/D-07, every zero-content context gets its own icon + headline + subtext + optional CTA slot. No per-view "no items" text remains in the app after Wave 1 wiring.
|
|
|
|
Output: `EmptyState.vue` (Options API, props: icon/headline/subtext/size, #cta slot) + Vitest unit tests covering prop rendering, slot rendering, and size variants.
|
|
</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/__tests__/FolderBreadcrumb.test.js
|
|
@frontend/src/views/SharedView.vue
|
|
@frontend/src/components/layout/AppSidebar.vue
|
|
|
|
<interfaces>
|
|
Per D-06, D-07 (CONTEXT.md):
|
|
- Props:
|
|
- `icon` (String, default null) — name from AppIcon registry (e.g. 'folder', 'inbox', 'cloud', 'search')
|
|
- `headline` (String, required) — main message text
|
|
- `subtext` (String, default '') — secondary description
|
|
- `size` (String, default 'md') — accepts 'sm' for sidebar micro states or 'md' for default centered layout
|
|
- Named `#cta` slot — renders nothing when slot is empty (use `$slots.cta` check or `<slot name="cta">` with no fallback content)
|
|
- Component is Options API (CLAUDE.md non-negotiable for new components)
|
|
- Depends on AppIcon.vue (from 10-01) — import as child component
|
|
|
|
Size class mapping (from 10-PATTERNS.md):
|
|
| size | container | icon | headline | subtext |
|
|
|------|-----------|------|----------|---------|
|
|
| md (default) | `text-center py-10 px-4` | `w-8 h-8 mx-auto mb-3 text-gray-300` | `text-sm font-medium text-gray-500` | `text-xs text-gray-400 mt-1` |
|
|
| sm | `flex items-center gap-2 py-1 text-xs text-gray-400` | `w-3.5 h-3.5 shrink-0` | `''` (empty — inherits from container) | `'hidden'` (sidebar micro states show no subtext) |
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Create EmptyState.test.js with failing tests</name>
|
|
<files>frontend/src/components/ui/__tests__/EmptyState.test.js</files>
|
|
<read_first>
|
|
- frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js (test style)
|
|
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"EmptyState.vue" (target structure + class table)
|
|
</read_first>
|
|
<behavior>
|
|
- Test 1: `renders headline text from the headline prop` — mount with headline='Nothing here', assert wrapper.text() includes 'Nothing here'
|
|
- Test 2: `renders subtext when provided; omits when empty` — mount with subtext='hello', assert text includes 'hello'; mount with no subtext, assert no second <p>
|
|
- Test 3: `renders AppIcon when icon prop is set; renders no icon when null` — mount with icon='folder' and stubs AppIcon, assert AppIcon component is found; mount with icon=null, assert no AppIcon
|
|
- Test 4: `renders nothing in the CTA area when #cta slot is empty` — mount with no slots, assert wrapper does NOT contain any <button> or <a> elements
|
|
- Test 5: `renders the #cta slot content when provided` — mount with slots: { cta: '<button data-test="cta-btn">Upload</button>' }, assert wrapper.find('[data-test="cta-btn"]').exists() === true
|
|
- Test 6: `size='sm' applies the sidebar micro layout (flex container with gap-2)` — mount with size='sm', assert root element classList contains 'flex' and 'gap-2'
|
|
- Test 7: `default size (md) applies the centered layout (text-center py-10)` — mount default, assert classList contains 'text-center' and 'py-10'
|
|
</behavior>
|
|
<action>
|
|
Create `frontend/src/components/ui/__tests__/EmptyState.test.js`. Use Vitest + @vue/test-utils. Stub the AppIcon component via `global.stubs: { AppIcon: true }` in mount options so tests don't depend on Wave 0 task ordering. Tests will fail until Task 2 creates EmptyState.vue. Write all 7 tests above as concrete `it(...)` blocks.
|
|
</action>
|
|
<verify>
|
|
<automated>cd frontend && npm run test -- --run EmptyState</automated>
|
|
Expected: tests collected; all 7 FAIL with module-not-found (RED phase).
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- File `frontend/src/components/ui/__tests__/EmptyState.test.js` exists
|
|
- File contains exactly 7 `it(...)` blocks
|
|
- Running `cd frontend && npm run test -- --run EmptyState` shows 7 failing tests
|
|
- Tests stub AppIcon via mount options (no hard dependency on AppIcon.vue existing)
|
|
</acceptance_criteria>
|
|
<done>Test file exists with 7 failing tests describing the EmptyState contract.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Implement EmptyState.vue</name>
|
|
<files>frontend/src/components/ui/EmptyState.vue</files>
|
|
<read_first>
|
|
- frontend/src/components/ui/__tests__/EmptyState.test.js (the failing tests)
|
|
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"EmptyState.vue" (full target structure incl. class table)
|
|
- frontend/src/views/SharedView.vue (existing inline empty-state pattern being replaced)
|
|
- frontend/src/components/layout/AppSidebar.vue (existing sidebar micro pattern: pl-7 py-1 text-xs text-gray-400)
|
|
</read_first>
|
|
<behavior>
|
|
- Options API component named `EmptyState`
|
|
- Registers `AppIcon` as a child component (imported from `./AppIcon.vue`)
|
|
- Props: `icon` (String, default null), `headline` (String, required), `subtext` (String, default ''), `size` (String, default 'md')
|
|
- Computed `containerClass`, `iconClass`, `headlineClass`, `subtextClass` returning the strings from the class table in 10-PATTERNS.md
|
|
- Template renders, in order: container `<div>`, AppIcon (when icon truthy), headline `<p>`, subtext `<p>` (when subtext truthy AND size !== 'sm' — sidebar micro hides subtext via the `hidden` class), `<slot name="cta" />`
|
|
</behavior>
|
|
<action>
|
|
Create `frontend/src/components/ui/EmptyState.vue` using the Options API target structure from `10-PATTERNS.md §"EmptyState.vue"`. Import `AppIcon` from `./AppIcon.vue`. Use the exact class mapping table from PATTERNS:
|
|
- size='md' container: `'text-center py-10 px-4'`; icon: `'w-8 h-8 mx-auto mb-3 text-gray-300'`; headline: `'text-sm font-medium text-gray-500'`; subtext: `'text-xs text-gray-400 mt-1'`
|
|
- size='sm' container: `'flex items-center gap-2 py-1 text-xs text-gray-400'`; icon: `'w-3.5 h-3.5 shrink-0'`; headline: `''`; subtext: `'hidden'`
|
|
|
|
Template structure:
|
|
```
|
|
<template>
|
|
<div :class="containerClass">
|
|
<AppIcon v-if="icon" :name="icon" :class="iconClass" />
|
|
<p :class="headlineClass">{{ headline }}</p>
|
|
<p v-if="subtext" :class="subtextClass">{{ subtext }}</p>
|
|
<slot name="cta" />
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
Do NOT add explanatory comments. Use Options API (CLAUDE.md).
|
|
</action>
|
|
<verify>
|
|
<automated>cd frontend && npm run test -- --run EmptyState</automated>
|
|
Expected: all 7 tests PASS (GREEN).
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- File `frontend/src/components/ui/EmptyState.vue` exists
|
|
- File imports AppIcon from `./AppIcon.vue`
|
|
- File contains `name: 'EmptyState'`
|
|
- File contains all 4 computed properties: `containerClass`, `iconClass`, `headlineClass`, `subtextClass`
|
|
- File contains `<slot name="cta" />`
|
|
- All 7 tests pass: `cd frontend && npm run test -- --run EmptyState` exits 0
|
|
- File uses Options API, not `<script setup>`
|
|
</acceptance_criteria>
|
|
<done>EmptyState.vue implemented; all 7 tests green.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `cd frontend && npm run test -- --run EmptyState` exits 0 with 7 passing tests
|
|
- `grep -E "name:\\s*'EmptyState'" frontend/src/components/ui/EmptyState.vue` returns 1 match
|
|
- `grep -E "import AppIcon" frontend/src/components/ui/EmptyState.vue` returns 1 match
|
|
- `grep -E "slot name=\"cta\"" frontend/src/components/ui/EmptyState.vue` returns 1 match
|
|
- `grep -v '^#' frontend/src/components/ui/EmptyState.vue | grep -c '<script setup>'` returns 0 (Options API enforced)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
EmptyState.vue is ready to be wired into all 9 empty-state contexts identified in 10-RESEARCH.md §Component Inventory §3. Wave 1 plans 10-06, 10-07, 10-08 will replace inline empty-state divs with `<EmptyState icon="..." headline="..." subtext="..." />` blocks (plus #cta slot where applicable).
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/10-ux-interaction/10-02-SUMMARY.md` when done.
|
|
</output>
|