@cratis/components 4.25.0 → 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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CommandDialog.d.ts","sourceRoot":"","sources":["../../../CommandDialog/CommandDialog.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,sBAAmB,CAAC;AAC7D,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAKH,KAAK,gBAAgB,EACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAAwB,CAAC;AAOnE;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB,CAAC,QAAQ,SAAS,MAAM,EAAE,SAAS,GAAG,MAAM,CAC3E,SACI,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,UAAU,GAAG,iBAAiB,CAAC,EAC3E,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5C;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAElD;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAEtD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B;AAkHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmHG;AACH,QAAA,MAAM,sBAAsB;KAAI,QAAQ,SAAS,MAAM,WAAW,SAAS,kBAChE,kBAAkB,CAAC,QAAQ,EAAE,SAAS,CAAC;;CA8EjD,CAAC;AAEF,QAAA,MAAM,0BAA0B,iBAAkB;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,sBAE9E,CAAC;AAKF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,+BAAyB,CAAC"}
1
+ {"version":3,"file":"CommandDialog.d.ts","sourceRoot":"","sources":["../../../CommandDialog/CommandDialog.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,sBAAmB,CAAC;AAC7D,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAKH,KAAK,gBAAgB,EACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAsB,CAAC;AAClE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAAwB,CAAC;AAOnE;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB,CAAC,QAAQ,SAAS,MAAM,EAAE,SAAS,GAAG,MAAM,CAC3E,SACI,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,UAAU,GAAG,iBAAiB,CAAC,EAC3E,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5C;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAElD;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAEtD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC9B;AAoHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmHG;AACH,QAAA,MAAM,sBAAsB;KAAI,QAAQ,SAAS,MAAM,WAAW,SAAS,kBAChE,kBAAkB,CAAC,QAAQ,EAAE,SAAS,CAAC;;CA8EjD,CAAC;AAEF,QAAA,MAAM,0BAA0B,iBAAkB;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,sBAE9E,CAAC;AAKF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,+BAAyB,CAAC"}
@@ -48,7 +48,7 @@ const CommandDialogWrapper = ({ isValid, onClose, onConfirm, onSuccess, onValida
48
48
  return (jsx(CommandFormFieldWrapper, { field: child }));
49
49
  }
50
50
  const childProps = child.props;
51
- if (childProps.children != null) {
51
+ if (childProps.children != null && typeof childProps.children !== 'function') {
52
52
  return React.cloneElement(child, {
53
53
  children: processChildren(childProps.children),
54
54
  });
@@ -1 +1 @@
1
- {"version":3,"file":"CommandDialog.js","sources":["../../../CommandDialog/CommandDialog.tsx"],"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 { DialogButtons, DialogResult } from '@cratis/arc.react/dialogs';\nimport { Dialog, type DialogProps } from '../Dialogs/Dialog';\nimport React from 'react';\nimport {\n CommandForm,\n CommandFormFieldWrapper,\n useCommandFormContext,\n useCommandInstance,\n type CommandFormProps,\n} from '@cratis/arc.react/commands';\nimport type { BeforeExecuteCallback } from './applyBeforeExecute';\nimport type { ConfirmBeforeExecute } from './confirmBeforeExecute';\nimport { useCommandExecution } from './useCommandExecution';\nimport {\n isCommandFormField,\n markAsCommandFormColumn,\n} from '../CommandForm/commandFormMarkers';\n\n/**\n * Props for {@link CommandDialog}. Combines the props of a `CommandForm`\n * (`command`, `initialValues`, `onSuccess`, `onValidationFailure`, `onFailed`,\n * `onBeforeExecute`, etc.) with the props of a {@link Dialog} (`title`,\n * `buttons`, `width`, `pt`, `unstyled`, …). Dialog presentation props\n * such as `subtitle`, `placement`, and `closeIcon` are forwarded to the dialog.\n *\n * @typeParam TCommand - The command record type (must extend `object`).\n * @typeParam TResponse - The response payload type returned by a successful command. Defaults to `object`.\n */\nexport interface CommandDialogProps<TCommand extends object, TResponse = object>\n extends\n Omit<CommandFormProps<TCommand, TResponse>, 'children' | 'onBeforeExecute'>,\n Omit<DialogProps, 'children' | 'isBusy'> {\n /**\n * A transformer invoked with the current command values immediately before\n * the command executes on confirm. It **must return** the values to run with\n * (mutated or not) — a callback that returns nothing does not execute the\n * command with `undefined`; the current values are kept and a warning is\n * logged. May be async.\n *\n * ⚠️ It runs **only on submit**, after the form has already been validated,\n * so a value produced here can never satisfy required-field validation — the\n * submit button stays disabled if a required value is only seeded here. Seed\n * required values through `initialValues`; reserve `onBeforeExecute` for\n * transforms that do not affect validity (for example a generated id).\n */\n onBeforeExecute?: BeforeExecuteCallback<TCommand>;\n\n /**\n * Ask whether to run the command after validation and `onBeforeExecute`.\n * Receives the transformed values. Only `true` executes; all other outcomes\n * keep the dialog open. May return a promise (for example from a confirmation dialog).\n * If command values change while confirmation is pending, execution is skipped\n * and the dialog stays open; confirm the new values again.\n * Unlike `onConfirm`, this runs before execution, not after success.\n */\n confirmBeforeExecute?: ConfirmBeforeExecute<TCommand>;\n\n /**\n * Form fields and arbitrary content for the dialog body. Children that are\n * `CommandFormField` instances are automatically wrapped so they bind to\n * the command instance.\n */\n children?: React.ReactNode;\n}\n\n// Requiring each key here makes a newly added Dialog prop fail compilation until it is routed.\ntype ForwardedDialogProps = Omit<DialogProps, 'children' | 'isBusy'> &\n Record<keyof Omit<DialogProps, 'children' | 'isBusy'>, unknown>;\n\nconst CommandDialogWrapper = <TCommand extends object, TResponse = object>({\n isValid,\n onClose,\n onConfirm,\n onSuccess,\n onValidationFailure,\n onFailed,\n onException,\n onUnauthorized,\n onBeforeExecute,\n confirmBeforeExecute,\n children,\n ...dialogProps\n}: Omit<DialogProps, 'isBusy'> & {\n onSuccess?: CommandFormProps<TCommand, TResponse>['onSuccess'];\n onValidationFailure?: CommandFormProps<TCommand, TResponse>['onValidationFailure'];\n onFailed?: CommandFormProps<TCommand, TResponse>['onFailed'];\n onException?: CommandFormProps<TCommand, TResponse>['onException'];\n onUnauthorized?: CommandFormProps<TCommand, TResponse>['onUnauthorized'];\n onBeforeExecute?: BeforeExecuteCallback<TCommand>;\n confirmBeforeExecute?: ConfirmBeforeExecute<TCommand>;\n}) => {\n const {\n setCommandValues,\n setCommandResult,\n isValid: isCommandFormValid,\n } = useCommandFormContext<TCommand>();\n const commandInstance = useCommandInstance<TCommand>();\n const submission = useCommandExecution<TCommand, TResponse>(\n commandInstance, setCommandValues, onBeforeExecute, confirmBeforeExecute, onException,\n );\n\n const handleConfirm = async () => {\n const result = await submission.run();\n if (!result) return false;\n\n if (!result.isSuccess) {\n await onFailed?.(result);\n if (result.hasExceptions) {\n await onException?.(result.exceptionMessages, result.exceptionStackTrace);\n }\n if (!result.isAuthorized) await onUnauthorized?.();\n if (!result.isValid) {\n await onValidationFailure?.(result.validationResults);\n }\n if (submission.isMounted()) setCommandResult(result);\n return false;\n }\n\n await onSuccess?.(result.response as TResponse);\n if (onConfirm) {\n const closeResult = await onConfirm();\n return closeResult === true;\n }\n if (onClose) {\n const closeResult = await onClose(DialogResult.Ok);\n return closeResult !== false;\n }\n return true;\n };\n\n const processChildren = (nodes: React.ReactNode): React.ReactNode => {\n return React.Children.map(nodes, (child) => {\n if (!React.isValidElement(child)) return child;\n\n const component = child.type as React.ComponentType<unknown>;\n if (isCommandFormField(component)) {\n type FieldElement = Parameters<\n typeof CommandFormFieldWrapper\n >[0]['field'];\n // SAFETY: isCommandFormField guards that child.type is a command form field component.\n return (\n <CommandFormFieldWrapper field={child as unknown as FieldElement} />\n );\n }\n\n const childProps = child.props as Record<string, unknown>;\n if (childProps.children != null) {\n return React.cloneElement(\n child as React.ReactElement<Record<string, unknown>>,\n {\n children: processChildren(childProps.children as React.ReactNode),\n },\n );\n }\n\n return child;\n });\n };\n\n const processedChildren = processChildren(children);\n const isDialogValid = isValid !== false && isCommandFormValid;\n\n return (\n <Dialog\n {...dialogProps}\n onClose={onClose}\n onConfirm={handleConfirm}\n isValid={isDialogValid}\n isBusy={submission.isSubmitting}\n >\n <div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>\n {processedChildren}\n </div>\n </Dialog>\n );\n};\n\n/**\n * A {@link Dialog} that hosts a Cratis Arc `CommandForm`, runs the bound\n * command on confirm, and only closes when the command succeeds. This is\n * the standard pattern for \"user fills in a form, clicks OK, command runs\n * server-side, dialog dismisses\" — `CommandDialog` collapses about thirty\n * lines of orchestration into the props on a single component.\n *\n * ## Where `TCommand` comes from\n *\n * `TCommand` is an auto-generated TypeScript class produced by the Arc proxy\n * generator from a C# `[Command]` record. `dotnet build` writes a `.ts` file\n * per command with the right property types and a typed `execute()` method;\n * importing the class is all the connection-to-the-backend you need. The\n * class also exposes a `.use()` hook for direct (non-dialog) usage and a\n * `useWithChangeTracking()` variant.\n *\n * ## What happens on confirm\n *\n * 1. If `onBeforeExecute` is supplied, it transforms the command values.\n * Used for generated IDs (`Guid.create()`) that the user did not type in.\n * 2. The command's `execute()` runs through Arc's command pipeline. The\n * backend handler returns either a typed response (mapped to `TResponse`)\n * or a `ValidationResult[]`.\n * 3. On `IsSuccess: true`, `onSuccess(response)` fires and the dialog closes\n * through the dialog host context.\n * 4. On a validation failure, `onValidationFailure(errors)` fires and the\n * form's per-field error display is updated. The dialog stays open.\n * 5. On a domain or transport failure, `onFailed(result)` fires. The dialog\n * stays open and the `CommandResult` is set on the form context so the\n * UI can surface the error to the user.\n *\n * Throughout, the dialog is in the `isBusy` state — every action button is\n * disabled and the confirm button shows a spinner.\n *\n * ## Destructive commands and initial focus\n *\n * The confirm button is focused when the dialog opens, and a focused native\n * button fires `click` from the `keydown` of `Enter`. A command whose form\n * has required fields is protected from a held or double-tapped `Enter` for\n * free, because `isCommandFormValid` keeps confirm disabled until something\n * is filled in. A command that takes **no** input — the typical \"delete\n * this, permanently\" command — has no such gate, so its confirm button is\n * armed the instant the dialog appears.\n *\n * Pass `initialFocus` (forwarded straight to {@link Dialog}) for those:\n *\n * ```tsx\n * <CommandDialog<DeletePerson>\n * command={DeletePerson}\n * title=\"Delete personal data?\"\n * okLabel=\"Delete\"\n * initialFocus={DialogInitialFocus.Cancel}\n * onSuccess={() => closeDialog(DialogResult.Ok)}>\n * This cannot be undone.\n * </CommandDialog>\n * ```\n *\n * Everything else — the footer, the close (X), `Escape`, and the confirm\n * wiring that runs the command — is untouched.\n *\n * ## Field binding\n *\n * Children that are `CommandFormField` instances (`InputTextField`,\n * `NumberField`, `DropdownField`, every `@cratis/components/CommandForm/fields`\n * widget) bind to a property on the command via the `value` accessor:\n *\n * ```tsx\n * <InputTextField value={c => c.name} title=\"Name\" />\n * ```\n *\n * The accessor's argument is the command instance — TypeScript infers\n * `c.name` is a string and the field type-checks against that. Arbitrary\n * non-field children (custom layout, additional text, decorative elements)\n * are rendered as plain content.\n *\n * ## Typed dialog host usage\n *\n * Combine with `useDialog<CommandResult<TResponse>>()` from\n * `@cratis/arc.react/dialogs` to get a fully-typed result at the call site:\n *\n * ```tsx\n * import { useDialog, DialogResult } from '@cratis/arc.react/dialogs';\n * import { CommandDialog } from '@cratis/components/CommandDialog';\n * import { RegisterAuthor } from './RegisterAuthor'; // proxy from C#\n *\n * type RegisterAuthorResponse = { authorId: string };\n *\n * const RegisterAuthorDialog = () => {\n * const { closeDialog } = useDialogContext<CommandResult<RegisterAuthorResponse>>();\n * return (\n * <CommandDialog<RegisterAuthor, RegisterAuthorResponse>\n * command={RegisterAuthor}\n * title=\"Register author\"\n * okLabel=\"Register\"\n * onSuccess={(response) => closeDialog(DialogResult.Ok)}\n * onCancel={() => closeDialog(DialogResult.Cancelled)}>\n * <InputTextField value={c => c.name} title=\"Name\" />\n * <InputTextField value={c => c.email} title=\"Email\" />\n * </CommandDialog>\n * );\n * };\n * ```\n *\n * ## What's special vs. raw Dialog + CommandForm\n *\n * - Confirm wiring (`onConfirm` → `commandInstance.execute()`) is handled\n * internally; you only supply `onSuccess` / `onValidationFailure` / `onFailed`.\n * - The dialog's `isBusy` state tracks the in-flight command without manual\n * `useState`.\n * - Field error propagation from `CommandResult.validationResults` to the\n * form context is automatic.\n *\n * @typeParam TCommand - The command class (proxy generated from C# `[Command]`).\n * @typeParam TResponse - The success payload type returned by the command's `Handle()` method on the backend.\n * @param props - {@link CommandDialogProps}.\n */\nconst CommandDialogComponent = <TCommand extends object = object, TResponse = object>(\n props: CommandDialogProps<TCommand, TResponse>,\n) => {\n const {\n title,\n subtitle,\n placement,\n closeIcon,\n visible,\n width,\n style,\n contentStyle,\n resizable,\n buttons = DialogButtons.OkCancel,\n initialFocus,\n okLabel,\n cancelLabel,\n yesLabel,\n noLabel,\n dismissable,\n closeAriaLabel,\n isValid,\n onClose,\n onConfirm,\n onCancel,\n onBeforeExecute,\n confirmBeforeExecute,\n className,\n pt,\n ptOptions,\n unstyled,\n children,\n ...commandFormProps\n } = props;\n\n const dialogProps = {\n title,\n subtitle,\n placement,\n closeIcon,\n visible,\n width,\n style,\n contentStyle,\n resizable,\n buttons,\n initialFocus,\n okLabel,\n cancelLabel,\n yesLabel,\n noLabel,\n dismissable,\n closeAriaLabel,\n isValid,\n onClose,\n onConfirm,\n onCancel,\n className,\n pt,\n ptOptions,\n unstyled,\n } satisfies ForwardedDialogProps;\n\n return (\n <CommandForm<TCommand, TResponse> {...commandFormProps}>\n <CommandDialogWrapper<TCommand, TResponse>\n {...dialogProps}\n onSuccess={props.onSuccess}\n onValidationFailure={props.onValidationFailure}\n onFailed={props.onFailed}\n onException={props.onException}\n onUnauthorized={props.onUnauthorized}\n onBeforeExecute={onBeforeExecute}\n confirmBeforeExecute={confirmBeforeExecute}\n >\n {children}\n </CommandDialogWrapper>\n </CommandForm>\n );\n};\n\nconst CommandDialogColumnWrapper = ({ children }: { children: React.ReactNode }) => (\n <CommandForm.Column>{children}</CommandForm.Column>\n);\nmarkAsCommandFormColumn(CommandDialogColumnWrapper);\n\nCommandDialogComponent.Column = CommandDialogColumnWrapper;\n\n/**\n * A {@link Dialog} that hosts a Cratis Arc `CommandForm`, runs the bound\n * command on confirm, and only closes when the command succeeds. This is\n * the standard pattern for \"user fills in a form, clicks OK, command runs\n * server-side, dialog dismisses\" — `CommandDialog` collapses about thirty\n * lines of orchestration into the props on a single component.\n *\n * See {@link CommandDialogComponent} for full documentation.\n *\n * @typeParam TCommand - The command class (proxy generated from C# `[Command]`).\n * @typeParam TResponse - The success payload type returned by the command's `Handle()` method on the backend.\n */\nexport const CommandDialog = CommandDialogComponent;\n"],"names":["_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"CommandDialog.js","sources":["../../../CommandDialog/CommandDialog.tsx"],"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 { DialogButtons, DialogResult } from '@cratis/arc.react/dialogs';\nimport { Dialog, type DialogProps } from '../Dialogs/Dialog';\nimport React from 'react';\nimport {\n CommandForm,\n CommandFormFieldWrapper,\n useCommandFormContext,\n useCommandInstance,\n type CommandFormProps,\n} from '@cratis/arc.react/commands';\nimport type { BeforeExecuteCallback } from './applyBeforeExecute';\nimport type { ConfirmBeforeExecute } from './confirmBeforeExecute';\nimport { useCommandExecution } from './useCommandExecution';\nimport {\n isCommandFormField,\n markAsCommandFormColumn,\n} from '../CommandForm/commandFormMarkers';\n\n/**\n * Props for {@link CommandDialog}. Combines the props of a `CommandForm`\n * (`command`, `initialValues`, `onSuccess`, `onValidationFailure`, `onFailed`,\n * `onBeforeExecute`, etc.) with the props of a {@link Dialog} (`title`,\n * `buttons`, `width`, `pt`, `unstyled`, …). Dialog presentation props\n * such as `subtitle`, `placement`, and `closeIcon` are forwarded to the dialog.\n *\n * @typeParam TCommand - The command record type (must extend `object`).\n * @typeParam TResponse - The response payload type returned by a successful command. Defaults to `object`.\n */\nexport interface CommandDialogProps<TCommand extends object, TResponse = object>\n extends\n Omit<CommandFormProps<TCommand, TResponse>, 'children' | 'onBeforeExecute'>,\n Omit<DialogProps, 'children' | 'isBusy'> {\n /**\n * A transformer invoked with the current command values immediately before\n * the command executes on confirm. It **must return** the values to run with\n * (mutated or not) — a callback that returns nothing does not execute the\n * command with `undefined`; the current values are kept and a warning is\n * logged. May be async.\n *\n * ⚠️ It runs **only on submit**, after the form has already been validated,\n * so a value produced here can never satisfy required-field validation — the\n * submit button stays disabled if a required value is only seeded here. Seed\n * required values through `initialValues`; reserve `onBeforeExecute` for\n * transforms that do not affect validity (for example a generated id).\n */\n onBeforeExecute?: BeforeExecuteCallback<TCommand>;\n\n /**\n * Ask whether to run the command after validation and `onBeforeExecute`.\n * Receives the transformed values. Only `true` executes; all other outcomes\n * keep the dialog open. May return a promise (for example from a confirmation dialog).\n * If command values change while confirmation is pending, execution is skipped\n * and the dialog stays open; confirm the new values again.\n * Unlike `onConfirm`, this runs before execution, not after success.\n */\n confirmBeforeExecute?: ConfirmBeforeExecute<TCommand>;\n\n /**\n * Form fields and arbitrary content for the dialog body. Children that are\n * `CommandFormField` instances are automatically wrapped so they bind to\n * the command instance.\n */\n children?: React.ReactNode;\n}\n\n// Requiring each key here makes a newly added Dialog prop fail compilation until it is routed.\ntype ForwardedDialogProps = Omit<DialogProps, 'children' | 'isBusy'> &\n Record<keyof Omit<DialogProps, 'children' | 'isBusy'>, unknown>;\n\nconst CommandDialogWrapper = <TCommand extends object, TResponse = object>({\n isValid,\n onClose,\n onConfirm,\n onSuccess,\n onValidationFailure,\n onFailed,\n onException,\n onUnauthorized,\n onBeforeExecute,\n confirmBeforeExecute,\n children,\n ...dialogProps\n}: Omit<DialogProps, 'isBusy'> & {\n onSuccess?: CommandFormProps<TCommand, TResponse>['onSuccess'];\n onValidationFailure?: CommandFormProps<TCommand, TResponse>['onValidationFailure'];\n onFailed?: CommandFormProps<TCommand, TResponse>['onFailed'];\n onException?: CommandFormProps<TCommand, TResponse>['onException'];\n onUnauthorized?: CommandFormProps<TCommand, TResponse>['onUnauthorized'];\n onBeforeExecute?: BeforeExecuteCallback<TCommand>;\n confirmBeforeExecute?: ConfirmBeforeExecute<TCommand>;\n}) => {\n const {\n setCommandValues,\n setCommandResult,\n isValid: isCommandFormValid,\n } = useCommandFormContext<TCommand>();\n const commandInstance = useCommandInstance<TCommand>();\n const submission = useCommandExecution<TCommand, TResponse>(\n commandInstance, setCommandValues, onBeforeExecute, confirmBeforeExecute, onException,\n );\n\n const handleConfirm = async () => {\n const result = await submission.run();\n if (!result) return false;\n\n if (!result.isSuccess) {\n await onFailed?.(result);\n if (result.hasExceptions) {\n await onException?.(result.exceptionMessages, result.exceptionStackTrace);\n }\n if (!result.isAuthorized) await onUnauthorized?.();\n if (!result.isValid) {\n await onValidationFailure?.(result.validationResults);\n }\n if (submission.isMounted()) setCommandResult(result);\n return false;\n }\n\n await onSuccess?.(result.response as TResponse);\n if (onConfirm) {\n const closeResult = await onConfirm();\n return closeResult === true;\n }\n if (onClose) {\n const closeResult = await onClose(DialogResult.Ok);\n return closeResult !== false;\n }\n return true;\n };\n\n const processChildren = (nodes: React.ReactNode): React.ReactNode => {\n return React.Children.map(nodes, (child) => {\n if (!React.isValidElement(child)) return child;\n\n const component = child.type as React.ComponentType<unknown>;\n if (isCommandFormField(component)) {\n type FieldElement = Parameters<\n typeof CommandFormFieldWrapper\n >[0]['field'];\n // SAFETY: isCommandFormField guards that child.type is a command form field component.\n return (\n <CommandFormFieldWrapper field={child as unknown as FieldElement} />\n );\n }\n\n const childProps = child.props as Record<string, unknown>;\n // A function child is a render prop, not a node: mapping it would replace the\n // function with its mapped result and break the component that calls it.\n if (childProps.children != null && typeof childProps.children !== 'function') {\n return React.cloneElement(\n child as React.ReactElement<Record<string, unknown>>,\n {\n children: processChildren(childProps.children as React.ReactNode),\n },\n );\n }\n\n return child;\n });\n };\n\n const processedChildren = processChildren(children);\n const isDialogValid = isValid !== false && isCommandFormValid;\n\n return (\n <Dialog\n {...dialogProps}\n onClose={onClose}\n onConfirm={handleConfirm}\n isValid={isDialogValid}\n isBusy={submission.isSubmitting}\n >\n <div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>\n {processedChildren}\n </div>\n </Dialog>\n );\n};\n\n/**\n * A {@link Dialog} that hosts a Cratis Arc `CommandForm`, runs the bound\n * command on confirm, and only closes when the command succeeds. This is\n * the standard pattern for \"user fills in a form, clicks OK, command runs\n * server-side, dialog dismisses\" — `CommandDialog` collapses about thirty\n * lines of orchestration into the props on a single component.\n *\n * ## Where `TCommand` comes from\n *\n * `TCommand` is an auto-generated TypeScript class produced by the Arc proxy\n * generator from a C# `[Command]` record. `dotnet build` writes a `.ts` file\n * per command with the right property types and a typed `execute()` method;\n * importing the class is all the connection-to-the-backend you need. The\n * class also exposes a `.use()` hook for direct (non-dialog) usage and a\n * `useWithChangeTracking()` variant.\n *\n * ## What happens on confirm\n *\n * 1. If `onBeforeExecute` is supplied, it transforms the command values.\n * Used for generated IDs (`Guid.create()`) that the user did not type in.\n * 2. The command's `execute()` runs through Arc's command pipeline. The\n * backend handler returns either a typed response (mapped to `TResponse`)\n * or a `ValidationResult[]`.\n * 3. On `IsSuccess: true`, `onSuccess(response)` fires and the dialog closes\n * through the dialog host context.\n * 4. On a validation failure, `onValidationFailure(errors)` fires and the\n * form's per-field error display is updated. The dialog stays open.\n * 5. On a domain or transport failure, `onFailed(result)` fires. The dialog\n * stays open and the `CommandResult` is set on the form context so the\n * UI can surface the error to the user.\n *\n * Throughout, the dialog is in the `isBusy` state — every action button is\n * disabled and the confirm button shows a spinner.\n *\n * ## Destructive commands and initial focus\n *\n * The confirm button is focused when the dialog opens, and a focused native\n * button fires `click` from the `keydown` of `Enter`. A command whose form\n * has required fields is protected from a held or double-tapped `Enter` for\n * free, because `isCommandFormValid` keeps confirm disabled until something\n * is filled in. A command that takes **no** input — the typical \"delete\n * this, permanently\" command — has no such gate, so its confirm button is\n * armed the instant the dialog appears.\n *\n * Pass `initialFocus` (forwarded straight to {@link Dialog}) for those:\n *\n * ```tsx\n * <CommandDialog<DeletePerson>\n * command={DeletePerson}\n * title=\"Delete personal data?\"\n * okLabel=\"Delete\"\n * initialFocus={DialogInitialFocus.Cancel}\n * onSuccess={() => closeDialog(DialogResult.Ok)}>\n * This cannot be undone.\n * </CommandDialog>\n * ```\n *\n * Everything else — the footer, the close (X), `Escape`, and the confirm\n * wiring that runs the command — is untouched.\n *\n * ## Field binding\n *\n * Children that are `CommandFormField` instances (`InputTextField`,\n * `NumberField`, `DropdownField`, every `@cratis/components/CommandForm/fields`\n * widget) bind to a property on the command via the `value` accessor:\n *\n * ```tsx\n * <InputTextField value={c => c.name} title=\"Name\" />\n * ```\n *\n * The accessor's argument is the command instance — TypeScript infers\n * `c.name` is a string and the field type-checks against that. Arbitrary\n * non-field children (custom layout, additional text, decorative elements)\n * are rendered as plain content.\n *\n * ## Typed dialog host usage\n *\n * Combine with `useDialog<CommandResult<TResponse>>()` from\n * `@cratis/arc.react/dialogs` to get a fully-typed result at the call site:\n *\n * ```tsx\n * import { useDialog, DialogResult } from '@cratis/arc.react/dialogs';\n * import { CommandDialog } from '@cratis/components/CommandDialog';\n * import { RegisterAuthor } from './RegisterAuthor'; // proxy from C#\n *\n * type RegisterAuthorResponse = { authorId: string };\n *\n * const RegisterAuthorDialog = () => {\n * const { closeDialog } = useDialogContext<CommandResult<RegisterAuthorResponse>>();\n * return (\n * <CommandDialog<RegisterAuthor, RegisterAuthorResponse>\n * command={RegisterAuthor}\n * title=\"Register author\"\n * okLabel=\"Register\"\n * onSuccess={(response) => closeDialog(DialogResult.Ok)}\n * onCancel={() => closeDialog(DialogResult.Cancelled)}>\n * <InputTextField value={c => c.name} title=\"Name\" />\n * <InputTextField value={c => c.email} title=\"Email\" />\n * </CommandDialog>\n * );\n * };\n * ```\n *\n * ## What's special vs. raw Dialog + CommandForm\n *\n * - Confirm wiring (`onConfirm` → `commandInstance.execute()`) is handled\n * internally; you only supply `onSuccess` / `onValidationFailure` / `onFailed`.\n * - The dialog's `isBusy` state tracks the in-flight command without manual\n * `useState`.\n * - Field error propagation from `CommandResult.validationResults` to the\n * form context is automatic.\n *\n * @typeParam TCommand - The command class (proxy generated from C# `[Command]`).\n * @typeParam TResponse - The success payload type returned by the command's `Handle()` method on the backend.\n * @param props - {@link CommandDialogProps}.\n */\nconst CommandDialogComponent = <TCommand extends object = object, TResponse = object>(\n props: CommandDialogProps<TCommand, TResponse>,\n) => {\n const {\n title,\n subtitle,\n placement,\n closeIcon,\n visible,\n width,\n style,\n contentStyle,\n resizable,\n buttons = DialogButtons.OkCancel,\n initialFocus,\n okLabel,\n cancelLabel,\n yesLabel,\n noLabel,\n dismissable,\n closeAriaLabel,\n isValid,\n onClose,\n onConfirm,\n onCancel,\n onBeforeExecute,\n confirmBeforeExecute,\n className,\n pt,\n ptOptions,\n unstyled,\n children,\n ...commandFormProps\n } = props;\n\n const dialogProps = {\n title,\n subtitle,\n placement,\n closeIcon,\n visible,\n width,\n style,\n contentStyle,\n resizable,\n buttons,\n initialFocus,\n okLabel,\n cancelLabel,\n yesLabel,\n noLabel,\n dismissable,\n closeAriaLabel,\n isValid,\n onClose,\n onConfirm,\n onCancel,\n className,\n pt,\n ptOptions,\n unstyled,\n } satisfies ForwardedDialogProps;\n\n return (\n <CommandForm<TCommand, TResponse> {...commandFormProps}>\n <CommandDialogWrapper<TCommand, TResponse>\n {...dialogProps}\n onSuccess={props.onSuccess}\n onValidationFailure={props.onValidationFailure}\n onFailed={props.onFailed}\n onException={props.onException}\n onUnauthorized={props.onUnauthorized}\n onBeforeExecute={onBeforeExecute}\n confirmBeforeExecute={confirmBeforeExecute}\n >\n {children}\n </CommandDialogWrapper>\n </CommandForm>\n );\n};\n\nconst CommandDialogColumnWrapper = ({ children }: { children: React.ReactNode }) => (\n <CommandForm.Column>{children}</CommandForm.Column>\n);\nmarkAsCommandFormColumn(CommandDialogColumnWrapper);\n\nCommandDialogComponent.Column = CommandDialogColumnWrapper;\n\n/**\n * A {@link Dialog} that hosts a Cratis Arc `CommandForm`, runs the bound\n * command on confirm, and only closes when the command succeeds. This is\n * the standard pattern for \"user fills in a form, clicks OK, command runs\n * server-side, dialog dismisses\" — `CommandDialog` collapses about thirty\n * lines of orchestration into the props on a single component.\n *\n * See {@link CommandDialogComponent} for full documentation.\n *\n * @typeParam TCommand - The command class (proxy generated from C# `[Command]`).\n * @typeParam TResponse - The success payload type returned by the command's `Handle()` method on the backend.\n */\nexport const CommandDialog = CommandDialogComponent;\n"],"names":["_jsx"],"mappings":";;;;;;;;AAwEA,MAAM,oBAAoB,GAAG,CAA8C,EACvE,OAAO,EACP,OAAO,EACP,SAAS,EACT,SAAS,EACT,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,QAAQ,EACR,GAAG,WAAW,EASjB,KAAI;AACD,IAAA,MAAM,EACF,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,EAAE,kBAAkB,GAC9B,GAAG,qBAAqB,EAAY;AACrC,IAAA,MAAM,eAAe,GAAG,kBAAkB,EAAY;AACtD,IAAA,MAAM,UAAU,GAAG,mBAAmB,CAClC,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,EAAE,WAAW,CACxF;AAED,IAAA,MAAM,aAAa,GAAG,YAAW;AAC7B,QAAA,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE;AACrC,QAAA,IAAI,CAAC,MAAM;AAAE,YAAA,OAAO,KAAK;AAEzB,QAAA,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;AACnB,YAAA,MAAM,QAAQ,GAAG,MAAM,CAAC;AACxB,YAAA,IAAI,MAAM,CAAC,aAAa,EAAE;gBACtB,MAAM,WAAW,GAAG,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,mBAAmB,CAAC;YAC7E;YACA,IAAI,CAAC,MAAM,CAAC,YAAY;gBAAE,MAAM,cAAc,IAAI;AAClD,YAAA,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;AACjB,gBAAA,MAAM,mBAAmB,GAAG,MAAM,CAAC,iBAAiB,CAAC;YACzD;YACA,IAAI,UAAU,CAAC,SAAS,EAAE;gBAAE,gBAAgB,CAAC,MAAM,CAAC;AACpD,YAAA,OAAO,KAAK;QAChB;AAEA,QAAA,MAAM,SAAS,GAAG,MAAM,CAAC,QAAqB,CAAC;QAC/C,IAAI,SAAS,EAAE;AACX,YAAA,MAAM,WAAW,GAAG,MAAM,SAAS,EAAE;YACrC,OAAO,WAAW,KAAK,IAAI;QAC/B;QACA,IAAI,OAAO,EAAE;YACT,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YAClD,OAAO,WAAW,KAAK,KAAK;QAChC;AACA,QAAA,OAAO,IAAI;AACf,IAAA,CAAC;AAED,IAAA,MAAM,eAAe,GAAG,CAAC,KAAsB,KAAqB;QAChE,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,KAAI;AACvC,YAAA,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;AAAE,gBAAA,OAAO,KAAK;AAE9C,YAAA,MAAM,SAAS,GAAG,KAAK,CAAC,IAAoC;AAC5D,YAAA,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE;gBAK/B,QACIA,IAAC,uBAAuB,EAAA,EAAC,KAAK,EAAE,KAAgC,EAAA,CAAI;YAE5E;AAEA,YAAA,MAAM,UAAU,GAAG,KAAK,CAAC,KAAgC;AAGzD,YAAA,IAAI,UAAU,CAAC,QAAQ,IAAI,IAAI,IAAI,OAAO,UAAU,CAAC,QAAQ,KAAK,UAAU,EAAE;AAC1E,gBAAA,OAAO,KAAK,CAAC,YAAY,CACrB,KAAoD,EACpD;AACI,oBAAA,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,QAA2B,CAAC;AACpE,iBAAA,CACJ;YACL;AAEA,YAAA,OAAO,KAAK;AAChB,QAAA,CAAC,CAAC;AACN,IAAA,CAAC;AAED,IAAA,MAAM,iBAAiB,GAAG,eAAe,CAAC,QAAQ,CAAC;AACnD,IAAA,MAAM,aAAa,GAAG,OAAO,KAAK,KAAK,IAAI,kBAAkB;IAE7D,QACIA,IAAC,MAAM,EAAA,EAAA,GACC,WAAW,EACf,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,UAAU,CAAC,YAAY,EAAA,QAAA,EAE/BA,GAAA,CAAA,KAAA,EAAA,EAAK,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,YAClE,iBAAiB,EAAA,CAChB,EAAA,CACD;AAEjB,CAAC;AAsHD,MAAM,sBAAsB,GAAG,CAC3B,KAA8C,KAC9C;AACA,IAAA,MAAM,EACF,KAAK,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,EACP,KAAK,EACL,KAAK,EACL,YAAY,EACZ,SAAS,EACT,OAAO,GAAG,aAAa,CAAC,QAAQ,EAChC,YAAY,EACZ,OAAO,EACP,WAAW,EACX,QAAQ,EACR,OAAO,EACP,WAAW,EACX,cAAc,EACd,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,EACR,eAAe,EACf,oBAAoB,EACpB,SAAS,EACT,EAAE,EACF,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,GAAG,gBAAgB,EACtB,GAAG,KAAK;AAET,IAAA,MAAM,WAAW,GAAG;QAChB,KAAK;QACL,QAAQ;QACR,SAAS;QACT,SAAS;QACT,OAAO;QACP,KAAK;QACL,KAAK;QACL,YAAY;QACZ,SAAS;QACT,OAAO;QACP,YAAY;QACZ,OAAO;QACP,WAAW;QACX,QAAQ;QACR,OAAO;QACP,WAAW;QACX,cAAc;QACd,OAAO;QACP,OAAO;QACP,SAAS;QACT,QAAQ;QACR,SAAS;QACT,EAAE;QACF,SAAS;QACT,QAAQ;KACoB;IAEhC,QACIA,GAAA,CAAC,WAAW,EAAA,EAAA,GAA0B,gBAAgB,YAClDA,GAAA,CAAC,oBAAoB,EAAA,EAAA,GACb,WAAW,EACf,SAAS,EAAE,KAAK,CAAC,SAAS,EAC1B,mBAAmB,EAAE,KAAK,CAAC,mBAAmB,EAC9C,QAAQ,EAAE,KAAK,CAAC,QAAQ,EACxB,WAAW,EAAE,KAAK,CAAC,WAAW,EAC9B,cAAc,EAAE,KAAK,CAAC,cAAc,EACpC,eAAe,EAAE,eAAe,EAChC,oBAAoB,EAAE,oBAAoB,EAAA,QAAA,EAEzC,QAAQ,EAAA,CACU,EAAA,CACb;AAEtB,CAAC;AAED,MAAM,0BAA0B,GAAG,CAAC,EAAE,QAAQ,EAAiC,MAC3EA,GAAA,CAAC,WAAW,CAAC,MAAM,cAAE,QAAQ,EAAA,CAAsB,CACtD;AACD,uBAAuB,CAAC,0BAA0B,CAAC;AAEnD,sBAAsB,CAAC,MAAM,GAAG,0BAA0B;AAcnD,MAAM,aAAa,GAAG;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/components",
3
- "version": "4.25.0",
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",