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,393 @@
---
phase: 10-ux-interaction
plan: 13
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/components/ui/TreeItem.vue
- frontend/src/components/storage/StorageBrowser.vue
- frontend/src/App.vue
- frontend/src/components/documents/SearchBar.vue
- frontend/src/components/layout/OsDragOverlay.vue
- frontend/src/__tests__/keyboard.test.js
- frontend/src/components/ui/__tests__/TreeItem.test.js
- frontend/src/components/storage/__tests__/StorageBrowser.showSearch.test.js
autonomous: true
requirements: [UX-02, UX-03, UX-05, UX-06, UX-07, UX-08, UX-09]
gap_closure: true
must_haves:
truths:
- "Sidebar Cloud/Folder sections show animated shimmer rows while async data loads — no 'Loading…' text visible"
- "Admin routes (/admin/*) render with no AppSidebar — AdminLayout is the sole layout component"
- "Pressing '/', 'U', or 'N' in the file manager dispatches to the actual FileManagerView instance — not a RouterView proxy"
- "Pressing Escape in the search input clears the field and preserves focus — no browser-native blur"
- "Dropping an OS file on the drag overlay triggers upload — capture-phase window listener fires before folder-row handlers"
- "Search bar and sort controls are visible at the root of both local and cloud file browsers"
artifacts:
- path: "frontend/src/components/ui/TreeItem.vue"
provides: "Three animate-pulse shimmer rows in the v-if='loading' branch"
contains: "animate-pulse"
- path: "frontend/src/components/storage/StorageBrowser.vue"
provides: "showSearch computed returns true for mode === 'local' OR mode === 'cloud'"
- path: "frontend/src/App.vue"
provides: "Third template branch for admin routes (no AppSidebar); getFileManagerInstance() via matched.find for keyboard dispatch"
- path: "frontend/src/components/documents/SearchBar.vue"
provides: "@keydown.escape.prevent.stop suppresses browser native blur on type='search'"
- path: "frontend/src/components/layout/OsDragOverlay.vue"
provides: "window drop listener registered with capture=true; removeEventListener also passes true"
key_links:
- from: "frontend/src/App.vue (onKeydown)"
to: "FileManagerView.focusSearch / triggerUpload / startNewFolder"
via: "router.currentRoute.value.matched.find(r => r.instances?.default)?.instances?.default"
pattern: "instances\\.default"
- from: "frontend/src/components/layout/OsDragOverlay.vue"
to: "window drop event"
via: "addEventListener('drop', handler, true)"
pattern: "addEventListener.*drop.*true"
---
<objective>
Close all 6 UAT gaps from 10-UAT.md that block Phase 10 sign-off: sidebar shimmer (Gap 1), search-at-root visibility (Gap 2), admin sidebar bleed (Gap 3), keyboard shortcuts broken by RouterView proxy (Gap 4 — covers tests 11, 12, 13), Escape-breaks-search (Gap 5), and OS drag-drop not uploading (Gap 6).
Purpose: Phase 10 is recorded complete in ROADMAP.md but UAT (10-UAT.md) shows 9 issues with 6 distinct root causes diagnosed. These targeted fixes close all 6 causes with minimal code change, restoring full UAT pass.
Output: Five production files modified, three test files added. No new dependencies. No regressions.
</objective>
<execution_context>
@/Users/nik/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nik/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@/Users/nik/Documents/Progamming/document_scanner/.planning/PROJECT.md
@/Users/nik/Documents/Progamming/document_scanner/.planning/ROADMAP.md
@/Users/nik/Documents/Progamming/document_scanner/.planning/STATE.md
@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/10-ux-interaction/10-UAT.md
<interfaces>
<!-- Key contracts the executor needs. Extracted from codebase. No codebase exploration needed. -->
From frontend/src/App.vue (current full state — relevant sections):
```html
<!-- Template — only two branches today: -->
<AuthLayout v-if="route.meta.layout === 'auth'" />
<div v-else class="flex h-screen overflow-hidden">
<AppSidebar />
<main class="flex-1 overflow-y-auto">
<router-view ref="routeViewRef" /> <!-- resolves to RouterView proxy, NOT FileManagerView -->
</main>
</div>
<ToastContainer />
<OsDragOverlay @files-dropped="onOsFilesDropped" />
```
```js
// script setup
import { ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const routeViewRef = ref(null) // ← remove this
function onOsFilesDropped(files) {
routeViewRef.value?.handleOsDrop?.(files) // ← fix to use getFileManagerInstance()
}
function onKeydown(e) {
// guard: INPUT/TEXTAREA/SELECT/contentEditable + dialog check omitted here
routeViewRef.value?.focusSearch?.() // line 37 ← fix
routeViewRef.value?.clearSearch?.() // line 40 ← fix
routeViewRef.value?.triggerUpload?.() // line 43 ← fix
routeViewRef.value?.startNewFolder?.() // line 46 ← fix
}
```
From frontend/src/router/index.js — admin route meta:
```js
{
path: '/admin',
component: () => import('../layouts/AdminLayout.vue'),
meta: { requiresAdmin: true }, // only on parent; children resolved via matched.some()
children: [/* AdminOverviewView, AdminUsersView, AdminQuotasView, AdminAiView, AdminAuditView */]
}
```
From frontend/src/components/ui/TreeItem.vue — loading branch to replace (lines 48-52):
```html
<div
v-if="loading"
class="text-xs text-gray-400 py-1"
:style="{ paddingLeft: `${(depth + 1) * 12 + 8}px` }"
>
Loading…
</div>
```
Shimmer pattern to replicate (from AppSidebar.vue lines 60-64):
```html
<div v-if="loadingRoots" class="pl-7 py-1 space-y-1">
<div v-for="n in 3" :key="`sk-f-${n}`" class="flex items-center gap-2 py-1">
<div class="w-4 h-4 bg-gray-100 rounded animate-pulse shrink-0"></div>
<div class="h-3 bg-gray-100 rounded animate-pulse" :style="{ width: (50 + n * 15) + 'px' }"></div>
</div>
</div>
```
From frontend/src/components/storage/StorageBrowser.vue line 287:
```js
const showSearch = computed(() => props.mode === 'local' && props.breadcrumb.length > 0)
// TARGET STATE:
const showSearch = computed(() => props.mode === 'local' || props.mode === 'cloud')
```
From frontend/src/components/documents/SearchBar.vue line 11:
```html
@keydown.escape="emit('update:modelValue', '')"
<!-- TARGET STATE: -->
@keydown.escape.prevent.stop="emit('update:modelValue', '')"
```
From frontend/src/components/layout/OsDragOverlay.vue (Options API — lines 52-63):
```js
mounted() {
window.addEventListener('dragenter', this.onDragEnter)
window.addEventListener('dragleave', this.onDragLeave)
window.addEventListener('dragover', this.onDragOver)
window.addEventListener('drop', this.onDrop) // ← bubble phase; must become capture
},
beforeUnmount() {
window.removeEventListener('dragenter', this.onDragEnter)
window.removeEventListener('dragleave', this.onDragLeave)
window.removeEventListener('dragover', this.onDragOver)
window.removeEventListener('drop', this.onDrop) // ← must match with true
}
```
From frontend/src/views/FileManagerView.vue defineExpose (lines 190-196):
```js
// These ARE correct — all methods are exposed. The problem is App.vue never reaches this instance.
defineExpose({
focusSearch: () => browserRef.value?.focusSearch?.(),
triggerUpload: () => browserRef.value?.triggerUpload?.(),
startNewFolder: () => browserRef.value?.startNewFolder?.(),
clearSearch: () => browserRef.value?.clearSearch?.(),
handleOsDrop: (files) => onFilesSelected({ files, autoClassify: true }),
})
```
From frontend/src/__tests__/keyboard.test.js (existing structure):
Uses mountFileManager() helper, vi.mock('../api/client.js', ...), vi.mock('../stores/auth.js', ...),
vi.mock('../stores/topics.js', ...), setActivePinia(createPinia()), createRouter+createMemoryHistory.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Sidebar shimmer rows (TreeItem.vue) and search-at-root (StorageBrowser.vue)</name>
<files>
frontend/src/components/ui/TreeItem.vue
frontend/src/components/storage/StorageBrowser.vue
</files>
<action>
TreeItem.vue — replace the v-if="loading" div (lines 48-52) with a shimmer block. The replacement wraps three shimmer rows in a single container div. Apply the indent :style on the container (same binding the removed div had: `{ paddingLeft: \`\${(depth + 1) * 12 + 8}px\` }`). The container also gets `class="py-1 space-y-1"`. Each of the three inner rows uses `class="flex items-center gap-2 py-1"` and contains two children:
- Icon placeholder: `class="w-4 h-4 bg-gray-100 rounded animate-pulse shrink-0"`
- Text placeholder: `class="h-3 bg-gray-100 rounded animate-pulse"` with `:style="{ width: (50 + n * 15) + 'px' }"`
Use `v-for="n in 3"` with `:key="\`sk-t-\${n}\`"` on the inner row div.
Do not change any other branch (v-else-if loadError, v-else-if children.length===0, slot children) or any script section.
StorageBrowser.vue — change line 287 only. Change:
`const showSearch = computed(() => props.mode === 'local' && props.breadcrumb.length > 0)`
to:
`const showSearch = computed(() => props.mode === 'local' || props.mode === 'cloud')`
No template changes needed — SearchBar and SortControls already share v-if="showSearch".
</action>
<verify>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && grep -c "animate-pulse" src/components/ui/TreeItem.vue && grep "showSearch" src/components/storage/StorageBrowser.vue | grep "cloud"</automated>
</verify>
<done>
- `grep -c "animate-pulse" src/components/ui/TreeItem.vue` returns at least 2
- `grep -c "Loading" src/components/ui/TreeItem.vue` returns 0
- `grep "showSearch" src/components/storage/StorageBrowser.vue` contains `props.mode === 'cloud'`
</done>
</task>
<task type="auto">
<name>Task 2: Admin sidebar bleed and keyboard instance resolution (App.vue)</name>
<files>frontend/src/App.vue</files>
<action>
Two root causes, one file. Apply both changes together.
CHANGE A — Admin sidebar bleed (Gap 3):
Insert a v-else-if branch in the template between the auth branch and the user-layout div. The new branch condition is `route.matched.some(r => r.meta.requiresAdmin)`. When true it renders only `<router-view />` — no AppSidebar, no main wrapper. The complete corrected template root (inside the component root, excluding ToastContainer and OsDragOverlay which stay unchanged):
1. `<AuthLayout v-if="route.meta.layout === 'auth'" />`
2. `<router-view v-else-if="route.matched.some(r => r.meta.requiresAdmin)" />`
3. `<div v-else class="flex h-screen overflow-hidden">` ... AppSidebar + main + router-view (no ref attribute) ... `</div>`
Note: the router-view in branch 3 must NOT carry `ref="routeViewRef"` — refs on router-view resolve to the RouterView proxy (same root cause as Gap 4). Remove the ref attribute.
CHANGE B — Keyboard instance resolution (Gap 4, also fixes tests 12 and 13):
The `routeViewRef` ref and its usage must be replaced with a direct instance lookup:
1. Add `useRouter` to the `vue-router` import: `import { useRoute, useRouter } from 'vue-router'`
2. Add `const router = useRouter()` after `const route = useRoute()`
3. Add helper: `function getFileManagerInstance() { return router.currentRoute.value.matched.find(r => r.instances?.default)?.instances?.default ?? null }`
4. Remove `const routeViewRef = ref(null)` — no longer used
5. Remove `ref` from the `ref` import if it is now unused (check — ref was only used for routeViewRef)
6. Replace all `routeViewRef.value?.X?.()` calls in onKeydown with `getFileManagerInstance()?.X?.()`
7. Replace `routeViewRef.value?.handleOsDrop?.(files)` in onOsFilesDropped with `getFileManagerInstance()?.handleOsDrop?.(files)`
After both changes, `ref` from vue should be removed from the import line if nothing else uses it (check first — if ToastContainer or OsDragOverlay use a local ref, keep it; otherwise remove to avoid lint warnings).
</action>
<verify>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && grep "routeViewRef" src/App.vue; echo "exit:$?"; grep "instances.default" src/App.vue; grep "requiresAdmin" src/App.vue</automated>
</verify>
<done>
- `grep "routeViewRef" src/App.vue` returns no output (fully removed)
- `grep "instances.default" src/App.vue` returns the getFileManagerInstance() line
- `grep "requiresAdmin" src/App.vue` returns the v-else-if template branch
- `grep "useRouter" src/App.vue` appears in import and instantiation
</done>
</task>
<task type="auto">
<name>Task 3: Escape modifier (SearchBar.vue) and capture-phase drop (OsDragOverlay.vue)</name>
<files>
frontend/src/components/documents/SearchBar.vue
frontend/src/components/layout/OsDragOverlay.vue
</files>
<action>
SearchBar.vue — one character change on line 11. Add `.prevent.stop` modifiers to `@keydown.escape`:
Before: `@keydown.escape="emit('update:modelValue', '')"`
After: `@keydown.escape.prevent.stop="emit('update:modelValue', '')"`
`.prevent` stops the browser's native clear+blur behavior on type="search" inputs. `.stop` stops bubbling to App.vue's global keydown handler (which would call clearSearch() a second time). No other changes.
OsDragOverlay.vue — two lines change in the Options API lifecycle hooks:
In mounted(): change `window.addEventListener('drop', this.onDrop)` to `window.addEventListener('drop', this.onDrop, true)`
In beforeUnmount(): change `window.removeEventListener('drop', this.onDrop)` to `window.removeEventListener('drop', this.onDrop, true)`
The third argument `true` registers the listener in capture phase, meaning it runs before any bubble-phase handlers on child elements (including `@drop.prevent` on folder rows in StorageBrowser.vue). The removeEventListener MUST also pass `true` — EventTarget tracks bubble-phase and capture-phase registrations separately, so without `true` the cleanup call would fail silently and the listener would leak.
Do not change dragenter, dragleave, or dragover listeners — those remain in bubble phase and work correctly.
</action>
<verify>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && grep "keydown.escape" src/components/documents/SearchBar.vue && grep -n "addEventListener.*drop\|removeEventListener.*drop" src/components/layout/OsDragOverlay.vue</automated>
</verify>
<done>
- `grep "keydown.escape" src/components/documents/SearchBar.vue` shows `.prevent.stop` in the modifier chain
- `grep "addEventListener.*drop" src/components/layout/OsDragOverlay.vue` shows `true` as third argument
- `grep "removeEventListener.*drop" src/components/layout/OsDragOverlay.vue` shows `true` as third argument
</done>
</task>
<task type="auto" tdd="true">
<name>Task 4: Regression tests for all 6 gaps</name>
<files>
frontend/src/__tests__/keyboard.test.js
frontend/src/components/ui/__tests__/TreeItem.test.js
frontend/src/components/storage/__tests__/StorageBrowser.showSearch.test.js
</files>
<behavior>
TreeItem shimmer:
- When loading=true and expanded=true → rendered HTML contains elements with class "animate-pulse"
- When loading=true and expanded=true → text "Loading" does not appear in rendered HTML
StorageBrowser showSearch:
- mode='local', breadcrumb=[] → showSearch is true (root-level local, previously false)
- mode='local', breadcrumb=[{name:'Folder'}] → showSearch is true (non-root local)
- mode='cloud', breadcrumb=[] → showSearch is true (cloud root, previously always false)
- mode='shared' → showSearch is false
App.vue keyboard dispatch:
- matched.find(r => r.instances?.default)?.instances?.default on a mounted FileManagerView router returns an object with focusSearch defined
</behavior>
<action>
keyboard.test.js — append a new describe block at the end of the existing file (do not modify or move existing describes):
"Gap 4: getFileManagerInstance resolves to actual component, not RouterView proxy" — using the existing mountFileManager() helper and makeRouter(), after mounting FileManagerView and flushing promises, access `router.currentRoute.value.matched.find(r => r.instances?.default)?.instances?.default` and assert that it is not null and has a `focusSearch` property. This proves the resolution mechanism the refactored App.vue uses is correct. Import `useRouter` from vue-router at the top if not already imported — check existing imports first.
TreeItem.test.js — create at `frontend/src/components/ui/__tests__/TreeItem.test.js`:
- Import: `describe, it, expect, vi` from vitest; `mount, flushPromises` from @vue/test-utils; TreeItem from the component
- Mock AppIcon: `vi.mock('../AppIcon.vue', () => ({ default: { template: '<span/>' } }))`
- Helper: `mountExpanded(overrides = {})` — mounts TreeItem with `label="Test"`, `loadChildren: vi.fn().mockResolvedValue([])`, `expandable: true` and spreads overrides. After mount, call `w.vm.toggleExpand()` via `w.vm.$.setupState.toggleExpand()` (or trigger the expand button click) and flush promises to reach expanded state with children loaded.
- For the loading=true test: mount with a loadChildren that returns a promise that never resolves (`new Promise(() => {})`). Click the expand button to trigger load. Before flushing, assert the HTML. The loading ref will be true while the promise is pending.
- describe "Gap 1: sidebar shimmer rows": two tests as per behavior block above.
- describe "Gap 1 regression: non-loading branches unchanged": one test asserting that with loadChildren resolving to [] and expanded, the "Empty" text appears (sibling v-else-if branch guard).
StorageBrowser.showSearch.test.js — create at `frontend/src/components/storage/__tests__/StorageBrowser.showSearch.test.js`:
- Heavy stub setup: all child components that StorageBrowser imports must be stubbed (SearchBar, SortControls, DocumentCard, FolderRow, EmptyState, AppIcon, BreadcrumbBar, DropZone, etc.) — use `stubs: { SearchBar: true, SortControls: true, ... }` in global mount options or individual vi.mock calls.
- Mock all API calls used on mount (listDocuments, listFolders, etc.) via vi.mock('../../../api/client.js', ...).
- After mount with given props, access `w.vm.showSearch` directly as a computed.
- Props for mount: at minimum `mode`, `breadcrumb`, `documents: []`, `folders: []`, `topicColorFn: () => '#000'`, `loading: false`.
- Four it() blocks as per behavior block above.
- Wrap in `describe("Gap 2: showSearch visible at root for local and cloud modes", ...)`.
All new tests run as part of `npm run test -- --run` without additional flags.
</action>
<verify>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run test -- --reporter=verbose --run 2>&1 | grep -E "(PASS|FAIL|SKIP| ✓ | × | ✗ )" | tail -50</automated>
</verify>
<done>
- All new describe blocks appear in output with passing test indicators
- `npm run test -- --run` exits 0 with zero failures
- `grep -c "animate-pulse" src/components/ui/__tests__/TreeItem.test.js` returns at least 1
- `grep -c "showSearch" src/components/storage/__tests__/StorageBrowser.showSearch.test.js` returns at least 4
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| OS file drag → window drop (capture) | Files arrive via browser DataTransfer API — browser-enforced; no network boundary crossed; payload handled only client-side before going through existing authenticated upload flow |
| Admin route layout selection | Template branch selection is display-only — backend enforces admin access via get_current_admin dep on every admin endpoint; changing template rendering creates no auth regression |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-10gc-01 | Tampering | OsDragOverlay capture-phase drop handler | accept | Handler reads only e.dataTransfer.files (browser-controlled), resets overlay state, emits to parent — no direct server interaction; upload goes through existing authenticated API path unchanged |
| T-10gc-02 | Spoofing | router.currentRoute.value.matched.find().instances.default | accept | Read-only Vue internals lookup; no user-supplied data flows through this path; cannot be spoofed via URL manipulation — Vue populates instances after component mount, not from route params |
| T-10gc-03 | Elevation of Privilege | App.vue v-else-if admin branch | accept | Branch adds a missing layout guard (removes AppSidebar for admin); it does not grant or deny route access — navigation guard in router/index.js is unchanged and continues to enforce requiresAdmin check |
| T-10gc-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed — all fixes are template/script edits to existing files |
</threat_model>
<verification>
Manual UAT re-run checklist (all must pass before marking complete):
Gap 1 — Sidebar shimmer: Expand a cloud provider tree item in the sidebar while data loads. Animated shimmer rows appear; "Loading…" text is absent.
Gap 2 — Search at root: Navigate to / (file manager root, no folder entered). Search bar and sort controls are visible in the content area header.
Gap 3 — Admin sidebar: Navigate to /admin/users in a logged-in admin session. Only the AdminLayout and AdminSidebar render — AppSidebar (user nav) is absent.
Gap 4 — Keyboard shortcuts: With the file manager at root, press '/'. Search bar receives cursor focus. Press 'U'. File picker dialog opens. Press 'N'. New folder inline input appears.
Gap 5 — Escape behavior: Type text into the search bar. Press Escape. Field clears. Cursor remains in the field. Type again immediately — new characters appear (no re-click required).
Gap 6 — OS drag-drop: Drag a file from Finder/Explorer over the browser window. Overlay appears. Release the file. Upload begins (progress visible or toast appears).
Automated gate:
`cd frontend && npm run test -- --run` — exits 0, zero failures, no skipped tests regressed.
</verification>
<success_criteria>
- All 6 UAT gaps confirmed closed by manual re-run of the 6 scenarios above
- `npm run test -- --run` exits 0 with zero failures across the full suite (baseline: 211 passing)
- Five production files modified with surgical changes only — no refactors, no feature additions
- Three new test files committed with passing tests covering each gap
- `grep -c "Loading" frontend/src/components/ui/TreeItem.vue` returns 0
- `grep "showSearch" frontend/src/components/storage/StorageBrowser.vue` matches the `|| props.mode === 'cloud'` form
- `grep "routeViewRef" frontend/src/App.vue` returns empty
- `grep "requiresAdmin" frontend/src/App.vue` returns the v-else-if template branch line
- `grep "addEventListener.*drop.*true" frontend/src/components/layout/OsDragOverlay.vue` returns a match
- `grep "removeEventListener.*drop.*true" frontend/src/components/layout/OsDragOverlay.vue` returns a match
- `grep "prevent.stop" frontend/src/components/documents/SearchBar.vue` returns the escape handler line
</success_criteria>
<output>
Create `/Users/nik/Documents/Progamming/document_scanner/.planning/phases/10-ux-interaction/10-13-SUMMARY.md` when done.
</output>
@@ -0,0 +1,42 @@
---
phase: 11-visual-design-responsive-layout-cleanup
plan: 1
type: execute
wave: 0
depends_on: [10-complete]
requirements: [PERF-02]
files_modified:
- frontend/vite.config.js
- .planning/phases/11-visual-design-responsive-layout-cleanup/11-RESEARCH.md
- .planning/perf/phase11-baseline.html
- .planning/perf/phase11-baseline-summary.md
autonomous: true
---
# Plan 11-01 — Bundle Baseline & UI Audit
## Objective
Capture the Phase 11 pre-optimization bundle baseline before any lazy-loading or visual cleanup begins, then record a targeted audit of the visual/responsive issues Phase 11 will address.
## Tasks
1. Wire existing `rollup-plugin-visualizer` into `frontend/vite.config.js` behind an opt-in environment flag such as `ANALYZE=true`. The dependency already exists in `frontend/package.json`.
2. Run a production build with analysis enabled and write the baseline report to `.planning/perf/phase11-baseline.html`.
3. Add `.planning/perf/phase11-baseline-summary.md` with bundle size, largest chunks, route/component observations, and the exact command used.
4. Update `11-RESEARCH.md` if execution discovers facts that differ from the refresh research.
5. Audit the frontend for Phase 11 targets:
- synchronous non-critical route imports
- responsive sidebar/admin sidebar gaps
- tables or grids that overflow below `sm`/`md`
- modal overflow below 640px
- inconsistent form, hover, focus, active, spacing, and typography patterns
- unreferenced files and imports
## Acceptance Criteria
- Baseline bundle report exists before any Phase 11 optimization commits.
- `vite.config.js` does not generate analyzer output unless explicitly requested.
- `11-RESEARCH.md` remains accurate after the baseline build.
- Audit notes are concrete enough that plans 11-02 through 11-06 can execute without rediscovering scope.
- `cd frontend && npm run build` succeeds with and without analysis enabled.
@@ -0,0 +1,43 @@
---
phase: 11-visual-design-responsive-layout-cleanup
plan: 2
type: execute
wave: 1
depends_on: [11-01]
requirements: [PERF-03]
files_modified:
- frontend/src/router/index.js
- frontend/src/router/__tests__/router.guard.test.js
autonomous: true
---
# Plan 11-02 — Lazy-Load Non-Critical Routes
## Objective
Satisfy PERF-03 by lazy-loading every route component that is not needed for the initial render, while preserving auth/admin guard behavior.
## Tasks
1. Keep `FileManagerView` synchronous for `/` as the critical first authenticated surface unless the baseline report shows a strong reason to split it. Document this in `router/index.js` near the import.
2. Replace synchronous imports in `frontend/src/router/index.js` for non-initial routes:
- `TopicsView`
- `DocumentView`
- `SettingsView`
- `CloudStorageView`
- `CloudFolderView`
3. Keep `/folders/:folderId` on the same `FileManagerView` component for behavior parity with `/`; it is already in the initial chunk because `/` uses the same component.
4. Preserve existing lazy auth/admin/shared route imports.
5. Extend router tests so guard behavior is verified with lazy route components:
- non-admin cannot enter `/admin/*`
- admin redirects away from non-admin routes
- refresh-before-guard still runs when access token is absent
- `/topics`, `/document/:id`, `/settings`, `/cloud`, and `/cloud/:provider/:folderId` still resolve
6. Build once and confirm route chunks are emitted.
## Acceptance Criteria
- `rg "import .*View" frontend/src/router/index.js` only returns `FileManagerView` unless a new initial-render route is explicitly justified.
- Admin child routes remain lazy-loaded.
- Router guard tests pass.
- `cd frontend && npm run build` succeeds and emits split route chunks.
@@ -0,0 +1,52 @@
---
phase: 11-visual-design-responsive-layout-cleanup
plan: 3
type: execute
wave: 2
depends_on: [11-02]
requirements: [RESP-01, RESP-02, RESP-03, RESP-05]
files_modified:
- frontend/src/App.vue
- frontend/src/layouts/AdminLayout.vue
- frontend/src/components/layout/AppSidebar.vue
- frontend/src/components/admin/AdminSidebar.vue
- frontend/src/components/storage/StorageBrowser.vue
- frontend/src/__tests__/keyboard.test.js
- frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js
autonomous: true
---
# Plan 11-03 — Responsive Shells & Storage Rows
## Objective
Make both user and admin layouts usable below `lg`, and make storage rows fit smaller viewports without losing core actions.
## Tasks
1. Use layout-local drawer refs, not a new Pinia store:
- `App.vue` owns user drawer state.
- `AdminLayout.vue` owns admin drawer state.
- Watch route changes in each layout root and close the drawer after navigation.
- Do not put drawer state in `AppSidebar.vue` or `AdminSidebar.vue`.
2. Add a mobile-only header with a hamburger button for the user layout.
3. Hide `AppSidebar` below `lg`; open it in a slide-in overlay drawer with backdrop tap, route-change close, and `translate-x-0` / `-translate-x-full` transition.
4. Apply the same responsive shell behavior to `AdminLayout` and `AdminSidebar`.
5. Update `StorageBrowser` row/grid classes so:
- Size column hides below `md`
- Modified column hides below `sm`
- icon, name, and actions remain visible
- grid templates do not reserve hidden column widths on mobile
6. Ensure inline icon action buttons have at least `36px` touch targets below `md`.
7. Add or update tests for drawer open/close, route-change close, admin drawer behavior, responsive column classes, and touch target classes.
8. Verify with browser screenshots or Playwright at 375px, 768px, 1024px, and desktop width.
## Acceptance Criteria
- User sidebar is hidden below 1024px and accessible through a hamburger drawer.
- Admin sidebar has matching mobile behavior.
- Drawer closes on backdrop tap and navigation tap.
- Storage rows satisfy RESP-02 without horizontal overflow at 375px.
- Icon actions satisfy RESP-03.
- Drawer state is owned only by `App.vue` and `AdminLayout.vue`.
- Frontend tests and build pass.
@@ -0,0 +1,40 @@
---
phase: 11-visual-design-responsive-layout-cleanup
plan: 4
type: execute
wave: 3
depends_on: [11-03]
requirements: [VISUAL-02, RESP-04]
files_modified:
- frontend/tailwind.config.js
- frontend/src/components/**/*.vue
- frontend/src/views/**/*.vue
autonomous: true
---
# Plan 11-04 — Forms Baseline & Mobile-Safe Modals
## Objective
Normalize form controls through `@tailwindcss/forms` and make every modal scroll safely on mobile viewports.
## Tasks
1. Confirm `@tailwindcss/forms` remains installed and active in `tailwind.config.js`; it is already wired today, so this should be a verification step unless execution finds drift.
2. Audit inputs, selects, textareas, checkboxes, and radio buttons for conflicting per-component browser-reset styles.
3. Normalize form classes to the smallest consistent Tailwind pattern already used by the app.
4. Update modal shells so content below 640px is scrollable and never exceeds viewport height:
- `ShareModal.vue`: centered panel gets mobile `max-h` and `overflow-y-auto`.
- `CloudCredentialModal.vue`: tall WebDAV/Nextcloud form gets mobile `max-h` and `overflow-y-auto`.
- `FolderDeleteModal.vue`: adopt the same mobile-safe panel pattern.
- `DocumentPreviewModal.vue`: preserve full-screen preview but verify header/content sizing at narrow widths.
- any auth/account confirmation modal-like surfaces found in the audit
5. Add focused tests or DOM assertions for mobile-safe modal classes and form baseline coverage.
## Acceptance Criteria
- Forms plugin is active and relied on consistently.
- No modal content overflows a 375x667 viewport.
- No modal text or action row is clipped below 640px.
- The desktop modal appearance remains behaviorally unchanged.
- `npm run test -- --run` and `npm run build` pass.
@@ -0,0 +1,46 @@
---
phase: 11-visual-design-responsive-layout-cleanup
plan: 5
type: execute
wave: 4
depends_on: [11-04]
requirements: [VISUAL-01, VISUAL-03, VISUAL-04]
files_modified:
- frontend/src/components/**/*.vue
- frontend/src/views/**/*.vue
autonomous: true
---
# Plan 11-05 — Visual Consistency Pass
## Objective
Make spacing, typography, hover, focus-visible, and active states consistent across the frontend without introducing a component-library rewrite.
## Tasks
1. Preserve legitimate data-driven inline styles:
- topic color swatches
- quota/progress widths
- Teleport dropdown coordinates
- tree indentation
2. Remove arbitrary spacing and decorative inline styles unless they are data-driven layout values with no Tailwind equivalent.
3. Normalize typography to this app scale:
- page title: `text-2xl font-semibold`
- section title: `text-lg font-semibold`
- panel/table heading: `text-sm font-semibold`
- body: `text-sm`
- caption/metadata: `text-xs`
4. Replace generic `focus:ring-*` only patterns on interactive elements with this keyboard focus convention: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1`.
5. Ensure buttons, links, card rows, table rows, menu items, and icon actions have coherent hover and active states.
6. Avoid broad palette changes; preserve the current DocuVault identity while removing one-off visual drift.
7. Add focused tests or static checks for the agreed invariants where practical.
## Acceptance Criteria
- No unnecessary `px-[...]`, margin `style=`, or one-off typography overrides remain.
- Interactive elements have hover plus keyboard-visible focus states.
- Typography reads as one app scale, not per-component choices.
- Data-driven inline styles remain where they carry runtime values.
- Visual changes are behavior-preserving.
- Frontend tests and build pass.
@@ -0,0 +1,45 @@
---
phase: 11-visual-design-responsive-layout-cleanup
plan: 6
type: execute
wave: 5
depends_on: [11-05]
requirements: [CODE-07, PERF-02]
files_modified:
- frontend/src/**/*
- .planning/perf/phase11-final.html
- .planning/perf/phase11-final-summary.md
- .planning/phases/11-visual-design-responsive-layout-cleanup/11-VERIFICATION.md
autonomous: true
---
# Plan 11-06 — Dead-Code Cleanup & Final Measurement
## Objective
Delete unreferenced frontend code and capture the final bundle report after all Phase 11 optimizations are complete.
## Tasks
1. Run dead-code searches for unreferenced components, stores, helpers, imports, and old route views.
2. Explicitly classify these known suspects before deleting or retaining:
- `frontend/src/views/AccountView.vue` (`/account` currently redirects to `/settings`)
- `frontend/src/components/admin/__tests__/AdminAiConfigTab.test.js`
- `frontend/src/components/admin/__tests__/AdminQuotasTab.test.js`
- `frontend/src/components/admin/__tests__/AdminUsersTab.test.js`
3. Confirm `HomeView.vue`, `FolderView.vue`, and `AdminView.vue` remain absent.
4. Delete files with no active route and no active import in the same commit as their references are removed.
5. Remove unused imports and stale tests that target deleted files; keep behavior tests that still protect live surfaces.
6. Run the final analyzer build and write `.planning/perf/phase11-final.html`.
7. Add `.planning/perf/phase11-final-summary.md` comparing baseline vs final bundle size, chunk count, and largest chunks.
8. Produce `11-VERIFICATION.md` mapping every Phase 11 requirement to code/test/build evidence.
## Acceptance Criteria
- CODE-07 is satisfied: no dead files, unused route components, unused stores, or unused imports remain.
- Known suspects are each documented as deleted or intentionally retained.
- Final bundle report and summary are committed under `.planning/perf/`.
- Baseline and final reports are both present.
- `cd frontend && npm run test -- --run` passes.
- `cd frontend && npm run build` passes.
- Phase 11 verification maps VISUAL-01..04, RESP-01..05, CODE-07, PERF-02, and PERF-03 to concrete evidence.
@@ -39,7 +39,7 @@ This phase is **purely frontend** — no backend changes, no new features.
### Performance (PERF-02, PERF-03)
- **D-09:** Bundle baseline captured in Plan 11-01 (before any changes). Final measurement in Plan 11-06 (after all changes). Both reports committed to `.planning/perf/`.
- **D-10:** Login/Register route (`/login`, `/register`) may stay synchronous if needed for first-paint — document the rationale in the router. All other authenticated routes (Topics, Document detail, Settings, Cloud, admin subtree) must be lazy.
- **D-10:** `FileManagerView` may stay synchronous for `/` as the critical first authenticated surface — document the rationale in the router. Auth routes and admin routes are already lazy-loaded. All other authenticated routes (Topics, Document detail, Settings, Cloud) must be lazy.
### Claude's Discretion
@@ -75,7 +75,7 @@ This phase is **purely frontend** — no backend changes, no new features.
- `frontend/src/components/admin/AdminSidebar.vue` — admin sidebar; same responsive behavior
- `frontend/src/components/storage/StorageBrowser.vue` — responsive column hiding: Size below `md`, Modified below `sm`; touch targets (RESP-03)
- `frontend/src/router/index.js` — add `() => import(...)` lazy loading for non-initial routes (PERF-03)
- `frontend/tailwind.config.js`add `@tailwindcss/forms` plugin (VISUAL-02)
- `frontend/tailwind.config.js`verify `@tailwindcss/forms` plugin remains active (VISUAL-02)
- `frontend/vite.config.js` — add `rollup-plugin-visualizer` behind `ANALYZE=true` env flag (PERF-02)
### Phase 10 Patterns (carry forward)
@@ -102,7 +102,7 @@ This phase is **purely frontend** — no backend changes, no new features.
- `App.vue` — user layout root; hamburger button + sidebar drawer mount here. Phase 10 already added `<Teleport>`-based toast container and drag overlay here.
- `AdminLayout.vue` — admin layout root; separate hamburger button needed for admin nav drawer (RESP-05); same structural pattern as `App.vue`
- `frontend/src/router/index.js` — lazy-load additions happen here; preserve all existing `meta: { requiresAuth, requiresAdmin }` guards
- `frontend/tailwind.config.js` add `@tailwindcss/forms` plugin; do not break existing utility classes
- `frontend/tailwind.config.js``@tailwindcss/forms` is already active; do not break existing utility classes
### PERF-02 Bundle Workflow
- Plan 11-01 adds `rollup-plugin-visualizer` behind `ANALYZE=true` and captures baseline in `.planning/perf/phase11-baseline.html`
@@ -118,7 +118,7 @@ This phase is **purely frontend** — no backend changes, no new features.
- Backdrop for drawer: semi-transparent overlay, tap to close. Use `<Teleport to="body">` consistent with existing overlay pattern.
- Touch target minimum: `min-w-[36px] min-h-[36px]` or equivalent padding on icon action buttons below `md`.
- Modal scroll: `max-h-[90vh] overflow-y-auto` pattern on modal content container for viewports below 640px.
- `@tailwindcss/forms` strategy: install plugin, audit all form elements for conflicting per-component reset styles, normalize to smallest consistent Tailwind class set already used in the app.
- `@tailwindcss/forms` strategy: verify plugin remains active, audit all form elements for conflicting per-component reset styles, normalize to smallest consistent Tailwind class set already used in the app.
</specifics>
@@ -0,0 +1,94 @@
# Phase 11 Research Refresh — Visual Design, Responsive Layout & Cleanup
**Researched:** 2026-06-16
**Inputs:** Phase 11 context, roadmap, requirements, pitfall notes, live frontend code.
## Executive Summary
The six-plan Phase 11 sequence is structurally sound: measure first, lazy-load routes, fix responsive shells, normalize forms/modals, do the visual consistency pass, then delete dead code and measure again. The second review found several places where the plans needed sharper implementation constraints, especially because `11-CONTEXT.md` contains locked decisions that were not fully reflected in the first review.
## Current Code Findings
### Performance
- `frontend/package.json` already includes `rollup-plugin-visualizer@^7.0.1`.
- `frontend/vite.config.js` currently has only `vue()` in `plugins`; analyzer wiring still needs to be added behind an opt-in flag.
- `frontend/src/router/index.js` still synchronously imports:
- `FileManagerView`
- `TopicsView`
- `DocumentView`
- `SettingsView`
- `CloudFolderView`
- `CloudStorageView`
- Auth routes, admin layout, admin child views, and `SharedView` are already lazy-loaded.
- Recommendation: keep `FileManagerView` synchronous only for `/` as the critical first authenticated surface. Lazy-load `TopicsView`, `DocumentView`, `SettingsView`, `CloudStorageView`, and `CloudFolderView`. Reuse the synchronous `FileManagerView` for `/folders/:folderId` unless the bundle baseline proves it should be split later.
### Responsive Shells
- `App.vue` renders a desktop-only `flex h-screen overflow-hidden` shell with permanent `AppSidebar`.
- `AdminLayout.vue` mirrors that desktop-only shell with permanent `AdminSidebar`.
- `AppSidebar.vue` and `AdminSidebar.vue` are presentational enough to stay drawer-agnostic.
- Recommendation: use layout-local refs in `App.vue` and `AdminLayout.vue`, not a new Pinia store. Nav link close behavior can be handled by watching route changes in each layout root; no child component needs global drawer state.
### Storage Rows
- `StorageBrowser.vue` already hides Size below `md` and Modified below `sm` via `hidden md:block` and `hidden sm:block`.
- The grid template remains fixed at `grid-cols-[2rem_1fr_6rem_8rem_6rem]`, so responsive column sizing still needs explicit adjustment to avoid empty tracks and mobile overflow.
- Existing row action buttons use `p-1.5`, which may not guarantee 36x36 touch targets below `md`.
### Forms
- `@tailwindcss/forms` is already installed and active in `frontend/tailwind.config.js`.
- Many controls still carry repeated full border/focus class stacks. Plan 11-04 should normalize those without introducing a new component library.
### Modals
Mobile overflow risk is concrete:
- `ShareModal.vue`: fixed overlay, centered panel, `rounded-2xl`, no `max-h` or `overflow-y-auto`.
- `CloudCredentialModal.vue`: fixed overlay, centered panel, `max-w-md p-6`, no `max-h` or `overflow-y-auto`; form can become tall with advanced Nextcloud fields.
- `FolderDeleteModal.vue`: smaller but should still receive the shared mobile-safe panel pattern.
- `DocumentPreviewModal.vue`: full-screen preview is structurally different; it should preserve full-screen behavior and ensure header/content do not overflow on narrow screens.
Recommended modal shell pattern: overlay uses `p-4 sm:p-6`; panel uses `max-h-[calc(100vh-2rem)] overflow-y-auto` below `sm`, with desktop styling preserved.
### Visual Consistency
- Current frontend heavily uses `focus:ring-2` but rarely `focus-visible:`.
- `rounded-xl` and `rounded-2xl` appear on several ordinary panels/modals even though AGENTS.md prefers cards at 8px radius unless the design system requires otherwise.
- There are data-driven inline styles that should remain:
- topic color swatches
- quota/progress bar widths
- Teleport dropdown coordinates
- tree indentation
- There are skeleton width styles such as `:style="{ width: (50 + n * 15) + 'px' }"` that are decorative and can be converted to static Tailwind widths during the visual pass.
Recommended convention:
- Page title: `text-2xl font-semibold`
- Section title: `text-lg font-semibold`
- Panel/table heading: `text-sm font-semibold`
- Body: `text-sm`
- Caption/metadata: `text-xs`
- Focus ring: `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1`
### Dead Code Signals
- `HomeView.vue` and `FolderView.vue` remain absent, satisfying AGENTS.md.
- Legacy admin-tab test names still exist under `frontend/src/components/admin/__tests__/`:
- `AdminAiConfigTab.test.js`
- `AdminQuotasTab.test.js`
- `AdminUsersTab.test.js`
- These tests may still cover current components via renamed imports or may be stale; Plan 11-06 should explicitly classify them.
- `AccountView.vue` exists but `/account` redirects to `/settings`. It may be dead unless imported outside the router. Plan 11-06 should confirm before deleting.
## Review Verdict
The plan set remains valid after review, but it should be refined with:
- a documented layout-local drawer-state decision
- exact route lazy-loading scope
- exact modal overflow targets
- a locked focus-ring and typography convention
- explicit dead-code suspects for Plan 11-06

Some files were not shown because too many files have changed in this diff Show More