Files
kite/.planning/milestones/v0.2-phases/10-ux-interaction/10-06-PLAN.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
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>
2026-06-17 14:34:52 +02:00

24 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
10-ux-interaction 06 execute 1
10-02
10-03
10-04
10-05
frontend/src/components/storage/StorageBrowser.vue
frontend/src/views/FileManagerView.vue
frontend/src/views/CloudFolderView.vue
frontend/src/components/folders/FolderBreadcrumb.vue
frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js
frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js
true
UX-02
UX-01
UX-10
UX-12
truths artifacts key_links
StorageBrowser shows 5+ animated skeleton rows when loading=true (no Loading… text)
Empty file list renders <EmptyState> with the appropriate icon/headline/subtext per context (root, folder, search)
BreadcrumbBar replaces FolderBreadcrumb in StorageBrowser; FolderBreadcrumb.vue is deleted in this plan
FileManagerView maps foldersStore.breadcrumb to [{id, label}] before passing as segments
CloudFolderView maps its breadcrumb to [{id, label}] before passing as segments
FileManagerView toast wiring fires on document delete and document move success/failure
path provides
frontend/src/components/storage/StorageBrowser.vue Updated with skeleton + EmptyState + BreadcrumbBar
path provides
frontend/src/views/FileManagerView.vue Updated breadcrumb mapping + toast call sites
path provides
frontend/src/views/CloudFolderView.vue Updated breadcrumb mapping
from to via pattern
StorageBrowser.vue BreadcrumbBar.vue import + <BreadcrumbBar :segments="breadcrumb" /> import BreadcrumbBar
from to via pattern
FileManagerView.vue useToastStore show() called in doMove and doDeleteDoc useToastStore
Wire the Wave 0 foundation components into `StorageBrowser.vue`, `FileManagerView.vue`, and `CloudFolderView.vue`. This plan completes UX-02 (skeleton rows), the StorageBrowser parts of UX-01 (EmptyState), the file-manager parts of UX-12 (BreadcrumbBar swap and FolderBreadcrumb deletion), and the FileManagerView parts of UX-10 (toast call sites).

Per CLAUDE.md "no dead code", FolderBreadcrumb.vue and its test file are deleted in the SAME commit as the BreadcrumbBar swap.

Output:

  • StorageBrowser: 5-row skeleton (UX-02), three EmptyState variants (root/folder/search), import swap FolderBreadcrumb → BreadcrumbBar
  • FileManagerView: breadcrumb {id, name}{id, label} mapping, toast.show() on doMove + doDeleteDoc success/error
  • CloudFolderView: breadcrumb mapping + BreadcrumbBar with rootLabel='Cloud'
  • FolderBreadcrumb.vue + FolderBreadcrumb.test.js DELETED
  • StorageBrowser.skeleton.test.js stub promoted to real tests

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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/storage/StorageBrowser.vue @frontend/src/views/FileManagerView.vue @frontend/src/views/CloudFolderView.vue @frontend/src/components/folders/FolderBreadcrumb.vue @frontend/src/components/ui/EmptyState.vue @frontend/src/components/ui/BreadcrumbBar.vue @frontend/src/stores/toast.js **StorageBrowser already uses `<script setup>`** — modifications stay in that style. Skeleton classes from RESEARCH.md §Pitfall 7: - Container: `grid grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 px-4 py-2.5 items-center border-b border-gray-100` - Col 1: `
` - Col 2: `
` - Col 3: `
` - Col 4: `
` - Col 5: `
`

EmptyState contexts in StorageBrowser (from RESEARCH.md §Component Inventory §3):

Condition icon headline subtext
Root: !currentFolderId && lists empty && !searchQuery folder "Nothing here yet" "Create a folder or upload your first file to get started."
In folder: currentFolderId && lists empty && !searchQuery document "This folder is empty" "Upload files above or create a sub-folder."
Search: searchQuery && lists empty search No results for "{{ searchQuery }}" "Try a different search term or clear the filter."

