@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.
Files changed (165) hide show
  1. package/README.md +38 -2
  2. package/dist/action-bar.js +16 -8
  3. package/dist/activity.d.ts +4 -2
  4. package/dist/activity.js +5 -2
  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 +101 -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 +82 -0
  13. package/dist/blocks/agent-run.js +262 -0
  14. package/dist/blocks/coding-agent.d.ts +1 -1
  15. package/dist/blocks/coding-agent.js +50 -69
  16. package/dist/blocks/design-canvas-data.d.ts +1 -1
  17. package/dist/blocks/design-canvas-inspector.js +1 -1
  18. package/dist/blocks/design-canvas.d.ts +2 -1
  19. package/dist/blocks/design-canvas.js +56 -42
  20. package/dist/blocks/discussion.js +1 -1
  21. package/dist/blocks/file-explorer.js +66 -47
  22. package/dist/blocks/settings.js +37 -41
  23. package/dist/blocks/team-chat.js +2 -1
  24. package/dist/blocks/theme-toggle.d.ts +2 -1
  25. package/dist/blocks/theme-toggle.js +13 -4
  26. package/dist/chat-message.js +33 -39
  27. package/dist/hooks/use-mobile.d.ts +1 -1
  28. package/dist/hooks/use-mobile.js +1 -1
  29. package/dist/knob-contracts/badge-knobs.d.ts +1 -1
  30. package/dist/knob-contracts/badge-knobs.js +3 -1
  31. package/dist/knob-contracts/button-knobs.d.ts +1 -1
  32. package/dist/knob-contracts/button-knobs.js +1 -0
  33. package/dist/knob-contracts/calendar-knobs.d.ts +1 -1
  34. package/dist/knob-contracts/calendar-knobs.js +9 -1
  35. package/dist/knob-contracts/card-knobs.d.ts +1 -1
  36. package/dist/knob-contracts/card-knobs.js +5 -1
  37. package/dist/knob-contracts/choice-knobs.d.ts +1 -1
  38. package/dist/knob-contracts/choice-knobs.js +1 -0
  39. package/dist/knob-contracts/collapsible-knobs.d.ts +1 -1
  40. package/dist/knob-contracts/collapsible-knobs.js +5 -1
  41. package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
  42. package/dist/knob-contracts/emoji-picker-knobs.js +3 -1
  43. package/dist/knob-contracts/field-knobs.d.ts +1 -1
  44. package/dist/knob-contracts/field-knobs.js +17 -1
  45. package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
  46. package/dist/knob-contracts/infinite-canvas-knobs.js +1 -7
  47. package/dist/knob-contracts/item-knobs.d.ts +1 -1
  48. package/dist/knob-contracts/item-knobs.js +5 -1
  49. package/dist/knob-contracts/kbd-knobs.d.ts +1 -1
  50. package/dist/knob-contracts/kbd-knobs.js +4 -1
  51. package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
  52. package/dist/knob-contracts/page-layout-knobs.js +1 -0
  53. package/dist/knob-contracts/pagination-knobs.d.ts +1 -1
  54. package/dist/knob-contracts/pagination-knobs.js +2 -1
  55. package/dist/knob-contracts/popup-knobs.d.ts +1 -1
  56. package/dist/knob-contracts/popup-knobs.js +9 -0
  57. package/dist/knob-contracts/range-knobs.d.ts +1 -1
  58. package/dist/knob-contracts/range-knobs.js +2 -0
  59. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  60. package/dist/knob-contracts/sidebar-knobs.js +2 -0
  61. package/dist/knob-contracts/swap-knobs.d.ts +4 -0
  62. package/dist/knob-contracts/swap-knobs.js +9 -0
  63. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  64. package/dist/knob-contracts/tabs-knobs.js +3 -0
  65. package/dist/knob-contracts/timeline-knobs.d.ts +1 -1
  66. package/dist/knob-contracts/timeline-knobs.js +5 -1
  67. package/dist/knob-contracts/tool-approval-knobs.d.ts +4 -0
  68. package/dist/knob-contracts/tool-approval-knobs.js +32 -0
  69. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  70. package/dist/knob-contracts/tree-knobs.js +3 -1
  71. package/dist/lib/infinite-canvas-camera-store.d.ts +65 -0
  72. package/dist/lib/infinite-canvas-camera-store.js +233 -0
  73. package/dist/lib/infinite-canvas-camera.d.ts +33 -0
  74. package/dist/lib/infinite-canvas-camera.js +98 -0
  75. package/dist/lib/infinite-canvas-navigation.d.ts +19 -0
  76. package/dist/lib/infinite-canvas-navigation.js +395 -0
  77. package/dist/markdown-editor/bubble-menu.d.ts +6 -0
  78. package/dist/markdown-editor/bubble-menu.js +45 -0
  79. package/dist/markdown-editor/toolbar.d.ts +17 -1
  80. package/dist/markdown-editor/toolbar.js +103 -81
  81. package/dist/skin.d.ts +2 -0
  82. package/dist/styles/index.css +11 -10
  83. package/dist/styles/recipes/activity.css +12 -0
  84. package/dist/styles/recipes/avatar.css +1 -1
  85. package/dist/styles/recipes/badge.css +17 -0
  86. package/dist/styles/recipes/button-group.css +12 -6
  87. package/dist/styles/recipes/button.css +20 -3
  88. package/dist/styles/recipes/calendar.css +82 -6
  89. package/dist/styles/recipes/card.css +40 -5
  90. package/dist/styles/recipes/chat-message-edit.css +6 -21
  91. package/dist/styles/recipes/choice.css +9 -2
  92. package/dist/styles/recipes/collapsible.css +8 -1
  93. package/dist/styles/recipes/color-picker-values.css +4 -0
  94. package/dist/styles/recipes/emoji-picker.css +29 -8
  95. package/dist/styles/recipes/field-parts.css +80 -5
  96. package/dist/styles/recipes/field.css +113 -40
  97. package/dist/styles/recipes/infinite-canvas.css +17 -57
  98. package/dist/styles/recipes/item.css +33 -5
  99. package/dist/styles/recipes/kbd.css +26 -6
  100. package/dist/styles/recipes/markdown-editor.css +12 -0
  101. package/dist/styles/recipes/morphing-panel.css +1 -1
  102. package/dist/styles/recipes/page-layout.css +18 -2
  103. package/dist/styles/recipes/pagination.css +8 -0
  104. package/dist/styles/recipes/popup-controls.css +9 -0
  105. package/dist/styles/recipes/popup-drawer.css +1 -0
  106. package/dist/styles/recipes/popup-lists.css +55 -6
  107. package/dist/styles/recipes/popup-motion.css +7 -1
  108. package/dist/styles/recipes/popup.css +10 -0
  109. package/dist/styles/recipes/range.css +16 -2
  110. package/dist/styles/recipes/sidebar-mobile.css +1 -1
  111. package/dist/styles/recipes/sidebar-parts.css +17 -1
  112. package/dist/styles/recipes/sidebar.css +11 -0
  113. package/dist/styles/recipes/swap.css +143 -0
  114. package/dist/styles/recipes/tabs.css +41 -8
  115. package/dist/styles/recipes/timeline.css +36 -2
  116. package/dist/styles/recipes/tool-approval.css +267 -0
  117. package/dist/styles/recipes/track-highlight.css +19 -6
  118. package/dist/styles/recipes/tree.css +33 -0
  119. package/dist/styles/theme.css +17 -6
  120. package/dist/tool-approval.d.ts +49 -0
  121. package/dist/tool-approval.js +260 -0
  122. package/dist/ui/alert-dialog.js +1 -1
  123. package/dist/ui/badge.js +1 -1
  124. package/dist/ui/calendar.js +9 -4
  125. package/dist/ui/card.d.ts +3 -1
  126. package/dist/ui/card.js +2 -1
  127. package/dist/ui/code.js +13 -3
  128. package/dist/ui/command.js +1 -0
  129. package/dist/ui/dialog.js +1 -1
  130. package/dist/ui/dropdown-menu.d.ts +1 -1
  131. package/dist/ui/dropdown-menu.js +23 -6
  132. package/dist/ui/dropzone.js +10 -11
  133. package/dist/ui/field.d.ts +6 -2
  134. package/dist/ui/field.js +4 -2
  135. package/dist/ui/infinite-canvas.d.ts +19 -22
  136. package/dist/ui/infinite-canvas.js +264 -325
  137. package/dist/ui/morphing-panel.d.ts +5 -1
  138. package/dist/ui/morphing-panel.js +4 -2
  139. package/dist/ui/rich-tooltip.d.ts +2 -1
  140. package/dist/ui/rich-tooltip.js +32 -29
  141. package/dist/ui/select.js +2 -2
  142. package/dist/ui/sidebar-mobile.js +1 -1
  143. package/dist/ui/sidebar-provider.d.ts +3 -1
  144. package/dist/ui/sidebar-provider.js +3 -2
  145. package/dist/ui/sidebar-resize-rail.js +1 -1
  146. package/dist/ui/sidebar.d.ts +6 -5
  147. package/dist/ui/sidebar.js +10 -9
  148. package/dist/ui/swap.d.ts +14 -0
  149. package/dist/ui/swap.js +69 -0
  150. package/dist/ui/tabs.d.ts +1 -1
  151. package/dist/ui/tabs.js +2 -13
  152. package/dist/ui/timeline.d.ts +1 -1
  153. package/dist/ui/timeline.js +2 -2
  154. package/dist/ui/typography.d.ts +11 -5
  155. package/dist/ui/typography.js +18 -6
  156. package/dist/user-ask.js +1 -1
  157. package/lint/biome-preset.json +3 -0
  158. package/lint/class-policy.mjs +17 -0
  159. package/lint/eslint-plugin.mjs +87 -0
  160. package/lint/grit-rules.mjs +40 -0
  161. package/lint/no-component-style-classes.grit +16 -0
  162. package/lint/no-raw-type-classes.grit +3 -4
  163. package/lint/write-grit-rules.mjs +19 -0
  164. package/package.json +12 -3
  165. 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