@pixel-point/toolcraft 0.0.14 → 0.0.15

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 (577) hide show
  1. package/README.md +13 -2
  2. package/package.json +1 -1
  3. package/scripts/prepare-pack.mjs +2 -28
  4. package/src/cli-io.mjs +174 -0
  5. package/src/cli-package-manager-flow.test.mjs +206 -0
  6. package/src/cli.mjs +7 -495
  7. package/src/cli.test.mjs +34 -177
  8. package/src/create-command.mjs +152 -0
  9. package/src/create-options.mjs +194 -0
  10. package/src/generate-test-assertions.mjs +27 -0
  11. package/src/generate-test-doc-assertions.mjs +162 -0
  12. package/src/generate-test-file-assertions.mjs +251 -0
  13. package/src/generate-test-fs-utils.mjs +59 -0
  14. package/src/generate-test-integrity-assertions.mjs +30 -0
  15. package/src/generate-test-integrity-framework-assertions.mjs +222 -0
  16. package/src/generate-test-integrity-manifest-assertions.mjs +150 -0
  17. package/src/generate-test-integrity-ownership-assertions.mjs +127 -0
  18. package/src/generate-test-integrity-test-utils.mjs +102 -0
  19. package/src/generate-test-package-assertions.mjs +75 -0
  20. package/src/generate-test-product-fixture.mjs +84 -0
  21. package/src/generate-test-product-fixture.test.mjs +73 -0
  22. package/src/generate-test-standalone-assertions.mjs +209 -0
  23. package/src/generate-transactional.test.mjs +151 -0
  24. package/src/generate.mjs +182 -102
  25. package/src/generate.test.mjs +242 -338
  26. package/src/generated-integrity-manifest.mjs +127 -0
  27. package/src/generated-integrity-manifest.test.mjs +151 -0
  28. package/src/generated-source-policy.mjs +22 -0
  29. package/src/generated-source-policy.test.mjs +35 -0
  30. package/src/generated-target-policy.mjs +55 -0
  31. package/src/generation-transaction.mjs +135 -0
  32. package/src/generation-transaction.test.mjs +74 -0
  33. package/src/package-json.mjs +18 -1
  34. package/src/package-json.test.mjs +38 -5
  35. package/src/package-manager.mjs +2 -0
  36. package/src/package-manager.test.mjs +5 -1
  37. package/src/packaged-template-fallback.test.mjs +126 -0
  38. package/src/prepare-pack-templates.mjs +34 -0
  39. package/templates/runtime/contracts/component-contract-builders.ts +95 -0
  40. package/templates/runtime/contracts/component-contracts.choices.test.ts +293 -0
  41. package/templates/runtime/contracts/component-contracts.choices.ts +322 -0
  42. package/templates/runtime/contracts/component-contracts.inputs.test.ts +210 -0
  43. package/templates/runtime/contracts/component-contracts.inputs.ts +252 -0
  44. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +116 -0
  45. package/templates/runtime/contracts/component-contracts.media-custom.ts +144 -0
  46. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +229 -0
  47. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +76 -0
  48. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +135 -0
  49. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +148 -0
  50. package/templates/runtime/contracts/component-contracts.runtime.ts +361 -0
  51. package/templates/runtime/contracts/component-contracts.test.ts +39 -1458
  52. package/templates/runtime/contracts/component-contracts.ts +44 -1505
  53. package/templates/runtime/contracts/component-contracts.visual.test.ts +316 -0
  54. package/templates/runtime/contracts/component-contracts.visual.ts +385 -0
  55. package/templates/runtime/contracts/contract-manifest.test.ts +64 -0
  56. package/templates/runtime/contracts/decision-contracts.test.ts +29 -4
  57. package/templates/runtime/contracts/decision-contracts.ts +38 -1
  58. package/templates/runtime/index.ts +1 -0
  59. package/templates/runtime/react/{runtime-public-api.test.tsx → app-shell/runtime-public-api.test.tsx} +29 -7
  60. package/templates/runtime/react/{settings-transfer.test.ts → app-shell/settings-transfer.test.ts} +4 -4
  61. package/templates/runtime/react/{settings-transfer.ts → app-shell/settings-transfer.ts} +3 -3
  62. package/templates/runtime/react/{toolbar-panel.test.tsx → app-shell/toolbar-panel.test.tsx} +2 -2
  63. package/templates/runtime/react/{toolbar-panel.tsx → app-shell/toolbar-panel.tsx} +3 -3
  64. package/templates/runtime/react/{toolcraft-app.integration.test.tsx → app-shell/toolcraft-app.integration.test.tsx} +6 -6
  65. package/templates/runtime/react/{toolcraft-app.test.tsx → app-shell/toolcraft-app.test.tsx} +3 -3
  66. package/templates/runtime/react/{toolcraft-app.tsx → app-shell/toolcraft-app.tsx} +11 -8
  67. package/templates/runtime/react/{toolcraft-root.test.tsx → app-shell/toolcraft-root.test.tsx} +1 -1
  68. package/templates/runtime/react/{toolcraft-root.tsx → app-shell/toolcraft-root.tsx} +5 -5
  69. package/templates/runtime/react/{use-toolcraft.ts → app-shell/use-toolcraft.ts} +1 -1
  70. package/templates/runtime/react/canvas/canvas-default-media-layer.tsx +81 -0
  71. package/templates/runtime/react/canvas/canvas-media-transform.ts +33 -0
  72. package/templates/runtime/react/canvas/canvas-shell-interactions.test.tsx +156 -0
  73. package/templates/runtime/react/canvas/canvas-shell-media-rendering.test.tsx +87 -0
  74. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +102 -0
  75. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +108 -0
  76. package/templates/runtime/react/canvas/canvas-shell-upload.test.tsx +106 -0
  77. package/templates/runtime/react/canvas/canvas-shell.tsx +146 -0
  78. package/templates/runtime/react/canvas/canvas-upload-routing.ts +84 -0
  79. package/templates/runtime/react/{media-file.ts → canvas/media-file.ts} +1 -1
  80. package/templates/runtime/react/canvas/use-canvas-drop-import.ts +145 -0
  81. package/templates/runtime/react/canvas/use-canvas-viewport-interactions.ts +227 -0
  82. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +89 -0
  83. package/templates/runtime/react/{controls-panel-filedrop-reorder.test.tsx → controls-panel/__tests__/controls-panel-filedrop-reorder.test.tsx} +4 -4
  84. package/templates/runtime/react/controls-panel/__tests__/controls-panel.actions-layout.test.tsx +91 -0
  85. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +213 -0
  86. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-layout.test.tsx +366 -0
  87. package/templates/runtime/react/controls-panel/__tests__/controls-panel.color-picker-models.test.tsx +169 -0
  88. package/templates/runtime/react/controls-panel/__tests__/controls-panel.compound-layout.test.tsx +160 -0
  89. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +352 -0
  90. package/templates/runtime/react/controls-panel/__tests__/controls-panel.curves.test.tsx +226 -0
  91. package/templates/runtime/react/controls-panel/__tests__/controls-panel.floating-shell.test.tsx +13 -0
  92. package/templates/runtime/react/controls-panel/__tests__/controls-panel.font-picker.test.tsx +302 -0
  93. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +401 -0
  94. package/templates/runtime/react/controls-panel/__tests__/controls-panel.gradient-control.test.tsx +139 -0
  95. package/templates/runtime/react/controls-panel/__tests__/controls-panel.help-policy.test.tsx +103 -0
  96. package/templates/runtime/react/controls-panel/__tests__/controls-panel.image-picker.test.tsx +16 -0
  97. package/templates/runtime/react/controls-panel/__tests__/controls-panel.inline-layout.test.tsx +290 -0
  98. package/templates/runtime/react/controls-panel/__tests__/controls-panel.keyframes.test.tsx +175 -0
  99. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-files.test.tsx +121 -0
  100. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-image.test.tsx +363 -0
  101. package/templates/runtime/react/controls-panel/__tests__/controls-panel.media-upload-multi-image.test.tsx +105 -0
  102. package/templates/runtime/react/controls-panel/__tests__/controls-panel.range-slider-editing.test.tsx +149 -0
  103. package/templates/runtime/react/controls-panel/__tests__/controls-panel.section-shell.test.tsx +138 -0
  104. package/templates/runtime/react/controls-panel/__tests__/controls-panel.select-layout.test.tsx +148 -0
  105. package/templates/runtime/react/controls-panel/__tests__/controls-panel.setup-controls.test.tsx +189 -0
  106. package/templates/runtime/react/controls-panel/__tests__/controls-panel.slider-rendering.test.tsx +317 -0
  107. package/templates/runtime/react/controls-panel/__tests__/controls-panel.state-reset.test.tsx +137 -0
  108. package/templates/runtime/react/controls-panel/__tests__/controls-panel.structure.test.tsx +66 -0
  109. package/templates/runtime/react/controls-panel/__tests__/controls-panel.text-input.test.tsx +205 -0
  110. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-interaction.test.tsx +388 -0
  111. package/templates/runtime/react/controls-panel/__tests__/controls-panel.vector-layout.test.tsx +155 -0
  112. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +172 -0
  113. package/templates/runtime/react/{control-conditions.ts → controls-panel/conditions/control-conditions.ts} +26 -3
  114. package/templates/runtime/react/{control-renderers.ts → controls-panel/control-renderers.ts} +2 -2
  115. package/templates/runtime/react/controls-panel/controls-panel.tsx +250 -0
  116. package/templates/runtime/react/controls-panel/keyframes/controls-panel-keyframes.tsx +265 -0
  117. package/templates/runtime/react/controls-panel/layout/controls-panel-collapse-storage.ts +131 -0
  118. package/templates/runtime/react/controls-panel/layout/controls-panel-help.tsx +132 -0
  119. package/templates/runtime/react/controls-panel/layout/controls-panel-inline-layout.tsx +311 -0
  120. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +216 -0
  121. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +401 -0
  122. package/templates/runtime/react/controls-panel/renderers/controls-panel-action-renderer.tsx +137 -0
  123. package/templates/runtime/react/controls-panel/renderers/controls-panel-basic-renderers.tsx +421 -0
  124. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +286 -0
  125. package/templates/runtime/react/controls-panel/renderers/controls-panel-compound-renderers.tsx +366 -0
  126. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +173 -0
  127. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +23 -0
  128. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +55 -0
  129. package/templates/runtime/react/controls-panel/renderers/controls-panel-settings-transfer-renderer.tsx +45 -0
  130. package/templates/runtime/react/controls-panel/testing/controls-panel-test-utils.tsx +300 -0
  131. package/templates/runtime/react/controls-panel/values/controls-panel-aspect-ratio-values.ts +94 -0
  132. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +87 -0
  133. package/templates/runtime/react/controls-panel/values/controls-panel-color-values.ts +82 -0
  134. package/templates/runtime/react/controls-panel/values/controls-panel-font-values.ts +69 -0
  135. package/templates/runtime/react/controls-panel/values/controls-panel-spatial-values.ts +64 -0
  136. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +93 -0
  137. package/templates/runtime/react/controls-panel/values/controls-panel-value-primitives.ts +30 -0
  138. package/templates/runtime/react/controls-panel/values/controls-panel-values.ts +7 -0
  139. package/templates/runtime/react/index.ts +13 -13
  140. package/templates/runtime/react/{layer-tree.ts → layers/layer-tree.ts} +1 -1
  141. package/templates/runtime/react/layers/layers-panel-drag-drop.test.tsx +190 -0
  142. package/templates/runtime/react/layers/layers-panel-editing.test.tsx +41 -0
  143. package/templates/runtime/react/layers/layers-panel-insert-targets.test.ts +87 -0
  144. package/templates/runtime/react/layers/layers-panel-insert-targets.ts +254 -0
  145. package/templates/runtime/react/layers/layers-panel-model.ts +15 -0
  146. package/templates/runtime/react/layers/layers-panel-rendering.test.tsx +43 -0
  147. package/templates/runtime/react/layers/layers-panel-reorder.ts +148 -0
  148. package/templates/runtime/react/layers/layers-panel-row.tsx +475 -0
  149. package/templates/runtime/react/layers/layers-panel-test-utils.tsx +135 -0
  150. package/templates/runtime/react/layers/layers-panel-visibility.test.tsx +126 -0
  151. package/templates/runtime/react/layers/layers-panel.tsx +349 -0
  152. package/templates/runtime/react/layers/use-layers-panel-drag-controller.ts +327 -0
  153. package/templates/runtime/react/{panel-host-types.ts → panel-host/panel-host-types.ts} +2 -2
  154. package/templates/runtime/react/{panel-host.test.tsx → panel-host/panel-host.test.tsx} +3 -3
  155. package/templates/runtime/react/{panel-host.tsx → panel-host/panel-host.tsx} +1 -1
  156. package/templates/runtime/react/timeline/timeline-easing-editor.tsx +409 -0
  157. package/templates/runtime/react/timeline/timeline-easing-geometry.ts +42 -0
  158. package/templates/runtime/react/timeline/timeline-easing-icons.tsx +135 -0
  159. package/templates/runtime/react/timeline/timeline-easing-model.test.ts +87 -0
  160. package/templates/runtime/react/timeline/timeline-easing-model.ts +271 -0
  161. package/templates/runtime/react/timeline/timeline-easing-popover-content.tsx +227 -0
  162. package/templates/runtime/react/timeline/timeline-easing-popover-layout.ts +69 -0
  163. package/templates/runtime/react/timeline/timeline-easing-popover.tsx +60 -0
  164. package/templates/runtime/react/timeline/timeline-event-targets.ts +19 -0
  165. package/templates/runtime/react/timeline/timeline-expanded-content.tsx +299 -0
  166. package/templates/runtime/react/timeline/timeline-icon-button.tsx +51 -0
  167. package/templates/runtime/react/timeline/timeline-keyframe-row.tsx +360 -0
  168. package/templates/runtime/react/timeline/timeline-keyframes.ts +23 -0
  169. package/templates/runtime/react/timeline/timeline-panel-floating-layout.test.tsx +328 -0
  170. package/templates/runtime/react/timeline/timeline-panel-header.tsx +404 -0
  171. package/templates/runtime/react/timeline/timeline-panel-keyframes.test.tsx +90 -0
  172. package/templates/runtime/react/timeline/timeline-panel-layout.ts +30 -0
  173. package/templates/runtime/react/timeline/timeline-panel-responsive-layout.ts +182 -0
  174. package/templates/runtime/react/timeline/timeline-panel-test-utils.tsx +88 -0
  175. package/templates/runtime/react/timeline/timeline-panel-transport.test.tsx +202 -0
  176. package/templates/runtime/react/timeline/timeline-panel-upload.test.tsx +53 -0
  177. package/templates/runtime/react/timeline/timeline-panel.tsx +449 -0
  178. package/templates/runtime/react/timeline/timeline-playback-hooks.ts +254 -0
  179. package/templates/runtime/schema/assembly-contract.ts +271 -0
  180. package/templates/runtime/schema/control-schema-normalization.ts +55 -0
  181. package/templates/runtime/schema/controls-panel-actions.ts +141 -0
  182. package/templates/runtime/schema/controls-panel-layout-groups.ts +144 -0
  183. package/templates/runtime/schema/controls-panel-normalization.ts +19 -0
  184. package/templates/runtime/schema/controls-panel-section-layout.ts +140 -0
  185. package/templates/runtime/schema/controls-panel-section-titles.ts +161 -0
  186. package/templates/runtime/schema/define-toolcraft-test-utils.ts +18 -0
  187. package/templates/runtime/schema/define-toolcraft.actions.test.ts +182 -0
  188. package/templates/runtime/schema/define-toolcraft.assembly.test.ts +246 -0
  189. package/templates/runtime/schema/define-toolcraft.canvas.test.ts +150 -0
  190. package/templates/runtime/schema/define-toolcraft.control-layout.test.ts +345 -0
  191. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +304 -0
  192. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +69 -0
  193. package/templates/runtime/schema/define-toolcraft.setup-persistence.test.ts +66 -0
  194. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +116 -0
  195. package/templates/runtime/schema/define-toolcraft.setup-settings-transfer.test.ts +291 -0
  196. package/templates/runtime/schema/define-toolcraft.ts +24 -1449
  197. package/templates/runtime/schema/panels-schema-normalization.ts +49 -0
  198. package/templates/runtime/schema/runtime-section-titles.ts +1 -0
  199. package/templates/runtime/schema/runtime-setup-section.ts +282 -0
  200. package/templates/runtime/schema/schema-resolvers.ts +168 -0
  201. package/templates/runtime/schema/types.ts +17 -0
  202. package/templates/runtime/state/canvas-reducer.ts +106 -0
  203. package/templates/runtime/state/canvas-state.ts +209 -0
  204. package/templates/runtime/state/controls-reducer.ts +277 -0
  205. package/templates/runtime/state/history-patches.ts +174 -0
  206. package/templates/runtime/state/layer-state.ts +12 -0
  207. package/templates/runtime/state/layers-reducer.ts +318 -0
  208. package/templates/runtime/state/media-reducer.ts +283 -0
  209. package/templates/runtime/state/media-state.ts +105 -0
  210. package/templates/runtime/state/panels-reducer.ts +58 -0
  211. package/templates/runtime/state/persistence-merge.ts +53 -0
  212. package/templates/runtime/state/persistence-reader-canvas.ts +27 -0
  213. package/templates/runtime/state/persistence-reader-layers.ts +73 -0
  214. package/templates/runtime/state/persistence-reader-media.ts +82 -0
  215. package/templates/runtime/state/persistence-reader-panels.ts +56 -0
  216. package/templates/runtime/state/persistence-reader-primitives.ts +27 -0
  217. package/templates/runtime/state/persistence-reader-timeline.ts +145 -0
  218. package/templates/runtime/state/persistence-reader-values.ts +40 -0
  219. package/templates/runtime/state/persistence-readers.ts +72 -0
  220. package/templates/runtime/state/persistence-shared.ts +23 -0
  221. package/templates/runtime/state/persistence-snapshot.ts +57 -0
  222. package/templates/runtime/state/persistence.ts +16 -581
  223. package/templates/runtime/state/reducer-canvas-panels.test.ts +324 -0
  224. package/templates/runtime/state/reducer-controls.test.ts +158 -0
  225. package/templates/runtime/state/reducer-layers.test.ts +306 -0
  226. package/templates/runtime/state/reducer-media-import-sizing.test.ts +197 -0
  227. package/templates/runtime/state/reducer-media-reorder-history.test.ts +116 -0
  228. package/templates/runtime/state/reducer-media-reset.test.ts +249 -0
  229. package/templates/runtime/state/reducer-media-test-utils.ts +40 -0
  230. package/templates/runtime/state/reducer-media-transform.test.ts +52 -0
  231. package/templates/runtime/state/reducer-test-utils.ts +30 -0
  232. package/templates/runtime/state/reducer-timeline.test.ts +242 -0
  233. package/templates/runtime/state/reducer.ts +57 -1785
  234. package/templates/runtime/state/timeline-reducer.ts +412 -0
  235. package/templates/runtime/state/timeline-values.test.ts +45 -0
  236. package/templates/runtime/state/timeline-values.ts +45 -0
  237. package/templates/runtime/testing/performance-browser-policy.test.ts +81 -0
  238. package/templates/runtime/testing/performance-browser-policy.ts +102 -0
  239. package/templates/runtime/testing/performance-budget-validation.ts +84 -0
  240. package/templates/runtime/testing/performance-control-classification.test.ts +106 -0
  241. package/templates/runtime/testing/performance-control-classification.ts +101 -0
  242. package/templates/runtime/testing/performance-coverage-context.ts +44 -0
  243. package/templates/runtime/testing/performance-coverage-renderer-policy.ts +221 -0
  244. package/templates/runtime/testing/performance-coverage-scenario-policy.ts +147 -0
  245. package/templates/runtime/testing/performance-coverage-target-policy.ts +121 -0
  246. package/templates/runtime/testing/performance-coverage-validator.ts +34 -0
  247. package/templates/runtime/testing/performance-export-coverage.test.ts +234 -0
  248. package/templates/runtime/testing/performance-fixture-load-profile.test.ts +360 -0
  249. package/templates/runtime/testing/performance-fixture-media.test.ts +80 -0
  250. package/templates/runtime/testing/performance-fixture-stress.test.ts +314 -0
  251. package/templates/runtime/testing/performance-fixture-validation.ts +255 -0
  252. package/templates/runtime/testing/performance-fixture-values.ts +275 -0
  253. package/templates/runtime/testing/performance-fixture-workload.test.ts +140 -0
  254. package/templates/runtime/testing/performance-load-profile-validation.ts +467 -0
  255. package/templates/runtime/testing/performance-renderer-coverage.test.ts +325 -0
  256. package/templates/runtime/testing/performance-renderer-pipeline-coverage.test.ts +115 -0
  257. package/templates/runtime/testing/performance-renderer-pipeline-interactions.test.ts +201 -0
  258. package/templates/runtime/testing/performance-renderer-pipeline-passes.test.ts +247 -0
  259. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +360 -0
  260. package/templates/runtime/testing/performance-renderer-technique-validation.ts +329 -0
  261. package/templates/runtime/testing/performance-renderer-technique.test.ts +275 -0
  262. package/templates/runtime/testing/performance-renderer-validation.ts +2 -0
  263. package/templates/runtime/testing/performance-scenario-coverage.test.ts +303 -0
  264. package/templates/runtime/testing/performance-scenario-validation.ts +49 -0
  265. package/templates/runtime/testing/performance-schema-derived-workloads.test.ts +231 -0
  266. package/templates/runtime/testing/performance-schema-queries.ts +181 -0
  267. package/templates/runtime/testing/performance-test-fixtures/index.ts +4 -0
  268. package/templates/runtime/testing/performance-test-fixtures/renderer-pipelines.ts +322 -0
  269. package/templates/runtime/testing/performance-test-fixtures/scenarios.ts +172 -0
  270. package/templates/runtime/testing/performance-test-fixtures/schemas.ts +151 -0
  271. package/templates/runtime/testing/performance-test-fixtures/stress-fixtures.ts +133 -0
  272. package/templates/runtime/testing/performance-test-fixtures.ts +1 -0
  273. package/templates/runtime/testing/performance-types.ts +338 -0
  274. package/templates/runtime/testing/performance.ts +15 -2485
  275. package/templates/starter/AGENTS.md +41 -26
  276. package/templates/starter/LICENSE.md +18 -95
  277. package/templates/starter/NOTICE.md +3 -3
  278. package/templates/starter/docs/toolcraft/README.md +5 -5
  279. package/templates/starter/docs/toolcraft/acceptance-testing.md +36 -8
  280. package/templates/starter/docs/toolcraft/assembly-workflow.md +12 -15
  281. package/templates/starter/docs/toolcraft/component-rules.md +6 -2
  282. package/templates/starter/docs/toolcraft/core/layout.md +1 -0
  283. package/templates/starter/docs/toolcraft/core/performance.md +7 -4
  284. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +11 -4
  285. package/templates/starter/docs/toolcraft/core/timeline-animation.md +1 -0
  286. package/templates/starter/docs/toolcraft/custom-controls.md +8 -1
  287. package/templates/starter/docs/toolcraft/decision-contract.md +26 -22
  288. package/templates/starter/docs/toolcraft/performance.md +42 -16
  289. package/templates/starter/docs/toolcraft/schema-reference.md +34 -23
  290. package/templates/starter/docs/toolcraft/workflow.md +25 -17
  291. package/templates/starter/e2e/app-browser-acceptance-evidence.spec.ts +111 -0
  292. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +118 -0
  293. package/templates/starter/e2e/app-browser-performance-evidence.spec.ts +217 -0
  294. package/templates/starter/e2e/app-browser-performance-probe.spec.ts +322 -0
  295. package/templates/starter/e2e/app-browser-proof-session.spec.ts +196 -0
  296. package/templates/starter/e2e/app-browser-runtime-evidence.spec.ts +9 -0
  297. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +240 -0
  298. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +180 -0
  299. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +310 -0
  300. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +432 -0
  301. package/templates/starter/e2e/app-browser-shell.spec.ts +171 -0
  302. package/templates/starter/e2e/app-browser-timeline-layers-reference.spec.ts +70 -0
  303. package/templates/starter/e2e/app-performance.spec.ts +0 -491
  304. package/templates/starter/e2e/browser-acceptance-outcome-helpers.ts +97 -0
  305. package/templates/starter/e2e/browser-acceptance-transition-helpers.ts +74 -0
  306. package/templates/starter/e2e/browser-conditional-output-evidence-helpers.ts +230 -0
  307. package/templates/starter/e2e/browser-control-target-helpers.ts +109 -0
  308. package/templates/starter/e2e/browser-layer-evidence-helpers.ts +229 -0
  309. package/templates/starter/e2e/browser-proof-session.ts +392 -0
  310. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +136 -0
  311. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +290 -0
  312. package/templates/starter/e2e/browser-runtime-evidence.ts +27 -0
  313. package/templates/starter/e2e/browser-state-evidence-helpers.ts +205 -0
  314. package/templates/starter/e2e/browser-timeline-evidence-helpers.ts +319 -0
  315. package/templates/starter/e2e/canvas-handle-helpers.ts +86 -4
  316. package/templates/starter/e2e/export-artifact-helpers.ts +132 -0
  317. package/templates/starter/e2e/performance-budget-helpers.ts +159 -0
  318. package/templates/starter/e2e/performance-canvas-helpers.ts +123 -0
  319. package/templates/starter/e2e/performance-control-helpers.ts +3 -0
  320. package/templates/starter/e2e/performance-control-layout-helpers.ts +161 -0
  321. package/templates/starter/e2e/performance-fixture-helpers.ts +217 -0
  322. package/templates/starter/e2e/performance-frame-probe.ts +247 -0
  323. package/templates/starter/e2e/performance-helpers.ts +6 -854
  324. package/templates/starter/e2e/performance-interaction-measurement.ts +251 -0
  325. package/templates/starter/e2e/performance-measurement-evidence.ts +72 -0
  326. package/templates/starter/e2e/performance-output-action-helpers.spec.ts +43 -0
  327. package/templates/starter/e2e/performance-output-action-helpers.ts +109 -0
  328. package/templates/starter/e2e/performance-probe-helpers.ts +12 -0
  329. package/templates/starter/e2e/performance-render-scale-helpers.ts +55 -0
  330. package/templates/starter/e2e/performance-slider-helpers.ts +271 -0
  331. package/templates/starter/e2e/product-observable-helpers.ts +72 -17
  332. package/templates/starter/e2e/stable-outcome-helpers.ts +208 -0
  333. package/templates/starter/package.json +10 -5
  334. package/templates/starter/playwright.config.ts +5 -2
  335. package/templates/starter/scripts/check-ai-skills.mjs +14 -8
  336. package/templates/starter/scripts/check-ai-skills.test.mjs +64 -0
  337. package/templates/starter/scripts/check-toolcraft-code-health.mjs +108 -0
  338. package/templates/starter/scripts/check-toolcraft-code-health.test.mjs +213 -0
  339. package/templates/starter/scripts/check-toolcraft-docs.mjs +47 -34
  340. package/templates/starter/scripts/check-toolcraft-integrity.mjs +141 -144
  341. package/templates/starter/scripts/run-browser-performance.mjs +118 -0
  342. package/templates/starter/scripts/run-browser-performance.test.mjs +53 -0
  343. package/templates/starter/scripts/toolcraft-code-health-core.mjs +297 -0
  344. package/templates/starter/scripts/toolcraft-contract-doc-integrity.mjs +77 -0
  345. package/templates/starter/scripts/toolcraft-contract-doc-integrity.test.mjs +81 -0
  346. package/templates/starter/scripts/toolcraft-contract-manifest.json +113 -0
  347. package/templates/starter/scripts/toolcraft-contract-manifest.mjs +128 -0
  348. package/templates/starter/scripts/toolcraft-contract-manifest.test.mjs +72 -0
  349. package/templates/starter/scripts/toolcraft-integrity-manifest.mjs +172 -0
  350. package/templates/starter/scripts/toolcraft-integrity-policy.mjs +60 -0
  351. package/templates/starter/scripts/toolcraft-product-boundary-ast-utils.mjs +23 -0
  352. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +264 -0
  353. package/templates/starter/scripts/toolcraft-product-boundary-test-fixtures.mjs +16 -0
  354. package/templates/starter/scripts/toolcraft-product-boundary.mjs +243 -0
  355. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +289 -0
  356. package/templates/starter/scripts/toolcraft-product-dependency-graph.mjs +176 -0
  357. package/templates/starter/scripts/toolcraft-product-dependency-graph.test.mjs +196 -0
  358. package/templates/starter/scripts/toolcraft-product-dependency-resolution.mjs +279 -0
  359. package/templates/starter/scripts/toolcraft-product-evidence-boundary-ast.mjs +91 -0
  360. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +138 -0
  361. package/templates/starter/scripts/toolcraft-product-style-boundary.mjs +219 -0
  362. package/templates/starter/scripts/toolcraft-source-inventory.d.mts +46 -0
  363. package/templates/starter/scripts/toolcraft-source-inventory.mjs +187 -0
  364. package/templates/starter/scripts/toolcraft-source-inventory.test.mjs +132 -0
  365. package/templates/starter/scripts/toolcraft-source-ownership.mjs +167 -0
  366. package/templates/starter/scripts/toolcraft-source-ownership.test.mjs +113 -0
  367. package/templates/starter/scripts/toolcraft-static-string.mjs +104 -0
  368. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +123 -0
  369. package/templates/starter/scripts/toolcraft-verification-receipt.d.mts +19 -0
  370. package/templates/starter/scripts/toolcraft-verification-receipt.mjs +284 -0
  371. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +282 -0
  372. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.d.mts +32 -0
  373. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.mjs +88 -0
  374. package/templates/starter/scripts/toolcraft-vitest-runtime-contract.test.mjs +87 -0
  375. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.mjs +109 -0
  376. package/templates/starter/scripts/toolcraft-vitest-runtime-evidence-reporter.test.mjs +111 -0
  377. package/templates/starter/scripts/toolcraft-workflow-routes.mjs +314 -0
  378. package/templates/starter/scripts/toolcraft-workflow-routes.test.mjs +204 -0
  379. package/templates/starter/src/app/acceptance/actions.ts +30 -0
  380. package/templates/starter/src/app/acceptance/animation-intent.ts +184 -0
  381. package/templates/starter/src/app/acceptance/canvas-handle-acceptance.ts +70 -0
  382. package/templates/starter/src/app/acceptance/color-bank-labels.ts +164 -0
  383. package/templates/starter/src/app/acceptance/conditions.ts +80 -0
  384. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +70 -0
  385. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +271 -0
  386. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +149 -0
  387. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +189 -0
  388. package/templates/starter/src/app/acceptance/control-acceptance.ts +70 -0
  389. package/templates/starter/src/app/acceptance/control-component-rules.ts +277 -0
  390. package/templates/starter/src/app/acceptance/control-labels.ts +223 -0
  391. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +107 -0
  392. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +104 -0
  393. package/templates/starter/src/app/acceptance/control-layout-model.ts +159 -0
  394. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +164 -0
  395. package/templates/starter/src/app/acceptance/control-layout.ts +45 -0
  396. package/templates/starter/src/app/acceptance/control-order.ts +123 -0
  397. package/templates/starter/src/app/acceptance/control-parts.ts +47 -0
  398. package/templates/starter/src/app/acceptance/control-section-inventory.ts +183 -0
  399. package/templates/starter/src/app/acceptance/controls.ts +27 -0
  400. package/templates/starter/src/app/acceptance/coverage.ts +81 -0
  401. package/templates/starter/src/app/acceptance/custom-controls.ts +140 -0
  402. package/templates/starter/src/app/acceptance/inline-layout.ts +192 -0
  403. package/templates/starter/src/app/acceptance/media-upload.ts +55 -0
  404. package/templates/starter/src/app/acceptance/output-background-rules.ts +135 -0
  405. package/templates/starter/src/app/acceptance/output-export-actions.test.ts +76 -0
  406. package/templates/starter/src/app/acceptance/output-export-actions.ts +34 -0
  407. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +46 -0
  408. package/templates/starter/src/app/acceptance/output-export-model.ts +146 -0
  409. package/templates/starter/src/app/acceptance/output-export.ts +45 -0
  410. package/templates/starter/src/app/acceptance/output-image-export-rules.ts +87 -0
  411. package/templates/starter/src/app/acceptance/reference-runtime-coverage.ts +96 -0
  412. package/templates/starter/src/app/acceptance/reference-runtime-feature-inventory.ts +171 -0
  413. package/templates/starter/src/app/acceptance/reference-runtime-study.ts +73 -0
  414. package/templates/starter/src/app/acceptance/reference-runtime-timeline.ts +291 -0
  415. package/templates/starter/src/app/acceptance/reference-runtime.ts +99 -0
  416. package/templates/starter/src/app/acceptance/runtime-coverage.ts +290 -0
  417. package/templates/starter/src/app/acceptance/runtime-setup.ts +117 -0
  418. package/templates/starter/src/app/acceptance/section-title-rules.ts +8 -0
  419. package/templates/starter/src/app/acceptance/sections.ts +6 -0
  420. package/templates/starter/src/app/acceptance/semantic.ts +63 -0
  421. package/templates/starter/src/app/acceptance/timeline-loop.ts +21 -0
  422. package/templates/starter/src/app/acceptance/types.ts +363 -0
  423. package/templates/starter/src/app/acceptance/validate-coverage.ts +217 -0
  424. package/templates/starter/src/app/acceptance/validation-pipeline.test.ts +56 -0
  425. package/templates/starter/src/app/acceptance/validation-pipeline.ts +60 -0
  426. package/templates/starter/src/app/acceptance/video-reference-study.ts +217 -0
  427. package/templates/starter/src/app/starter-acceptance-data.ts +21 -0
  428. package/templates/starter/src/app/starter-acceptance.animation-intent.test.ts +383 -0
  429. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +349 -0
  430. package/templates/starter/src/app/starter-acceptance.base-coverage.test.ts +47 -0
  431. package/templates/starter/src/app/starter-acceptance.canvas-handles.test.ts +71 -0
  432. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +199 -0
  433. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +244 -0
  434. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +63 -0
  435. package/templates/starter/src/app/starter-acceptance.control-label-context.test.ts +172 -0
  436. package/templates/starter/src/app/starter-acceptance.control-labels.test.ts +387 -0
  437. package/templates/starter/src/app/starter-acceptance.control-naming-rules.test.ts +215 -0
  438. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +274 -0
  439. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +275 -0
  440. package/templates/starter/src/app/starter-acceptance.curves-rules.test.ts +184 -0
  441. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +200 -0
  442. package/templates/starter/src/app/starter-acceptance.custom-control-coverage.test.ts +111 -0
  443. package/templates/starter/src/app/starter-acceptance.custom-control-fit-check.test.ts +135 -0
  444. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +74 -0
  445. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +107 -0
  446. package/templates/starter/src/app/starter-acceptance.framework-boundary.test.ts +327 -0
  447. package/templates/starter/src/app/starter-acceptance.image-video-export-settings.test.ts +328 -0
  448. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +242 -0
  449. package/templates/starter/src/app/starter-acceptance.layers.test.ts +203 -0
  450. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +110 -0
  451. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +153 -0
  452. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +97 -0
  453. package/templates/starter/src/app/starter-acceptance.persistence-coverage.test.ts +134 -0
  454. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +202 -0
  455. package/templates/starter/src/app/starter-acceptance.product-readiness-rules.test.ts +14 -0
  456. package/templates/starter/src/app/starter-acceptance.product-readiness-test-utils.ts +25 -0
  457. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +50 -0
  458. package/templates/starter/src/app/starter-acceptance.product-worklog.test.ts +27 -0
  459. package/templates/starter/src/app/starter-acceptance.range-slider-rules.test.ts +147 -0
  460. package/templates/starter/src/app/starter-acceptance.reference-custom-timeline.test.ts +197 -0
  461. package/templates/starter/src/app/starter-acceptance.reference-foundation.test.ts +304 -0
  462. package/templates/starter/src/app/starter-acceptance.reference-test-utils.ts +102 -0
  463. package/templates/starter/src/app/starter-acceptance.reference-timeline.test.ts +267 -0
  464. package/templates/starter/src/app/starter-acceptance.schema-test-utils.ts +116 -0
  465. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +373 -0
  466. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +260 -0
  467. package/templates/starter/src/app/starter-acceptance.section-titles.test.ts +180 -0
  468. package/templates/starter/src/app/starter-acceptance.setup-readiness.test.ts +205 -0
  469. package/templates/starter/src/app/starter-acceptance.slider-rules.test.ts +470 -0
  470. package/templates/starter/src/app/starter-acceptance.slider-unit-rules.test.ts +61 -0
  471. package/templates/starter/src/app/starter-acceptance.source-test-utils.ts +43 -0
  472. package/templates/starter/src/app/starter-acceptance.test-utils.ts +21 -0
  473. package/templates/starter/src/app/starter-acceptance.text-control-kind.test.ts +89 -0
  474. package/templates/starter/src/app/starter-acceptance.timeline-playback.test.ts +264 -0
  475. package/templates/starter/src/app/starter-acceptance.timeline-test-utils.ts +56 -0
  476. package/templates/starter/src/app/starter-acceptance.toggle-pair-layout.test.ts +200 -0
  477. package/templates/starter/src/app/starter-acceptance.toggle-parameter-layout.test.ts +277 -0
  478. package/templates/starter/src/app/starter-acceptance.ts +151 -4576
  479. package/templates/starter/src/app/starter-acceptance.video-export-test-utils.ts +37 -0
  480. package/templates/starter/src/app/starter-acceptance.video-reference-study.test.ts +262 -0
  481. package/templates/starter/src/app/starter-acceptance.worklog-test-utils.ts +428 -0
  482. package/templates/starter/src/app/starter-acceptance.worklog.test.ts +283 -0
  483. package/templates/starter/src/app/starter-automated-runtime-evidence.test.ts +34 -0
  484. package/templates/starter/src/app/starter-composition.tsx +7 -0
  485. package/templates/starter/src/app/starter-performance-test-utils.ts +275 -0
  486. package/templates/starter/src/app/starter-performance.budgets.test.ts +374 -0
  487. package/templates/starter/src/app/starter-performance.gates.test.ts +128 -0
  488. package/templates/starter/src/app/starter-performance.renderer-source.test.ts +136 -0
  489. package/templates/starter/src/app/starter-performance.renderer-technique.test.ts +291 -0
  490. package/templates/starter/src/app/starter-performance.scenarios.test.ts +355 -0
  491. package/templates/starter/src/app/test-evidence/browser-runtime-contract.test.ts +253 -0
  492. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +243 -0
  493. package/templates/starter/src/routes/index.tsx +11 -2
  494. package/templates/starter/vite.config.ts +8 -0
  495. package/templates/ui/components/controls/color/color-control-types.ts +38 -0
  496. package/templates/ui/components/controls/color/color-control.tsx +19 -437
  497. package/templates/ui/components/controls/color/color-footer.tsx +272 -0
  498. package/templates/ui/components/controls/color/color-model-slider.tsx +271 -0
  499. package/templates/ui/components/controls/color/color-opacity-input.tsx +94 -0
  500. package/templates/ui/components/controls/color/color-surface.tsx +194 -0
  501. package/templates/ui/components/controls/color/color-value-control.tsx +159 -0
  502. package/templates/ui/components/controls/color/color-value-utils.ts +170 -0
  503. package/templates/ui/components/controls/color/index.ts +10 -3
  504. package/templates/ui/components/controls/color/palette-control-controller.ts +407 -0
  505. package/templates/ui/components/controls/color/palette-control-layout.ts +14 -0
  506. package/templates/ui/components/controls/color/palette-control-types.ts +50 -0
  507. package/templates/ui/components/controls/color/palette-control-view.tsx +174 -0
  508. package/templates/ui/components/controls/color/palette-control.tsx +6 -544
  509. package/templates/ui/components/controls/color/style-guide-color-picker-color-utils.ts +39 -0
  510. package/templates/ui/components/controls/color/style-guide-color-picker-controller.ts +370 -0
  511. package/templates/ui/components/controls/color/style-guide-color-picker-hue-handlers.ts +66 -0
  512. package/templates/ui/components/controls/color/style-guide-color-picker-interaction-state.ts +49 -0
  513. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +2 -2
  514. package/templates/ui/components/controls/color/style-guide-color-picker-model.ts +125 -0
  515. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +10 -707
  516. package/templates/ui/components/controls/color/style-guide-color-picker-surface-drag.ts +93 -0
  517. package/templates/ui/components/controls/color/style-guide-color-picker-surface-geometry.ts +114 -0
  518. package/templates/ui/components/controls/color/style-guide-color-picker-surface-preview.ts +45 -0
  519. package/templates/ui/components/controls/color/style-guide-color-picker-types.ts +84 -0
  520. package/templates/ui/components/controls/color/style-guide-color-picker-view.tsx +69 -0
  521. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +6 -495
  522. package/templates/ui/components/controls/curves/curves-control.tsx +4 -3
  523. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +39 -515
  524. package/templates/ui/components/controls/file-drop/file-drop-empty-state.tsx +35 -0
  525. package/templates/ui/components/controls/file-drop/file-drop-file-list.tsx +160 -0
  526. package/templates/ui/components/controls/file-drop/file-drop-image-grid.tsx +175 -0
  527. package/templates/ui/components/controls/file-drop/file-drop-model.tsx +170 -0
  528. package/templates/ui/components/controls/file-drop/file-drop-single-preview.tsx +63 -0
  529. package/templates/ui/components/controls/file-drop/file-drop-types.ts +48 -0
  530. package/templates/ui/components/controls/file-drop/index.ts +2 -2
  531. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +133 -906
  532. package/templates/ui/components/controls/font-picker/font-picker-footer.tsx +116 -0
  533. package/templates/ui/components/controls/font-picker/font-picker-list.tsx +194 -0
  534. package/templates/ui/components/controls/font-picker/font-picker-popover-content.tsx +191 -0
  535. package/templates/ui/components/controls/font-picker/font-picker-value.ts +222 -0
  536. package/templates/ui/components/controls/font-picker/index.ts +4 -1
  537. package/templates/ui/components/controls/font-picker/use-font-picker-preview-pipeline.ts +163 -0
  538. package/templates/ui/components/controls/font-picker/use-font-picker-virtual-list.ts +293 -0
  539. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +2 -25
  540. package/templates/ui/components/controls/gradient/gradient-control.tsx +4 -464
  541. package/templates/ui/components/controls/gradient/gradient-stops-controller.ts +332 -0
  542. package/templates/ui/components/controls/gradient/gradient-stops-track.tsx +159 -0
  543. package/templates/ui/components/controls/vector/vector-control-types.ts +26 -0
  544. package/templates/ui/components/controls/vector/vector-control.tsx +10 -486
  545. package/templates/ui/components/controls/vector/vector-pad-field.tsx +211 -0
  546. package/templates/ui/components/controls/vector/vector-pad-geometry.ts +61 -0
  547. package/templates/ui/components/controls/vector/vector-pad-parts.tsx +72 -0
  548. package/templates/ui/components/controls/vector/vector-pad-variants.ts +20 -0
  549. package/templates/ui/components/controls/vector/vector-size-field.tsx +104 -0
  550. package/templates/ui/components/controls/vector/vector-value.ts +46 -0
  551. package/templates/ui/components/primitives/slider/slider-discrete-state.ts +181 -0
  552. package/templates/ui/components/primitives/slider/slider-marker-policy.ts +52 -0
  553. package/templates/ui/components/primitives/slider/slider-pointer-dragging.ts +87 -0
  554. package/templates/ui/components/primitives/slider/slider-runtime.ts +109 -0
  555. package/templates/ui/components/primitives/slider/slider-types.ts +21 -0
  556. package/templates/ui/components/primitives/slider/slider.tsx +2 -403
  557. package/templates/runtime/react/canvas-shell.test.tsx +0 -554
  558. package/templates/runtime/react/canvas-shell.tsx +0 -585
  559. package/templates/runtime/react/controls-panel.test.tsx +0 -5446
  560. package/templates/runtime/react/controls-panel.tsx +0 -3220
  561. package/templates/runtime/react/layers-panel.test.tsx +0 -487
  562. package/templates/runtime/react/layers-panel.tsx +0 -1348
  563. package/templates/runtime/react/timeline-panel.test.tsx +0 -751
  564. package/templates/runtime/react/timeline-panel.tsx +0 -3078
  565. package/templates/runtime/schema/define-toolcraft.test.ts +0 -1742
  566. package/templates/runtime/state/reducer.test.ts +0 -1703
  567. package/templates/runtime/testing/performance.test.ts +0 -3081
  568. package/templates/starter/e2e/app-browser-acceptance.spec.ts +0 -785
  569. package/templates/starter/src/app/starter-acceptance.test.ts +0 -10068
  570. package/templates/starter/src/app/starter-performance.test.ts +0 -1583
  571. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +0 -490
  572. /package/templates/runtime/react/{storage-key-migration.ts → app-shell/storage-key-migration.ts} +0 -0
  573. /package/templates/runtime/react/{theme-runtime.tsx → app-shell/theme-runtime.tsx} +0 -0
  574. /package/templates/runtime/react/{curve-geometry.test.ts → controls-panel/__tests__/curve-geometry.test.ts} +0 -0
  575. /package/templates/runtime/react/{panel-host-config.ts → panel-host/panel-host-config.ts} +0 -0
  576. /package/templates/runtime/react/{panel-host-geometry.test.ts → panel-host/panel-host-geometry.test.ts} +0 -0
  577. /package/templates/runtime/react/{panel-host-geometry.ts → panel-host/panel-host-geometry.ts} +0 -0
