`. Replace with:
1. Add per-file refs is impractical (folders are iterated). Use a single shared trigger ref + a single Teleport.
2. Add new refs: `const pickerTriggerEl = ref(null)`, `const pickerStyle = ref({})`.
3. Add a function `function openFolderPicker(fileId, ev) { folderPickerFileId.value = fileId; const trigger = ev.currentTarget; const rect = trigger.getBoundingClientRect(); updatePickerPosition(rect) }`.
4. Add `function updatePickerPosition(rect) { const spaceBelow = window.innerHeight - rect.bottom; const dropH = 240; if (spaceBelow >= dropH || spaceBelow > 120) { pickerStyle.value = { top: `${rect.bottom + 4}px`, left: `${rect.left}px`, width: '192px' } } else { pickerStyle.value = { bottom: `${window.innerHeight - rect.top + 4}px`, left: `${rect.left}px`, width: '192px' } } }`.
5. Update the move-button to use `@click.stop="openFolderPicker(file.id, $event)"` (replacing the inline state setter).
6. Store the trigger button in a Map keyed by fileId (`const pickerTriggerMap = new Map()`) so reposition-on-scroll knows which trigger to recompute. On `openFolderPicker`, set `pickerTriggerMap.set(fileId, ev.currentTarget)`.
7. Move the dropdown `
` OUT of the inline `
` and into a single `
` block at the end of the template:
```html
```
Reuse the existing list-of-folders markup verbatim - only the wrapping changes.
8. Add a window scroll listener to keep the picker positioned:
```js
function onWindowScroll() {
if (!folderPickerFileId.value) return
const trig = pickerTriggerMap.get(folderPickerFileId.value)
if (trig) updatePickerPosition(trig.getBoundingClientRect())
}
onMounted(() => { window.addEventListener('scroll', onWindowScroll, true); window.addEventListener('resize', onWindowScroll) })
onUnmounted(() => { window.removeEventListener('scroll', onWindowScroll, true); window.removeEventListener('resize', onWindowScroll) })
```
9. Keep the existing outside-click handler (`onOutsideClick`) but update it to also close the picker when clicking outside both the trigger AND the teleported dropdown. Simplest: in onOutsideClick, check `if (!e.target.closest('[data-test="folder-picker"]') && !e.target.closest('.relative')) folderPickerFileId.value = null`. Or rely on the existing `.relative` check since the trigger button is still wrapped in `.relative` even if the dropdown is teleported.
Preserve all other StorageBrowser functionality untouched.
cd frontend && npm run test -- --run dragmove; cd frontend && npm run test -- --run StorageBrowser
Expected: 4 UX-11 dragmove tests pass; existing StorageBrowser suite unaffected.
- `grep -E "draggingFile\\s*\\?\\s*null\\s*:\\s*\\$emit\\('file-open'" frontend/src/components/storage/StorageBrowser.vue` returns 1 (click guard in place)
- `grep -E "function onFileDragEnd" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "@dragend=\"onFileDragEnd\"" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "await nextTick\\(\\)" frontend/src/components/storage/StorageBrowser.vue` returns >= 1 (in onDropDocOnFolder)
- `grep -E "" frontend/src/components/storage/StorageBrowser.vue` returns >= 1
- `grep -E "data-test=\"folder-picker\"" frontend/src/components/storage/StorageBrowser.vue` returns 1
- `grep -E "getBoundingClientRect" frontend/src/components/storage/StorageBrowser.vue` returns >= 1
- `grep -E "addEventListener\\('scroll'" frontend/src/components/storage/StorageBrowser.vue` returns 1
- 4 UX-11 dragmove tests pass
- Existing StorageBrowser test suite passes
Click guard + teleport in place; UX-11 dragmove tests green.
Task 3: Teleport DocumentCard folder picker
frontend/src/components/documents/DocumentCard.vue
- frontend/src/components/documents/DocumentCard.vue (current state - has folder picker absolute-positioned)
- frontend/src/components/ui/SearchableModelSelect.vue (Teleport reference)
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"DocumentCard.vue + FolderRow.vue"
Apply the same Teleport pattern to the DocumentCard folder picker:
1. Add refs: `pickerTriggerEl`, `pickerStyle`. Add `updatePosition()` helper using getBoundingClientRect.
2. Change `toggleFolderPicker()` to capture the trigger element and recompute position before setting `showFolderPicker = true`.
3. Wrap the picker dropdown in `` with `class="fixed z-[9999]"` and `:style="pickerStyle"`. Add `data-test="folder-picker"`.
4. Add window scroll + resize listeners that call updatePosition when open.
5. Add window scroll + resize listener cleanup in onUnmounted.
6. Ensure outside-click closes the picker (existing logic should still work if it checks for the trigger button's wrapper class).
Reuse the exact dropdown markup; only the wrapping changes.
cd frontend && npm run test -- --run dropdown
Expected: 2 DocumentCard-related tests pass (Teleport + position).
- `grep -E "" frontend/src/components/documents/DocumentCard.vue` returns 1
- `grep -E "getBoundingClientRect" frontend/src/components/documents/DocumentCard.vue` returns >= 1
- `grep -E "data-test=\"folder-picker\"" frontend/src/components/documents/DocumentCard.vue` returns 1
- `grep -E "addEventListener\\('scroll'" frontend/src/components/documents/DocumentCard.vue` returns 1
- 2 DocumentCard-related dropdown tests pass
DocumentCard folder picker teleported.
Task 4: Teleport FolderRow three-dot menu
frontend/src/components/folders/FolderRow.vue
- frontend/src/components/folders/FolderRow.vue (current state - has three-dot menu absolute-positioned, lines ~37-66)
- frontend/src/components/ui/SearchableModelSelect.vue (Teleport reference)
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"DocumentCard.vue + FolderRow.vue"
Apply the same Teleport pattern to FolderRow's three-dot menu:
1. Add refs: `menuTriggerEl`, `menuStyle`. Add `updatePosition()` helper using getBoundingClientRect.
2. Change `toggleMenu()` to capture the trigger element and recompute position before flipping `menuOpen`.
3. Wrap the menu `` in `
` with `class="fixed z-[9999]"` and `:style="menuStyle"`. Add `data-test="folder-row-menu"`.
4. Add window scroll + resize listeners.
5. Outside-click logic preserved or updated.
cd frontend && npm run test -- --run dropdown; cd frontend && npm run test -- --run
Expected: all 4 dropdown tests pass + full suite green.
- `grep -E "" frontend/src/components/folders/FolderRow.vue` returns 1
- `grep -E "getBoundingClientRect" frontend/src/components/folders/FolderRow.vue` returns >= 1
- `grep -E "data-test=\"folder-row-menu\"" frontend/src/components/folders/FolderRow.vue` returns 1
- `grep -E "addEventListener\\('scroll'" frontend/src/components/folders/FolderRow.vue` returns 1
- All 4 dropdown tests pass
- Full test suite passes
FolderRow three-dot menu teleported; UX-13 fully closed.
- `cd frontend && npm run test -- --run dragmove` exits 0
- `cd frontend && npm run test -- --run dropdown` exits 0
- `cd frontend && npm run test -- --run` (full suite) exits 0
- StorageBrowser, DocumentCard, FolderRow each contain ``
- StorageBrowser file-row click guard present (grep on draggingFile + file-open)
Drag a document onto a folder row -> folder row gets the amber ring -> release -> document moves and a "Document moved" toast appears -> clicking the same file row does NOT navigate (the drag suppressed the click). Opening the move-to-folder dropdown on any file (in StorageBrowser or DocumentCard) shows a properly positioned dropdown that does not clip at the viewport edge and follows the trigger when the page scrolls. Same for FolderRow three-dot menu.