@ctrl-ui/react 0.9.5 → 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 (64) hide show
  1. package/README.md +2 -2
  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/design-canvas-data.d.ts +1 -1
  16. package/dist/blocks/design-canvas.d.ts +2 -1
  17. package/dist/blocks/design-canvas.js +15 -8
  18. package/dist/chat-message.js +33 -39
  19. package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
  20. package/dist/knob-contracts/infinite-canvas-knobs.js +1 -7
  21. package/dist/knob-contracts/swap-knobs.d.ts +4 -0
  22. package/dist/knob-contracts/swap-knobs.js +9 -0
  23. package/dist/knob-contracts/timeline-knobs.d.ts +1 -1
  24. package/dist/knob-contracts/timeline-knobs.js +1 -0
  25. package/dist/knob-contracts/tool-approval-knobs.d.ts +4 -0
  26. package/dist/knob-contracts/tool-approval-knobs.js +32 -0
  27. package/dist/lib/infinite-canvas-camera-store.d.ts +65 -0
  28. package/dist/lib/infinite-canvas-camera-store.js +233 -0
  29. package/dist/lib/infinite-canvas-camera.d.ts +33 -0
  30. package/dist/lib/infinite-canvas-camera.js +98 -0
  31. package/dist/lib/infinite-canvas-navigation.d.ts +19 -0
  32. package/dist/lib/infinite-canvas-navigation.js +395 -0
  33. package/dist/markdown-editor/bubble-menu.d.ts +6 -0
  34. package/dist/markdown-editor/bubble-menu.js +45 -0
  35. package/dist/markdown-editor/toolbar.d.ts +17 -1
  36. package/dist/markdown-editor/toolbar.js +103 -81
  37. package/dist/styles/index.css +10 -8
  38. package/dist/styles/recipes/activity.css +8 -0
  39. package/dist/styles/recipes/button-group.css +12 -6
  40. package/dist/styles/recipes/button.css +12 -2
  41. package/dist/styles/recipes/chat-message-edit.css +6 -21
  42. package/dist/styles/recipes/infinite-canvas.css +17 -57
  43. package/dist/styles/recipes/markdown-editor.css +12 -0
  44. package/dist/styles/recipes/swap.css +143 -0
  45. package/dist/styles/recipes/timeline.css +11 -0
  46. package/dist/styles/recipes/tool-approval.css +267 -0
  47. package/dist/styles/recipes/track-highlight.css +19 -6
  48. package/dist/styles/theme.css +6 -5
  49. package/dist/styles/type-scale.css +1 -1
  50. package/dist/tool-approval.d.ts +49 -0
  51. package/dist/tool-approval.js +260 -0
  52. package/dist/ui/code.js +13 -3
  53. package/dist/ui/command.js +1 -0
  54. package/dist/ui/infinite-canvas.d.ts +19 -22
  55. package/dist/ui/infinite-canvas.js +264 -325
  56. package/dist/ui/rich-tooltip.d.ts +2 -1
  57. package/dist/ui/rich-tooltip.js +32 -29
  58. package/dist/ui/swap.d.ts +14 -0
  59. package/dist/ui/swap.js +69 -0
  60. package/dist/ui/timeline.d.ts +1 -1
  61. package/dist/ui/timeline.js +1 -1
  62. package/dist/user-ask.js +1 -1
  63. package/lint/no-raw-type-classes.grit +2 -2
  64. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
- import { ChatLayout } from "../chat-layout.js";
2
1
  import { SidebarProviderProps } from "../ui/sidebar-provider.js";
3
2
  import "../ui/sidebar.js";
3
+ import { ChatLayout } from "../chat-layout.js";
4
4
  import { ComponentProps, ReactNode } from "react";
5
5
  //#region src/blocks/coding-agent.d.ts
