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:
@@ -22,6 +22,9 @@ import { stripPhotosFromCache } from '../../hooks/usePhotosQuery'
|
||||
import { toast } from '../ToastContainer'
|
||||
import { PhotoInfoPanel } from '../sidebar/PhotoInfoPanel'
|
||||
import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
/**
|
||||
* Right-hand details panel.
|
||||
@@ -208,14 +211,16 @@ export function RightSidebar() {
|
||||
</h2>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{selectedPhotos.length > 0 && (
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-text-muted"
|
||||
onClick={clearSelection}
|
||||
className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
|
||||
title="Clear selection (Esc)"
|
||||
aria-label="Clear selection"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -305,7 +310,7 @@ export function RightSidebar() {
|
||||
|
||||
{/* Bulk rating */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-muted">Rating</label>
|
||||
<Label className="mb-1 block">Rating</Label>
|
||||
<div className="flex gap-1">
|
||||
{[1, 2, 3, 4, 5].map((value) => (
|
||||
<button
|
||||
@@ -333,7 +338,7 @@ export function RightSidebar() {
|
||||
|
||||
{/* Bulk color */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-muted">Color label</label>
|
||||
<Label className="mb-1 block">Color label</Label>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{COLOR_LABEL_OPTIONS.map(({ value, className }) => (
|
||||
<button
|
||||
@@ -362,18 +367,18 @@ export function RightSidebar() {
|
||||
|
||||
{/* Bulk flag */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-muted">Flag</label>
|
||||
<Label className="mb-1 block">Flag</Label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (!activeHeap) return
|
||||
heapMutation.mutate({ ids: selectedPhotos, remove: allMembers })
|
||||
}}
|
||||
disabled={!activeHeap || heapMutation.isPending}
|
||||
className={clsx(
|
||||
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
|
||||
allMembers
|
||||
? 'bg-pick/20 text-pick'
|
||||
? 'bg-pick/20 text-pick hover:bg-pick/30'
|
||||
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'
|
||||
)}
|
||||
title={
|
||||
@@ -384,16 +389,18 @@ export function RightSidebar() {
|
||||
: 'Set an active heap first'
|
||||
}
|
||||
>
|
||||
<ShoppingBasket className="h-3 w-3" />
|
||||
<ShoppingBasket className="mr-1 h-3 w-3" />
|
||||
{allMembers ? 'Selected' : 'Select'}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => bulkDiscardMutation.mutate(selectedPhotos)}
|
||||
className="flex items-center gap-1 rounded bg-surface-2 px-2 py-1 text-sm text-text-muted transition-colors hover:bg-surface-offset"
|
||||
className="text-text-muted"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
Discard
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -402,7 +409,7 @@ export function RightSidebar() {
|
||||
* input adds an existing tag if it matches a name, or creates
|
||||
* a new tag and applies it. */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-muted">Tags</label>
|
||||
<Label className="mb-1 block">Tags</Label>
|
||||
<BulkTagsEditor
|
||||
allTags={allTags}
|
||||
tagInput={tagInput}
|
||||
@@ -435,7 +442,7 @@ export function RightSidebar() {
|
||||
* clock (1970 epoch) and for legacy libraries where the folder
|
||||
* structure is the only trustworthy date signal. */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-muted">Date Taken</label>
|
||||
<Label className="mb-1 block">Date Taken</Label>
|
||||
<BulkTakenAtEditor
|
||||
disabled={
|
||||
bulkTakenAtMutation.isPending || bulkTakenAtMapMutation.isPending
|
||||
@@ -518,33 +525,36 @@ function BulkTakenAtEditor({
|
||||
<div className="space-y-2">
|
||||
{/* Apply-one row */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<input
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={uniformDraft}
|
||||
onChange={(e) => setUniformDraft(e.target.value)}
|
||||
disabled={disabled}
|
||||
className="flex-1 rounded border border-border bg-bg px-2 py-1 text-xs text-text focus:border-primary focus:outline-none disabled:opacity-50"
|
||||
className="h-7 flex-1 text-xs"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApplyUniform}
|
||||
disabled={disabled || !uniformDraft}
|
||||
className="rounded bg-primary/20 px-2 py-1 text-xs text-primary hover:bg-primary/30 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
className="bg-primary/20 text-primary hover:bg-primary/30"
|
||||
title={`Apply this date to all ${selectedCount} selected`}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Guess-from-path preview */}
|
||||
{preview === null ? (
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleGuess}
|
||||
disabled={disabled}
|
||||
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-primary/50 px-2 py-1 text-xs text-primary hover:bg-primary/10 disabled:opacity-50"
|
||||
className="w-full border-dashed border-primary/50 bg-transparent text-primary hover:bg-primary/10"
|
||||
title="Scan each photo's folder + filename for a date pattern"
|
||||
>
|
||||
Guess from folder paths
|
||||
</button>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="rounded border border-border bg-bg p-2 text-[11px]">
|
||||
<div className="mb-1.5 text-text-muted">
|
||||
@@ -570,20 +580,23 @@ function BulkTakenAtEditor({
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex gap-1.5">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleApplyPreview}
|
||||
disabled={disabled || preview.hits.length === 0}
|
||||
className="flex-1 rounded bg-primary/20 px-2 py-1 text-xs text-primary hover:bg-primary/30 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
className="flex-1 bg-primary/20 text-primary hover:bg-primary/30"
|
||||
>
|
||||
Apply {preview.hits.length}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setPreview(null)}
|
||||
disabled={disabled}
|
||||
className="rounded bg-surface-2 px-2 py-1 text-xs text-text-muted hover:bg-surface-offset disabled:opacity-50"
|
||||
className="text-text-muted"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -640,7 +653,7 @@ function BulkTagsEditor({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => onTagInputChange(e.target.value)}
|
||||
@@ -654,18 +667,20 @@ function BulkTagsEditor({
|
||||
}}
|
||||
placeholder="Filter or create…"
|
||||
disabled={disabled}
|
||||
className="w-full rounded border border-border bg-bg px-2 py-1 text-xs text-text placeholder-text-faint focus:border-primary focus:outline-none disabled:opacity-50"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
|
||||
{trimmed && !exactMatch && (
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSubmit}
|
||||
disabled={disabled}
|
||||
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-primary/50 px-2 py-1 text-xs text-primary hover:bg-primary/10 disabled:opacity-50"
|
||||
className="w-full border-dashed border-primary/50 bg-transparent text-primary hover:bg-primary/10"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Create "{trimmed}" and apply
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{filtered.length > 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user