@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.
- package/README.md +2 -2
- package/dist/action-bar.js +16 -8
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +3 -1
- package/dist/audio-visualizer.js +1 -1
- package/dist/blocks/agent-run-clock.d.ts +5 -0
- package/dist/blocks/agent-run-clock.js +52 -0
- package/dist/blocks/agent-run-sidebar.d.ts +13 -0
- package/dist/blocks/agent-run-sidebar.js +100 -0
- package/dist/blocks/agent-run-status.d.ts +32 -0
- package/dist/blocks/agent-run-status.js +27 -0
- package/dist/blocks/agent-run.d.ts +80 -0
- package/dist/blocks/agent-run.js +254 -0
- package/dist/blocks/coding-agent.d.ts +1 -1
- package/dist/blocks/design-canvas-data.d.ts +1 -1
- package/dist/blocks/design-canvas.d.ts +2 -1
- package/dist/blocks/design-canvas.js +15 -8
- package/dist/chat-message.js +33 -39
- package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
- package/dist/knob-contracts/infinite-canvas-knobs.js +1 -7
- package/dist/knob-contracts/swap-knobs.d.ts +4 -0
- package/dist/knob-contracts/swap-knobs.js +9 -0
- package/dist/knob-contracts/timeline-knobs.d.ts +1 -1
- package/dist/knob-contracts/timeline-knobs.js +1 -0
- package/dist/knob-contracts/tool-approval-knobs.d.ts +4 -0
- package/dist/knob-contracts/tool-approval-knobs.js +32 -0
- package/dist/lib/infinite-canvas-camera-store.d.ts +65 -0
- package/dist/lib/infinite-canvas-camera-store.js +233 -0
- package/dist/lib/infinite-canvas-camera.d.ts +33 -0
- package/dist/lib/infinite-canvas-camera.js +98 -0
- package/dist/lib/infinite-canvas-navigation.d.ts +19 -0
- package/dist/lib/infinite-canvas-navigation.js +395 -0
- package/dist/markdown-editor/bubble-menu.d.ts +6 -0
- package/dist/markdown-editor/bubble-menu.js +45 -0
- package/dist/markdown-editor/toolbar.d.ts +17 -1
- package/dist/markdown-editor/toolbar.js +103 -81
- package/dist/styles/index.css +10 -8
- package/dist/styles/recipes/activity.css +8 -0
- package/dist/styles/recipes/button-group.css +12 -6
- package/dist/styles/recipes/button.css +12 -2
- package/dist/styles/recipes/chat-message-edit.css +6 -21
- package/dist/styles/recipes/infinite-canvas.css +17 -57
- package/dist/styles/recipes/markdown-editor.css +12 -0
- package/dist/styles/recipes/swap.css +143 -0
- package/dist/styles/recipes/timeline.css +11 -0
- package/dist/styles/recipes/tool-approval.css +267 -0
- package/dist/styles/recipes/track-highlight.css +19 -6
- package/dist/styles/theme.css +6 -5
- package/dist/styles/type-scale.css +1 -1
- package/dist/tool-approval.d.ts +49 -0
- package/dist/tool-approval.js +260 -0
- package/dist/ui/code.js +13 -3
- package/dist/ui/command.js +1 -0
- package/dist/ui/infinite-canvas.d.ts +19 -22
- package/dist/ui/infinite-canvas.js +264 -325
- package/dist/ui/rich-tooltip.d.ts +2 -1
- package/dist/ui/rich-tooltip.js +32 -29
- package/dist/ui/swap.d.ts +14 -0
- package/dist/ui/swap.js +69 -0
- package/dist/ui/timeline.d.ts +1 -1
- package/dist/ui/timeline.js +1 -1
- package/dist/user-ask.js +1 -1
- package/lint/no-raw-type-classes.grit +2 -2
- 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 "../
|
|
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 (
|
|
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:
|
|
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" &&
|
|
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
|
-
|
|
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 })] });
|
package/dist/chat-message.js
CHANGED
|
@@ -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__ */
|
|
272
|
+
/* @__PURE__ */ jsx("div", {
|
|
272
273
|
"data-control-ui": "chat-message",
|
|
273
274
|
"data-control-family": "chat-message",
|
|
274
|
-
"data-slot": "
|
|
275
|
-
children:
|
|
276
|
-
"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
"
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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"
|
|
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
|
|
@@ -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
|