StorageBrowser does not know currentFolderId directly — keep using the existing emptyMessage and emptyHint props, but change FileManagerView to pass per-context strings, OR replace the inline empty divs with three explicit conditionals inside StorageBrowser using breadcrumb.length > 0 as a proxy for "in folder".

Decision: keep the props (emptyMessage, emptyHint) but render via <EmptyState> block. Use breadcrumb.length === 0 (root) vs > 0 (in folder) as the discriminator inside StorageBrowser for the icon name (folder vs document). For search no-results, the searchQuery check overrides.

BreadcrumbBar wiring:

  • StorageBrowser passes through :segments="breadcrumb" — parents must already map name→label
  • FileManagerView template: :breadcrumb="mappedBreadcrumb" where mappedBreadcrumb = computed mapping foldersStore.breadcrumb to [{id: f.id, label: f.name}]
  • CloudFolderView: existing breadcrumb computed already returns {id, name} — add a mappedBreadcrumb computed
  • Pass rootLabel="Home" for local mode and rootLabel="Cloud" for cloud mode (handled by passing through a prop or hardcoding in StorageBrowser based on mode)

Toast call sites in FileManagerView (per RESEARCH.md §Toast System):

  • doMove(docId, folderId): on success → useToastStore().show('Document moved', 'success'); on catch → useToastStore().show('Move failed: ' + (e.message || 'unknown error'), 'error')
  • doDeleteDoc(docId): on success → useToastStore().show('Document deleted', 'success'); on catch → useToastStore().show('Delete failed: ' + (e.message || 'unknown error'), 'error')
  • (Upload toasts are handled in plan 10-07 to keep file ownership clean? — NO, FileManagerView owns upload too. Defer upload toast wiring to this plan as well.)
  • onFilesSelected({files, autoClassify}): after Promise.allSettled, count items with done=true and items with errors, and show one toast: show(${successCount} of ${files.length} file(s) uploaded, successCount === files.length ? 'success' : 'warning') and for each item with item.error set, show show('Upload failed: ' + item.error, 'error').

FolderBreadcrumb.vue deletion:

  • Delete frontend/src/components/folders/FolderBreadcrumb.vue
  • Delete frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js
  • Grep verify no remaining import: grep -r "FolderBreadcrumb" frontend/src/ returns 0 matches after edits
