feat(deps): Tier 2 UX polish — embla, lightbox, gestures, virtuoso, motion

Installs all five Tier 2 polish deps the audit flagged. Each integrates
where it adds concrete value today:

- **embla-carousel-react** — shadcn-style `<Carousel>` primitive in
  `src/components/ui/carousel.tsx`. Available for future berth/yacht
  photo galleries; no current call site beyond the primitive.
- **yet-another-react-lightbox** — wired into the image branch of
  `file-preview-dialog.tsx`. Clicking the preview image now opens a
  fullscreen lightbox with zoom/pan/keyboard nav. Lazy-loaded so the
  ~50kb only ships when a user actually previews an image.
- **@use-gesture/react** — `usePinch` on the PdfViewer's content
  pane for native pinch-zoom on tablets/phones. Clamped to the
  same [50%, 300%] range as the +/- buttons; desktop wheel still
  scrolls.
- **react-virtuoso** — installed but NOT wired. Inbox is naturally
  bounded by recent-notifications filter at ~10-20 items; ScrollArea
  handles it fine. Reserve for actual scale issues (admin audit log
  archive, etc.).
- **motion** — installed but NOT wired. Pipeline kanban uses
  dnd-kit's own transforms and conflicts with motion's layout
  animation. @formkit/auto-animate already handles list-mutation
  animations elsewhere. Available for opportunistic adoption when
  a polish surface emerges that the existing libraries don't cover.

Verified: tsc clean, vitest 1315/1315, next build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 23:29:22 +02:00
parent 4329db7fc3
commit d1c9469fa7
5 changed files with 418 additions and 5 deletions

View File

@@ -2,6 +2,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import { usePinch } from '@use-gesture/react';
import { ChevronLeft, ChevronRight, Loader2, Minus, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -61,6 +62,22 @@ export function PdfViewer({ url, fileName }: PdfViewerProps) {
setError(null);
}, [url]);
// Pinch-zoom on touch devices. usePinch's `offset` already maps
// gesture distance to a smoothly-changing scalar; we clamp it to
// the same [0.5, 3] range as the +/- buttons.
const pinchBindings = usePinch(
({ offset: [s] }) => {
setScale(Math.max(0.5, Math.min(3, s)));
},
{
scaleBounds: { min: 0.5, max: 3 },
from: () => [scale, 0],
// Don't hijack wheel events — desktop users zoom via buttons,
// wheel still scrolls the page.
eventOptions: { passive: false },
},
);
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between gap-2 border-b bg-muted/40 px-3 py-2 text-sm">
@@ -113,7 +130,14 @@ export function PdfViewer({ url, fileName }: PdfViewerProps) {
</div>
</div>
<div className="flex-1 overflow-auto bg-muted/30 p-4">
<div
className="flex-1 overflow-auto bg-muted/30 p-4 touch-pan-y"
// Pinch-zoom on touch devices (tablets, phones). On desktop the
// +/- buttons stay primary; the gesture handler ignores wheel
// events so it doesn't fight scroll-zoom. Range matches the
// button zoom (50%300%).
{...pinchBindings()}
>
{error ? (
<div className="flex h-full items-center justify-center text-sm text-destructive">
{error}