@bearmenu/ui 0.8.35 → 0.8.37

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 (76) 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-TVEIUTBQ.js → chunk-IQIJCMG2.js} +1 -1
  4. package/dist/{chunk-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
  5. package/dist/chunk-LAEVMNJN.js +135 -0
  6. package/dist/{chunk-VCRXL7H5.js → chunk-PGQXVJXW.js} +2 -2
  7. package/dist/{chunk-F3XK34DV.js → chunk-TB2K5JN3.js} +2 -2
  8. package/dist/{chunk-XNBDACL7.js → chunk-ZUBBMLBD.js} +1 -1
  9. package/dist/components/badge.d.ts +8 -2
  10. package/dist/components/badge.js +1 -1
  11. package/dist/components/banner.d.ts +17 -2
  12. package/dist/components/banner.js +10 -7
  13. package/dist/components/booking-row.js +1 -1
  14. package/dist/components/button-group.js +3 -39
  15. package/dist/components/calendar.js +1 -1
  16. package/dist/components/combobox.js +3 -3
  17. package/dist/components/command.js +2 -2
  18. package/dist/components/concept-convergence.js +1 -1
  19. package/dist/components/dialog.js +1 -1
  20. package/dist/components/dropdown-menu.js +6 -6
  21. package/dist/components/filter-category-row.js +1 -1
  22. package/dist/components/input-group.js +1 -1
  23. package/dist/components/menu-item.js +3 -3
  24. package/dist/components/multi-select-combobox.js +10 -5
  25. package/dist/components/multi-select-menu.js +1 -1
  26. package/dist/components/pan-zoom-viewport.d.ts +137 -0
  27. package/dist/components/pan-zoom-viewport.js +531 -0
  28. package/dist/components/place-card.js +2 -2
  29. package/dist/components/place-details-mobile.js +1 -1
  30. package/dist/components/responsive-overlay.d.ts +29 -1
  31. package/dist/components/responsive-overlay.js +46 -7
  32. package/dist/components/searchable-select.js +3 -3
  33. package/dist/components/select.js +2 -2
  34. package/dist/components/selection-bar.js +1 -1
  35. package/dist/components/sheet.d.ts +6 -0
  36. package/dist/components/sheet.js +3 -133
  37. package/dist/components/signature-chip.js +2 -2
  38. package/dist/components/tabs.d.ts +16 -1
  39. package/dist/components/tabs.js +21 -3
  40. package/dist/components/toggle-group.js +1 -1
  41. package/dist/components/toggle.d.ts +6 -0
  42. package/dist/components/toggle.js +1 -1
  43. package/dist/components/top-ranking-entry.js +3 -3
  44. package/package.json +1 -1
  45. package/src/components/badge.stories.tsx +32 -1
  46. package/src/components/badge.test.tsx +45 -0
  47. package/src/components/badge.tsx +41 -3
  48. package/src/components/banner.stories.tsx +32 -0
  49. package/src/components/banner.test.tsx +58 -0
  50. package/src/components/banner.tsx +28 -4
  51. package/src/components/calendar.tsx +1 -1
  52. package/src/components/combobox.tsx +1 -1
  53. package/src/components/command.test.tsx +10 -0
  54. package/src/components/command.tsx +1 -1
  55. package/src/components/dialog.tsx +1 -1
  56. package/src/components/dropdown-menu.test.tsx +31 -0
  57. package/src/components/dropdown-menu.tsx +6 -6
  58. package/src/components/input-group.tsx +1 -1
  59. package/src/components/multi-select-combobox.tsx +7 -1
  60. package/src/components/multi-select-menu.tsx +1 -1
  61. package/src/components/pan-zoom-viewport.stories.tsx +134 -0
  62. package/src/components/pan-zoom-viewport.test.tsx +381 -0
  63. package/src/components/pan-zoom-viewport.tsx +727 -0
  64. package/src/components/responsive-overlay.stories.tsx +61 -0
  65. package/src/components/responsive-overlay.test.tsx +104 -0
  66. package/src/components/responsive-overlay.tsx +87 -22
  67. package/src/components/searchable-select.tsx +1 -1
  68. package/src/components/select.tsx +2 -2
  69. package/src/components/selection-bar.tsx +1 -1
  70. package/src/components/sheet.tsx +14 -3
  71. package/src/components/tabs.stories.tsx +23 -0
  72. package/src/components/tabs.test.tsx +64 -0
  73. package/src/components/tabs.tsx +36 -3
  74. package/src/components/toggle.test.tsx +19 -0
  75. package/src/components/toggle.tsx +8 -2
  76. package/src/globals.css +28 -19
@@ -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';
@@ -23,6 +23,32 @@ type ResponsiveOverlayProps = {
23
23
  closeLabel?: string;
24
24
  /** Widths for the desktop dialog. Mobile is always the full-width drawer. */
25
25
  size?: React.ComponentPropsWithoutRef<typeof DialogContent>["size"];
26
+ /**
27
+ * The container on a wide screen. `dialog` (the default) is the centred box.
28
+ * `sheet` is a right rail: an inset panel held 12px off the top, right and
29
+ * bottom, 420px wide — the booking pages' table sheet, booking details,
30
+ * guest panel and create forms (floor/14-w1, calendar/07-n1, guests/07-g6).
31
+ * Narrow screens get the bottom drawer either way.
32
+ */
33
+ desktop?: "dialog" | "sheet";
34
+ /**
35
+ * Whether the wide `sheet` dims and locks the page. `true` (the default)
36
+ * is a form rail: a scrim, focus held inside, the page inert. `false` is a
37
+ * working rail: no scrim, no focus trap, and the page behind stays live —
38
+ * clicking another table on the floor does NOT close it, so the caller can
39
+ * swap what it shows. Escape and the close button still close it.
40
+ *
41
+ * Applies to `desktop="sheet"` only. The dialog is always modal, and so is
42
+ * the phone drawer: at 390 the rail covers most of the page, and a page
43
+ * that stays live under a sheet the thumb cannot see past is a trap.
44
+ */
45
+ modal?: boolean;
46
+ /**
47
+ * Where the wide container takes over from the drawer. `md` (768, the
48
+ * default) for most overlays; `lg` (1024) where the page beside the rail
49
+ * needs the room — the floor plan's rails switch there.
50
+ */
51
+ wideFrom?: "md" | "lg";
26
52
  /** Classes for the panel itself — the dialog box or the drawer sheet. */
27
53
  className?: string;
28
54
  /** Classes for the scrolling body. */
@@ -30,7 +56,9 @@ type ResponsiveOverlayProps = {
30
56
  };
31
57
  /**
32
58
  * ResponsiveOverlay — one overlay authored once: a `Drawer` under md, a
33
- * `Dialog` from md.
59
+ * `Dialog` from md — or, with `desktop="sheet"`, an inset right rail that can
60
+ * leave the page live (`modal={false}`), switching at md or at lg
61
+ * (`wideFrom`).
34
62
  *
35
63
  * The shared-table board draws the same chrome on T1, T3, T5, T6 and T8
36
64
  * (16-shared-table.html) and the two widths never disagree about it: the
@@ -1,13 +1,20 @@
1
- import { useMediaQuery } from '../chunk-OPVKL4VS.js';
1
+ import { useMediaQuery, DESKTOP_MEDIA_QUERY } from '../chunk-OPVKL4VS.js';
2
+ import { Sheet, SheetContent, SheetTitle, SheetDescription } from '../chunk-LAEVMNJN.js';
2
3
  import { Drawer, DrawerContent, DrawerTitle, DrawerDescription } from '../chunk-XLHMF6F5.js';
3
- import { Dialog, DialogContent, DialogTitle, DialogDescription } from '../chunk-TVEIUTBQ.js';
4
+ import { Dialog, DialogContent, DialogTitle, DialogDescription } from '../chunk-IQIJCMG2.js';
4
5
  import { cn } from '../chunk-FEK5XGZW.js';
5
- import '../chunk-MMUBH76A.js';
6
+ import { __spreadProps, __spreadValues } from '../chunk-MMUBH76A.js';
6
7
  import * as React from 'react';
7
8
  import { X } from 'lucide-react';
8
9
  import { jsx, jsxs } from 'react/jsx-runtime';
9
10
 
10
11
  var MD_MEDIA_QUERY = "(min-width: 768px)";
12
+ var CONTAINER_TITLE = { drawer: DrawerTitle, dialog: DialogTitle, sheet: SheetTitle };
13
+ var CONTAINER_DESCRIPTION = {
14
+ drawer: DrawerDescription,
15
+ dialog: DialogDescription,
16
+ sheet: SheetDescription
17
+ };
11
18
  function useFocusReturn(open) {
12
19
  const openerRef = React.useRef(null);
13
20
  const wasOpen = React.useRef(false);
@@ -44,17 +51,23 @@ var ResponsiveOverlay = React.forwardRef(
44
51
  footer,
45
52
  closeLabel = "Close",
46
53
  size = "lg",
54
+ desktop = "dialog",
55
+ modal = true,
56
+ wideFrom = "md",
47
57
  className,
48
58
  bodyClassName
49
59
  }, ref) => {
50
- const isWide = useMediaQuery(MD_MEDIA_QUERY);
60
+ const isWide = useMediaQuery(wideFrom === "lg" ? DESKTOP_MEDIA_QUERY : MD_MEDIA_QUERY);
61
+ const container = isWide ? desktop : "drawer";
51
62
  const titleId = React.useId();
52
63
  const subtitleId = React.useId();
53
64
  useFocusReturn(open);
65
+ const Title = CONTAINER_TITLE[container];
66
+ const Description = CONTAINER_DESCRIPTION[container];
54
67
  const header = /* @__PURE__ */ jsxs("div", { "data-slot": "responsive-overlay-header", className: "flex items-start gap-3 px-5 pt-5", children: [
55
68
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
56
- isWide ? /* @__PURE__ */ jsx(DialogTitle, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }) : /* @__PURE__ */ jsx(DrawerTitle, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }),
57
- subtitle ? isWide ? /* @__PURE__ */ jsx(DialogDescription, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : /* @__PURE__ */ jsx(DrawerDescription, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
69
+ /* @__PURE__ */ jsx(Title, { id: titleId, className: "text-[19px] font-bold leading-tight", children: title }),
70
+ subtitle ? /* @__PURE__ */ jsx(Description, { id: subtitleId, className: "mt-1 text-[13px] text-muted-foreground", children: subtitle }) : null
58
71
  ] }),
59
72
  /* @__PURE__ */ jsxs(
60
73
  "button",
@@ -86,7 +99,33 @@ var ResponsiveOverlay = React.forwardRef(
86
99
  children: footer
87
100
  }
88
101
  ) : null;
89
- if (isWide) {
102
+ if (container === "sheet") {
103
+ return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, modal, children: /* @__PURE__ */ jsxs(
104
+ SheetContent,
105
+ __spreadProps(__spreadValues({
106
+ ref,
107
+ side: "right",
108
+ attachment: "inset",
109
+ hideClose: true,
110
+ overlayClassName: "bg-scrim-overlay backdrop-blur-none",
111
+ "aria-labelledby": titleId,
112
+ "aria-describedby": subtitle ? subtitleId : void 0,
113
+ "data-slot": "responsive-overlay-sheet",
114
+ "data-modal": modal ? "true" : "false"
115
+ }, modal ? {} : { onInteractOutside: (event) => event.preventDefault() }), {
116
+ className: cn(
117
+ "flex w-[420px] max-w-[calc(100vw-1.5rem)] flex-col gap-0 bg-card p-0",
118
+ className
119
+ ),
120
+ children: [
121
+ header,
122
+ body,
123
+ foot
124
+ ]
125
+ })
126
+ ) });
127
+ }
128
+ if (container === "dialog") {
90
129
  return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
91
130
  DialogContent,
92
131
  {