@ctrl-ui/react 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +27 -0
  2. package/dist/action-bar.js +16 -8
  3. package/dist/activity.d.ts +1 -1
  4. package/dist/activity.js +3 -1
  5. package/dist/audio-visualizer.js +1 -1
  6. package/dist/blocks/agent-run-clock.d.ts +5 -0
  7. package/dist/blocks/agent-run-clock.js +52 -0
  8. package/dist/blocks/agent-run-sidebar.d.ts +13 -0
  9. package/dist/blocks/agent-run-sidebar.js +100 -0
  10. package/dist/blocks/agent-run-status.d.ts +32 -0
  11. package/dist/blocks/agent-run-status.js +27 -0
  12. package/dist/blocks/agent-run.d.ts +80 -0
  13. package/dist/blocks/agent-run.js +254 -0
  14. package/dist/blocks/coding-agent.d.ts +1 -1
  15. package/dist/blocks/coding-agent.js +1 -4
  16. package/dist/blocks/design-canvas-data.d.ts +1 -1
  17. package/dist/blocks/design-canvas.d.ts +2 -1
  18. package/dist/blocks/design-canvas.js +15 -8
  19. package/dist/blocks/file-explorer.js +14 -16
  20. package/dist/chat-message.js +33 -39
  21. package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
  22. package/dist/knob-contracts/infinite-canvas-knobs.js +1 -7
  23. package/dist/knob-contracts/swap-knobs.d.ts +4 -0
  24. package/dist/knob-contracts/swap-knobs.js +9 -0
  25. package/dist/knob-contracts/timeline-knobs.d.ts +1 -1
  26. package/dist/knob-contracts/timeline-knobs.js +1 -0
  27. package/dist/knob-contracts/tool-approval-knobs.d.ts +4 -0
  28. package/dist/knob-contracts/tool-approval-knobs.js +32 -0
  29. package/dist/lib/cn.d.ts +2 -0
  30. package/dist/lib/cn.js +29 -24
  31. package/dist/lib/infinite-canvas-camera-store.d.ts +65 -0
  32. package/dist/lib/infinite-canvas-camera-store.js +233 -0
  33. package/dist/lib/infinite-canvas-camera.d.ts +33 -0
  34. package/dist/lib/infinite-canvas-camera.js +98 -0
  35. package/dist/lib/infinite-canvas-navigation.d.ts +19 -0
  36. package/dist/lib/infinite-canvas-navigation.js +395 -0
  37. package/dist/markdown-editor/bubble-menu.d.ts +6 -0
  38. package/dist/markdown-editor/bubble-menu.js +45 -0
  39. package/dist/markdown-editor/toolbar.d.ts +17 -1
  40. package/dist/markdown-editor/toolbar.js +103 -81
  41. package/dist/styles/index.css +10 -8
  42. package/dist/styles/recipes/activity.css +8 -0
  43. package/dist/styles/recipes/button-group.css +12 -6
  44. package/dist/styles/recipes/button-states.css +12 -0
  45. package/dist/styles/recipes/button.css +13 -3
  46. package/dist/styles/recipes/chat-composer-attachment.css +7 -0
  47. package/dist/styles/recipes/chat-message-edit.css +6 -21
  48. package/dist/styles/recipes/color-picker-details.css +7 -0
  49. package/dist/styles/recipes/field.css +7 -0
  50. package/dist/styles/recipes/infinite-canvas.css +17 -57
  51. package/dist/styles/recipes/inline-attachment.css +7 -0
  52. package/dist/styles/recipes/kbd.css +7 -0
  53. package/dist/styles/recipes/markdown-editor.css +12 -0
  54. package/dist/styles/recipes/pagination.css +8 -1
  55. package/dist/styles/recipes/scroll-area.css +1 -3
  56. package/dist/styles/recipes/swap.css +143 -0
  57. package/dist/styles/recipes/tabs.css +12 -0
  58. package/dist/styles/recipes/timeline.css +11 -0
  59. package/dist/styles/recipes/tool-approval.css +267 -0
  60. package/dist/styles/recipes/track-highlight.css +19 -6
  61. package/dist/styles/theme.css +6 -5
  62. package/dist/styles/type-scale.css +2 -1
  63. package/dist/tool-approval.d.ts +49 -0
  64. package/dist/tool-approval.js +260 -0
  65. package/dist/ui/code.js +13 -3
  66. package/dist/ui/command.js +1 -0
  67. package/dist/ui/infinite-canvas.d.ts +19 -22
  68. package/dist/ui/infinite-canvas.js +264 -325
  69. package/dist/ui/rich-tooltip.d.ts +2 -1
  70. package/dist/ui/rich-tooltip.js +32 -29
  71. package/dist/ui/sidebar.js +1 -1
  72. package/dist/ui/swap.d.ts +14 -0
  73. package/dist/ui/swap.js +69 -0
  74. package/dist/ui/timeline.d.ts +1 -1
  75. package/dist/ui/timeline.js +1 -1
  76. package/dist/ui/typography.d.ts +2 -0
  77. package/dist/ui/typography.js +2 -0
  78. package/dist/user-ask.js +1 -1
  79. package/lint/no-raw-type-classes.grit +7 -0
  80. package/package.json +4 -3