@@ -1,1808 +1,80 @@
1
+ import { reduceToolcraftCanvasCommand } from "./canvas-reducer";
2
+ import { reduceToolcraftControlsCommand } from "./controls-reducer";
1
3
  import {
2
- getToolcraftCanvasSizeTargetDimension,
3
- isToolcraftCanvasAspectRatioTarget,
4
- isToolcraftTimelinePanelExtendedTarget,
5
- isToolcraftTimelinePanelVisibleTarget,
6
- } from "../schema/runtime-targets";
7
- import { getToolcraftCanvasAspectRatioPreset } from "../schema/canvas-aspect-ratio-presets";
8
- import {
9
- clampToolcraftCanvasZoom,
10
- toolcraftCanvasZoomDefault,
11
- toolcraftCanvasZoomStep,
12
- } from "./canvas-zoom";
13
- import {
14
- cloneToolcraftLayers,
15
- cloneToolcraftMediaAssets,
16
- createToolcraftDefaultMediaState,
17
- } from "./media-defaults";
18
- import { getMediaReadyTimelineState } from "./timeline-readiness";
4
+ redoToolcraftHistory,
5
+ undoToolcraftHistory,
6
+ } from "./history-patches";
7
+ import { reduceToolcraftLayersCommand } from "./layers-reducer";
8
+ import { reduceToolcraftMediaCommand } from "./media-reducer";
9
+ import { reduceToolcraftPanelsCommand } from "./panels-reducer";
10
+ import { reduceToolcraftTimelineCommand } from "./timeline-reducer";
19
11
  import type {
20
12
  ToolcraftCommand,
21
- ToolcraftHistoryPatch,
22
- ToolcraftHistoryMode,
23
- ToolcraftLayer,
24
- ToolcraftLayerDraft,
25
- ToolcraftMediaAsset,
26
- ToolcraftMediaTransform,
27
13
  ToolcraftState,
28
- ToolcraftTimelineKeyframe,
29
- ToolcraftTimelineKeyframeGroup,
30
14
  } from "./types";
