---
phase: 10-ux-interaction
plan: 10
type: execute
wave: 3
depends_on: [10-04, 10-09, 10-05, 10-06]
files_modified:
  - frontend/src/components/layout/OsDragOverlay.vue
  - frontend/src/App.vue
  - frontend/src/views/FileManagerView.vue
  - frontend/src/components/layout/__tests__/OsDragOverlay.test.js
autonomous: true
requirements: [UX-09]
must_haves:
  truths:
    - "Dragging files from the OS over the browser window shows the full-screen drop overlay"
    - "An in-app element drag (no Files type in dataTransfer.types) does NOT show the overlay"
    - "Releasing files over the window calls FileManagerView.handleOsDrop(files) which uploads them via the existing flow"
    - "The overlay disappears on drop and on the depth counter reaching 0 via dragleave"
    - "Overlay z-index (z-[9998]) is below ToastContainer (z-[9999]) so toasts remain visible during drop"
  artifacts:
    - path: "frontend/src/components/layout/OsDragOverlay.vue"
      provides: "Full-screen OS file drag overlay with depth-counter pattern"
    - path: "frontend/src/App.vue"
      provides: "Updated to mount OsDragOverlay and wire @files-dropped to active route view"
  key_links:
    - from: "OsDragOverlay.vue"
      to: "window dragenter/dragleave/dragover/drop events"
      via: "mounted() addEventListener + beforeUnmount() removeEventListener"
      pattern: "window\\.addEventListener\\('drag"
    - from: "App.vue"
      to: "FileManagerView.handleOsDrop"
      via: "@files-dropped handler -> routeViewRef.handleOsDrop"
      pattern: "routeViewRef\\.value\\?\\.handleOsDrop"
---

<objective>
Implement UX-09 (OS file drag onto browser window -> full-screen overlay -> upload). The overlay uses the depth-counter pattern from D-16 (Pitfall 3) to prevent flicker as the cursor moves between child elements, and only activates when the dragged item has `dataTransfer.types.includes('Files')` (which is true only for OS-origin drags).

Output:
- New component `frontend/src/components/layout/OsDragOverlay.vue` (Options API; Teleport to body; window-level event listeners; emits `files-dropped`)
- App.vue mounts `<OsDragOverlay @files-dropped="..." />` and routes to the active view via `routeViewRef.value?.handleOsDrop?.(files)`
- FileManagerView exposes `handleOsDrop(files)` via `defineExpose` that calls the existing `onFilesSelected({files, autoClassify: true})`
- OsDragOverlay test 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/documents/DocumentPreviewModal.vue
@frontend/src/components/ui/AppIcon.vue
@frontend/src/components/upload/DropZone.vue

<interfaces>
OsDragOverlay.vue (Options API) - from PATTERNS.md §"OsDragOverlay.vue":

Data:
- `dragDepth: 0` (counter)
- `showOverlay: false`

Methods:
- `onDragEnter(e)` -> ignore unless `e.dataTransfer?.types.includes('Files')`; increment dragDepth; set showOverlay=true
- `onDragLeave()` -> dragDepth = max(0, dragDepth - 1); if dragDepth === 0 set showOverlay=false
- `onDragOver(e)` -> e.preventDefault() (required to allow drop event)
- `onDrop(e)` -> e.preventDefault(); reset dragDepth=0, showOverlay=false; emit `files-dropped` with `Array.from(e.dataTransfer.files)` (when files.length > 0)

mounted(): addEventListener for dragenter/dragleave/dragover/drop on `window`.
beforeUnmount(): remove all four listeners.

Template (Teleport to body):
```
<Teleport to="body">
  <Transition name="fade">
    <div
      v-if="showOverlay"
      class="fixed inset-0 z-[9998] bg-indigo-900/40 flex items-center justify-center pointer-events-none"
      data-test="os-drag-overlay"
    >
      <div class="bg-white rounded-2xl px-10 py-8 text-center shadow-xl pointer-events-none">
        <AppIcon name="upload" class="w-10 h-10 text-indigo-400 mx-auto mb-3" />
        <p class="text-base font-semibold text-gray-800">Drop files to upload</p>
      </div>
    </div>
  </Transition>
</Teleport>
```

Plus a `<style scoped>` block with `.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease } .fade-enter-from, .fade-leave-to { opacity: 0 }`.

App.vue wiring:
- Import `OsDragOverlay from './components/layout/OsDragOverlay.vue'`
- Add `<OsDragOverlay @files-dropped="onOsFilesDropped" />` to the template (after the ToastContainer mount from 10-04)
- Add handler:
  ```js
  function onOsFilesDropped(files) {
    routeViewRef.value?.handleOsDrop?.(files)
  }
  ```

