@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,279 @@
1
+ import type { ResolvedToolcraftAppSchema } from "../schema/types";
2
+ import type {
3
+ ToolcraftCommand,
4
+ ToolcraftState,
5
+ ToolcraftTimelineState,
6
+ } from "../state/types";
7
+
8
+ const settingsTransferPayloadSource = "toolcraft-settings";
9
+ const settingsTransferPayloadVersion = 1;
10
+ const settingsTransferImportHistoryGroup = "settings.import";
11
+
12
+ type ToolcraftDispatch = (command: ToolcraftCommand) => void;
13
+
14
+ export type ToolcraftSettingsTransferPayload = {
15
+ appId: string;
16
+ canvas: {
17
+ size: ToolcraftState["canvas"]["size"];
18
+ };
19
+ exportedAt: string;
20
+ source: typeof settingsTransferPayloadSource;
21
+ timeline: Pick<
22
+ ToolcraftTimelineState,
23
+ "currentTimeSeconds" | "durationSeconds" | "expanded" | "isLooping"
24
+ > & {
25
+ isPlaying: false;
26
+ };
27
+ values: Record<string, unknown>;
28
+ version: typeof settingsTransferPayloadVersion;
29
+ };
30
+
31
+ type ImportContext = {
32
+ dispatch: ToolcraftDispatch;
33
+ state: ToolcraftState;
34
+ };
35
+
36
+ function isRecord(value: unknown): value is Record<string, unknown> {
37
+ return typeof value === "object" && value !== null && !Array.isArray(value);
38
+ }
39
+
40
+ function isFinitePositiveNumber(value: unknown): value is number {
41
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
42
+ }
43
+
44
+ function isFiniteNumber(value: unknown): value is number {
45
+ return typeof value === "number" && Number.isFinite(value);
46
+ }
47
+
48
+ function getKnownValueTargets(schema: ResolvedToolcraftAppSchema): Set<string> {
49
+ const targets = new Set<string>();
50
+
51
+ for (const section of schema.panels.controls?.sections ?? []) {
52
+ for (const control of Object.values(section.controls)) {
53
+ if (control.type !== "panelActions" && control.type !== "settingsTransfer") {
54
+ targets.add(control.target);
55
+ }
56
+ }
57
+ }
58
+
59
+ return targets;
60
+ }
61
+
62
+ function pickTransferValues(state: ToolcraftState): Record<string, unknown> {
63
+ const values: Record<string, unknown> = {};
64
+
65
+ for (const target of getKnownValueTargets(state.schema)) {
66
+ if (Object.hasOwn(state.values, target)) {
67
+ values[target] = state.values[target];
68
+ }
69
+ }
70
+
71
+ return values;
72
+ }
73
+
74
+ export function createToolcraftSettingsPayload(
75
+ state: ToolcraftState,
76
+ ): ToolcraftSettingsTransferPayload {
77
+ return {
78
+ appId: state.schema.settingsTransfer.appId,
79
+ canvas: {
80
+ size: state.canvas.size,
81
+ },
82
+ exportedAt: new Date().toISOString(),
83
+ source: settingsTransferPayloadSource,
84
+ timeline: {
85
+ currentTimeSeconds: state.timeline.currentTimeSeconds,
86
+ durationSeconds: state.timeline.durationSeconds,
87
+ expanded: state.timeline.expanded,
88
+ isLooping: state.timeline.isLooping,
89
+ isPlaying: false,
90
+ },
91
+ values: pickTransferValues(state),
92
+ version: settingsTransferPayloadVersion,
93
+ };
94
+ }
95
+
96
+ export function parseToolcraftSettingsPayload(
97
+ schema: ResolvedToolcraftAppSchema,
98
+ value: unknown,
99
+ ): ToolcraftSettingsTransferPayload | null {
100
+ if (!isRecord(value)) {
101
+ return null;
102
+ }
103
+
104
+ if (
105
+ value.source !== settingsTransferPayloadSource ||
106
+ value.version !== settingsTransferPayloadVersion ||
107
+ value.appId !== schema.settingsTransfer.appId
108
+ ) {
109
+ return null;
110
+ }
111
+
112
+ if (!isRecord(value.values) || !isRecord(value.canvas) || !isRecord(value.timeline)) {
113
+ return null;
114
+ }
115
+
116
+ const canvasSize = value.canvas.size;
117
+
118
+ if (!isRecord(canvasSize)) {
119
+ return null;
120
+ }
121
+
122
+ return value as ToolcraftSettingsTransferPayload;
123
+ }
124
+
125
+ function applyCanvasSize(
126
+ dispatch: ToolcraftDispatch,
127
+ size: ToolcraftSettingsTransferPayload["canvas"]["size"],
128
+ ): void {
129
+ if (isFinitePositiveNumber(size.width)) {
130
+ dispatch({
131
+ history: "merge",
132
+ historyGroup: settingsTransferImportHistoryGroup,
133
+ label: "Import settings",
134
+ target: "canvas.size.width",
135
+ type: "controls.setValue",
136
+ value: size.width,
137
+ });
138
+ }
139
+
140
+ if (isFinitePositiveNumber(size.height)) {
141
+ dispatch({
142
+ history: "merge",
143
+ historyGroup: settingsTransferImportHistoryGroup,
144
+ label: "Import settings",
145
+ target: "canvas.size.height",
146
+ type: "controls.setValue",
147
+ value: size.height,
148
+ });
149
+ }
150
+ }
151
+
152
+ function applyTimeline(
153
+ { dispatch, state }: ImportContext,
154
+ timeline: ToolcraftSettingsTransferPayload["timeline"],
155
+ ): void {
156
+ if (state.schema.panels.timeline?.enabled && isFinitePositiveNumber(timeline.durationSeconds)) {
157
+ dispatch({
158
+ durationSeconds: timeline.durationSeconds,
159
+ type: "timeline.setDuration",
160
+ });
161
+ }
162
+
163
+ if (state.schema.panels.timeline?.enabled && isFiniteNumber(timeline.currentTimeSeconds)) {
164
+ dispatch({
165
+ currentTimeSeconds: timeline.currentTimeSeconds,
166
+ type: "timeline.setCurrentTime",
167
+ });
168
+ }
169
+
170
+ if (state.schema.panels.timeline?.enabled && typeof timeline.expanded === "boolean") {
171
+ dispatch({
172
+ expanded: timeline.expanded,
173
+ type: "timeline.setExpanded",
174
+ });
175
+ }
176
+
177
+ if (
178
+ state.schema.panels.timeline?.enabled &&
179
+ typeof timeline.isLooping === "boolean" &&
180
+ timeline.isLooping !== state.timeline.isLooping
181
+ ) {
182
+ dispatch({ type: "timeline.toggleLoop" });
183
+ }
184
+
185
+ if (state.schema.panels.timeline?.enabled) {
186
+ dispatch({
187
+ isPlaying: false,
188
+ type: "timeline.setPlaying",
189
+ });
190
+ }
191
+ }
192
+
193
+ export function applyToolcraftSettingsPayload(
194
+ context: ImportContext,
195
+ payload: ToolcraftSettingsTransferPayload,
196
+ ): void {
197
+ const importableTargets = getKnownValueTargets(context.state.schema);
198
+
199
+ for (const [target, value] of Object.entries(payload.values)) {
200
+ if (!importableTargets.has(target)) {
201
+ continue;
202
+ }
203
+
204
+ context.dispatch({
205
+ history: "merge",
206
+ historyGroup: settingsTransferImportHistoryGroup,
207
+ label: "Import settings",
208
+ target,
209
+ type: "controls.setValue",
210
+ value,
211
+ });
212
+ }
213
+
214
+ applyCanvasSize(context.dispatch, payload.canvas.size);
215
+ applyTimeline(context, payload.timeline);
216
+ }
217
+
218
+ export function downloadToolcraftSettings(state: ToolcraftState): void {
219
+ const payload = createToolcraftSettingsPayload(state);
220
+ const blob = new Blob([JSON.stringify(payload, null, 2)], {
221
+ type: "application/json",
222
+ });
223
+ const url = URL.createObjectURL(blob);
224
+ const link = document.createElement("a");
225
+
226
+ link.href = url;
227
+ link.download = state.schema.settingsTransfer.fileName;
228
+ link.click();
229
+
230
+ window.setTimeout(() => {
231
+ URL.revokeObjectURL(url);
232
+ }, 0);
233
+ }
234
+
235
+ function reportImportError(error: unknown): void {
236
+ console.error("Could not import Toolcraft settings.", error);
237
+ window.alert("Could not import settings JSON.");
238
+ }
239
+
240
+ export async function importToolcraftSettings(context: ImportContext): Promise<void> {
241
+ const input = document.createElement("input");
242
+ input.accept = "application/json,.json";
243
+ input.style.display = "none";
244
+ input.type = "file";
245
+ document.body.append(input);
246
+
247
+ await new Promise<void>((resolve) => {
248
+ input.addEventListener(
249
+ "change",
250
+ () => {
251
+ resolve();
252
+ },
253
+ { once: true },
254
+ );
255
+ input.click();
256
+ });
257
+
258
+ const file = input.files?.item(0);
259
+ input.remove();
260
+
261
+ if (!file) {
262
+ return;
263
+ }
264
+
265
+ try {
266
+ const payload = parseToolcraftSettingsPayload(
267
+ context.state.schema,
268
+ JSON.parse(await file.text()),
269
+ );
270
+
271
+ if (!payload) {
272
+ throw new Error("Invalid Toolcraft settings payload.");
273
+ }
274
+
275
+ applyToolcraftSettingsPayload(context, payload);
276
+ } catch (error) {
277
+ reportImportError(error);
278
+ }
279
+ }
@@ -0,0 +1,48 @@
1
+ const previousToolcraftNamespace = ["creative", "apps", "kit"].join("-");
2
+
3
+ export function getPreviousToolcraftStorageKey(storageKey: string): string | null {
4
+ if (!storageKey.startsWith("toolcraft:")) {
5
+ return null;
6
+ }
7
+
8
+ return `${previousToolcraftNamespace}${storageKey.slice("toolcraft".length)}`;
9
+ }
10
+
11
+ export function readToolcraftLocalStorageValue(storageKey: string): string | null {
12
+ const currentValue = window.localStorage.getItem(storageKey);
13
+
14
+ if (currentValue !== null) {
15
+ return currentValue;
16
+ }
17
+
18
+ const previousStorageKey = getPreviousToolcraftStorageKey(storageKey);
19
+
20
+ if (!previousStorageKey) {
21
+ return null;
22
+ }
23
+
24
+ const previousValue = window.localStorage.getItem(previousStorageKey);
25
+
26
+ if (previousValue === null) {
27
+ return null;
28
+ }
29
+
30
+ try {
31
+ window.localStorage.setItem(storageKey, previousValue);
32
+ window.localStorage.removeItem(previousStorageKey);
33
+ } catch {
34
+ // Migration is best-effort; the caller can still use the previous value.
35
+ }
36
+
37
+ return previousValue;
38
+ }
39
+
40
+ export function removeToolcraftLocalStorageValue(storageKey: string): void {
41
+ window.localStorage.removeItem(storageKey);
42
+
43
+ const previousStorageKey = getPreviousToolcraftStorageKey(storageKey);
44
+
45
+ if (previousStorageKey) {
46
+ window.localStorage.removeItem(previousStorageKey);
47
+ }
48
+ }
@@ -0,0 +1,177 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ PortalLayerContainerProvider,
6
+ TooltipProvider,
7
+ } from "@repo/ui/primitives";
8
+
9
+ export const TOOLCRAFT_THEME_PREFERENCE_STORAGE_KEY = "appearance.theme.v1" as const;
10
+ export const TOOLCRAFT_DEFAULT_THEME_PREFERENCE = "dark" as const;
11
+
12
+ export type ToolcraftThemePreference = "dark" | "light" | "system";
13
+ export type ToolcraftResolvedTheme = "dark" | "light";
14
+
15
+ export type ToolcraftThemeContextValue = {
16
+ initialized: boolean;
17
+ resolvedTheme: ToolcraftResolvedTheme;
18
+ setThemePreference: (themePreference: ToolcraftThemePreference) => void;
19
+ themePreference: ToolcraftThemePreference;
20
+ toggleResolvedTheme: () => void;
21
+ };
22
+
23
+ const colorSchemeMediaQuery = "(prefers-color-scheme: dark)";
24
+
25
+ const ToolcraftThemeContext = React.createContext<ToolcraftThemeContextValue | null>(null);
26
+
27
+ function isToolcraftThemePreference(
28
+ value: unknown,
29
+ ): value is ToolcraftThemePreference {
30
+ return value === "dark" || value === "light" || value === "system";
31
+ }
32
+
33
+ function getSystemResolvedTheme(): ToolcraftResolvedTheme {
34
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
35
+ return TOOLCRAFT_DEFAULT_THEME_PREFERENCE;
36
+ }
37
+
38
+ return window.matchMedia(colorSchemeMediaQuery).matches ? "dark" : "light";
39
+ }
40
+
41
+ function resolveThemePreference(
42
+ themePreference: ToolcraftThemePreference,
43
+ ): ToolcraftResolvedTheme {
44
+ return themePreference === "system" ? getSystemResolvedTheme() : themePreference;
45
+ }
46
+
47
+ function readStoredThemePreference(): ToolcraftThemePreference | null {
48
+ if (typeof window === "undefined") {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ const rawValue = window.localStorage.getItem(TOOLCRAFT_THEME_PREFERENCE_STORAGE_KEY);
54
+
55
+ return isToolcraftThemePreference(rawValue) ? rawValue : null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function writeStoredThemePreference(themePreference: ToolcraftThemePreference): void {
62
+ if (typeof window === "undefined") {
63
+ return;
64
+ }
65
+
66
+ try {
67
+ window.localStorage.setItem(TOOLCRAFT_THEME_PREFERENCE_STORAGE_KEY, themePreference);
68
+ } catch {
69
+ // Keep the active theme in memory if storage is unavailable.
70
+ }
71
+ }
72
+
73
+ function getInitialThemePreference(): ToolcraftThemePreference {
74
+ return readStoredThemePreference() ?? TOOLCRAFT_DEFAULT_THEME_PREFERENCE;
75
+ }
76
+
77
+ export function ToolcraftThemeProvider({
78
+ children,
79
+ }: {
80
+ children: React.ReactNode;
81
+ }): React.JSX.Element {
82
+ const [initialized, setInitialized] = React.useState(false);
83
+ const [themePreference, setThemePreferenceState] =
84
+ React.useState<ToolcraftThemePreference>(getInitialThemePreference);
85
+ const [resolvedTheme, setResolvedTheme] = React.useState<ToolcraftResolvedTheme>(() =>
86
+ resolveThemePreference(getInitialThemePreference()),
87
+ );
88
+
89
+ const setThemePreference = React.useCallback(
90
+ (nextThemePreference: ToolcraftThemePreference): void => {
91
+ const nextResolvedTheme = resolveThemePreference(nextThemePreference);
92
+
93
+ writeStoredThemePreference(nextThemePreference);
94
+ setThemePreferenceState(nextThemePreference);
95
+ setResolvedTheme(nextResolvedTheme);
96
+ setInitialized(true);
97
+ },
98
+ [],
99
+ );
100
+
101
+ const toggleResolvedTheme = React.useCallback((): void => {
102
+ setThemePreference(resolvedTheme === "dark" ? "light" : "dark");
103
+ }, [resolvedTheme, setThemePreference]);
104
+
105
+ React.useEffect(() => {
106
+ setThemePreference(themePreference);
107
+ }, [setThemePreference, themePreference]);
108
+
109
+ React.useEffect(() => {
110
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
111
+ return undefined;
112
+ }
113
+
114
+ const mediaQueryList = window.matchMedia(colorSchemeMediaQuery);
115
+ const handleChange = (): void => {
116
+ if (themePreference !== "system") {
117
+ return;
118
+ }
119
+
120
+ const nextResolvedTheme = getSystemResolvedTheme();
121
+
122
+ setResolvedTheme(nextResolvedTheme);
123
+ };
124
+
125
+ mediaQueryList.addEventListener("change", handleChange);
126
+
127
+ return () => {
128
+ mediaQueryList.removeEventListener("change", handleChange);
129
+ };
130
+ }, [themePreference]);
131
+
132
+ const value = React.useMemo(
133
+ () => ({
134
+ initialized,
135
+ resolvedTheme,
136
+ setThemePreference,
137
+ themePreference,
138
+ toggleResolvedTheme,
139
+ }),
140
+ [initialized, resolvedTheme, setThemePreference, themePreference, toggleResolvedTheme],
141
+ );
142
+
143
+ const portalRootRef = React.useRef<HTMLDivElement | null>(null);
144
+
145
+ return (
146
+ <ToolcraftThemeContext.Provider value={value}>
147
+ <div
148
+ data-toolcraft-theme={resolvedTheme}
149
+ data-toolcraft-theme-scope=""
150
+ style={{
151
+ colorScheme: resolvedTheme,
152
+ display: "contents",
153
+ }}
154
+ >
155
+ <PortalLayerContainerProvider container={portalRootRef}>
156
+ <TooltipProvider>{children}</TooltipProvider>
157
+ </PortalLayerContainerProvider>
158
+ <div
159
+ aria-hidden="true"
160
+ data-toolcraft-portal-root=""
161
+ ref={portalRootRef}
162
+ style={{ display: "contents" }}
163
+ />
164
+ </div>
165
+ </ToolcraftThemeContext.Provider>
166
+ );
167
+ }
168
+
169
+ export function useToolcraftTheme(): ToolcraftThemeContextValue {
170
+ const context = React.useContext(ToolcraftThemeContext);
171
+
172
+ if (!context) {
173
+ throw new Error("useToolcraftTheme must be used within ToolcraftThemeProvider");
174
+ }
175
+
176
+ return context;
177
+ }