fix(phase-11): close mobile storage UAT gaps
This commit is contained in:
@@ -2,40 +2,92 @@
|
||||
<div class="flex flex-col h-full">
|
||||
|
||||
<div class="sticky top-0 z-10 bg-white border-b border-gray-100">
|
||||
<div class="px-6 py-3 flex items-center gap-3 flex-wrap">
|
||||
<BreadcrumbBar
|
||||
:segments="breadcrumb"
|
||||
:root-label="mode === 'cloud' ? 'Cloud' : 'Home'"
|
||||
@navigate="$emit('breadcrumb-navigate', $event)"
|
||||
/>
|
||||
<div class="ml-auto flex items-center gap-2 shrink-0">
|
||||
<SearchBar v-if="showSearch" ref="searchBarRef" :model-value="searchQuery" @update:modelValue="$emit('search-change', $event)" />
|
||||
<div class="px-4 sm:px-6 py-3 space-y-2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="min-w-0 flex-1">
|
||||
<BreadcrumbBar
|
||||
:segments="breadcrumb"
|
||||
:root-label="mode === 'cloud' ? 'Cloud' : 'Home'"
|
||||
@navigate="$emit('breadcrumb-navigate', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="hidden sm:flex items-center gap-2 shrink-0" data-test="desktop-toolbar">
|
||||
<SearchBar v-if="showSearch" ref="searchBarRef" :model-value="searchQuery" @update:modelValue="$emit('search-change', $event)" />
|
||||
<SortControls
|
||||
v-if="showSearch"
|
||||
:sort="sortField"
|
||||
:order="sortOrder"
|
||||
@change="handleSortChange"
|
||||
/>
|
||||
<button
|
||||
v-if="mode === 'local'"
|
||||
@click="$emit('new-folder')"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-indigo-600 border border-indigo-200 hover:bg-indigo-50 active:bg-indigo-100 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<AppIcon name="plus" class="w-4 h-4" />
|
||||
New folder
|
||||
</button>
|
||||
</div>
|
||||
<div class="sm:hidden flex items-center gap-1 shrink-0" data-test="mobile-toolbar">
|
||||
<button
|
||||
v-if="showSearch"
|
||||
type="button"
|
||||
title="Search"
|
||||
aria-label="Search"
|
||||
:aria-expanded="mobileSearchOpen"
|
||||
class="w-9 h-9 rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 active:bg-gray-100 flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
@click="mobileSearchOpen = !mobileSearchOpen"
|
||||
>
|
||||
<AppIcon name="search" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
v-if="showSearch"
|
||||
type="button"
|
||||
title="Sort"
|
||||
aria-label="Sort"
|
||||
:aria-expanded="mobileSortOpen"
|
||||
class="w-9 h-9 rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 active:bg-gray-100 flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
@click="mobileSortOpen = !mobileSortOpen"
|
||||
>
|
||||
<AppIcon name="chartBar" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
v-if="mode === 'local'"
|
||||
type="button"
|
||||
title="New folder"
|
||||
aria-label="New folder"
|
||||
class="w-9 h-9 rounded-lg border border-indigo-200 text-indigo-600 hover:bg-indigo-50 active:bg-indigo-100 flex items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
@click="$emit('new-folder')"
|
||||
>
|
||||
<AppIcon name="plus" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showSearch && mobileSearchOpen" class="sm:hidden w-full" data-test="mobile-search-row">
|
||||
<SearchBar ref="searchBarRef" :model-value="searchQuery" @update:modelValue="$emit('search-change', $event)" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showSearch && mobileSortOpen"
|
||||
class="sm:hidden w-full rounded-lg border border-gray-200 bg-gray-50 px-2 py-2"
|
||||
data-test="mobile-sort-panel"
|
||||
>
|
||||
<SortControls
|
||||
v-if="showSearch"
|
||||
:sort="sortField"
|
||||
:order="sortOrder"
|
||||
@change="$emit('sort-change', $event)"
|
||||
@change="handleSortChange"
|
||||
/>
|
||||
<button
|
||||
v-if="mode === 'local'"
|
||||
@click="$emit('new-folder')"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-indigo-600 border border-indigo-200 hover:bg-indigo-50 active:bg-indigo-100 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<AppIcon name="plus" class="w-4 h-4" />
|
||||
New folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto flex flex-col">
|
||||
|
||||
<div class="px-6 pt-5 pb-3">
|
||||
<div class="px-4 sm:px-6 pt-5 pb-3">
|
||||
<DropZone ref="dropZoneRef" @files-selected="$emit('upload', $event)" />
|
||||
<UploadProgress :items="uploadQueue" />
|
||||
</div>
|
||||
|
||||
<div class="mx-6 px-4 py-2 grid grid-cols-[2rem_1fr_6rem] sm:grid-cols-[2rem_1fr_8rem_6rem] md:grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center rounded-lg bg-gray-50 text-xs font-semibold text-gray-400 uppercase tracking-wider select-none" data-test="list-header">
|
||||
<div class="mx-4 sm:mx-6 px-4 py-2 grid grid-cols-[2rem_minmax(0,1fr)_7rem] sm:grid-cols-[2rem_minmax(0,1fr)_8rem_7rem] md:grid-cols-[2rem_minmax(0,1fr)_6rem_8rem_7rem] gap-3 items-center rounded-lg bg-gray-50 text-xs font-semibold text-gray-400 uppercase tracking-wider select-none" data-test="list-header">
|
||||
<span></span>
|
||||
<span>Name</span>
|
||||
<span class="text-right hidden md:block">Size</span>
|
||||
@@ -43,17 +95,17 @@
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div v-if="showNewFolderInput" class="mx-6 mt-1 px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem] sm:grid-cols-[2rem_1fr_8rem_6rem] md:grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center rounded-lg border border-amber-200 bg-amber-50/40">
|
||||
<div v-if="showNewFolderInput" class="mx-4 sm:mx-6 mt-1 px-4 py-2.5 grid grid-cols-[2rem_minmax(0,1fr)_7rem] sm:grid-cols-[2rem_minmax(0,1fr)_8rem_7rem] md:grid-cols-[2rem_minmax(0,1fr)_6rem_8rem_7rem] gap-3 items-center rounded-lg border border-amber-200 bg-amber-50/40">
|
||||
<div class="w-7 h-7 bg-amber-50 rounded-lg flex items-center justify-center">
|
||||
<AppIcon name="folder" class="w-4 h-4 text-amber-400" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 col-span-4">
|
||||
<div class="flex items-center gap-2 col-span-2 sm:col-span-3 md:col-span-4 min-w-0 flex-wrap">
|
||||
<input
|
||||
ref="newFolderInputRef"
|
||||
v-model="newFolderName"
|
||||
type="text"
|
||||
placeholder="Folder name"
|
||||
class="border border-gray-300 rounded-lg px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
class="border border-gray-300 rounded-lg px-2 py-1 text-sm min-w-0 flex-1 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
@keydown.enter="submitNewFolder"
|
||||
@keydown.escape="cancelNewFolder"
|
||||
/>
|
||||
@@ -63,12 +115,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-6 mt-1 mb-6 flex flex-col divide-y divide-gray-100 border border-gray-100 rounded-xl overflow-hidden">
|
||||
<div class="mx-4 sm:mx-6 mt-1 mb-6 flex flex-col divide-y divide-gray-100 border border-gray-100 rounded-xl overflow-hidden">
|
||||
|
||||
<div
|
||||
v-for="folder in folders"
|
||||
:key="`f-${folder.id}`"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem] sm:grid-cols-[2rem_1fr_8rem_6rem] md:grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_minmax(0,1fr)_7rem] sm:grid-cols-[2rem_minmax(0,1fr)_8rem_7rem] md:grid-cols-[2rem_minmax(0,1fr)_6rem_8rem_7rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors"
|
||||
:class="{
|
||||
'bg-amber-50 ring-2 ring-inset ring-amber-300': dragOverFolderId === folder.id,
|
||||
'bg-gray-50': renamingId === folder.id,
|
||||
@@ -97,7 +149,7 @@
|
||||
<span class="text-right text-xs text-gray-400 hidden md:block">—</span>
|
||||
<span class="text-right text-xs text-gray-400 hidden sm:block">{{ formatDate(folder.created_at) }}</span>
|
||||
|
||||
<div class="flex justify-end gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity" @click.stop>
|
||||
<div class="flex justify-end gap-0.5" data-test="folder-row-actions" @click.stop>
|
||||
<template v-if="mode === 'local'">
|
||||
<button @click.stop="startRename(folder)" title="Rename"
|
||||
class="p-1.5 md:p-1.5 min-w-[36px] min-h-[36px] md:min-w-0 md:min-h-0 rounded hover:bg-gray-200 active:bg-gray-300 text-gray-400 hover:text-gray-700 transition-colors flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1">
|
||||
@@ -116,7 +168,7 @@
|
||||
v-for="file in files"
|
||||
:key="`d-${file.id}`"
|
||||
:draggable="mode === 'local'"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem] sm:grid-cols-[2rem_1fr_8rem_6rem] md:grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors select-none"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_minmax(0,1fr)_7rem] sm:grid-cols-[2rem_minmax(0,1fr)_8rem_7rem] md:grid-cols-[2rem_minmax(0,1fr)_6rem_8rem_7rem] gap-3 items-center hover:bg-gray-50 group cursor-pointer transition-colors select-none"
|
||||
:class="{ 'opacity-50': draggingFile?.id === file.id }"
|
||||
@click="draggingFile ? null : $emit('file-open', file)"
|
||||
@dragstart="mode === 'local' ? onFileDragStart(file, $event) : null"
|
||||
@@ -144,7 +196,7 @@
|
||||
<span class="text-right text-xs text-gray-400 hidden md:block">{{ formatSize(file.size_bytes ?? file.size) }}</span>
|
||||
<span class="text-right text-xs text-gray-400 hidden sm:block">{{ formatDate(file.created_at) }}</span>
|
||||
|
||||
<div class="flex justify-end gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity" @click.stop>
|
||||
<div class="flex justify-end gap-0.5" data-test="file-row-actions" @click.stop>
|
||||
<template v-if="mode === 'local'">
|
||||
<button @click.stop="$emit('file-share', file)" title="Share"
|
||||
class="p-1.5 min-w-[36px] min-h-[36px] md:min-w-0 md:min-h-0 rounded hover:bg-gray-200 active:bg-gray-300 text-gray-400 hover:text-gray-700 transition-colors flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1">
|
||||
@@ -171,7 +223,7 @@
|
||||
<div
|
||||
v-for="n in 5"
|
||||
:key="`sk-${n}`"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_1fr_6rem] sm:grid-cols-[2rem_1fr_8rem_6rem] md:grid-cols-[2rem_1fr_6rem_8rem_6rem] gap-3 items-center border-b border-gray-100"
|
||||
class="px-4 py-2.5 grid grid-cols-[2rem_minmax(0,1fr)_7rem] sm:grid-cols-[2rem_minmax(0,1fr)_8rem_7rem] md:grid-cols-[2rem_minmax(0,1fr)_6rem_8rem_7rem] gap-3 items-center border-b border-gray-100"
|
||||
>
|
||||
<div class="w-7 h-7 bg-gray-100 rounded-lg animate-pulse"></div>
|
||||
<div class="h-4 bg-gray-100 rounded animate-pulse w-2/3"></div>
|
||||
@@ -292,6 +344,8 @@ function topicColor(name) {
|
||||
|
||||
const dropZoneRef = ref(null)
|
||||
const searchBarRef = ref(null)
|
||||
const mobileSearchOpen = ref(false)
|
||||
const mobileSortOpen = ref(false)
|
||||
|
||||
const showNewFolderInput = ref(false)
|
||||
const newFolderName = ref('')
|
||||
@@ -310,6 +364,11 @@ function cancelNewFolder() {
|
||||
newFolderError.value = ''
|
||||
}
|
||||
|
||||
function handleSortChange(payload) {
|
||||
emit('sort-change', payload)
|
||||
mobileSortOpen.value = false
|
||||
}
|
||||
|
||||
async function submitNewFolder() {
|
||||
const name = newFolderName.value.trim()
|
||||
if (!name) {
|
||||
|
||||
Reference in New Issue
Block a user