ui: migrate to shadcn/ui primitives across dialogs, filters, and forms

Adopts shadcn/ui components (Dialog, Button, Input, Select, Popover,
Command, Checkbox, Switch, Toggle, Calendar, etc.) across the app,
replacing hand-rolled modals, dropdowns, and form controls. Adds a
reusable cmdk-backed MultiSelect for the Type, Tags, and Flag filters
so all multi-value filter popovers share one component and layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 09:07:20 +02:00
parent 8529771122
commit 7efac4354e
51 changed files with 3032 additions and 1660 deletions

View File

@@ -17,6 +17,12 @@ import { useHeapsQuery, HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
import { FOLDER_TREE_QUERY_KEY } from '../../hooks/useFolderTreeQuery'
import { LIBRARY_STATS_QUERY_KEY } from '../../hooks/useLibraryStatsQuery'
import { toast } from '../ToastContainer'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
interface UploadModalProps {
isOpen: boolean
@@ -123,15 +129,8 @@ export function UploadModal({ isOpen, onClose, initialFolderId }: UploadModalPro
}
}, [isOpen, initialFolderId, folderTree, destFolderId])
// Esc closes (unless mid-upload — don't orphan in-flight requests).
useEffect(() => {
if (!isOpen) return
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isUploading) onClose()
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [isOpen, isUploading, onClose])
// Esc / overlay-click dismissal lives on the Dialog primitive below.
// We only need to guard against closing while an upload is in flight.
// Reset transient state on open so a previous session's queue doesn't
// bleed into a fresh one.
@@ -324,31 +323,20 @@ export function UploadModal({ isOpen, onClose, initialFolderId }: UploadModalPro
)
const overallPct = totalBytes > 0 ? Math.round((uploadedBytes / totalBytes) * 100) : 0
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={!isUploading ? onClose : undefined}
/>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<div className="relative z-10 flex max-h-[85vh] w-[760px] flex-col rounded-lg border border-border bg-surface shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-5 py-3">
<div className="flex items-center gap-2">
<UploadIcon className="h-4 w-4 text-text-muted" />
<h2 className="text-base font-semibold text-text">Upload photos</h2>
</div>
<button
onClick={onClose}
disabled={isUploading}
className="rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text disabled:opacity-40"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
</div>
<Dialog
open={isOpen}
onOpenChange={(o) => {
if (!o && !isUploading) onClose()
}}
>
<DialogContent className="flex max-h-[85vh] w-[760px] max-w-[760px] flex-col p-0">
<DialogHeader className="border-b border-border px-5 py-3">
<DialogTitle className="flex items-center gap-2">
<UploadIcon className="h-4 w-4 text-text-muted" />
Upload photos
</DialogTitle>
</DialogHeader>
{/* Body */}
<div className="flex min-h-0 flex-1 gap-4 overflow-hidden p-5">
@@ -535,9 +523,8 @@ export function UploadModal({ isOpen, onClose, initialFolderId }: UploadModalPro
{isUploading ? 'Uploading…' : `Upload ${queue.length || ''}`.trim()}
</button>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}