31
15
 
32
- const minTimelineDurationSeconds = 1;
33
- const maxTimelineDurationSeconds = 60;
34
- const canvasAspectRatioTarget = "canvas.aspectRatio";
35
- const canvasSizeWidthTarget = "canvas.size.width";
36
- const canvasSizeHeightTarget = "canvas.size.height";
37
-
38
- type CanvasAspectRatioValue = {
39
- height: number;
40
- mode: "custom" | "preset";
41
- value: string;
42
- width: number;
43
- };
44
-
45
- function asCanvasSizeDimension(value: unknown): number | null {
46
- const numberValue =
47
- typeof value === "number"
48
- ? value
49
- : typeof value === "string"
50
- ? Number.parseFloat(value)
51
- : Number.NaN;
52
-
53
- if (!Number.isFinite(numberValue)) {
54
- return null;
55
- }
56
-
57
- return Math.max(1, Math.round(numberValue));
58
- }
59
-
60
- function getGreatestCommonDivisor(left: number, right: number): number {
61
- let a = Math.abs(Math.round(left));
62
- let b = Math.abs(Math.round(right));
63
-
64
- while (b !== 0) {
65
- const next = b;
66
- b = a % b;
67
- a = next;
68
- }
69
-
70
- return a || 1;
71
- }
72
-
73
- function getCanvasAspectRatioFromSize(
74
- size: ToolcraftState["canvas"]["size"],
75
- ): CanvasAspectRatioValue {
76
- const divisor = getGreatestCommonDivisor(size.width, size.height);
77
- const width = Math.max(1, Math.round(size.width / divisor));
78
- const height = Math.max(1, Math.round(size.height / divisor));
79
- const value = `${width}:${height}`;
80
-
81
- return {
82
- height,
83
- mode: "custom",
84
- value,
85
- width,
86
- };
87
- }
88
-
89
- function isRecord(value: unknown): value is Record<string, unknown> {
90
- return typeof value === "object" && value !== null && !Array.isArray(value);
91
- }
92
-
93
- function parseCanvasAspectRatioString(value: string): CanvasAspectRatioValue | null {
94
- const match = /^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/u.exec(value);
95
-
96
- if (!match) {
97
- return null;
98
- }
99
-
100
- const width = asCanvasSizeDimension(match[1]);
101
- const height = asCanvasSizeDimension(match[2]);
102
-
103
- if (width === null || height === null) {
104
- return null;
105
- }
106
-
107
- return {
108
- height,
109
- mode: getToolcraftCanvasAspectRatioPreset(`${width}:${height}`)
110
- ? "preset"
111
- : "custom",
112
- value: `${width}:${height}`,
113
- width,
114
- };
115
- }
116
-
117
- function normalizeCanvasAspectRatioValue(
118
- value: unknown,
119
- fallbackSize: ToolcraftState["canvas"]["size"],
120
- ): CanvasAspectRatioValue {
121
- if (typeof value === "string") {
122
- return parseCanvasAspectRatioString(value) ?? getCanvasAspectRatioFromSize(fallbackSize);
123
- }
124
-
125
- if (isRecord(value)) {
126
- const width = asCanvasSizeDimension(value.width);
127
- const height = asCanvasSizeDimension(value.height);
128
-
129
- if (width !== null && height !== null) {
130
- const rawValue = typeof value.value === "string" ? value.value : `${width}:${height}`;
131
- const mode = value.mode === "preset" ? "preset" : "custom";
132
-
133
- return {
134
- height,
135
- mode,
136
- value: mode === "preset" ? rawValue : `${width}:${height}`,
137
- width,
138
- };
139
- }
140
- }
141
-
142
- return getCanvasAspectRatioFromSize(fallbackSize);
143
- }
144
-
145
- function canvasAspectRatioValuesEqual(
146
- first: unknown,
147
- second: CanvasAspectRatioValue,
148
- ): boolean {
149
- if (!isRecord(first)) {
150
- return false;
151
- }
152
-
153
- return (
154
- first.height === second.height &&
155
- first.mode === second.mode &&
156
- first.value === second.value &&
157
- first.width === second.width
158
- );
159
- }
160
-
161
- function applyCanvasAspectRatioToSize({
162
- anchor,
163
- ratio,
164
- size,
165
- value,
166
- }: {
167
- anchor: "height" | "width";
168
- ratio: CanvasAspectRatioValue;
169
- size: ToolcraftState["canvas"]["size"];
170
- value: number;
171
- }): ToolcraftState["canvas"]["size"] {
172
- if (anchor === "width") {
173
- return {
174
- ...size,
175
- height: Math.max(1, Math.round((value * ratio.height) / ratio.width)),
176
- width: value,
177
- };
178
- }
179
-
180
- return {
181
- ...size,
182
- height: value,
183
- width: Math.max(1, Math.round((value * ratio.width) / ratio.height)),
184
- };
185
- }
186
-
187
- function getCanvasAspectRatioPresetSize(
188
- ratio: CanvasAspectRatioValue,
189
- ): ToolcraftState["canvas"]["size"] | null {
190
- if (ratio.mode !== "preset") {
191
- return null;
192
- }
193
-
194
- const preset = getToolcraftCanvasAspectRatioPreset(ratio.value);
195
-
196
- if (!preset) {
197
- return null;
198
- }
199
-
200
- return {
201
- height: preset.height,
202
- unit: "px",
203
- width: preset.width,
204
- };
205
- }
206
-
207
- function getResetCanvasSize(
208
- state: ToolcraftState,
209
- ): ToolcraftState["canvas"]["size"] | null {
210
- const width = asCanvasSizeDimension(state.defaults["canvas.size.width"]);
211
- const height = asCanvasSizeDimension(state.defaults["canvas.size.height"]);
212
-
213
- if (width === null && height === null) {
214
- return null;
215
- }
216
-
217
- return {
218
- ...state.canvas.size,
219
- height: height ?? state.canvas.size.height,
220
- width: width ?? state.canvas.size.width,
221
- };
222
- }
223
-
224
- function getFileDropResetTargets(
225
- state: ToolcraftState,
226
- targets?: ReadonlySet<string>,
227
- ): Set<string> {
228
- const fileDropTargets = new Set<string>();
229
-
230
- for (const section of state.schema.panels.controls?.sections ?? []) {
231
- for (const control of Object.values(section.controls)) {
232
- if (control.type !== "fileDrop" || (targets && !targets.has(control.target))) {
233
- continue;
234
- }
235
-
236
- fileDropTargets.add(control.target);
237
- }
238
- }
239
-
240
- return fileDropTargets;
241
- }
242
-
243
- function getResetMediaPatch(
244
- state: ToolcraftState,
245
- targets?: ReadonlySet<string>,
246
- ): Pick<ToolcraftHistoryPatch, "after" | "before"> | null {
247
- if (state.schema.panels.layers) {
248
- return null;
249
- }
250
-
251
- const fileDropTargets = getFileDropResetTargets(state, targets);
252
-
253
- if (fileDropTargets.size === 0) {
254
- return null;
255
- }
256
-
257
- const defaultMediaState = createToolcraftDefaultMediaState(state.schema);
258
- const defaultTargetMediaAssets = defaultMediaState.mediaAssets.filter((asset) =>
259
- asset.sourceTarget ? fileDropTargets.has(asset.sourceTarget) : false,
260
- );
261
- const defaultTargetLayerIds = new Set(defaultTargetMediaAssets.map((asset) => asset.layerId));
262
- const currentTargetLayerIds = new Set(
263
- state.mediaAssets.flatMap((asset) =>
264
- asset.sourceTarget && fileDropTargets.has(asset.sourceTarget) ? [asset.layerId] : [],
265
- ),
266
- );
267
- const mediaAssets = [
268
- ...state.mediaAssets.filter((asset) => {
269
- if (asset.sourceTarget) {
270
- return !fileDropTargets.has(asset.sourceTarget);
271
- }
272
-
273
- return false;
274
- }),
275
- ...cloneToolcraftMediaAssets(defaultTargetMediaAssets),
276
- ];
277
- const layers = [
278
- ...state.layers.filter(
279
- (layer) => !currentTargetLayerIds.has(layer.id) && !defaultTargetLayerIds.has(layer.id),
280
- ),
281
- ...cloneToolcraftLayers(
282
- defaultMediaState.layers.filter((layer) => defaultTargetLayerIds.has(layer.id)),
283
- ),
284
- ];
285
- const selectedLayerId =
286
- state.selectedLayerId && layers.some((layer) => layer.id === state.selectedLayerId)
287
- ? state.selectedLayerId
288
- : (layers[0]?.id ?? null);
289
-
290
- if (
291
- mediaAssets.length === state.mediaAssets.length &&
292
- mediaAssets.every((asset, index) => asset.id === state.mediaAssets[index]?.id) &&
293
- layers.length === state.layers.length &&
294
- layers.every((layer, index) => layer.id === state.layers[index]?.id) &&
295
- selectedLayerId === state.selectedLayerId
296
- ) {
297
- return null;
298
- }
299
-
300
- const timeline = getMediaReadyTimelineState(state.schema, state.timeline, mediaAssets);
301
- const shouldCommitTimeline = timeline !== state.timeline;
302
-
303
- return {
304
- after: {
305
- layers,
306
- mediaAssets,
307
- selectedLayerId,
308
- ...(shouldCommitTimeline ? { timeline } : {}),
309
- },
310
- before: {
311
- layers: state.layers,
312
- mediaAssets: state.mediaAssets,
313
- selectedLayerId: state.selectedLayerId,
314
- ...(shouldCommitTimeline ? { timeline: state.timeline } : {}),
315
- },
316
- };
317
- }
318
-
319
- function canvasSizesEqual(
320
- first: ToolcraftState["canvas"]["size"],
321
- second: ToolcraftState["canvas"]["size"],
322
- ): boolean {
323
- return (
324
- first.height === second.height &&
325
- first.unit === second.unit &&
326
- first.width === second.width
327
- );
328
- }
329
-
330
- function clampTimelineDuration(value: number): number {
331
- if (!Number.isFinite(value)) {
332
- return minTimelineDurationSeconds;
333
- }
334
-
335
- return Math.max(minTimelineDurationSeconds, Math.min(maxTimelineDurationSeconds, value));
336
- }
337
-
338
- function clampTimelineTime(value: number, durationSeconds: number): number {
339
- if (!Number.isFinite(value)) {
340
- return 0;
341
- }
342
-
343
- return Math.max(0, Math.min(durationSeconds, value));
344
- }
345
-
346
- function formatTimelineSeconds(value: number): string {
347
- return Number.isInteger(value)
348
- ? String(value)
349
- : value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
350
- }
351
-
352
- function getRoundedTimelineKeyframeTime(value: number): number {
353
- return Math.round(value * 100) / 100;
354
- }
355
-
356
- function getTimelineKeyframeId(controlId: string, timeSeconds: number): string {
357
- return `${controlId}::${formatTimelineSeconds(timeSeconds)}`;
358
- }
359
-
360
- function createTimelineControlKeyframe({
361
- controlId,
362
- controlLabel,
363
- state,
364
- timeSeconds,
365
- value,
366
- valueLabel,
367
- }: {
368
- controlId: string;
369
- controlLabel: string;
370
- state: ToolcraftState;
371
- timeSeconds?: number;
372
- value: unknown;
373
- valueLabel: string;
374
- }): ToolcraftTimelineKeyframe {
375
- const resolvedTimeSeconds = getRoundedTimelineKeyframeTime(
376
- clampTimelineTime(
377
- timeSeconds ?? state.timeline.currentTimeSeconds,
378
- state.timeline.durationSeconds,
379
- ),
380
- );
381
-
382
- return {
383
- controlId,
384
- controlLabel,
385
- id: getTimelineKeyframeId(controlId, resolvedTimeSeconds),
386
- timeSeconds: resolvedTimeSeconds,
387
- value,
388
- valueLabel,
389
- };
390
- }
391
-
392
- function upsertTimelineControlKeyframeGroup({
393
- controlId,
394
- controlLabel,
395
- keyframe,
396
- keyframeGroups,
397
- }: {
398
- controlId: string;
399
- controlLabel: string;
400
- keyframe: ToolcraftTimelineKeyframe;
401
- keyframeGroups: readonly ToolcraftTimelineKeyframeGroup[];
402
- }): ToolcraftTimelineKeyframeGroup[] {
403
- const existingGroup = keyframeGroups.find((group) => group.controlId === controlId);
404
- const nextKeyframes = [
405
- ...(existingGroup?.keyframes.filter((item) => item.id !== keyframe.id) ?? []),
406
- keyframe,
407
- ].sort(
408
- (firstKeyframe, secondKeyframe) => firstKeyframe.timeSeconds - secondKeyframe.timeSeconds,
409
- );
410
- const nextGroup: ToolcraftTimelineKeyframeGroup = {
411
- controlId,
412
- keyframes: nextKeyframes,
413
- label: existingGroup?.label ?? controlLabel,
414
- };
415
-
416
- if (!existingGroup) {
417
- return [...keyframeGroups, nextGroup];
418
- }
419
-
420
- return keyframeGroups.map((group) => (group.controlId === controlId ? nextGroup : group));
421
- }
422
-
423
- function commitPatch(
424
- state: ToolcraftState,
425
- patch: ToolcraftHistoryPatch,
426
- values: Record<string, unknown>,
427
- historyOptions?: ToolcraftHistoryOptions,
428
- ): ToolcraftState {
429
- return {
430
- ...state,
431
- history: getNextHistoryState(state, patch, historyOptions),
432
- values,
433
- };
434
- }
435
-
436
- function commitStatePatch(
437
- state: ToolcraftState,
438
- patch: ToolcraftHistoryPatch,
439
- historyOptions?: ToolcraftHistoryOptions,
440
- ): ToolcraftState {
441
- const next = applyHistoryPatch(state, patch.after);
442
-
443
- return {
444
- ...state,
445
- canvas: next.canvas,
446
- history: getNextHistoryState(state, patch, historyOptions),
447
- layers: next.layers,
448
- mediaAssets: next.mediaAssets,
449
- selectedLayerId: next.selectedLayerId,
450
- timeline: next.timeline,
451
- values: next.values,
452
- };
453
- }
454
-
455
- type ToolcraftHistoryOptions = {
456
- group?: string;
457
- mode?: ToolcraftHistoryMode;
458
- };
459
-
460
- function getNextHistoryState(
461
- state: ToolcraftState,
462
- patch: ToolcraftHistoryPatch,
463
- options?: ToolcraftHistoryOptions,
464
- ): ToolcraftState["history"] {
465
- const mode = options?.mode ?? "record";
466
-
467
- if (mode === "skip") {
468
- return state.history;
469
- }
470
-
471
- const group = mode === "merge" ? options?.group : undefined;
472
-
473
- if (group) {
474
- const previousPatch = state.history.undo.at(-1);
475
-
476
- if (previousPatch?.group === group) {
477
- return {
478
- redo: [],
479
- undo: [
480
- ...state.history.undo.slice(0, -1),
481
- {
482
- ...previousPatch,
483
- after: patch.after,
484
- label: patch.label,
485
- },
486
- ],
487
- };
488
- }
489
- }
490
-
491
- return {
492
- redo: [],
493
- undo: [...state.history.undo, group ? { ...patch, group } : patch],
494
- };
495
- }
496
-
497
- function applyValuePatch(
498
- values: Record<string, unknown>,
499
- patch: Record<string, unknown>,
500
- ): Record<string, unknown> {
501
- const nextValues = { ...values };
502
-
503
- for (const [target, value] of Object.entries(patch)) {
504
- if (target in nextValues) {
505
- nextValues[target] = value;
506
- }
507
- }
508
-
509
- return nextValues;
510
- }
511
-
512
- function applyHistoryPatch(
513
- state: ToolcraftState,
514
- patch: Record<string, unknown>,
515
- ): Pick<
516
- ToolcraftState,
517
- "canvas" | "layers" | "mediaAssets" | "selectedLayerId" | "timeline" | "values"
518
- > {
519
- const nextCanvas =
520
- "canvas.size" in patch
521
- ? {
522
- ...state.canvas,
523
- size: patch["canvas.size"] as ToolcraftState["canvas"]["size"],
524
- }
525
- : state.canvas;
526
-
527
- return {
528
- canvas: nextCanvas,
529
- layers: "layers" in patch ? (patch.layers as ToolcraftState["layers"]) : state.layers,
530
- mediaAssets:
531
- "mediaAssets" in patch
532
- ? (patch.mediaAssets as ToolcraftState["mediaAssets"])
533
- : state.mediaAssets,
534
- selectedLayerId:
535
- "selectedLayerId" in patch
536
- ? (patch.selectedLayerId as ToolcraftState["selectedLayerId"])
537
- : state.selectedLayerId,
538
- timeline:
539
- "timeline" in patch ? (patch.timeline as ToolcraftState["timeline"]) : state.timeline,
540
- values: applyValuePatch(state.values, patch),
541
- };
542
- }
543
-
544
- function getNextMediaId(state: ToolcraftState): string {
545
- const existingIds = new Set(state.mediaAssets.map((asset) => asset.id));
546
- let index = state.mediaAssets.length + 1;
547
-
548
- while (existingIds.has(`media-${index}`)) {
549
- index += 1;
550
- }
551
-
552
- return `media-${index}`;
553
- }
554
-
555
- function getNextLayerId(state: ToolcraftState): string {
556
- const existingIds = new Set(state.layers.map((layer) => layer.id));
557
- let index = state.layers.length + 1;
558
-
559
- while (existingIds.has(`layer-${index}`)) {
560
- index += 1;
561
- }
562
-
563
- return `layer-${index}`;
564
- }
565
-
566
- function getSingleLayerImportId(state: ToolcraftState): string | undefined {
567
- return state.schema.panels.layers ? undefined : state.layers.find((layer) => layer.kind !== "group")?.id;
568
- }
569
-
570
- function getSingleMediaImportId(state: ToolcraftState): string | undefined {
571
- return state.schema.panels.layers ? undefined : state.mediaAssets[0]?.id;
572
- }
573
-
574
- function getImportedLayerName(fileName: string): string {
575
- const name = fileName.replace(/\.[^.]+$/, "").trim();
576
-
577
- return name || "Material";
578
- }
579
-
580
- function normalizeMediaRotation(rotationDeg: number): 0 | 90 | 180 | 270 {
581
- const normalized = ((Math.round(rotationDeg / 90) * 90) % 360 + 360) % 360;
582
-
583
- return normalized === 90 || normalized === 180 || normalized === 270 ? normalized : 0;
584
- }
585
-
586
- function compactMediaTransform(
587
- transform: ToolcraftMediaTransform,
588
- ): ToolcraftMediaTransform | undefined {
589
- const normalizedRotation = normalizeMediaRotation(transform.rotationDeg ?? 0);
590
- const nextTransform: ToolcraftMediaTransform = {};
591
-
592
- if (normalizedRotation !== 0) {
593
- nextTransform.rotationDeg = normalizedRotation;
594
- }
595
-
596
- if (transform.flipHorizontal) {
597
- nextTransform.flipHorizontal = true;
598
- }
599
-
600
- if (transform.flipVertical) {
601
- nextTransform.flipVertical = true;
602
- }
603
-
604
- return Object.keys(nextTransform).length > 0 ? nextTransform : undefined;
605
- }
606
-
607
- function getTransformedMediaAsset(
608
- mediaAsset: ToolcraftMediaAsset,
609
- operation: Extract<ToolcraftCommand, { type: "media.transform" }>["operation"],
610
- ): ToolcraftMediaAsset {
611
- const currentTransform = mediaAsset.transform ?? {};
612
- const rotationDeg = normalizeMediaRotation(currentTransform.rotationDeg ?? 0);
613
- const transform: ToolcraftMediaTransform = {
614
- flipHorizontal: currentTransform.flipHorizontal,
615
- flipVertical: currentTransform.flipVertical,
616
- rotationDeg,
617
- };
618
-
619
- switch (operation) {
620
- case "flip-horizontal":
621
- transform.flipHorizontal = !transform.flipHorizontal;
622
- break;
623
- case "flip-vertical":
624
- transform.flipVertical = !transform.flipVertical;
625
- break;
626
- case "rotate-left":
627
- transform.rotationDeg = normalizeMediaRotation(rotationDeg - 90);
628
- break;
629
- case "rotate-right":
630
- transform.rotationDeg = normalizeMediaRotation(rotationDeg + 90);
631
- break;
632
- }
633
-
634
- const compactTransform = compactMediaTransform(transform);
635
-
636
- if (!compactTransform) {
637
- const { transform: _transform, ...rest } = mediaAsset;
638
-
639
- return rest;
640
- }
641
-
642
- return {
643
- ...mediaAsset,
644
- transform: compactTransform,
645
- };
646
- }
647
-
648
- function getNextLayerName(
649
- layers: readonly ToolcraftLayer[],
650
- prefix: "Group" | "Layer",
651
- ): string {
652
- const nextIndex =
653
- layers.reduce((highestIndex, layer) => {
654
- const label = layer.displayName ?? layer.name;
655
- const match = new RegExp(`^${prefix} (\\d+)$`).exec(label);
656
- const currentIndex = Number(match?.[1] ?? 0);
657
-
658
- return Math.max(highestIndex, currentIndex);
659
- }, 0) + 1;
660
-
661
- return `${prefix} ${nextIndex}`;
662
- }
663
-
664
- function createLayer(
665
- state: ToolcraftState,
666
- draft: ToolcraftLayerDraft | undefined,
667
- ): ToolcraftLayer {
668
- const kind = draft?.kind ?? "layer";
669
- const name =
670
- draft?.name ??
671
- draft?.displayName ??
672
- getNextLayerName(state.layers, kind === "group" ? "Group" : "Layer");
673
-
674
- return {
675
- collapsed: kind === "group" ? (draft?.collapsed ?? false) : draft?.collapsed,
676
- displayName: draft?.displayName ?? name,
677
- id: draft?.id ?? getNextLayerId(state),
678
- kind,
679
- name,
680
- parentGroupId: draft?.parentGroupId,
681
- visible: draft?.visible ?? true,
682
- };
683
- }
684
-
685
- function clampInsertIndex(length: number, insertIndex: number | undefined): number {
686
- return Math.max(0, Math.min(length, insertIndex ?? length));
687
- }
688
-
689
- function getLayerBlockIds(
690
- layers: readonly ToolcraftLayer[],
691
- layerId: string,
692
- ): Set<string> {
693
- const blockIds = new Set<string>([layerId]);
694
- let changed = true;
695
-
696
- while (changed) {
697
- changed = false;
698
-
699
- for (const layer of layers) {
700
- if (layer.parentGroupId && blockIds.has(layer.parentGroupId) && !blockIds.has(layer.id)) {
701
- blockIds.add(layer.id);
702
- changed = true;
703
- }
704
- }
705
- }
706
-
707
- return blockIds;
708
- }
709
-
710
- function canMoveLayerToParent(
711
- layers: readonly ToolcraftLayer[],
712
- layerId: string,
713
- parentGroupId: string | null,
714
- ): boolean {
715
- if (!parentGroupId) {
716
- return true;
717
- }
718
-
719
- if (layerId === parentGroupId) {
720
- return false;
721
- }
722
-
723
- const parent = layers.find((layer) => layer.id === parentGroupId);
724
-
725
- if (!parent || parent.kind !== "group") {
726
- return false;
727
- }
728
-
729
- return !getLayerBlockIds(layers, layerId).has(parentGroupId);
730
- }
731
-
732
- function mapTimelineKeyframeGroups(
733
- keyframeGroups: readonly ToolcraftTimelineKeyframeGroup[],
734
- keyframeId: string,
735
- updateKeyframe: (
736
- keyframe: ToolcraftTimelineKeyframeGroup["keyframes"][number],
737
- ) => ToolcraftTimelineKeyframeGroup["keyframes"][number],
738
- ): ToolcraftTimelineKeyframeGroup[] {
739
- return keyframeGroups.map((group) => ({
740
- ...group,
741
- keyframes: group.keyframes.map((keyframe) =>
742
- keyframe.id === keyframeId ? updateKeyframe(keyframe) : keyframe,
743
- ),
744
- }));
745
- }
746
-
747
16
  export function toolcraftReducer(
748
17
  state: ToolcraftState,
749
18
  command: ToolcraftCommand,
750
19
  ): ToolcraftState {
751
20
  switch (command.type) {
752
- case "controls.setValue": {
753
- const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(command.target);
754
-
755
- if (isToolcraftTimelinePanelExtendedTarget(command.target)) {
756
- const extended = command.value === true;
757
-
758
- if (state.panels.timeline.extended === extended) {
759
- return state;
760
- }
761
-
762
- return {
763
- ...state,
764
- panels: {
765
- ...state.panels,
766
- timeline: {
767
- ...state.panels.timeline,
768
- extended,
769
- },
770
- },
771
- };
772
- }
773
-
774
- if (isToolcraftTimelinePanelVisibleTarget(command.target)) {
775
- const hidden = command.value === false;
776
-
777
- if (state.panels.timeline.hidden === hidden) {
778
- return state;
779
- }
780
-
781
- return {
782
- ...state,
783
- panels: {
784
- ...state.panels,
785
- timeline: {
786
- ...state.panels.timeline,
787
- hidden,
788
- },
789
- },
790
- };
791
- }
792
-
793
- if (isToolcraftCanvasAspectRatioTarget(command.target)) {
794
- const ratio = normalizeCanvasAspectRatioValue(command.value, state.canvas.size);
795
- const size =
796
- getCanvasAspectRatioPresetSize(ratio) ??
797
- applyCanvasAspectRatioToSize({
798
- anchor: "width",
799
- ratio,
800
- size: state.canvas.size,
801
- value: state.canvas.size.width,
802
- });
803
-
804
- if (
805
- state.canvas.size.width === size.width &&
806
- state.canvas.size.height === size.height &&
807
- canvasAspectRatioValuesEqual(state.values[command.target], ratio)
808
- ) {
809
- return state;
810
- }
811
-
812
- return commitStatePatch(state, {
813
- after: {
814
- [canvasAspectRatioTarget]: ratio,
815
- "canvas.size": size,
816
- [canvasSizeWidthTarget]: size.width,
817
- [canvasSizeHeightTarget]: size.height,
818
- },
819
- before: {
820
- [canvasAspectRatioTarget]: state.values[command.target],
821
- "canvas.size": state.canvas.size,
822
- [canvasSizeWidthTarget]: state.values[canvasSizeWidthTarget],
823
- [canvasSizeHeightTarget]: state.values[canvasSizeHeightTarget],
824
- },
825
- label: command.label ?? command.target,
826
- }, {
827
- group: command.historyGroup,
828
- mode: command.history,
829
- });
830
- }
831
-
832
- if (canvasSizeDimension) {
833
- const dimensionValue = asCanvasSizeDimension(command.value);
834
-
835
- if (dimensionValue === null) {
836
- return state;
837
- }
838
-
839
- const hasAspectRatioControl =
840
- canvasAspectRatioTarget in state.values ||
841
- canvasAspectRatioTarget in state.defaults;
842
- const size = {
843
- ...state.canvas.size,
844
- [canvasSizeDimension]: dimensionValue,
845
- };
846
- const aspectRatio = getCanvasAspectRatioFromSize(size);
847
- const targetValue = size[canvasSizeDimension];
848
- const otherTarget =
849
- canvasSizeDimension === "width" ? canvasSizeHeightTarget : canvasSizeWidthTarget;
850
- const otherValue = canvasSizeDimension === "width" ? size.height : size.width;
851
-
852
- const sizeUnchanged =
853
- state.canvas.size.width === size.width &&
854
- state.canvas.size.height === size.height &&
855
- state.values[command.target] === targetValue &&
856
- state.values[otherTarget] === otherValue;
857
-
858
- if (sizeUnchanged) {
859
- return state;
860
- }
861
-
862
- return commitStatePatch(state, {
863
- after: {
864
- ...(hasAspectRatioControl
865
- ? { [canvasAspectRatioTarget]: aspectRatio }
866
- : {}),
867
- "canvas.size": size,
868
- [command.target]: targetValue,
869
- [otherTarget]: otherValue,
870
- },
871
- before: {
872
- ...(hasAspectRatioControl
873
- ? { [canvasAspectRatioTarget]: state.values[canvasAspectRatioTarget] }
874
- : {}),
875
- "canvas.size": state.canvas.size,
876
- [command.target]: state.values[command.target],
877
- [otherTarget]: state.values[otherTarget],
878
- },
879
- label: command.label ?? command.target,
880
- }, {
881
- group: command.historyGroup,
882
- mode: command.history,
883
- });
884
- }
885
-
886
- if (Object.is(state.values[command.target], command.value)) {
887
- return state;
888
- }
889
-
890
- return commitPatch(
891
- state,
892
- {
893
- after: { [command.target]: command.value },
894
- before: { [command.target]: state.values[command.target] },
895
- label: command.label ?? command.target,
896
- },
897
- { ...state.values, [command.target]: command.value },
898
- {
899
- group: command.historyGroup,
900
- mode: command.history,
901
- },
902
- );
903
- }
904
-
905
21
  case "controls.apply":
906
- return state;
907
-
908
- case "controls.reset": {
909
- const resetCanvasSize = getResetCanvasSize(state);
910
- const resetMediaPatch = getResetMediaPatch(state);
911
-
912
- if (resetCanvasSize || resetMediaPatch) {
913
- return commitStatePatch(state, {
914
- after: {
915
- ...state.defaults,
916
- ...(resetCanvasSize ? { "canvas.size": resetCanvasSize } : {}),
917
- ...resetMediaPatch?.after,
918
- },
919
- before: {
920
- ...state.values,
921
- ...(resetCanvasSize ? { "canvas.size": state.canvas.size } : {}),
922
- ...resetMediaPatch?.before,
923
- },
924
- label: "Reset controls",
925
- });
926
- }
927
-
928
- return commitPatch(
929
- state,
930
- {
931
- after: { ...state.defaults },
932
- before: { ...state.values },
933
- label: "Reset controls",
934
- },
935
- { ...state.defaults },
936
- );
937
- }
938
-
939
- case "controls.resetTargets": {
940
- const targetSet = new Set(command.targets);
941
- const before: Record<string, unknown> = {};
942
- const after: Record<string, unknown> = {};
943
- const resetMediaPatch = getResetMediaPatch(state, targetSet);
944
-
945
- for (const target of targetSet) {
946
- if (!(target in state.defaults) || Object.is(state.values[target], state.defaults[target])) {
947
- continue;
948
- }
949
-
950
- before[target] = state.values[target];
951
- after[target] = state.defaults[target];
952
- }
953
-
954
- const resetCanvasSize = getResetCanvasSize(state);
955
- const shouldResetCanvasSize =
956
- resetCanvasSize !== null &&
957
- (targetSet.has(canvasSizeWidthTarget) || targetSet.has(canvasSizeHeightTarget)) &&
958
- !canvasSizesEqual(state.canvas.size, resetCanvasSize);
959
-
960
- if (shouldResetCanvasSize) {
961
- before["canvas.size"] = state.canvas.size;
962
- after["canvas.size"] = resetCanvasSize;
963
- }
964
-
965
- if (resetMediaPatch) {
966
- Object.assign(before, resetMediaPatch.before);
967
- Object.assign(after, resetMediaPatch.after);
968
- }
969
-
970
- if (Object.keys(after).length === 0) {
971
- return state;
972
- }
973
-
974
- return commitStatePatch(state, {
975
- after,
976
- before,
977
- label: command.label ?? "Reset section",
978
- });
979
- }
980
-
981
- case "layers.add": {
982
- const layer = createLayer(state, command.layer);
983
- const insertIndex = clampInsertIndex(state.layers.length, command.insertIndex);
984
- const layers = [
985
- ...state.layers.slice(0, insertIndex),
986
- layer,
987
- ...state.layers.slice(insertIndex),
988
- ];
989
-
990
- return commitStatePatch(state, {
991
- after: {
992
- layers,
993
- selectedLayerId: layer.id,
994
- },
995
- before: {
996
- layers: state.layers,
997
- selectedLayerId: state.selectedLayerId,
998
- },
999
- label: layer.kind === "group" ? "Add group" : "Add layer",
1000
- });
1001
- }
1002
-
1003
- case "layers.delete": {
1004
- if (!state.layers.some((layer) => layer.id === command.layerId)) {
1005
- return state;
1006
- }
1007
-
1008
- const deletedLayerIds = getLayerBlockIds(state.layers, command.layerId);
1009
- const layers = state.layers.filter((layer) => !deletedLayerIds.has(layer.id));
1010
- const mediaAssets = state.mediaAssets.filter((asset) => !deletedLayerIds.has(asset.layerId));
1011
- const selectedLayerId = deletedLayerIds.has(state.selectedLayerId ?? "")
1012
- ? (layers[0]?.id ?? null)
1013
- : state.selectedLayerId;
1014
-
1015
- return commitStatePatch(state, {
1016
- after: {
1017
- layers,
1018
- mediaAssets,
1019
- selectedLayerId,
1020
- },
1021
- before: {
1022
- layers: state.layers,
1023
- mediaAssets: state.mediaAssets,
1024
- selectedLayerId: state.selectedLayerId,
1025
- },
1026
- label: "Delete layer",
1027
- });
1028
- }
1029
-
1030
- case "layers.moveToGroup": {
1031
- const movedRootLayerIds = new Set(
1032
- command.layerIds.filter((layerId) =>
1033
- canMoveLayerToParent(state.layers, layerId, command.parentGroupId),
1034
- ),
1035
- );
1036
-
1037
- if (movedRootLayerIds.size === 0) {
1038
- return state;
1039
- }
1040
-
1041
- const nextParentGroupId = command.parentGroupId ?? undefined;
1042
- const movedBlockIds = new Set<string>();
1043
-
1044
- for (const layerId of movedRootLayerIds) {
1045
- getLayerBlockIds(state.layers, layerId).forEach((blockLayerId) => {
1046
- movedBlockIds.add(blockLayerId);
1047
- });
1048
- }
1049
-
1050
- const movingBlock = state.layers.filter((layer) => movedBlockIds.has(layer.id));
1051
- const updatedMovingBlock = movingBlock.map((layer) =>
1052
- movedRootLayerIds.has(layer.id) ? { ...layer, parentGroupId: nextParentGroupId } : layer,
1053
- );
1054
- const remainingLayers = state.layers.filter((layer) => !movedBlockIds.has(layer.id));
1055
- const targetGroupIndex = command.parentGroupId
1056
- ? remainingLayers.findIndex((layer) => layer.id === command.parentGroupId)
1057
- : -1;
1058
- const movedLayers = command.parentGroupId
1059
- ? targetGroupIndex >= 0
1060
- ? [
1061
- ...remainingLayers.slice(0, targetGroupIndex + 1),
1062
- ...updatedMovingBlock,
1063
- ...remainingLayers.slice(targetGroupIndex + 1),
1064
- ]
1065
- : state.layers
1066
- : state.layers.map((layer) =>
1067
- movedRootLayerIds.has(layer.id) ? { ...layer, parentGroupId: nextParentGroupId } : layer,
1068
- );
1069
- const layers = command.parentGroupId
1070
- ? movedLayers.map((layer) =>
1071
- layer.id === command.parentGroupId && layer.kind === "group" && layer.collapsed
1072
- ? { ...layer, collapsed: false }
1073
- : layer,
1074
- )
1075
- : movedLayers;
1076
-
1077
- if (layers === state.layers) {
1078
- return state;
1079
- }
1080
-
1081
- if (
1082
- layers.every(
1083
- (layer, index) =>
1084
- layer.id === state.layers[index]?.id &&
1085
- layer.parentGroupId === state.layers[index]?.parentGroupId &&
1086
- layer.collapsed === state.layers[index]?.collapsed,
1087
- )
1088
- ) {
1089
- return state;
1090
- }
1091
-
1092
- return commitStatePatch(state, {
1093
- after: { layers },
1094
- before: { layers: state.layers },
1095
- label: command.parentGroupId ? "Move layers to group" : "Move layers to root",
1096
- });
1097
- }
1098
-
22
+ case "controls.reset":
23
+ case "controls.resetTargets":
24
+ case "controls.setValue":
25
+ return reduceToolcraftControlsCommand(state, command);
26
+
27
+ case "layers.add":
28
+ case "layers.delete":
29
+ case "layers.moveToGroup":
30
+ case "layers.rename":
31
+ case "layers.reorder":
1099
32
  case "layers.select":
1100
- if (!state.layers.some((layer) => layer.id === command.layerId)) {
1101
- return state;
1102
- }
1103
-
1104
- return {
1105
- ...state,
1106
- selectedLayerId: command.layerId,
1107
- };
1108
-
1109
- case "layers.rename": {
1110
- const name = command.name.trim();
1111
-
1112
- if (!name || !state.layers.some((layer) => layer.id === command.layerId)) {
1113
- return state;
1114
- }
1115
-
1116
- const layers = state.layers.map((layer) =>
1117
- layer.id === command.layerId ? { ...layer, displayName: name } : layer,
1118
- );
1119
-
1120
- return commitStatePatch(state, {
1121
- after: { layers },
1122
- before: { layers: state.layers },
1123
- label: "Rename layer",
1124
- });
1125
- }
1126
-
1127
- case "layers.toggleCollapsed": {
1128
- const targetLayer = state.layers.find((layer) => layer.id === command.layerId);
1129
-
1130
- if (!targetLayer || targetLayer.kind !== "group") {
1131
- return state;
1132
- }
1133
-
1134
- const layers = state.layers.map((layer) =>
1135
- layer.id === command.layerId ? { ...layer, collapsed: !layer.collapsed } : layer,
1136
- );
1137
-
1138
- return commitStatePatch(state, {
1139
- after: { layers },
1140
- before: { layers: state.layers },
1141
- label: "Toggle group",
1142
- });
1143
- }
1144
-
1145
- case "layers.toggleVisibility": {
1146
- if (!state.layers.some((layer) => layer.id === command.layerId)) {
1147
- return state;
1148
- }
1149
-
1150
- const layers = state.layers.map((layer) =>
1151
- layer.id === command.layerId ? { ...layer, visible: !layer.visible } : layer,
1152
- );
1153
-
1154
- return commitStatePatch(state, {
1155
- after: { layers },
1156
- before: { layers: state.layers },
1157
- label: "Toggle layer visibility",
1158
- });
1159
- }
1160
-
1161
- case "layers.reorder": {
1162
- const nextLayerIds = new Set(command.layers.map((layer) => layer.id));
1163
-
1164
- if (nextLayerIds.size !== command.layers.length || nextLayerIds.size !== state.layers.length) {
1165
- return state;
1166
- }
1167
-
1168
- if (!state.layers.every((layer) => nextLayerIds.has(layer.id))) {
1169
- return state;
1170
- }
1171
-
1172
- return commitStatePatch(state, {
1173
- after: {
1174
- layers: command.layers,
1175
- selectedLayerId: command.selectedLayerId ?? state.selectedLayerId,
1176
- },
1177
- before: {
1178
- layers: state.layers,
1179
- selectedLayerId: state.selectedLayerId,
1180
- },
1181
- label: "Reorder layers",
1182
- });
1183
- }
1184
-
1185
- case "canvas.setOffset":
1186
- return {
1187
- ...state,
1188
- canvas: {
1189
- ...state.canvas,
1190
- offset: command.offset,
1191
- },
1192
- };
33
+ case "layers.toggleCollapsed":
34
+ case "layers.toggleVisibility":
35
+ return reduceToolcraftLayersCommand(state, command);
1193
36
 
37
+ case "canvas.center":
1194
38
  case "canvas.panBy":
1195
- return {
1196
- ...state,
1197
- canvas: {
1198
- ...state.canvas,
1199
- offset: {
1200
- x: state.canvas.offset.x + command.delta.x,
1201
- y: state.canvas.offset.y + command.delta.y,
1202
- },
1203
- },
1204
- };
1205
-
39
+ case "canvas.setOffset":
1206
40
  case "canvas.setSize":
1207
- return {
1208
- ...state,
1209
- canvas: {
1210
- ...state.canvas,
1211
- size: command.size,
1212
- },
1213
- history: {
1214
- redo: [],
1215
- undo: [
1216
- ...state.history.undo,
1217
- {
1218
- after: { "canvas.size": command.size },
1219
- before: { "canvas.size": state.canvas.size },
1220
- label: "Resize canvas",
1221
- },
1222
- ],
1223
- },
1224
- };
1225
-
1226
- case "canvas.center":
1227
- return {
1228
- ...state,
1229
- canvas: {
1230
- ...state.canvas,
1231
- offset: { x: 0, y: 0 },
1232
- },
1233
- };
1234
-
41
+ case "canvas.setViewport":
1235
42
  case "canvas.zoomIn":
1236
- return {
1237
- ...state,
1238
- canvas: {
1239
- ...state.canvas,
1240
- zoom: clampToolcraftCanvasZoom(state.canvas.zoom + toolcraftCanvasZoomStep),
1241
- },
1242
- };
1243
-
1244
43
  case "canvas.zoomOut":
1245
- return {
1246
- ...state,
1247
- canvas: {
1248
- ...state.canvas,
1249
- zoom: clampToolcraftCanvasZoom(state.canvas.zoom - toolcraftCanvasZoomStep),
1250
- },
1251
- };
1252
-
1253
44
  case "canvas.zoomReset":
1254
- return {
1255
- ...state,
1256
- canvas: {
1257
- ...state.canvas,
1258
- zoom: toolcraftCanvasZoomDefault,
1259
- },
1260
- };
1261
-
1262
- case "canvas.setViewport":
1263
- return {
1264
- ...state,
1265
- canvas: {
1266
- ...state.canvas,
1267
- offset: command.offset,
1268
- zoom: clampToolcraftCanvasZoom(command.zoom),
1269
- },
1270
- };
1271
-
1272
- case "panels.setOffset":
1273
- return {
1274
- ...state,
1275
- panels: {
1276
- ...state.panels,
1277
- [command.panelId]: {
1278
- ...state.panels[command.panelId],
1279
- offset: command.offset,
1280
- },
1281
- },
1282
- };
1283
-
1284
- case "panels.setHidden":
1285
- if (state.panels[command.panelId].hidden === command.hidden) {
1286
- return state;
1287
- }
1288
-
1289
- return {
1290
- ...state,
1291
- panels: {
1292
- ...state.panels,
1293
- [command.panelId]: {
1294
- ...state.panels[command.panelId],
1295
- hidden: command.hidden,
1296
- },
1297
- },
1298
- };
45
+ return reduceToolcraftCanvasCommand(state, command);
1299
46
 
1300
47
  case "panels.resetOffset":
1301
- return {
1302
- ...state,
1303
- panels: {
1304
- ...state.panels,
1305
- [command.panelId]: {
1306
- ...state.panels[command.panelId],
1307
- offset: { x: 0, y: 0 },
1308
- },
1309
- },
1310
- };
1311
-
1312
- case "media.import": {
1313
- const shouldReplaceSingleLayerMedia =
1314
- !state.schema.panels.layers && command.replaceExisting !== false;
1315
- const assetKind = command.asset.assetKind ?? "image";
1316
- const sourceTarget = command.asset.sourceTarget;
1317
- const existingSourceMediaAsset =
1318
- shouldReplaceSingleLayerMedia && sourceTarget
1319
- ? state.mediaAssets.find((asset) => asset.sourceTarget === sourceTarget)
1320
- : undefined;
1321
- const shouldResizeCanvas =
1322
- state.schema.canvas.sizing.mode === "intrinsic-media" &&
1323
- assetKind === "image" &&
1324
- command.asset.size !== undefined;
1325
- const layerId =
1326
- command.asset.layerId ??
1327
- existingSourceMediaAsset?.layerId ??
1328
- (shouldReplaceSingleLayerMedia && !sourceTarget
1329
- ? getSingleLayerImportId(state)
1330
- : undefined) ??
1331
- getNextLayerId(state);
1332
- const mediaId =
1333
- command.asset.id ??
1334
- existingSourceMediaAsset?.id ??
1335
- (shouldReplaceSingleLayerMedia && !sourceTarget
1336
- ? getSingleMediaImportId(state)
1337
- : undefined) ??
1338
- getNextMediaId(state);
1339
- const layer = {
1340
- displayName: command.asset.layerName ?? getImportedLayerName(command.asset.fileName),
1341
- id: layerId,
1342
- kind: "layer" as const,
1343
- name: command.asset.layerName ?? getImportedLayerName(command.asset.fileName),
1344
- visible: true,
1345
- };
1346
- const mediaAsset = {
1347
- ...(command.asset.assetKind ? { assetKind: command.asset.assetKind } : {}),
1348
- dataUrl: command.asset.dataUrl,
1349
- fileName: command.asset.fileName,
1350
- id: mediaId,
1351
- layerId,
1352
- mimeType: command.asset.mimeType,
1353
- position: shouldResizeCanvas ? { x: 0, y: 0 } : command.asset.position,
1354
- ...(command.asset.size ? { size: command.asset.size } : {}),
1355
- ...(sourceTarget ? { sourceTarget } : {}),
1356
- };
1357
- const layers = shouldReplaceSingleLayerMedia
1358
- ? sourceTarget
1359
- ? state.layers.some((entry) => entry.id === layerId)
1360
- ? state.layers.map((entry) => (entry.id === layerId ? layer : entry))
1361
- : [...state.layers, layer]
1362
- : [layer]
1363
- : [...state.layers, layer];
1364
- const mediaAssets = shouldReplaceSingleLayerMedia
1365
- ? sourceTarget
1366
- ? existingSourceMediaAsset
1367
- ? state.mediaAssets.map((asset) =>
1368
- asset.id === existingSourceMediaAsset.id ? mediaAsset : asset,
1369
- )
1370
- : [...state.mediaAssets, mediaAsset]
1371
- : [mediaAsset]
1372
- : [...state.mediaAssets, mediaAsset];
1373
- const after = {
1374
- ...(shouldResizeCanvas ? { "canvas.size": command.asset.size } : {}),
1375
- layers,
1376
- mediaAssets,
1377
- selectedLayerId: layerId,
1378
- };
1379
- const before = {
1380
- ...(shouldResizeCanvas ? { "canvas.size": state.canvas.size } : {}),
1381
- layers: state.layers,
1382
- mediaAssets: state.mediaAssets,
1383
- selectedLayerId: state.selectedLayerId,
1384
- };
1385
-
1386
- return commitStatePatch(state, {
1387
- after,
1388
- before,
1389
- label: "Import media",
1390
- });
1391
- }
1392
-
1393
- case "media.delete": {
1394
- if (!state.mediaAssets.some((asset) => asset.id === command.mediaId)) {
1395
- return state;
1396
- }
1397
-
1398
- const mediaAssets = state.mediaAssets.filter((asset) => asset.id !== command.mediaId);
1399
- const timeline = getMediaReadyTimelineState(state.schema, state.timeline, mediaAssets);
1400
- const shouldCommitTimeline = timeline !== state.timeline;
1401
-
1402
- return commitStatePatch(state, {
1403
- after: {
1404
- mediaAssets,
1405
- ...(shouldCommitTimeline ? { timeline } : {}),
1406
- },
1407
- before: {
1408
- mediaAssets: state.mediaAssets,
1409
- ...(shouldCommitTimeline ? { timeline: state.timeline } : {}),
1410
- },
1411
- label: "Delete media",
1412
- });
1413
- }
1414
-
1415
- case "media.reorder": {
1416
- if (state.mediaAssets.length < 2 || command.mediaIds.length === 0) {
1417
- return state;
1418
- }
1419
-
1420
- const mediaById = new Map(state.mediaAssets.map((asset) => [asset.id, asset]));
1421
- const seenIds = new Set<string>();
1422
- const reorderedMediaAssets = command.mediaIds.flatMap((mediaId) => {
1423
- const mediaAsset = mediaById.get(mediaId);
1424
-
1425
- if (!mediaAsset || seenIds.has(mediaId)) {
1426
- return [];
1427
- }
1428
-
1429
- seenIds.add(mediaId);
1430
- return [mediaAsset];
1431
- });
1432
-
1433
- if (reorderedMediaAssets.length === 0) {
1434
- return state;
1435
- }
1436
-
1437
- for (const mediaAsset of state.mediaAssets) {
1438
- if (!seenIds.has(mediaAsset.id)) {
1439
- reorderedMediaAssets.push(mediaAsset);
1440
- }
1441
- }
1442
-
1443
- if (
1444
- reorderedMediaAssets.length === state.mediaAssets.length &&
1445
- reorderedMediaAssets.every((asset, index) => asset.id === state.mediaAssets[index]?.id)
1446
- ) {
1447
- return state;
1448
- }
1449
-
1450
- return commitStatePatch(state, {
1451
- after: { mediaAssets: reorderedMediaAssets },
1452
- before: { mediaAssets: state.mediaAssets },
1453
- label: "Reorder media",
1454
- });
1455
- }
1456
-
1457
- case "media.transform": {
1458
- const targetMediaAsset = state.mediaAssets.find((asset) => asset.id === command.mediaId);
1459
-
1460
- if (!targetMediaAsset || (targetMediaAsset.assetKind ?? "image") !== "image") {
1461
- return state;
1462
- }
1463
-
1464
- const mediaAssets = state.mediaAssets.map((asset) =>
1465
- asset.id === targetMediaAsset.id
1466
- ? getTransformedMediaAsset(asset, command.operation)
1467
- : asset,
1468
- );
1469
-
1470
- return commitStatePatch(state, {
1471
- after: { mediaAssets },
1472
- before: { mediaAssets: state.mediaAssets },
1473
- label: "Transform media",
1474
- });
1475
- }
1476
-
1477
- case "timeline.setCurrentTime": {
1478
- return {
1479
- ...state,
1480
- timeline: {
1481
- ...state.timeline,
1482
- currentTimeSeconds: clampTimelineTime(
1483
- command.currentTimeSeconds,
1484
- state.timeline.durationSeconds,
1485
- ),
1486
- },
1487
- };
1488
- }
1489
-
1490
- case "timeline.setDuration": {
1491
- const durationSeconds = clampTimelineDuration(command.durationSeconds);
1492
- const timeline = {
1493
- ...state.timeline,
1494
- currentTimeSeconds: clampTimelineTime(state.timeline.currentTimeSeconds, durationSeconds),
1495
- durationSeconds,
1496
- };
1497
-
1498
- return commitStatePatch(state, {
1499
- after: { timeline },
1500
- before: { timeline: state.timeline },
1501
- label: "Set timeline duration",
1502
- });
1503
- }
1504
-
1505
- case "timeline.setExpanded": {
1506
- if (state.timeline.expanded === command.expanded) {
1507
- return state;
1508
- }
1509
-
1510
- return {
1511
- ...state,
1512
- timeline: {
1513
- ...state.timeline,
1514
- expanded: command.expanded,
1515
- },
1516
- };
1517
- }
1518
-
1519
- case "timeline.toggleExpanded": {
1520
- return {
1521
- ...state,
1522
- timeline: {
1523
- ...state.timeline,
1524
- expanded: !state.timeline.expanded,
1525
- },
1526
- };
1527
- }
1528
-
1529
- case "timeline.setPlaying": {
1530
- return {
1531
- ...state,
1532
- timeline: {
1533
- ...state.timeline,
1534
- isPlaying: command.isPlaying,
1535
- },
1536
- };
1537
- }
1538
-
1539
- case "timeline.togglePlayback": {
1540
- const shouldRestartPlayback =
1541
- !state.timeline.isPlaying &&
1542
- state.timeline.currentTimeSeconds >= state.timeline.durationSeconds;
1543
-
1544
- return {
1545
- ...state,
1546
- timeline: {
1547
- ...state.timeline,
1548
- currentTimeSeconds: shouldRestartPlayback ? 0 : state.timeline.currentTimeSeconds,
1549
- isPlaying: !state.timeline.isPlaying,
1550
- },
1551
- };
1552
- }
1553
-
1554
- case "timeline.toggleLoop": {
1555
- return {
1556
- ...state,
1557
- timeline: {
1558
- ...state.timeline,
1559
- isLooping: !state.timeline.isLooping,
1560
- },
1561
- };
1562
- }
1563
-
1564
- case "timeline.selectKeyframe": {
1565
- return {
1566
- ...state,
1567
- timeline: {
1568
- ...state.timeline,
1569
- selectedKeyframeId: command.keyframeId,
1570
- },
1571
- };
1572
- }
1573
-
1574
- case "timeline.deleteKeyframe": {
1575
- if (
1576
- !state.timeline.keyframeGroups.some((group) =>
1577
- group.keyframes.some((keyframe) => keyframe.id === command.keyframeId),
1578
- )
1579
- ) {
1580
- return state;
1581
- }
1582
-
1583
- const timeline = {
1584
- ...state.timeline,
1585
- keyframeGroups: state.timeline.keyframeGroups
1586
- .map((group) => ({
1587
- ...group,
1588
- keyframes: group.keyframes.filter((keyframe) => keyframe.id !== command.keyframeId),
1589
- }))
1590
- .filter((group) => group.keyframes.length > 0),
1591
- selectedKeyframeId: null,
1592
- };
1593
-
1594
- return commitStatePatch(state, {
1595
- after: { timeline },
1596
- before: { timeline: state.timeline },
1597
- label: "Delete keyframe",
1598
- });
1599
- }
1600
-
1601
- case "timeline.deleteControlKeyframes": {
1602
- if (!state.timeline.keyframeGroups.some((group) => group.controlId === command.controlId)) {
1603
- return state;
1604
- }
1605
-
1606
- const timeline = {
1607
- ...state.timeline,
1608
- keyframeGroups: state.timeline.keyframeGroups.filter(
1609
- (group) => group.controlId !== command.controlId,
1610
- ),
1611
- selectedKeyframeId: null,
1612
- };
1613
-
1614
- return commitStatePatch(state, {
1615
- after: { timeline },
1616
- before: { timeline: state.timeline },
1617
- label: "Delete control keyframes",
1618
- });
1619
- }
1620
-
1621
- case "timeline.toggleControlKeyframes": {
1622
- const existingGroup = state.timeline.keyframeGroups.find(
1623
- (group) => group.controlId === command.controlId,
1624
- );
1625
-
1626
- if (existingGroup) {
1627
- const timeline = {
1628
- ...state.timeline,
1629
- expanded: true,
1630
- keyframeGroups: state.timeline.keyframeGroups.filter(
1631
- (group) => group.controlId !== command.controlId,
1632
- ),
1633
- selectedKeyframeId: null,
1634
- };
1635
-
1636
- return commitStatePatch(state, {
1637
- after: { timeline },
1638
- before: { timeline: state.timeline },
1639
- label: "Delete control keyframes",
1640
- });
1641
- }
1642
-
1643
- const keyframe = createTimelineControlKeyframe({
1644
- controlId: command.controlId,
1645
- controlLabel: command.controlLabel,
1646
- state,
1647
- timeSeconds: command.timeSeconds,
1648
- value: command.value,
1649
- valueLabel: command.valueLabel,
1650
- });
1651
- const timeline = {
1652
- ...state.timeline,
1653
- expanded: true,
1654
- keyframeGroups: upsertTimelineControlKeyframeGroup({
1655
- controlId: command.controlId,
1656
- controlLabel: command.controlLabel,
1657
- keyframe,
1658
- keyframeGroups: state.timeline.keyframeGroups,
1659
- }),
1660
- selectedKeyframeId: keyframe.id,
1661
- };
1662
-
1663
- return commitStatePatch(state, {
1664
- after: { timeline },
1665
- before: { timeline: state.timeline },
1666
- label: "Add control keyframe",
1667
- });
1668
- }
1669
-
1670
- case "timeline.upsertControlKeyframe": {
1671
- const keyframe = createTimelineControlKeyframe({
1672
- controlId: command.controlId,
1673
- controlLabel: command.controlLabel,
1674
- state,
1675
- timeSeconds: command.timeSeconds,
1676
- value: command.value,
1677
- valueLabel: command.valueLabel,
1678
- });
1679
- const timeline = {
1680
- ...state.timeline,
1681
- expanded: true,
1682
- keyframeGroups: upsertTimelineControlKeyframeGroup({
1683
- controlId: command.controlId,
1684
- controlLabel: command.controlLabel,
1685
- keyframe,
1686
- keyframeGroups: state.timeline.keyframeGroups,
1687
- }),
1688
- selectedKeyframeId: keyframe.id,
1689
- };
1690
-
1691
- return commitStatePatch(state, {
1692
- after: { timeline },
1693
- before: { timeline: state.timeline },
1694
- label: "Set control keyframe",
1695
- });
1696
- }
1697
-
1698
- case "timeline.moveKeyframe": {
1699
- const targetKeyframe = state.timeline.keyframeGroups
1700
- .flatMap((group) => group.keyframes)
1701
- .find((keyframe) => keyframe.id === command.keyframeId);
1702
-
1703
- if (!targetKeyframe) {
1704
- return state;
1705
- }
1706
-
1707
- const timeSeconds = getRoundedTimelineKeyframeTime(
1708
- clampTimelineTime(command.timeSeconds, state.timeline.durationSeconds),
1709
- );
1710
- const nextKeyframeId = getTimelineKeyframeId(targetKeyframe.controlId, timeSeconds);
1711
- const timeline = {
1712
- ...state.timeline,
1713
- keyframeGroups: mapTimelineKeyframeGroups(
1714
- state.timeline.keyframeGroups,
1715
- command.keyframeId,
1716
- (keyframe) => ({
1717
- ...keyframe,
1718
- id: nextKeyframeId,
1719
- timeSeconds,
1720
- }),
1721
- ),
1722
- selectedKeyframeId: nextKeyframeId,
1723
- };
1724
-
1725
- return commitStatePatch(state, {
1726
- after: { timeline },
1727
- before: { timeline: state.timeline },
1728
- label: "Move keyframe",
1729
- });
1730
- }
1731
-
1732
- case "timeline.changeKeyframeEasing": {
1733
- if (
1734
- !state.timeline.keyframeGroups.some((group) =>
1735
- group.keyframes.some((keyframe) => keyframe.id === command.keyframeId),
1736
- )
1737
- ) {
1738
- return state;
1739
- }
1740
-
1741
- const timeline = {
1742
- ...state.timeline,
1743
- keyframeGroups: mapTimelineKeyframeGroups(
1744
- state.timeline.keyframeGroups,
1745
- command.keyframeId,
1746
- (keyframe) => ({
1747
- ...keyframe,
1748
- easing: command.easing,
1749
- }),
1750
- ),
1751
- };
1752
-
1753
- return commitStatePatch(state, {
1754
- after: { timeline },
1755
- before: { timeline: state.timeline },
1756
- label: "Change keyframe easing",
1757
- });
1758
- }
1759
-
1760
- case "history.undo": {
1761
- const patch = state.history.undo.at(-1);
1762
-
1763
- if (!patch) {
1764
- return state;
1765
- }
1766
-
1767
- const next = applyHistoryPatch(state, patch.before);
1768
-
1769
- return {
1770
- ...state,
1771
- canvas: next.canvas,
1772
- history: {
1773
- redo: [...state.history.redo, patch],
1774
- undo: state.history.undo.slice(0, -1),
1775
- },
1776
- layers: next.layers,
1777
- mediaAssets: next.mediaAssets,
1778
- selectedLayerId: next.selectedLayerId,
1779
- timeline: next.timeline,
1780
- values: next.values,
1781
- };
1782
- }
1783
-
1784
- case "history.redo": {
1785
- const patch = state.history.redo.at(-1);
1786
-
1787
- if (!patch) {
1788
- return state;
1789
- }
1790
-
1791
- const next = applyHistoryPatch(state, patch.after);
1792
-
1793
- return {
1794
- ...state,
1795
- canvas: next.canvas,
1796
- history: {
1797
- redo: state.history.redo.slice(0, -1),
1798
- undo: [...state.history.undo, patch],
1799
- },
1800
- layers: next.layers,
1801
- mediaAssets: next.mediaAssets,
1802
- selectedLayerId: next.selectedLayerId,
1803
- timeline: next.timeline,
1804
- values: next.values,
1805
- };
1806
- }
48
+ case "panels.setHidden":
49
+ case "panels.setOffset":
50
+ return reduceToolcraftPanelsCommand(state, command);
51
+
52
+ case "media.delete":
53
+ case "media.import":
54
+ case "media.reorder":
55
+ case "media.transform":
56
+ return reduceToolcraftMediaCommand(state, command);
57
+
58
+ case "timeline.changeKeyframeEasing":
59
+ case "timeline.deleteControlKeyframes":
60
+ case "timeline.deleteKeyframe":
61
+ case "timeline.moveKeyframe":
62
+ case "timeline.selectKeyframe":
63
+ case "timeline.setCurrentTime":
64
+ case "timeline.setDuration":
65
+ case "timeline.setExpanded":
66
+ case "timeline.setPlaying":
67
+ case "timeline.toggleControlKeyframes":
68
+ case "timeline.toggleExpanded":
69
+ case "timeline.toggleLoop":
70
+ case "timeline.togglePlayback":
71
+ case "timeline.upsertControlKeyframe":
72
+ return reduceToolcraftTimelineCommand(state, command);
73
+
74
+ case "history.undo":
75
+ return undoToolcraftHistory(state);
76
+
77
+ case "history.redo":
78
+ return redoToolcraftHistory(state);
1807
79
  }
1808
80
  }