Task 1: Promote StorageBrowser.skeleton.test.js stubs to real tests frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js - frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js (Wave 0 stub from plan 10-05) - frontend/src/components/storage/StorageBrowser.vue (current state) - .planning/phases/10-ux-interaction/10-PATTERNS.md §"5-column StorageBrowser skeleton" Replace the `.todo` entries (UX-02 group) with real assertions: - Test 1: `renders 5 skeleton rows when loading=true and lists empty` — mount StorageBrowser with loading=true, folders=[], files=[], stub BreadcrumbBar/SearchBar/SortControls/DropZone/UploadProgress/TopicBadge. Assert `wrapper.findAll('.animate-pulse').length >= 5` (or assert a count of skeleton row containers >= 5). - Test 2: `Loading… text is absent when loading=true` — mount loading=true, assert `wrapper.text()` does NOT include 'Loading…'. - Test 3: `skeleton rows are NOT rendered when loading=false` — mount loading=false, folders=[], files=[], assert `wrapper.findAll('.animate-pulse').length === 0`. - Test 4: `skeleton row grid matches grid-cols-[2rem_1fr_6rem_8rem_6rem]` — mount loading=true, assert at least one element with class `grid-cols-[2rem_1fr_6rem_8rem_6rem]` exists in the skeleton block.
Keep the UX-13 `it.todo` stubs as-is (deferred to plan 10-12).
Modify `frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js`. Replace ONLY the UX-02 `describe` block's `it.todo` entries with the 4 real tests above. Keep the UX-13 describe block untouched (its tests are promoted in plan 10-12). Import `StorageBrowser` from `../StorageBrowser.vue` and use `import { mount } from '@vue/test-utils'`. Stub child components via `global.stubs: { BreadcrumbBar: true, SearchBar: true, SortControls: true, DropZone: true, UploadProgress: true, TopicBadge: true }`. Create a fresh Pinia instance per test using `setActivePinia(createPinia())` if needed. cd frontend && npm run test -- --run StorageBrowser.skeleton Expected: 4 UX-02 tests FAIL (RED — StorageBrowser still shows Loading… text); 3 UX-13 .todo entries stay pending. - File still exists with both `describe('UX-02: ...')` and `describe('UX-13: ...')` blocks - The UX-02 describe block contains 4 real `it(...)` tests (no `.todo` for UX-02) - The UX-13 describe block still contains `.todo` entries - Running `cd frontend && npm run test -- --run StorageBrowser.skeleton` shows 4 failing UX-02 tests 4 RED tests in place for UX-02 skeleton behavior. Task 2: Update StorageBrowser.vue — skeleton, EmptyState, BreadcrumbBar swap frontend/src/components/storage/StorageBrowser.vue - frontend/src/components/storage/StorageBrowser.vue (current state — uses <script setup>) - frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js (failing tests) - frontend/src/components/ui/BreadcrumbBar.vue (interface) - frontend/src/components/ui/EmptyState.vue (interface) - .planning/phases/10-ux-interaction/10-PATTERNS.md §"StorageBrowser.vue" + §"5-column StorageBrowser skeleton" - .planning/phases/10-ux-interaction/10-RESEARCH.md §"Component Inventory §3" 1. Import swap: replace `import FolderBreadcrumb from '../folders/FolderBreadcrumb.vue'` with `import BreadcrumbBar from '../ui/BreadcrumbBar.vue'`; also import `EmptyState`. 2. Template: replace `` with ``. 3. Replace the three inline empty-state divs (lines 226-241 of current file) with `` blocks based on the discriminators: - Search no-results: `Clear search` - In-folder empty (breadcrumb non-empty): `` - Root empty: `` (Use the `emptyMessage` / `emptyHint` props for headline/subtext so existing parent prop-driven configuration still works.) 4. Replace the `
Loading…
` element (current line 244) with a `` block containing 5 skeleton row divs matching the grid: ```
``` Ensure the empty-state blocks use `v-else-if` so they only render when `!loading`. Edit `frontend/src/components/storage/StorageBrowser.vue`:
**Step 1 — Imports (in `<script setup>` block):**
Replace `import FolderBreadcrumb from '../folders/FolderBreadcrumb.vue'` with `import BreadcrumbBar from '../ui/BreadcrumbBar.vue'` and add `import EmptyState from '../ui/EmptyState.vue'`.

**Step 2 — Template breadcrumb usage:**
Replace lines 7-10 (the `<FolderBreadcrumb>` block) with:
```
<BreadcrumbBar
  :segments="breadcrumb"
  :root-label="mode === 'cloud' ? 'Cloud' : 'Home'"
  @navigate="$emit('breadcrumb-navigate', $event)"
/>
```

**Step 3 — Replace empty state and loading blocks (current lines 226-244):**
Replace the existing three blocks (lines 226-244) with:
```
<template v-if="loading">
  <div
    v-for="n in 5"
    :key="`sk-${n}`"
    class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center border-b border-gray-100"
  >
    <div class="w-7 h-7 bg-gray-100 rounded-lg animate-pulse"></div>
    <div class="h-4 bg-gray-100 rounded animate-pulse w-2/3"></div>
    <div class="h-3 bg-gray-100 rounded animate-pulse hidden md:block"></div>
    <div class="h-3 bg-gray-100 rounded animate-pulse hidden sm:block"></div>
    <div class="w-14 h-3 bg-gray-100 rounded animate-pulse"></div>
  </div>
</template>

<EmptyState
  v-else-if="searchQuery && folders.length === 0 && files.length === 0"
  icon="search"
  :headline="`No results for &quot;${searchQuery}&quot;`"
  subtext="Try a different search term or clear the filter."
>
  <template #cta>
    <button @click="$emit('search-change', '')" class="mt-3 text-sm text-indigo-600 hover:underline">
      Clear search
    </button>
  </template>
</EmptyState>

