@bearmenu/ui 0.8.34 → 0.8.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/chunk-EGU6EG73.js +39 -0
  2. package/dist/{chunk-NIAL4ZU2.js → chunk-FQAIYKAS.js} +45 -8
  3. package/dist/{chunk-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
  4. package/dist/chunk-LAEVMNJN.js +135 -0
  5. package/dist/{chunk-XNBDACL7.js → chunk-ZUBBMLBD.js} +1 -1
  6. package/dist/components/badge.d.ts +8 -2
  7. package/dist/components/badge.js +1 -1
  8. package/dist/components/banner.d.ts +17 -2
  9. package/dist/components/banner.js +10 -7
  10. package/dist/components/booking-row.d.ts +11 -1
  11. package/dist/components/booking-row.js +4 -1
  12. package/dist/components/button-group.js +3 -39
  13. package/dist/components/concept-convergence.js +1 -1
  14. package/dist/components/filter-category-row.js +1 -1
  15. package/dist/components/menu-item.js +3 -3
  16. package/dist/components/multi-select-combobox.js +8 -3
  17. package/dist/components/pan-zoom-viewport.d.ts +137 -0
  18. package/dist/components/pan-zoom-viewport.js +531 -0
  19. package/dist/components/place-card.js +2 -2
  20. package/dist/components/place-details-mobile.js +1 -1
  21. package/dist/components/responsive-overlay.d.ts +29 -1
  22. package/dist/components/responsive-overlay.js +45 -6
  23. package/dist/components/sheet.d.ts +6 -0
  24. package/dist/components/sheet.js +3 -133
  25. package/dist/components/signature-chip.js +2 -2
  26. package/dist/components/tabs.d.ts +16 -1
  27. package/dist/components/tabs.js +21 -3
  28. package/dist/components/top-ranking-entry.js +3 -3
  29. package/package.json +23 -216
  30. package/src/components/badge.stories.tsx +32 -1
  31. package/src/components/badge.test.tsx +45 -0
  32. package/src/components/badge.tsx +41 -3
  33. package/src/components/banner.stories.tsx +32 -0
  34. package/src/components/banner.test.tsx +58 -0
  35. package/src/components/banner.tsx +28 -4
  36. package/src/components/booking-row.tsx +13 -0
  37. package/src/components/multi-select-combobox.tsx +7 -1
  38. package/src/components/pan-zoom-viewport.stories.tsx +134 -0
  39. package/src/components/pan-zoom-viewport.test.tsx +381 -0
  40. package/src/components/pan-zoom-viewport.tsx +727 -0
  41. package/src/components/responsive-overlay.stories.tsx +61 -0
  42. package/src/components/responsive-overlay.test.tsx +104 -0
  43. package/src/components/responsive-overlay.tsx +87 -22
  44. package/src/components/sheet.tsx +14 -3
  45. package/src/components/tabs.stories.tsx +23 -0
  46. package/src/components/tabs.test.tsx +64 -0
  47. package/src/components/tabs.tsx +36 -3
@@ -0,0 +1,137 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * PanZoomViewport — a window onto content larger or smaller than itself, that
5
+ * pans and zooms: the floor plan in service and in the plan editor
6
+ * (floor/07-f0, floor/12-s11, seating/01-k2, registry/10-p0, registry/21-p15).
7
+ *
8
+ * The content is laid out at its natural size (`contentWidth` ×
9
+ * `contentHeight`, in CSS px) and moved with ONE transform —
10
+ * `translate(x, y) scale(s)` from its top-left corner — so what it draws is
11
+ * the caller's business and nothing here knows about tables.
12
+ *
13
+ * Input, all of it through the same clamp:
14
+ * - Drag with a mouse, a pen or one finger pans. The pan starts after 4px, so
15
+ * a tap still reaches a table underneath as a click, and the click a drag
16
+ * would otherwise become is swallowed. An element marked
17
+ * `data-pan-zoom-ignore` never starts a pan (the plan editor's drag handles).
18
+ * - Two fingers pinch; a trackpad pinch (a ctrl-wheel in Chromium and
19
+ * Firefox, a gesture event in Safari) and ctrl / ⌘ + wheel zoom about the
20
+ * pointer. A plain wheel or two-finger scroll pans — and hands the scroll
21
+ * back to the page once the content cannot move further that way, so the
22
+ * viewport never traps a page scroll.
23
+ * - Keyboard, with the viewport focused: arrows pan (Shift for four times the
24
+ * step); `+` / `-` zoom and `0` fits, from anywhere inside it. Tabbing onto
25
+ * a table that sits off screen pans it into view.
26
+ * - The + / − / Fit controls, bottom right.
27
+ *
28
+ * The min-scale floor: `Fit` never shrinks below `minScale`. On a phone the
29
+ * plan would otherwise fit at a size no finger can hit, so below the floor it
30
+ * stops shrinking, anchors top-left, and the rest is a pan away — with a
31
+ * shaded edge on every side where content runs past the viewport, so the
32
+ * clipped tables read as "more this way", not as a broken layout.
33
+ *
34
+ * Zooms from the controls and the keyboard ease over `--duration-quick`;
35
+ * gestures follow the finger with no easing; `prefers-reduced-motion` drops
36
+ * the easing entirely. Every string is injected through `labels`: the design
37
+ * system holds no copy.
38
+ *
39
+ * The viewport takes its size from the caller — give it a height. It refits
40
+ * when the viewport resizes or the content changes size, until the user moves
41
+ * it; after that it keeps their view and only pulls it back inside the
42
+ * bounds. To start a different plan fitted (switching Inside → Terrace),
43
+ * give the viewport a `key` per plan, or call `apiRef.current.fit()`.
44
+ */
45
+ type PanZoomTransform = {
46
+ /** 1 is the content's natural size. */
47
+ scale: number;
48
+ /** The content's top-left corner, in viewport px. */
49
+ x: number;
50
+ y: number;
51
+ };
52
+ type PanZoomViewportLabels = {
53
+ /** The viewport's accessible name — "Floor plan, Inside". */
54
+ viewport: string;
55
+ /** Read after the name — "Arrow keys move the plan; plus and minus zoom; 0 fits it." */
56
+ instructions?: string;
57
+ /** The + button — "Zoom in". */
58
+ zoomIn: string;
59
+ /** The − button — "Zoom out". */
60
+ zoomOut: string;
61
+ /** The fit button's visible text — "Fit". */
62
+ fit: string;
63
+ /** The controls group — "Zoom". */
64
+ controls?: string;
65
+ /**
66
+ * Announced politely after a zoom from the controls or the keyboard —
67
+ * `(percent) => \`${percent}%\``. Omit it and nothing is announced.
68
+ */
69
+ zoomLevel?: (percent: number) => string;
70
+ };
71
+ /** Drive the viewport from outside: a "Show T5" link, a reset on area change. */
72
+ type PanZoomViewportHandle = {
73
+ zoomIn: () => void;
74
+ zoomOut: () => void;
75
+ fit: () => void;
76
+ /** Zoom to `scale` about the viewport's centre. */
77
+ zoomTo: (scale: number) => void;
78
+ /** Move the content by `dx`, `dy` viewport px. */
79
+ panBy: (dx: number, dy: number) => void;
80
+ /** Bring the content point (`x`, `y`, in natural px) to the viewport's centre. */
81
+ centerOn: (x: number, y: number) => void;
82
+ getTransform: () => PanZoomTransform;
83
+ };
84
+ type PanZoomViewportProps = Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
85
+ /** The content's natural width, in CSS px. */
86
+ contentWidth: number;
87
+ /** The content's natural height, in CSS px. */
88
+ contentHeight: number;
89
+ children: React.ReactNode;
90
+ labels: PanZoomViewportLabels;
91
+ /** The floor: neither Fit nor zooming out goes below it. Default 0.5. */
92
+ minScale?: number;
93
+ /** Default 2.5. */
94
+ maxScale?: number;
95
+ /** The factor one + / − step multiplies by. Default 1.25. */
96
+ zoomStep?: number;
97
+ /** One arrow press, in viewport px. Default 48. */
98
+ panStep?: number;
99
+ /** The air Fit leaves around the content, and how far past it a pan may go. Default 24. */
100
+ fitPadding?: number;
101
+ /** Render the + / − / Fit controls. Default true. */
102
+ controls?: boolean;
103
+ /** Called with every transform — gestures included, so keep it cheap. */
104
+ onTransformChange?: (transform: PanZoomTransform) => void;
105
+ /** An imperative handle; see `PanZoomViewportHandle`. */
106
+ apiRef?: React.Ref<PanZoomViewportHandle>;
107
+ /** Classes for the transformed content layer. */
108
+ contentClassName?: string;
109
+ };
110
+ declare const PanZoomViewport: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "children"> & {
111
+ /** The content's natural width, in CSS px. */
112
+ contentWidth: number;
113
+ /** The content's natural height, in CSS px. */
114
+ contentHeight: number;
115
+ children: React.ReactNode;
116
+ labels: PanZoomViewportLabels;
117
+ /** The floor: neither Fit nor zooming out goes below it. Default 0.5. */
118
+ minScale?: number;
119
+ /** Default 2.5. */
120
+ maxScale?: number;
121
+ /** The factor one + / − step multiplies by. Default 1.25. */
122
+ zoomStep?: number;
123
+ /** One arrow press, in viewport px. Default 48. */
124
+ panStep?: number;
125
+ /** The air Fit leaves around the content, and how far past it a pan may go. Default 24. */
126
+ fitPadding?: number;
127
+ /** Render the + / − / Fit controls. Default true. */
128
+ controls?: boolean;
129
+ /** Called with every transform — gestures included, so keep it cheap. */
130
+ onTransformChange?: (transform: PanZoomTransform) => void;
131
+ /** An imperative handle; see `PanZoomViewportHandle`. */
132
+ apiRef?: React.Ref<PanZoomViewportHandle>;
133
+ /** Classes for the transformed content layer. */
134
+ contentClassName?: string;
135
+ } & React.RefAttributes<HTMLDivElement>>;
136
+
137
+ export { type PanZoomTransform, PanZoomViewport, type PanZoomViewportHandle, type PanZoomViewportLabels, type PanZoomViewportProps };
@@ -0,0 +1,531 @@
1
+ import { ButtonGroup } from '../chunk-EGU6EG73.js';
2
+ import { Button } from '../chunk-5SGKGFMN.js';
3
+ import '../chunk-RIH2IS7X.js';
4
+ import '../chunk-5YNIQ2BL.js';
5
+ import { cn } from '../chunk-FEK5XGZW.js';
6
+ import { __objRest, __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
7
+ import * as React from 'react';
8
+ import { Plus, Minus } from 'lucide-react';
9
+ import { jsxs, jsx } from 'react/jsx-runtime';
10
+
11
+ var DRAG_THRESHOLD = 4;
12
+ var MAX_WHEEL_ZOOM_DELTA = 25;
13
+ var WHEEL_ZOOM_SPEED = 0.01;
14
+ var EPSILON = 1e-3;
15
+ function clamp(value, min, max) {
16
+ return Math.min(max, Math.max(min, value));
17
+ }
18
+ function clampAxis(offset, content, viewport, pad) {
19
+ if (content + pad * 2 <= viewport) return (viewport - content) / 2;
20
+ return clamp(offset, viewport - content - pad, pad);
21
+ }
22
+ function distance(a, b) {
23
+ return Math.hypot(a.x - b.x, a.y - b.y);
24
+ }
25
+ function midpoint(a, b) {
26
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
27
+ }
28
+ function isEditable(target) {
29
+ return target instanceof HTMLElement && (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
30
+ }
31
+ var PanZoomViewport = React.forwardRef(
32
+ (_a, ref) => {
33
+ var _b = _a, {
34
+ contentWidth,
35
+ contentHeight,
36
+ children,
37
+ labels,
38
+ minScale = 0.5,
39
+ maxScale = 2.5,
40
+ zoomStep = 1.25,
41
+ panStep = 48,
42
+ fitPadding = 24,
43
+ controls = true,
44
+ onTransformChange,
45
+ apiRef,
46
+ className,
47
+ contentClassName
48
+ } = _b, props = __objRest(_b, [
49
+ "contentWidth",
50
+ "contentHeight",
51
+ "children",
52
+ "labels",
53
+ "minScale",
54
+ "maxScale",
55
+ "zoomStep",
56
+ "panStep",
57
+ "fitPadding",
58
+ "controls",
59
+ "onTransformChange",
60
+ "apiRef",
61
+ "className",
62
+ "contentClassName"
63
+ ]);
64
+ const surfaceRef = React.useRef(null);
65
+ const [viewport, setViewport] = React.useState(null);
66
+ const [transform, setTransform] = React.useState({ scale: 1, x: 0, y: 0 });
67
+ const [animate, setAnimate] = React.useState(false);
68
+ const [announcement, setAnnouncement] = React.useState("");
69
+ const transformRef = React.useRef(transform);
70
+ const fittedRef = React.useRef(true);
71
+ const pointers = React.useRef(/* @__PURE__ */ new Map());
72
+ const gesture = React.useRef({ kind: "idle" });
73
+ const didDrag = React.useRef(false);
74
+ const pressing = React.useRef(false);
75
+ const instructionsId = React.useId();
76
+ const onTransformChangeRef = React.useRef(onTransformChange);
77
+ const zoomLevelRef = React.useRef(labels.zoomLevel);
78
+ React.useEffect(() => {
79
+ onTransformChangeRef.current = onTransformChange;
80
+ zoomLevelRef.current = labels.zoomLevel;
81
+ });
82
+ const clampTransform = React.useCallback(
83
+ (next) => {
84
+ if (!viewport || viewport.width <= 0 || viewport.height <= 0) return next;
85
+ const scale = clamp(next.scale, minScale, maxScale);
86
+ return {
87
+ scale,
88
+ x: clampAxis(next.x, contentWidth * scale, viewport.width, fitPadding),
89
+ y: clampAxis(next.y, contentHeight * scale, viewport.height, fitPadding)
90
+ };
91
+ },
92
+ [viewport, minScale, maxScale, contentWidth, contentHeight, fitPadding]
93
+ );
94
+ const fitted = React.useCallback(() => {
95
+ if (!viewport || viewport.width <= 0 || viewport.height <= 0) return null;
96
+ const scale = Math.min(
97
+ (viewport.width - fitPadding * 2) / contentWidth,
98
+ (viewport.height - fitPadding * 2) / contentHeight
99
+ );
100
+ return clampTransform({ scale, x: fitPadding, y: fitPadding });
101
+ }, [viewport, fitPadding, contentWidth, contentHeight, clampTransform]);
102
+ const commit = React.useCallback(
103
+ (next, options) => {
104
+ var _a2;
105
+ const previous = transformRef.current;
106
+ fittedRef.current = (_a2 = options.fitted) != null ? _a2 : false;
107
+ setAnimate(options.animate);
108
+ if (previous.scale === next.scale && previous.x === next.x && previous.y === next.y) {
109
+ return;
110
+ }
111
+ transformRef.current = next;
112
+ setTransform(next);
113
+ const zoomLevel = zoomLevelRef.current;
114
+ if (options.announce && zoomLevel && Math.abs(next.scale - previous.scale) > EPSILON) {
115
+ setAnnouncement(zoomLevel(Math.round(next.scale * 100)));
116
+ }
117
+ },
118
+ []
119
+ );
120
+ const zoomAbout = React.useCallback(
121
+ (scale, at, options) => {
122
+ const current = transformRef.current;
123
+ const nextScale = clamp(scale, minScale, maxScale);
124
+ const ratio = nextScale / current.scale;
125
+ commit(
126
+ clampTransform({
127
+ scale: nextScale,
128
+ x: at.x - (at.x - current.x) * ratio,
129
+ y: at.y - (at.y - current.y) * ratio
130
+ }),
131
+ options
132
+ );
133
+ },
134
+ [commit, clampTransform, minScale, maxScale]
135
+ );
136
+ const centre = React.useCallback(
137
+ () => {
138
+ var _a2, _b2;
139
+ return { x: ((_a2 = viewport == null ? void 0 : viewport.width) != null ? _a2 : 0) / 2, y: ((_b2 = viewport == null ? void 0 : viewport.height) != null ? _b2 : 0) / 2 };
140
+ },
141
+ [viewport]
142
+ );
143
+ const zoomIn = React.useCallback(
144
+ () => zoomAbout(transformRef.current.scale * zoomStep, centre(), { animate: true, announce: true }),
145
+ [zoomAbout, zoomStep, centre]
146
+ );
147
+ const zoomOut = React.useCallback(
148
+ () => zoomAbout(transformRef.current.scale / zoomStep, centre(), { animate: true, announce: true }),
149
+ [zoomAbout, zoomStep, centre]
150
+ );
151
+ const fit = React.useCallback(() => {
152
+ const next = fitted();
153
+ if (next) commit(next, { animate: true, fitted: true, announce: true });
154
+ }, [fitted, commit]);
155
+ const panBy = React.useCallback(
156
+ (dx, dy) => {
157
+ const current = transformRef.current;
158
+ commit(clampTransform(__spreadProps(__spreadValues({}, current), { x: current.x + dx, y: current.y + dy })), { animate: true });
159
+ },
160
+ [commit, clampTransform]
161
+ );
162
+ React.useImperativeHandle(
163
+ apiRef,
164
+ () => ({
165
+ zoomIn,
166
+ zoomOut,
167
+ fit,
168
+ zoomTo: (scale) => zoomAbout(scale, centre(), { animate: true, announce: true }),
169
+ panBy,
170
+ centerOn: (x, y) => {
171
+ const current = transformRef.current;
172
+ const middle = centre();
173
+ commit(
174
+ clampTransform({
175
+ scale: current.scale,
176
+ x: middle.x - x * current.scale,
177
+ y: middle.y - y * current.scale
178
+ }),
179
+ { animate: true }
180
+ );
181
+ },
182
+ getTransform: () => transformRef.current
183
+ }),
184
+ [zoomIn, zoomOut, fit, zoomAbout, centre, panBy, commit, clampTransform]
185
+ );
186
+ React.useLayoutEffect(() => {
187
+ const surface = surfaceRef.current;
188
+ if (!surface) return;
189
+ setViewport({ width: surface.clientWidth, height: surface.clientHeight });
190
+ if (typeof ResizeObserver === "undefined") return;
191
+ const observer = new ResizeObserver(([entry]) => {
192
+ if (!entry) return;
193
+ const { width, height } = entry.contentRect;
194
+ setViewport(
195
+ (previous) => previous && previous.width === width && previous.height === height ? previous : { width, height }
196
+ );
197
+ });
198
+ observer.observe(surface);
199
+ return () => observer.disconnect();
200
+ }, []);
201
+ React.useLayoutEffect(() => {
202
+ if (fittedRef.current) {
203
+ const next = fitted();
204
+ if (next) commit(next, { animate: false, fitted: true });
205
+ } else {
206
+ commit(clampTransform(transformRef.current), { animate: false });
207
+ }
208
+ }, [fitted, clampTransform, commit]);
209
+ React.useEffect(() => {
210
+ var _a2;
211
+ (_a2 = onTransformChangeRef.current) == null ? void 0 : _a2.call(onTransformChangeRef, transform);
212
+ }, [transform]);
213
+ React.useEffect(() => {
214
+ const surface = surfaceRef.current;
215
+ if (!surface) return;
216
+ const local = (clientX, clientY) => {
217
+ const rect = surface.getBoundingClientRect();
218
+ return { x: clientX - rect.left, y: clientY - rect.top };
219
+ };
220
+ const onWheel = (event) => {
221
+ const unit = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 16 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? surface.clientHeight : 1;
222
+ const current = transformRef.current;
223
+ if (event.ctrlKey || event.metaKey) {
224
+ event.preventDefault();
225
+ const delta = clamp(event.deltaY * unit, -MAX_WHEEL_ZOOM_DELTA, MAX_WHEEL_ZOOM_DELTA);
226
+ zoomAbout(current.scale * Math.exp(-delta * WHEEL_ZOOM_SPEED), local(event.clientX, event.clientY), {
227
+ animate: false
228
+ });
229
+ return;
230
+ }
231
+ const next = clampTransform(__spreadProps(__spreadValues({}, current), {
232
+ x: current.x - event.deltaX * unit,
233
+ y: current.y - event.deltaY * unit
234
+ }));
235
+ if (Math.abs(next.x - current.x) < 0.5 && Math.abs(next.y - current.y) < 0.5) return;
236
+ event.preventDefault();
237
+ commit(next, { animate: false });
238
+ };
239
+ let gestureStartScale = 1;
240
+ const onGestureStart = (event) => {
241
+ event.preventDefault();
242
+ gestureStartScale = transformRef.current.scale;
243
+ };
244
+ const onGestureChange = (event) => {
245
+ event.preventDefault();
246
+ if (pointers.current.size >= 2) return;
247
+ const gestureEvent = event;
248
+ zoomAbout(
249
+ gestureStartScale * gestureEvent.scale,
250
+ local(gestureEvent.clientX, gestureEvent.clientY),
251
+ { animate: false }
252
+ );
253
+ };
254
+ surface.addEventListener("wheel", onWheel, { passive: false });
255
+ surface.addEventListener("gesturestart", onGestureStart);
256
+ surface.addEventListener("gesturechange", onGestureChange);
257
+ return () => {
258
+ surface.removeEventListener("wheel", onWheel);
259
+ surface.removeEventListener("gesturestart", onGestureStart);
260
+ surface.removeEventListener("gesturechange", onGestureChange);
261
+ };
262
+ }, [zoomAbout, clampTransform, commit]);
263
+ const localPoint = (event) => {
264
+ const rect = event.currentTarget.getBoundingClientRect();
265
+ return { x: event.clientX - rect.left, y: event.clientY - rect.top };
266
+ };
267
+ const capture = (element, pointerId) => {
268
+ try {
269
+ element.setPointerCapture(pointerId);
270
+ } catch (e) {
271
+ }
272
+ };
273
+ const onPointerDown = (event) => {
274
+ if (event.pointerType === "mouse" && event.button !== 0) return;
275
+ if (event.target instanceof Element && event.target.closest("[data-pan-zoom-ignore]")) return;
276
+ pressing.current = true;
277
+ if (event.pointerType === "mouse") pointers.current.clear();
278
+ const point = localPoint(event);
279
+ pointers.current.set(event.pointerId, point);
280
+ if (pointers.current.size === 1) {
281
+ didDrag.current = false;
282
+ gesture.current = {
283
+ kind: "press",
284
+ id: event.pointerId,
285
+ start: point,
286
+ origin: transformRef.current
287
+ };
288
+ return;
289
+ }
290
+ if (pointers.current.size === 2) {
291
+ const [a, b] = [...pointers.current.values()];
292
+ didDrag.current = true;
293
+ for (const id of pointers.current.keys()) capture(event.currentTarget, id);
294
+ gesture.current = {
295
+ kind: "pinch",
296
+ startDistance: Math.max(distance(a, b), 1),
297
+ startMid: midpoint(a, b),
298
+ origin: transformRef.current
299
+ };
300
+ }
301
+ };
302
+ const onPointerMove = (event) => {
303
+ if (!pointers.current.has(event.pointerId)) return;
304
+ const point = localPoint(event);
305
+ pointers.current.set(event.pointerId, point);
306
+ const current = gesture.current;
307
+ if ((current.kind === "press" || current.kind === "pan") && current.id === event.pointerId) {
308
+ const dx = point.x - current.start.x;
309
+ const dy = point.y - current.start.y;
310
+ if (current.kind === "press") {
311
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
312
+ gesture.current = __spreadProps(__spreadValues({}, current), { kind: "pan" });
313
+ didDrag.current = true;
314
+ capture(event.currentTarget, event.pointerId);
315
+ }
316
+ commit(
317
+ clampTransform(__spreadProps(__spreadValues({}, current.origin), { x: current.origin.x + dx, y: current.origin.y + dy })),
318
+ { animate: false }
319
+ );
320
+ return;
321
+ }
322
+ if (current.kind === "pinch" && pointers.current.size >= 2) {
323
+ const [a, b] = [...pointers.current.values()];
324
+ const mid = midpoint(a, b);
325
+ const { origin } = current;
326
+ const scale = clamp(
327
+ origin.scale * (distance(a, b) / current.startDistance),
328
+ minScale,
329
+ maxScale
330
+ );
331
+ const anchorX = (current.startMid.x - origin.x) / origin.scale;
332
+ const anchorY = (current.startMid.y - origin.y) / origin.scale;
333
+ commit(clampTransform({ scale, x: mid.x - anchorX * scale, y: mid.y - anchorY * scale }), {
334
+ animate: false
335
+ });
336
+ }
337
+ };
338
+ const onPointerEnd = (event) => {
339
+ if (!pointers.current.delete(event.pointerId)) return;
340
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
341
+ event.currentTarget.releasePointerCapture(event.pointerId);
342
+ }
343
+ const remaining = [...pointers.current.entries()];
344
+ if (remaining.length === 1 && gesture.current.kind === "pinch") {
345
+ const [[id, point]] = remaining;
346
+ gesture.current = { kind: "pan", id, start: point, origin: transformRef.current };
347
+ return;
348
+ }
349
+ if (remaining.length === 0) {
350
+ gesture.current = { kind: "idle" };
351
+ pressing.current = false;
352
+ }
353
+ };
354
+ const onClickCapture = (event) => {
355
+ if (!didDrag.current) return;
356
+ event.preventDefault();
357
+ event.stopPropagation();
358
+ didDrag.current = false;
359
+ };
360
+ const onKeyDown = (event) => {
361
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
362
+ if (isEditable(event.target)) return;
363
+ if (event.key === "+" || event.key === "=") zoomIn();
364
+ else if (event.key === "-" || event.key === "_") zoomOut();
365
+ else if (event.key === "0") fit();
366
+ else {
367
+ if (event.target !== event.currentTarget) return;
368
+ const step = panStep * (event.shiftKey ? 4 : 1);
369
+ const move = {
370
+ ArrowLeft: { x: step, y: 0 },
371
+ ArrowRight: { x: -step, y: 0 },
372
+ ArrowUp: { x: 0, y: step },
373
+ ArrowDown: { x: 0, y: -step }
374
+ };
375
+ const delta = move[event.key];
376
+ if (!delta) return;
377
+ panBy(delta.x, delta.y);
378
+ }
379
+ event.preventDefault();
380
+ };
381
+ const onFocus = (event) => {
382
+ if (pressing.current || event.target === event.currentTarget) return;
383
+ const bounds = event.currentTarget.getBoundingClientRect();
384
+ const rect = event.target.getBoundingClientRect();
385
+ const margin = fitPadding;
386
+ let dx = 0;
387
+ let dy = 0;
388
+ if (rect.left < bounds.left + margin) dx = bounds.left + margin - rect.left;
389
+ else if (rect.right > bounds.right - margin) dx = bounds.right - margin - rect.right;
390
+ if (rect.top < bounds.top + margin) dy = bounds.top + margin - rect.top;
391
+ else if (rect.bottom > bounds.bottom - margin) dy = bounds.bottom - margin - rect.bottom;
392
+ if (dx !== 0 || dy !== 0) panBy(dx, dy);
393
+ };
394
+ const scaledWidth = contentWidth * transform.scale;
395
+ const scaledHeight = contentHeight * transform.scale;
396
+ const overflow = viewport ? {
397
+ left: transform.x < -0.5,
398
+ right: transform.x + scaledWidth > viewport.width + 0.5,
399
+ top: transform.y < -0.5,
400
+ bottom: transform.y + scaledHeight > viewport.height + 0.5
401
+ } : { left: false, right: false, top: false, bottom: false };
402
+ const atMin = transform.scale <= minScale + EPSILON;
403
+ const atMax = transform.scale >= maxScale - EPSILON;
404
+ return /* @__PURE__ */ jsxs(
405
+ "div",
406
+ __spreadProps(__spreadValues({
407
+ ref,
408
+ "data-slot": "pan-zoom-viewport",
409
+ className: cn("relative overflow-hidden", className)
410
+ }, props), {
411
+ children: [
412
+ /* @__PURE__ */ jsx(
413
+ "div",
414
+ {
415
+ ref: surfaceRef,
416
+ "data-slot": "pan-zoom-viewport-surface",
417
+ role: "group",
418
+ tabIndex: 0,
419
+ "aria-label": labels.viewport,
420
+ "aria-describedby": labels.instructions ? instructionsId : void 0,
421
+ className: cn(
422
+ "absolute inset-0 touch-none select-none outline-none",
423
+ "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
424
+ "cursor-grab active:cursor-grabbing"
425
+ ),
426
+ onPointerDown,
427
+ onPointerMove,
428
+ onPointerUp: onPointerEnd,
429
+ onPointerCancel: onPointerEnd,
430
+ onClickCapture,
431
+ onDragStart: (event) => event.preventDefault(),
432
+ onKeyDown,
433
+ onFocus,
434
+ children: /* @__PURE__ */ jsx(
435
+ "div",
436
+ {
437
+ "data-slot": "pan-zoom-viewport-content",
438
+ className: cn(
439
+ "absolute left-0 top-0 origin-top-left",
440
+ // Hidden for the one render before the first measurement, so the
441
+ // unfitted plan never flashes.
442
+ !viewport && "invisible",
443
+ animate && "transition-transform duration-[var(--duration-quick)] ease-[var(--ease-standard)] motion-reduce:transition-none",
444
+ contentClassName
445
+ ),
446
+ style: {
447
+ width: contentWidth,
448
+ height: contentHeight,
449
+ transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.scale})`
450
+ },
451
+ children
452
+ }
453
+ )
454
+ }
455
+ ),
456
+ [
457
+ ["left", "inset-y-0 left-0 w-6 bg-gradient-to-r"],
458
+ ["right", "inset-y-0 right-0 w-6 bg-gradient-to-l"],
459
+ ["top", "inset-x-0 top-0 h-6 bg-gradient-to-b"],
460
+ ["bottom", "inset-x-0 bottom-0 h-6 bg-gradient-to-t"]
461
+ ].map(([edge, position]) => /* @__PURE__ */ jsx(
462
+ "div",
463
+ {
464
+ "aria-hidden": true,
465
+ "data-slot": "pan-zoom-viewport-edge",
466
+ "data-edge": edge,
467
+ "data-visible": overflow[edge] ? "true" : "false",
468
+ className: cn(
469
+ "pointer-events-none absolute from-foreground/10 to-transparent transition-opacity duration-[var(--duration-quick)] motion-reduce:transition-none",
470
+ position,
471
+ overflow[edge] ? "opacity-100" : "opacity-0"
472
+ )
473
+ },
474
+ edge
475
+ )),
476
+ controls ? /* @__PURE__ */ jsxs(
477
+ ButtonGroup,
478
+ {
479
+ orientation: "vertical",
480
+ "aria-label": labels.controls,
481
+ "data-slot": "pan-zoom-viewport-controls",
482
+ className: "absolute bottom-3 right-3 rounded-xl bg-card shadow-sm",
483
+ children: [
484
+ /* @__PURE__ */ jsx(
485
+ Button,
486
+ {
487
+ type: "button",
488
+ variant: "outline",
489
+ size: "icon-sm",
490
+ "aria-label": labels.zoomIn,
491
+ disabled: atMax,
492
+ onClick: zoomIn,
493
+ children: /* @__PURE__ */ jsx(Plus, { "aria-hidden": true })
494
+ }
495
+ ),
496
+ /* @__PURE__ */ jsx(
497
+ Button,
498
+ {
499
+ type: "button",
500
+ variant: "outline",
501
+ size: "icon-sm",
502
+ "aria-label": labels.zoomOut,
503
+ disabled: atMin,
504
+ onClick: zoomOut,
505
+ children: /* @__PURE__ */ jsx(Minus, { "aria-hidden": true })
506
+ }
507
+ ),
508
+ /* @__PURE__ */ jsx(
509
+ Button,
510
+ {
511
+ type: "button",
512
+ variant: "outline",
513
+ size: "icon-sm",
514
+ className: "text-xs font-semibold",
515
+ onClick: fit,
516
+ children: labels.fit
517
+ }
518
+ )
519
+ ]
520
+ }
521
+ ) : null,
522
+ labels.instructions ? /* @__PURE__ */ jsx("span", { id: instructionsId, className: "sr-only", children: labels.instructions }) : null,
523
+ labels.zoomLevel ? /* @__PURE__ */ jsx("span", { className: "sr-only", "aria-live": "polite", children: announcement }) : null
524
+ ]
525
+ })
526
+ );
527
+ }
528
+ );
529
+ PanZoomViewport.displayName = "PanZoomViewport";
530
+
531
+ export { PanZoomViewport };
@@ -1,12 +1,12 @@
1
+ import { NEUTRAL_ICON_FILTER } from '../chunk-LQWGZIYA.js';
1
2
  import { OVERLAY_PILL } from '../chunk-EZG2NELX.js';
2
3
  import { HOVER_IMAGE_MOTION, HOVER_IMAGE_WRAPPER, HOVER_MOTION, HOVER_LIFT_GRID_GROUP } from '../chunk-XCB3F6SF.js';
3
- import { NEUTRAL_ICON_FILTER } from '../chunk-LQWGZIYA.js';
4
4
  import { GradeAtom } from '../chunk-YE4ZQUTI.js';
5
5
  import { showsGradeAtom } from '../chunk-I5V446BX.js';
6
6
  import { Separator } from '../chunk-NWZGSLPU.js';
7
7
  import { springSnappy } from '../chunk-OIV7B44M.js';
8
8
  import { Skeleton } from '../chunk-XBBEX4SF.js';
9
- import { Badge } from '../chunk-NIAL4ZU2.js';
9
+ import { Badge } from '../chunk-FQAIYKAS.js';
10
10
  import { cn } from '../chunk-FEK5XGZW.js';
11
11
  import { __objRest, __spreadValues } from '../chunk-MMUBH76A.js';
12
12
  import { forwardRef, memo, useState, useMemo } from 'react';
@@ -3,7 +3,7 @@ import '../chunk-FMTJGO76.js';
3
3
  import '../chunk-MWB7S54O.js';
4
4
  import '../chunk-NWZGSLPU.js';
5
5
  import { Text } from '../chunk-ZLMRVBD7.js';
6
- import { Badge } from '../chunk-NIAL4ZU2.js';
6
+ import { Badge } from '../chunk-FQAIYKAS.js';
7
7
  import { cn } from '../chunk-FEK5XGZW.js';
8
8
  import '../chunk-MMUBH76A.js';
9
9
  import { useState } from 'react';