FileManagerView.vue:
- Extend `defineExpose` to also include `handleOsDrop: (files) => onFilesSelected({ files, autoClassify: true })`. Reuse the existing onFilesSelected function so the upload path, quota handling, and toast wiring (10-06) work identically to a DropZone drop.
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Promote OsDragOverlay.test.js stubs to real tests</name>
  <files>frontend/src/components/layout/__tests__/OsDragOverlay.test.js</files>
  <read_first>
    - The Wave 0 stub file
    - .planning/phases/10-ux-interaction/10-PATTERNS.md §"OsDragOverlay.vue"
  </read_first>
  <behavior>
    Replace `.todo` entries with real tests:
    1. `overlay hidden by default (dragDepth=0)` - mount, assert wrapper does not contain the overlay element (or assert it's not visible). Use a Teleport stub.
    2. `dragenter with Files type shows overlay (dragDepth=1)` - mount, dispatch a window dragenter event with a synthetic dataTransfer carrying `types: ['Files']`; assert showOverlay=true via vm or DOM.
    3. `dragenter without Files type is ignored` - dispatch with `types: ['text/plain']`; assert showOverlay still false.
    4. `dragleave decrements depth; overlay hides when depth reaches 0` - enter once (depth=1), leave (depth=0), assert hidden.
    5. `nested dragenter+dragleave maintains overlay until depth=0` - dispatch enter twice (depth=2), leave once (depth=1, still showing), leave again (depth=0, hidden).
    6. `drop emits files-dropped with the file list` - dispatch a window drop with synthetic dataTransfer.files=[new File(['x'], 'a.txt')]; assert emitted('files-dropped')[0][0] is an array containing one File.
    7. `drop resets dragDepth to 0 and hides overlay` - enter 3x (depth=3), drop, assert showOverlay=false and subsequent leave doesn't go negative.
    8. `overlay element has class z-[9998]` - enter once, find the overlay element in body, assert classList contains 'z-[9998]'.

    Use `vi.fn()`, dispatch `new DragEvent(...)` or `new CustomEvent('dragenter', { ... })` and patch a fake `dataTransfer` on the event by `Object.defineProperty(event, 'dataTransfer', { value: { types: ['Files'], files: [...] } })`. happy-dom supports these.

    Tests fail until Task 2 creates OsDragOverlay.vue.
  </behavior>
  <action>
    Modify `frontend/src/components/layout/__tests__/OsDragOverlay.test.js`. Replace each `it.todo` with the real tests above. Import `OsDragOverlay` from `../OsDragOverlay.vue` (file doesn't exist yet -> tests fail on import). Use Vitest + @vue/test-utils + happy-dom defaults.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run OsDragOverlay</automated>
    Expected: 8 tests RED.
  </verify>
  <acceptance_criteria>
    - File has 8 real `it(...)` blocks, no `.todo`
    - Tests dispatch DragEvent / CustomEvent with synthetic dataTransfer
    - All 8 tests are RED
  </acceptance_criteria>
  <done>8 RED tests describing OsDragOverlay contract.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement OsDragOverlay.vue</name>
  <files>frontend/src/components/layout/OsDragOverlay.vue</files>
  <read_first>
    - frontend/src/components/layout/__tests__/OsDragOverlay.test.js (failing tests)
    - frontend/src/components/documents/DocumentPreviewModal.vue (window listener pattern reference)
    - .planning/phases/10-ux-interaction/10-PATTERNS.md §"OsDragOverlay.vue"
    - frontend/src/components/ui/AppIcon.vue
  </read_first>
  <behavior>
    Component name `OsDragOverlay`, Options API, emits `['files-dropped']`. Registers `AppIcon` as child component. Data + methods per the <interfaces> block. Template uses `<Teleport to="body">` + `<Transition name="fade">` and renders the overlay only when `showOverlay`. Includes `<style scoped>` block defining `.fade-enter-active`/`.fade-leave-active`/`.fade-enter-from`/`.fade-leave-to` transitions.

    The overlay element has `class="fixed inset-0 z-[9998] bg-indigo-900/40 flex items-center justify-center pointer-events-none"` and `data-test="os-drag-overlay"`.
  </behavior>
  <action>
    Create `frontend/src/components/layout/OsDragOverlay.vue` using the exact Options API structure from PATTERNS.md §"OsDragOverlay.vue". Use the template + style above. No comments inside the file. Add `data-test="os-drag-overlay"` for testability.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run OsDragOverlay</automated>
    Expected: 8 tests GREEN.
  </verify>
  <acceptance_criteria>
    - File `frontend/src/components/layout/OsDragOverlay.vue` exists
    - File contains `name: 'OsDragOverlay'`
    - File contains `emits: ['files-dropped']`
    - File contains `dragDepth: 0` in data
    - File contains `dataTransfer?.types.includes('Files')` guard
    - File contains all four window listeners (dragenter, dragleave, dragover, drop)
    - File contains `<Teleport to="body">`
    - File contains class string with `z-[9998]`
    - File uses Options API (no `<script setup>`)
    - 8 tests pass
  </acceptance_criteria>
  <done>OsDragOverlay implemented; 8 tests green.</done>
</task>

<task type="auto">
  <name>Task 3: Mount OsDragOverlay in App.vue; expose handleOsDrop in FileManagerView</name>
  <files>frontend/src/App.vue, frontend/src/views/FileManagerView.vue</files>
  <read_first>
    - frontend/src/App.vue (current state after 10-04 + 10-09)
    - frontend/src/views/FileManagerView.vue (current state after 10-06 + 10-09)
    - frontend/src/components/layout/OsDragOverlay.vue (newly created)
  </read_first>
  <action>
    Step A - App.vue:
    1. Add `import OsDragOverlay from './components/layout/OsDragOverlay.vue'` to script imports.
    2. Add `<OsDragOverlay @files-dropped="onOsFilesDropped" />` to the template (place it after `<ToastContainer />` from 10-04 so the overlay z-[9998] is below the toast z-[9999]).
    3. Add handler in script:
       ```js
       function onOsFilesDropped(files) {
         routeViewRef.value?.handleOsDrop?.(files)
       }
       ```

    Step B - FileManagerView.vue:
    Extend the existing `defineExpose` block (added in 10-09) to also include `handleOsDrop`:
    ```js
    defineExpose({
      focusSearch: () => browserRef.value?.focusSearch?.(),
      triggerUpload: () => browserRef.value?.triggerUpload?.(),
      startNewFolder: () => browserRef.value?.startNewFolder?.(),
      clearSearch: () => browserRef.value?.clearSearch?.(),
      handleOsDrop: (files) => onFilesSelected({ files, autoClassify: true }),
    })
    ```

    Do not modify any other logic. The existing onFilesSelected handles the upload + per-file UploadProgress + toast wiring (from 10-06) end to end.
  </action>
  <verify>
    <automated>cd frontend && npm run test -- --run OsDragOverlay; cd frontend && npm run test -- --run FileManagerView; cd frontend && npm run test -- --run keyboard; cd frontend && npm run test -- --run toast</automated>
    Expected: all suites pass; no regression.
  </verify>
  <acceptance_criteria>
    - `grep -E "import OsDragOverlay" frontend/src/App.vue` returns 1
    - `grep -E "<OsDragOverlay" frontend/src/App.vue` returns 1
    - `grep -E "onOsFilesDropped" frontend/src/App.vue` returns 2 (handler + binding)
    - `grep -E "routeViewRef\\.value\\?\\.handleOsDrop" frontend/src/App.vue` returns 1
    - `grep -E "handleOsDrop:\\s*\\(files\\)" frontend/src/views/FileManagerView.vue` returns 1
    - `grep -E "onFilesSelected\\(\\{\\s*files,\\s*autoClassify:\\s*true\\s*\\}\\)" frontend/src/views/FileManagerView.vue` returns 1
    - OsDragOverlay placed AFTER ToastContainer in App.vue template (toast z-[9999] dominates) - inspect via `grep -n` ordering
    - All test suites pass
  </acceptance_criteria>
  <done>OsDragOverlay mounted in App.vue; FileManagerView exposes handleOsDrop; UX-09 fully wired.</done>
</task>

</tasks>

<verification>
- `cd frontend && npm run test -- --run OsDragOverlay` exits 0 with 8 tests passing
- `cd frontend && npm run test -- --run` (full) exits 0
- `grep -n "ToastContainer\\|OsDragOverlay" frontend/src/App.vue` shows OsDragOverlay AFTER ToastContainer (or below it in source order)
- OsDragOverlay.vue uses z-[9998] (below ToastContainer z-[9999])
- FileManagerView.handleOsDrop reuses onFilesSelected (no duplicate upload logic)
</verification>

<success_criteria>
Dragging one or more files from the OS file explorer over the browser window shows an indigo overlay with an upload icon and "Drop files to upload" prompt. Releasing the files uploads them via the existing FileManagerView upload flow (with per-file progress in UploadProgress and a summary toast). The overlay does not appear when dragging an in-app element (file row, folder row). The overlay does not appear when on non-file-manager routes (admin, settings) because routeViewRef does not expose handleOsDrop there.
</success_criteria>

<output>
Create `.planning/phases/10-ux-interaction/10-10-SUMMARY.md` when done.
</output>
