@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,353 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ animate,
6
+ motion,
7
+ useDragControls,
8
+ useMotionValue,
9
+ type MotionValue,
10
+ type PanInfo,
11
+ } from "motion/react";
12
+
13
+ import {
14
+ panelDragHandleSelector,
15
+ panelDragIgnoredTargetSelector,
16
+ panelDragTransition,
17
+ panelHostConfig,
18
+ panelSnapAnimation,
19
+ } from "./panel-host-config";
20
+ import { resolvePanelSnapPosition } from "./panel-host-geometry";
21
+ import type {
22
+ ToolcraftPanelHostProps,
23
+ PanelContainerProps,
24
+ PanelHostProps,
25
+ PanelPoint,
26
+ PanelStageProps,
27
+ PanelViewport,
28
+ } from "./panel-host-types";
29
+ import { useToolcraft } from "./use-toolcraft";
30
+
31
+ function cn(...classNames: Array<string | false | null | undefined>): string {
32
+ return classNames.filter(Boolean).join(" ");
33
+ }
34
+
35
+ function isPanelDragHandleTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {
36
+ if (!(target instanceof Element)) {
37
+ return false;
38
+ }
39
+
40
+ const handleTarget = target.closest(panelDragHandleSelector);
41
+
42
+ return handleTarget !== null && currentTarget.contains(handleTarget);
43
+ }
44
+
45
+ function shouldIgnorePanelTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {
46
+ if (!(target instanceof Element)) {
47
+ return false;
48
+ }
49
+
50
+ const ignoredTarget = target.closest(panelDragIgnoredTargetSelector);
51
+
52
+ return ignoredTarget !== null && currentTarget.contains(ignoredTarget);
53
+ }
54
+
55
+ function shouldIgnorePanelDrag(event: React.PointerEvent<HTMLElement>): boolean {
56
+ return shouldIgnorePanelTarget(event.target, event.currentTarget);
57
+ }
58
+
59
+ function getPanelVisualViewport(): PanelViewport {
60
+ const visualViewport = window.visualViewport;
61
+
62
+ if (visualViewport) {
63
+ return {
64
+ height: visualViewport.height,
65
+ offsetLeft: visualViewport.offsetLeft,
66
+ offsetTop: visualViewport.offsetTop,
67
+ width: visualViewport.width,
68
+ };
69
+ }
70
+
71
+ return {
72
+ height: window.innerHeight,
73
+ offsetLeft: 0,
74
+ offsetTop: 0,
75
+ width: window.innerWidth,
76
+ };
77
+ }
78
+
79
+ function animatePanelMotionValue(value: MotionValue<number>, target: number): void {
80
+ void animate(value, target, panelSnapAnimation);
81
+ }
82
+
83
+ function usePanelSnapControls({
84
+ onPositionChange,
85
+ onResetPosition,
86
+ position = { x: 0, y: 0 },
87
+ snap,
88
+ }: Pick<PanelHostProps, "onPositionChange" | "onResetPosition" | "position" | "snap">) {
89
+ const panelRef = React.useRef<HTMLDivElement>(null);
90
+ const x = useMotionValue(position.x);
91
+ const y = useMotionValue(position.y);
92
+
93
+ React.useEffect(() => {
94
+ animatePanelMotionValue(x, position.x);
95
+ animatePanelMotionValue(y, position.y);
96
+ }, [position.x, position.y, x, y]);
97
+
98
+ const publishPosition = (nextPosition: PanelPoint): void => {
99
+ onPositionChange?.(nextPosition);
100
+ };
101
+
102
+ const publishCurrentPosition = (): void => {
103
+ publishPosition({ x: x.get(), y: y.get() });
104
+ };
105
+
106
+ const handleDragEnd = (info: PanInfo): void => {
107
+ if (!snap || snap.edges.length === 0) {
108
+ publishCurrentPosition();
109
+ return;
110
+ }
111
+
112
+ const panel = panelRef.current;
113
+
114
+ if (!panel) {
115
+ publishCurrentPosition();
116
+ return;
117
+ }
118
+
119
+ const rect = panel.getBoundingClientRect();
120
+ const offset = { x: x.get(), y: y.get() };
121
+ const target = resolvePanelSnapPosition({
122
+ dimensions: { height: rect.height, width: rect.width },
123
+ edges: snap.edges,
124
+ margin: snap.margin,
125
+ position: { x: rect.left, y: rect.top },
126
+ velocity: { x: info.velocity.x / 1000, y: info.velocity.y / 1000 },
127
+ viewport: getPanelVisualViewport(),
128
+ zone: snap.zone,
129
+ });
130
+
131
+ if (!target) {
132
+ publishCurrentPosition();
133
+ return;
134
+ }
135
+
136
+ const nextPosition = {
137
+ x: target.x - (rect.left - offset.x),
138
+ y: target.y - (rect.top - offset.y),
139
+ };
140
+
141
+ animatePanelMotionValue(x, nextPosition.x);
142
+ animatePanelMotionValue(y, nextPosition.y);
143
+ publishPosition(nextPosition);
144
+ };
145
+
146
+ const resetPosition = (): void => {
147
+ animatePanelMotionValue(x, 0);
148
+ animatePanelMotionValue(y, 0);
149
+ publishPosition({ x: 0, y: 0 });
150
+ onResetPosition?.();
151
+ };
152
+
153
+ return { handleDragEnd, panelRef, resetPosition, x, y };
154
+ }
155
+
156
+ export function PanelHost({
157
+ children,
158
+ className,
159
+ dragMode,
160
+ innerClassName,
161
+ onPositionChange,
162
+ onResetPosition,
163
+ panelId,
164
+ panelType,
165
+ position,
166
+ snap,
167
+ style,
168
+ }: PanelHostProps): React.JSX.Element {
169
+ const config = panelHostConfig[panelType];
170
+ const resolvedDragMode = dragMode ?? config.dragMode;
171
+ const resolvedSnap = snap ?? { edges: config.snapEdges };
172
+ const resolvedPanelId = panelId ?? config.panelId;
173
+ const dragControls = useDragControls();
174
+ const {
175
+ handleDragEnd: handleSnapDragEnd,
176
+ panelRef,
177
+ resetPosition,
178
+ x,
179
+ y,
180
+ } = usePanelSnapControls({
181
+ onPositionChange,
182
+ onResetPosition,
183
+ position,
184
+ snap: resolvedSnap,
185
+ });
186
+ const [isDragging, setIsDragging] = React.useState(false);
187
+
188
+ const handlePointerDown: React.PointerEventHandler<HTMLElement> = (event) => {
189
+ if (event.button !== 0) {
190
+ return;
191
+ }
192
+
193
+ if (
194
+ resolvedDragMode === "handle" &&
195
+ !isPanelDragHandleTarget(event.target, event.currentTarget)
196
+ ) {
197
+ return;
198
+ }
199
+
200
+ if (event.defaultPrevented || shouldIgnorePanelDrag(event)) {
201
+ return;
202
+ }
203
+
204
+ event.preventDefault();
205
+ event.stopPropagation();
206
+ dragControls.start(event);
207
+ };
208
+
209
+ const handleDragEnd = (_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo): void => {
210
+ setIsDragging(false);
211
+ handleSnapDragEnd(info);
212
+ };
213
+
214
+ const handleDoubleClick: React.MouseEventHandler<HTMLElement> = (event) => {
215
+ if (
216
+ !resolvedSnap ||
217
+ event.defaultPrevented ||
218
+ (resolvedDragMode === "handle" &&
219
+ !isPanelDragHandleTarget(event.target, event.currentTarget)) ||
220
+ shouldIgnorePanelTarget(event.target, event.currentTarget)
221
+ ) {
222
+ return;
223
+ }
224
+
225
+ resetPosition();
226
+ };
227
+
228
+ return (
229
+ <div className={cn("pointer-events-none", config.wrapperClassName, className)} style={style}>
230
+ <motion.div
231
+ className={cn("pointer-events-auto", isDragging && "cursor-grabbing", innerClassName)}
232
+ data-dragging={isDragging ? "true" : "false"}
233
+ data-drag-mode={resolvedDragMode}
234
+ data-panel-id={resolvedPanelId}
235
+ data-panel-type={panelType}
236
+ data-slot="toolcraft-runtime-panel-host"
237
+ data-snap-edges={resolvedSnap?.edges.join(" ")}
238
+ drag
239
+ dragControls={dragControls}
240
+ dragElastic={0}
241
+ dragListener={false}
242
+ dragMomentum={false}
243
+ dragTransition={panelDragTransition}
244
+ onDoubleClick={handleDoubleClick}
245
+ onDragEnd={handleDragEnd}
246
+ onDragStart={() => setIsDragging(true)}
247
+ onPointerDown={handlePointerDown}
248
+ ref={panelRef}
249
+ style={{ x, y }}
250
+ >
251
+ {children}
252
+ </motion.div>
253
+ </div>
254
+ );
255
+ }
256
+
257
+ export function ToolcraftPanelHost({
258
+ onPositionChange,
259
+ onResetPosition,
260
+ panelType,
261
+ position,
262
+ ...props
263
+ }: ToolcraftPanelHostProps): React.JSX.Element {
264
+ const { dispatch, state } = useToolcraft();
265
+ const panelState = state.panels[panelType];
266
+
267
+ return (
268
+ <PanelHost
269
+ {...props}
270
+ onPositionChange={(offset) => {
271
+ onPositionChange?.(offset);
272
+ dispatch({ offset, panelId: panelType, type: "panels.setOffset" });
273
+ }}
274
+ onResetPosition={() => {
275
+ onResetPosition?.();
276
+ dispatch({ panelId: panelType, type: "panels.resetOffset" });
277
+ }}
278
+ panelType={panelType}
279
+ position={position ?? panelState.offset}
280
+ />
281
+ );
282
+ }
283
+
284
+ export function PanelStage({
285
+ children,
286
+ className,
287
+ ...props
288
+ }: PanelStageProps): React.JSX.Element {
289
+ return (
290
+ <div
291
+ {...props}
292
+ className={cn(
293
+ "relative w-full min-w-0 overflow-hidden rounded-lg bg-[color:var(--background)]",
294
+ className,
295
+ )}
296
+ data-toolcraft-panel-stage=""
297
+ >
298
+ {children}
299
+ </div>
300
+ );
301
+ }
302
+
303
+ export function PanelContainer({
304
+ children,
305
+ className,
306
+ dragMode,
307
+ onPanelStateChange,
308
+ panelClassName,
309
+ panelState,
310
+ panelType,
311
+ placement,
312
+ ...props
313
+ }: PanelContainerProps): React.JSX.Element {
314
+ const config = panelHostConfig[panelType];
315
+
316
+ if (placement === "surface") {
317
+ return <>{children}</>;
318
+ }
319
+
320
+ if (placement === "floating") {
321
+ return (
322
+ <PanelHost
323
+ className={panelClassName}
324
+ dragMode={dragMode}
325
+ onPositionChange={(offset) => onPanelStateChange?.({ offset })}
326
+ panelType={panelType}
327
+ position={panelState?.offset}
328
+ snap={{ edges: config.snapEdges }}
329
+ >
330
+ {children}
331
+ </PanelHost>
332
+ );
333
+ }
334
+
335
+ return (
336
+ <PanelStage
337
+ {...props}
338
+ className={cn(config.stageClassName, className)}
339
+ data-panel-type={panelType}
340
+ >
341
+ <PanelHost
342
+ className={panelClassName}
343
+ dragMode={dragMode}
344
+ onPositionChange={(offset) => onPanelStateChange?.({ offset })}
345
+ panelType={panelType}
346
+ position={panelState?.offset}
347
+ snap={{ edges: config.snapEdges }}
348
+ >
349
+ {children}
350
+ </PanelHost>
351
+ </PanelStage>
352
+ );
353
+ }
@@ -0,0 +1,132 @@
1
+ import type * as React from "react";
2
+ import { describe, expect, expectTypeOf, it } from "vitest";
3
+
4
+ import { defineToolcraft } from "../schema/define-toolcraft";
5
+ import { CanvasShell } from "./canvas-shell";
6
+ import { ControlsPanel } from "./controls-panel";
7
+ import { ToolcraftApp } from "./toolcraft-app";
8
+ import { ToolcraftRoot } from "./toolcraft-root";
9
+ import { LayersPanel } from "./layers-panel";
10
+ import { PanelContainer, PanelHost, PanelStage } from "./panel-host";
11
+ import { TimelinePanel } from "./timeline-panel";
12
+ import { ToolbarPanel } from "./toolbar-panel";
13
+
14
+ const runtimePublicApiSchema = defineToolcraft({
15
+ canvas: { enabled: true },
16
+ panels: {},
17
+ });
18
+
19
+ describe("Toolcraft template runtime public API", () => {
20
+ it("keeps app assembly props narrow", () => {
21
+ expectTypeOf<
22
+ keyof React.ComponentProps<typeof ToolcraftApp>
23
+ >().toEqualTypeOf<
24
+ | "canvasContent"
25
+ | "className"
26
+ | "controlRenderers"
27
+ | "onPanelAction"
28
+ | "renderDefaultCanvasMedia"
29
+ | "schema"
30
+ | "style"
31
+ >();
32
+ expectTypeOf<
33
+ keyof React.ComponentProps<typeof ToolcraftRoot>
34
+ >().toEqualTypeOf<"children" | "initialState" | "schema">();
35
+ });
36
+
37
+ it("keeps runtime panel props focused on host integration", () => {
38
+ type RuntimePanelKeys =
39
+ | "className"
40
+ | "framed"
41
+ | "onPanelStateChange"
42
+ | "panelPlacement"
43
+ | "panelState";
44
+ type ControlsPanelKeys = RuntimePanelKeys | "controlRenderers" | "onPanelAction";
45
+
46
+ expectTypeOf<
47
+ keyof React.ComponentProps<typeof ControlsPanel>
48
+ >().toEqualTypeOf<ControlsPanelKeys>();
49
+ expectTypeOf<
50
+ keyof React.ComponentProps<typeof TimelinePanel>
51
+ >().toEqualTypeOf<RuntimePanelKeys | "defaultExpanded">();
52
+ expectTypeOf<
53
+ keyof React.ComponentProps<typeof ToolbarPanel>
54
+ >().toEqualTypeOf<RuntimePanelKeys>();
55
+ expectTypeOf<
56
+ keyof React.ComponentProps<typeof LayersPanel>
57
+ >().toEqualTypeOf<RuntimePanelKeys | "groupCreation">();
58
+ });
59
+
60
+ it("keeps shell and panel host props separate", () => {
61
+ expectTypeOf<
62
+ keyof React.ComponentProps<typeof CanvasShell>
63
+ >().toEqualTypeOf<"children" | "renderDefaultMedia">();
64
+ expectTypeOf<React.ComponentProps<typeof PanelHost>>().toHaveProperty(
65
+ "panelType",
66
+ );
67
+ expectTypeOf<React.ComponentProps<typeof PanelContainer>>().toHaveProperty(
68
+ "placement",
69
+ );
70
+ expectTypeOf<React.ComponentProps<typeof PanelStage>>().toHaveProperty(
71
+ "children",
72
+ );
73
+ });
74
+
75
+ it("rejects hidden runtime wiring props at compile time", () => {
76
+ expect(<ToolcraftApp schema={runtimePublicApiSchema} />).toBeTruthy();
77
+ expect(<CanvasShell />).toBeTruthy();
78
+ expect(<ToolbarPanel />).toBeTruthy();
79
+
80
+ const appWithInitialStateProps: React.ComponentProps<
81
+ typeof ToolcraftApp
82
+ > = {
83
+ // @ts-expect-error Runtime state seeding belongs to ToolcraftRoot, not the app shell.
84
+ initialState: {},
85
+ schema: runtimePublicApiSchema,
86
+ };
87
+ const appWithChildrenProps: React.ComponentProps<typeof ToolcraftApp> =
88
+ {
89
+ // @ts-expect-error ToolcraftApp is assembled from schema and does not accept children.
90
+ children: "Child",
91
+ schema: runtimePublicApiSchema,
92
+ };
93
+ const appWithToolbarThemeToggleProps: React.ComponentProps<
94
+ typeof ToolcraftApp
95
+ > = {
96
+ schema: runtimePublicApiSchema,
97
+ // @ts-expect-error Toolbar theme visibility belongs to schema.toolbar.theme.
98
+ toolbarThemeToggle: false,
99
+ };
100
+ const appWithCanvasSlotProps: React.ComponentProps<typeof ToolcraftApp> =
101
+ {
102
+ canvasContent: <div />,
103
+ onPanelAction: ({ action }) => {
104
+ void action.value;
105
+ },
106
+ renderDefaultCanvasMedia: false,
107
+ schema: runtimePublicApiSchema,
108
+ };
109
+ const canvasWithSlotProps: React.ComponentProps<typeof CanvasShell> = {
110
+ children: <div />,
111
+ renderDefaultMedia: false,
112
+ };
113
+ // @ts-expect-error Canvas upload belongs to schema.canvas.upload.
114
+ const canvasWithUpload = <CanvasShell upload />;
115
+ // @ts-expect-error CanvasShell is a runtime-owned shell, not a styled wrapper.
116
+ const styledCanvas = <CanvasShell className="custom" />;
117
+ // @ts-expect-error Toolbar history visibility belongs to schema.toolbar.history.
118
+ const toolbarWithHistory = <ToolbarPanel history />;
119
+ // @ts-expect-error Toolbar theme visibility belongs to schema.toolbar.theme.
120
+ const toolbarWithThemeToggle = <ToolbarPanel themeToggle={false} />;
121
+
122
+ expect(appWithInitialStateProps).toBeTruthy();
123
+ expect(appWithChildrenProps).toBeTruthy();
124
+ expect(appWithToolbarThemeToggleProps).toBeTruthy();
125
+ expect(appWithCanvasSlotProps).toBeTruthy();
126
+ expect(canvasWithSlotProps).toBeTruthy();
127
+ expect(canvasWithUpload).toBeTruthy();
128
+ expect(styledCanvas).toBeTruthy();
129
+ expect(toolbarWithHistory).toBeTruthy();
130
+ expect(toolbarWithThemeToggle).toBeTruthy();
131
+ });
132
+ });
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { defineToolcraft } from "../schema/define-toolcraft";
4
+ import { createToolcraftState } from "../state/create-template-state";
5
+ import { toolcraftReducer } from "../state/reducer";
6
+ import type { ToolcraftCommand, ToolcraftState } from "../state/types";
7
+ import {
8
+ applyToolcraftSettingsPayload,
9
+ createToolcraftSettingsPayload,
10
+ parseToolcraftSettingsPayload,
11
+ } from "./settings-transfer";
12
+
13
+ function createSettingsSchema() {
14
+ return defineToolcraft({
15
+ canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
16
+ panels: {
17
+ controls: {
18
+ sections: [
19
+ {
20
+ controls: {
21
+ prompt: {
22
+ defaultValue: "Initial prompt",
23
+ label: "Prompt",
24
+ target: "generation.prompt",
25
+ type: "text",
26
+ },
27
+ opacity: {
28
+ defaultValue: 75,
29
+ label: "Opacity",
30
+ target: "style.opacity",
31
+ type: "slider",
32
+ },
33
+ },
34
+ title: "Generation",
35
+ },
36
+ ],
37
+ title: "Settings Test",
38
+ },
39
+ timeline: { mode: "playback" },
40
+ },
41
+ settingsTransfer: {
42
+ appId: "Settings Test",
43
+ enabled: true,
44
+ },
45
+ });
46
+ }
47
+
48
+ function reduceWithCommands(
49
+ initialState: ToolcraftState,
50
+ run: (dispatch: (command: ToolcraftCommand) => void) => void,
51
+ ): ToolcraftState {
52
+ let state = initialState;
53
+
54
+ run((command) => {
55
+ state = toolcraftReducer(state, command);
56
+ });
57
+
58
+ return state;
59
+ }
60
+
61
+ describe("settings transfer", () => {
62
+ it("creates and parses a versioned app-specific payload", () => {
63
+ const schema = createSettingsSchema();
64
+ const state = createToolcraftState(schema, {
65
+ values: {
66
+ "generation.prompt": "Exported prompt",
67
+ "style.opacity": 24,
68
+ },
69
+ });
70
+
71
+ const payload = createToolcraftSettingsPayload(state);
72
+
73
+ expect(payload).toMatchObject({
74
+ appId: "settings-test",
75
+ canvas: {
76
+ size: { height: 720, unit: "px", width: 1280 },
77
+ },
78
+ source: "toolcraft-settings",
79
+ values: {
80
+ "generation.prompt": "Exported prompt",
81
+ "style.opacity": 24,
82
+ },
83
+ version: 1,
84
+ });
85
+ expect(parseToolcraftSettingsPayload(schema, payload)).toEqual(payload);
86
+ expect(
87
+ parseToolcraftSettingsPayload(
88
+ schema,
89
+ { ...payload, appId: "other-app" },
90
+ ),
91
+ ).toBeNull();
92
+ });
93
+
94
+ it("applies imported values, canvas size, and timeline through runtime commands", () => {
95
+ const schema = createSettingsSchema();
96
+ const initialState = createToolcraftState(schema, {
97
+ timeline: {
98
+ currentTimeSeconds: 0,
99
+ durationSeconds: 8,
100
+ expanded: false,
101
+ isLooping: true,
102
+ isPlaying: true,
103
+ },
104
+ values: {
105
+ "generation.prompt": "Initial prompt",
106
+ "style.opacity": 75,
107
+ },
108
+ });
109
+ const payload = {
110
+ ...createToolcraftSettingsPayload(initialState),
111
+ canvas: {
112
+ size: { height: 900, unit: "px" as const, width: 1600 },
113
+ },
114
+ timeline: {
115
+ currentTimeSeconds: 3,
116
+ durationSeconds: 12,
117
+ expanded: true,
118
+ isLooping: false,
119
+ isPlaying: false as const,
120
+ },
121
+ values: {
122
+ "generation.prompt": "Imported prompt",
123
+ "unknown.target": "Ignored",
124
+ },
125
+ };
126
+
127
+ const state = reduceWithCommands(initialState, (dispatch) => {
128
+ applyToolcraftSettingsPayload({ dispatch, state: initialState }, payload);
129
+ });
130
+
131
+ expect(state.values["generation.prompt"]).toBe("Imported prompt");
132
+ expect(state.values["unknown.target"]).toBeUndefined();
133
+ expect(state.canvas.size).toEqual({ height: 900, unit: "px", width: 1600 });
134
+ expect(state.timeline).toMatchObject({
135
+ currentTimeSeconds: 3,
136
+ durationSeconds: 12,
137
+ expanded: true,
138
+ isLooping: false,
139
+ isPlaying: false,
140
+ });
141
+ expect(state.history.undo).toEqual(
142
+ expect.arrayContaining([
143
+ expect.objectContaining({
144
+ group: "settings.import",
145
+ label: "Import settings",
146
+ }),
147
+ ]),
148
+ );
149
+ });
150
+ });