6
6
  export type CodingAgentNavigationItem = {
@@ -1,6 +1,6 @@
1
1
  //#region src/blocks/design-canvas-data.d.ts
2
2
  export type DesignCanvasLayerKind = "frame" | "rectangle" | "ellipse" | "text";
3
- export type DesignCanvasTool = "move" | DesignCanvasLayerKind;
3
+ export type DesignCanvasTool = "move" | "hand" | DesignCanvasLayerKind;
4
4
  export type DesignCanvasFill = {
5
5
  color: string;
6
6
  opacity: number;
@@ -1,5 +1,6 @@
1
1
  import { DesignCanvasFill, DesignCanvasLayer, DesignCanvasLayerKind, DesignCanvasTool } from "./design-canvas-data.js";
2
- import { InfiniteCanvasTransform } from "../ui/infinite-canvas.js";
2
+ import { InfiniteCanvasTransform } from "../lib/infinite-canvas-camera.js";
3
+ import "../ui/infinite-canvas.js";
3
4
  import { ComponentProps } from "react";
4
5
  //#region src/blocks/design-canvas.d.ts
5
6
  export type DesignCanvasBlockProps = Omit<ComponentProps<"div">, "children" | "defaultValue" | "onChange"> & {
@@ -7,12 +7,12 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/
7
7
  import { Drawer, DrawerContent, DrawerTitle } from "../ui/drawer.js";
8
8
  import { createDesignCanvasLayer, designCanvasFillColor, drawnDesignCanvasBounds } from "./design-canvas-data.js";
9
9
  import { DesignCanvasInspector } from "./design-canvas-inspector.js";
10
+ import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator } from "../ui/toolbar.js";
10
11
  import { InfiniteCanvas, InfiniteCanvasContent, InfiniteCanvasControls, InfiniteCanvasItem } from "../ui/infinite-canvas.js";
11
12
  import { ResizableFloatingPanel } from "../ui/resizable.js";
12
- import { Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator } from "../ui/toolbar.js";
13
13
  import { useRef, useState } from "react";
14
14
  import { jsx, jsxs } from "react/jsx-runtime";
15
- import { CircleIcon, FrameIcon, LayersIcon, MousePointer2Icon, SlidersHorizontalIcon, SquareIcon, TypeIcon } from "lucide-react";
15
+ import { CircleIcon, FrameIcon, HandIcon, LayersIcon, MousePointer2Icon, SlidersHorizontalIcon, SquareIcon, TypeIcon } from "lucide-react";
16
16
  //#region src/blocks/design-canvas.tsx
17
17
  const TOOLS = [
18
18
  {
@@ -21,6 +21,12 @@ const TOOLS = [
21
21
  shortcut: "V",
22
22
  icon: /* @__PURE__ */ jsx(MousePointer2Icon, {})
23
23
  },
24
+ {
25
+ tool: "hand",
26
+ label: "Hand",
27
+ shortcut: "H",
28
+ icon: /* @__PURE__ */ jsx(HandIcon, {})
29
+ },
24
30
  {
25
31
  tool: "frame",
26
32
  label: "Frame",
@@ -99,6 +105,7 @@ function DesignCanvasBlock({ layers, defaultLayers = [], onLayersChange, selecte
99
105
  const currentLayers = layers ?? internalLayers;
100
106
  const currentSelectedLayerId = selectedLayerId === void 0 ? internalSelectedLayerId : selectedLayerId;
101
107
  const selectedLayer = currentLayers.find((layer) => layer.id === currentSelectedLayerId);
108
+ const drawingKind = tool === "move" || tool === "hand" ? null : tool;
102
109
  function changeLayers(next) {
103
110
  if (layers === void 0) setInternalLayers(next);
104
111
  onLayersChange?.(next);
@@ -139,7 +146,7 @@ function DesignCanvasBlock({ layers, defaultLayers = [], onLayersChange, selecte
139
146
  }
140
147
  function beginDrawOrDeselect(event) {
141
148
  if (event.defaultPrevented || event.target !== event.currentTarget) return;
142
- if (tool === "move") {
149
+ if (!drawingKind) {
143
150
  selectLayer(null);
144
151
  return;
145
152
  }
@@ -148,7 +155,7 @@ function DesignCanvasBlock({ layers, defaultLayers = [], onLayersChange, selecte
148
155
  event.currentTarget.setPointerCapture(event.pointerId);
149
156
  const point = worldPointAt(event);
150
157
  setDraft({
151
- kind: tool,
158
+ kind: drawingKind,
152
159
  pointerId: event.pointerId,
153
160
  start: point,
154
161
  end: point,
@@ -200,7 +207,7 @@ function DesignCanvasBlock({ layers, defaultLayers = [], onLayersChange, selecte
200
207
  setDraft(null);
201
208
  setTool("move");
202
209
  selectLayer(null);
203
- } else if (event.key === "Enter" && tool !== "move" && event.target === event.currentTarget) addLayerAtViewportCenter(tool, event.currentTarget);
210
+ } else if (event.key === "Enter" && drawingKind && event.target === event.currentTarget) addLayerAtViewportCenter(drawingKind, event.currentTarget);
204
211
  else if (!selectedLayer || !editLayerWithKey(selectedLayer, event)) return;
205
212
  event.preventDefault();
206
213
  }
@@ -226,7 +233,8 @@ function DesignCanvasBlock({ layers, defaultLayers = [], onLayersChange, selecte
226
233
  transformRef.current = next;
227
234
  },
228
235
  "data-tool": tool,
229
- className: cn("size-full", tool !== "move" && "cursor-crosshair"),
236
+ panOnDrag: tool === "hand" ? "anywhere" : "background",
237
+ className: cn("size-full", drawingKind && "cursor-crosshair"),
230
238
  style: {
231
239
  "--cui-infinite-canvas-radius": "0px",
232
240
  "--cui-infinite-canvas-border-color": "transparent",
@@ -452,8 +460,7 @@ function DesignCanvasToolbarButton({ label, hint, pressed, children, ...props })
452
460
  ...props,
453
461
  "aria-label": label,
454
462
  iconOnly: true,
455
- "aria-pressed": pressed,
456
- "data-pressed": pressed ? "" : void 0
463
+ "aria-pressed": pressed
457
464
  }),
458
465
  children
459
466
  }), /* @__PURE__ */ jsx(TooltipContent, { children: hint })] });
@@ -224,6 +224,7 @@ function ChatMessageEditable({ value, onSave, onSaveError, editorLabel = "Edit m
224
224
  children: [
225
225
  /* @__PURE__ */ jsxs(ChatMessageContent, {
226
226
  "data-editable": "",
227
+ "data-focus-ring": "within",
227
228
  children: [
228
229
  /* @__PURE__ */ jsx("span", {
229
230
  "data-control-ui": "chat-message",
@@ -268,47 +269,40 @@ function ChatMessageEditable({ value, onSave, onSaveError, editorLabel = "Edit m
268
269
  })
269
270
  ]
270
271
  }),
271
- /* @__PURE__ */ jsxs("div", {
272
+ /* @__PURE__ */ jsx("div", {
272
273
  "data-control-ui": "chat-message",
273
274
  "data-control-family": "chat-message",
274
- "data-slot": "edit-toolbars",
275
- children: [/* @__PURE__ */ jsx("div", {
276
- "data-control-ui": "chat-message",
277
- "data-control-family": "chat-message",
278
- "data-slot": "edit-toolbar",
279
- "data-active": !editing ? "" : void 0,
280
- inert: editing,
281
- "aria-hidden": editing,
282
- children: /* @__PURE__ */ jsx(ActionBar, {
283
- align: message.isEndAligned ? "end" : "start",
284
- copyValue: value,
285
- onEdit: startEditing,
286
- children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActionBarCopy, {}), /* @__PURE__ */ jsx(ActionBarEdit, {})] })
287
- })
288
- }), /* @__PURE__ */ jsx("div", {
289
- "data-control-ui": "chat-message",
290
- "data-control-family": "chat-message",
291
- "data-slot": "edit-toolbar",
292
- "data-active": editing ? "" : void 0,
293
- inert: !editing,
294
- "aria-hidden": !editing,
295
- children: /* @__PURE__ */ jsxs(ActionBar, {
296
- align: message.isEndAligned ? "end" : "start",
297
- label: "Edit actions",
298
- "aria-busy": saving,
299
- children: [/* @__PURE__ */ jsx(ActionBarItem, {
300
- type: "button",
301
- disabled: saving || !draft.trim(),
302
- onClick: () => void save(),
303
- children: saveLabel
304
- }), /* @__PURE__ */ jsx(ActionBarItem, {
305
- type: "button",
306
- disabled: saving,
307
- onClick: cancelEditing,
308
- children: cancelLabel
309
- })]
310
- })
311
- })]
275
+ "data-slot": "read-actions",
276
+ children: /* @__PURE__ */ jsx(ActionBar, {
277
+ align: message.isEndAligned ? "end" : "start",
278
+ copyValue: value,
279
+ onEdit: startEditing,
280
+ children: children ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActionBarCopy, {}), /* @__PURE__ */ jsx(ActionBarEdit, {})] })
281
+ })
282
+ }),
283
+ /* @__PURE__ */ jsx("div", {
284
+ "data-control-ui": "chat-message",
285
+ "data-control-family": "chat-message",
286
+ "data-slot": "edit-actions",
287
+ children: /* @__PURE__ */ jsxs(ActionBar, {
288
+ align: message.isEndAligned ? "end" : "start",
289
+ label: "Edit actions",
290
+ "aria-busy": saving,
291
+ style: { "--cui-action-bar-hidden-opacity": "1" },
292
+ children: [/* @__PURE__ */ jsx(ActionBarItem, {
293
+ type: "button",
294
+ variant: "surface",
295
+ disabled: saving,
296
+ onClick: cancelEditing,
297
+ children: cancelLabel
298
+ }), /* @__PURE__ */ jsx(ActionBarItem, {
299
+ type: "button",
300
+ variant: "solid",
301
+ disabled: saving || !draft.trim(),
302
+ onClick: () => void save(),
303
+ children: saveLabel
304
+ })]
305
+ })
312
306
  }),
313
307
  /* @__PURE__ */ jsx(LiveStatus, { message: saving ? savingLabel : "" }),
314
308
  /* @__PURE__ */ jsx("span", {
@@ -1,4 +1,4 @@
1
1
  //#region src/knob-contracts/infinite-canvas-knobs.d.ts
2
- export declare const infiniteCanvasKnobs: readonly ["--cui-infinite-canvas-radius", "--cui-infinite-canvas-background", "--cui-infinite-canvas-grid-dot-color", "--cui-infinite-canvas-border-color", "--cui-infinite-canvas-padding", "--cui-infinite-canvas-controls-item-radius", "--cui-infinite-canvas-controls-padding", "--cui-infinite-canvas-controls-radius", "--cui-infinite-canvas-controls-background", "--cui-infinite-canvas-controls-border-color", "--cui-infinite-canvas-controls-shadow"];
2
+ export declare const infiniteCanvasKnobs: readonly ["--cui-infinite-canvas-radius", "--cui-infinite-canvas-background", "--cui-infinite-canvas-grid-dot-color", "--cui-infinite-canvas-border-color", "--cui-infinite-canvas-padding"];
3
3
  export type InfiniteCanvasKnobStyle = Partial<Record<(typeof infiniteCanvasKnobs)[number], string>>;
4
4
  //#endregion
@@ -4,13 +4,7 @@ const infiniteCanvasKnobs = [
4
4
  "--cui-infinite-canvas-background",
5
5
  "--cui-infinite-canvas-grid-dot-color",
6
6
  "--cui-infinite-canvas-border-color",
7
- "--cui-infinite-canvas-padding",
8
- "--cui-infinite-canvas-controls-item-radius",
9
- "--cui-infinite-canvas-controls-padding",
10
- "--cui-infinite-canvas-controls-radius",
11
- "--cui-infinite-canvas-controls-background",
12
- "--cui-infinite-canvas-controls-border-color",
13
- "--cui-infinite-canvas-controls-shadow"
7
+ "--cui-infinite-canvas-padding"
14
8
  ];
15
9
  //#endregion
16
10
  export { infiniteCanvasKnobs };
@@ -0,0 +1,4 @@
1
+ //#region src/knob-contracts/swap-knobs.d.ts
2
+ export declare const swapKnobs: readonly ["--cui-swap-animation-duration", "--cui-swap-easing", "--cui-swap-label-inset", "--cui-swap-icon-scale"];
3
+ export type SwapKnobStyle = Partial<Record<(typeof swapKnobs)[number], string>>;
4
+ //#endregion
@@ -0,0 +1,9 @@
1
+ //#region src/knob-contracts/swap-knobs.ts
2
+ const swapKnobs = [
3
+ "--cui-swap-animation-duration",
4
+ "--cui-swap-easing",
5
+ "--cui-swap-label-inset",
6
+ "--cui-swap-icon-scale"
7
+ ];
8
+ //#endregion
9
+ export { swapKnobs };
@@ -1,4 +1,4 @@
1
1
  //#region src/knob-contracts/timeline-knobs.d.ts
2
- export declare const timelineKnobs: readonly ["--cui-timeline-indicator-background", "--cui-timeline-indicator-foreground", "--cui-timeline-running-foreground", "--cui-timeline-success-foreground", "--cui-timeline-error-foreground", "--cui-timeline-separator-background", "--cui-timeline-title-foreground"];
2
+ export declare const timelineKnobs: readonly ["--cui-timeline-indicator-background", "--cui-timeline-indicator-foreground", "--cui-timeline-running-foreground", "--cui-timeline-waiting-foreground", "--cui-timeline-success-foreground", "--cui-timeline-error-foreground", "--cui-timeline-separator-background", "--cui-timeline-title-foreground"];
3
3
  export type TimelineKnobStyle = Partial<Record<(typeof timelineKnobs)[number], string>>;
4
4
  //#endregion
@@ -3,6 +3,7 @@ const timelineKnobs = [
3
3
  "--cui-timeline-indicator-background",
4
4
  "--cui-timeline-indicator-foreground",
5
5
  "--cui-timeline-running-foreground",
6
+ "--cui-timeline-waiting-foreground",
6
7
  "--cui-timeline-success-foreground",
7
8
  "--cui-timeline-error-foreground",
8
9
  "--cui-timeline-separator-background",
@@ -0,0 +1,4 @@
1
+ //#region src/knob-contracts/tool-approval-knobs.d.ts
2
+ export declare const toolApprovalKnobs: readonly ["--cui-tool-approval-background", "--cui-tool-approval-foreground", "--cui-tool-approval-border-color", "--cui-tool-approval-border-width", "--cui-tool-approval-radius", "--cui-tool-approval-shadow", "--cui-tool-approval-padding", "--cui-tool-approval-body-background", "--cui-tool-approval-body-border-color", "--cui-tool-approval-body-border-width", "--cui-tool-approval-body-radius", "--cui-tool-approval-body-shadow", "--cui-tool-approval-body-padding", "--cui-tool-approval-body-gap", "--cui-tool-approval-footer-border-color", "--cui-tool-approval-footer-border-width", "--cui-tool-approval-footer-padding", "--cui-tool-approval-footer-gap", "--cui-tool-approval-preview-background", "--cui-tool-approval-preview-border-color", "--cui-tool-approval-preview-border-width", "--cui-tool-approval-preview-radius", "--cui-tool-approval-preview-padding", "--cui-tool-approval-preview-gap", "--cui-tool-approval-label-foreground", "--cui-tool-approval-description-foreground", "--cui-tool-approval-status-foreground"];
3
+ export type ToolApprovalKnobStyle = Partial<Record<(typeof toolApprovalKnobs)[number], string>>;
4
+ //#endregion
@@ -0,0 +1,32 @@
1
+ //#region src/knob-contracts/tool-approval-knobs.ts
2
+ const toolApprovalKnobs = [
3
+ "--cui-tool-approval-background",
4
+ "--cui-tool-approval-foreground",
5
+ "--cui-tool-approval-border-color",
6
+ "--cui-tool-approval-border-width",
7
+ "--cui-tool-approval-radius",
8
+ "--cui-tool-approval-shadow",
9
+ "--cui-tool-approval-padding",
10
+ "--cui-tool-approval-body-background",
11
+ "--cui-tool-approval-body-border-color",
12
+ "--cui-tool-approval-body-border-width",
13
+ "--cui-tool-approval-body-radius",
14
+ "--cui-tool-approval-body-shadow",
15
+ "--cui-tool-approval-body-padding",
16
+ "--cui-tool-approval-body-gap",
17
+ "--cui-tool-approval-footer-border-color",
18
+ "--cui-tool-approval-footer-border-width",
19
+ "--cui-tool-approval-footer-padding",
20
+ "--cui-tool-approval-footer-gap",
21
+ "--cui-tool-approval-preview-background",
22
+ "--cui-tool-approval-preview-border-color",
23
+ "--cui-tool-approval-preview-border-width",
24
+ "--cui-tool-approval-preview-radius",
25
+ "--cui-tool-approval-preview-padding",
26
+ "--cui-tool-approval-preview-gap",
27
+ "--cui-tool-approval-label-foreground",
28
+ "--cui-tool-approval-description-foreground",
29
+ "--cui-tool-approval-status-foreground"
30
+ ];
31
+ //#endregion
32
+ export { toolApprovalKnobs };
@@ -0,0 +1,65 @@
1
+ import { InfiniteCanvasBounds, InfiniteCanvasPoint, InfiniteCanvasTransform } from "./infinite-canvas-camera.js";
2
+ //#region src/lib/infinite-canvas-camera-store.d.ts
3
+ export type InfiniteCanvasMoveReason = "pointer" | "pinch" | "wheel" | "keyboard" | "control" | "reveal";
4
+ export type InfiniteCanvasMoveDetails = {
5
+ reason: InfiniteCanvasMoveReason;
6
+ };
7
+ export type InfiniteCanvasMoveOptions = {
8
+ reason?: InfiniteCanvasMoveReason;
9
+ };
10
+ export type InfiniteCanvasZoomOptions = InfiniteCanvasMoveOptions & {
11
+ origin?: InfiniteCanvasPoint;
12
+ };
13
+ export type InfiniteCanvasRevealOptions = InfiniteCanvasMoveOptions & {
14
+ padding?: number;
15
+ area?: InfiniteCanvasBounds;
16
+ };
17
+ export type InfiniteCanvasFitOptions = InfiniteCanvasRevealOptions & {
18
+ maxScale?: number;
19
+ };
20
+ export type InfiniteCanvasTransformUpdate = InfiniteCanvasTransform | ((current: InfiniteCanvasTransform) => InfiniteCanvasTransform);
21
+ export type InfiniteCanvasClientPoint = {
22
+ clientX: number;
23
+ clientY: number;
24
+ };
25
+ export type InfiniteCanvasCamera = {
26
+ getTransform: () => InfiniteCanvasTransform;
27
+ setTransform: (update: InfiniteCanvasTransformUpdate, options?: InfiniteCanvasMoveOptions) => void;
28
+ zoomBy: (factor: number, options?: InfiniteCanvasZoomOptions) => void;
29
+ zoomTo: (scale: number, options?: InfiniteCanvasZoomOptions) => void;
30
+ reset: (options?: InfiniteCanvasMoveOptions) => void;
31
+ fit: (options?: InfiniteCanvasFitOptions) => void;
32
+ fitBounds: (bounds: InfiniteCanvasBounds, options?: InfiniteCanvasFitOptions) => void;
33
+ revealBounds: (bounds: InfiniteCanvasBounds, options?: InfiniteCanvasRevealOptions) => void;
34
+ toWorldPoint: (point: InfiniteCanvasClientPoint) => InfiniteCanvasPoint;
35
+ subscribe: (listener: () => void) => () => void;
36
+ };
37
+ export type InfiniteCanvasCameraOptions = {
38
+ minScale: number;
39
+ maxScale: number;
40
+ defaultTransform: InfiniteCanvasTransform;
41
+ controlled: boolean;
42
+ onTransformChange?: (transform: InfiniteCanvasTransform, details: InfiniteCanvasMoveDetails) => void;
43
+ };
44
+ export type InfiniteCanvasCameraStore = InfiniteCanvasCamera & {
45
+ camera: InfiniteCanvasCamera;
46
+ configure: (options: InfiniteCanvasCameraOptions) => void;
47
+ attachRoot: (element: HTMLElement | null) => void;
48
+ attachContent: (element: HTMLElement | null) => void;
49
+ contentElement: () => HTMLElement | null;
50
+ getCommittedTransform: () => InfiniteCanvasTransform;
51
+ limitScale: (scale: number) => number;
52
+ move: (next: InfiniteCanvasTransform, reason: InfiniteCanvasMoveReason, eased?: boolean) => void;
53
+ moveNow: (next: InfiniteCanvasTransform, reason: InfiniteCanvasMoveReason) => void;
54
+ adopt: (next: InfiniteCanvasTransform) => void;
55
+ revealElement: (element: Element) => void;
56
+ toLocalPoint: (point: InfiniteCanvasClientPoint) => InfiniteCanvasPoint;
57
+ dispose: () => void;
58
+ };
59
+ export declare function infiniteCanvasTransformCss({ x, y, scale }: InfiniteCanvasTransform): string;
60
+ export declare function infiniteCanvasGridStyle({ x, y, scale }: InfiniteCanvasTransform): {
61
+ backgroundPosition: string;
62
+ backgroundSize: string;
63
+ };
64
+ export declare function createInfiniteCanvasCamera(initial: InfiniteCanvasTransform, initialOptions: InfiniteCanvasCameraOptions): InfiniteCanvasCameraStore;
65
+ //#endregion
@@ -0,0 +1,233 @@
1
+ import "./canvas-grid.js";
2
+ import { boundedTransform, clampScale, fitTransform, revealOffset, sameTransform, toWorldPoint, unionBounds, zoomAroundPoint } from "./infinite-canvas-camera.js";
3
+ //#region src/lib/infinite-canvas-camera-store.ts
4
+ const EASED_BY_REASON = {
5
+ pointer: false,
6
+ pinch: false,
7
+ wheel: false,
8
+ keyboard: true,
9
+ control: true,
10
+ reveal: false
11
+ };
12
+ const CAMERA_IDLE_MS = 240;
13
+ const DEFAULT_FIT_PADDING_PX = 48;
14
+ const DEFAULT_FIT_MAX_SCALE = 1;
15
+ const DEFAULT_REVEAL_PADDING_PX = 24;
16
+ const MIN_GRID_SPACING_PX = 12;
17
+ function infiniteCanvasTransformCss({ x, y, scale }) {
18
+ return `translate(${x}px, ${y}px) scale(${scale})`;
19
+ }
20
+ function infiniteCanvasGridStyle({ x, y, scale }) {
21
+ let spacing = 24 * scale;
22
+ while (spacing < MIN_GRID_SPACING_PX) spacing *= 2;
23
+ return {
24
+ backgroundPosition: `${x}px ${y}px`,
25
+ backgroundSize: `${spacing}px ${spacing}px`
26
+ };
27
+ }
28
+ function createInfiniteCanvasCamera(initial, initialOptions) {
29
+ let options = initialOptions;
30
+ let latest = initial;
31
+ let committed = initial;
32
+ let pending = null;
33
+ let lastEmitted = null;
34
+ let frame = 0;
35
+ let idleTimer;
36
+ let root = null;
37
+ let content = null;
38
+ const listeners = /* @__PURE__ */ new Set();
39
+ function writeTransform() {
40
+ if (content) {
41
+ content.style.transform = infiniteCanvasTransformCss(committed);
42
+ content.dataset.scale = String(committed.scale);
43
+ }
44
+ if (root) Object.assign(root.style, infiniteCanvasGridStyle(committed));
45
+ }
46
+ function markMotion(eased) {
47
+ const element = root;
48
+ if (!element) return;
49
+ element.toggleAttribute("data-moving", true);
50
+ if (eased) element.setAttribute("data-easing", "true");
51
+ else element.removeAttribute("data-easing");
52
+ clearTimeout(idleTimer);
53
+ idleTimer = setTimeout(() => element.removeAttribute("data-moving"), CAMERA_IDLE_MS);
54
+ }
55
+ function commit(next, eased) {
56
+ committed = next;
57
+ markMotion(eased);
58
+ writeTransform();
59
+ for (const listener of listeners) listener();
60
+ }
61
+ function flush() {
62
+ frame = 0;
63
+ if (!pending) return;
64
+ const { reason, eased } = pending;
65
+ pending = null;
66
+ lastEmitted = {
67
+ transform: latest,
68
+ eased
69
+ };
70
+ if (!options.controlled) commit(latest, eased);
71
+ options.onTransformChange?.(latest, { reason });
72
+ }
73
+ function flushNow() {
74
+ if (!frame) return;
75
+ cancelAnimationFrame(frame);
76
+ flush();
77
+ }
78
+ function move(next, reason, eased = EASED_BY_REASON[reason]) {
79
+ const bounded = boundedTransform(next, options.minScale, options.maxScale);
80
+ if (!bounded || sameTransform(bounded, latest)) return;
81
+ latest = bounded;
82
+ pending = {
83
+ reason,
84
+ eased
85
+ };
86
+ frame ||= requestAnimationFrame(flush);
87
+ }
88
+ function viewportArea() {
89
+ return {
90
+ x: 0,
91
+ y: 0,
92
+ width: root?.clientWidth ?? 0,
93
+ height: root?.clientHeight ?? 0
94
+ };
95
+ }
96
+ function toLocalPoint(point) {
97
+ const bounds = root?.getBoundingClientRect();
98
+ return {
99
+ x: point.clientX - (bounds?.left ?? 0),
100
+ y: point.clientY - (bounds?.top ?? 0)
101
+ };
102
+ }
103
+ function contentBounds() {
104
+ if (!content) return null;
105
+ const origin = content.getBoundingClientRect();
106
+ const renderedScale = new DOMMatrixReadOnly(getComputedStyle(content).transform).a || committed.scale;
107
+ const rects = [];
108
+ for (const child of content.children) for (const element of [child, ...child.children]) {
109
+ const bounds = element.getBoundingClientRect();
110
+ if (bounds.width === 0 && bounds.height === 0) continue;
111
+ rects.push({
112
+ x: (bounds.left - origin.left) / renderedScale,
113
+ y: (bounds.top - origin.top) / renderedScale,
114
+ width: bounds.width / renderedScale,
115
+ height: bounds.height / renderedScale
116
+ });
117
+ }
118
+ return unionBounds(rects);
119
+ }
120
+ function revealScreenBounds(screenBounds, revealOptions) {
121
+ const offset = revealOffset(screenBounds, revealOptions.area ?? viewportArea(), revealOptions.padding ?? DEFAULT_REVEAL_PADDING_PX);
122
+ if (offset.x === 0 && offset.y === 0) return;
123
+ move({
124
+ ...latest,
125
+ x: latest.x + offset.x,
126
+ y: latest.y + offset.y
127
+ }, revealOptions.reason ?? "control");
128
+ }
129
+ function zoomTo(scale, zoomOptions = {}) {
130
+ const area = viewportArea();
131
+ const origin = zoomOptions.origin ?? {
132
+ x: area.width / 2,
133
+ y: area.height / 2
134
+ };
135
+ move(zoomAroundPoint(latest, clampScale(scale, options.minScale, options.maxScale), origin), zoomOptions.reason ?? "control");
136
+ }
137
+ function fitBounds(bounds, fitOptions = {}) {
138
+ const limits = {
139
+ padding: fitOptions.padding ?? DEFAULT_FIT_PADDING_PX,
140
+ minScale: options.minScale,
141
+ maxScale: Math.min(fitOptions.maxScale ?? DEFAULT_FIT_MAX_SCALE, options.maxScale)
142
+ };
143
+ move(fitTransform(bounds, fitOptions.area ?? viewportArea(), limits), fitOptions.reason ?? "control");
144
+ }
145
+ const camera = {
146
+ getTransform: () => latest,
147
+ setTransform(update, moveOptions = {}) {
148
+ move(typeof update === "function" ? update(latest) : update, moveOptions.reason ?? "control");
149
+ },
150
+ zoomTo,
151
+ zoomBy(factor, zoomOptions = {}) {
152
+ zoomTo(latest.scale * factor, zoomOptions);
153
+ },
154
+ reset(moveOptions = {}) {
155
+ move(options.defaultTransform, moveOptions.reason ?? "control");
156
+ },
157
+ fitBounds,
158
+ fit(fitOptions = {}) {
159
+ const bounds = contentBounds();
160
+ if (bounds) fitBounds(bounds, fitOptions);
161
+ else move(options.defaultTransform, fitOptions.reason ?? "control");
162
+ },
163
+ revealBounds(bounds, revealOptions = {}) {
164
+ revealScreenBounds({
165
+ x: latest.x + bounds.x * latest.scale,
166
+ y: latest.y + bounds.y * latest.scale,
167
+ width: bounds.width * latest.scale,
168
+ height: bounds.height * latest.scale
169
+ }, revealOptions);
170
+ },
171
+ toWorldPoint: (point) => toWorldPoint(latest, toLocalPoint(point)),
172
+ subscribe(listener) {
173
+ listeners.add(listener);
174
+ return () => {
175
+ listeners.delete(listener);
176
+ };
177
+ }
178
+ };
179
+ return {
180
+ ...camera,
181
+ camera,
182
+ configure(next) {
183
+ options = next;
184
+ },
185
+ attachRoot(element) {
186
+ root = element;
187
+ writeTransform();
188
+ },
189
+ attachContent(element) {
190
+ content = element;
191
+ writeTransform();
192
+ },
193
+ contentElement: () => content,
194
+ getCommittedTransform: () => committed,
195
+ limitScale: (scale) => clampScale(scale, options.minScale, options.maxScale),
196
+ move,
197
+ moveNow(next, reason) {
198
+ move(next, reason, false);
199
+ flushNow();
200
+ },
201
+ adopt(next) {
202
+ const bounded = boundedTransform(next, options.minScale, options.maxScale);
203
+ if (!bounded) return;
204
+ const echo = lastEmitted && sameTransform(bounded, lastEmitted.transform) ? lastEmitted : null;
205
+ if (!echo) {
206
+ cancelAnimationFrame(frame);
207
+ frame = 0;
208
+ pending = null;
209
+ }
210
+ if (!pending) latest = bounded;
211
+ if (!sameTransform(bounded, committed)) commit(bounded, echo?.eased ?? false);
212
+ },
213
+ revealElement(element) {
214
+ const bounds = element.getBoundingClientRect();
215
+ revealScreenBounds({
216
+ ...toLocalPoint({
217
+ clientX: bounds.left,
218
+ clientY: bounds.top
219
+ }),
220
+ width: bounds.width,
221
+ height: bounds.height
222
+ }, { reason: "reveal" });
223
+ flushNow();
224
+ },
225
+ toLocalPoint,
226
+ dispose() {
227
+ flushNow();
228
+ clearTimeout(idleTimer);
229
+ }
230
+ };
231
+ }
232
+ //#endregion
233
+ export { createInfiniteCanvasCamera, infiniteCanvasGridStyle, infiniteCanvasTransformCss };
@@ -0,0 +1,33 @@
1
+ //#region src/lib/infinite-canvas-camera.d.ts
2
+ export type InfiniteCanvasTransform = {
3
+ x: number;
4
+ y: number;
5
+ scale: number;
6
+ };
7
+ export type InfiniteCanvasPoint = {
8
+ x: number;
9
+ y: number;
10
+ };
11
+ export type InfiniteCanvasBounds = {
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ };
17
+ export type CanvasWheelInput = Pick<WheelEvent, "deltaMode" | "deltaX" | "deltaY" | "shiftKey">;
18
+ export declare function clampScale(scale: number, minScale: number, maxScale: number): number;
19
+ export declare function sameTransform(left: InfiniteCanvasTransform, right: InfiniteCanvasTransform): boolean;
20
+ export declare function boundedTransform(next: InfiniteCanvasTransform, minScale: number, maxScale: number): InfiniteCanvasTransform | null;
21
+ export declare function toWorldPoint(transform: InfiniteCanvasTransform, point: InfiniteCanvasPoint): InfiniteCanvasPoint;
22
+ export declare function zoomAroundPoint(transform: InfiniteCanvasTransform, nextScale: number, point: InfiniteCanvasPoint): InfiniteCanvasTransform;
23
+ export declare function pinchTransform(start: InfiniteCanvasTransform, startMidpoint: InfiniteCanvasPoint, midpoint: InfiniteCanvasPoint, nextScale: number): InfiniteCanvasTransform;
24
+ export declare function wheelDeltaMultiplier(deltaMode: number, pageHeight: number): number;
25
+ export declare function wheelDeltaPixels(event: CanvasWheelInput, pageHeight: number): InfiniteCanvasPoint;
26
+ export declare function unionBounds(bounds: readonly InfiniteCanvasBounds[]): InfiniteCanvasBounds | null;
27
+ export declare function fitTransform(bounds: InfiniteCanvasBounds, area: InfiniteCanvasBounds, limits: {
28
+ padding: number;
29
+ minScale: number;
30
+ maxScale: number;
31
+ }): InfiniteCanvasTransform;
32
+ export declare function revealOffset(screenBounds: InfiniteCanvasBounds, area: InfiniteCanvasBounds, padding: number): InfiniteCanvasPoint;
33
+ //#endregion