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