@@ -1,10 +1,13 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { Button } from "./button.js";
4
- import "../lib/canvas-grid.js";
5
- import { createContext, use, useEffect, useEffectEvent, useRef, useState } from "react";
4
+ import { createInfiniteCanvasCamera, infiniteCanvasGridStyle, infiniteCanvasTransformCss } from "../lib/infinite-canvas-camera-store.js";
5
+ import { ARROW_KEY_DIRECTIONS, INFINITE_CANVAS_KEY_SHORTCUTS, attachInfiniteCanvasNavigation, keyboardStep, runInfiniteCanvasKeyCommand } from "../lib/infinite-canvas-navigation.js";
6
+ import { Toolbar, ToolbarButton, ToolbarSeparator } from "./toolbar.js";
7
+ import { createContext, use, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
+ import { useRender } from "@base-ui/react/use-render";
6
9
  import { jsx, jsxs } from "react/jsx-runtime";
7
- import { MinusIcon, PlusIcon } from "lucide-react";
10
+ import { GripVerticalIcon, MaximizeIcon, MinusIcon, PlusIcon } from "lucide-react";
8
11
  //#region src/ui/infinite-canvas.tsx
9
12
  const DEFAULT_TRANSFORM = {
10
13
  x: 0,
@@ -13,386 +16,322 @@ const DEFAULT_TRANSFORM = {
13
16
  };
14
17
  const DEFAULT_MIN_SCALE = .35;
15
18
  const DEFAULT_MAX_SCALE = 2.5;
16
- const KEYBOARD_PAN_STEP = 32;
17
- const WHEEL_ZOOM_SPEED = .004;
18
- const MAX_WHEEL_ZOOM_DELTA = 50;
19
- const CANVAS_KEY_SHORTCUTS = "ArrowLeft ArrowRight ArrowUp ArrowDown + - 0";
20
- const NOTCHED_WHEEL_DELTA = 40;
21
- const KEYBOARD_PAN_DIRECTIONS = {
22
- ArrowLeft: {
23
- x: -1,
24
- y: 0
25
- },
26
- ArrowRight: {
27
- x: 1,
28
- y: 0
29
- },
30
- ArrowUp: {
31
- x: 0,
32
- y: -1
33
- },
34
- ArrowDown: {
35
- x: 0,
36
- y: 1
37
- }
38
- };
39
- const KEYBOARD_ZOOM_FACTORS = {
40
- "+": 1.15,
41
- "=": 1.15,
42
- "-": 1 / 1.15
43
- };
44
- function keyboardPanStep(shiftKey) {
45
- return shiftKey ? 96 : KEYBOARD_PAN_STEP;
46
- }
19
+ const CONTROL_ZOOM_FACTOR = 1.2;
20
+ const ITEM_KEY_SHORTCUTS = "ArrowLeft ArrowRight ArrowUp ArrowDown";
47
21
  const InfiniteCanvasContext = createContext(null);
48
- function clampScale(scale, minScale, maxScale) {
49
- return Math.min(Math.max(scale, minScale), maxScale);
50
- }
51
- function zoomAroundPoint(transform, nextScale, point) {
52
- const worldX = (point.x - transform.x) / transform.scale;
53
- const worldY = (point.y - transform.y) / transform.scale;
54
- return {
55
- x: point.x - worldX * nextScale,
56
- y: point.y - worldY * nextScale,
57
- scale: nextScale
58
- };
59
- }
60
- function wheelDeltaMultiplier(event, viewportHeight) {
61
- if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) return 16;
62
- if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) return viewportHeight;
63
- return 1;
64
- }
65
- function panFromWheel(transform, event, viewportHeight) {
66
- const deltaMultiplier = wheelDeltaMultiplier(event, viewportHeight);
67
- const horizontalDelta = event.shiftKey && event.deltaX === 0 ? event.deltaY : event.deltaX;
68
- const verticalDelta = event.shiftKey && event.deltaX === 0 ? 0 : event.deltaY;
69
- return {
70
- x: transform.x - horizontalDelta * deltaMultiplier,
71
- y: transform.y - verticalDelta * deltaMultiplier,
72
- scale: transform.scale
73
- };
22
+ const InfiniteCanvasStoreContext = createContext(null);
23
+ const InfiniteCanvasItemContext = createContext(null);
24
+ function selectScale(transform) {
25
+ return transform.scale;
74
26
  }
75
27
  function useInfiniteCanvas() {
76
28
  const context = use(InfiniteCanvasContext);
77
29
  if (!context) throw new Error("useInfiniteCanvas must be used within InfiniteCanvas");
78
30
  return context;
79
31
  }
80
- function InfiniteCanvas({ transform: controlledTransform, defaultTransform = DEFAULT_TRANSFORM, onTransformChange, minScale: minScaleProp = DEFAULT_MIN_SCALE, maxScale: maxScaleProp = DEFAULT_MAX_SCALE, className, children, style, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onWheel, onKeyDown, "aria-label": ariaLabel = "Infinite canvas", "aria-keyshortcuts": ariaKeyShortcuts, ...props }) {
32
+ function useInfiniteCanvasStore() {
33
+ const context = use(InfiniteCanvasStoreContext);
34
+ if (!context) throw new Error("InfiniteCanvas parts must be used within InfiniteCanvas");
35
+ return context;
36
+ }
37
+ function useInfiniteCanvasTransform(select) {
38
+ const { store, initialTransform } = useInfiniteCanvasStore();
39
+ return useSyncExternalStore(store.subscribe, () => select ? select(store.getCommittedTransform()) : store.getCommittedTransform(), () => select ? select(initialTransform) : initialTransform);
40
+ }
41
+ function InfiniteCanvas({ transform, defaultTransform = DEFAULT_TRANSFORM, onTransformChange, minScale: minScaleProp = DEFAULT_MIN_SCALE, maxScale: maxScaleProp = DEFAULT_MAX_SCALE, panOnDrag = "background", keyboardNavigation = true, ref, className, children, style, onPointerDown, onWheel, onKeyDown, "aria-label": ariaLabel = "Infinite canvas", "aria-keyshortcuts": ariaKeyShortcuts, ...props }) {
81
42
  const minScale = minScaleProp > 0 ? minScaleProp : DEFAULT_MIN_SCALE;
82
43
  const maxScale = Math.max(minScale, maxScaleProp);
83
- const [uncontrolledTransform, setUncontrolledTransform] = useState(defaultTransform);
84
- const [panning, setPanning] = useState(false);
85
- const [easing, setEasing] = useState(false);
86
- const panRef = useRef(null);
44
+ const cameraOptions = {
45
+ minScale,
46
+ maxScale,
47
+ defaultTransform,
48
+ controlled: transform !== void 0,
49
+ onTransformChange
50
+ };
51
+ const [initialTransform] = useState(() => transform ?? defaultTransform);
52
+ const [store] = useState(() => createInfiniteCanvasCamera(initialTransform, cameraOptions));
87
53
  const rootRef = useRef(null);
88
- const transform = controlledTransform ?? uncontrolledTransform;
89
- const transformRef = useRef(transform);
90
- useEffect(() => {
91
- transformRef.current = transform;
92
- }, [transform]);
93
- function commitTransform(next, reason, eased = reason === "control" || reason === "keyboard") {
94
- setEasing(eased);
95
- const bounded = {
96
- ...next,
97
- scale: clampScale(next.scale, minScale, maxScale)
98
- };
99
- transformRef.current = bounded;
100
- if (controlledTransform === void 0) setUncontrolledTransform(bounded);
101
- onTransformChange?.(bounded, { reason });
102
- }
103
- function zoomBy(factor, reason) {
104
- const currentTransform = transformRef.current;
105
- const nextScale = clampScale(currentTransform.scale * factor, minScale, maxScale);
106
- const root = rootRef.current;
107
- commitTransform(zoomAroundPoint(currentTransform, nextScale, root ? {
108
- x: root.clientWidth / 2,
109
- y: root.clientHeight / 2
110
- } : {
111
- x: 0,
112
- y: 0
113
- }), reason);
114
- }
115
- function reset(reason) {
116
- commitTransform(defaultTransform, reason);
117
- }
118
- function beginPan(event) {
119
- onPointerDown?.(event);
120
- if (event.defaultPrevented || event.target !== event.currentTarget || !event.isPrimary || event.button !== 0) return;
121
- event.preventDefault();
122
- event.currentTarget.setPointerCapture(event.pointerId);
123
- panRef.current = {
124
- pointerId: event.pointerId,
125
- startPointer: {
126
- x: event.clientX,
127
- y: event.clientY
128
- },
129
- startTransform: transformRef.current
54
+ const navigationRef = useRef(null);
55
+ const navigationOptionsRef = useRef({
56
+ panOnDrag,
57
+ onWheel
58
+ });
59
+ const controlledX = transform?.x;
60
+ const controlledY = transform?.y;
61
+ const controlledScale = transform?.scale;
62
+ useLayoutEffect(() => {
63
+ store.configure(cameraOptions);
64
+ navigationOptionsRef.current = {
65
+ panOnDrag,
66
+ onWheel
130
67
  };
131
- setPanning(true);
132
- }
133
- function updatePan(event) {
134
- onPointerMove?.(event);
135
- if (event.defaultPrevented) return;
136
- const pan = panRef.current;
137
- if (!pan || pan.pointerId !== event.pointerId) return;
138
- commitTransform({
139
- x: pan.startTransform.x + event.clientX - pan.startPointer.x,
140
- y: pan.startTransform.y + event.clientY - pan.startPointer.y,
141
- scale: pan.startTransform.scale
142
- }, "pointer");
143
- }
144
- function finishPan(event) {
145
- const pan = panRef.current;
146
- if (!pan || pan.pointerId !== event.pointerId) return;
147
- if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
148
- panRef.current = null;
149
- setPanning(false);
150
- }
151
- function endPan(event) {
152
- onPointerUp?.(event);
153
- if (!event.defaultPrevented) finishPan(event);
154
- }
155
- function cancelPan(event) {
156
- onPointerCancel?.(event);
157
- if (!event.defaultPrevented) finishPan(event);
158
- }
159
- const handleWheel = useEffectEvent((event) => {
160
- onWheel?.(event);
161
- if (event.defaultPrevented) return;
162
- const root = rootRef.current;
163
- if (!root) return;
164
- event.preventDefault();
165
- const currentTransform = transformRef.current;
166
- if (event.ctrlKey || event.metaKey) {
167
- const bounds = root.getBoundingClientRect();
168
- const point = {
169
- x: event.clientX - bounds.left,
170
- y: event.clientY - bounds.top
171
- };
172
- const zoomDelta = event.deltaY * wheelDeltaMultiplier(event, root.clientHeight);
173
- const boundedZoomDelta = Math.min(Math.max(zoomDelta, -50), MAX_WHEEL_ZOOM_DELTA);
174
- const nextScale = clampScale(currentTransform.scale * Math.exp(-boundedZoomDelta * WHEEL_ZOOM_SPEED), minScale, maxScale);
175
- const notchedWheel = Math.abs(zoomDelta) >= NOTCHED_WHEEL_DELTA;
176
- commitTransform(zoomAroundPoint(currentTransform, nextScale, point), "wheel", notchedWheel);
177
- return;
178
- }
179
- commitTransform(panFromWheel(currentTransform, event, root.clientHeight), "wheel");
68
+ navigationRef.current?.syncPanReady();
180
69
  });
70
+ useLayoutEffect(() => {
71
+ if (controlledX === void 0 || controlledY === void 0 || controlledScale === void 0) return;
72
+ store.adopt({
73
+ x: controlledX,
74
+ y: controlledY,
75
+ scale: controlledScale
76
+ });
77
+ }, [
78
+ store,
79
+ controlledX,
80
+ controlledY,
81
+ controlledScale
82
+ ]);
181
83
  useEffect(() => {
182
84
  const root = rootRef.current;
183
85
  if (!root) return;
184
- const listener = (event) => handleWheel(event);
185
- root.addEventListener("wheel", listener, { passive: false });
186
- return () => root.removeEventListener("wheel", listener);
187
- }, []);
188
- function applyKeyboardCommand(key, shiftKey) {
189
- const direction = KEYBOARD_PAN_DIRECTIONS[key];
190
- if (direction) {
191
- const currentTransform = transformRef.current;
192
- const panStep = keyboardPanStep(shiftKey);
193
- commitTransform({
194
- ...currentTransform,
195
- x: currentTransform.x + direction.x * panStep,
196
- y: currentTransform.y + direction.y * panStep
197
- }, "keyboard");
198
- return true;
199
- }
200
- const zoomFactor = KEYBOARD_ZOOM_FACTORS[key];
201
- if (zoomFactor) {
202
- zoomBy(zoomFactor, "keyboard");
203
- return true;
204
- }
205
- if (key !== "0") return false;
206
- reset("keyboard");
207
- return true;
86
+ store.attachRoot(root);
87
+ const navigation = attachInfiniteCanvasNavigation(root, store, () => navigationOptionsRef.current);
88
+ navigationRef.current = navigation;
89
+ return () => {
90
+ navigation.detach();
91
+ navigationRef.current = null;
92
+ store.dispose();
93
+ store.attachRoot(null);
94
+ };
95
+ }, [store]);
96
+ const context = useMemo(() => ({
97
+ ...store.camera,
98
+ minScale,
99
+ maxScale
100
+ }), [
101
+ store,
102
+ minScale,
103
+ maxScale
104
+ ]);
105
+ const storeContext = useMemo(() => ({
106
+ store,
107
+ initialTransform
108
+ }), [store, initialTransform]);
109
+ function beginBackgroundPan(event) {
110
+ onPointerDown?.(event);
111
+ navigationRef.current?.beginBackgroundPan(event.nativeEvent);
208
112
  }
209
- function moveWithKeyboard(event) {
113
+ function navigateWithKeyboard(event) {
210
114
  onKeyDown?.(event);
211
- if (event.defaultPrevented || event.target !== event.currentTarget) return;
115
+ if (!keyboardNavigation || event.defaultPrevented || event.target !== event.currentTarget) return;
212
116
  if (event.ctrlKey || event.metaKey || event.altKey) return;
213
- if (applyKeyboardCommand(event.key, event.shiftKey)) event.preventDefault();
117
+ if (runInfiniteCanvasKeyCommand(store, event.key, event.shiftKey)) event.preventDefault();
214
118
  }
215
- const context = {
216
- transform,
217
- minScale,
218
- maxScale,
219
- reset,
220
- zoomBy
221
- };
222
- const canvasStyle = {
223
- ...style,
224
- backgroundPosition: `${transform.x}px ${transform.y}px`,
225
- backgroundSize: `${24 * transform.scale}px ${24 * transform.scale}px`
226
- };
227
- return /* @__PURE__ */ jsx(InfiniteCanvasContext.Provider, {
228
- value: context,
229
- children: /* @__PURE__ */ jsx("section", {
230
- ref: rootRef,
119
+ const keyShortcuts = [keyboardNavigation ? INFINITE_CANVAS_KEY_SHORTCUTS : "", ariaKeyShortcuts].filter(Boolean).join(" ");
120
+ const root = useRender({
121
+ defaultTagName: "section",
122
+ ref: [ref ?? null, rootRef],
123
+ props: {
124
+ tabIndex: 0,
231
125
  ...props,
232
126
  "data-control-ui": "infinite-canvas",
233
127
  "data-control-family": "infinite-canvas",
234
128
  "data-slot": "root",
235
- "data-panning": panning || void 0,
236
- "data-easing": easing || void 0,
237
129
  "aria-label": ariaLabel,
238
130
  role: "application",
239
- "aria-keyshortcuts": ariaKeyShortcuts ? `${CANVAS_KEY_SHORTCUTS} ${ariaKeyShortcuts}` : CANVAS_KEY_SHORTCUTS,
240
- tabIndex: 0,
241
- className: cn("relative isolate overflow-hidden overscroll-contain touch-none select-none data-panning:cursor-grabbing", className),
242
- style: canvasStyle,
243
- onPointerDown: beginPan,
244
- onPointerMove: updatePan,
245
- onPointerUp: endPan,
246
- onPointerCancel: cancelPan,
247
- onKeyDown: moveWithKeyboard,
131
+ "aria-keyshortcuts": keyShortcuts || void 0,
132
+ className: cn("relative isolate overflow-hidden overscroll-none touch-none data-panning:select-none", className),
133
+ style: {
134
+ ...style,
135
+ ...infiniteCanvasGridStyle(initialTransform)
136
+ },
137
+ onPointerDown: beginBackgroundPan,
138
+ onKeyDown: navigateWithKeyboard,
248
139
  children
140
+ }
141
+ });
142
+ return /* @__PURE__ */ jsx(InfiniteCanvasStoreContext.Provider, {
143
+ value: storeContext,
144
+ children: /* @__PURE__ */ jsx(InfiniteCanvasContext.Provider, {
145
+ value: context,
146
+ children: root
249
147
  })
250
148
  });
251
149
  }
252
150
  function InfiniteCanvasContent({ className, style, ...props }) {
253
- const { transform } = useInfiniteCanvas();
151
+ const { store, initialTransform } = useInfiniteCanvasStore();
254
152
  return /* @__PURE__ */ jsx("div", {
153
+ ref: store.attachContent,
255
154
  ...props,
256
155
  "data-control-ui": "infinite-canvas",
257
156
  "data-control-family": "infinite-canvas",
258
157
  "data-slot": "content",
259
- "data-scale": transform.scale,
260
- className: cn("pointer-events-none absolute top-0 left-0 h-0 w-0 transform-gpu will-change-transform [&>*]:pointer-events-auto", className),
158
+ "data-scale": initialTransform.scale,
159
+ className: cn("absolute top-0 left-0 h-0 w-0", className),
261
160
  style: {
262
161
  ...style,
263
- transform: `translate3d(${transform.x}px, ${transform.y}px, 0) scale(${transform.scale})`
162
+ transform: infiniteCanvasTransformCss(initialTransform)
264
163
  }
265
164
  });
266
165
  }
267
- function InfiniteCanvasItem({ x, y, onPositionChange, movableItemLabel = "Movable item", className, style, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onKeyDown, ...props }) {
268
- const { transform } = useInfiniteCanvas();
166
+ function useInfiniteCanvasItemDrag(position, onPositionChange, handlers) {
167
+ const canvas = useInfiniteCanvas();
269
168
  const dragRef = useRef(null);
270
169
  const [dragging, setDragging] = useState(false);
271
- function beginDrag(event) {
272
- onPointerDown?.(event);
273
- if (event.defaultPrevented || !onPositionChange || !event.isPrimary || event.button !== 0) return;
274
- event.currentTarget.setPointerCapture(event.pointerId);
275
- dragRef.current = {
276
- pointerId: event.pointerId,
277
- startPointer: {
278
- x: event.clientX,
279
- y: event.clientY
280
- },
281
- startPosition: {
282
- x,
283
- y
284
- }
285
- };
286
- setDragging(true);
287
- }
288
- function updateDrag(event) {
289
- onPointerMove?.(event);
290
- const drag = dragRef.current;
291
- if (event.defaultPrevented || !drag || drag.pointerId !== event.pointerId) return;
292
- onPositionChange?.({
293
- x: drag.startPosition.x + (event.clientX - drag.startPointer.x) / transform.scale,
294
- y: drag.startPosition.y + (event.clientY - drag.startPointer.y) / transform.scale
295
- });
296
- }
297
170
  function finishDrag(event) {
298
171
  if (dragRef.current?.pointerId !== event.pointerId) return;
299
172
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
300
173
  dragRef.current = null;
301
174
  setDragging(false);
302
175
  }
303
- function endDrag(event) {
304
- onPointerUp?.(event);
305
- finishDrag(event);
306
- }
307
- function cancelDrag(event) {
308
- onPointerCancel?.(event);
309
- finishDrag(event);
310
- }
311
- function moveWithKeyboard(event) {
312
- onKeyDown?.(event);
313
- if (event.defaultPrevented || !onPositionChange || event.target !== event.currentTarget) return;
314
- if (event.ctrlKey || event.metaKey || event.altKey) return;
315
- const direction = KEYBOARD_PAN_DIRECTIONS[event.key];
316
- if (!direction) return;
317
- event.preventDefault();
318
- const step = keyboardPanStep(event.shiftKey) / transform.scale;
319
- onPositionChange({
320
- x: x + direction.x * step,
321
- y: y + direction.y * step
322
- });
323
- }
324
- return /* @__PURE__ */ jsx("div", {
325
- ...onPositionChange ? {
326
- role: "group",
327
- "aria-roledescription": movableItemLabel,
328
- "aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown",
329
- tabIndex: 0,
330
- onPointerDown: beginDrag,
331
- onPointerMove: updateDrag,
332
- onPointerUp: endDrag,
333
- onPointerCancel: cancelDrag,
334
- onKeyDown: moveWithKeyboard
335
- } : {
336
- onPointerDown,
337
- onPointerMove,
338
- onPointerUp,
339
- onPointerCancel,
340
- onKeyDown
341
- },
176
+ return {
177
+ dragging,
178
+ dragHandlers: {
179
+ onPointerDown(event) {
180
+ handlers.onPointerDown?.(event);
181
+ if (event.defaultPrevented || !onPositionChange || !event.isPrimary || event.button !== 0) return;
182
+ event.currentTarget.setPointerCapture(event.pointerId);
183
+ dragRef.current = {
184
+ pointerId: event.pointerId,
185
+ startWorld: canvas.toWorldPoint(event),
186
+ startPosition: position
187
+ };
188
+ setDragging(true);
189
+ },
190
+ onPointerMove(event) {
191
+ handlers.onPointerMove?.(event);
192
+ const drag = dragRef.current;
193
+ if (event.defaultPrevented || drag?.pointerId !== event.pointerId) return;
194
+ const world = canvas.toWorldPoint(event);
195
+ onPositionChange?.({
196
+ x: drag.startPosition.x + world.x - drag.startWorld.x,
197
+ y: drag.startPosition.y + world.y - drag.startWorld.y
198
+ });
199
+ },
200
+ onPointerUp(event) {
201
+ handlers.onPointerUp?.(event);
202
+ finishDrag(event);
203
+ },
204
+ onPointerCancel(event) {
205
+ handlers.onPointerCancel?.(event);
206
+ finishDrag(event);
207
+ },
208
+ onLostPointerCapture(event) {
209
+ handlers.onLostPointerCapture?.(event);
210
+ finishDrag(event);
211
+ },
212
+ onKeyDown(event) {
213
+ handlers.onKeyDown?.(event);
214
+ if (event.defaultPrevented || !onPositionChange || event.target !== event.currentTarget) return;
215
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
216
+ const direction = ARROW_KEY_DIRECTIONS[event.key];
217
+ if (!direction) return;
218
+ event.preventDefault();
219
+ const step = keyboardStep(event.shiftKey) / canvas.getTransform().scale;
220
+ onPositionChange({
221
+ x: position.x + direction.x * step,
222
+ y: position.y + direction.y * step
223
+ });
224
+ }
225
+ }
226
+ };
227
+ }
228
+ function InfiniteCanvasItem({ x, y, onPositionChange, movableItemLabel = "Movable item", className, style, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, ...props }) {
229
+ const position = useMemo(() => ({
230
+ x,
231
+ y
232
+ }), [x, y]);
233
+ const { dragging, dragHandlers } = useInfiniteCanvasItemDrag(position, onPositionChange, {
234
+ onPointerDown,
235
+ onPointerMove,
236
+ onPointerUp,
237
+ onPointerCancel,
238
+ onLostPointerCapture,
239
+ onKeyDown
240
+ });
241
+ const movable = onPositionChange ? {
242
+ role: "group",
243
+ "aria-roledescription": movableItemLabel,
244
+ "aria-keyshortcuts": ITEM_KEY_SHORTCUTS,
245
+ tabIndex: 0
246
+ } : null;
247
+ return /* @__PURE__ */ jsx(InfiniteCanvasItemContext.Provider, {
248
+ value: position,
249
+ children: /* @__PURE__ */ jsx("div", {
250
+ ...movable,
251
+ ...props,
252
+ ...dragHandlers,
253
+ "data-control-ui": "infinite-canvas",
254
+ "data-control-family": "infinite-canvas",
255
+ "data-slot": "item",
256
+ "data-dragging": dragging || void 0,
257
+ className: cn("absolute", onPositionChange && "touch-none select-none", className),
258
+ style: {
259
+ ...style,
260
+ left: x,
261
+ top: y
262
+ }
263
+ })
264
+ });
265
+ }
266
+ function InfiniteCanvasItemHandle({ onPositionChange, className, children, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onLostPointerCapture, onKeyDown, "aria-label": ariaLabel = "Move item", ...props }) {
267
+ const position = use(InfiniteCanvasItemContext);
268
+ if (!position) throw new Error("InfiniteCanvasItemHandle must be used within InfiniteCanvasItem");
269
+ const { dragging, dragHandlers } = useInfiniteCanvasItemDrag(position, onPositionChange, {
270
+ onPointerDown,
271
+ onPointerMove,
272
+ onPointerUp,
273
+ onPointerCancel,
274
+ onLostPointerCapture,
275
+ onKeyDown
276
+ });
277
+ return /* @__PURE__ */ jsx(Button, {
278
+ type: "button",
279
+ variant: "ghost",
280
+ size: "xs",
281
+ iconOnly: true,
282
+ "aria-label": ariaLabel,
283
+ "aria-keyshortcuts": ITEM_KEY_SHORTCUTS,
342
284
  ...props,
343
- "data-control-ui": "infinite-canvas",
344
- "data-control-family": "infinite-canvas",
345
- "data-slot": "item",
285
+ ...dragHandlers,
346
286
  "data-dragging": dragging || void 0,
347
- className: cn("absolute", onPositionChange && "cursor-grab touch-none data-dragging:cursor-grabbing", className),
348
- style: {
349
- ...style,
350
- left: x,
351
- top: y
352
- }
287
+ className: cn("touch-none", className),
288
+ children: children ?? /* @__PURE__ */ jsx(GripVerticalIcon, {})
353
289
  });
354
290
  }
355
291
  function InfiniteCanvasControls({ className, ...props }) {
356
292
  const canvas = useInfiniteCanvas();
357
- const percentage = Math.round(canvas.transform.scale * 100);
358
- return /* @__PURE__ */ jsxs("div", {
293
+ const scale = useInfiniteCanvasTransform(selectScale);
294
+ const percentage = Math.round(scale * 100);
295
+ return /* @__PURE__ */ jsx("div", {
359
296
  "data-control-ui": "infinite-canvas",
360
297
  "data-control-family": "infinite-canvas",
361
298
  "data-slot": "controls",
362
- className: cn("absolute z-40 flex items-center", className),
299
+ className: cn("absolute z-40", className),
363
300
  ...props,
364
- children: [
365
- /* @__PURE__ */ jsx(Button, {
366
- type: "button",
367
- size: "sm",
368
- variant: "ghost",
369
- iconOnly: true,
370
- disabled: canvas.transform.scale <= canvas.minScale,
371
- "aria-label": "Zoom out",
372
- onClick: () => canvas.zoomBy(1 / 1.2, "control"),
373
- children: /* @__PURE__ */ jsx(MinusIcon, {})
374
- }),
375
- /* @__PURE__ */ jsxs(Button, {
376
- type: "button",
377
- size: "sm",
378
- variant: "ghost",
379
- className: "tabular-nums",
380
- "aria-label": `Reset canvas zoom, currently ${percentage}%`,
381
- onClick: () => canvas.reset("control"),
382
- children: [percentage, "%"]
383
- }),
384
- /* @__PURE__ */ jsx(Button, {
385
- type: "button",
386
- size: "sm",
387
- variant: "ghost",
388
- iconOnly: true,
389
- disabled: canvas.transform.scale >= canvas.maxScale,
390
- "aria-label": "Zoom in",
391
- onClick: () => canvas.zoomBy(1.2, "control"),
392
- children: /* @__PURE__ */ jsx(PlusIcon, {})
393
- })
394
- ]
301
+ children: /* @__PURE__ */ jsxs(Toolbar, {
302
+ variant: "floating",
303
+ "aria-label": "Canvas view",
304
+ children: [
305
+ /* @__PURE__ */ jsx(ToolbarButton, {
306
+ iconOnly: true,
307
+ disabled: scale <= canvas.minScale,
308
+ "aria-label": "Zoom out",
309
+ onClick: () => canvas.zoomBy(1 / CONTROL_ZOOM_FACTOR),
310
+ children: /* @__PURE__ */ jsx(MinusIcon, {})
311
+ }),
312
+ /* @__PURE__ */ jsxs(ToolbarButton, {
313
+ className: "tabular-nums",
314
+ "aria-label": `Reset canvas zoom, currently ${percentage}%`,
315
+ onClick: () => canvas.reset(),
316
+ children: [percentage, "%"]
317
+ }),
318
+ /* @__PURE__ */ jsx(ToolbarButton, {
319
+ iconOnly: true,
320
+ disabled: scale >= canvas.maxScale,
321
+ "aria-label": "Zoom in",
322
+ onClick: () => canvas.zoomBy(CONTROL_ZOOM_FACTOR),
323
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
324
+ }),
325
+ /* @__PURE__ */ jsx(ToolbarSeparator, {}),
326
+ /* @__PURE__ */ jsx(ToolbarButton, {
327
+ iconOnly: true,
328
+ "aria-label": "Fit view",
329
+ onClick: () => canvas.fit(),
330
+ children: /* @__PURE__ */ jsx(MaximizeIcon, {})
331
+ })
332
+ ]
333
+ })
395
334
  });
396
335
  }
397
336
  //#endregion
398
- export { InfiniteCanvas, InfiniteCanvasContent, InfiniteCanvasControls, InfiniteCanvasItem, useInfiniteCanvas };
337
+ export { InfiniteCanvas, InfiniteCanvasContent, InfiniteCanvasControls, InfiniteCanvasItem, InfiniteCanvasItemHandle, useInfiniteCanvas, useInfiniteCanvasTransform };
@@ -48,8 +48,9 @@ export type RichTooltipContentProps = Omit<ComponentProps<typeof Popover.Popup>,
48
48
  collisionPadding?: ComponentProps<typeof Popover.Positioner>["collisionPadding"];
49
49
  arrow?: boolean;
50
50
  scrollAnchorIntoView?: boolean;
51
+ container?: ComponentProps<typeof Popover.Portal>["container"];
51
52
  };
52
- export declare function RichTooltipContent({ className, children, side, align, sideOffset, anchor, collisionPadding, arrow, scrollAnchorIntoView, ...props }: RichTooltipContentProps): import("react").JSX.Element;
53
+ export declare function RichTooltipContent({ className, children, side, align, sideOffset, anchor, collisionPadding, arrow, scrollAnchorIntoView, container, ...props }: RichTooltipContentProps): import("react").JSX.Element;
53
54
  export declare function RichTooltipMedia({ className, ...props }: ComponentProps<"div"> & {
54
55
  style?: CSSProperties & PopupKnobStyle;
55
56
  }): import("react").JSX.Element;