@pixel-point/toolcraft 0.0.2

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 (257) hide show
  1. package/LICENSE.md +98 -0
  2. package/README.md +41 -0
  3. package/bin/create-toolcraft-app.mjs +8 -0
  4. package/bin/toolcraft.mjs +8 -0
  5. package/package.json +24 -0
  6. package/scripts/prepare-pack.mjs +29 -0
  7. package/src/cli.mjs +392 -0
  8. package/src/cli.test.mjs +284 -0
  9. package/src/copy-recursive.mjs +86 -0
  10. package/src/generate.mjs +212 -0
  11. package/src/generate.test.mjs +322 -0
  12. package/src/import-map.mjs +14 -0
  13. package/src/package-json.mjs +80 -0
  14. package/src/package-json.test.mjs +67 -0
  15. package/src/rewrite-imports.mjs +85 -0
  16. package/src/rewrite-imports.test.mjs +58 -0
  17. package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
  18. package/templates/runtime/contracts/component-contracts.ts +1340 -0
  19. package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
  20. package/templates/runtime/contracts/decision-contracts.ts +283 -0
  21. package/templates/runtime/contracts/index.test.ts +14 -0
  22. package/templates/runtime/contracts/index.ts +3 -0
  23. package/templates/runtime/contracts/types.ts +56 -0
  24. package/templates/runtime/export/export.test.ts +203 -0
  25. package/templates/runtime/export/export.ts +132 -0
  26. package/templates/runtime/export/index.ts +1 -0
  27. package/templates/runtime/index.ts +14 -0
  28. package/templates/runtime/react/canvas-shell.test.tsx +424 -0
  29. package/templates/runtime/react/canvas-shell.tsx +408 -0
  30. package/templates/runtime/react/control-renderers.ts +31 -0
  31. package/templates/runtime/react/controls-panel.test.tsx +3736 -0
  32. package/templates/runtime/react/controls-panel.tsx +2327 -0
  33. package/templates/runtime/react/curve-geometry.test.ts +70 -0
  34. package/templates/runtime/react/index.ts +15 -0
  35. package/templates/runtime/react/layer-tree.ts +96 -0
  36. package/templates/runtime/react/layers-panel.test.tsx +487 -0
  37. package/templates/runtime/react/layers-panel.tsx +1348 -0
  38. package/templates/runtime/react/media-file.ts +82 -0
  39. package/templates/runtime/react/panel-host-config.ts +80 -0
  40. package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
  41. package/templates/runtime/react/panel-host-geometry.ts +109 -0
  42. package/templates/runtime/react/panel-host-types.ts +74 -0
  43. package/templates/runtime/react/panel-host.test.tsx +102 -0
  44. package/templates/runtime/react/panel-host.tsx +353 -0
  45. package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
  46. package/templates/runtime/react/settings-transfer.test.ts +150 -0
  47. package/templates/runtime/react/settings-transfer.ts +279 -0
  48. package/templates/runtime/react/storage-key-migration.ts +48 -0
  49. package/templates/runtime/react/theme-runtime.tsx +177 -0
  50. package/templates/runtime/react/timeline-panel.test.tsx +668 -0
  51. package/templates/runtime/react/timeline-panel.tsx +2953 -0
  52. package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
  53. package/templates/runtime/react/toolbar-panel.tsx +205 -0
  54. package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
  55. package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
  56. package/templates/runtime/react/toolcraft-app.tsx +81 -0
  57. package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
  58. package/templates/runtime/react/toolcraft-root.tsx +203 -0
  59. package/templates/runtime/react/use-toolcraft.ts +41 -0
  60. package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
  61. package/templates/runtime/schema/define-toolcraft.ts +1442 -0
  62. package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
  63. package/templates/runtime/schema/keyframe-capability.ts +51 -0
  64. package/templates/runtime/schema/runtime-targets.ts +40 -0
  65. package/templates/runtime/schema/types.ts +370 -0
  66. package/templates/runtime/state/canvas-zoom.ts +8 -0
  67. package/templates/runtime/state/create-template-state.test.ts +242 -0
  68. package/templates/runtime/state/create-template-state.ts +95 -0
  69. package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
  70. package/templates/runtime/state/keyframe-evaluation.ts +203 -0
  71. package/templates/runtime/state/persistence.test.ts +217 -0
  72. package/templates/runtime/state/persistence.ts +511 -0
  73. package/templates/runtime/state/reducer.test.ts +937 -0
  74. package/templates/runtime/state/reducer.ts +1212 -0
  75. package/templates/runtime/state/timeline-readiness.ts +43 -0
  76. package/templates/runtime/state/types.ts +242 -0
  77. package/templates/runtime/styles.css +125 -0
  78. package/templates/runtime/testing/performance.test.ts +1058 -0
  79. package/templates/runtime/testing/performance.ts +1078 -0
  80. package/templates/starter/AGENTS.md +186 -0
  81. package/templates/starter/LICENSE.md +98 -0
  82. package/templates/starter/NOTICE.md +8 -0
  83. package/templates/starter/docs/toolcraft/README.md +41 -0
  84. package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
  85. package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
  86. package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
  87. package/templates/starter/docs/toolcraft/component-rules.md +299 -0
  88. package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
  89. package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
  90. package/templates/starter/docs/toolcraft/performance.md +112 -0
  91. package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
  92. package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
  93. package/templates/starter/docs/toolcraft/workflow.md +87 -0
  94. package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
  95. package/templates/starter/e2e/app-controls.spec.ts +41 -0
  96. package/templates/starter/e2e/app-performance.spec.ts +326 -0
  97. package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
  98. package/templates/starter/e2e/performance-helpers.ts +612 -0
  99. package/templates/starter/e2e/product-observable-helpers.ts +170 -0
  100. package/templates/starter/index.html +12 -0
  101. package/templates/starter/package.json +52 -0
  102. package/templates/starter/playwright.config.ts +43 -0
  103. package/templates/starter/scripts/check-ai-skills.mjs +95 -0
  104. package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
  105. package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
  106. package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
  107. package/templates/starter/scripts/toolcraft-port.mjs +54 -0
  108. package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
  109. package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
  110. package/templates/starter/src/app/starter-acceptance.ts +2646 -0
  111. package/templates/starter/src/app/starter-performance.test.ts +1390 -0
  112. package/templates/starter/src/app/starter-performance.ts +12 -0
  113. package/templates/starter/src/app/starter-schema.test.ts +70 -0
  114. package/templates/starter/src/app/starter-schema.ts +15 -0
  115. package/templates/starter/src/main.tsx +18 -0
  116. package/templates/starter/src/router.tsx +16 -0
  117. package/templates/starter/src/routes/index.tsx +7 -0
  118. package/templates/starter/src/routes/root.tsx +19 -0
  119. package/templates/starter/src/styles.css +120 -0
  120. package/templates/starter/tsconfig.json +11 -0
  121. package/templates/starter/vite.config.ts +13 -0
  122. package/templates/ui/components/composites/accordion.tsx +73 -0
  123. package/templates/ui/components/composites/alert-dialog.tsx +190 -0
  124. package/templates/ui/components/composites/alert.tsx +74 -0
  125. package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
  126. package/templates/ui/components/composites/avatar.tsx +98 -0
  127. package/templates/ui/components/composites/badge.tsx +69 -0
  128. package/templates/ui/components/composites/breadcrumb.tsx +106 -0
  129. package/templates/ui/components/composites/card.tsx +91 -0
  130. package/templates/ui/components/composites/combobox.tsx +486 -0
  131. package/templates/ui/components/composites/command.tsx +296 -0
  132. package/templates/ui/components/composites/context-menu.tsx +247 -0
  133. package/templates/ui/components/composites/dialog.tsx +282 -0
  134. package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
  135. package/templates/ui/components/composites/empty.tsx +110 -0
  136. package/templates/ui/components/composites/hover-card.tsx +44 -0
  137. package/templates/ui/components/composites/index.ts +30 -0
  138. package/templates/ui/components/composites/menubar.tsx +214 -0
  139. package/templates/ui/components/composites/navigation-menu.tsx +167 -0
  140. package/templates/ui/components/composites/pagination.tsx +131 -0
  141. package/templates/ui/components/composites/progress.tsx +72 -0
  142. package/templates/ui/components/composites/radio-group.tsx +84 -0
  143. package/templates/ui/components/composites/resizable.tsx +42 -0
  144. package/templates/ui/components/composites/sheet.tsx +153 -0
  145. package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
  146. package/templates/ui/components/composites/sidebar.tsx +431 -0
  147. package/templates/ui/components/composites/sonner.tsx +35 -0
  148. package/templates/ui/components/composites/spinner.tsx +43 -0
  149. package/templates/ui/components/composites/table.tsx +108 -0
  150. package/templates/ui/components/composites/tabs.tsx +83 -0
  151. package/templates/ui/components/control-layout/index.tsx +437 -0
  152. package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
  153. package/templates/ui/components/controls/actions/index.ts +9 -0
  154. package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
  155. package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
  156. package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
  157. package/templates/ui/components/controls/boolean/index.ts +4 -0
  158. package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
  159. package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
  160. package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
  161. package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
  162. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
  163. package/templates/ui/components/controls/code-textarea/index.ts +4 -0
  164. package/templates/ui/components/controls/color/color-control.tsx +571 -0
  165. package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
  166. package/templates/ui/components/controls/color/index.ts +41 -0
  167. package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
  168. package/templates/ui/components/controls/color/palette-control.tsx +535 -0
  169. package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
  170. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
  171. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
  172. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
  173. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
  174. package/templates/ui/components/controls/control-types.ts +43 -0
  175. package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
  176. package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
  177. package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
  178. package/templates/ui/components/controls/curves/index.ts +6 -0
  179. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
  180. package/templates/ui/components/controls/file-drop/index.ts +5 -0
  181. package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
  182. package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
  183. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
  184. package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
  185. package/templates/ui/components/controls/font-picker/index.ts +24 -0
  186. package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
  187. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
  188. package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
  189. package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
  190. package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
  191. package/templates/ui/components/controls/gradient/index.ts +4 -0
  192. package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
  193. package/templates/ui/components/controls/image-picker/index.ts +7 -0
  194. package/templates/ui/components/controls/index.ts +192 -0
  195. package/templates/ui/components/controls/range-input/index.ts +4 -0
  196. package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
  197. package/templates/ui/components/controls/range-slider/index.ts +4 -0
  198. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
  199. package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
  200. package/templates/ui/components/controls/segmented/index.ts +8 -0
  201. package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
  202. package/templates/ui/components/controls/select/index.ts +4 -0
  203. package/templates/ui/components/controls/select/select-control.tsx +223 -0
  204. package/templates/ui/components/controls/slider/index.ts +4 -0
  205. package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
  206. package/templates/ui/components/controls/slider/slider-value.ts +56 -0
  207. package/templates/ui/components/controls/text-input/index.ts +4 -0
  208. package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
  209. package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
  210. package/templates/ui/components/controls/vector/index.ts +8 -0
  211. package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
  212. package/templates/ui/components/panel/index.ts +19 -0
  213. package/templates/ui/components/panel/panel-actions.tsx +165 -0
  214. package/templates/ui/components/panel/panel-header.tsx +61 -0
  215. package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
  216. package/templates/ui/components/panel/panel-section.tsx +168 -0
  217. package/templates/ui/components/panel/panel-surface.tsx +206 -0
  218. package/templates/ui/components/panel/panel.tsx +210 -0
  219. package/templates/ui/components/primitives/animated-loader.tsx +61 -0
  220. package/templates/ui/components/primitives/button-group.tsx +134 -0
  221. package/templates/ui/components/primitives/button.tsx +429 -0
  222. package/templates/ui/components/primitives/checkbox.tsx +62 -0
  223. package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
  224. package/templates/ui/components/primitives/field.tsx +225 -0
  225. package/templates/ui/components/primitives/index.ts +82 -0
  226. package/templates/ui/components/primitives/input-group.tsx +298 -0
  227. package/templates/ui/components/primitives/input.tsx +61 -0
  228. package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
  229. package/templates/ui/components/primitives/label.tsx +16 -0
  230. package/templates/ui/components/primitives/popover.tsx +126 -0
  231. package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
  232. package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
  233. package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
  234. package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
  235. package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
  236. package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
  237. package/templates/ui/components/primitives/select.tsx +408 -0
  238. package/templates/ui/components/primitives/selection-state.ts +31 -0
  239. package/templates/ui/components/primitives/separator.tsx +21 -0
  240. package/templates/ui/components/primitives/slider/index.ts +4 -0
  241. package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
  242. package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
  243. package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
  244. package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
  245. package/templates/ui/components/primitives/slider/slider.tsx +511 -0
  246. package/templates/ui/components/primitives/switch.tsx +35 -0
  247. package/templates/ui/components/primitives/textarea.tsx +49 -0
  248. package/templates/ui/components/primitives/toggle-group.tsx +114 -0
  249. package/templates/ui/components/primitives/toggle.tsx +46 -0
  250. package/templates/ui/components/primitives/tooltip.tsx +100 -0
  251. package/templates/ui/hooks/use-mobile.ts +21 -0
  252. package/templates/ui/index.ts +31 -0
  253. package/templates/ui/lib/control-outline.ts +3 -0
  254. package/templates/ui/lib/input-control-style.ts +131 -0
  255. package/templates/ui/lib/style-guide-color-utils.ts +111 -0
  256. package/templates/ui/lib/utils.ts +6 -0
  257. package/templates/ui/styles.css +291 -0