<EmptyState
  v-else-if="breadcrumb.length > 0 && folders.length === 0 && files.length === 0 && !showNewFolderInput"
  icon="document"
  :headline="emptyMessage"
  :subtext="emptyHint"
/>

<EmptyState
  v-else-if="folders.length === 0 && files.length === 0 && !showNewFolderInput"
  icon="folder"
  :headline="emptyMessage"
  :subtext="emptyHint"
/>
```

No comments. Keep the rest of StorageBrowser.vue unchanged.
cd frontend && npm run test -- --run StorageBrowser.skeleton Expected: 4 UX-02 tests PASS. - `grep -E "import BreadcrumbBar from '../ui/BreadcrumbBar.vue'" frontend/src/components/storage/StorageBrowser.vue` returns 1 - `grep -E "import EmptyState from '../ui/EmptyState.vue'" frontend/src/components/storage/StorageBrowser.vue` returns 1 - `grep -E "import FolderBreadcrumb" frontend/src/components/storage/StorageBrowser.vue` returns 0 - `grep -E " StorageBrowser has skeleton, EmptyState, and BreadcrumbBar wired; UX-02 tests green. Task 3: Update FileManagerView + CloudFolderView for breadcrumb mapping + toast wiring; delete FolderBreadcrumb frontend/src/views/FileManagerView.vue, frontend/src/views/CloudFolderView.vue, frontend/src/components/folders/FolderBreadcrumb.vue, frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js - frontend/src/views/FileManagerView.vue (current state — uses <script setup>) - frontend/src/views/CloudFolderView.vue (current breadcrumb computed) - frontend/src/stores/folders.js (foldersStore.breadcrumb shape) - frontend/src/stores/toast.js (useToastStore signature) - .planning/phases/10-ux-interaction/10-PATTERNS.md §"FileManagerView.vue" **Step A — Modify `frontend/src/views/FileManagerView.vue`:**
Add `import { useToastStore } from '../stores/toast.js'` near the other imports.

Add a `computed` for `mappedBreadcrumb`:
```js
const mappedBreadcrumb = computed(() =>
  (foldersStore.breadcrumb || []).map(f => ({ id: f.id, label: f.name }))
)
```

Change the template `:breadcrumb="foldersStore.breadcrumb"` to `:breadcrumb="mappedBreadcrumb"` on the `<StorageBrowser>` element.

Update `doMove`:
```js
async function doMove(docId, folderId) {
  const toast = useToastStore()
  try {
    await docsStore.moveToFolder(docId, folderId)
    toast.show('Document moved', 'success')
  } catch (e) {
    toast.show('Move failed: ' + (e.message || 'unknown error'), 'error')
  }
}
```

Update `doDeleteDoc`:
```js
async function doDeleteDoc(docId) {
  const toast = useToastStore()
  try {
    await docsStore.remove(docId)
    toast.show('Document deleted', 'success')
  } catch (e) {
    toast.show('Delete failed: ' + (e.message || 'unknown error'), 'error')
  }
}
```

Update `onFilesSelected` to fire a summary toast at the end:
```js
async function onFilesSelected({ files, autoClassify }) {
  const folderId = currentFolderId.value
  const toast = useToastStore()
  const promises = files.map(file => {
    const item = reactive({ name: file.name, done: false, error: null, quotaError: null, topics: null })
    uploadQueue.value.unshift(item)
    return docsStore.upload(file, autoClassify, folderId)
      .then(({ doc }) => { item.done = true; item.topics = doc.topics ?? [] })
      .catch(e => {
        if (e.status === 413 && e.payload) item.quotaError = e.payload
        else item.error = e.message
      })
  })
  await Promise.allSettled(promises)
  await topicsStore.fetchTopics()
  const succeeded = uploadQueue.value.slice(0, files.length).filter(i => i.done).length
  if (succeeded === files.length) {
    toast.show(`${succeeded} file(s) uploaded`, 'success')
  } else if (succeeded > 0) {
    toast.show(`${succeeded} of ${files.length} file(s) uploaded`, 'warning')
  } else {
    toast.show('Upload failed', 'error')
  }
}
```
(Important: the upload error per-item already populates `item.error`; UploadProgress already renders these. The summary toast is in addition.)

