@bearmenu/ui 0.8.35 → 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.
- package/dist/chunk-EGU6EG73.js +39 -0
- package/dist/{chunk-NIAL4ZU2.js → chunk-FQAIYKAS.js} +45 -8
- package/dist/{chunk-6W25S5CF.js → chunk-KGXTBTCJ.js} +1 -1
- package/dist/chunk-LAEVMNJN.js +135 -0
- package/dist/{chunk-XNBDACL7.js → chunk-ZUBBMLBD.js} +1 -1
- package/dist/components/badge.d.ts +8 -2
- package/dist/components/badge.js +1 -1
- package/dist/components/banner.d.ts +17 -2
- package/dist/components/banner.js +10 -7
- package/dist/components/booking-row.js +1 -1
- package/dist/components/button-group.js +3 -39
- package/dist/components/button.d.ts +2 -2
- package/dist/components/command.d.ts +3 -3
- package/dist/components/concept-convergence.js +1 -1
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/multi-select-combobox.js +8 -3
- package/dist/components/pan-zoom-viewport.d.ts +137 -0
- package/dist/components/pan-zoom-viewport.js +531 -0
- package/dist/components/place-card.js +2 -2
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/responsive-overlay.d.ts +29 -1
- package/dist/components/responsive-overlay.js +45 -6
- package/dist/components/sheet.d.ts +6 -0
- package/dist/components/sheet.js +3 -133
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tabs.d.ts +16 -1
- package/dist/components/tabs.js +21 -3
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +2 -2
- package/dist/components/top-ranking-entry.js +3 -3
- package/package.json +1 -1
- package/src/components/badge.stories.tsx +32 -1
- package/src/components/badge.test.tsx +45 -0
- package/src/components/badge.tsx +41 -3
- package/src/components/banner.stories.tsx +32 -0
- package/src/components/banner.test.tsx +58 -0
- package/src/components/banner.tsx +28 -4
- package/src/components/multi-select-combobox.tsx +7 -1
- package/src/components/pan-zoom-viewport.stories.tsx +134 -0
- package/src/components/pan-zoom-viewport.test.tsx +381 -0
- package/src/components/pan-zoom-viewport.tsx +727 -0
- package/src/components/responsive-overlay.stories.tsx +61 -0
- package/src/components/responsive-overlay.test.tsx +104 -0
- package/src/components/responsive-overlay.tsx +87 -22
- package/src/components/sheet.tsx +14 -3
- package/src/components/tabs.stories.tsx +23 -0
- package/src/components/tabs.test.tsx +64 -0
- 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-
|
|
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-
|
|
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';
|