@@ -0,0 +1,408 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { clampToolcraftCanvasZoom } from "../state/canvas-zoom";
6
+ import type { ToolcraftPoint } from "../state/types";
7
+ import { isToolcraftLayerVisibleInTree } from "./layer-tree";
8
+ import { readImportedImageFile } from "./media-file";
9
+ import { useToolcraft } from "./use-toolcraft";
10
+
11
+ export type CanvasShellProps = {
12
+ children?: React.ReactNode;
13
+ renderDefaultMedia?: boolean;
14
+ };
15
+
16
+ type CanvasDragState = {
17
+ originX: number;
18
+ originY: number;
19
+ pointerId: number;
20
+ startX: number;
21
+ startY: number;
22
+ };
23
+
24
+ const wheelZoomSensitivity = 0.12;
25
+ const wheelPinchZoomSensitivity = 0.5;
26
+
27
+ function cn(...classNames: Array<string | false | null | undefined>): string {
28
+ return classNames.filter(Boolean).join(" ");
29
+ }
30
+
31
+ function isImageFile(file: File): boolean {
32
+ return (
33
+ file.type.startsWith("image/") ||
34
+ /\.(avif|gif|jpe?g|png|svg|webp)$/i.test(file.name)
35
+ );
36
+ }
37
+
38
+ function isDragLeavingCurrentTarget(
39
+ event: React.DragEvent<HTMLElement>,
40
+ ): boolean {
41
+ const nextTarget = event.relatedTarget;
42
+
43
+ return !(
44
+ nextTarget instanceof Node && event.currentTarget.contains(nextTarget)
45
+ );
46
+ }
47
+
48
+ function getCanvasPositionFromEvent(
49
+ event: React.DragEvent<HTMLElement>,
50
+ offset: ToolcraftPoint,
51
+ zoom: number,
52
+ ): ToolcraftPoint {
53
+ const rect = event.currentTarget.getBoundingClientRect();
54
+ const scale = zoom / 100;
55
+
56
+ return {
57
+ x: (event.clientX - rect.left - rect.width / 2 - offset.x) / scale,
58
+ y: (event.clientY - rect.top - rect.height / 2 - offset.y) / scale,
59
+ };
60
+ }
61
+
62
+ function isEventTargetInsideElement(
63
+ target: EventTarget | null,
64
+ element: HTMLElement,
65
+ ): boolean {
66
+ return target instanceof Node && element.contains(target);
67
+ }
68
+
69
+ function getNextWheelZoom(
70
+ currentZoom: number,
71
+ event: Pick<WheelEvent, "ctrlKey" | "deltaY">,
72
+ ): number {
73
+ const sensitivity = event.ctrlKey
74
+ ? wheelPinchZoomSensitivity
75
+ : wheelZoomSensitivity;
76
+ const rawDelta = -event.deltaY * sensitivity;
77
+ const zoomDelta = Math.trunc(rawDelta) || Math.sign(rawDelta);
78
+
79
+ return clampToolcraftCanvasZoom(currentZoom + zoomDelta);
80
+ }
81
+
82
+ function getZoomedCanvasOffset({
83
+ clientX,
84
+ clientY,
85
+ currentZoom,
86
+ nextZoom,
87
+ offset,
88
+ viewportElement,
89
+ }: {
90
+ clientX: number;
91
+ clientY: number;
92
+ currentZoom: number;
93
+ nextZoom: number;
94
+ offset: ToolcraftPoint;
95
+ viewportElement: HTMLElement;
96
+ }): ToolcraftPoint {
97
+ const rect = viewportElement.getBoundingClientRect();
98
+ const currentScale = currentZoom / 100;
99
+ const nextScale = nextZoom / 100;
100
+ const pointerX = clientX - rect.left - rect.width / 2;
101
+ const pointerY = clientY - rect.top - rect.height / 2;
102
+ const worldX = (pointerX - offset.x) / currentScale;
103
+ const worldY = (pointerY - offset.y) / currentScale;
104
+
105
+ return {
106
+ x: pointerX - worldX * nextScale,
107
+ y: pointerY - worldY * nextScale,
108
+ };
109
+ }
110
+
111
+ export function CanvasShell({
112
+ children,
113
+ renderDefaultMedia = true,
114
+ }: CanvasShellProps): React.JSX.Element {
115
+ const { dispatch, state } = useToolcraft();
116
+ const [dragOver, setDragOver] = React.useState(false);
117
+ const dragRef = React.useRef<CanvasDragState | null>(null);
118
+ const viewportRef = React.useRef<HTMLDivElement | null>(null);
119
+ const uploadEnabled = state.schema.canvas.upload;
120
+ const { offset, size, zoom } = state.canvas;
121
+ const scale = zoom / 100;
122
+ const visibleMediaAssets = React.useMemo(
123
+ () =>
124
+ state.mediaAssets.filter((mediaAsset) =>
125
+ isToolcraftLayerVisibleInTree(state.layers, mediaAsset.layerId),
126
+ ),
127
+ [state.layers, state.mediaAssets],
128
+ );
129
+ const hasCanvasContent = visibleMediaAssets.length > 0;
130
+ const hasCanvasSlot = React.Children.count(children) > 0;
131
+ const renderEditableCanvas =
132
+ state.schema.canvas.sizing.mode !== "intrinsic-media" ||
133
+ state.schema.canvas.sizeSource === "app" ||
134
+ hasCanvasContent ||
135
+ hasCanvasSlot;
136
+
137
+ React.useEffect(() => {
138
+ const viewportElement = viewportRef.current;
139
+
140
+ if (!viewportElement) {
141
+ return undefined;
142
+ }
143
+
144
+ const workspaceElement =
145
+ viewportElement.closest<HTMLElement>(
146
+ '[data-slot="toolcraft-runtime-app"]',
147
+ ) ?? viewportElement;
148
+ const listenerOptions = { capture: true, passive: false };
149
+ const handleWheel = (event: WheelEvent): void => {
150
+ const targetIsInsideCanvas = isEventTargetInsideElement(
151
+ event.target,
152
+ viewportElement,
153
+ );
154
+ if (!targetIsInsideCanvas) {
155
+ if (event.ctrlKey) {
156
+ event.preventDefault();
157
+ event.stopPropagation();
158
+ }
159
+
160
+ return;
161
+ }
162
+
163
+ event.preventDefault();
164
+ event.stopPropagation();
165
+
166
+ if (!event.ctrlKey) {
167
+ dispatch({
168
+ offset: {
169
+ x: offset.x - event.deltaX,
170
+ y: offset.y - event.deltaY,
171
+ },
172
+ type: "canvas.setOffset",
173
+ });
174
+ return;
175
+ }
176
+
177
+ const nextZoom = getNextWheelZoom(zoom, event);
178
+
179
+ if (nextZoom === zoom) {
180
+ return;
181
+ }
182
+
183
+ dispatch({
184
+ offset: getZoomedCanvasOffset({
185
+ clientX: event.clientX,
186
+ clientY: event.clientY,
187
+ currentZoom: zoom,
188
+ nextZoom,
189
+ offset,
190
+ viewportElement,
191
+ }),
192
+ type: "canvas.setViewport",
193
+ zoom: nextZoom,
194
+ });
195
+ };
196
+
197
+ workspaceElement.addEventListener("wheel", handleWheel, listenerOptions);
198
+
199
+ return () => {
200
+ workspaceElement.removeEventListener(
201
+ "wheel",
202
+ handleWheel,
203
+ listenerOptions,
204
+ );
205
+ };
206
+ }, [dispatch, offset, zoom]);
207
+
208
+ const beginDragOver = (event: React.DragEvent<HTMLDivElement>): void => {
209
+ if (!uploadEnabled) {
210
+ return;
211
+ }
212
+
213
+ event.preventDefault();
214
+ setDragOver(true);
215
+ };
216
+
217
+ const handlePointerDown: React.PointerEventHandler<HTMLDivElement> = (
218
+ event,
219
+ ) => {
220
+ if (!state.schema.canvas.draggable || event.button !== 0) {
221
+ return;
222
+ }
223
+
224
+ event.preventDefault();
225
+
226
+ if (typeof event.currentTarget.setPointerCapture === "function") {
227
+ event.currentTarget.setPointerCapture(event.pointerId);
228
+ }
229
+
230
+ dragRef.current = {
231
+ originX: offset.x,
232
+ originY: offset.y,
233
+ pointerId: event.pointerId,
234
+ startX: event.clientX,
235
+ startY: event.clientY,
236
+ };
237
+ };
238
+
239
+ const handlePointerMove: React.PointerEventHandler<HTMLDivElement> = (
240
+ event,
241
+ ) => {
242
+ const drag = dragRef.current;
243
+
244
+ if (!drag || drag.pointerId !== event.pointerId) {
245
+ return;
246
+ }
247
+
248
+ dispatch({
249
+ offset: {
250
+ x: drag.originX + event.clientX - drag.startX,
251
+ y: drag.originY + event.clientY - drag.startY,
252
+ },
253
+ type: "canvas.setOffset",
254
+ });
255
+ };
256
+
257
+ const handlePointerUp: React.PointerEventHandler<HTMLDivElement> = (
258
+ event,
259
+ ) => {
260
+ if (dragRef.current?.pointerId !== event.pointerId) {
261
+ return;
262
+ }
263
+
264
+ if (
265
+ typeof event.currentTarget.hasPointerCapture === "function" &&
266
+ event.currentTarget.hasPointerCapture(event.pointerId)
267
+ ) {
268
+ event.currentTarget.releasePointerCapture(event.pointerId);
269
+ }
270
+
271
+ dragRef.current = null;
272
+ };
273
+
274
+ const handleDrop: React.DragEventHandler<HTMLDivElement> = (event) => {
275
+ if (!uploadEnabled) {
276
+ return;
277
+ }
278
+
279
+ event.preventDefault();
280
+ setDragOver(false);
281
+
282
+ const uploadedFile = Array.from(event.dataTransfer?.files ?? [])[0];
283
+
284
+ if (!uploadedFile || !isImageFile(uploadedFile)) {
285
+ return;
286
+ }
287
+
288
+ const position = getCanvasPositionFromEvent(event, offset, zoom);
289
+
290
+ void readImportedImageFile(uploadedFile, size).then((importedImage) => {
291
+ if (!importedImage) {
292
+ return;
293
+ }
294
+
295
+ dispatch({
296
+ asset: {
297
+ dataUrl: importedImage.dataUrl,
298
+ fileName: uploadedFile.name,
299
+ mimeType: uploadedFile.type || "image/*",
300
+ position,
301
+ size: importedImage.size,
302
+ },
303
+ type: "media.import",
304
+ });
305
+ });
306
+ };
307
+
308
+ return (
309
+ <div
310
+ aria-label="Canvas viewport"
311
+ className="group/canvas absolute inset-0 cursor-grab touch-none overflow-hidden bg-[color:var(--background)] active:cursor-grabbing"
312
+ data-drag-over={dragOver}
313
+ data-slot="toolcraft-runtime-canvas"
314
+ onDragEnter={beginDragOver}
315
+ onDragLeave={(event) => {
316
+ if (isDragLeavingCurrentTarget(event)) {
317
+ setDragOver(false);
318
+ }
319
+ }}
320
+ onDragOver={beginDragOver}
321
+ onDrop={handleDrop}
322
+ onPointerCancel={handlePointerUp}
323
+ onPointerDown={handlePointerDown}
324
+ onPointerMove={handlePointerMove}
325
+ onPointerUp={handlePointerUp}
326
+ ref={viewportRef}
327
+ role="application"
328
+ >
329
+ <div
330
+ className="absolute top-1/2 left-1/2"
331
+ data-toolcraft-canvas-world=""
332
+ style={{
333
+ transform: `translate(-50%, -50%) translate(${offset.x}px, ${offset.y}px) scale(${scale})`,
334
+ transformOrigin: "center",
335
+ }}
336
+ >
337
+ {renderEditableCanvas ? (
338
+ <div
339
+ className="relative z-10 overflow-hidden"
340
+ data-toolcraft-canvas-content=""
341
+ data-toolcraft-editable-canvas=""
342
+ style={{
343
+ height: size.height,
344
+ width: size.width,
345
+ }}
346
+ >
347
+ {renderDefaultMedia
348
+ ? visibleMediaAssets.map((mediaAsset) => {
349
+ const selected = state.selectedLayerId === mediaAsset.layerId;
350
+
351
+ return (
352
+ <button
353
+ aria-label={`Select ${mediaAsset.fileName}`}
354
+ className={cn(
355
+ "absolute top-1/2 left-1/2 block cursor-pointer rounded-md border bg-[color:color-mix(in_oklab,var(--background)_84%,transparent)] p-1 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out",
356
+ selected
357
+ ? "border-[color:var(--link)] shadow-[0_0_0_1px_color-mix(in_oklab,var(--link)_48%,transparent)]"
358
+ : "border-[color:color-mix(in_oklab,var(--border)_10%,transparent)] hover:border-[color:color-mix(in_oklab,var(--border)_24%,transparent)]",
359
+ )}
360
+ data-canvas-media-layer={mediaAsset.layerId}
361
+ key={mediaAsset.id}
362
+ onClick={(event) => {
363
+ event.stopPropagation();
364
+ dispatch({
365
+ layerId: mediaAsset.layerId,
366
+ type: "layers.select",
367
+ });
368
+ }}
369
+ onPointerDown={(event) => event.stopPropagation()}
370
+ style={{
371
+ transform: `translate(calc(-50% + ${mediaAsset.position.x}px), calc(-50% + ${mediaAsset.position.y}px))`,
372
+ }}
373
+ type="button"
374
+ >
375
+ <img
376
+ alt={mediaAsset.fileName}
377
+ className="block select-none object-contain"
378
+ data-toolcraft-generated-output=""
379
+ draggable={false}
380
+ src={mediaAsset.dataUrl}
381
+ style={{
382
+ height: mediaAsset.size.height,
383
+ width: mediaAsset.size.width,
384
+ }}
385
+ />
386
+ </button>
387
+ );
388
+ })
389
+ : null}
390
+ {children ? (
391
+ <div
392
+ className="absolute inset-0 z-20"
393
+ data-toolcraft-canvas-slot=""
394
+ >
395
+ {children}
396
+ </div>
397
+ ) : null}
398
+ </div>
399
+ ) : null}
400
+ </div>
401
+ <div
402
+ aria-hidden="true"
403
+ className="pointer-events-none absolute inset-0 z-10 bg-[color:color-mix(in_oklab,var(--link)_8%,transparent)] opacity-0 transition-opacity duration-150 ease-out group-data-[drag-over=true]/canvas:opacity-100"
404
+ data-canvas-drag-highlight=""
405
+ />
406
+ </div>
407
+ );
408
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+ import type { ControlChangeMeta } from "@repo/ui";
5
+
6
+ import type { ToolcraftControlSchema } from "../schema/types";
7
+ import type { ToolcraftCommand, ToolcraftState } from "../state/types";
8
+
9
+ export type ToolcraftCustomControlSetValue<Value = unknown> = (
10
+ value: Value,
11
+ meta?: ControlChangeMeta,
12
+ ) => void;
13
+
14
+ export type ToolcraftCustomControlRendererProps<Value = unknown> = {
15
+ control: ToolcraftControlSchema;
16
+ controlId: string;
17
+ dispatch: React.Dispatch<ToolcraftCommand>;
18
+ keyframeAction: React.ReactNode;
19
+ name: string;
20
+ setValue: ToolcraftCustomControlSetValue<Value>;
21
+ state: ToolcraftState;
22
+ value: Value;
23
+ };
24
+
25
+ export type ToolcraftCustomControlRenderer<Value = unknown> = (
26
+ props: ToolcraftCustomControlRendererProps<Value>,
27
+ ) => React.ReactNode;
28
+
29
+ export type ToolcraftControlRendererMap = Readonly<
30
+ Record<string, ToolcraftCustomControlRenderer>
31
+ >;