- App.vue: mobile header with hamburger button; slide-in overlay drawer with Teleport backdrop, translate-x-0/-translate-x-full transition; drawer state owned by App.vue (D-04/D-05); route-change auto-close - AdminLayout.vue: matching responsive treatment — hamburger, backdrop, drawer, route-change close (RESP-05) - StorageBrowser.vue: responsive grid templates (mobile: grid-cols-[2rem_1fr_6rem], sm: +modified, md: all 5 cols); Size hidden below md, Modified hidden below sm; action buttons get min-w-[36px] min-h-[36px] touch targets (RESP-02, RESP-03) - Tests: drawer open/close/backdrop-close/route-change behaviour; admin drawer; responsive column and touch-target class assertions
102 lines
3.5 KiB
Vue
102 lines
3.5 KiB
Vue
<template>
|
|
<AuthLayout v-if="route.meta.layout === 'auth'" />
|
|
<router-view v-else-if="route.matched.some(r => r.meta.requiresAdmin)" />
|
|
<div v-else class="flex h-screen overflow-hidden">
|
|
<!-- Mobile-only top header with hamburger -->
|
|
<header class="lg:hidden fixed top-0 left-0 right-0 z-30 flex items-center gap-3 px-4 py-3 bg-white border-b border-gray-200">
|
|
<button
|
|
@click="drawerOpen = true"
|
|
aria-label="Open navigation"
|
|
data-test="hamburger-btn"
|
|
class="p-2 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-700 transition-colors"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
<span class="text-base font-bold text-indigo-600 tracking-tight">DocuVault</span>
|
|
</header>
|
|
|
|
<!-- Drawer backdrop (mobile only) — teleported to body to guarantee stacking -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="drawerOpen"
|
|
class="lg:hidden fixed inset-0 z-40 bg-black/40"
|
|
data-test="drawer-backdrop"
|
|
@click="drawerOpen = false"
|
|
></div>
|
|
</Teleport>
|
|
|
|
<!-- Sidebar: always visible on lg+; slides in as overlay drawer below lg -->
|
|
<div
|
|
class="fixed inset-y-0 left-0 z-50 transition-transform duration-200 ease-in-out lg:static lg:z-auto lg:translate-x-0 shrink-0"
|
|
:class="drawerOpen ? 'translate-x-0' : '-translate-x-full'"
|
|
data-test="app-sidebar-wrapper"
|
|
>
|
|
<AppSidebar />
|
|
</div>
|
|
|
|
<main class="flex-1 overflow-y-auto pt-[53px] lg:pt-0">
|
|
<router-view />
|
|
</main>
|
|
</div>
|
|
<ToastContainer />
|
|
<OsDragOverlay @files-dropped="onOsFilesDropped" />
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import AppSidebar from './components/layout/AppSidebar.vue'
|
|
import AuthLayout from './layouts/AuthLayout.vue'
|
|
import ToastContainer from './components/ui/ToastContainer.vue'
|
|
import OsDragOverlay from './components/layout/OsDragOverlay.vue'
|
|
import { useTopicsStore } from './stores/topics.js'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const topicsStore = useTopicsStore()
|
|
|
|
// Drawer state owned by App.vue (per D-04/D-05 — not in AppSidebar)
|
|
const drawerOpen = ref(false)
|
|
|
|
// Close drawer on route change (navigation tap closes the drawer automatically)
|
|
watch(() => route.fullPath, () => {
|
|
drawerOpen.value = false
|
|
})
|
|
|
|
function getFileManagerInstance() {
|
|
return router.currentRoute.value.matched.find(r => r.instances?.default)?.instances?.default ?? null
|
|
}
|
|
|
|
function onOsFilesDropped(files) {
|
|
getFileManagerInstance()?.handleOsDrop?.(files)
|
|
}
|
|
|
|
function onKeydown(e) {
|
|
const tag = document.activeElement?.tagName
|
|
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return
|
|
if (document.querySelector('[role="dialog"]')) return
|
|
|
|
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {
|
|
e.preventDefault()
|
|
getFileManagerInstance()?.focusSearch?.()
|
|
}
|
|
if (e.key === 'Escape') {
|
|
getFileManagerInstance()?.clearSearch?.()
|
|
}
|
|
if (e.key === 'u' || e.key === 'U') {
|
|
getFileManagerInstance()?.triggerUpload?.()
|
|
}
|
|
if (e.key === 'n' || e.key === 'N') {
|
|
getFileManagerInstance()?.startNewFolder?.()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
topicsStore.fetchTopics()
|
|
document.addEventListener('keydown', onKeydown)
|
|
})
|
|
onUnmounted(() => document.removeEventListener('keydown', onKeydown))
|
|
</script>
|