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>
This commit is contained in:
curo1305
2026-06-17 14:34:52 +02:00
co-authored by Claude Sonnet 4.6
parent e008bf7dae
commit 123ae5b29b
101 changed files with 759 additions and 4 deletions
@@ -0,0 +1,406 @@
---
phase: 10-ux-interaction
plan: 06
type: execute
wave: 1
depends_on: [10-02, 10-03, 10-04, 10-05]
files_modified:
- 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
autonomous: true
requirements: [UX-02, UX-01, UX-10, UX-12]
must_haves:
truths:
- "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"
artifacts:
- path: "frontend/src/components/storage/StorageBrowser.vue"
provides: "Updated with skeleton + EmptyState + BreadcrumbBar"
- path: "frontend/src/views/FileManagerView.vue"
provides: "Updated breadcrumb mapping + toast call sites"
- path: "frontend/src/views/CloudFolderView.vue"
provides: "Updated breadcrumb mapping"
key_links:
- from: "StorageBrowser.vue"
to: "BreadcrumbBar.vue"
via: "import + <BreadcrumbBar :segments=\"breadcrumb\" />"
pattern: "import BreadcrumbBar"
- from: "FileManagerView.vue"
to: "useToastStore"
via: "show() called in doMove and doDeleteDoc"
pattern: "useToastStore"
---
<objective>
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
</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/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
<interfaces>
**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: `<div class="w-7 h-7 bg-gray-100 rounded-lg animate-pulse"></div>`
- Col 2: `<div class="h-4 bg-gray-100 rounded animate-pulse w-2/3"></div>`
- Col 3: `<div class="h-3 bg-gray-100 rounded animate-pulse hidden md:block"></div>`
- Col 4: `<div class="h-3 bg-gray-100 rounded animate-pulse hidden sm:block"></div>`
- Col 5: `<div class="w-14 h-3 bg-gray-100 rounded animate-pulse"></div>`
**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
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Promote StorageBrowser.skeleton.test.js stubs to real tests</name>
<files>frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js</files>
<read_first>
- 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"
</read_first>
<behavior>
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).
</behavior>
<action>
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.
</action>
<verify>
<automated>cd frontend && npm run test -- --run StorageBrowser.skeleton</automated>
Expected: 4 UX-02 tests FAIL (RED — StorageBrowser still shows Loading… text); 3 UX-13 .todo entries stay pending.
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>4 RED tests in place for UX-02 skeleton behavior.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Update StorageBrowser.vue — skeleton, EmptyState, BreadcrumbBar swap</name>
<files>frontend/src/components/storage/StorageBrowser.vue</files>
<read_first>
- 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"
</read_first>
<behavior>
1. Import swap: replace `import FolderBreadcrumb from '../folders/FolderBreadcrumb.vue'` with `import BreadcrumbBar from '../ui/BreadcrumbBar.vue'`; also import `EmptyState`.
2. Template: replace `<FolderBreadcrumb :segments="breadcrumb" ...>` with `<BreadcrumbBar :segments="breadcrumb" :root-label="mode === 'cloud' ? 'Cloud' : 'Home'" @navigate="$emit('breadcrumb-navigate', $event)" />`.
3. Replace the three inline empty-state divs (lines 226-241 of current file) with `<EmptyState>` blocks based on the discriminators:
- Search no-results: `<EmptyState v-else-if="!loading && searchQuery && (folders.length + files.length) === 0" icon="search" :headline="'No results for ' + JSON.stringify(searchQuery)" 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>`
- In-folder empty (breadcrumb non-empty): `<EmptyState v-else-if="!loading && breadcrumb.length > 0 && folders.length === 0 && files.length === 0 && !showNewFolderInput" icon="document" :headline="emptyMessage" :subtext="emptyHint" />`
- Root empty: `<EmptyState v-else-if="!loading && breadcrumb.length === 0 && folders.length === 0 && files.length === 0 && !showNewFolderInput" icon="folder" :headline="emptyMessage" :subtext="emptyHint" />`
(Use the `emptyMessage` / `emptyHint` props for headline/subtext so existing parent prop-driven configuration still works.)
4. Replace the `<div v-if="loading" ...>Loading…</div>` element (current line 244) with a `<template v-if="loading">` block containing 5 skeleton row divs matching the grid:
```
<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>
```
Ensure the empty-state blocks use `v-else-if` so they only render when `!loading`.
</behavior>
<action>
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.
</action>
<verify>
<automated>cd frontend && npm run test -- --run StorageBrowser.skeleton</automated>
Expected: 4 UX-02 tests PASS.
</verify>
<acceptance_criteria>
- `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 "<FolderBreadcrumb" frontend/src/components/storage/StorageBrowser.vue` returns 0
- `grep -E "<BreadcrumbBar" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "<EmptyState" frontend/src/components/storage/StorageBrowser.vue` returns 3 (root, folder, search)
- `grep -v '^#' frontend/src/components/storage/StorageBrowser.vue | grep -c "Loading…"` returns 0
- `grep -E "animate-pulse" frontend/src/components/storage/StorageBrowser.vue` returns ≥ 5
- `cd frontend && npm run test -- --run StorageBrowser.skeleton` exits 0 (UX-02 GREEN)
</acceptance_criteria>
<done>StorageBrowser has skeleton, EmptyState, and BreadcrumbBar wired; UX-02 tests green.</done>
</task>
<task type="auto">
<name>Task 3: Update FileManagerView + CloudFolderView for breadcrumb mapping + toast wiring; delete FolderBreadcrumb</name>
<files>
frontend/src/views/FileManagerView.vue,
frontend/src/views/CloudFolderView.vue,
frontend/src/components/folders/FolderBreadcrumb.vue,
frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js
</files>
<read_first>
- 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"
</read_first>
<action>
**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.
</action>
<verify>
<automated>cd frontend && npm run test -- --run FileManagerView &amp;&amp; cd frontend && npm run test -- --run StorageBrowser.skeleton &amp;&amp; cd frontend && npm run test -- --run BreadcrumbBar &amp;&amp; cd frontend && npm run test -- --run toast</automated>
Expected: all relevant test suites pass. FolderBreadcrumb tests no longer collected (file deleted).
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>FileManagerView + CloudFolderView wired; FolderBreadcrumb deleted; toast call sites live; all tests green.</done>
</task>
</tasks>
<verification>
- `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 `<EmptyState>` blocks and exactly 1 `<BreadcrumbBar>` block
- StorageBrowser.vue contains 5 skeleton `<div>`s with `animate-pulse`
- Loading… text removed (grep returns 0 in StorageBrowser.vue)
</verification>
<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>
<output>
Create `.planning/phases/10-ux-interaction/10-06-SUMMARY.md` when done. List which behaviors became GREEN and the exact line ranges modified.
</output>