Files
kite/.planning/milestones/v0.2-phases/10-ux-interaction/10-09-PLAN.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to
.planning/milestones/v0.2-phases/ to keep .planning/phases/ clean
for the next milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:34:52 +02:00

18 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
10-ux-interaction 09 execute 2
10-04
10-06
10-05
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
true
UX-05
UX-06
UX-07
UX-08
truths artifacts key_links
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
path provides
frontend/src/App.vue Global keydown handler + routeViewRef
path provides
frontend/src/views/FileManagerView.vue defineExpose of focusSearch/triggerUpload/startNewFolder/clearSearch
path provides
frontend/src/components/storage/StorageBrowser.vue Updated defineExpose with triggerUpload + focusSearch + clearSearch
path provides
frontend/src/components/upload/DropZone.vue defineExpose of triggerInput
path provides
frontend/src/components/documents/SearchBar.vue defineExpose of focus()
from to via pattern
App.vue keydown handler routeViewRef.value methods optional chaining (?.) routeViewRef.value?.
from to via pattern
StorageBrowser.vue DropZone.vue dropZoneRef.value?.triggerInput() dropZoneRef.value
from to via pattern
StorageBrowser.vue SearchBar.vue searchBarRef.value?.focus() searchBarRef.value
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

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@CLAUDE.md @.planning/phases/10-ux-interaction/10-CONTEXT.md @.planning/phases/10-ux-interaction/10-RESEARCH.md @.planning/phases/10-ux-interaction/10-PATTERNS.md @frontend/src/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 App.vue keydown handler signature (Composition API, from RESEARCH.md §Code Examples):
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.

Task 1: Promote keyboard.test.js stubs to real tests frontend/src/__tests__/keyboard.test.js - frontend/src/__tests__/keyboard.test.js (Wave 0 stub) - frontend/src/App.vue (current state) - frontend/src/views/FileManagerView.vue (current state) 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.
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.
cd frontend && npm run test -- --run keyboard Expected: 9 tests RED. - 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 9 RED keyboard shortcut tests in place. Task 2: Plumb the ref chain - DropZone + SearchBar + StorageBrowser + FileManagerView frontend/src/components/upload/DropZone.vue, frontend/src/components/documents/SearchBar.vue, frontend/src/components/storage/StorageBrowser.vue, frontend/src/views/FileManagerView.vue - frontend/src/components/upload/DropZone.vue (verify triggerInput already exists; no defineExpose currently) - frontend/src/components/documents/SearchBar.vue (current state - check for 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) 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.
cd frontend && npm run test -- --run FileManagerView; cd frontend && npm run test -- --run StorageBrowser Expected: existing FileManagerView + StorageBrowser tests still pass (no regression). The keyboard.test.js still fails (because App.vue handler not added yet). - `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 Ref chain fully plumbed from DropZone/SearchBar up to FileManagerView. Task 3: Add global keydown handler to App.vue + routeViewRef frontend/src/App.vue - 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) 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.
cd frontend && npm run test -- --run keyboard Expected: 9 tests GREEN. - `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 App.vue global keydown handler live; 9 keyboard tests GREEN; full suite still green. - `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)

<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>

Create `.planning/phases/10-ux-interaction/10-09-SUMMARY.md` when done.