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:
co-authored by
Claude Sonnet 4.6
parent
e008bf7dae
commit
123ae5b29b
@@ -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>
|
||||||
+42
@@ -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.
|
||||||
+43
@@ -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.
|
||||||
+52
@@ -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.
|
||||||
+40
@@ -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.
|
||||||
+46
@@ -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.
|
||||||
+45
@@ -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.
|
||||||
+4
-4
@@ -39,7 +39,7 @@ This phase is **purely frontend** — no backend changes, no new features.
|
|||||||
### Performance (PERF-02, PERF-03)
|
### 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-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
|
### 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/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/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/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)
|
- `frontend/vite.config.js` — add `rollup-plugin-visualizer` behind `ANALYZE=true` env flag (PERF-02)
|
||||||
|
|
||||||
### Phase 10 Patterns (carry forward)
|
### 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.
|
- `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`
|
- `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/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
|
### PERF-02 Bundle Workflow
|
||||||
- Plan 11-01 adds `rollup-plugin-visualizer` behind `ANALYZE=true` and captures baseline in `.planning/perf/phase11-baseline.html`
|
- 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.
|
- 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`.
|
- 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.
|
- 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>
|
</specifics>
|
||||||
|
|
||||||
+94
@@ -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
Reference in New Issue
Block a user