@ctrl-ui/react 0.9.5 → 0.9.8
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 +38 -2
- package/dist/action-bar.js +16 -8
- package/dist/activity.d.ts +4 -2
- package/dist/activity.js +5 -2
- 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 +101 -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 +82 -0
- package/dist/blocks/agent-run.js +262 -0
- package/dist/blocks/coding-agent.d.ts +1 -1
- package/dist/blocks/coding-agent.js +50 -69
- package/dist/blocks/design-canvas-data.d.ts +1 -1
- package/dist/blocks/design-canvas-inspector.js +1 -1
- package/dist/blocks/design-canvas.d.ts +2 -1
- package/dist/blocks/design-canvas.js +56 -42
- package/dist/blocks/discussion.js +1 -1
- package/dist/blocks/file-explorer.js +66 -47
- package/dist/blocks/settings.js +37 -41
- package/dist/blocks/team-chat.js +2 -1
- package/dist/blocks/theme-toggle.d.ts +2 -1
- package/dist/blocks/theme-toggle.js +13 -4
- package/dist/chat-message.js +33 -39
- package/dist/hooks/use-mobile.d.ts +1 -1
- package/dist/hooks/use-mobile.js +1 -1
- package/dist/knob-contracts/badge-knobs.d.ts +1 -1
- package/dist/knob-contracts/badge-knobs.js +3 -1
- package/dist/knob-contracts/button-knobs.d.ts +1 -1
- package/dist/knob-contracts/button-knobs.js +1 -0
- package/dist/knob-contracts/calendar-knobs.d.ts +1 -1
- package/dist/knob-contracts/calendar-knobs.js +9 -1
- package/dist/knob-contracts/card-knobs.d.ts +1 -1
- package/dist/knob-contracts/card-knobs.js +5 -1
- package/dist/knob-contracts/choice-knobs.d.ts +1 -1
- package/dist/knob-contracts/choice-knobs.js +1 -0
- package/dist/knob-contracts/collapsible-knobs.d.ts +1 -1
- package/dist/knob-contracts/collapsible-knobs.js +5 -1
- package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
- package/dist/knob-contracts/emoji-picker-knobs.js +3 -1
- package/dist/knob-contracts/field-knobs.d.ts +1 -1
- package/dist/knob-contracts/field-knobs.js +17 -1
- 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/item-knobs.d.ts +1 -1
- package/dist/knob-contracts/item-knobs.js +5 -1
- package/dist/knob-contracts/kbd-knobs.d.ts +1 -1
- package/dist/knob-contracts/kbd-knobs.js +4 -1
- package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
- package/dist/knob-contracts/page-layout-knobs.js +1 -0
- package/dist/knob-contracts/pagination-knobs.d.ts +1 -1
- package/dist/knob-contracts/pagination-knobs.js +2 -1
- package/dist/knob-contracts/popup-knobs.d.ts +1 -1
- package/dist/knob-contracts/popup-knobs.js +9 -0
- package/dist/knob-contracts/range-knobs.d.ts +1 -1
- package/dist/knob-contracts/range-knobs.js +2 -0
- package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
- package/dist/knob-contracts/sidebar-knobs.js +2 -0
- package/dist/knob-contracts/swap-knobs.d.ts +4 -0
- package/dist/knob-contracts/swap-knobs.js +9 -0
- package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
- package/dist/knob-contracts/tabs-knobs.js +3 -0
- package/dist/knob-contracts/timeline-knobs.d.ts +1 -1
- package/dist/knob-contracts/timeline-knobs.js +5 -1
- package/dist/knob-contracts/tool-approval-knobs.d.ts +4 -0
- package/dist/knob-contracts/tool-approval-knobs.js +32 -0
- package/dist/knob-contracts/tree-knobs.d.ts +1 -1
- package/dist/knob-contracts/tree-knobs.js +3 -1
- 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/skin.d.ts +2 -0
- package/dist/styles/index.css +11 -10
- package/dist/styles/recipes/activity.css +12 -0
- package/dist/styles/recipes/avatar.css +1 -1
- package/dist/styles/recipes/badge.css +17 -0
- package/dist/styles/recipes/button-group.css +12 -6
- package/dist/styles/recipes/button.css +20 -3
- package/dist/styles/recipes/calendar.css +82 -6
- package/dist/styles/recipes/card.css +40 -5
- package/dist/styles/recipes/chat-message-edit.css +6 -21
- package/dist/styles/recipes/choice.css +9 -2
- package/dist/styles/recipes/collapsible.css +8 -1
- package/dist/styles/recipes/color-picker-values.css +4 -0
- package/dist/styles/recipes/emoji-picker.css +29 -8
- package/dist/styles/recipes/field-parts.css +80 -5
- package/dist/styles/recipes/field.css +113 -40
- package/dist/styles/recipes/infinite-canvas.css +17 -57
- package/dist/styles/recipes/item.css +33 -5
- package/dist/styles/recipes/kbd.css +26 -6
- package/dist/styles/recipes/markdown-editor.css +12 -0
- package/dist/styles/recipes/morphing-panel.css +1 -1
- package/dist/styles/recipes/page-layout.css +18 -2
- package/dist/styles/recipes/pagination.css +8 -0
- package/dist/styles/recipes/popup-controls.css +9 -0
- package/dist/styles/recipes/popup-drawer.css +1 -0
- package/dist/styles/recipes/popup-lists.css +55 -6
- package/dist/styles/recipes/popup-motion.css +7 -1
- package/dist/styles/recipes/popup.css +10 -0
- package/dist/styles/recipes/range.css +16 -2
- package/dist/styles/recipes/sidebar-mobile.css +1 -1
- package/dist/styles/recipes/sidebar-parts.css +17 -1
- package/dist/styles/recipes/sidebar.css +11 -0
- package/dist/styles/recipes/swap.css +143 -0
- package/dist/styles/recipes/tabs.css +41 -8
- package/dist/styles/recipes/timeline.css +36 -2
- package/dist/styles/recipes/tool-approval.css +267 -0
- package/dist/styles/recipes/track-highlight.css +19 -6
- package/dist/styles/recipes/tree.css +33 -0
- package/dist/styles/theme.css +17 -6
- package/dist/tool-approval.d.ts +49 -0
- package/dist/tool-approval.js +260 -0
- package/dist/ui/alert-dialog.js +1 -1
- package/dist/ui/badge.js +1 -1
- package/dist/ui/calendar.js +9 -4
- package/dist/ui/card.d.ts +3 -1
- package/dist/ui/card.js +2 -1
- package/dist/ui/code.js +13 -3
- package/dist/ui/command.js +1 -0
- package/dist/ui/dialog.js +1 -1
- package/dist/ui/dropdown-menu.d.ts +1 -1
- package/dist/ui/dropdown-menu.js +23 -6
- package/dist/ui/dropzone.js +10 -11
- package/dist/ui/field.d.ts +6 -2
- package/dist/ui/field.js +4 -2
- package/dist/ui/infinite-canvas.d.ts +19 -22
- package/dist/ui/infinite-canvas.js +264 -325
- package/dist/ui/morphing-panel.d.ts +5 -1
- package/dist/ui/morphing-panel.js +4 -2
- package/dist/ui/rich-tooltip.d.ts +2 -1
- package/dist/ui/rich-tooltip.js +32 -29
- package/dist/ui/select.js +2 -2
- package/dist/ui/sidebar-mobile.js +1 -1
- package/dist/ui/sidebar-provider.d.ts +3 -1
- package/dist/ui/sidebar-provider.js +3 -2
- package/dist/ui/sidebar-resize-rail.js +1 -1
- package/dist/ui/sidebar.d.ts +6 -5
- package/dist/ui/sidebar.js +10 -9
- package/dist/ui/swap.d.ts +14 -0
- package/dist/ui/swap.js +69 -0
- package/dist/ui/tabs.d.ts +1 -1
- package/dist/ui/tabs.js +2 -13
- package/dist/ui/timeline.d.ts +1 -1
- package/dist/ui/timeline.js +2 -2
- package/dist/ui/typography.d.ts +11 -5
- package/dist/ui/typography.js +18 -6
- package/dist/user-ask.js +1 -1
- package/lint/biome-preset.json +3 -0
- package/lint/class-policy.mjs +17 -0
- package/lint/eslint-plugin.mjs +87 -0
- package/lint/grit-rules.mjs +40 -0
- package/lint/no-component-style-classes.grit +16 -0
- package/lint/no-raw-type-classes.grit +3 -4
- package/lint/write-grit-rules.mjs +19 -0
- package/package.json +12 -3
- package/dist/styles/type-scale.css +0 -72
|
@@ -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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
//#region src/knob-contracts/tree-knobs.d.ts
|
|
2
|
-
export declare const treeKnobs: readonly ["--cui-tree-item-trigger-radius", "--cui-tree-item-trigger-foreground", "--cui-tree-item-trigger-hover-background", "--cui-tree-item-trigger-hover-foreground", "--cui-tree-item-trigger-selected-background", "--cui-tree-item-trigger-selected-foreground", "--cui-tree-item-trigger-font-size", "--cui-tree-item-trigger-selected-font-weight", "--cui-tree-indent-inset", "--cui-tree-item-trigger-background"];
|
|
2
|
+
export declare const treeKnobs: readonly ["--cui-tree-item-trigger-radius", "--cui-tree-item-trigger-foreground", "--cui-tree-item-trigger-hover-background", "--cui-tree-item-trigger-hover-foreground", "--cui-tree-item-trigger-selected-background", "--cui-tree-item-trigger-selected-foreground", "--cui-tree-item-trigger-font-size", "--cui-tree-item-trigger-selected-font-weight", "--cui-tree-indent-inset", "--cui-tree-item-trigger-background", "--cui-tree-guide-background", "--cui-tree-guide-inline-size"];
|
|
3
3
|
export type TreeKnobStyle = Partial<Record<(typeof treeKnobs)[number], string>>;
|
|
4
4
|
//#endregion
|
|
@@ -9,7 +9,9 @@ const treeKnobs = [
|
|
|
9
9
|
"--cui-tree-item-trigger-font-size",
|
|
10
10
|
"--cui-tree-item-trigger-selected-font-weight",
|
|
11
11
|
"--cui-tree-indent-inset",
|
|
12
|
-
"--cui-tree-item-trigger-background"
|
|
12
|
+
"--cui-tree-item-trigger-background",
|
|
13
|
+
"--cui-tree-guide-background",
|
|
14
|
+
"--cui-tree-guide-inline-size"
|
|
13
15
|
];
|
|
14
16
|
//#endregion
|
|
15
17
|
export { treeKnobs };
|
|
@@ -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
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/lib/infinite-canvas-camera.ts
|
|
2
|
+
const MAX_CAMERA_OFFSET_PX = 1e7;
|
|
3
|
+
const WHEEL_LINE_HEIGHT_PX = 16;
|
|
4
|
+
const DOM_DELTA_LINE = 1;
|
|
5
|
+
const DOM_DELTA_PAGE = 2;
|
|
6
|
+
function clampScale(scale, minScale, maxScale) {
|
|
7
|
+
return Math.min(Math.max(scale, minScale), maxScale);
|
|
8
|
+
}
|
|
9
|
+
function sameTransform(left, right) {
|
|
10
|
+
return left.x === right.x && left.y === right.y && left.scale === right.scale;
|
|
11
|
+
}
|
|
12
|
+
function boundedTransform(next, minScale, maxScale) {
|
|
13
|
+
if (!(Number.isFinite(next.x) && Number.isFinite(next.y) && Number.isFinite(next.scale)) || next.scale <= 0) return null;
|
|
14
|
+
return {
|
|
15
|
+
x: Math.min(Math.max(next.x, -1e7), MAX_CAMERA_OFFSET_PX),
|
|
16
|
+
y: Math.min(Math.max(next.y, -1e7), MAX_CAMERA_OFFSET_PX),
|
|
17
|
+
scale: clampScale(next.scale, minScale, maxScale)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function toWorldPoint(transform, point) {
|
|
21
|
+
return {
|
|
22
|
+
x: (point.x - transform.x) / transform.scale,
|
|
23
|
+
y: (point.y - transform.y) / transform.scale
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function zoomAroundPoint(transform, nextScale, point) {
|
|
27
|
+
const world = toWorldPoint(transform, point);
|
|
28
|
+
return {
|
|
29
|
+
x: point.x - world.x * nextScale,
|
|
30
|
+
y: point.y - world.y * nextScale,
|
|
31
|
+
scale: nextScale
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function pinchTransform(start, startMidpoint, midpoint, nextScale) {
|
|
35
|
+
const zoomed = zoomAroundPoint(start, nextScale, startMidpoint);
|
|
36
|
+
return {
|
|
37
|
+
x: zoomed.x + midpoint.x - startMidpoint.x,
|
|
38
|
+
y: zoomed.y + midpoint.y - startMidpoint.y,
|
|
39
|
+
scale: nextScale
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function wheelDeltaMultiplier(deltaMode, pageHeight) {
|
|
43
|
+
if (deltaMode === DOM_DELTA_LINE) return WHEEL_LINE_HEIGHT_PX;
|
|
44
|
+
if (deltaMode === DOM_DELTA_PAGE) return pageHeight;
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
function wheelDeltaPixels(event, pageHeight) {
|
|
48
|
+
const multiplier = wheelDeltaMultiplier(event.deltaMode, pageHeight);
|
|
49
|
+
const verticalAsHorizontal = event.shiftKey && event.deltaX === 0;
|
|
50
|
+
return {
|
|
51
|
+
x: (verticalAsHorizontal ? event.deltaY : event.deltaX) * multiplier,
|
|
52
|
+
y: (verticalAsHorizontal ? 0 : event.deltaY) * multiplier
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function unionBounds(bounds) {
|
|
56
|
+
if (bounds.length === 0) return null;
|
|
57
|
+
let left = Number.POSITIVE_INFINITY;
|
|
58
|
+
let top = Number.POSITIVE_INFINITY;
|
|
59
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
60
|
+
let bottom = Number.NEGATIVE_INFINITY;
|
|
61
|
+
for (const entry of bounds) {
|
|
62
|
+
left = Math.min(left, entry.x);
|
|
63
|
+
top = Math.min(top, entry.y);
|
|
64
|
+
right = Math.max(right, entry.x + entry.width);
|
|
65
|
+
bottom = Math.max(bottom, entry.y + entry.height);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
x: left,
|
|
69
|
+
y: top,
|
|
70
|
+
width: right - left,
|
|
71
|
+
height: bottom - top
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function fitTransform(bounds, area, limits) {
|
|
75
|
+
const availableWidth = Math.max(1, area.width - limits.padding * 2);
|
|
76
|
+
const availableHeight = Math.max(1, area.height - limits.padding * 2);
|
|
77
|
+
const scale = clampScale(Math.min(availableWidth / Math.max(bounds.width, 1), availableHeight / Math.max(bounds.height, 1)), limits.minScale, limits.maxScale);
|
|
78
|
+
return {
|
|
79
|
+
x: area.x + area.width / 2 - (bounds.x + bounds.width / 2) * scale,
|
|
80
|
+
y: area.y + area.height / 2 - (bounds.y + bounds.height / 2) * scale,
|
|
81
|
+
scale
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function revealAxisOffset(start, size, areaStart, areaSize, padding) {
|
|
85
|
+
const visibleStart = areaStart + padding;
|
|
86
|
+
const visibleEnd = areaStart + areaSize - padding;
|
|
87
|
+
if (size > visibleEnd - visibleStart || start < visibleStart) return visibleStart - start;
|
|
88
|
+
if (start + size > visibleEnd) return visibleEnd - (start + size);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
function revealOffset(screenBounds, area, padding) {
|
|
92
|
+
return {
|
|
93
|
+
x: revealAxisOffset(screenBounds.x, screenBounds.width, area.x, area.width, padding),
|
|
94
|
+
y: revealAxisOffset(screenBounds.y, screenBounds.height, area.y, area.height, padding)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { boundedTransform, clampScale, fitTransform, pinchTransform, revealOffset, sameTransform, toWorldPoint, unionBounds, wheelDeltaMultiplier, wheelDeltaPixels, zoomAroundPoint };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { InfiniteCanvasPoint } from "./infinite-canvas-camera.js";
|
|
2
|
+
import { InfiniteCanvasCameraStore } from "./infinite-canvas-camera-store.js";
|
|
3
|
+
//#region src/lib/infinite-canvas-navigation.d.ts
|
|
4
|
+
export type InfiniteCanvasPanOnDrag = "background" | "anywhere" | "never";
|
|
5
|
+
export type InfiniteCanvasNavigationOptions = {
|
|
6
|
+
panOnDrag: InfiniteCanvasPanOnDrag;
|
|
7
|
+
onWheel?: (event: WheelEvent) => void;
|
|
8
|
+
};
|
|
9
|
+
export type InfiniteCanvasNavigation = {
|
|
10
|
+
beginBackgroundPan: (event: PointerEvent) => void;
|
|
11
|
+
syncPanReady: () => void;
|
|
12
|
+
detach: () => void;
|
|
13
|
+
};
|
|
14
|
+
export declare const INFINITE_CANVAS_KEY_SHORTCUTS = "ArrowLeft ArrowRight ArrowUp ArrowDown + - 0 1";
|
|
15
|
+
export declare const ARROW_KEY_DIRECTIONS: Partial<Record<string, InfiniteCanvasPoint>>;
|
|
16
|
+
export declare function keyboardStep(shiftKey: boolean): number;
|
|
17
|
+
export declare function runInfiniteCanvasKeyCommand(camera: InfiniteCanvasCameraStore, key: string, shiftKey: boolean): boolean;
|
|
18
|
+
export declare function attachInfiniteCanvasNavigation(root: HTMLElement, camera: InfiniteCanvasCameraStore, readOptions: () => InfiniteCanvasNavigationOptions): InfiniteCanvasNavigation;
|
|
19
|
+
//#endregion
|