@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,3220 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { ArrowCounterClockwiseIcon, DiamondIcon } from "@phosphor-icons/react";
5
- import {
6
- Actions,
7
- AnchorGrid,
8
- Button,
9
- ChannelMixer,
10
- Checkbox,
11
- CodeTextarea,
12
- CollectionActions,
13
- Color,
14
- ColorOpacity,
15
- ControlInlineGroup,
16
- ControlFieldLabelActionProvider,
17
- ControlFieldLabelHelpProvider,
18
- Curves,
19
- FileDrop,
20
- FontPicker,
21
- Gradient,
22
- ImagePicker,
23
- Palette,
24
- Panel,
25
- PanelActions,
26
- PanelSection,
27
- type PanelActionObjectOption,
28
- RangeInput,
29
- RangeSlider,
30
- Segmented,
31
- Select,
32
- Slider,
33
- Switch,
34
- TextInput,
35
- Tooltip,
36
- TooltipContent,
37
- TooltipTrigger,
38
- Vector,
39
- type ChannelMixerValues,
40
- type ControlChangeMeta,
41
- type ColorControlInput,
42
- type ColorControlInputPair,
43
- type ColorOpacityValue,
44
- type CurveInterpolation,
45
- type FontPickerValue,
46
- type FileDropPreview,
47
- type GradientStop,
48
- type GradientType,
49
- type ImagePickerItem,
50
- type VectorPadCoordinateMode,
51
- type VectorPadVariant,
52
- } from "@repo/ui";
53
-
54
- import type {
55
- ToolcraftActionCommand,
56
- ToolcraftActionSchema,
57
- ToolcraftControlConditionSchema,
58
- ToolcraftControlLayoutGroupSchema,
59
- ToolcraftControlSectionSchema,
60
- ToolcraftControlSchema,
61
- ResolvedToolcraftAppSchema,
62
- } from "../schema/types";
63
- import {
64
- getToolcraftCanvasAspectRatioPreset,
65
- toolcraftCanvasAspectRatioPresets,
66
- } from "../schema/canvas-aspect-ratio-presets";
67
- import { getToolcraftControlKeyframeCapability } from "../schema/keyframe-capability";
68
- import {
69
- getToolcraftCanvasSizeTargetDimension,
70
- isToolcraftRuntimeOwnedTarget,
71
- isToolcraftTimelinePanelExtendedTarget,
72
- isToolcraftTimelinePanelVisibleTarget,
73
- } from "../schema/runtime-targets";
74
- import type {
75
- ToolcraftCommand,
76
- ToolcraftPanelState,
77
- ToolcraftState,
78
- } from "../state/types";
79
- import {
80
- isToolcraftImageFile,
81
- readImportedFile,
82
- readImportedImageFile,
83
- } from "./media-file";
84
- import { PanelContainer } from "./panel-host";
85
- import type { PanelPlacement, PanelStateChange } from "./panel-host-types";
86
- import type { ToolcraftControlRendererMap } from "./control-renderers";
87
- import {
88
- downloadToolcraftSettings,
89
- importToolcraftSettings,
90
- } from "./settings-transfer";
91
- import {
92
- readToolcraftLocalStorageValue,
93
- removeToolcraftLocalStorageValue,
94
- } from "./storage-key-migration";
95
- import { useToolcraft } from "./use-toolcraft";
96
-
97
- export type ControlsPanelProps = {
98
- className?: string;
99
- controlRenderers?: ToolcraftControlRendererMap;
100
- framed?: boolean;
101
- onPanelAction?: ToolcraftPanelActionHandler;
102
- onPanelStateChange?: PanelStateChange;
103
- panelPlacement?: PanelPlacement;
104
- panelState?: ToolcraftPanelState;
105
- };
106
-
107
- export type ToolcraftPanelActionContext = {
108
- action: ToolcraftActionSchema;
109
- dispatch: React.Dispatch<ToolcraftCommand>;
110
- reportProgress: (progress: number) => void;
111
- state: ToolcraftState;
112
- };
113
-
114
- export type ToolcraftPanelActionHandler = (
115
- context: ToolcraftPanelActionContext,
116
- ) => PromiseLike<unknown> | void;
117
-
118
- type AnyRecord = Record<string, unknown>;
119
- type ControlEntry = [string, ToolcraftControlSchema];
120
- type ControlRenderGroup =
121
- | { entries: readonly ControlEntry[]; kind: "colorGroup" }
122
- | { entry: ControlEntry; kind: "control" };
123
- type RenderedControlRenderGroup = {
124
- ids: readonly string[];
125
- node: React.ReactNode;
126
- };
127
- type FooterActionProgressEntry = {
128
- id: number;
129
- progress: number | null;
130
- };
131
- type CanvasAspectRatioValue = {
132
- height: number;
133
- mode: "custom" | "preset";
134
- value: string;
135
- width: number;
136
- };
137
-
138
- const hiddenDiscreteMarkerCount = 2;
139
- const controlsPanelSectionCollapseStorageVersion = 1;
140
- const canvasAspectRatioOptions = [
141
- ...toolcraftCanvasAspectRatioPresets.map((preset) => ({
142
- label: preset.value,
143
- value: preset.value,
144
- })),
145
- { label: "Custom...", value: "custom" },
146
- ] as const;
147
-
148
- const sectionedCompoundControlTypes = new Set([
149
- "channelMixer",
150
- "collectionActions",
151
- "fontPicker",
152
- "gradient",
153
- "palette",
154
- ]);
155
-
156
- const defaultGradientStops = [
157
- { color: "#FFFFFF", position: "0%" },
158
- { color: "#7CFF3A", position: "46%" },
159
- { color: "#111111", position: "100%" },
160
- ] as const satisfies readonly GradientStop[];
161
-
162
- const defaultChannelMixerValues = {
163
- B: { B: 100, G: 0, R: 0 },
164
- G: { B: 0, G: 100, R: 0 },
165
- R: { B: 0, G: 0, R: 100 },
166
- } satisfies ChannelMixerValues;
167
-
168
- function cn(...classNames: Array<string | false | null | undefined>): string {
169
- return classNames.filter(Boolean).join(" ");
170
- }
171
-
172
- function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
173
- return (
174
- typeof value === "object" &&
175
- value !== null &&
176
- "then" in value &&
177
- typeof (value as { then?: unknown }).then === "function"
178
- );
179
- }
180
-
181
- function noopReportProgress(): void {}
182
-
183
- function clampFooterActionProgress(progress: number): number {
184
- if (!Number.isFinite(progress)) {
185
- return 0;
186
- }
187
-
188
- return Math.max(0, Math.min(1, progress));
189
- }
190
-
191
- function canCreateControlKeyframe(control: ToolcraftControlSchema): boolean {
192
- return getToolcraftControlKeyframeCapability(control).capable;
193
- }
194
-
195
- function shouldRenderCompoundControlSectionDivider(
196
- control: ToolcraftControlSchema,
197
- ): boolean {
198
- if (control.type === "curves") {
199
- return control.variant !== "single";
200
- }
201
-
202
- return sectionedCompoundControlTypes.has(control.type);
203
- }
204
-
205
- function withCompoundControlSectionDivider({
206
- children,
207
- control,
208
- }: {
209
- children: React.ReactNode;
210
- control: ToolcraftControlSchema;
211
- }): React.ReactNode {
212
- if (!shouldRenderCompoundControlSectionDivider(control)) {
213
- return children;
214
- }
215
-
216
- return (
217
- <div className="contents" data-control-section-divider="compound">
218
- {children}
219
- </div>
220
- );
221
- }
222
-
223
- function isRecord(value: unknown): value is AnyRecord {
224
- return typeof value === "object" && value !== null && !Array.isArray(value);
225
- }
226
-
227
- function valuesEqual(first: unknown, second: unknown): boolean {
228
- if (Object.is(first, second)) {
229
- return true;
230
- }
231
-
232
- if (
233
- (typeof first !== "object" || first === null) &&
234
- (typeof second !== "object" || second === null)
235
- ) {
236
- return false;
237
- }
238
-
239
- try {
240
- return JSON.stringify(first) === JSON.stringify(second);
241
- } catch {
242
- return false;
243
- }
244
- }
245
-
246
- function valuesInclude(value: unknown, options: readonly unknown[]): boolean {
247
- return options.some((option) => valuesEqual(value, option));
248
- }
249
-
250
- function readComparableNumber(value: unknown): number | null {
251
- if (typeof value === "number" && Number.isFinite(value)) {
252
- return value;
253
- }
254
-
255
- if (typeof value === "string") {
256
- const numberValue = Number(value.trim());
257
-
258
- return Number.isFinite(numberValue) ? numberValue : null;
259
- }
260
-
261
- return null;
262
- }
263
-
264
- function compareConditionNumbers(
265
- value: unknown,
266
- expected: number | undefined,
267
- comparator: (value: number, expected: number) => boolean,
268
- ): boolean | null {
269
- if (typeof expected !== "number" || !Number.isFinite(expected)) {
270
- return null;
271
- }
272
-
273
- const numberValue = readComparableNumber(value);
274
-
275
- return numberValue === null ? false : comparator(numberValue, expected);
276
- }
277
-
278
- function asNumber(value: unknown, fallback: number): number {
279
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
280
- }
281
-
282
- function asString(value: unknown, fallback = ""): string {
283
- if (typeof value === "string") {
284
- return value;
285
- }
286
-
287
- return typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
288
- }
289
-
290
- function parseCanvasAspectRatioOption(value: string): CanvasAspectRatioValue | null {
291
- const preset = getToolcraftCanvasAspectRatioPreset(value);
292
-
293
- if (preset) {
294
- return {
295
- height: preset.ratioHeight,
296
- mode: "preset",
297
- value: preset.value,
298
- width: preset.ratioWidth,
299
- };
300
- }
301
-
302
- const match = /^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/u.exec(value);
303
-
304
- if (!match) {
305
- return null;
306
- }
307
-
308
- const width = Number.parseFloat(match[1] ?? "");
309
- const height = Number.parseFloat(match[2] ?? "");
310
-
311
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
312
- return null;
313
- }
314
-
315
- return {
316
- height: Math.round(height),
317
- mode: "custom",
318
- value: `${Math.round(width)}:${Math.round(height)}`,
319
- width: Math.round(width),
320
- };
321
- }
322
-
323
- function asCanvasAspectRatioValue(
324
- value: unknown,
325
- fallback: unknown,
326
- ): CanvasAspectRatioValue {
327
- if (isRecord(value)) {
328
- const width = asNumber(value.width, NaN);
329
- const height = asNumber(value.height, NaN);
330
- const mode = value.mode === "preset" ? "preset" : "custom";
331
-
332
- if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
333
- return {
334
- height: Math.round(height),
335
- mode,
336
- value:
337
- typeof value.value === "string"
338
- ? value.value
339
- : `${Math.round(width)}:${Math.round(height)}`,
340
- width: Math.round(width),
341
- };
342
- }
343
- }
344
-
345
- if (typeof value === "string") {
346
- const parsed = parseCanvasAspectRatioOption(value);
347
-
348
- if (parsed) {
349
- return parsed;
350
- }
351
- }
352
-
353
- if (fallback !== value) {
354
- return asCanvasAspectRatioValue(fallback, {
355
- height: 1,
356
- mode: "preset",
357
- value: "1:1",
358
- width: 1,
359
- });
360
- }
361
-
362
- return {
363
- height: 1,
364
- mode: "preset",
365
- value: "1:1",
366
- width: 1,
367
- };
368
- }
369
-
370
- function asBoolean(value: unknown, fallback = false): boolean {
371
- return typeof value === "boolean" ? value : fallback;
372
- }
373
-
374
- function getPanelActionButtonVariant(
375
- variant: ToolcraftActionSchema["variant"],
376
- ): PanelActionObjectOption["variant"] {
377
- switch (variant) {
378
- case "destructive":
379
- case "ghost":
380
- case "link":
381
- case "outline":
382
- case "secondary":
383
- return variant;
384
- default:
385
- return "default";
386
- }
387
- }
388
-
389
- function asNumberArray(value: unknown, fallback: readonly number[]): readonly number[] {
390
- return Array.isArray(value) && value.every((item) => typeof item === "number")
391
- ? value
392
- : fallback;
393
- }
394
-
395
- function asRangeInputValue(value: unknown): { end: string; start: string } {
396
- if (isRecord(value)) {
397
- return {
398
- end: asString(value.end, "100%"),
399
- start: asString(value.start, "0%"),
400
- };
401
- }
402
-
403
- return { end: "100%", start: "0%" };
404
- }
405
-
406
- function asVectorValue(value: unknown): { x: string; y: string } {
407
- if (isRecord(value)) {
408
- return {
409
- x: asString(value.x, "0.00"),
410
- y: asString(value.y, "0.00"),
411
- };
412
- }
413
-
414
- return { x: "0.00", y: "0.00" };
415
- }
416
-
417
- function asVectorPadVariant(value: string | undefined): VectorPadVariant {
418
- if (
419
- value === "whiteBalance" ||
420
- value === "colorBalance" ||
421
- value === "chromaOffset" ||
422
- value === "toneBias"
423
- ) {
424
- return value;
425
- }
426
-
427
- return "default";
428
- }
429
-
430
- function asVectorPadCoordinateMode(
431
- value: string | undefined,
432
- padVariant: VectorPadVariant,
433
- ): VectorPadCoordinateMode {
434
- if (value === "cartesian" || value === "screen") {
435
- return value;
436
- }
437
-
438
- return padVariant === "default" ? "screen" : "cartesian";
439
- }
440
-
441
- function asCurveInterpolation(value: string | undefined): CurveInterpolation | undefined {
442
- return value === "monotone" || value === "smooth" ? value : undefined;
443
- }
444
-
445
- function formatControlValueLabel(
446
- control: ToolcraftControlSchema,
447
- value: unknown,
448
- ): string {
449
- if (typeof control.valueLabel === "string") {
450
- return control.valueLabel;
451
- }
452
-
453
- switch (control.type) {
454
- case "aspectRatio":
455
- return asCanvasAspectRatioValue(value, control.defaultValue).value;
456
- case "checkbox":
457
- case "switch":
458
- return asBoolean(value) ? "On" : "Off";
459
- case "color":
460
- return asColorValue(value).hex;
461
- case "colorOpacity": {
462
- const colorOpacityValue = asColorOpacityValue(value);
463
-
464
- return `${colorOpacityValue.hex} ${colorOpacityValue.opacity}%`;
465
- }
466
- case "collectionActions":
467
- return `${asCollectionItems(value, control.defaultValue).length} items`;
468
- case "fontPicker":
469
- return asFontPickerValue(value).fontId;
470
- case "gradient":
471
- return `${asGradientValue(value).stops.length} stops`;
472
- case "imagePicker":
473
- return (
474
- control.items?.find((item) => item.value === value)?.alt ??
475
- asString(value)
476
- );
477
- case "palette": {
478
- if (isRecord(value)) {
479
- const family = asString(value.family);
480
- const shade = asString(value.shade);
481
-
482
- return [family, shade].filter(Boolean).join(" ") || "Palette";
483
- }
484
-
485
- return "Palette";
486
- }
487
- case "rangeInput": {
488
- const rangeValue = asRangeInputValue(value);
489
-
490
- return `${rangeValue.start} – ${rangeValue.end}`;
491
- }
492
- case "rangeSlider": {
493
- const rangeValue = asNumberArray(value, []);
494
-
495
- return rangeValue.length > 0
496
- ? rangeValue.map((item) => `${item}${control.unit ?? ""}`).join(" – ")
497
- : "Range";
498
- }
499
- case "select":
500
- case "segmented":
501
- return (
502
- control.options?.find((option) => option.value === value)?.label ??
503
- asString(value)
504
- );
505
- case "slider":
506
- return `${asNumber(value, asNumber(control.defaultValue, control.min ?? 0))}${
507
- control.unit ?? ""
508
- }`;
509
- case "vector": {
510
- const vectorValue = asVectorValue(value);
511
-
512
- return `${vectorValue.x}, ${vectorValue.y}`;
513
- }
514
- default:
515
- return typeof value === "string" || typeof value === "number"
516
- ? String(value)
517
- : control.type;
518
- }
519
- }
520
-
521
- function asColorValue(value: unknown): { hex: string } {
522
- if (typeof value === "string") {
523
- return { hex: value };
524
- }
525
-
526
- if (isRecord(value)) {
527
- return { hex: asString(value.hex, "#C1FF00") };
528
- }
529
-
530
- return { hex: "#C1FF00" };
531
- }
532
-
533
- function asColorOpacityValue(value: unknown): ColorOpacityValue {
534
- if (typeof value === "string") {
535
- return { hex: value, opacity: 100 };
536
- }
537
-
538
- if (isRecord(value)) {
539
- return {
540
- hex: asString(value.hex, "#C1FF00"),
541
- opacity: Math.min(100, Math.max(0, Math.round(asNumber(value.opacity, 100)))),
542
- };
543
- }
544
-
545
- return { hex: "#C1FF00", opacity: 100 };
546
- }
547
-
548
- function asCollectionItems(value: unknown, fallback: unknown): unknown[] {
549
- if (Array.isArray(value)) {
550
- return [...value];
551
- }
552
-
553
- return Array.isArray(fallback) ? [...fallback] : [];
554
- }
555
-
556
- function getCollectionMinItems(control: ToolcraftControlSchema): number {
557
- return Math.max(0, Math.floor(asNumber(control.minItems, 0)));
558
- }
559
-
560
- function getCollectionHardMaxItems(control: ToolcraftControlSchema): number | null {
561
- if (typeof control.hardMaxItems !== "number" || !Number.isFinite(control.hardMaxItems)) {
562
- return null;
563
- }
564
-
565
- return Math.max(0, Math.floor(control.hardMaxItems));
566
- }
567
-
568
- function getCollectionItemType(control: ToolcraftControlSchema): string {
569
- return control.itemControl?.type ?? "color";
570
- }
571
-
572
- function getCollectionItemBaseLabel(control: ToolcraftControlSchema): string {
573
- if (control.itemLabel) {
574
- return control.itemLabel;
575
- }
576
-
577
- const label = control.itemControl?.label;
578
-
579
- return typeof label === "string" ? label : "Item";
580
- }
581
-
582
- function getCollectionItemName(
583
- control: ToolcraftControlSchema,
584
- index: number,
585
- ): string {
586
- const label = control.itemControl?.label;
587
-
588
- if (label === false) {
589
- return "";
590
- }
591
-
592
- return `${getCollectionItemBaseLabel(control)} ${index + 1}`;
593
- }
594
-
595
- function getCollectionItemDefaultValue(control: ToolcraftControlSchema): unknown {
596
- if ("itemDefaultValue" in control) {
597
- return control.itemDefaultValue;
598
- }
599
-
600
- if (typeof control.itemControl?.defaultValue !== "undefined") {
601
- return control.itemControl.defaultValue;
602
- }
603
-
604
- switch (getCollectionItemType(control)) {
605
- case "color":
606
- return { hex: "#C1FF00" };
607
- case "colorOpacity":
608
- return { hex: "#C1FF00", opacity: 100 };
609
- case "fontPicker":
610
- return asFontPickerValue(control.itemControl?.defaultValue);
611
- case "checkbox":
612
- case "switch":
613
- return false;
614
- case "rangeInput":
615
- return { end: "100%", start: "0%" };
616
- case "select":
617
- case "segmented":
618
- return control.itemControl?.options?.[0]?.value ?? "";
619
- case "slider":
620
- return control.itemControl?.min ?? 0;
621
- case "text":
622
- return "";
623
- default:
624
- return "";
625
- }
626
- }
627
-
628
- function asGradientType(value: unknown): GradientType {
629
- return value === "linear" ||
630
- value === "radial" ||
631
- value === "angular" ||
632
- value === "diamond"
633
- ? value
634
- : "linear";
635
- }
636
-
637
- function asGradientValue(value: unknown): {
638
- angle: number;
639
- gradientType: GradientType;
640
- stops: readonly GradientStop[];
641
- } {
642
- if (isRecord(value)) {
643
- return {
644
- angle: asNumber(value.angle, 90),
645
- gradientType: asGradientType(value.gradientType),
646
- stops: Array.isArray(value.stops)
647
- ? (value.stops as readonly GradientStop[])
648
- : defaultGradientStops,
649
- };
650
- }
651
-
652
- return {
653
- angle: 90,
654
- gradientType: "linear",
655
- stops: defaultGradientStops,
656
- };
657
- }
658
-
659
- function asFontPickerValue(value: unknown): FontPickerValue {
660
- if (typeof value === "string") {
661
- return {
662
- color: "#FFFFFF",
663
- fontId: value,
664
- fontSize: 16,
665
- fontWeight: "400",
666
- letterSpacing: "normal",
667
- lineHeight: "normal",
668
- opacity: 100,
669
- textCase: "original",
670
- };
671
- }
672
-
673
- if (isRecord(value)) {
674
- return {
675
- color: asString(value.color, "#FFFFFF"),
676
- fontId: asString(value.fontId, "inter"),
677
- fontSize: asNumber(value.fontSize, 16),
678
- fontWeight: asString(value.fontWeight, "400"),
679
- letterSpacing:
680
- value.letterSpacing === "tighter" ||
681
- value.letterSpacing === "tight" ||
682
- value.letterSpacing === "normal" ||
683
- value.letterSpacing === "wide" ||
684
- value.letterSpacing === "wider" ||
685
- value.letterSpacing === "widest"
686
- ? value.letterSpacing
687
- : "normal",
688
- lineHeight:
689
- value.lineHeight === "none" ||
690
- value.lineHeight === "tight" ||
691
- value.lineHeight === "snug" ||
692
- value.lineHeight === "normal" ||
693
- value.lineHeight === "relaxed" ||
694
- value.lineHeight === "loose"
695
- ? value.lineHeight
696
- : "normal",
697
- opacity: Math.min(100, Math.max(0, Math.round(asNumber(value.opacity, 100)))),
698
- textCase:
699
- value.textCase === "original" ||
700
- value.textCase === "uppercase" ||
701
- value.textCase === "lowercase" ||
702
- value.textCase === "capitalize" ||
703
- value.textCase === "titleCase"
704
- ? value.textCase
705
- : "original",
706
- };
707
- }
708
-
709
- return {
710
- color: "#FFFFFF",
711
- fontId: "inter",
712
- fontSize: 16,
713
- fontWeight: "400",
714
- letterSpacing: "normal",
715
- lineHeight: "normal",
716
- opacity: 100,
717
- textCase: "original",
718
- };
719
- }
720
-
721
- function CanvasAspectRatioControl({
722
- defaultValue,
723
- name,
724
- onValueChange,
725
- value,
726
- }: {
727
- defaultValue: unknown;
728
- name: string;
729
- onValueChange?: (
730
- value: CanvasAspectRatioValue,
731
- meta?: ControlChangeMeta,
732
- ) => void;
733
- value: unknown;
734
- }): React.JSX.Element {
735
- const ratio = asCanvasAspectRatioValue(value, defaultValue);
736
- const selectedValue = ratio.mode === "custom" ? "custom" : ratio.value;
737
-
738
- function commitRatio(
739
- nextRatio: CanvasAspectRatioValue,
740
- meta?: ControlChangeMeta,
741
- ): void {
742
- onValueChange?.(nextRatio, meta);
743
- }
744
-
745
- function updatePreset(nextValue: string): void {
746
- if (nextValue === "custom") {
747
- commitRatio({
748
- height: ratio.height,
749
- mode: "custom",
750
- value: `${ratio.width}:${ratio.height}`,
751
- width: ratio.width,
752
- });
753
- return;
754
- }
755
-
756
- const nextRatio = parseCanvasAspectRatioOption(nextValue);
757
-
758
- if (nextRatio) {
759
- commitRatio(nextRatio);
760
- }
761
- }
762
-
763
- function updateCustomDimension(
764
- dimension: "height" | "width",
765
- nextValue: string,
766
- meta?: ControlChangeMeta,
767
- ): void {
768
- const numberValue = Number.parseFloat(nextValue);
769
-
770
- if (!Number.isFinite(numberValue) || numberValue <= 0) {
771
- return;
772
- }
773
-
774
- const width =
775
- dimension === "width" ? Math.max(1, Math.round(numberValue)) : ratio.width;
776
- const height =
777
- dimension === "height" ? Math.max(1, Math.round(numberValue)) : ratio.height;
778
-
779
- commitRatio(
780
- {
781
- height,
782
- mode: "custom",
783
- value: `${width}:${height}`,
784
- width,
785
- },
786
- meta,
787
- );
788
- }
789
-
790
- return (
791
- <div className="min-w-0 space-y-2" data-slot="canvas-aspect-ratio-control">
792
- <Select
793
- name={name}
794
- onValueChange={updatePreset}
795
- options={canvasAspectRatioOptions}
796
- value={selectedValue}
797
- />
798
- {ratio.mode === "custom" ? (
799
- <TextInput
800
- inputs={[
801
- {
802
- commitOnBlur: true,
803
- defaultValue: String(ratio.width),
804
- name: "Width",
805
- onValueChange: (nextValue, meta) =>
806
- updateCustomDimension("width", nextValue, meta),
807
- value: String(ratio.width),
808
- },
809
- {
810
- commitOnBlur: true,
811
- defaultValue: String(ratio.height),
812
- name: "Height",
813
- onValueChange: (nextValue, meta) =>
814
- updateCustomDimension("height", nextValue, meta),
815
- value: String(ratio.height),
816
- },
817
- ]}
818
- inputsPerRow={2}
819
- />
820
- ) : null}
821
- </div>
822
- );
823
- }
824
-
825
- function asActionSchemas(
826
- actions: readonly (ToolcraftActionSchema | string)[] | undefined,
827
- ): readonly ToolcraftActionSchema[] {
828
- return (actions ?? []).map((action) =>
829
- typeof action === "string"
830
- ? {
831
- label: action,
832
- value: action,
833
- }
834
- : action,
835
- );
836
- }
837
-
838
- function getActionCommand(action: ToolcraftActionSchema): ToolcraftActionCommand | null {
839
- if (action.command) {
840
- return action.command;
841
- }
842
-
843
- switch (action.value.toLowerCase()) {
844
- case "apply":
845
- return "controls.apply";
846
- case "reset":
847
- return "controls.reset";
848
- default:
849
- return null;
850
- }
851
- }
852
-
853
- function getActionLabel(action: ToolcraftActionSchema): string {
854
- return action.label ?? action.value;
855
- }
856
-
857
- function isExportPanelAction(action: ToolcraftActionSchema): boolean {
858
- const label = getActionLabel(action);
859
- const value = action.value;
860
-
861
- return (
862
- /\bexport\b/i.test(label) ||
863
- /(?:^|[._:-])export(?:[._:-]|$)/i.test(value) ||
864
- /^export(?:[._:-]|$)/i.test(value)
865
- );
866
- }
867
-
868
- function getPanelActionIcon(
869
- action: ToolcraftActionSchema,
870
- ): PanelActionObjectOption["icon"] {
871
- return isExportPanelAction(action) ? "upload-simple" : action.icon;
872
- }
873
-
874
- function getControlName(id: string, label: boolean | string | undefined): string {
875
- if (typeof label === "string") {
876
- return label;
877
- }
878
-
879
- return id;
880
- }
881
-
882
- function ControlKeyframeButton({
883
- active,
884
- name,
885
- onClick,
886
- }: {
887
- active: boolean;
888
- name: string;
889
- onClick: () => void;
890
- }): React.JSX.Element {
891
- const label = active ? `Disable ${name} keyframes` : `Add ${name} keyframe`;
892
-
893
- return (
894
- <Tooltip>
895
- <TooltipTrigger
896
- render={
897
- <Button
898
- aria-label={label}
899
- aria-pressed={active}
900
- className={cn(
901
- "size-4 opacity-100 transition-opacity duration-150 ease-out hover:!bg-transparent active:!bg-transparent aria-pressed:!bg-transparent data-popup-open:!bg-transparent [&_svg:not([class*='size-'])]:!size-2.5 [&_svg:not([class*='size-'])]:!opacity-70 data-[icon-active=true]:[&_svg:not([class*='size-'])]:!opacity-100",
902
- active &&
903
- "!text-[color:var(--link)] aria-pressed:!text-[color:var(--link)] data-popup-open:!text-[color:var(--link)] [&_svg]:!text-[color:var(--link)] [&_svg]:!fill-[color:var(--link)]",
904
- )}
905
- data-icon-active={active}
906
- onClick={(event) => {
907
- event.stopPropagation();
908
- onClick();
909
-
910
- if (typeof event.currentTarget.blur === "function") {
911
- event.currentTarget.blur();
912
- }
913
- }}
914
- size="icon-sm"
915
- style={active ? { color: "var(--link)" } : undefined}
916
- type="button"
917
- variant="ghost-static"
918
- />
919
- }
920
- >
921
- <DiamondIcon weight={active ? "fill" : "regular"} />
922
- </TooltipTrigger>
923
- <TooltipContent side="top">{label}</TooltipContent>
924
- </Tooltip>
925
- );
926
- }
927
-
928
- function getControlRenderGroups(entries: readonly ControlEntry[]): ControlRenderGroup[] {
929
- const groups: ControlRenderGroup[] = [];
930
- let index = 0;
931
-
932
- while (index < entries.length) {
933
- const entry = entries[index];
934
-
935
- if (!entry) {
936
- index += 1;
937
- continue;
938
- }
939
-
940
- if (entry[1].type !== "color") {
941
- groups.push({ entry, kind: "control" });
942
- index += 1;
943
- continue;
944
- }
945
-
946
- const colorEntries: ControlEntry[] = [];
947
-
948
- while (entries[index]?.[1].type === "color") {
949
- const colorEntry = entries[index];
950
-
951
- if (colorEntry) {
952
- colorEntries.push(colorEntry);
953
- }
954
-
955
- index += 1;
956
- }
957
-
958
- for (let colorIndex = 0; colorIndex < colorEntries.length; colorIndex += 2) {
959
- const firstEntry = colorEntries[colorIndex];
960
- const secondEntry = colorEntries[colorIndex + 1];
961
-
962
- if (firstEntry && secondEntry) {
963
- groups.push({ entries: [firstEntry, secondEntry], kind: "colorGroup" });
964
- } else if (firstEntry) {
965
- groups.push({ entry: firstEntry, kind: "control" });
966
- }
967
- }
968
- }
969
-
970
- return groups;
971
- }
972
-
973
- function getControlRenderGroupIds(group: ControlRenderGroup): readonly string[] {
974
- return group.kind === "colorGroup"
975
- ? group.entries.map(([id]) => id)
976
- : [group.entry[0]];
977
- }
978
-
979
- function countControlsByType(
980
- sections: readonly ToolcraftControlSectionSchema[],
981
- type: string,
982
- ): number {
983
- return sections.reduce(
984
- (count, section) =>
985
- count +
986
- Object.values(section.controls).filter((control) => control.type === type)
987
- .length,
988
- 0,
989
- );
990
- }
991
-
992
- function shouldCommitTextControlOnBlur(control: ToolcraftControlSchema): boolean {
993
- return (
994
- control.commitMode === "setting" ||
995
- Boolean(getToolcraftCanvasSizeTargetDimension(control.target))
996
- );
997
- }
998
-
999
- function isColorFieldControl(control: ToolcraftControlSchema | undefined): boolean {
1000
- return control?.type === "color" || control?.type === "colorOpacity";
1001
- }
1002
-
1003
- function isColorOnlySection(entries: readonly ControlEntry[]): boolean {
1004
- return entries.length > 0 && entries.every(([, control]) => isColorFieldControl(control));
1005
- }
1006
-
1007
- function getRenderedControlsSectionTitle(
1008
- section: ToolcraftControlSectionSchema,
1009
- ): ToolcraftControlSectionSchema["title"] {
1010
- return isStickyFooterActionSection(section) ? undefined : section.title;
1011
- }
1012
-
1013
- function isRuntimeSetupSection({
1014
- entries,
1015
- section,
1016
- }: {
1017
- entries: readonly ControlEntry[];
1018
- section: ToolcraftControlSectionSchema;
1019
- }): boolean {
1020
- return (
1021
- section.title === "Setup" &&
1022
- entries.some(([, control]) => isToolcraftRuntimeOwnedTarget(control.target))
1023
- );
1024
- }
1025
-
1026
- function isStickyFooterActionSection(section: ToolcraftControlSectionSchema): boolean {
1027
- return (
1028
- section.actionGroup !== undefined &&
1029
- Object.values(section.controls).some((control) => control.type === "panelActions")
1030
- );
1031
- }
1032
-
1033
- function getControlsPanelSectionCollapseKey({
1034
- entries,
1035
- section,
1036
- sectionIndex,
1037
- }: {
1038
- entries: readonly ControlEntry[];
1039
- section: ToolcraftControlSectionSchema;
1040
- sectionIndex: number;
1041
- }): string {
1042
- const targets = entries.map(([id, control]) => `${id}:${control.target}`).join("|");
1043
-
1044
- return `${section.title ?? "section"}:${sectionIndex}:${targets}`;
1045
- }
1046
-
1047
- function hashStorageKeyPart(value: string): string {
1048
- let hash = 2166136261;
1049
-
1050
- for (let index = 0; index < value.length; index += 1) {
1051
- hash ^= value.charCodeAt(index);
1052
- hash = Math.imul(hash, 16777619);
1053
- }
1054
-
1055
- return (hash >>> 0).toString(36);
1056
- }
1057
-
1058
- function getControlsPanelSectionCollapseStorageKey(
1059
- schema: ResolvedToolcraftAppSchema,
1060
- ): string | null {
1061
- const controlsPanel = schema.panels.controls;
1062
-
1063
- if (!controlsPanel) {
1064
- return null;
1065
- }
1066
-
1067
- const appIdentity =
1068
- schema.persistence.storage === "localStorage"
1069
- ? `persistence:${schema.persistence.key}:v${schema.persistence.version}`
1070
- : JSON.stringify({
1071
- sections: controlsPanel.sections.map((section) => ({
1072
- controls: Object.entries(section.controls).map(([id, control]) => ({
1073
- id,
1074
- label: control.label,
1075
- target: control.target,
1076
- type: control.type,
1077
- })),
1078
- title: section.title,
1079
- })),
1080
- title: controlsPanel.title,
1081
- });
1082
-
1083
- return `toolcraft:ui:controls-panel-sections:${hashStorageKeyPart(appIdentity)}:v${controlsPanelSectionCollapseStorageVersion}`;
1084
- }
1085
-
1086
- function readControlsPanelCollapsedSections(
1087
- storageKey: string | null,
1088
- ): Record<string, boolean> {
1089
- if (!storageKey || typeof window === "undefined") {
1090
- return {};
1091
- }
1092
-
1093
- try {
1094
- const rawValue = readToolcraftLocalStorageValue(storageKey);
1095
-
1096
- if (!rawValue) {
1097
- return {};
1098
- }
1099
-
1100
- const payload: unknown = JSON.parse(rawValue);
1101
-
1102
- if (
1103
- !payload ||
1104
- typeof payload !== "object" ||
1105
- !("version" in payload) ||
1106
- payload.version !== controlsPanelSectionCollapseStorageVersion ||
1107
- !("collapsed" in payload) ||
1108
- !Array.isArray(payload.collapsed)
1109
- ) {
1110
- return {};
1111
- }
1112
-
1113
- return Object.fromEntries(
1114
- payload.collapsed
1115
- .filter((item): item is string => typeof item === "string" && item.length > 0)
1116
- .map((item) => [item, true]),
1117
- );
1118
- } catch {
1119
- return {};
1120
- }
1121
- }
1122
-
1123
- function writeControlsPanelCollapsedSections(
1124
- storageKey: string | null,
1125
- collapsedSectionByKey: Record<string, boolean>,
1126
- ): void {
1127
- if (!storageKey || typeof window === "undefined") {
1128
- return;
1129
- }
1130
-
1131
- const collapsed = Object.entries(collapsedSectionByKey)
1132
- .filter(([, collapsedValue]) => collapsedValue)
1133
- .map(([key]) => key);
1134
-
1135
- try {
1136
- if (collapsed.length === 0) {
1137
- removeToolcraftLocalStorageValue(storageKey);
1138
- return;
1139
- }
1140
-
1141
- window.localStorage.setItem(
1142
- storageKey,
1143
- JSON.stringify({
1144
- collapsed,
1145
- version: controlsPanelSectionCollapseStorageVersion,
1146
- }),
1147
- );
1148
- } catch {
1149
- // UI preferences are best-effort; panel interaction stays authoritative.
1150
- }
1151
- }
1152
-
1153
- function shouldShowColorFieldLabel({
1154
- control,
1155
- sectionHasOnlyColorFields,
1156
- }: {
1157
- control: ToolcraftControlSchema;
1158
- sectionHasOnlyColorFields: boolean;
1159
- }): boolean {
1160
- return control.label !== false && !sectionHasOnlyColorFields;
1161
- }
1162
-
1163
- function hasColorOpacityControl(
1164
- controlsById: Record<string, ToolcraftControlSchema>,
1165
- layoutGroup: ToolcraftControlLayoutGroupSchema,
1166
- ): boolean {
1167
- return layoutGroup.controls.some(
1168
- (controlId) => controlsById[controlId]?.type === "colorOpacity",
1169
- );
1170
- }
1171
-
1172
- function hasSliderControl(
1173
- controlsById: Record<string, ToolcraftControlSchema>,
1174
- layoutGroup: ToolcraftControlLayoutGroupSchema,
1175
- ): boolean {
1176
- return layoutGroup.controls.some(
1177
- (controlId) =>
1178
- controlsById[controlId]?.type === "slider" ||
1179
- controlsById[controlId]?.type === "rangeSlider",
1180
- );
1181
- }
1182
-
1183
- function hasSegmentedControl(
1184
- controlsById: Record<string, ToolcraftControlSchema>,
1185
- layoutGroup: ToolcraftControlLayoutGroupSchema,
1186
- ): boolean {
1187
- return layoutGroup.controls.some(
1188
- (controlId) => controlsById[controlId]?.type === "segmented",
1189
- );
1190
- }
1191
-
1192
- function hasSectionedCompoundControl(
1193
- controlsById: Record<string, ToolcraftControlSchema>,
1194
- layoutGroup: ToolcraftControlLayoutGroupSchema,
1195
- ): boolean {
1196
- return layoutGroup.controls.some((controlId) => {
1197
- const control = controlsById[controlId];
1198
-
1199
- return control ? shouldRenderCompoundControlSectionDivider(control) : false;
1200
- });
1201
- }
1202
-
1203
- const maxInlineSwitchLabelLength = 16;
1204
- const maxInlineSwitchLabelWordCount = 2;
1205
-
1206
- function isInlineSwitchLabelSafe(
1207
- controlId: string,
1208
- control: ToolcraftControlSchema,
1209
- ): boolean {
1210
- const label = getControlName(controlId, control.label).trim();
1211
- const wordCount = label.split(/\s+/u).filter(Boolean).length;
1212
-
1213
- return label.length <= maxInlineSwitchLabelLength && wordCount <= maxInlineSwitchLabelWordCount;
1214
- }
1215
-
1216
- function hasUnsafeInlineSwitchLabels(
1217
- controlsById: Record<string, ToolcraftControlSchema>,
1218
- layoutGroup: ToolcraftControlLayoutGroupSchema,
1219
- ): boolean {
1220
- const switchEntries = layoutGroup.controls
1221
- .map((controlId) => [controlId, controlsById[controlId]] as const)
1222
- .filter(
1223
- (entry): entry is readonly [string, ToolcraftControlSchema] =>
1224
- Boolean(entry[1]) && entry[1].type === "switch",
1225
- );
1226
-
1227
- return (
1228
- switchEntries.length > 1 &&
1229
- switchEntries.some(([controlId, control]) => !isInlineSwitchLabelSafe(controlId, control))
1230
- );
1231
- }
1232
-
1233
- function isBooleanControl(control: ToolcraftControlSchema | undefined): boolean {
1234
- return control?.type === "checkbox" || control?.type === "switch";
1235
- }
1236
-
1237
- function isToggleParameterLayoutGroup(
1238
- controlsById: Record<string, ToolcraftControlSchema>,
1239
- layoutGroup: ToolcraftControlLayoutGroupSchema,
1240
- ): boolean {
1241
- if (layoutGroup.controls.length !== 2) {
1242
- return false;
1243
- }
1244
-
1245
- const controls = layoutGroup.controls.map((controlId) => controlsById[controlId]);
1246
- const booleanControlCount = controls.filter((control) => isBooleanControl(control)).length;
1247
- const parameterControlCount = controls.filter((control) => !isBooleanControl(control)).length;
1248
-
1249
- return booleanControlCount === 1 && parameterControlCount === 1;
1250
- }
1251
-
1252
- function shouldHideToggleParameterControlLabel({
1253
- control,
1254
- controlsById,
1255
- layoutGroup,
1256
- }: {
1257
- control: ToolcraftControlSchema;
1258
- controlsById: Record<string, ToolcraftControlSchema>;
1259
- layoutGroup: ToolcraftControlLayoutGroupSchema | undefined;
1260
- }): boolean {
1261
- return Boolean(
1262
- layoutGroup &&
1263
- isToggleParameterLayoutGroup(controlsById, layoutGroup) &&
1264
- !isBooleanControl(control),
1265
- );
1266
- }
1267
-
1268
- function getControlTargetEntityKey(control: ToolcraftControlSchema): string | null {
1269
- const parts = control.target.split(".");
1270
-
1271
- if (parts.length < 2) {
1272
- return null;
1273
- }
1274
-
1275
- return parts.slice(0, -1).join(".");
1276
- }
1277
-
1278
- function shouldAutoInlineBooleanPair(
1279
- controlsById: Record<string, ToolcraftControlSchema>,
1280
- firstId: string,
1281
- secondId: string,
1282
- ): boolean {
1283
- const firstControl = controlsById[firstId];
1284
- const secondControl = controlsById[secondId];
1285
-
1286
- if (!isBooleanControl(firstControl) || !isBooleanControl(secondControl)) {
1287
- return false;
1288
- }
1289
-
1290
- if (
1291
- !isInlineSwitchLabelSafe(firstId, firstControl) ||
1292
- !isInlineSwitchLabelSafe(secondId, secondControl)
1293
- ) {
1294
- return false;
1295
- }
1296
-
1297
- const firstEntityKey = getControlTargetEntityKey(firstControl);
1298
- const secondEntityKey = getControlTargetEntityKey(secondControl);
1299
-
1300
- return firstEntityKey !== null && firstEntityKey === secondEntityKey;
1301
- }
1302
-
1303
- function getAutoInlineBooleanLayoutGroups({
1304
- controlsById,
1305
- renderedGroups,
1306
- reservedControlIds,
1307
- }: {
1308
- controlsById: Record<string, ToolcraftControlSchema>;
1309
- renderedGroups: readonly RenderedControlRenderGroup[];
1310
- reservedControlIds: ReadonlySet<string>;
1311
- }): ToolcraftControlLayoutGroupSchema[] {
1312
- const layoutGroups: ToolcraftControlLayoutGroupSchema[] = [];
1313
- let index = 0;
1314
-
1315
- while (index < renderedGroups.length) {
1316
- const firstIds = renderedGroups[index]?.ids;
1317
- const secondIds = renderedGroups[index + 1]?.ids;
1318
- const firstId = firstIds?.length === 1 ? firstIds[0] : undefined;
1319
- const secondId = secondIds?.length === 1 ? secondIds[0] : undefined;
1320
-
1321
- if (
1322
- firstId &&
1323
- secondId &&
1324
- !reservedControlIds.has(firstId) &&
1325
- !reservedControlIds.has(secondId) &&
1326
- shouldAutoInlineBooleanPair(controlsById, firstId, secondId)
1327
- ) {
1328
- layoutGroups.push({
1329
- columns: 2,
1330
- controls: [firstId, secondId],
1331
- layout: "inline",
1332
- });
1333
- index += 2;
1334
- continue;
1335
- }
1336
-
1337
- index += 1;
1338
- }
1339
-
1340
- return layoutGroups;
1341
- }
1342
-
1343
- function getControlMarkerCount(
1344
- control: ToolcraftControlSchema,
1345
- markerLimit?: number,
1346
- ): number | undefined {
1347
- const markerCount = control.markerCount;
1348
-
1349
- if (
1350
- markerLimit &&
1351
- control.variant === "discrete" &&
1352
- typeof markerCount === "number" &&
1353
- markerCount > markerLimit
1354
- ) {
1355
- return hiddenDiscreteMarkerCount;
1356
- }
1357
-
1358
- return markerCount;
1359
- }
1360
-
1361
- function renderControlLayoutGroups({
1362
- controlsById,
1363
- layoutGroups,
1364
- renderedGroups,
1365
- }: {
1366
- controlsById: Record<string, ToolcraftControlSchema>;
1367
- layoutGroups?: readonly ToolcraftControlLayoutGroupSchema[];
1368
- renderedGroups: readonly RenderedControlRenderGroup[];
1369
- }): React.ReactNode[] {
1370
- if (!layoutGroups?.length) {
1371
- const autoLayoutGroups = getAutoInlineBooleanLayoutGroups({
1372
- controlsById,
1373
- renderedGroups,
1374
- reservedControlIds: new Set(),
1375
- });
1376
-
1377
- if (autoLayoutGroups.length === 0) {
1378
- return renderedGroups.map((group) => group.node);
1379
- }
1380
-
1381
- return renderControlLayoutGroups({
1382
- controlsById,
1383
- layoutGroups: autoLayoutGroups,
1384
- renderedGroups,
1385
- });
1386
- }
1387
-
1388
- const reservedControlIds = new Set(layoutGroups.flatMap((group) => group.controls));
1389
- const resolvedLayoutGroups = [
1390
- ...layoutGroups,
1391
- ...getAutoInlineBooleanLayoutGroups({
1392
- controlsById,
1393
- renderedGroups,
1394
- reservedControlIds,
1395
- }),
1396
- ];
1397
- const layoutGroupByControlId = getInlineLayoutGroupByControlId({
1398
- controlsById,
1399
- layoutGroups: resolvedLayoutGroups,
1400
- });
1401
-
1402
- const nodes: React.ReactNode[] = [];
1403
-
1404
- for (const renderedGroup of renderedGroups) {
1405
- const layoutGroup = renderedGroup.ids
1406
- .map((id) => layoutGroupByControlId.get(id))
1407
- .find((group): group is ToolcraftControlLayoutGroupSchema => Boolean(group));
1408
-
1409
- if (!layoutGroup) {
1410
- nodes.push(renderedGroup.node);
1411
- continue;
1412
- }
1413
-
1414
- const groupedRenderedControls = renderedGroups.filter((candidate) =>
1415
- candidate.ids.some((id) => layoutGroup.controls.includes(id)),
1416
- );
1417
- const firstGroupedControl = groupedRenderedControls[0];
1418
-
1419
- if (firstGroupedControl !== renderedGroup) {
1420
- continue;
1421
- }
1422
-
1423
- if (groupedRenderedControls.length < 2) {
1424
- nodes.push(renderedGroup.node);
1425
- continue;
1426
- }
1427
-
1428
- nodes.push(
1429
- <ControlInlineGroup
1430
- columns={layoutGroup.columns ?? 2}
1431
- kind={
1432
- isToggleParameterLayoutGroup(controlsById, layoutGroup)
1433
- ? "toggleParameter"
1434
- : "default"
1435
- }
1436
- key={`layout-group-${layoutGroup.controls.join("-")}`}
1437
- >
1438
- {groupedRenderedControls.map((group) => group.node)}
1439
- </ControlInlineGroup>,
1440
- );
1441
- }
1442
-
1443
- return nodes;
1444
- }
1445
-
1446
- function getInlineLayoutGroupByControlId({
1447
- controlsById,
1448
- layoutGroups,
1449
- }: {
1450
- controlsById: Record<string, ToolcraftControlSchema>;
1451
- layoutGroups?: readonly ToolcraftControlLayoutGroupSchema[];
1452
- }): Map<string, ToolcraftControlLayoutGroupSchema> {
1453
- const layoutGroupByControlId = new Map<string, ToolcraftControlLayoutGroupSchema>();
1454
-
1455
- for (const layoutGroup of layoutGroups ?? []) {
1456
- if (layoutGroup.layout !== "inline") {
1457
- continue;
1458
- }
1459
-
1460
- if (
1461
- hasColorOpacityControl(controlsById, layoutGroup) ||
1462
- hasSliderControl(controlsById, layoutGroup) ||
1463
- hasSegmentedControl(controlsById, layoutGroup) ||
1464
- hasSectionedCompoundControl(controlsById, layoutGroup) ||
1465
- hasUnsafeInlineSwitchLabels(controlsById, layoutGroup)
1466
- ) {
1467
- continue;
1468
- }
1469
-
1470
- for (const controlId of layoutGroup.controls) {
1471
- layoutGroupByControlId.set(controlId, layoutGroup);
1472
- }
1473
- }
1474
-
1475
- return layoutGroupByControlId;
1476
- }
1477
-
1478
- export function ControlsPanel({
1479
- className,
1480
- controlRenderers,
1481
- framed = true,
1482
- onPanelAction,
1483
- onPanelStateChange,
1484
- panelPlacement,
1485
- panelState,
1486
- }: ControlsPanelProps): React.JSX.Element | null {
1487
- const { dispatch, state } = useToolcraft();
1488
- const nextFooterActionIdRef = React.useRef(0);
1489
- const [footerActionProgressEntries, setFooterActionProgressEntries] = React.useState<
1490
- readonly FooterActionProgressEntry[]
1491
- >([]);
1492
- const sectionCollapseStorageKey = React.useMemo(
1493
- () => getControlsPanelSectionCollapseStorageKey(state.schema),
1494
- [state.schema],
1495
- );
1496
- const [collapsedSectionByKey, setCollapsedSectionByKey] = React.useState<
1497
- Record<string, boolean>
1498
- >(() => readControlsPanelCollapsedSections(sectionCollapseStorageKey));
1499
- const stickyFooterProgress = React.useMemo(() => {
1500
- for (let index = footerActionProgressEntries.length - 1; index >= 0; index -= 1) {
1501
- const progress = footerActionProgressEntries[index]?.progress;
1502
-
1503
- if (typeof progress === "number") {
1504
- return progress;
1505
- }
1506
- }
1507
-
1508
- return null;
1509
- }, [footerActionProgressEntries]);
1510
- const controlsPanel = state.schema.panels.controls;
1511
- const keyframedControlIds = React.useMemo(
1512
- () => new Set(state.timeline.keyframeGroups.map((group) => group.controlId)),
1513
- [state.timeline.keyframeGroups],
1514
- );
1515
- const keyframeControlsEnabled = Boolean(
1516
- state.schema.assembly.capabilities.includes("timeline.keyframes") &&
1517
- state.timeline.expanded,
1518
- );
1519
-
1520
- React.useEffect(() => {
1521
- setCollapsedSectionByKey(readControlsPanelCollapsedSections(sectionCollapseStorageKey));
1522
- }, [sectionCollapseStorageKey]);
1523
-
1524
- if (!controlsPanel) {
1525
- return null;
1526
- }
1527
-
1528
- const resolvedControlsPanel = controlsPanel;
1529
- const placement = panelPlacement ?? (framed ? "frame" : "surface");
1530
- const lastHistoryPatch = state.history.undo.at(-1);
1531
- const controlsResetKey =
1532
- lastHistoryPatch?.label === "Reset controls" ? state.history.undo.length : 0;
1533
-
1534
- function dispatchCommand(command: ToolcraftCommand): void {
1535
- dispatch(command);
1536
- }
1537
-
1538
- function setControlValue(
1539
- target: string,
1540
- value: unknown,
1541
- label?: string,
1542
- meta?: ControlChangeMeta,
1543
- ): void {
1544
- dispatchCommand({
1545
- history: meta?.history,
1546
- historyGroup: meta?.historyGroup,
1547
- label,
1548
- target,
1549
- type: "controls.setValue",
1550
- value,
1551
- });
1552
- }
1553
-
1554
- function createFooterActionProgressTracker(): {
1555
- reportProgress: (progress: number) => void;
1556
- trackResult: (result: PromiseLike<unknown> | void) => void;
1557
- } {
1558
- const id = nextFooterActionIdRef.current;
1559
- nextFooterActionIdRef.current += 1;
1560
-
1561
- let latestProgress: number | null = null;
1562
- let isTracked = false;
1563
-
1564
- function reportProgress(progress: number): void {
1565
- latestProgress = clampFooterActionProgress(progress);
1566
-
1567
- if (!isTracked) {
1568
- return;
1569
- }
1570
-
1571
- setFooterActionProgressEntries((entries) =>
1572
- entries.map((entry) =>
1573
- entry.id === id ? { ...entry, progress: latestProgress } : entry,
1574
- ),
1575
- );
1576
- }
1577
-
1578
- function trackResult(result: PromiseLike<unknown> | void): void {
1579
- if (!isPromiseLike(result)) {
1580
- return;
1581
- }
1582
-
1583
- isTracked = true;
1584
- setFooterActionProgressEntries((entries) => [
1585
- ...entries,
1586
- { id, progress: latestProgress },
1587
- ]);
1588
-
1589
- void Promise.resolve(result)
1590
- .catch((error: unknown) => {
1591
- console.error("Toolcraft panel action failed.", error);
1592
- })
1593
- .finally(() => {
1594
- setFooterActionProgressEntries((entries) =>
1595
- entries.filter((entry) => entry.id !== id),
1596
- );
1597
- });
1598
- }
1599
-
1600
- return { reportProgress, trackResult };
1601
- }
1602
-
1603
- function runAction(
1604
- action: ToolcraftActionSchema,
1605
- options: { trackFooterPending?: boolean } = {},
1606
- ): void {
1607
- const command = action.command ?? (onPanelAction ? null : getActionCommand(action));
1608
-
1609
- if (command) {
1610
- dispatchCommand({ type: command });
1611
- return;
1612
- }
1613
-
1614
- const footerActionProgressTracker = options.trackFooterPending
1615
- ? createFooterActionProgressTracker()
1616
- : null;
1617
- const result = onPanelAction?.({
1618
- action,
1619
- dispatch,
1620
- reportProgress:
1621
- footerActionProgressTracker?.reportProgress ?? noopReportProgress,
1622
- state,
1623
- });
1624
-
1625
- footerActionProgressTracker?.trackResult(result);
1626
- }
1627
-
1628
- function getControlValue(control: ToolcraftControlSchema): unknown {
1629
- const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(control.target);
1630
-
1631
- if (isToolcraftTimelinePanelExtendedTarget(control.target)) {
1632
- return state.panels.timeline.extended === true;
1633
- }
1634
-
1635
- if (isToolcraftTimelinePanelVisibleTarget(control.target)) {
1636
- return state.panels.timeline.hidden !== true;
1637
- }
1638
-
1639
- return canvasSizeDimension
1640
- ? state.canvas.size[canvasSizeDimension]
1641
- : (state.values[control.target] ?? control.defaultValue);
1642
- }
1643
-
1644
- function getControlDefaultValueByTarget(target: string): unknown {
1645
- for (const section of resolvedControlsPanel.sections) {
1646
- for (const control of Object.values(section.controls)) {
1647
- if (control.target === target) {
1648
- return control.defaultValue;
1649
- }
1650
- }
1651
- }
1652
-
1653
- return undefined;
1654
- }
1655
-
1656
- function getTargetValue(target: string): unknown {
1657
- const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(target);
1658
-
1659
- if (isToolcraftTimelinePanelExtendedTarget(target)) {
1660
- return state.panels.timeline.extended === true;
1661
- }
1662
-
1663
- if (isToolcraftTimelinePanelVisibleTarget(target)) {
1664
- return state.panels.timeline.hidden !== true;
1665
- }
1666
-
1667
- return canvasSizeDimension
1668
- ? state.canvas.size[canvasSizeDimension]
1669
- : (state.values[target] ?? getControlDefaultValueByTarget(target));
1670
- }
1671
-
1672
- function conditionMatches(condition: ToolcraftControlConditionSchema): boolean {
1673
- const value = getTargetValue(condition.target);
1674
- const matches: boolean[] = [];
1675
-
1676
- if ("equals" in condition) {
1677
- matches.push(valuesEqual(value, condition.equals));
1678
- }
1679
-
1680
- if ("notEquals" in condition) {
1681
- matches.push(!valuesEqual(value, condition.notEquals));
1682
- }
1683
-
1684
- if (Array.isArray(condition.oneOf)) {
1685
- matches.push(valuesInclude(value, condition.oneOf));
1686
- }
1687
-
1688
- if (Array.isArray(condition.notOneOf)) {
1689
- matches.push(!valuesInclude(value, condition.notOneOf));
1690
- }
1691
-
1692
- const greaterThan = compareConditionNumbers(
1693
- value,
1694
- condition.greaterThan,
1695
- (numberValue, expected) => numberValue > expected,
1696
- );
1697
- const greaterThanOrEqual = compareConditionNumbers(
1698
- value,
1699
- condition.greaterThanOrEqual,
1700
- (numberValue, expected) => numberValue >= expected,
1701
- );
1702
- const lessThan = compareConditionNumbers(
1703
- value,
1704
- condition.lessThan,
1705
- (numberValue, expected) => numberValue < expected,
1706
- );
1707
- const lessThanOrEqual = compareConditionNumbers(
1708
- value,
1709
- condition.lessThanOrEqual,
1710
- (numberValue, expected) => numberValue <= expected,
1711
- );
1712
-
1713
- for (const match of [
1714
- greaterThan,
1715
- greaterThanOrEqual,
1716
- lessThan,
1717
- lessThanOrEqual,
1718
- ]) {
1719
- if (match !== null) {
1720
- matches.push(match);
1721
- }
1722
- }
1723
-
1724
- return matches.length > 0 && matches.every(Boolean);
1725
- }
1726
-
1727
- function isControlDisabled(control: ToolcraftControlSchema): boolean {
1728
- if (control.disabled) {
1729
- return true;
1730
- }
1731
-
1732
- return control.disabledWhen ? conditionMatches(control.disabledWhen) : false;
1733
- }
1734
-
1735
- function isControlVisible(control: ToolcraftControlSchema): boolean {
1736
- return control.visibleWhen ? conditionMatches(control.visibleWhen) : true;
1737
- }
1738
-
1739
- function isSectionVisible(section: ToolcraftControlSectionSchema): boolean {
1740
- return section.visibleWhen ? conditionMatches(section.visibleWhen) : true;
1741
- }
1742
-
1743
- function getVisibleSectionEntries(
1744
- section: ToolcraftControlSectionSchema,
1745
- ): ControlEntry[] {
1746
- return Object.entries(section.controls).filter(([, control]) =>
1747
- isControlVisible(control),
1748
- );
1749
- }
1750
-
1751
- function getControlsRecord(
1752
- entries: readonly ControlEntry[],
1753
- ): Record<string, ToolcraftControlSchema> {
1754
- return Object.fromEntries(entries);
1755
- }
1756
-
1757
- function getSelectedControlKeyframeTime(controlId: string): number | undefined {
1758
- const selectedKeyframeId = state.timeline.selectedKeyframeId;
1759
-
1760
- if (!selectedKeyframeId) {
1761
- return undefined;
1762
- }
1763
-
1764
- const selectedKeyframe = state.timeline.keyframeGroups
1765
- .find((group) => group.controlId === controlId)
1766
- ?.keyframes.find((keyframe) => keyframe.id === selectedKeyframeId);
1767
-
1768
- return selectedKeyframe?.timeSeconds;
1769
- }
1770
-
1771
- function maybeUpsertControlKeyframe(
1772
- control: ToolcraftControlSchema,
1773
- name: string,
1774
- value: unknown,
1775
- ): void {
1776
- if (
1777
- !keyframeControlsEnabled ||
1778
- !keyframedControlIds.has(control.target) ||
1779
- !canCreateControlKeyframe(control)
1780
- ) {
1781
- return;
1782
- }
1783
-
1784
- dispatchCommand({
1785
- controlId: control.target,
1786
- controlLabel: name,
1787
- timeSeconds: getSelectedControlKeyframeTime(control.target),
1788
- type: "timeline.upsertControlKeyframe",
1789
- value,
1790
- valueLabel: formatControlValueLabel(control, value),
1791
- });
1792
- }
1793
-
1794
- function getKeyframeLabelAction(
1795
- control: ToolcraftControlSchema,
1796
- name: string,
1797
- value: unknown,
1798
- ): React.ReactNode {
1799
- if (!keyframeControlsEnabled || !canCreateControlKeyframe(control)) {
1800
- return null;
1801
- }
1802
-
1803
- return (
1804
- <ControlKeyframeButton
1805
- active={keyframedControlIds.has(control.target)}
1806
- name={name}
1807
- onClick={() => {
1808
- dispatchCommand({
1809
- controlId: control.target,
1810
- controlLabel: name,
1811
- type: "timeline.toggleControlKeyframes",
1812
- value,
1813
- valueLabel: formatControlValueLabel(control, value),
1814
- });
1815
- }}
1816
- />
1817
- );
1818
- }
1819
-
1820
- function withKeyframeLabelAction({
1821
- children,
1822
- control,
1823
- disableAction = false,
1824
- labelActionName,
1825
- name,
1826
- providerKey,
1827
- value,
1828
- }: {
1829
- children: React.ReactNode;
1830
- control: ToolcraftControlSchema;
1831
- disableAction?: boolean;
1832
- labelActionName?: string;
1833
- name: string;
1834
- providerKey: string;
1835
- value: unknown;
1836
- }): React.ReactNode {
1837
- if (disableAction) {
1838
- return children;
1839
- }
1840
-
1841
- const actionName = labelActionName ?? name;
1842
- const action = getKeyframeLabelAction(control, actionName, value);
1843
-
1844
- if (!action) {
1845
- return children;
1846
- }
1847
-
1848
- return (
1849
- <ControlFieldLabelActionProvider
1850
- action={action}
1851
- key={providerKey}
1852
- label={actionName}
1853
- >
1854
- {children}
1855
- </ControlFieldLabelActionProvider>
1856
- );
1857
- }
1858
-
1859
- function normalizeControlHelpContext(value: string): string {
1860
- return value
1861
- .toLowerCase()
1862
- .replace(/[^a-z0-9]+/g, " ")
1863
- .trim();
1864
- }
1865
-
1866
- function isColorSectionTitle(sectionTitle: string | undefined): boolean {
1867
- return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
1868
- sectionTitle ?? "",
1869
- );
1870
- }
1871
-
1872
- function isSequentialColorLabel(label: string): boolean {
1873
- return /^colou?r\s+\d+$/i.test(label.trim());
1874
- }
1875
-
1876
- function isSimplePaletteDistributionLabel(label: string): boolean {
1877
- return /^(spread|mix|distribution)$/i.test(label.trim());
1878
- }
1879
-
1880
- function isGenericControlHelpDescription(description: string): boolean {
1881
- return /^(adjusts?|changes?|chooses?|controls?|defines?|selects?|sets?|updates?)\b/i.test(
1882
- description.trim(),
1883
- );
1884
- }
1885
-
1886
- function shouldSuppressObviousControlHelp({
1887
- control,
1888
- description,
1889
- label,
1890
- sectionTitle,
1891
- }: {
1892
- control: ToolcraftControlSchema;
1893
- description: string;
1894
- label: string;
1895
- sectionTitle: string | undefined;
1896
- }): boolean {
1897
- const isColorControl = control.type === "color" || control.type === "colorOpacity";
1898
-
1899
- if (isColorSectionTitle(sectionTitle)) {
1900
- if (isColorControl && isSequentialColorLabel(label)) {
1901
- return true;
1902
- }
1903
-
1904
- if (
1905
- isSimplePaletteDistributionLabel(label) &&
1906
- isGenericControlHelpDescription(description)
1907
- ) {
1908
- return true;
1909
- }
1910
- }
1911
-
1912
- const normalizedDescription = normalizeControlHelpContext(description);
1913
- const normalizedLabel = normalizeControlHelpContext(label);
1914
-
1915
- return (
1916
- Boolean(normalizedLabel) &&
1917
- isGenericControlHelpDescription(description) &&
1918
- normalizedDescription === normalizedLabel
1919
- );
1920
- }
1921
-
1922
- function getControlHelpText({
1923
- control,
1924
- label,
1925
- sectionTitle,
1926
- }: {
1927
- control: ToolcraftControlSchema;
1928
- label: string;
1929
- sectionTitle: string | undefined;
1930
- }): string | null {
1931
- const description = control.description?.trim();
1932
-
1933
- if (!description) {
1934
- return null;
1935
- }
1936
-
1937
- if (
1938
- shouldSuppressObviousControlHelp({
1939
- control,
1940
- description,
1941
- label,
1942
- sectionTitle,
1943
- })
1944
- ) {
1945
- return null;
1946
- }
1947
-
1948
- return description;
1949
- }
1950
-
1951
- function withControlLabelHelp({
1952
- children,
1953
- control,
1954
- label,
1955
- providerKey,
1956
- sectionTitle,
1957
- }: {
1958
- children: React.ReactNode;
1959
- control: ToolcraftControlSchema;
1960
- label: string;
1961
- providerKey: string;
1962
- sectionTitle: string | undefined;
1963
- }): React.ReactNode {
1964
- if (control.label === false) {
1965
- return children;
1966
- }
1967
-
1968
- const help = getControlHelpText({ control, label, sectionTitle });
1969
-
1970
- if (!help) {
1971
- return children;
1972
- }
1973
-
1974
- return (
1975
- <ControlFieldLabelHelpProvider
1976
- help={help}
1977
- key={`${providerKey}-help`}
1978
- label={label}
1979
- >
1980
- {children}
1981
- </ControlFieldLabelHelpProvider>
1982
- );
1983
- }
1984
-
1985
- function getSectionHeaderKeyframeEntry(
1986
- entries: readonly ControlEntry[],
1987
- title: React.ReactNode,
1988
- ): ControlEntry | null {
1989
- if (typeof title !== "string") {
1990
- return null;
1991
- }
1992
-
1993
- const matchingTitleEntry = entries.find(([id, control]) => {
1994
- if (control.type === "channelMixer" || control.type === "curves") {
1995
- return false;
1996
- }
1997
-
1998
- const name = getControlName(id, control.label);
1999
-
2000
- return name === title && canCreateControlKeyframe(control);
2001
- });
2002
-
2003
- if (matchingTitleEntry) {
2004
- return matchingTitleEntry;
2005
- }
2006
-
2007
- return null;
2008
- }
2009
-
2010
- function getSectionHeaderKeyframeAction(entry: ControlEntry): React.ReactNode {
2011
- const [id, control] = entry;
2012
- const name = getControlName(id, control.label);
2013
-
2014
- return getKeyframeLabelAction(control, name, getControlValue(control));
2015
- }
2016
-
2017
- function getSectionResetAction({
2018
- sectionTitle,
2019
- targets,
2020
- }: {
2021
- sectionTitle: string;
2022
- targets: readonly string[];
2023
- }): React.ReactNode {
2024
- const label = `Reset ${sectionTitle} section`;
2025
-
2026
- return (
2027
- <Tooltip>
2028
- <TooltipTrigger
2029
- render={
2030
- <Button
2031
- aria-label={label}
2032
- data-control-section-reset-button=""
2033
- onClick={() => {
2034
- dispatchCommand({
2035
- label,
2036
- targets: Array.from(new Set(targets)),
2037
- type: "controls.resetTargets",
2038
- });
2039
- }}
2040
- size="icon-sm"
2041
- type="button"
2042
- variant="ghost"
2043
- />
2044
- }
2045
- >
2046
- <ArrowCounterClockwiseIcon />
2047
- </TooltipTrigger>
2048
- <TooltipContent side="top">{label}</TooltipContent>
2049
- </Tooltip>
2050
- );
2051
- }
2052
-
2053
- function renderColorGroup(
2054
- entries: readonly ControlEntry[],
2055
- headerKeyframeTarget: string | null,
2056
- sectionHasOnlyColorFields: boolean,
2057
- ): React.JSX.Element | null {
2058
- const colorInputs = entries.map(([id, control]) => {
2059
- const name = getControlName(id, control.label);
2060
- const value = getControlValue(control);
2061
- const colorValue = asColorValue(value);
2062
-
2063
- return {
2064
- hex: colorValue.hex,
2065
- name,
2066
- onValueChange: (nextValue, meta) => {
2067
- setControlValue(control.target, nextValue, name, meta);
2068
- maybeUpsertControlKeyframe(control, name, nextValue);
2069
- },
2070
- showLabel: shouldShowColorFieldLabel({
2071
- control,
2072
- sectionHasOnlyColorFields,
2073
- }),
2074
- } satisfies ColorControlInput;
2075
- });
2076
- const [firstInput, secondInput] = colorInputs;
2077
-
2078
- if (!firstInput) {
2079
- return null;
2080
- }
2081
-
2082
- if (!secondInput) {
2083
- const firstEntry = entries[0];
2084
- const firstControl = firstEntry?.[1];
2085
- const firstValue = firstControl ? getControlValue(firstControl) : undefined;
2086
-
2087
- return firstControl ? (
2088
- withKeyframeLabelAction({
2089
- children: (
2090
- <Color
2091
- hex={firstInput.hex}
2092
- key={firstInput.name}
2093
- name={firstInput.name}
2094
- onValueChange={firstInput.onValueChange}
2095
- showLabel={shouldShowColorFieldLabel({
2096
- control: firstControl,
2097
- sectionHasOnlyColorFields,
2098
- })}
2099
- />
2100
- ),
2101
- control: firstControl,
2102
- disableAction: firstControl.target === headerKeyframeTarget,
2103
- name: firstInput.name,
2104
- providerKey: firstInput.name,
2105
- value: firstValue,
2106
- }) as React.JSX.Element
2107
- ) : (
2108
- <Color
2109
- hex={firstInput.hex}
2110
- key={firstInput.name}
2111
- name={firstInput.name}
2112
- onValueChange={firstInput.onValueChange}
2113
- showLabel={firstInput.showLabel}
2114
- />
2115
- );
2116
- }
2117
-
2118
- return (
2119
- <Color
2120
- inputs={[firstInput, secondInput] as ColorControlInputPair}
2121
- key={`${firstInput.name}-${secondInput.name}`}
2122
- />
2123
- );
2124
- }
2125
-
2126
- function renderCollectionActionsControl({
2127
- control,
2128
- name,
2129
- value,
2130
- }: {
2131
- control: ToolcraftControlSchema;
2132
- name: string;
2133
- value: unknown;
2134
- }): React.JSX.Element {
2135
- const items = asCollectionItems(value, control.defaultValue);
2136
- const minItems = getCollectionMinItems(control);
2137
- const hardMaxItems = getCollectionHardMaxItems(control);
2138
- const canAdd = hardMaxItems === null || items.length < hardMaxItems;
2139
- const canRemove = items.length > minItems;
2140
- const itemType = getCollectionItemType(control);
2141
-
2142
- function setItems(nextItems: unknown[], label: string): void {
2143
- setControlValue(control.target, nextItems, label);
2144
- }
2145
-
2146
- function addItem(): void {
2147
- if (!canAdd) {
2148
- return;
2149
- }
2150
-
2151
- setItems([...items, getCollectionItemDefaultValue(control)], control.addLabel ?? "Add item");
2152
- }
2153
-
2154
- function removeItem(): void {
2155
- if (!canRemove) {
2156
- return;
2157
- }
2158
-
2159
- setItems(items.slice(0, -1), control.removeLabel ?? "Remove item");
2160
- }
2161
-
2162
- function updateItem(index: number, nextValue: unknown, meta?: ControlChangeMeta): void {
2163
- const nextItems = items.map((item, itemIndex) =>
2164
- itemIndex === index ? nextValue : item,
2165
- );
2166
- const itemName = getCollectionItemName(control, index) || name;
2167
-
2168
- setControlValue(control.target, nextItems, itemName, meta);
2169
- }
2170
-
2171
- function renderColorItems(): React.ReactNode {
2172
- return (
2173
- <div
2174
- className="grid min-w-0 grid-cols-2 gap-x-2 gap-y-4"
2175
- data-slot="collection-actions-items-grid"
2176
- >
2177
- {items.map((item, index) => {
2178
- const itemName = getCollectionItemName(control, index);
2179
-
2180
- return (
2181
- <Color
2182
- hex={asColorValue(item).hex}
2183
- key={itemName || index}
2184
- name={itemName}
2185
- onValueChange={(nextValue: { hex: string }, meta?: ControlChangeMeta) =>
2186
- updateItem(index, nextValue, meta)
2187
- }
2188
- showLabel={false}
2189
- />
2190
- );
2191
- })}
2192
- </div>
2193
- );
2194
- }
2195
-
2196
- function renderColorOpacityItems(): React.ReactNode {
2197
- return items.map((item, index) => {
2198
- const colorOpacityValue = asColorOpacityValue(item);
2199
- const itemName = getCollectionItemName(control, index);
2200
-
2201
- return (
2202
- <ColorOpacity
2203
- hex={colorOpacityValue.hex}
2204
- key={itemName || index}
2205
- name={itemName}
2206
- onValueChange={(nextValue, meta) => updateItem(index, nextValue, meta)}
2207
- opacity={colorOpacityValue.opacity}
2208
- showLabel={false}
2209
- />
2210
- );
2211
- });
2212
- }
2213
-
2214
- function renderStackedItemControl(item: unknown, index: number): React.ReactNode {
2215
- const itemControl = control.itemControl;
2216
- const itemName = getCollectionItemName(control, index);
2217
- const key = `${itemName || "item"}-${index}`;
2218
- const update = (nextValue: unknown, meta?: ControlChangeMeta) => {
2219
- updateItem(index, nextValue, meta);
2220
- };
2221
-
2222
- switch (itemType) {
2223
- case "checkbox":
2224
- return (
2225
- <Checkbox
2226
- checked={asBoolean(item)}
2227
- key={key}
2228
- name={itemName}
2229
- onCheckedChange={update}
2230
- showLabel={itemControl?.label !== false}
2231
- />
2232
- );
2233
- case "rangeInput": {
2234
- const rangeValue = asRangeInputValue(item);
2235
-
2236
- return (
2237
- <RangeInput
2238
- defaultValue={asRangeInputValue(itemControl?.defaultValue)}
2239
- end={rangeValue.end}
2240
- key={key}
2241
- name={itemName}
2242
- onValueChange={update}
2243
- start={rangeValue.start}
2244
- />
2245
- );
2246
- }
2247
- case "segmented":
2248
- return (
2249
- <Segmented
2250
- key={key}
2251
- name={itemName}
2252
- onValueChange={update}
2253
- options={itemControl?.options ?? []}
2254
- value={asString(item, itemControl?.options?.[0]?.value ?? "")}
2255
- variant={itemControl?.variant === "dots" ? "dots" : "default"}
2256
- />
2257
- );
2258
- case "select":
2259
- return (
2260
- <Select
2261
- key={key}
2262
- name={itemName}
2263
- onValueChange={update}
2264
- options={itemControl?.options ?? []}
2265
- value={asString(item, itemControl?.options?.[0]?.value ?? "")}
2266
- />
2267
- );
2268
- case "slider":
2269
- return (
2270
- <Slider
2271
- baseValue={asNumber(
2272
- itemControl?.defaultValue,
2273
- itemControl?.min ?? 0,
2274
- )}
2275
- key={key}
2276
- markerCount={
2277
- typeof itemControl?.markerCount === "number"
2278
- ? itemControl.markerCount
2279
- : undefined
2280
- }
2281
- max={itemControl?.max ?? 100}
2282
- min={itemControl?.min ?? 0}
2283
- name={itemName}
2284
- onValueChange={update}
2285
- step={itemControl?.step ?? 1}
2286
- unit={itemControl?.unit}
2287
- value={asNumber(
2288
- item,
2289
- asNumber(itemControl?.defaultValue, itemControl?.min ?? 0),
2290
- )}
2291
- variant={itemControl?.variant === "discrete" ? "discrete" : "continuous"}
2292
- />
2293
- );
2294
- case "switch":
2295
- return (
2296
- <Switch
2297
- checked={asBoolean(item)}
2298
- key={key}
2299
- name={itemName}
2300
- onCheckedChange={update}
2301
- showLabel={itemControl?.label !== false}
2302
- />
2303
- );
2304
- case "fontPicker":
2305
- return (
2306
- <FontPicker
2307
- defaultValue={asFontPickerValue(itemControl?.defaultValue)}
2308
- key={key}
2309
- name={itemName || "Font"}
2310
- onValueChange={update}
2311
- value={asFontPickerValue(item)}
2312
- />
2313
- );
2314
- case "text":
2315
- return (
2316
- <TextInput
2317
- commitOnBlur={itemControl?.commitMode === "setting"}
2318
- defaultValue={asString(itemControl?.defaultValue, asString(item))}
2319
- key={key}
2320
- name={itemName}
2321
- onValueChange={update}
2322
- value={asString(item)}
2323
- />
2324
- );
2325
- default:
2326
- return (
2327
- <TextInput
2328
- defaultValue={asString(itemControl?.defaultValue, asString(item))}
2329
- key={key}
2330
- name={itemName}
2331
- onValueChange={update}
2332
- value={asString(item)}
2333
- />
2334
- );
2335
- }
2336
- }
2337
-
2338
- function renderCollectionItems(): React.ReactNode {
2339
- if (itemType === "color") {
2340
- return renderColorItems();
2341
- }
2342
-
2343
- if (itemType === "colorOpacity") {
2344
- return renderColorOpacityItems();
2345
- }
2346
-
2347
- return items.map(renderStackedItemControl);
2348
- }
2349
-
2350
- return (
2351
- <div className="min-w-0 space-y-3" data-slot="collection-actions-control">
2352
- <CollectionActions
2353
- addLabel={control.addLabel ?? `Add ${getCollectionItemBaseLabel(control)}`}
2354
- canAdd={canAdd}
2355
- canRemove={canRemove}
2356
- name={name}
2357
- onAdd={addItem}
2358
- onRemove={removeItem}
2359
- removeLabel={
2360
- control.removeLabel ?? `Remove ${getCollectionItemBaseLabel(control)}`
2361
- }
2362
- />
2363
- <div className="min-w-0 space-y-4" data-slot="collection-actions-items">
2364
- {renderCollectionItems()}
2365
- </div>
2366
- </div>
2367
- );
2368
- }
2369
-
2370
- const visibleSections = resolvedControlsPanel.sections
2371
- .map((section) => ({
2372
- entries: getVisibleSectionEntries(section),
2373
- section,
2374
- }))
2375
- .filter(({ entries, section }) => isSectionVisible(section) && entries.length > 0);
2376
- const visibleControlsPanelSections = visibleSections.map(({ entries, section }) => ({
2377
- ...section,
2378
- controls: getControlsRecord(entries),
2379
- }));
2380
- const vectorControlCount = countControlsByType(visibleControlsPanelSections, "vector");
2381
- const vectorPadShape = vectorControlCount === 1 ? "square" : "compact";
2382
-
2383
- const panel = (
2384
- <Panel
2385
- className={cn(
2386
- "shrink-0",
2387
- placement === "frame" && "max-h-none",
2388
- className,
2389
- )}
2390
- collapsed={panelState?.collapsed}
2391
- contentTransitionSuppressionKey={
2392
- keyframeControlsEnabled ? "keyframes" : "plain"
2393
- }
2394
- key={controlsResetKey}
2395
- onCollapsedChange={(collapsed) => onPanelStateChange?.({ collapsed })}
2396
- onResetControls={() => dispatchCommand({ type: "controls.reset" })}
2397
- stickyFooterActive={footerActionProgressEntries.length > 0}
2398
- stickyFooterProgress={stickyFooterProgress}
2399
- title={resolvedControlsPanel.title}
2400
- >
2401
- {visibleSections.map(({ entries, section }, sectionIndex) => {
2402
- const visibleControls = getControlsRecord(entries);
2403
- const sectionHasOnlyColorFields = isColorOnlySection(entries);
2404
- const isRuntimeSetup = isRuntimeSetupSection({ entries, section });
2405
- const renderedSectionTitle = isRuntimeSetup
2406
- ? undefined
2407
- : getRenderedControlsSectionTitle(section);
2408
- const sectionSpacing = "default";
2409
- const sectionCollapseKey = getControlsPanelSectionCollapseKey({
2410
- entries,
2411
- section,
2412
- sectionIndex,
2413
- });
2414
- const isSectionCollapsible = !isRuntimeSetup && renderedSectionTitle !== undefined;
2415
- const isSectionCollapsed =
2416
- isSectionCollapsible && collapsedSectionByKey[sectionCollapseKey] === true;
2417
- const headerKeyframeEntry = getSectionHeaderKeyframeEntry(entries, section.title);
2418
- const headerKeyframeTarget = headerKeyframeEntry?.[1].target ?? null;
2419
- const headerKeyframeAction = headerKeyframeEntry
2420
- ? getSectionHeaderKeyframeAction(headerKeyframeEntry)
2421
- : null;
2422
- const sectionResetAction = isSectionCollapsible
2423
- ? getSectionResetAction({
2424
- sectionTitle:
2425
- typeof renderedSectionTitle === "string" ? renderedSectionTitle : "section",
2426
- targets: entries.map(([, control]) => control.target),
2427
- })
2428
- : null;
2429
- const sectionHeaderAction =
2430
- headerKeyframeAction || sectionResetAction ? (
2431
- <>
2432
- {headerKeyframeAction}
2433
- {sectionResetAction}
2434
- </>
2435
- ) : undefined;
2436
- const inlineLayoutGroupByControlId = getInlineLayoutGroupByControlId({
2437
- controlsById: visibleControls,
2438
- layoutGroups: section.layoutGroups,
2439
- });
2440
-
2441
- return (
2442
- <PanelSection
2443
- action={sectionHeaderAction}
2444
- actionGroup={section.actionGroup}
2445
- allowCompoundDividers={entries.length > 1}
2446
- collapsed={isSectionCollapsed}
2447
- collapsible={isSectionCollapsible}
2448
- key={`${section.title ?? "section"}-${sectionIndex}`}
2449
- onCollapsedChange={(collapsed) => {
2450
- setCollapsedSectionByKey((current) => {
2451
- const next = {
2452
- ...current,
2453
- [sectionCollapseKey]: collapsed,
2454
- };
2455
-
2456
- writeControlsPanelCollapsedSections(sectionCollapseStorageKey, next);
2457
-
2458
- return next;
2459
- });
2460
- }}
2461
- spacing={sectionSpacing}
2462
- title={renderedSectionTitle}
2463
- >
2464
- {renderControlLayoutGroups({
2465
- controlsById: visibleControls,
2466
- layoutGroups: section.layoutGroups,
2467
- renderedGroups: getControlRenderGroups(entries).map((group) => {
2468
- const ids = getControlRenderGroupIds(group);
2469
-
2470
- if (group.kind === "colorGroup") {
2471
- return {
2472
- ids,
2473
- node: renderColorGroup(
2474
- group.entries,
2475
- headerKeyframeTarget,
2476
- sectionHasOnlyColorFields,
2477
- ),
2478
- };
2479
- }
2480
-
2481
- const [id, rawControl] = group.entry;
2482
- const inlineLayoutGroup = inlineLayoutGroupByControlId.get(id);
2483
- const isInInlineLayoutGroup = Boolean(inlineLayoutGroup);
2484
- const disabled = isControlDisabled(rawControl);
2485
- const resolvedControl =
2486
- disabled === Boolean(rawControl.disabled)
2487
- ? rawControl
2488
- : { ...rawControl, disabled };
2489
- const shouldHideLabel = shouldHideToggleParameterControlLabel({
2490
- control: resolvedControl,
2491
- controlsById: visibleControls,
2492
- layoutGroup: inlineLayoutGroup,
2493
- });
2494
- const control =
2495
- shouldHideLabel && resolvedControl.label !== false
2496
- ? { ...resolvedControl, label: false }
2497
- : resolvedControl;
2498
- const name = getControlName(id, resolvedControl.label);
2499
- const value = getControlValue(control);
2500
- const usesHeaderKeyframeAction = control.target === headerKeyframeTarget;
2501
- const commitWithLabel =
2502
- (label: string) =>
2503
- (nextValue: unknown, meta?: ControlChangeMeta): void => {
2504
- setControlValue(control.target, nextValue, label, meta);
2505
- maybeUpsertControlKeyframe(control, label, nextValue);
2506
- };
2507
- const commit = commitWithLabel(name);
2508
- const node = (() => {
2509
- switch (control.type) {
2510
- case "actions": {
2511
- const actions = asActionSchemas(control.actions);
2512
-
2513
- return (
2514
- <Actions
2515
- actions={actions.map((action) => ({
2516
- icon: action.icon,
2517
- label: action.label,
2518
- value: action.value,
2519
- }))}
2520
- key={id}
2521
- name={name}
2522
- onAction={(actionValue) => {
2523
- const action = actions.find((item) => item.value === actionValue);
2524
-
2525
- if (action) {
2526
- runAction(action);
2527
- }
2528
- }}
2529
- showLabel={control.label !== false}
2530
- />
2531
- );
2532
- }
2533
-
2534
- case "aspectRatio":
2535
- return (
2536
- <CanvasAspectRatioControl
2537
- defaultValue={control.defaultValue}
2538
- key={id}
2539
- name={name}
2540
- onValueChange={commit}
2541
- value={value}
2542
- />
2543
- );
2544
-
2545
- case "anchorGrid":
2546
- return withKeyframeLabelAction({
2547
- children: (
2548
- <AnchorGrid
2549
- key={id}
2550
- name={name}
2551
- onValueChange={commit}
2552
- value={
2553
- asString(value, "center") as React.ComponentProps<
2554
- typeof AnchorGrid
2555
- >["value"]
2556
- }
2557
- />
2558
- ),
2559
- control,
2560
- disableAction: usesHeaderKeyframeAction,
2561
- name,
2562
- providerKey: id,
2563
- value,
2564
- });
2565
-
2566
- case "channelMixer": {
2567
- const channelMixerName = name;
2568
-
2569
- return withKeyframeLabelAction({
2570
- children: (
2571
- <ChannelMixer
2572
- key={id}
2573
- name={channelMixerName}
2574
- onValueChange={(nextValue) =>
2575
- commitWithLabel(channelMixerName)(nextValue.values)
2576
- }
2577
- values={
2578
- isRecord(value) ? (value as ChannelMixerValues) : defaultChannelMixerValues
2579
- }
2580
- />
2581
- ),
2582
- control,
2583
- disableAction: false,
2584
- labelActionName: channelMixerName,
2585
- name,
2586
- providerKey: id,
2587
- value,
2588
- });
2589
- }
2590
-
2591
- case "checkbox":
2592
- return (
2593
- <Checkbox
2594
- checked={asBoolean(value)}
2595
- key={id}
2596
- name={name}
2597
- onCheckedChange={commit}
2598
- showLabel={control.label !== false}
2599
- />
2600
- );
2601
-
2602
- case "code":
2603
- return withKeyframeLabelAction({
2604
- children: (
2605
- <CodeTextarea
2606
- defaultValue={asString(control.defaultValue, asString(value))}
2607
- key={id}
2608
- name={name}
2609
- onValueChange={commit}
2610
- showLabel={control.label !== false}
2611
- value={asString(value)}
2612
- />
2613
- ),
2614
- control,
2615
- disableAction: usesHeaderKeyframeAction,
2616
- name,
2617
- providerKey: id,
2618
- value,
2619
- });
2620
-
2621
- case "color": {
2622
- const colorValue = asColorValue(value);
2623
-
2624
- return withKeyframeLabelAction({
2625
- children: (
2626
- <Color
2627
- hex={colorValue.hex}
2628
- key={id}
2629
- name={name}
2630
- onValueChange={commit}
2631
- showLabel={shouldShowColorFieldLabel({
2632
- control,
2633
- sectionHasOnlyColorFields,
2634
- })}
2635
- />
2636
- ),
2637
- control,
2638
- disableAction: usesHeaderKeyframeAction,
2639
- name,
2640
- providerKey: id,
2641
- value,
2642
- });
2643
- }
2644
-
2645
- case "colorOpacity": {
2646
- const colorOpacityValue = asColorOpacityValue(value);
2647
-
2648
- return withKeyframeLabelAction({
2649
- children: (
2650
- <ColorOpacity
2651
- hex={colorOpacityValue.hex}
2652
- key={id}
2653
- name={name}
2654
- onValueChange={commit}
2655
- opacity={colorOpacityValue.opacity}
2656
- showLabel={shouldShowColorFieldLabel({
2657
- control,
2658
- sectionHasOnlyColorFields,
2659
- })}
2660
- />
2661
- ),
2662
- control,
2663
- disableAction: usesHeaderKeyframeAction,
2664
- name,
2665
- providerKey: id,
2666
- value: colorOpacityValue,
2667
- });
2668
- }
2669
-
2670
- case "collectionActions":
2671
- return renderCollectionActionsControl({ control, name, value });
2672
-
2673
- case "curves": {
2674
- const curvesName = control.label === false ? "Curves" : name;
2675
-
2676
- return withKeyframeLabelAction({
2677
- children: (
2678
- <Curves
2679
- interpolation={asCurveInterpolation(control.interpolation)}
2680
- key={id}
2681
- name={curvesName}
2682
- onValueChange={commitWithLabel(curvesName)}
2683
- variant={control.variant === "single" ? "single" : "rgb"}
2684
- {...(isRecord(value) ? value : {})}
2685
- />
2686
- ),
2687
- control,
2688
- disableAction: false,
2689
- labelActionName: curvesName,
2690
- name: curvesName,
2691
- providerKey: id,
2692
- value,
2693
- });
2694
- }
2695
-
2696
- case "fileDrop": {
2697
- const assetKind = control.assetKind === "file" ? "file" : "image";
2698
- const previewMediaAssets = state.schema.panels.layers
2699
- ? []
2700
- : state.mediaAssets.filter(
2701
- (asset) =>
2702
- asset.sourceTarget === undefined ||
2703
- asset.sourceTarget === control.target,
2704
- );
2705
- const previewMediaAsset = previewMediaAssets[0];
2706
- const previews = previewMediaAssets.map((asset): FileDropPreview => ({
2707
- alt: asset.fileName,
2708
- assetKind: asset.assetKind ?? "image",
2709
- fileName: asset.fileName,
2710
- id: asset.id,
2711
- size: asset.size,
2712
- src: asset.dataUrl,
2713
- transform: asset.transform,
2714
- }));
2715
- const previewMediaIds = previewMediaAssets.map((asset) => asset.id);
2716
- const importFile = (file: File, replaceExisting: boolean): void => {
2717
- if (assetKind === "file") {
2718
- void readImportedFile(file).then((importedFile) => {
2719
- if (!importedFile) {
2720
- return;
2721
- }
2722
-
2723
- dispatchCommand({
2724
- asset: {
2725
- assetKind: "file",
2726
- dataUrl: importedFile.dataUrl,
2727
- fileName: file.name,
2728
- mimeType: file.type || "application/octet-stream",
2729
- position: { x: 0, y: 0 },
2730
- sourceTarget: control.target,
2731
- },
2732
- replaceExisting,
2733
- type: "media.import",
2734
- });
2735
- });
2736
- return;
2737
- }
2738
-
2739
- if (!isToolcraftImageFile(file)) {
2740
- return;
2741
- }
2742
-
2743
- void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
2744
- if (!importedImage) {
2745
- return;
2746
- }
2747
-
2748
- dispatchCommand({
2749
- asset: {
2750
- assetKind: "image",
2751
- dataUrl: importedImage.dataUrl,
2752
- fileName: file.name,
2753
- mimeType: file.type || "image/*",
2754
- position: { x: 0, y: 0 },
2755
- size: importedImage.size,
2756
- sourceTarget: control.target,
2757
- },
2758
- replaceExisting,
2759
- type: "media.import",
2760
- });
2761
- });
2762
- };
2763
-
2764
- return (
2765
- <FileDrop
2766
- accept={
2767
- control.accept ??
2768
- (assetKind === "file" ? "" : "PNG, JPEG, GIF, SVG, WebP")
2769
- }
2770
- assetKind={assetKind}
2771
- multiple={control.multiple}
2772
- key={id}
2773
- onClear={
2774
- previewMediaAsset
2775
- ? () => {
2776
- dispatchCommand({
2777
- mediaId: previewMediaAsset.id,
2778
- type: "media.delete",
2779
- });
2780
- }
2781
- : undefined
2782
- }
2783
- onFilesSelect={(files) => {
2784
- files.forEach((file) => importFile(file, false));
2785
- }}
2786
- onFileSelect={(file) => importFile(file, true)}
2787
- onPreviewRemove={(item) => {
2788
- if (!item.id) {
2789
- return;
2790
- }
2791
-
2792
- dispatchCommand({
2793
- mediaId: item.id,
2794
- type: "media.delete",
2795
- });
2796
- }}
2797
- onPreviewReorder={(orderedPreviews) => {
2798
- const orderedPreviewIds = orderedPreviews.flatMap((item) =>
2799
- item.id ? [item.id] : [],
2800
- );
2801
-
2802
- if (orderedPreviewIds.length !== previewMediaIds.length) {
2803
- return;
2804
- }
2805
-
2806
- const previewIdSet = new Set(previewMediaIds);
2807
- let orderedPreviewIndex = 0;
2808
- const mediaIds = state.mediaAssets.map((asset) => {
2809
- if (!previewIdSet.has(asset.id)) {
2810
- return asset.id;
2811
- }
2812
-
2813
- const nextId = orderedPreviewIds[orderedPreviewIndex];
2814
- orderedPreviewIndex += 1;
2815
- return nextId ?? asset.id;
2816
- });
2817
-
2818
- dispatchCommand({
2819
- mediaIds,
2820
- type: "media.reorder",
2821
- });
2822
- }}
2823
- onPreviewTransform={(item, operation) => {
2824
- if (!item.id) {
2825
- return;
2826
- }
2827
-
2828
- dispatchCommand({
2829
- mediaId: item.id,
2830
- operation,
2831
- type: "media.transform",
2832
- });
2833
- }}
2834
- preview={
2835
- previewMediaAsset
2836
- ? {
2837
- id: previewMediaAsset.id,
2838
- alt: previewMediaAsset.fileName,
2839
- size: previewMediaAsset.size,
2840
- src: previewMediaAsset.dataUrl,
2841
- transform: previewMediaAsset.transform,
2842
- }
2843
- : undefined
2844
- }
2845
- previews={previews}
2846
- />
2847
- );
2848
- }
2849
-
2850
- case "gradient": {
2851
- const gradientValue = asGradientValue(value);
2852
-
2853
- return withKeyframeLabelAction({
2854
- children: (
2855
- <Gradient
2856
- angle={gradientValue.angle}
2857
- gradientType={gradientValue.gradientType}
2858
- key={id}
2859
- onValueChange={commit}
2860
- stops={gradientValue.stops}
2861
- />
2862
- ),
2863
- control,
2864
- disableAction: usesHeaderKeyframeAction,
2865
- name,
2866
- providerKey: id,
2867
- value,
2868
- });
2869
- }
2870
-
2871
- case "fontPicker": {
2872
- const fontPickerValue = asFontPickerValue(value);
2873
-
2874
- return withKeyframeLabelAction({
2875
- children: (
2876
- <FontPicker
2877
- defaultValue={asFontPickerValue(control.defaultValue)}
2878
- disabled={control.disabled}
2879
- key={id}
2880
- name={name}
2881
- onValueChange={commit}
2882
- value={fontPickerValue}
2883
- />
2884
- ),
2885
- control,
2886
- disableAction: usesHeaderKeyframeAction,
2887
- name,
2888
- providerKey: id,
2889
- value: fontPickerValue,
2890
- });
2891
- }
2892
-
2893
- case "imagePicker":
2894
- return (
2895
- <ImagePicker
2896
- items={control.items as readonly ImagePickerItem[] | undefined}
2897
- key={id}
2898
- name={name}
2899
- onValueChange={commit}
2900
- value={asString(value, control.items?.[0]?.value ?? "")}
2901
- />
2902
- );
2903
-
2904
- case "palette":
2905
- return withKeyframeLabelAction({
2906
- children: (
2907
- <Palette
2908
- defaultValue={
2909
- isRecord(control.defaultValue)
2910
- ? (control.defaultValue as React.ComponentProps<
2911
- typeof Palette
2912
- >["defaultValue"])
2913
- : undefined
2914
- }
2915
- key={id}
2916
- onValueChange={commit}
2917
- value={
2918
- isRecord(value)
2919
- ? (value as React.ComponentProps<typeof Palette>["value"])
2920
- : undefined
2921
- }
2922
- />
2923
- ),
2924
- control,
2925
- disableAction: usesHeaderKeyframeAction,
2926
- name,
2927
- providerKey: id,
2928
- value,
2929
- });
2930
-
2931
- case "panelActions": {
2932
- const actions = asActionSchemas(control.actions);
2933
-
2934
- return (
2935
- <PanelActions
2936
- actions={actions.map((action) => ({
2937
- icon: getPanelActionIcon(action),
2938
- name: getActionLabel(action),
2939
- value: action.value,
2940
- variant: getPanelActionButtonVariant(action.variant),
2941
- }))}
2942
- key={id}
2943
- onAction={(actionValue) => {
2944
- const action = actions.find((item) => item.value === actionValue);
2945
-
2946
- if (action) {
2947
- runAction(action, { trackFooterPending: true });
2948
- }
2949
- }}
2950
- />
2951
- );
2952
- }
2953
-
2954
- case "rangeInput": {
2955
- const rangeValue = asRangeInputValue(value);
2956
-
2957
- return withKeyframeLabelAction({
2958
- children: (
2959
- <RangeInput
2960
- defaultValue={asRangeInputValue(control.defaultValue)}
2961
- end={rangeValue.end}
2962
- key={id}
2963
- name={name}
2964
- onValueChange={commit}
2965
- showLabel={control.label !== false}
2966
- start={rangeValue.start}
2967
- />
2968
- ),
2969
- control,
2970
- disableAction: usesHeaderKeyframeAction,
2971
- name,
2972
- providerKey: id,
2973
- value,
2974
- });
2975
- }
2976
-
2977
- case "rangeSlider":
2978
- return withKeyframeLabelAction({
2979
- children: (
2980
- <RangeSlider
2981
- baseValue={asNumberArray(control.defaultValue, [])}
2982
- disabled={control.disabled}
2983
- markerCount={getControlMarkerCount(control)}
2984
- max={control.max ?? 100}
2985
- min={control.min ?? 0}
2986
- key={id}
2987
- name={name}
2988
- onValueChange={commit}
2989
- step={control.step ?? 0.1}
2990
- unit={control.unit}
2991
- value={asNumberArray(value, [control.min ?? 0, control.max ?? 100])}
2992
- valueLabel={control.valueLabel}
2993
- variant={
2994
- control.variant === "discrete" ? "discrete" : "continuous"
2995
- }
2996
- />
2997
- ),
2998
- control,
2999
- disableAction: usesHeaderKeyframeAction,
3000
- name,
3001
- providerKey: id,
3002
- value,
3003
- });
3004
-
3005
- case "segmented":
3006
- return withKeyframeLabelAction({
3007
- children: (
3008
- <Segmented
3009
- key={id}
3010
- name={name}
3011
- onValueChange={commit}
3012
- options={control.options ?? []}
3013
- value={asString(value, control.options?.[0]?.value ?? "")}
3014
- variant={control.variant === "dots" ? "dots" : "default"}
3015
- />
3016
- ),
3017
- control,
3018
- disableAction: usesHeaderKeyframeAction,
3019
- name,
3020
- providerKey: id,
3021
- value,
3022
- });
3023
-
3024
- case "select":
3025
- return (
3026
- <Select
3027
- key={id}
3028
- name={name}
3029
- onValueChange={commit}
3030
- options={control.options ?? []}
3031
- showLabel={control.label !== false}
3032
- value={asString(value, control.options?.[0]?.value ?? "")}
3033
- />
3034
- );
3035
-
3036
- case "settingsTransfer":
3037
- return (
3038
- <PanelActions
3039
- actions={[
3040
- {
3041
- icon: "upload-simple",
3042
- name: "Export Settings",
3043
- onClick: () => downloadToolcraftSettings(state),
3044
- variant: "outline",
3045
- },
3046
- {
3047
- icon: "download-simple",
3048
- name: "Import Settings",
3049
- onClick: () => {
3050
- void importToolcraftSettings({ dispatch, state });
3051
- },
3052
- variant: "outline",
3053
- },
3054
- ]}
3055
- key={id}
3056
- columns={2}
3057
- />
3058
- );
3059
-
3060
- case "slider":
3061
- return withKeyframeLabelAction({
3062
- children: (
3063
- <Slider
3064
- baseValue={asNumber(control.defaultValue, control.min ?? 0)}
3065
- disabled={control.disabled}
3066
- key={id}
3067
- markerCount={getControlMarkerCount(control)}
3068
- max={control.max ?? 100}
3069
- min={control.min ?? 0}
3070
- name={name}
3071
- onValueChange={commit}
3072
- step={control.step ?? 1}
3073
- unit={control.unit}
3074
- value={asNumber(
3075
- value,
3076
- asNumber(control.defaultValue, control.min ?? 0),
3077
- )}
3078
- valueLabel={control.valueLabel}
3079
- variant={
3080
- control.variant === "discrete" ? "discrete" : "continuous"
3081
- }
3082
- />
3083
- ),
3084
- control,
3085
- disableAction: usesHeaderKeyframeAction,
3086
- name,
3087
- providerKey: id,
3088
- value,
3089
- });
3090
-
3091
- case "switch":
3092
- return (
3093
- <Switch
3094
- checked={asBoolean(value)}
3095
- key={id}
3096
- name={name}
3097
- onCheckedChange={commit}
3098
- showLabel={control.label !== false}
3099
- />
3100
- );
3101
-
3102
- case "text":
3103
- return withKeyframeLabelAction({
3104
- children: (
3105
- <TextInput
3106
- commitOnBlur={shouldCommitTextControlOnBlur(control)}
3107
- defaultValue={asString(control.defaultValue, asString(value))}
3108
- key={id}
3109
- name={name}
3110
- onValueChange={commit}
3111
- showLabel={control.label !== false}
3112
- value={asString(value)}
3113
- />
3114
- ),
3115
- control,
3116
- disableAction: usesHeaderKeyframeAction,
3117
- name,
3118
- providerKey: id,
3119
- value,
3120
- });
3121
-
3122
- case "vector": {
3123
- const vectorValue = asVectorValue(value);
3124
- const padVariant = asVectorPadVariant(control.variant);
3125
-
3126
- return withKeyframeLabelAction({
3127
- children: (
3128
- <Vector
3129
- defaultValue={asVectorValue(control.defaultValue)}
3130
- key={id}
3131
- name={name}
3132
- onValueChange={commit}
3133
- padCoordinateMode={asVectorPadCoordinateMode(
3134
- control.coordinateMode,
3135
- padVariant,
3136
- )}
3137
- padShape={vectorPadShape}
3138
- padVariant={padVariant}
3139
- x={vectorValue.x}
3140
- xLabel={control.xLabel}
3141
- y={vectorValue.y}
3142
- yLabel={control.yLabel}
3143
- />
3144
- ),
3145
- control,
3146
- disableAction: usesHeaderKeyframeAction,
3147
- name,
3148
- providerKey: id,
3149
- value,
3150
- });
3151
- }
3152
-
3153
- default: {
3154
- const CustomControl = controlRenderers?.[control.type];
3155
-
3156
- if (!CustomControl) {
3157
- return null;
3158
- }
3159
-
3160
- return withKeyframeLabelAction({
3161
- children: (
3162
- <React.Fragment key={id}>
3163
- {CustomControl({
3164
- control,
3165
- controlId: id,
3166
- dispatch,
3167
- keyframeAction: getKeyframeLabelAction(control, name, value),
3168
- name,
3169
- setValue: commit,
3170
- state,
3171
- value,
3172
- })}
3173
- </React.Fragment>
3174
- ),
3175
- control,
3176
- disableAction: usesHeaderKeyframeAction,
3177
- name,
3178
- providerKey: id,
3179
- value,
3180
- });
3181
- }
3182
- }
3183
- })();
3184
-
3185
- return {
3186
- ids,
3187
- node: withCompoundControlSectionDivider({
3188
- children: withControlLabelHelp({
3189
- children: node,
3190
- control,
3191
- label: name,
3192
- providerKey: id,
3193
- sectionTitle: section.title,
3194
- }),
3195
- control,
3196
- }),
3197
- };
3198
- }),
3199
- })}
3200
- </PanelSection>
3201
- );
3202
- })}
3203
- </Panel>
3204
- );
3205
-
3206
- if (placement === "surface") {
3207
- return panel;
3208
- }
3209
-
3210
- return (
3211
- <PanelContainer
3212
- onPanelStateChange={onPanelStateChange}
3213
- panelState={panelState}
3214
- panelType="controls"
3215
- placement={placement}
3216
- >
3217
- {panel}
3218
- </PanelContainer>
3219
- );
3220
- }