@cratis/components 4.25.1 → 4.25.2

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.
@@ -68,7 +68,7 @@
68
68
  {
69
69
  "name": "@cratis/components",
70
70
  "role": "core",
71
- "version": "4.25.1",
71
+ "version": "4.25.2",
72
72
  "releaseMajorRange": ">=4 <5",
73
73
  "independentRelease": false,
74
74
  "private": false,
@@ -92,7 +92,7 @@
92
92
  {
93
93
  "name": "@cratis/eslint-plugin-components",
94
94
  "role": "eslint",
95
- "version": "4.25.1",
95
+ "version": "4.25.2",
96
96
  "releaseMajorRange": ">=4 <5",
97
97
  "independentRelease": false,
98
98
  "private": false,
@@ -105,7 +105,7 @@
105
105
  {
106
106
  "name": "@cratis/components.migrator",
107
107
  "role": "migrator",
108
- "version": "4.25.1",
108
+ "version": "4.25.2",
109
109
  "releaseMajorRange": ">=4 <5",
110
110
  "independentRelease": false,
111
111
  "private": false,
@@ -116,13 +116,13 @@
116
116
  {
117
117
  "name": "@cratis/components.conformance",
118
118
  "role": "conformance",
119
- "version": "4.25.1",
119
+ "version": "4.25.2",
120
120
  "releaseMajorRange": ">=4 <5",
121
121
  "independentRelease": false,
122
122
  "private": false,
123
123
  "packageAccess": "public",
124
124
  "peerDependencies": {
125
- "@cratis/components": "4.25.1",
125
+ "@cratis/components": "4.25.2",
126
126
  "react": "^19.0.0",
127
127
  "react-dom": "^19.0.0"
128
128
  },
@@ -132,13 +132,13 @@
132
132
  {
133
133
  "name": "@cratis/components.mui",
134
134
  "role": "renderer-adapter",
135
- "version": "4.25.1",
135
+ "version": "4.25.2",
136
136
  "releaseMajorRange": ">=4 <5",
137
137
  "independentRelease": false,
138
138
  "private": false,
139
139
  "packageAccess": "public",
140
140
  "peerDependencies": {
141
- "@cratis/components": "4.25.1",
141
+ "@cratis/components": "4.25.2",
142
142
  "@emotion/react": ">=11.5 <12",
143
143
  "@emotion/styled": ">=11.11 <12",
144
144
  "@mui/material": ">=9 <10",
@@ -188,13 +188,13 @@
188
188
  {
189
189
  "name": "@cratis/components.primereact",
190
190
  "role": "renderer-adapter",
191
- "version": "4.25.1",
191
+ "version": "4.25.2",
192
192
  "releaseMajorRange": ">=4 <5",
193
193
  "independentRelease": false,
194
194
  "private": false,
195
195
  "packageAccess": "public",
196
196
  "peerDependencies": {
197
- "@cratis/components": "4.25.1",
197
+ "@cratis/components": "4.25.2",
198
198
  "@primereact/core": ">=11 <12",
199
199
  "@primereact/ui": ">=11 <12",
200
200
  "@primeuix/themes": ">=3 <4",
@@ -256,13 +256,13 @@
256
256
  {
257
257
  "name": "@cratis/components.primereact10",
258
258
  "role": "renderer-adapter",
259
- "version": "4.25.1",
259
+ "version": "4.25.2",
260
260
  "releaseMajorRange": ">=4 <5",
261
261
  "independentRelease": false,
262
262
  "private": false,
263
263
  "packageAccess": "public",
264
264
  "peerDependencies": {
265
- "@cratis/components": "4.25.1",
265
+ "@cratis/components": "4.25.2",
266
266
  "primereact": ">=10.9.9 <11",
267
267
  "react": "^19.0.0",
268
268
  "react-dom": "^19.0.0"
@@ -0,0 +1,30 @@
1
+ import type { PointerPosition } from './pinchGesture.js';
2
+ /** The camera state and operations {@link attachCanvasZoomListeners} drives. */
3
+ export interface CanvasZoomListenerOptions {
4
+ container: HTMLElement;
5
+ /** Cancels a still-coasting touch pan when a fresh gesture starts. */
6
+ cancelMomentum: () => void;
7
+ getPan: () => {
8
+ x: number;
9
+ y: number;
10
+ };
11
+ setPan: (pan: {
12
+ x: number;
13
+ y: number;
14
+ }) => void;
15
+ /** Touch pointers currently down; a touch pinch owns zooming while any are. */
16
+ touchPointers: ReadonlyMap<number, PointerPosition>;
17
+ zoomTowards: (focusX: number, focusY: number, factor: number) => void;
18
+ noteGestureActivity: () => void;
19
+ scheduleTransformApply: () => void;
20
+ }
21
+ /**
22
+ * Wires wheel pan, Ctrl/Cmd+wheel zoom (a trackpad pinch in Chrome/Firefox/Edge, and Safari's
23
+ * synthesized equivalent) and Safari/iPadOS gesture events to the canvas camera, and cancels the
24
+ * browser's own page zoom for all of them. A real touch pinch is handled elsewhere from pointer
25
+ * events; here, with touch pointers down, the gesture events only have their page zoom cancelled.
26
+ * @param options The container, camera state and operations.
27
+ * @returns A function removing every listener.
28
+ */
29
+ export declare const attachCanvasZoomListeners: ({ container, cancelMomentum, getPan, setPan, touchPointers, zoomTowards, noteGestureActivity, scheduleTransformApply, }: CanvasZoomListenerOptions) => (() => void);
30
+ //# sourceMappingURL=canvasZoomListeners.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasZoomListeners.d.ts","sourceRoot":"","sources":["../../../Canvas/canvasZoomListeners.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAgB,CAAC;AAGtD,gFAAgF;AAChF,MAAM,WAAW,yBAAyB;IACtC,SAAS,EAAE,WAAW,CAAC;IACvB,sEAAsE;IACtE,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,MAAM,EAAE,CAAC,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChD,+EAA+E;IAC/E,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACpD,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACtC;AAKD;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,4HASnC,yBAAyB,KAAG,CAAC,MAAM,IAAI,CA4IzC,CAAC"}
@@ -0,0 +1,121 @@
1
+ import { ZOOM_INTENSITY } from './canvasGestureTuning.js';
2
+ import { isWithinScrollableContent } from './isWithinScrollableContent.js';
3
+
4
+ const isFinitePoint = (x, y) => typeof x === 'number' && typeof y === 'number' && Number.isFinite(x) && Number.isFinite(y);
5
+ const attachCanvasZoomListeners = ({ container, cancelMomentum, getPan, setPan, touchPointers, zoomTowards, noteGestureActivity, scheduleTransformApply, }) => {
6
+ let lastPointer = null;
7
+ const trackPointer = (event) => {
8
+ if (isFinitePoint(event.clientX, event.clientY)) {
9
+ lastPointer = { x: event.clientX, y: event.clientY };
10
+ }
11
+ };
12
+ const gestureFocus = (event) => {
13
+ const gesture = event;
14
+ if (isFinitePoint(gesture.clientX, gesture.clientY)) {
15
+ return { x: gesture.clientX, y: gesture.clientY };
16
+ }
17
+ if (lastPointer)
18
+ return lastPointer;
19
+ const rect = container.getBoundingClientRect();
20
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
21
+ };
22
+ const isWithinCanvas = (point) => {
23
+ const rect = container.getBoundingClientRect();
24
+ return (point.x >= rect.left &&
25
+ point.x <= rect.right &&
26
+ point.y >= rect.top &&
27
+ point.y <= rect.bottom);
28
+ };
29
+ const handleWheel = (event) => {
30
+ if (!(event.ctrlKey || event.metaKey) &&
31
+ isWithinScrollableContent(event.target, container))
32
+ return;
33
+ event.preventDefault();
34
+ cancelMomentum();
35
+ const rect = container.getBoundingClientRect();
36
+ if (event.ctrlKey || event.metaKey) {
37
+ zoomTowards(event.clientX - rect.left, event.clientY - rect.top, Math.exp(-event.deltaY * ZOOM_INTENSITY));
38
+ }
39
+ else {
40
+ const pan = getPan();
41
+ setPan({ x: pan.x - event.deltaX, y: pan.y - event.deltaY });
42
+ noteGestureActivity();
43
+ scheduleTransformApply();
44
+ }
45
+ };
46
+ let lastScale = 1;
47
+ const applyGestureChange = (event, focus) => {
48
+ if (touchPointers.size > 0)
49
+ return;
50
+ const gesture = event;
51
+ if (!gesture.scale || !lastScale)
52
+ return;
53
+ const rect = container.getBoundingClientRect();
54
+ zoomTowards(focus.x - rect.left, focus.y - rect.top, gesture.scale / lastScale);
55
+ lastScale = gesture.scale;
56
+ };
57
+ const handleGestureStart = (event) => {
58
+ event.preventDefault();
59
+ lastScale = event.scale || 1;
60
+ };
61
+ const handleGestureChange = (event) => {
62
+ event.preventDefault();
63
+ applyGestureChange(event, gestureFocus(event));
64
+ };
65
+ const handleGestureEnd = (event) => event.preventDefault();
66
+ const reachesContainerListeners = (event) => event.target instanceof Node && container.contains(event.target);
67
+ const handleWindowWheel = (event) => {
68
+ if ((event.ctrlKey || event.metaKey) &&
69
+ isWithinCanvas({ x: event.clientX, y: event.clientY })) {
70
+ event.preventDefault();
71
+ }
72
+ };
73
+ const handleWindowGestureStart = (event) => {
74
+ if (!isWithinCanvas(gestureFocus(event)))
75
+ return;
76
+ event.preventDefault();
77
+ lastScale = event.scale || 1;
78
+ };
79
+ const handleWindowGestureChange = (event) => {
80
+ const focus = gestureFocus(event);
81
+ if (!isWithinCanvas(focus))
82
+ return;
83
+ event.preventDefault();
84
+ if (reachesContainerListeners(event))
85
+ return;
86
+ applyGestureChange(event, focus);
87
+ };
88
+ const handleWindowGestureEnd = (event) => {
89
+ if (isWithinCanvas(gestureFocus(event)))
90
+ event.preventDefault();
91
+ };
92
+ const nonPassive = { passive: false };
93
+ const trackingOptions = { passive: true, capture: true };
94
+ container.addEventListener('wheel', handleWheel, nonPassive);
95
+ container.addEventListener('gesturestart', handleGestureStart, nonPassive);
96
+ container.addEventListener('gesturechange', handleGestureChange, nonPassive);
97
+ container.addEventListener('gestureend', handleGestureEnd, nonPassive);
98
+ window.addEventListener('wheel', handleWindowWheel, nonPassive);
99
+ window.addEventListener('gesturestart', handleWindowGestureStart, nonPassive);
100
+ window.addEventListener('gesturechange', handleWindowGestureChange, nonPassive);
101
+ window.addEventListener('gestureend', handleWindowGestureEnd, nonPassive);
102
+ window.addEventListener('pointermove', trackPointer, trackingOptions);
103
+ window.addEventListener('pointerdown', trackPointer, trackingOptions);
104
+ window.addEventListener('wheel', trackPointer, trackingOptions);
105
+ return () => {
106
+ container.removeEventListener('wheel', handleWheel);
107
+ container.removeEventListener('gesturestart', handleGestureStart);
108
+ container.removeEventListener('gesturechange', handleGestureChange);
109
+ container.removeEventListener('gestureend', handleGestureEnd);
110
+ window.removeEventListener('wheel', handleWindowWheel);
111
+ window.removeEventListener('gesturestart', handleWindowGestureStart);
112
+ window.removeEventListener('gesturechange', handleWindowGestureChange);
113
+ window.removeEventListener('gestureend', handleWindowGestureEnd);
114
+ window.removeEventListener('pointermove', trackPointer, trackingOptions);
115
+ window.removeEventListener('pointerdown', trackPointer, trackingOptions);
116
+ window.removeEventListener('wheel', trackPointer, trackingOptions);
117
+ };
118
+ };
119
+
120
+ export { attachCanvasZoomListeners };
121
+ //# sourceMappingURL=canvasZoomListeners.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasZoomListeners.js","sources":["../../../Canvas/canvasZoomListeners.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { ZOOM_INTENSITY } from './canvasGestureTuning';\nimport { isWithinScrollableContent } from './isWithinScrollableContent';\nimport type { PointerPosition } from './pinchGesture';\nimport type { WebKitGestureEvent } from './WebKitGestureEvent';\n\n/** The camera state and operations {@link attachCanvasZoomListeners} drives. */\nexport interface CanvasZoomListenerOptions {\n container: HTMLElement;\n /** Cancels a still-coasting touch pan when a fresh gesture starts. */\n cancelMomentum: () => void;\n getPan: () => { x: number; y: number };\n setPan: (pan: { x: number; y: number }) => void;\n /** Touch pointers currently down; a touch pinch owns zooming while any are. */\n touchPointers: ReadonlyMap<number, PointerPosition>;\n zoomTowards: (focusX: number, focusY: number, factor: number) => void;\n noteGestureActivity: () => void;\n scheduleTransformApply: () => void;\n}\n\nconst isFinitePoint = (x: unknown, y: unknown): boolean =>\n typeof x === 'number' && typeof y === 'number' && Number.isFinite(x) && Number.isFinite(y);\n\n/**\n * Wires wheel pan, Ctrl/Cmd+wheel zoom (a trackpad pinch in Chrome/Firefox/Edge, and Safari's\n * synthesized equivalent) and Safari/iPadOS gesture events to the canvas camera, and cancels the\n * browser's own page zoom for all of them. A real touch pinch is handled elsewhere from pointer\n * events; here, with touch pointers down, the gesture events only have their page zoom cancelled.\n * @param options The container, camera state and operations.\n * @returns A function removing every listener.\n */\nexport const attachCanvasZoomListeners = ({\n container,\n cancelMomentum,\n getPan,\n setPan,\n touchPointers,\n zoomTowards,\n noteGestureActivity,\n scheduleTransformApply,\n}: CanvasZoomListenerOptions): (() => void) => {\n // The last place a pointer was seen in the viewport. iPadOS reports its gesture events without\n // coordinates, and a NaN focus would poison the camera and defeat the \"is it over the canvas\"\n // test, so a missing position falls back to this, then to the canvas centre.\n let lastPointer: PointerPosition | null = null;\n const trackPointer = (event: PointerEvent | MouseEvent) => {\n if (isFinitePoint(event.clientX, event.clientY)) {\n lastPointer = { x: event.clientX, y: event.clientY };\n }\n };\n\n const gestureFocus = (event: Event): PointerPosition => {\n const gesture = event as WebKitGestureEvent;\n if (isFinitePoint(gesture.clientX, gesture.clientY)) {\n return { x: gesture.clientX, y: gesture.clientY };\n }\n if (lastPointer) return lastPointer;\n const rect = container.getBoundingClientRect();\n return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };\n };\n\n const isWithinCanvas = (point: PointerPosition) => {\n const rect = container.getBoundingClientRect();\n return (\n point.x >= rect.left &&\n point.x <= rect.right &&\n point.y >= rect.top &&\n point.y <= rect.bottom\n );\n };\n\n const handleWheel = (event: WheelEvent) => {\n // A plain scroll landing over a scrollable overlay (a chat's message list and similar)\n // scrolls that content instead of panning the board. A zoom gesture (ctrl/cmd) always wins.\n if (\n !(event.ctrlKey || event.metaKey) &&\n isWithinScrollableContent(event.target, container)\n )\n return;\n\n event.preventDefault();\n cancelMomentum();\n\n const rect = container.getBoundingClientRect();\n if (event.ctrlKey || event.metaKey) {\n zoomTowards(\n event.clientX - rect.left,\n event.clientY - rect.top,\n Math.exp(-event.deltaY * ZOOM_INTENSITY),\n );\n } else {\n const pan = getPan();\n setPan({ x: pan.x - event.deltaX, y: pan.y - event.deltaY });\n noteGestureActivity();\n scheduleTransformApply();\n }\n };\n\n // Safari/WebKit reports a trackpad pinch (including iPadOS with a Magic Keyboard) through its\n // own gesture events, never as wheel+ctrlKey. Cancelling the default stops the page zoom; the\n // change event also has to drive the zoom or the pinch would be swallowed entirely.\n let lastScale = 1;\n const applyGestureChange = (event: Event, focus: PointerPosition) => {\n if (touchPointers.size > 0) return;\n const gesture = event as WebKitGestureEvent;\n if (!gesture.scale || !lastScale) return;\n\n const rect = container.getBoundingClientRect();\n zoomTowards(focus.x - rect.left, focus.y - rect.top, gesture.scale / lastScale);\n lastScale = gesture.scale;\n };\n const handleGestureStart = (event: Event) => {\n event.preventDefault();\n lastScale = (event as WebKitGestureEvent).scale || 1;\n };\n const handleGestureChange = (event: Event) => {\n event.preventDefault();\n applyGestureChange(event, gestureFocus(event));\n };\n const handleGestureEnd = (event: Event) => event.preventDefault();\n\n // Floating content over the canvas (toolbars, panels, the minimap) is often portalled out of the\n // container's subtree, so its events never reach the listeners above and the browser would zoom\n // the page. Listen on the window as well, going purely by screen position. For targets inside\n // the container this only cancels the browser zoom — the container listeners drive the zoom.\n const reachesContainerListeners = (event: Event) =>\n event.target instanceof Node && container.contains(event.target);\n const handleWindowWheel = (event: WheelEvent) => {\n if (\n (event.ctrlKey || event.metaKey) &&\n isWithinCanvas({ x: event.clientX, y: event.clientY })\n ) {\n event.preventDefault();\n }\n };\n const handleWindowGestureStart = (event: Event) => {\n if (!isWithinCanvas(gestureFocus(event))) return;\n event.preventDefault();\n lastScale = (event as WebKitGestureEvent).scale || 1;\n };\n const handleWindowGestureChange = (event: Event) => {\n const focus = gestureFocus(event);\n if (!isWithinCanvas(focus)) return;\n event.preventDefault();\n if (reachesContainerListeners(event)) return;\n applyGestureChange(event, focus);\n };\n const handleWindowGestureEnd = (event: Event) => {\n if (isWithinCanvas(gestureFocus(event))) event.preventDefault();\n };\n\n // Everything is explicitly non-passive: WebKit treating an options-less listener as passive\n // silently ignores preventDefault(), which is what lets the page zoom through.\n const nonPassive = { passive: false } as const;\n const trackingOptions = { passive: true, capture: true } as const;\n container.addEventListener('wheel', handleWheel, nonPassive);\n container.addEventListener('gesturestart', handleGestureStart, nonPassive);\n container.addEventListener('gesturechange', handleGestureChange, nonPassive);\n container.addEventListener('gestureend', handleGestureEnd, nonPassive);\n window.addEventListener('wheel', handleWindowWheel, nonPassive);\n window.addEventListener('gesturestart', handleWindowGestureStart, nonPassive);\n window.addEventListener('gesturechange', handleWindowGestureChange, nonPassive);\n window.addEventListener('gestureend', handleWindowGestureEnd, nonPassive);\n window.addEventListener('pointermove', trackPointer, trackingOptions);\n window.addEventListener('pointerdown', trackPointer, trackingOptions);\n window.addEventListener('wheel', trackPointer, trackingOptions);\n\n return () => {\n container.removeEventListener('wheel', handleWheel);\n container.removeEventListener('gesturestart', handleGestureStart);\n container.removeEventListener('gesturechange', handleGestureChange);\n container.removeEventListener('gestureend', handleGestureEnd);\n window.removeEventListener('wheel', handleWindowWheel);\n window.removeEventListener('gesturestart', handleWindowGestureStart);\n window.removeEventListener('gesturechange', handleWindowGestureChange);\n window.removeEventListener('gestureend', handleWindowGestureEnd);\n window.removeEventListener('pointermove', trackPointer, trackingOptions);\n window.removeEventListener('pointerdown', trackPointer, trackingOptions);\n window.removeEventListener('wheel', trackPointer, trackingOptions);\n };\n};\n"],"names":[],"mappings":"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"}
@@ -16,7 +16,8 @@ export interface CanvasWheelZoomOptions {
16
16
  }
17
17
  /**
18
18
  * Wheel pan, Ctrl/Cmd+wheel zoom, and Safari's trackpad gesture events, on the canvas and for
19
- * floating content over it, without letting the browser zoom the page.
19
+ * floating content over it, without letting the browser zoom the page. See
20
+ * {@link attachCanvasZoomListeners}.
20
21
  * @param options The container, camera state and operations.
21
22
  */
22
23
  export declare const useCanvasWheelZoom: ({ containerRef, animationFrameRef, panRef, touchPointersRef, zoomTowards, noteGestureActivity, scheduleTransformApply, }: CanvasWheelZoomOptions) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasWheelZoom.d.ts","sourceRoot":"","sources":["../../../Canvas/useCanvasWheelZoom.ts"],"names":[],"mappings":"AAGA,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAgB,CAAC;AAGtD,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACnC,YAAY,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC/C,iBAAiB,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,SAAS,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5C,+EAA+E;IAC/E,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;IAC1D,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACtC;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,6HAQ5B,sBAAsB,SA8LxB,CAAC"}
1
+ {"version":3,"file":"useCanvasWheelZoom.d.ts","sourceRoot":"","sources":["../../../Canvas/useCanvasWheelZoom.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAgB,CAAC;AAEtD,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACnC,YAAY,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAC/C,iBAAiB,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,SAAS,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5C,+EAA+E;IAC/E,gBAAgB,EAAE,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;IAC1D,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACtC;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,6HAQ5B,sBAAsB,SAgCxB,CAAC"}
@@ -1,145 +1,37 @@
1
- import { useCallback, useEffect } from 'react';
2
- import { ZOOM_INTENSITY } from './canvasGestureTuning.js';
3
- import { isWithinScrollableContent } from './isWithinScrollableContent.js';
1
+ import { useEffect } from 'react';
2
+ import { attachCanvasZoomListeners } from './canvasZoomListeners.js';
4
3
 
5
4
  const useCanvasWheelZoom = ({ containerRef, animationFrameRef, panRef, touchPointersRef, zoomTowards, noteGestureActivity, scheduleTransformApply, }) => {
6
- const handleWheel = useCallback((e) => {
5
+ useEffect(() => {
7
6
  const container = containerRef.current;
8
7
  if (!container)
9
8
  return;
10
- if (!(e.ctrlKey || e.metaKey) &&
11
- isWithinScrollableContent(e.target, container))
12
- return;
13
- e.preventDefault();
14
- if (animationFrameRef.current !== null) {
15
- cancelAnimationFrame(animationFrameRef.current);
16
- animationFrameRef.current = null;
17
- }
18
- const rect = container.getBoundingClientRect();
19
- const mouseX = e.clientX - rect.left;
20
- const mouseY = e.clientY - rect.top;
21
- if (e.ctrlKey || e.metaKey) {
22
- zoomTowards(mouseX, mouseY, Math.exp(-e.deltaY * ZOOM_INTENSITY));
23
- }
24
- else {
25
- panRef.current = {
26
- x: panRef.current.x - e.deltaX,
27
- y: panRef.current.y - e.deltaY,
28
- };
29
- noteGestureActivity();
30
- scheduleTransformApply();
31
- }
9
+ return attachCanvasZoomListeners({
10
+ container,
11
+ cancelMomentum: () => {
12
+ if (animationFrameRef.current !== null) {
13
+ cancelAnimationFrame(animationFrameRef.current);
14
+ animationFrameRef.current = null;
15
+ }
16
+ },
17
+ getPan: () => panRef.current,
18
+ setPan: pan => {
19
+ panRef.current = pan;
20
+ },
21
+ touchPointers: touchPointersRef.current,
22
+ zoomTowards,
23
+ noteGestureActivity,
24
+ scheduleTransformApply,
25
+ });
32
26
  }, [
27
+ containerRef,
28
+ animationFrameRef,
29
+ panRef,
30
+ touchPointersRef,
33
31
  zoomTowards,
34
32
  noteGestureActivity,
35
33
  scheduleTransformApply,
36
- animationFrameRef,
37
- containerRef,
38
- panRef,
39
34
  ]);
40
- useEffect(() => {
41
- const container = containerRef.current;
42
- if (!container)
43
- return;
44
- container.addEventListener('wheel', handleWheel, { passive: false });
45
- return () => container.removeEventListener('wheel', handleWheel);
46
- }, [handleWheel, containerRef]);
47
- useEffect(() => {
48
- const container = containerRef.current;
49
- if (!container)
50
- return;
51
- let lastScale = 1;
52
- const handleGestureStart = (event) => {
53
- event.preventDefault();
54
- lastScale = event.scale || 1;
55
- };
56
- const handleGestureChange = (event) => {
57
- event.preventDefault();
58
- if (touchPointersRef.current.size > 0)
59
- return;
60
- const gesture = event;
61
- if (!gesture.scale || !lastScale)
62
- return;
63
- const rect = container.getBoundingClientRect();
64
- zoomTowards(gesture.clientX - rect.left, gesture.clientY - rect.top, gesture.scale / lastScale);
65
- lastScale = gesture.scale;
66
- };
67
- const handleGestureEnd = (event) => event.preventDefault();
68
- container.addEventListener('gesturestart', handleGestureStart, {
69
- passive: false,
70
- });
71
- container.addEventListener('gesturechange', handleGestureChange, {
72
- passive: false,
73
- });
74
- container.addEventListener('gestureend', handleGestureEnd, { passive: false });
75
- return () => {
76
- container.removeEventListener('gesturestart', handleGestureStart);
77
- container.removeEventListener('gesturechange', handleGestureChange);
78
- container.removeEventListener('gestureend', handleGestureEnd);
79
- };
80
- }, [zoomTowards, containerRef, touchPointersRef]);
81
- useEffect(() => {
82
- const isWithinCanvas = (x, y) => {
83
- const rect = containerRef.current?.getBoundingClientRect();
84
- return (!!rect &&
85
- x >= rect.left &&
86
- x <= rect.right &&
87
- y >= rect.top &&
88
- y <= rect.bottom);
89
- };
90
- const reachesContainerListeners = (event) => event.target instanceof Node &&
91
- !!containerRef.current?.contains(event.target);
92
- const handleWindowWheel = (event) => {
93
- if ((event.ctrlKey || event.metaKey) &&
94
- isWithinCanvas(event.clientX, event.clientY)) {
95
- event.preventDefault();
96
- }
97
- };
98
- let lastScale = 1;
99
- const handleWindowGestureStart = (event) => {
100
- const gesture = event;
101
- if (!isWithinCanvas(gesture.clientX, gesture.clientY))
102
- return;
103
- event.preventDefault();
104
- lastScale = gesture.scale || 1;
105
- };
106
- const handleWindowGestureChange = (event) => {
107
- const gesture = event;
108
- if (!isWithinCanvas(gesture.clientX, gesture.clientY))
109
- return;
110
- event.preventDefault();
111
- if (reachesContainerListeners(event))
112
- return;
113
- if (touchPointersRef.current.size > 0)
114
- return;
115
- if (!gesture.scale || !lastScale)
116
- return;
117
- const rect = containerRef.current?.getBoundingClientRect();
118
- if (!rect)
119
- return;
120
- zoomTowards(gesture.clientX - rect.left, gesture.clientY - rect.top, gesture.scale / lastScale);
121
- lastScale = gesture.scale;
122
- };
123
- const handleWindowGestureEnd = (event) => {
124
- const gesture = event;
125
- if (isWithinCanvas(gesture.clientX, gesture.clientY))
126
- event.preventDefault();
127
- };
128
- window.addEventListener('wheel', handleWindowWheel, { passive: false });
129
- window.addEventListener('gesturestart', handleWindowGestureStart, {
130
- passive: false,
131
- });
132
- window.addEventListener('gesturechange', handleWindowGestureChange, {
133
- passive: false,
134
- });
135
- window.addEventListener('gestureend', handleWindowGestureEnd, { passive: false });
136
- return () => {
137
- window.removeEventListener('wheel', handleWindowWheel);
138
- window.removeEventListener('gesturestart', handleWindowGestureStart);
139
- window.removeEventListener('gesturechange', handleWindowGestureChange);
140
- window.removeEventListener('gestureend', handleWindowGestureEnd);
141
- };
142
- }, [zoomTowards, containerRef, touchPointersRef]);
143
35
  };
144
36
 
145
37
  export { useCanvasWheelZoom };
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasWheelZoom.js","sources":["../../../Canvas/useCanvasWheelZoom.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { useCallback, useEffect, type RefObject } from 'react';\nimport { ZOOM_INTENSITY } from './canvasGestureTuning';\nimport { isWithinScrollableContent } from './isWithinScrollableContent';\nimport type { PointerPosition } from './pinchGesture';\nimport type { WebKitGestureEvent } from './WebKitGestureEvent';\n\n/** The camera state and operations {@link useCanvasWheelZoom} drives. */\nexport interface CanvasWheelZoomOptions {\n containerRef: RefObject<HTMLDivElement | null>;\n animationFrameRef: RefObject<number | null>;\n panRef: RefObject<{ x: number; y: number }>;\n /** Touch pointers currently down; a touch pinch owns zooming while any are. */\n touchPointersRef: RefObject<Map<number, PointerPosition>>;\n zoomTowards: (focusX: number, focusY: number, factor: number) => void;\n noteGestureActivity: () => void;\n scheduleTransformApply: () => void;\n}\n\n/**\n * Wheel pan, Ctrl/Cmd+wheel zoom, and Safari's trackpad gesture events, on the canvas and for\n * floating content over it, without letting the browser zoom the page.\n * @param options The container, camera state and operations.\n */\nexport const useCanvasWheelZoom = ({\n containerRef,\n animationFrameRef,\n panRef,\n touchPointersRef,\n zoomTowards,\n noteGestureActivity,\n scheduleTransformApply,\n}: CanvasWheelZoomOptions) => {\n // Wheel: pan (scroll) or zoom (Ctrl+scroll / pinch)\n const handleWheel = useCallback(\n (e: WheelEvent) => {\n const container = containerRef.current;\n if (!container) return;\n\n // A plain scroll/trackpad gesture landing over a scrollable overlay (a chat's message list\n // and similar) scrolls that content instead of panning the board underneath it. A zoom\n // gesture (ctrl/cmd) always wins — pinch-to-zoom over a chat\n // panel is still a zoom, not a captured scroll.\n if (\n !(e.ctrlKey || e.metaKey) &&\n isWithinScrollableContent(e.target, container)\n )\n return;\n\n e.preventDefault();\n\n // A fresh gesture always wins over a still-coasting one from the last touch pan.\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n\n const rect = container.getBoundingClientRect();\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n if (e.ctrlKey || e.metaKey) {\n zoomTowards(mouseX, mouseY, Math.exp(-e.deltaY * ZOOM_INTENSITY));\n } else {\n panRef.current = {\n x: panRef.current.x - e.deltaX,\n y: panRef.current.y - e.deltaY,\n };\n noteGestureActivity();\n scheduleTransformApply();\n }\n },\n [\n zoomTowards,\n noteGestureActivity,\n scheduleTransformApply,\n animationFrameRef,\n containerRef,\n panRef,\n ],\n );\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n container.addEventListener('wheel', handleWheel, { passive: false });\n return () => container.removeEventListener('wheel', handleWheel);\n }, [handleWheel, containerRef]);\n\n // Safari/WebKit reports a trackpad pinch through its own non-standard gesture events —\n // including on iPadOS with a Magic Keyboard trackpad — separately from the wheel+ctrlKey path\n // above, which Safari never fires for a trackpad pinch. Left unhandled, Safari's default action\n // is to zoom the whole page instead of the canvas — but canceling that alone would swallow the\n // pinch entirely, so gesturechange also drives the same zoom-towards-focus the ctrl+wheel path\n // applies. A touchscreen pinch fires these gesture events too, alongside the pointer events the\n // touch path already zooms from — with touch pointers down, this handler only cancels the page\n // zoom and leaves the zooming to the touch path. The events don't exist outside WebKit, so all\n // of this is a no-op everywhere else.\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let lastScale = 1;\n const handleGestureStart = (event: Event) => {\n event.preventDefault();\n lastScale = (event as WebKitGestureEvent).scale || 1;\n };\n const handleGestureChange = (event: Event) => {\n event.preventDefault();\n if (touchPointersRef.current.size > 0) return;\n\n const gesture = event as WebKitGestureEvent;\n if (!gesture.scale || !lastScale) return;\n\n const rect = container.getBoundingClientRect();\n zoomTowards(\n gesture.clientX - rect.left,\n gesture.clientY - rect.top,\n gesture.scale / lastScale,\n );\n lastScale = gesture.scale;\n };\n const handleGestureEnd = (event: Event) => event.preventDefault();\n\n // Explicitly non-passive: WebKit is free to treat an options-less gesture listener as\n // passive, silently ignoring preventDefault() — which would zoom the browser page instead\n // of the canvas on an iPad Magic Keyboard trackpad.\n container.addEventListener('gesturestart', handleGestureStart, {\n passive: false,\n });\n container.addEventListener('gesturechange', handleGestureChange, {\n passive: false,\n });\n container.addEventListener('gestureend', handleGestureEnd, { passive: false });\n return () => {\n container.removeEventListener('gesturestart', handleGestureStart);\n container.removeEventListener('gesturechange', handleGestureChange);\n container.removeEventListener('gestureend', handleGestureEnd);\n };\n }, [zoomTowards, containerRef, touchPointersRef]);\n\n // The two handlers above only ever see events that bubble through the canvas surface's own DOM\n // subtree — but a toolbar, panel, or minimap floating over the canvas is frequently portalled to\n // document.body (or otherwise rendered as a sibling, not a descendant), so a pinch or Ctrl/Cmd+\n // wheel with the cursor sitting over one of those never reaches the listeners above at all, and\n // the browser is free to zoom the whole page instead. Listen at the window level too, and go\n // purely by screen position — no matter what element actually receives the\n // event, if it lands within the canvas's own rectangle the browser's native zoom is canceled.\n // For events whose target sits inside the canvas subtree this only cancels the browser zoom —\n // the container's own listeners drive the canvas zoom, so applying it here too would double up.\n // For a pinch whose target is a portalled sibling (never reaching the container listeners), it\n // also drives the same zoom-towards-focus, so a pinch over a floating panel zooms the board\n // instead of being swallowed after the browser zoom is canceled.\n useEffect(() => {\n const isWithinCanvas = (x: number, y: number) => {\n const rect = containerRef.current?.getBoundingClientRect();\n return (\n !!rect &&\n x >= rect.left &&\n x <= rect.right &&\n y >= rect.top &&\n y <= rect.bottom\n );\n };\n const reachesContainerListeners = (event: Event) =>\n event.target instanceof Node &&\n !!containerRef.current?.contains(event.target);\n const handleWindowWheel = (event: WheelEvent) => {\n if (\n (event.ctrlKey || event.metaKey) &&\n isWithinCanvas(event.clientX, event.clientY)\n ) {\n event.preventDefault();\n }\n };\n\n let lastScale = 1;\n const handleWindowGestureStart = (event: Event) => {\n const gesture = event as WebKitGestureEvent;\n if (!isWithinCanvas(gesture.clientX, gesture.clientY)) return;\n event.preventDefault();\n lastScale = gesture.scale || 1;\n };\n const handleWindowGestureChange = (event: Event) => {\n const gesture = event as WebKitGestureEvent;\n if (!isWithinCanvas(gesture.clientX, gesture.clientY)) return;\n event.preventDefault();\n if (reachesContainerListeners(event)) return;\n if (touchPointersRef.current.size > 0) return;\n if (!gesture.scale || !lastScale) return;\n\n const rect = containerRef.current?.getBoundingClientRect();\n if (!rect) return;\n zoomTowards(\n gesture.clientX - rect.left,\n gesture.clientY - rect.top,\n gesture.scale / lastScale,\n );\n lastScale = gesture.scale;\n };\n const handleWindowGestureEnd = (event: Event) => {\n const gesture = event as WebKitGestureEvent;\n if (isWithinCanvas(gesture.clientX, gesture.clientY)) event.preventDefault();\n };\n\n // Non-passive everywhere: WebKit treating any of these as passive is what lets the page\n // zoom through, and window-level listeners are exactly where it is most inclined to.\n window.addEventListener('wheel', handleWindowWheel, { passive: false });\n window.addEventListener('gesturestart', handleWindowGestureStart, {\n passive: false,\n });\n window.addEventListener('gesturechange', handleWindowGestureChange, {\n passive: false,\n });\n window.addEventListener('gestureend', handleWindowGestureEnd, { passive: false });\n return () => {\n window.removeEventListener('wheel', handleWindowWheel);\n window.removeEventListener('gesturestart', handleWindowGestureStart);\n window.removeEventListener('gesturechange', handleWindowGestureChange);\n window.removeEventListener('gestureend', handleWindowGestureEnd);\n };\n }, [zoomTowards, containerRef, touchPointersRef]);\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useCanvasWheelZoom.js","sources":["../../../Canvas/useCanvasWheelZoom.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { useEffect, type RefObject } from 'react';\nimport { attachCanvasZoomListeners } from './canvasZoomListeners';\nimport type { PointerPosition } from './pinchGesture';\n\n/** The camera state and operations {@link useCanvasWheelZoom} drives. */\nexport interface CanvasWheelZoomOptions {\n containerRef: RefObject<HTMLDivElement | null>;\n animationFrameRef: RefObject<number | null>;\n panRef: RefObject<{ x: number; y: number }>;\n /** Touch pointers currently down; a touch pinch owns zooming while any are. */\n touchPointersRef: RefObject<Map<number, PointerPosition>>;\n zoomTowards: (focusX: number, focusY: number, factor: number) => void;\n noteGestureActivity: () => void;\n scheduleTransformApply: () => void;\n}\n\n/**\n * Wheel pan, Ctrl/Cmd+wheel zoom, and Safari's trackpad gesture events, on the canvas and for\n * floating content over it, without letting the browser zoom the page. See\n * {@link attachCanvasZoomListeners}.\n * @param options The container, camera state and operations.\n */\nexport const useCanvasWheelZoom = ({\n containerRef,\n animationFrameRef,\n panRef,\n touchPointersRef,\n zoomTowards,\n noteGestureActivity,\n scheduleTransformApply,\n}: CanvasWheelZoomOptions) => {\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n return attachCanvasZoomListeners({\n container,\n cancelMomentum: () => {\n // A fresh gesture always wins over a still-coasting one from the last touch pan.\n if (animationFrameRef.current !== null) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = null;\n }\n },\n getPan: () => panRef.current,\n setPan: pan => {\n panRef.current = pan;\n },\n touchPointers: touchPointersRef.current,\n zoomTowards,\n noteGestureActivity,\n scheduleTransformApply,\n });\n }, [\n containerRef,\n animationFrameRef,\n panRef,\n touchPointersRef,\n zoomTowards,\n noteGestureActivity,\n scheduleTransformApply,\n ]);\n};\n"],"names":[],"mappings":";;;MAyBa,kBAAkB,GAAG,CAAC,EAC/B,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,gBAAgB,EAChB,WAAW,EACX,mBAAmB,EACnB,sBAAsB,GACD,KAAI;IACzB,SAAS,CAAC,MAAK;AACX,QAAA,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO;AACtC,QAAA,IAAI,CAAC,SAAS;YAAE;AAEhB,QAAA,OAAO,yBAAyB,CAAC;YAC7B,SAAS;YACT,cAAc,EAAE,MAAK;AAEjB,gBAAA,IAAI,iBAAiB,CAAC,OAAO,KAAK,IAAI,EAAE;AACpC,oBAAA,oBAAoB,CAAC,iBAAiB,CAAC,OAAO,CAAC;AAC/C,oBAAA,iBAAiB,CAAC,OAAO,GAAG,IAAI;gBACpC;YACJ,CAAC;AACD,YAAA,MAAM,EAAE,MAAM,MAAM,CAAC,OAAO;YAC5B,MAAM,EAAE,GAAG,IAAG;AACV,gBAAA,MAAM,CAAC,OAAO,GAAG,GAAG;YACxB,CAAC;YACD,aAAa,EAAE,gBAAgB,CAAC,OAAO;YACvC,WAAW;YACX,mBAAmB;YACnB,sBAAsB;AACzB,SAAA,CAAC;AACN,IAAA,CAAC,EAAE;QACC,YAAY;QACZ,iBAAiB;QACjB,MAAM;QACN,gBAAgB;QAChB,WAAW;QACX,mBAAmB;QACnB,sBAAsB;AACzB,KAAA,CAAC;AACN;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/components",
3
- "version": "4.25.1",
3
+ "version": "4.25.2",
4
4
  "description": "React components for CQRS and event-sourced apps — command dialogs, typed forms, and query-backed data tables for Cratis Arc",
5
5
  "keywords": [
6
6
  "react",