docs(10): create phase plan — 12 plans, 6 waves, UX-01..14 + CODE-05

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-15 06:55:11 +02:00
co-authored by Claude Sonnet 4.6
parent c84dcb77c1
commit d3d3f711eb
16 changed files with 4611 additions and 2 deletions
@@ -0,0 +1,342 @@
---
phase: 10-ux-interaction
plan: 09
type: execute
wave: 2
depends_on: [10-04, 10-06, 10-05]
files_modified:
- frontend/src/App.vue
- frontend/src/views/FileManagerView.vue
- frontend/src/components/storage/StorageBrowser.vue
- frontend/src/components/upload/DropZone.vue
- frontend/src/components/documents/SearchBar.vue
- frontend/src/__tests__/keyboard.test.js
autonomous: true
requirements: [UX-05, UX-06, UX-07, UX-08]
must_haves:
truths:
- "Pressing `/` (when no input focused) calls focus() on the SearchBar input via App.vue routeViewRef chain"
- "Pressing Escape (when no input focused) clears the active search query"
- "Pressing U (when no input focused) triggers DropZone.triggerInput via the ref chain"
- "Pressing N (when no input focused) starts the new-folder inline input in StorageBrowser"
- "The global keydown handler guards against active INPUT/TEXTAREA/SELECT/contenteditable elements and returns early"
- "DropZone.triggerInput is exposed via defineExpose"
- "SearchBar.focus is exposed via defineExpose"
- "StorageBrowser exposes triggerUpload, focusSearch, clearSearch alongside its existing startNewFolder"
- "FileManagerView exposes focusSearch, triggerUpload, startNewFolder, clearSearch via defineExpose"
artifacts:
- path: "frontend/src/App.vue"
provides: "Global keydown handler + routeViewRef"
- path: "frontend/src/views/FileManagerView.vue"
provides: "defineExpose of focusSearch/triggerUpload/startNewFolder/clearSearch"
- path: "frontend/src/components/storage/StorageBrowser.vue"
provides: "Updated defineExpose with triggerUpload + focusSearch + clearSearch"
- path: "frontend/src/components/upload/DropZone.vue"
provides: "defineExpose of triggerInput"
- path: "frontend/src/components/documents/SearchBar.vue"
provides: "defineExpose of focus()"
key_links:
- from: "App.vue keydown handler"
to: "routeViewRef.value methods"
via: "optional chaining (?.)"
pattern: "routeViewRef\\.value\\?\\."
- from: "StorageBrowser.vue"
to: "DropZone.vue"
via: "dropZoneRef.value?.triggerInput()"
pattern: "dropZoneRef\\.value"
- from: "StorageBrowser.vue"
to: "SearchBar.vue"
via: "searchBarRef.value?.focus()"
pattern: "searchBarRef\\.value"
---
<objective>
Implement the four global keyboard shortcuts (`/`, `Escape`, `U`, `N`) per D-14, D-15. The handler lives in `App.vue` (already `<script setup>`) and delegates to the active route component through a chain of `ref` + `defineExpose` calls:
App.vue routeViewRef -> FileManagerView (focusSearch/triggerUpload/startNewFolder/clearSearch) -> StorageBrowser (focusSearch/triggerUpload/clearSearch + existing startNewFolder) -> DropZone (triggerInput) / SearchBar (focus).
Use optional chaining at every hop so views that do not expose a method silently no-op (per D-15 and Open Question 2 in RESEARCH.md).
Output:
- DropZone exposes triggerInput
- SearchBar exposes focus()
- StorageBrowser exposes triggerUpload + focusSearch + clearSearch
- FileManagerView exposes all four route-level methods
- App.vue gains routeViewRef + onKeydown listener
- keyboard.test.js stubs promoted to real tests
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@CLAUDE.md
@.planning/phases/10-ux-interaction/10-CONTEXT.md
@.planning/phases/10-ux-interaction/10-RESEARCH.md
@.planning/phases/10-ux-interaction/10-PATTERNS.md
@frontend/src/App.vue
@frontend/src/views/FileManagerView.vue
@frontend/src/components/storage/StorageBrowser.vue
@frontend/src/components/upload/DropZone.vue
@frontend/src/components/documents/SearchBar.vue
@frontend/src/components/documents/DocumentPreviewModal.vue
<interfaces>
App.vue keydown handler signature (Composition API, from RESEARCH.md §Code Examples):
```js
import { ref, onMounted, onUnmounted } from 'vue'
const routeViewRef = ref(null)
function onKeydown(e) {
const tag = document.activeElement?.tagName
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
routeViewRef.value?.focusSearch?.()
}
if (e.key === 'Escape') {
routeViewRef.value?.clearSearch?.()
}
if (e.key === 'u' || e.key === 'U') {
routeViewRef.value?.triggerUpload?.()
}
if (e.key === 'n' || e.key === 'N') {
routeViewRef.value?.startNewFolder?.()
}
}
onMounted(() => document.addEventListener('keydown', onKeydown))
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
```
Template: change `<router-view />` to `<router-view ref="routeViewRef" />`.
Ref chain plumbing (PATTERNS.md):
- SearchBar.vue: add `const inputEl = ref(null)`; add `ref="inputEl"` to the search `<input>`; add `defineExpose({ focus() { inputEl.value?.focus() } })`.
- DropZone.vue: add `defineExpose({ triggerInput })` (the function already exists).
- StorageBrowser.vue: add `const dropZoneRef = ref(null)` and `const searchBarRef = ref(null)`; add `ref="dropZoneRef"` on `<DropZone>` and `ref="searchBarRef"` on `<SearchBar>`; extend `defineExpose` to `{ startNewFolder, triggerUpload, focusSearch, clearSearch }` where:
- `triggerUpload() { dropZoneRef.value?.triggerInput() }`
- `focusSearch() { searchBarRef.value?.focus() }`
- `clearSearch() { emit('search-change', '') }` (emit, since searchQuery is a prop)
- FileManagerView.vue: add `defineExpose({ focusSearch: () => browserRef.value?.focusSearch?.(), triggerUpload: () => browserRef.value?.triggerUpload?.(), startNewFolder: () => browserRef.value?.startNewFolder?.(), clearSearch: () => browserRef.value?.clearSearch?.() })`
DocumentPreviewModal already owns its own Escape handler (PITFALL 4) - leave it alone. The App.vue Escape branch only calls clearSearch which is a no-op when no FileManagerView is mounted.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Promote keyboard.test.js stubs to real tests</name>
<files>frontend/src/__tests__/keyboard.test.js</files>
<read_first>
- frontend/src/__tests__/keyboard.test.js (Wave 0 stub)
- frontend/src/App.vue (current state)
- frontend/src/views/FileManagerView.vue (current state)
</read_first>
<behavior>
Replace .todo entries with real assertions. Strategy: test the App.vue keydown handler in isolation by mounting App with a stubbed router-view that has a known shape (spy methods for focusSearch / clearSearch / triggerUpload / startNewFolder). Dispatch keyboard events on document and assert the spies were called or not called based on the guard.
UX-05 (3 tests):
1. `keydown "/" calls focusSearch on routeViewRef` - mount App with a stub router-view exposing a focusSearch spy as defineExpose; dispatch keydown "/", assert spy called once.
2. `keydown "/" does NOT call focusSearch when an INPUT is focused` - create an <input>, focus it, dispatch keydown "/", assert spy NOT called.
3. `keydown "/" calls preventDefault` - capture the event; assert defaultPrevented=true after dispatch when no input focused.
UX-06 (2 tests):
4. `keydown Escape calls clearSearch on routeViewRef` - dispatch Escape, assert spy called.
5. `keydown Escape does NOT call clearSearch when an INPUT is focused` - assert NOT called.
UX-07 (2 tests):
6. `keydown "u" calls triggerUpload on routeViewRef` - dispatch "u", assert spy called.
7. `keydown "U" (uppercase) also calls triggerUpload` - assert spy called when Shift+u dispatches "U".
UX-08 (2 tests):
8. `keydown "n" calls startNewFolder on routeViewRef` - assert spy called.
9. `keydown "n" does NOT call startNewFolder when CloudFolderView is the route` - mount App with a stub router-view that does NOT expose startNewFolder; assert no error thrown (optional chaining silently no-ops).
Use `vi.fn()` for spies. For "stub router-view": pass a custom component as the router-view stub via global.stubs or replace `<router-view>` with a test component. Use happy-dom default Vitest env (already in project).
Note: testing App.vue's keydown listener directly may require mounting App.vue with a mock router. A simpler approach: extract the onKeydown logic into a test helper inside App.vue (export it), OR test via component instance access. Use whichever approach works with @vue/test-utils. If mounting App.vue is too complex, write a focused unit test that imports a small reusable handler.
Alternative simpler approach (recommended): write tests that mount FileManagerView with stubbed StorageBrowser, then call `wrapper.vm.focusSearch()` / `wrapper.vm.triggerUpload()` directly to verify the defineExpose surface delegates to browserRef. This still validates the contract App.vue depends on.
Use either approach. The 9 tests above cover the App.vue handler contract.
</behavior>
<action>
Modify `frontend/src/__tests__/keyboard.test.js`. Replace each .todo with a real `it(...)` block per the behavior list above. Choose ONE testing strategy (full App.vue mount OR FileManagerView defineExpose direct invocation OR a hybrid). Aim for 9 real tests covering the four shortcuts plus their guards.
Tests fail initially because no ref chain or App.vue handler exists yet.
</action>
<verify>
<automated>cd frontend && npm run test -- --run keyboard</automated>
Expected: 9 tests RED.
</verify>
<acceptance_criteria>
- File contains 9 real `it(...)` tests across 4 describe blocks (UX-05 to UX-08)
- No `.todo` entries remain
- Tests use `vi.fn()` for spies and dispatch real KeyboardEvent objects
- All 9 tests are RED before Task 2
</acceptance_criteria>
<done>9 RED keyboard shortcut tests in place.</done>
</task>
<task type="auto">
<name>Task 2: Plumb the ref chain - DropZone + SearchBar + StorageBrowser + FileManagerView</name>
<files>
frontend/src/components/upload/DropZone.vue,
frontend/src/components/documents/SearchBar.vue,
frontend/src/components/storage/StorageBrowser.vue,
frontend/src/views/FileManagerView.vue
</files>
<read_first>
- frontend/src/components/upload/DropZone.vue (verify triggerInput already exists; no defineExpose currently)
- frontend/src/components/documents/SearchBar.vue (current state - check for <input> structure and existing exposes)
- frontend/src/components/storage/StorageBrowser.vue (current defineExpose at line 327 - has only startNewFolder)
- frontend/src/views/FileManagerView.vue (browserRef at line 61 - no defineExpose yet)
- .planning/phases/10-ux-interaction/10-PATTERNS.md (DropZone / SearchBar / StorageBrowser / FileManagerView sections)
</read_first>
<action>
Step A - DropZone.vue:
Add `defineExpose({ triggerInput })` after the `triggerInput` function definition (around line 49). The function already exists - this only exposes it.
Step B - SearchBar.vue:
Modify the component so the search `<input>` element has `ref="inputEl"`. In the script (add `import { ref } from 'vue'` if not already imported), declare `const inputEl = ref(null)`. Add `defineExpose({ focus() { inputEl.value?.focus() } })`.
Step C - StorageBrowser.vue:
1. Declare two new refs: `const dropZoneRef = ref(null)` and `const searchBarRef = ref(null)` (near the existing `newFolderInputRef` declaration around line 306).
2. Add `ref="dropZoneRef"` to the `<DropZone>` element (around line 38).
3. Add `ref="searchBarRef"` to the `<SearchBar>` element (around line 12).
4. Replace the existing `defineExpose({ startNewFolder })` at line 327 with:
```js
defineExpose({
startNewFolder,
triggerUpload: () => dropZoneRef.value?.triggerInput(),
focusSearch: () => searchBarRef.value?.focus(),
clearSearch: () => emit('search-change', ''),
})
```
Step D - FileManagerView.vue:
Add a `defineExpose(...)` block after the existing function definitions:
```js
defineExpose({
focusSearch: () => browserRef.value?.focusSearch?.(),
triggerUpload: () => browserRef.value?.triggerUpload?.(),
startNewFolder: () => browserRef.value?.startNewFolder?.(),
clearSearch: () => browserRef.value?.clearSearch?.(),
})
```
Keep all existing logic untouched.
</action>
<verify>
<automated>cd frontend && npm run test -- --run FileManagerView; cd frontend && npm run test -- --run StorageBrowser</automated>
Expected: existing FileManagerView + StorageBrowser tests still pass (no regression). The keyboard.test.js still fails (because App.vue handler not added yet).
</verify>
<acceptance_criteria>
- `grep -E "defineExpose\\(\\{ triggerInput \\}\\)" frontend/src/components/upload/DropZone.vue` returns 1
- `grep -E "defineExpose\\(\\{ focus" frontend/src/components/documents/SearchBar.vue` returns 1
- `grep -E "const inputEl = ref" frontend/src/components/documents/SearchBar.vue` returns 1
- `grep -E "ref=\"inputEl\"" frontend/src/components/documents/SearchBar.vue` returns 1
- `grep -E "const dropZoneRef\\s*=\\s*ref" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "const searchBarRef\\s*=\\s*ref" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "triggerUpload:\\s*\\(\\)\\s*=>\\s*dropZoneRef\\.value" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "focusSearch:\\s*\\(\\)\\s*=>\\s*searchBarRef\\.value" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "clearSearch:\\s*\\(\\)\\s*=>\\s*emit\\('search-change',\\s*''\\)" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "defineExpose" frontend/src/views/FileManagerView.vue` returns 1
- `grep -E "browserRef\\.value\\?\\.focusSearch" frontend/src/views/FileManagerView.vue` returns 1
- `grep -E "browserRef\\.value\\?\\.triggerUpload" frontend/src/views/FileManagerView.vue` returns 1
- `grep -E "browserRef\\.value\\?\\.startNewFolder" frontend/src/views/FileManagerView.vue` returns 1
- `grep -E "browserRef\\.value\\?\\.clearSearch" frontend/src/views/FileManagerView.vue` returns 1
- Existing FileManagerView + StorageBrowser test suites still pass
</acceptance_criteria>
<done>Ref chain fully plumbed from DropZone/SearchBar up to FileManagerView.</done>
</task>
<task type="auto">
<name>Task 3: Add global keydown handler to App.vue + routeViewRef</name>
<files>frontend/src/App.vue</files>
<read_first>
- frontend/src/App.vue (current state - uses <script setup>)
- frontend/src/__tests__/keyboard.test.js (failing tests)
- frontend/src/components/documents/DocumentPreviewModal.vue (Pitfall 4 reference - has its own Escape handler)
- .planning/phases/10-ux-interaction/10-PATTERNS.md (App.vue keydown handler section)
</read_first>
<action>
Edit `frontend/src/App.vue`:
Template change: replace `<router-view />` with `<router-view ref="routeViewRef" />`.
Script changes (inside the existing `<script setup>`):
1. Extend the import line `import { onMounted } from 'vue'` to also import `ref` and `onUnmounted`: `import { ref, onMounted, onUnmounted } from 'vue'`.
2. Add `const routeViewRef = ref(null)`.
3. Add the keydown handler:
```js
function onKeydown(e) {
const tag = document.activeElement?.tagName
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
e.preventDefault()
routeViewRef.value?.focusSearch?.()
}
if (e.key === 'Escape') {
routeViewRef.value?.clearSearch?.()
}
if (e.key === 'u' || e.key === 'U') {
routeViewRef.value?.triggerUpload?.()
}
if (e.key === 'n' || e.key === 'N') {
routeViewRef.value?.startNewFolder?.()
}
}
onMounted(() => document.addEventListener('keydown', onKeydown))
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
```
Keep the existing `topicsStore.fetchTopics()` call inside `onMounted` intact - either chain it into the same onMounted callback or use two separate onMounted calls.
No comments. Do not touch any other part of App.vue.
</action>
<verify>
<automated>cd frontend && npm run test -- --run keyboard</automated>
Expected: 9 tests GREEN.
</verify>
<acceptance_criteria>
- `grep -E "ref=\"routeViewRef\"" frontend/src/App.vue` returns 1
- `grep -E "const routeViewRef = ref\\(null\\)" frontend/src/App.vue` returns 1
- `grep -E "function onKeydown" frontend/src/App.vue` returns 1
- `grep -E "document\\.activeElement\\?\\.tagName" frontend/src/App.vue` returns 1
- `grep -E "isContentEditable" frontend/src/App.vue` returns 1
- `grep -E "e\\.preventDefault\\(\\)" frontend/src/App.vue` returns 1 (inside the / branch)
- `grep -E "addEventListener\\('keydown'" frontend/src/App.vue` returns 1
- `grep -E "removeEventListener\\('keydown'" frontend/src/App.vue` returns 1
- `cd frontend && npm run test -- --run keyboard` exits 0 with 9 passing tests
- `cd frontend && npm run test -- --run` (full) exits 0 with no regression
</acceptance_criteria>
<done>App.vue global keydown handler live; 9 keyboard tests GREEN; full suite still green.</done>
</task>
</tasks>
<verification>
- `cd frontend && npm run test -- --run keyboard` exits 0 with 9 tests passing
- `cd frontend && npm run test -- --run FileManagerView` exits 0
- `cd frontend && npm run test -- --run StorageBrowser` exits 0 (regression)
- `cd frontend && npm run test -- --run` (full suite) exits 0
- App.vue contains routeViewRef + onKeydown + document.addEventListener('keydown', ...) + cleanup
- All four shortcut branches present (`/`, Escape, U, N)
</verification>
<success_criteria>
With a fresh browser tab on `/`, pressing `/` jumps focus into the search bar. Typing in the search bar and pressing Escape clears it. Pressing `U` opens the OS file picker. Pressing `N` opens the inline new-folder input. None of these fire while typing into a focused input. On admin and settings routes, all four keys silently no-op because those views do not expose the corresponding methods.
</success_criteria>
<output>
Create `.planning/phases/10-ux-interaction/10-09-SUMMARY.md` when done.
</output>