**Step B — Modify `frontend/src/views/CloudFolderView.vue`:**

Add a `mappedBreadcrumb` computed mapping `breadcrumb.value` (or the existing computed) to `[{id, label: name}]`. Pass `:breadcrumb="mappedBreadcrumb"` instead of `:breadcrumb="breadcrumb"`. Leave everything else unchanged.

**Step C — Delete FolderBreadcrumb files:**

Delete both files:
- `frontend/src/components/folders/FolderBreadcrumb.vue`
- `frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js`

Use the file deletion tool or `rm` via Bash. Confirm with `ls frontend/src/components/folders/`.

No comments added. No `console.error` removed from doMove/doDeleteDoc — leave existing behavior intact aside from the toast addition. Actually: REMOVE the `console.error(e.message)` lines because the toast now communicates the error to the user.

Per D-11/D-13: the `mappedBreadcrumb` computed is also used in any test for FileManagerView — update those tests in the next step.

Final grep verification: `grep -r "FolderBreadcrumb" frontend/src/` returns 0 matches.
cd frontend && npm run test -- --run FileManagerView && cd frontend && npm run test -- --run StorageBrowser.skeleton && cd frontend && npm run test -- --run BreadcrumbBar && cd frontend && npm run test -- --run toast Expected: all relevant test suites pass. FolderBreadcrumb tests no longer collected (file deleted). - File `frontend/src/components/folders/FolderBreadcrumb.vue` no longer exists - File `frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js` no longer exists - `grep -r "FolderBreadcrumb" frontend/src/` returns 0 lines - `grep -E "useToastStore" frontend/src/views/FileManagerView.vue` returns ≥ 4 occurrences (import + 3 action handlers minimum) - `grep -E "toast\\.show\\('Document moved'" frontend/src/views/FileManagerView.vue` returns 1 match - `grep -E "toast\\.show\\('Document deleted'" frontend/src/views/FileManagerView.vue` returns 1 match - `grep -E "mappedBreadcrumb" frontend/src/views/FileManagerView.vue` returns ≥ 2 matches - `grep -E "mappedBreadcrumb" frontend/src/views/CloudFolderView.vue` returns ≥ 2 matches - `grep -E "console\\.error" frontend/src/views/FileManagerView.vue` returns ≤ 1 (or 0 if previously only present in doMove/doDeleteDoc) - Suites pass: FileManagerView, StorageBrowser.skeleton, BreadcrumbBar, toast all exit 0 FileManagerView + CloudFolderView wired; FolderBreadcrumb deleted; toast call sites live; all tests green. - `cd frontend && npm run test -- --run StorageBrowser.skeleton` exits 0 - `cd frontend && npm run test -- --run FileManagerView` exits 0 - `cd frontend && npm run test -- --run BreadcrumbBar` exits 0 (regression) - `cd frontend && npm run test -- --run toast` exits 0 (regression) - `grep -r "FolderBreadcrumb" frontend/src/` returns 0 lines (dead code removed) - `cd frontend && npm run test -- --run` (full suite) exits 0 — no other suite broke - StorageBrowser.vue contains exactly 3 `` blocks and exactly 1 `` block - StorageBrowser.vue contains 5 skeleton `
`s with `animate-pulse` - Loading… text removed (grep returns 0 in StorageBrowser.vue)

<success_criteria> Loading the file manager with loading=true shows 5 animated skeleton rows. After load completes:

  • Empty root folder → "Nothing here yet" with folder icon
  • Empty sub-folder → "This folder is empty" with document icon
  • Search with no matches → "No results for {query}" with search icon and Clear button Breadcrumbs use the shared BreadcrumbBar with rootLabel="Home"/"Cloud" per mode. Document move/delete/upload actions fire toasts. </success_criteria>
Create `.planning/phases/10-ux-interaction/10-06-SUMMARY.md` when done. List which behaviors became GREEN and the exact line ranges modified.