@pixel-point/toolcraft 0.0.16 → 0.0.18

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 (313) hide show
  1. package/package.json +1 -1
  2. package/src/generate-performance-iteration-execution.test.mjs +2 -2
  3. package/src/generate-test-demand-only-lifecycle.mjs +14 -3
  4. package/src/generate-test-dependency-sandbox.mjs +42 -5
  5. package/src/generate-test-doc-assertions.mjs +109 -69
  6. package/src/generate-test-doc-assertions.test.mjs +65 -0
  7. package/src/generate-test-doc-policy-assertions.mjs +92 -0
  8. package/src/generate-test-file-assertions.mjs +36 -118
  9. package/src/generate-test-output-assertions.mjs +95 -0
  10. package/src/generate-test-package-assertions.mjs +6 -0
  11. package/src/generate-test-performance-boundary-assertions.mjs +45 -0
  12. package/src/generate-test-product-fixture.mjs +1 -0
  13. package/src/generate-test-product-fixture.test.mjs +18 -0
  14. package/src/generate-test-product-iteration.mjs +1 -0
  15. package/src/generate-test-product-iteration.test.mjs +18 -0
  16. package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
  17. package/src/generate-test-standalone-assertions.mjs +10 -0
  18. package/src/generate-test-standalone-delivery-stages.mjs +14 -6
  19. package/src/generate.test.mjs +14 -0
  20. package/templates/runtime/contracts/component-contracts.choices.test.ts +59 -12
  21. package/templates/runtime/contracts/component-contracts.choices.ts +58 -12
  22. package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
  23. package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
  24. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
  25. package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
  26. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +21 -6
  27. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
  28. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +4 -1
  29. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +4 -1
  30. package/templates/runtime/contracts/component-contracts.runtime.ts +15 -10
  31. package/templates/runtime/contracts/component-contracts.test.ts +2 -0
  32. package/templates/runtime/contracts/decision-contracts.test.ts +43 -4
  33. package/templates/runtime/contracts/decision-contracts.ts +10 -10
  34. package/templates/runtime/contracts/performance-verification-policy.test.ts +59 -6
  35. package/templates/runtime/contracts/performance-verification-policy.ts +5 -2
  36. package/templates/runtime/export/artifact-download.test.ts +71 -0
  37. package/templates/runtime/export/artifact-download.ts +80 -0
  38. package/templates/runtime/export/artifact-export-request.ts +23 -0
  39. package/templates/runtime/export/artifact-export-settings.test.ts +75 -0
  40. package/templates/runtime/export/artifact-export-settings.ts +96 -0
  41. package/templates/runtime/export/artifact-frame-renderer.test.ts +107 -0
  42. package/templates/runtime/export/artifact-frame-renderer.ts +96 -0
  43. package/templates/runtime/export/artifact-frame-state.test.ts +54 -0
  44. package/templates/runtime/export/artifact-frame-state.ts +44 -0
  45. package/templates/runtime/export/artifact-scene-frame.test.ts +84 -0
  46. package/templates/runtime/export/artifact-scene-frame.ts +118 -0
  47. package/templates/runtime/export/export-background.ts +46 -0
  48. package/templates/runtime/export/export-error.ts +43 -0
  49. package/templates/runtime/export/export.test.ts +49 -226
  50. package/templates/runtime/export/image-artifact-export.test.ts +111 -0
  51. package/templates/runtime/export/image-artifact-export.ts +145 -0
  52. package/templates/runtime/export/index.ts +44 -3
  53. package/templates/runtime/export/product-export-renderer.test.ts +77 -0
  54. package/templates/runtime/export/product-export-renderer.ts +63 -0
  55. package/templates/runtime/export/video-artifact-export.test.ts +166 -0
  56. package/templates/runtime/export/video-artifact-export.ts +164 -0
  57. package/templates/runtime/export/video-encoding-backend.ts +124 -0
  58. package/templates/runtime/export/video-encoding-policy.test.ts +68 -0
  59. package/templates/runtime/export/video-encoding-policy.ts +86 -0
  60. package/templates/runtime/export/video-frame-schedule.test.ts +58 -0
  61. package/templates/runtime/export/video-frame-schedule.ts +42 -0
  62. package/templates/runtime/index.ts +3 -0
  63. package/templates/runtime/model-import/model-import-limits.ts +15 -4
  64. package/templates/runtime/react/app-shell/runtime-public-api-runtime.test.tsx +291 -0
  65. package/templates/runtime/react/app-shell/runtime-public-api-test-support.ts +21 -0
  66. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +15 -304
  67. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +41 -27
  68. package/templates/runtime/react/app-shell/toolcraft-app.tsx +39 -11
  69. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.test.tsx +16 -57
  70. package/templates/runtime/react/app-shell/toolcraft-source-asset-context.tsx +75 -434
  71. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.test.ts +194 -0
  72. package/templates/runtime/react/app-shell/toolcraft-source-asset-owner.ts +227 -0
  73. package/templates/runtime/react/app-shell/toolcraft-source-asset-provider-ownership.test.tsx +243 -0
  74. package/templates/runtime/react/app-shell/toolcraft-source-asset-test-support.ts +51 -0
  75. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.test.ts +138 -0
  76. package/templates/runtime/react/app-shell/toolcraft-unavailable-resource-proof.ts +378 -0
  77. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
  78. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
  79. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
  80. package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
  81. package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
  82. package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
  83. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
  84. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
  85. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
  86. package/templates/runtime/react/controls-panel/__tests__/controls-panel.footer-actions.test.tsx +7 -12
  87. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +18 -84
  88. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
  89. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
  90. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +43 -87
  91. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +68 -0
  92. package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
  93. package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
  94. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
  95. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
  96. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
  97. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
  98. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
  99. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
  100. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
  101. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
  102. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
  103. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
  104. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
  105. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
  106. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
  107. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
  108. package/templates/runtime/react/index.ts +4 -0
  109. package/templates/runtime/react/model-rendering/model-export.ts +1 -1
  110. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
  111. package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
  112. package/templates/runtime/react/panel-host/panel-host-runtime-placement.test.tsx +30 -0
  113. package/templates/runtime/react/panel-host/panel-host.tsx +4 -0
  114. package/templates/runtime/scene/scene-bounds.test.ts +23 -0
  115. package/templates/runtime/scene/scene-bounds.ts +32 -19
  116. package/templates/runtime/schema/app-capabilities.test.ts +4 -0
  117. package/templates/runtime/schema/canvas-render-scale.ts +62 -0
  118. package/templates/runtime/schema/collection-item-controls.ts +29 -0
  119. package/templates/runtime/schema/control-applicability.test.ts +127 -0
  120. package/templates/runtime/schema/control-applicability.ts +433 -0
  121. package/templates/runtime/schema/control-schema-normalization.ts +177 -11
  122. package/templates/runtime/schema/controls-panel-actions.ts +2 -2
  123. package/templates/runtime/schema/controls-panel-section-id.test.ts +1 -1
  124. package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
  125. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +62 -3
  126. package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
  127. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
  128. package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
  129. package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
  130. package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
  131. package/templates/runtime/schema/define-toolcraft.setup-background.test.ts +1 -1
  132. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
  133. package/templates/runtime/schema/define-toolcraft.setup-render-scale-timeline.test.ts +71 -3
  134. package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
  135. package/templates/runtime/schema/define-toolcraft.ts +1 -1
  136. package/templates/runtime/schema/runtime-setup-section.ts +24 -5
  137. package/templates/runtime/schema/schema-resolvers.ts +0 -56
  138. package/templates/runtime/schema/types.ts +72 -17
  139. package/templates/runtime/state/canvas-background-state.ts +6 -0
  140. package/templates/runtime/testing/performance-render-plan-integration.test.ts +64 -0
  141. package/templates/runtime/testing/performance-renderer-pipeline-validation.ts +18 -1
  142. package/templates/starter/AGENTS.md +18 -15
  143. package/templates/starter/NOTICE.md +6 -0
  144. package/templates/starter/docs/toolcraft/README.md +2 -1
  145. package/templates/starter/docs/toolcraft/acceptance-testing.md +23 -35
  146. package/templates/starter/docs/toolcraft/agent-worklog.md +18 -3
  147. package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -5
  148. package/templates/starter/docs/toolcraft/component-rules.md +8 -6
  149. package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
  150. package/templates/starter/docs/toolcraft/core/layout.md +7 -7
  151. package/templates/starter/docs/toolcraft/core/media-upload.md +4 -2
  152. package/templates/starter/docs/toolcraft/core/performance.md +14 -3
  153. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +16 -3
  154. package/templates/starter/docs/toolcraft/core/setup-export.md +49 -19
  155. package/templates/starter/docs/toolcraft/core/timeline-animation.md +4 -4
  156. package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
  157. package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
  158. package/templates/starter/docs/toolcraft/performance.md +20 -6
  159. package/templates/starter/docs/toolcraft/renderer-technique.md +5 -1
  160. package/templates/starter/docs/toolcraft/schema-reference.md +23 -25
  161. package/templates/starter/docs/toolcraft/workflow.md +7 -2
  162. package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
  163. package/templates/starter/e2e/app-browser-control-coverage.spec.ts +33 -0
  164. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +50 -3
  165. package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
  166. package/templates/starter/e2e/app-browser-render-scale-evidence.spec.ts +60 -6
  167. package/templates/starter/e2e/app-browser-render-scale-requirements.spec.ts +77 -1
  168. package/templates/starter/e2e/app-browser-runtime-provenance.spec.ts +8 -0
  169. package/templates/starter/e2e/app-browser-runtime-reporter.spec.ts +35 -0
  170. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +5 -34
  171. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -16
  172. package/templates/starter/e2e/app-browser-shell.spec.ts +13 -5
  173. package/templates/starter/e2e/app-performance-path-adapters.ts +8 -1
  174. package/templates/starter/e2e/app-performance.spec.ts +16 -9
  175. package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
  176. package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
  177. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +29 -341
  178. package/templates/starter/e2e/browser-infinity-canvas-unavailable-image-evidence.ts +370 -0
  179. package/templates/starter/e2e/browser-media-export-evidence.spec.ts +77 -0
  180. package/templates/starter/e2e/browser-media-export-evidence.ts +208 -0
  181. package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
  182. package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
  183. package/templates/starter/e2e/browser-render-scale-evidence.ts +37 -29
  184. package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
  185. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +13 -3
  186. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +72 -35
  187. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
  188. package/templates/starter/e2e/browser-standard-timeline-evidence.ts +106 -0
  189. package/templates/starter/e2e/decoded-pixel-observation.spec.ts +74 -0
  190. package/templates/starter/e2e/decoded-pixel-observation.ts +227 -0
  191. package/templates/starter/e2e/export-artifact-helpers.ts +206 -25
  192. package/templates/starter/e2e/image-artifact-inspection.spec.ts +41 -0
  193. package/templates/starter/e2e/image-artifact-inspection.ts +99 -0
  194. package/templates/starter/e2e/performance-canvas-helpers.ts +22 -21
  195. package/templates/starter/e2e/performance-canvas-quality-guard.spec.ts +312 -0
  196. package/templates/starter/e2e/performance-canvas-quality-guard.ts +196 -0
  197. package/templates/starter/e2e/performance-canvas-quality-observer.ts +351 -0
  198. package/templates/starter/e2e/performance-control-layout-helpers.ts +2 -2
  199. package/templates/starter/e2e/performance-path-adapter-contract.ts +4 -4
  200. package/templates/starter/e2e/performance-path-helpers.spec.ts +203 -70
  201. package/templates/starter/e2e/performance-path-helpers.ts +210 -91
  202. package/templates/starter/e2e/performance-pipeline-evidence-continuity.spec.ts +76 -0
  203. package/templates/starter/e2e/performance-pipeline-evidence-test-fixtures.ts +11 -2
  204. package/templates/starter/e2e/performance-pipeline-invariants.ts +47 -4
  205. package/templates/starter/e2e/performance-render-scale-runner.spec.ts +140 -38
  206. package/templates/starter/e2e/video-artifact-inspection.spec.ts +64 -0
  207. package/templates/starter/e2e/video-artifact-inspection.ts +281 -0
  208. package/templates/starter/gitignore +1 -0
  209. package/templates/starter/package.json +1 -0
  210. package/templates/starter/scripts/check-toolcraft-docs.mjs +17 -0
  211. package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
  212. package/templates/starter/scripts/toolcraft-delivery-plan-authority.test.mjs +60 -0
  213. package/templates/starter/scripts/toolcraft-delivery-plan-errors.test.mjs +7 -2
  214. package/templates/starter/scripts/toolcraft-delivery-plan.mjs +7 -3
  215. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +95 -103
  216. package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
  217. package/templates/starter/scripts/toolcraft-product-boundary.mjs +12 -0
  218. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -26
  219. package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
  220. package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
  221. package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
  222. package/templates/starter/scripts/toolcraft-product-evidence-boundary.test.mjs +85 -20
  223. package/templates/starter/scripts/toolcraft-product-evidence-import-policy.mjs +43 -0
  224. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +157 -0
  225. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
  226. package/templates/starter/scripts/toolcraft-typescript-analysis.d.mts +73 -0
  227. package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
  228. package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
  229. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade-test-helpers.mjs +318 -0
  230. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +157 -206
  231. package/templates/starter/scripts/toolcraft-verification-inventory.mjs +1 -0
  232. package/templates/starter/scripts/toolcraft-verification-receipt.test.mjs +51 -0
  233. package/templates/starter/scripts/toolcraft-vite-env-activation.test.mjs +128 -59
  234. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
  235. package/templates/starter/src/app/acceptance/conditions.ts +1 -1
  236. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
  237. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
  238. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
  239. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
  240. package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
  241. package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
  242. package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
  243. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
  244. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
  245. package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
  246. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
  247. package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
  248. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
  249. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
  250. package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
  251. package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
  252. package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
  253. package/templates/starter/src/app/acceptance/export-artifact-coverage.ts +113 -0
  254. package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
  255. package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
  256. package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
  257. package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
  258. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
  259. package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
  260. package/templates/starter/src/app/acceptance/output-export.ts +35 -12
  261. package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
  262. package/templates/starter/src/app/acceptance/types.ts +30 -8
  263. package/templates/starter/src/app/acceptance/validate-coverage.ts +20 -2
  264. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
  265. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
  266. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
  267. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
  268. package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
  269. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
  270. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
  271. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
  272. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
  273. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
  274. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
  275. package/templates/starter/src/app/starter-acceptance.export-artifact.test.ts +131 -0
  276. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
  277. package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
  278. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
  279. package/templates/starter/src/app/starter-acceptance.keyframes.test.ts +14 -4
  280. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
  281. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  282. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
  283. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +46 -1
  284. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
  285. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
  286. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
  287. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
  288. package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
  289. package/templates/starter/src/app/starter-acceptance.ts +13 -2
  290. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
  291. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
  292. package/templates/starter/src/app/starter-performance-test-utils.ts +103 -0
  293. package/templates/starter/src/app/starter-performance.gates.test.ts +231 -278
  294. package/templates/starter/src/app/starter-performance.lifecycle.test.ts +261 -0
  295. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +4 -2
  296. package/templates/starter/src/routes/index.tsx +2 -0
  297. package/templates/ui/components/composites/tabs.tsx +2 -2
  298. package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
  299. package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
  300. package/templates/ui/components/controls/collection-actions/index.ts +2 -0
  301. package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
  302. package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
  303. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
  304. package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
  305. package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
  306. package/templates/ui/components/controls/file-drop/index.ts +1 -0
  307. package/templates/ui/components/controls/index.ts +3 -0
  308. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
  309. package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
  310. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
  311. package/templates/runtime/export/export.ts +0 -166
  312. package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
  313. package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
@@ -3,10 +3,12 @@ import type * as React from "react";
3
3
  import { vi } from "vitest";
4
4
 
5
5
  import { defineToolcraft } from "../../schema/define-toolcraft";
6
+ import type { ToolcraftProductSceneBoundsProvider } from "../../scene";
6
7
  import type { ToolcraftInitialState } from "../../state/types";
7
8
  import { ToolcraftRoot } from "../app-shell/toolcraft-root";
8
9
  import { useToolcraft } from "../app-shell/use-toolcraft";
9
10
  import { CanvasShell } from "./canvas-shell";
11
+ import { ToolcraftProductSceneBoundsBoundary } from "./product-scene-surface";
10
12
 
11
13
  export function createSchema() {
12
14
  return defineToolcraft({
@@ -121,7 +123,9 @@ type RenderCanvasShellOptions = {
121
123
  afterCanvas?: React.ReactNode;
122
124
  children?: React.ReactNode;
123
125
  frameProps?: React.HTMLAttributes<HTMLDivElement>;
126
+ infiniteCanvasContent?: React.ReactNode;
124
127
  initialState?: ToolcraftInitialState;
128
+ productSceneBoundsProvider?: ToolcraftProductSceneBoundsProvider;
125
129
  schema?: ReturnType<typeof defineToolcraft>;
126
130
  };
127
131
 
@@ -129,7 +133,9 @@ export function renderCanvasShell({
129
133
  afterCanvas,
130
134
  children,
131
135
  frameProps,
136
+ infiniteCanvasContent,
132
137
  initialState,
138
+ productSceneBoundsProvider,
133
139
  schema = createSchema(),
134
140
  }: RenderCanvasShellOptions = {}) {
135
141
  const { style, ...restFrameProps } = frameProps ?? {};
@@ -140,7 +146,13 @@ export function renderCanvasShell({
140
146
  {...restFrameProps}
141
147
  style={{ height: 640, position: "relative", width: 640, ...style }}
142
148
  >
143
- <CanvasShell>{children}</CanvasShell>
149
+ <ToolcraftProductSceneBoundsBoundary
150
+ boundsProvider={productSceneBoundsProvider}
151
+ >
152
+ <CanvasShell infiniteCanvasContent={infiniteCanvasContent}>
153
+ {children}
154
+ </CanvasShell>
155
+ </ToolcraftProductSceneBoundsBoundary>
144
156
  {afterCanvas}
145
157
  </div>
146
158
  </ToolcraftRoot>,
@@ -17,6 +17,7 @@ import {
17
17
  } from "./use-canvas-drop-import";
18
18
  import { useCanvasViewportInteractions } from "./use-canvas-viewport-interactions";
19
19
  import { useToolcraftCanvasFrame } from "./use-toolcraft-canvas-frame";
20
+ import { ToolcraftProductSceneSurface } from "./product-scene-surface";
20
21
  import { useToolcraftStore } from "../app-shell/toolcraft-store-context";
21
22
  import { useToolcraftSourceAssetCoordinator } from "../app-shell/toolcraft-source-asset-context";
22
23
  import { useToolcraftCommittedSelector } from "../app-shell/toolcraft-selectors";
@@ -32,6 +33,7 @@ const directCanvasModelOperationTargets = [DIRECT_CANVAS_OPERATION_TARGET] as co
32
33
 
33
34
  export type CanvasShellProps = {
34
35
  children?: React.ReactNode;
36
+ infiniteCanvasContent?: React.ReactNode;
35
37
  renderDefaultMedia?: boolean;
36
38
  };
37
39
 
@@ -58,6 +60,7 @@ const selectSelectedLayerId = (state: ToolcraftState) => state.selectedLayerId;
58
60
 
59
61
  export function CanvasShell({
60
62
  children,
63
+ infiniteCanvasContent,
61
64
  renderDefaultMedia = true,
62
65
  }: CanvasShellProps): React.JSX.Element {
63
66
  const dispatch = useToolcraftDispatch();
@@ -170,6 +173,14 @@ export function CanvasShell({
170
173
  : undefined
171
174
  }
172
175
  >
176
+ {canvasFrame.kind === "infinite" && infiniteCanvasContent ? (
177
+ <div
178
+ className="pointer-events-none absolute inset-0 overflow-hidden"
179
+ data-toolcraft-infinite-canvas-content=""
180
+ >
181
+ {infiniteCanvasContent}
182
+ </div>
183
+ ) : null}
173
184
  <CanvasViewportWorld>
174
185
  {renderEditableCanvas ? (
175
186
  <CanvasSceneSurface frame={canvasFrame}>
@@ -193,7 +204,11 @@ export function CanvasShell({
193
204
  : null}
194
205
  {children ? (
195
206
  <CanvasSceneSlot frame={canvasFrame}>
196
- {children}
207
+ <ToolcraftProductSceneSurface
208
+ frame={canvasFrame}
209
+ >
210
+ {children}
211
+ </ToolcraftProductSceneSurface>
197
212
  </CanvasSceneSlot>
198
213
  ) : null}
199
214
  </CanvasSceneSurface>
@@ -0,0 +1,195 @@
1
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { defineToolcraft } from "../../schema/define-toolcraft";
5
+ import type { ToolcraftCanvasFrame } from "../../state/canvas-frame";
6
+ import { ToolcraftRoot } from "../app-shell/toolcraft-root";
7
+ import { useToolcraftDispatch } from "../app-shell/use-toolcraft";
8
+ import {
9
+ ToolcraftProductSceneBoundsBoundary,
10
+ ToolcraftProductSceneSurface,
11
+ useToolcraftProductSceneFrame,
12
+ } from "./product-scene-surface";
13
+
14
+ const schema = defineToolcraft({
15
+ canvas: {
16
+ enabled: true,
17
+ size: { height: 200, unit: "px", width: 300 },
18
+ sizing: { mode: "editable-output" },
19
+ },
20
+ panels: {},
21
+ persistence: { storage: "none" },
22
+ });
23
+
24
+ afterEach(() => {
25
+ cleanup();
26
+ });
27
+
28
+ function FrameProbe(): React.JSX.Element {
29
+ const frame = useToolcraftProductSceneFrame();
30
+ return <output data-testid="frame">{JSON.stringify(frame)}</output>;
31
+ }
32
+
33
+ function renderSurface(
34
+ frame: ToolcraftCanvasFrame,
35
+ boundsProvider = vi.fn(() => [
36
+ { height: 50, width: 80, x: -40, y: -25 },
37
+ { height: 20, width: 30, x: 30, y: 15 },
38
+ ]),
39
+ ) {
40
+ return {
41
+ boundsProvider,
42
+ ...render(
43
+ <ToolcraftRoot
44
+ initialState={{
45
+ canvas: {
46
+ mode: frame.kind,
47
+ offset: { x: 0, y: 0 },
48
+ size: { height: 200, unit: "px", width: 300 },
49
+ zoom: 100,
50
+ },
51
+ }}
52
+ schema={schema}
53
+ >
54
+ <ToolcraftProductSceneBoundsBoundary boundsProvider={boundsProvider}>
55
+ <ToolcraftProductSceneSurface frame={frame}>
56
+ <FrameProbe />
57
+ </ToolcraftProductSceneSurface>
58
+ </ToolcraftProductSceneBoundsBoundary>
59
+ </ToolcraftRoot>,
60
+ ),
61
+ };
62
+ }
63
+
64
+ describe("ToolcraftProductSceneSurface", () => {
65
+ it("keeps the finite frame canonical without invoking product bounds", () => {
66
+ const { boundsProvider, container } = renderSurface({
67
+ kind: "finite",
68
+ size: { height: 200, unit: "px", width: 300 },
69
+ });
70
+
71
+ expect(boundsProvider).not.toHaveBeenCalled();
72
+ expect(screen.getByTestId("frame").textContent).toBe(
73
+ JSON.stringify({
74
+ kind: "finite",
75
+ rect: { height: 200, width: 300, x: 0, y: 0 },
76
+ }),
77
+ );
78
+ expect(
79
+ container.querySelector("[data-toolcraft-product-scene]"),
80
+ ).toBeNull();
81
+ });
82
+
83
+ it("positions one infinite product surface from the exact provider union", () => {
84
+ const { boundsProvider, container } = renderSurface({ kind: "infinite" });
85
+ const productScene = container.querySelector<HTMLElement>(
86
+ "[data-toolcraft-product-scene]",
87
+ );
88
+
89
+ expect(boundsProvider).toHaveBeenCalledTimes(1);
90
+ expect(productScene?.dataset.toolcraftProductSceneStatus).toBe("ready");
91
+ expect(productScene?.style.cssText).toContain("height: 60px");
92
+ expect(productScene?.style.cssText).toContain("left: -40px");
93
+ expect(productScene?.style.cssText).toContain("top: -25px");
94
+ expect(productScene?.style.cssText).toContain("width: 100px");
95
+ expect(screen.getByTestId("frame").textContent).toBe(
96
+ JSON.stringify({
97
+ kind: "infinite",
98
+ rect: { height: 60, width: 100, x: -40, y: -25 },
99
+ }),
100
+ );
101
+ });
102
+
103
+ it("keeps hook consumers stable when state changes without changing bounds", () => {
104
+ const renderProbe = vi.fn();
105
+
106
+ function StableFrameProbe(): React.JSX.Element {
107
+ renderProbe();
108
+ const dispatch = useToolcraftDispatch();
109
+ useToolcraftProductSceneFrame();
110
+ return (
111
+ <button
112
+ onClick={() =>
113
+ dispatch({ delta: { x: 12, y: -8 }, type: "canvas.panBy" })
114
+ }
115
+ type="button"
116
+ >
117
+ Pan
118
+ </button>
119
+ );
120
+ }
121
+
122
+ const boundsProvider = vi.fn(() => [
123
+ { height: 60, width: 100, x: -40, y: -25 },
124
+ ]);
125
+ render(
126
+ <ToolcraftRoot
127
+ initialState={{
128
+ canvas: {
129
+ mode: "infinite",
130
+ offset: { x: 0, y: 0 },
131
+ size: { height: 200, unit: "px", width: 300 },
132
+ zoom: 100,
133
+ },
134
+ }}
135
+ schema={schema}
136
+ >
137
+ <ToolcraftProductSceneBoundsBoundary boundsProvider={boundsProvider}>
138
+ <ToolcraftProductSceneSurface frame={{ kind: "infinite" }}>
139
+ <StableFrameProbe />
140
+ </ToolcraftProductSceneSurface>
141
+ </ToolcraftProductSceneBoundsBoundary>
142
+ </ToolcraftRoot>,
143
+ );
144
+
145
+ expect(renderProbe).toHaveBeenCalledTimes(1);
146
+ fireEvent.click(screen.getByRole("button", { name: "Pan" }));
147
+ expect(boundsProvider).toHaveBeenCalledTimes(2);
148
+ expect(renderProbe).toHaveBeenCalledTimes(1);
149
+ });
150
+
151
+ it.each([
152
+ {
153
+ expectedKind: "empty",
154
+ provider: () => [],
155
+ },
156
+ {
157
+ expectedKind: "unavailable",
158
+ provider: () => [
159
+ { height: 20, width: Number.NaN, x: 0, y: 0 },
160
+ ],
161
+ },
162
+ {
163
+ expectedKind: "unavailable",
164
+ provider: () => {
165
+ throw new Error("broken provider");
166
+ },
167
+ },
168
+ ])(
169
+ "reports $expectedKind without falling back to finite geometry",
170
+ ({ expectedKind, provider }) => {
171
+ const { container } = renderSurface(
172
+ { kind: "infinite" },
173
+ vi.fn(provider),
174
+ );
175
+ const productScene = container.querySelector<HTMLElement>(
176
+ "[data-toolcraft-product-scene]",
177
+ );
178
+
179
+ expect(productScene?.dataset.toolcraftProductSceneStatus).toBe(
180
+ expectedKind,
181
+ );
182
+ expect(productScene?.style.width).toBe("");
183
+ expect(productScene?.style.height).toBe("");
184
+ expect(screen.getByTestId("frame").textContent).toBe(
185
+ JSON.stringify({ kind: expectedKind, rect: null }),
186
+ );
187
+ },
188
+ );
189
+
190
+ it("fails directly when the frame hook escapes the product scene", () => {
191
+ expect(() => render(<FrameProbe />)).toThrow(
192
+ "useToolcraftProductSceneFrame must be used inside Toolcraft product scene content",
193
+ );
194
+ });
195
+ });
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import {
6
+ resolveToolcraftProductSceneBounds,
7
+ type ToolcraftProductSceneBoundsProvider,
8
+ type ToolcraftSceneRect,
9
+ } from "../../scene";
10
+ import type { ToolcraftCanvasFrame } from "../../state/canvas-frame";
11
+ import type { ToolcraftState } from "../../state/types";
12
+ import { useToolcraftCommittedSelector } from "../app-shell/toolcraft-selectors";
13
+
14
+ export type ToolcraftProductSceneFrame =
15
+ | Readonly<{ kind: "finite" | "infinite"; rect: ToolcraftSceneRect }>
16
+ | Readonly<{ kind: "empty" | "unavailable"; rect: null }>;
17
+
18
+ const ProductSceneFrameContext =
19
+ React.createContext<ToolcraftProductSceneFrame | null>(null);
20
+ const ProductSceneBoundsContext =
21
+ React.createContext<ToolcraftProductSceneBoundsProvider | undefined>(
22
+ undefined,
23
+ );
24
+
25
+ function resolveInfiniteProductFrame(
26
+ boundsProvider: ToolcraftProductSceneBoundsProvider | undefined,
27
+ state: ToolcraftState,
28
+ ): ToolcraftProductSceneFrame {
29
+ if (!boundsProvider) {
30
+ return { kind: "unavailable", rect: null };
31
+ }
32
+
33
+ try {
34
+ const result = resolveToolcraftProductSceneBounds(
35
+ boundsProvider({ state }),
36
+ );
37
+ return result.ok
38
+ ? { kind: "infinite", rect: result.bounds }
39
+ : {
40
+ kind:
41
+ result.code === "empty-scene" ? "empty" : "unavailable",
42
+ rect: null,
43
+ };
44
+ } catch {
45
+ return { kind: "unavailable", rect: null };
46
+ }
47
+ }
48
+
49
+ function productSceneFramesEqual(
50
+ previous: ToolcraftProductSceneFrame,
51
+ next: ToolcraftProductSceneFrame,
52
+ ): boolean {
53
+ if (previous.kind !== next.kind) return false;
54
+ if (previous.rect === null || next.rect === null) return true;
55
+
56
+ return (
57
+ Object.is(previous.rect.height, next.rect.height) &&
58
+ Object.is(previous.rect.width, next.rect.width) &&
59
+ Object.is(previous.rect.x, next.rect.x) &&
60
+ Object.is(previous.rect.y, next.rect.y)
61
+ );
62
+ }
63
+
64
+ export function useToolcraftProductSceneFrame(): ToolcraftProductSceneFrame {
65
+ const frame = React.useContext(ProductSceneFrameContext);
66
+ if (!frame) {
67
+ throw new Error(
68
+ "useToolcraftProductSceneFrame must be used inside Toolcraft product scene content.",
69
+ );
70
+ }
71
+ return frame;
72
+ }
73
+
74
+ export function ToolcraftProductSceneBoundsBoundary({
75
+ boundsProvider,
76
+ children,
77
+ }: Readonly<{
78
+ boundsProvider?: ToolcraftProductSceneBoundsProvider;
79
+ children: React.ReactNode;
80
+ }>): React.JSX.Element {
81
+ return (
82
+ <ProductSceneBoundsContext.Provider value={boundsProvider}>
83
+ {children}
84
+ </ProductSceneBoundsContext.Provider>
85
+ );
86
+ }
87
+
88
+ export function ToolcraftProductSceneSurface({
89
+ children,
90
+ frame,
91
+ }: Readonly<{
92
+ children: React.ReactNode;
93
+ frame: ToolcraftCanvasFrame;
94
+ }>): React.JSX.Element {
95
+ if (frame.kind === "finite") {
96
+ const productFrame: ToolcraftProductSceneFrame = {
97
+ kind: "finite",
98
+ rect: {
99
+ height: frame.size.height,
100
+ width: frame.size.width,
101
+ x: 0,
102
+ y: 0,
103
+ },
104
+ };
105
+
106
+ return (
107
+ <ProductSceneFrameContext.Provider value={productFrame}>
108
+ {children}
109
+ </ProductSceneFrameContext.Provider>
110
+ );
111
+ }
112
+
113
+ return <InfiniteProductSceneSurface>{children}</InfiniteProductSceneSurface>;
114
+ }
115
+
116
+ function InfiniteProductSceneSurface({
117
+ children,
118
+ }: Readonly<{ children: React.ReactNode }>): React.JSX.Element {
119
+ const boundsProvider = React.useContext(ProductSceneBoundsContext);
120
+ const selectProductFrame = React.useCallback(
121
+ (state: ToolcraftState) =>
122
+ resolveInfiniteProductFrame(boundsProvider, state),
123
+ [boundsProvider],
124
+ );
125
+ const productFrame = useToolcraftCommittedSelector(
126
+ selectProductFrame,
127
+ productSceneFramesEqual,
128
+ );
129
+ const readyRect =
130
+ productFrame.kind === "infinite" ? productFrame.rect : null;
131
+
132
+ return (
133
+ <ProductSceneFrameContext.Provider value={productFrame}>
134
+ <div
135
+ className={readyRect ? "absolute" : "contents"}
136
+ data-toolcraft-product-scene=""
137
+ data-toolcraft-product-scene-status={
138
+ readyRect ? "ready" : productFrame.kind
139
+ }
140
+ style={
141
+ readyRect
142
+ ? {
143
+ height: readyRect.height,
144
+ left: readyRect.x,
145
+ top: readyRect.y,
146
+ width: readyRect.width,
147
+ }
148
+ : undefined
149
+ }
150
+ >
151
+ {children}
152
+ </div>
153
+ </ProductSceneFrameContext.Provider>
154
+ );
155
+ }
@@ -1,4 +1,4 @@
1
- import type { ToolcraftExportFrame } from "../../export";
1
+ import type { ToolcraftExportFrame } from "../../export/export-frame";
2
2
  import type { ToolcraftRuntimeSceneVisibility } from "../../scene";
3
3
  import type { ToolcraftCanvasFrame } from "../../state/canvas-frame";
4
4
  import type { ToolcraftImageAsset, ToolcraftState } from "../../state/types";
@@ -9,6 +9,67 @@ import {
9
9
  } from "../conditions/control-conditions";
10
10
 
11
11
  describe("Toolcraft control conditions", () => {
12
+ it("requires every applicability predicate to match", () => {
13
+ const schema = defineToolcraft({
14
+ canvas: { enabled: true },
15
+ panels: {
16
+ controls: {
17
+ sections: [
18
+ {
19
+ controls: {
20
+ sides: {
21
+ applicability: {
22
+ all: [
23
+ { equals: "create", target: "source.mode" },
24
+ {
25
+ oneOf: ["polygon", "star"],
26
+ target: "shape.kind",
27
+ },
28
+ ],
29
+ mode: "conditional",
30
+ },
31
+ target: "shape.sides",
32
+ type: "slider",
33
+ },
34
+ },
35
+ title: "Shape",
36
+ },
37
+ ],
38
+ title: "Controls",
39
+ },
40
+ },
41
+ });
42
+ const control = schema.panels.controls?.sections.find(
43
+ (section) => section.title === "Shape",
44
+ )?.controls.sides;
45
+
46
+ expect(control).toBeTruthy();
47
+ expect(
48
+ isToolcraftControlVisible(
49
+ createToolcraftState(schema, {
50
+ values: { "shape.kind": "star", "source.mode": "upload" },
51
+ }),
52
+ control!,
53
+ ),
54
+ ).toBe(false);
55
+ expect(
56
+ isToolcraftControlVisible(
57
+ createToolcraftState(schema, {
58
+ values: { "shape.kind": "heart", "source.mode": "create" },
59
+ }),
60
+ control!,
61
+ ),
62
+ ).toBe(false);
63
+ expect(
64
+ isToolcraftControlVisible(
65
+ createToolcraftState(schema, {
66
+ values: { "shape.kind": "star", "source.mode": "create" },
67
+ }),
68
+ control!,
69
+ ),
70
+ ).toBe(true);
71
+ });
72
+
12
73
  it("resolves timeline runtime targets through the canonical condition helpers", () => {
13
74
  const schema = defineToolcraft({
14
75
  canvas: { enabled: true, renderScale: true, sizing: { mode: "editable-output" } },
@@ -54,6 +54,9 @@ describe("ControlsPanel collection actions", () => {
54
54
  expect(collectionHeader?.lastElementChild?.contains(addButton)).toBe(true);
55
55
  expect(colorGrid).toBeTruthy();
56
56
  expect(colorGrid?.children).toHaveLength(2);
57
+ expect(
58
+ container.querySelector('[data-slot="collection-item-groups"]'),
59
+ ).toBeNull();
57
60
  expect(screen.queryByText("Color 1")).toBeNull();
58
61
  expect(screen.queryByText("Color 2")).toBeNull();
59
62
 
@@ -77,6 +80,92 @@ describe("ControlsPanel collection actions", () => {
77
80
  expect((removeButton as HTMLButtonElement).disabled).toBe(true);
78
81
  });
79
82
 
83
+ it("adds, edits, separates, and removes complete logical item records", () => {
84
+ const schema = defineToolcraft({
85
+ canvas: { enabled: false },
86
+ panels: {
87
+ controls: {
88
+ sections: [
89
+ {
90
+ controls: {
91
+ layers: {
92
+ defaultValue: [
93
+ {
94
+ invert: true,
95
+ rendererToken: "keep",
96
+ strength: 0.4,
97
+ },
98
+ ],
99
+ itemControls: {
100
+ strength: {
101
+ defaultValue: 0.5,
102
+ label: "Strength",
103
+ max: 1,
104
+ min: 0,
105
+ step: 0.1,
106
+ type: "slider",
107
+ },
108
+ invert: {
109
+ defaultValue: false,
110
+ label: "Invert",
111
+ type: "switch",
112
+ },
113
+ },
114
+ itemLabel: "Layer",
115
+ minItems: 1,
116
+ target: "surface.layers",
117
+ type: "collectionActions",
118
+ },
119
+ },
120
+ title: "Surface Layers",
121
+ },
122
+ ],
123
+ title: "Controls",
124
+ },
125
+ },
126
+ });
127
+ const { container } = renderControlsPanelWithSchema(schema);
128
+
129
+ expect(
130
+ container.querySelectorAll('[data-slot="collection-item-group"]'),
131
+ ).toHaveLength(1);
132
+ expect(screen.queryByText(/Item 1/u)).toBeNull();
133
+
134
+ fireEvent.click(screen.getByRole("button", { name: "Add Layer" }));
135
+ let values = JSON.parse(
136
+ screen.getByTestId("values-json").textContent ?? "{}",
137
+ );
138
+ expect(values["surface.layers"]).toEqual([
139
+ { invert: true, rendererToken: "keep", strength: 0.4 },
140
+ { invert: false, strength: 0.5 },
141
+ ]);
142
+
143
+ const groups = container.querySelectorAll(
144
+ '[data-slot="collection-item-group"]',
145
+ );
146
+ expect(groups).toHaveLength(2);
147
+ expect(groups[0]?.className).not.toContain("border-t");
148
+ expect(groups[1]?.className).toContain("border-t");
149
+
150
+ fireEvent.click(
151
+ screen.getAllByRole("button", { name: "Edit Strength value" })[0]!,
152
+ );
153
+ const editor = screen.getByRole("textbox", { name: "Strength value" });
154
+ editor.textContent = "0.7";
155
+ fireEvent.keyDown(editor, { key: "Enter" });
156
+ values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
157
+ expect(values["surface.layers"][0]).toEqual({
158
+ invert: true,
159
+ rendererToken: "keep",
160
+ strength: 0.7,
161
+ });
162
+
163
+ fireEvent.click(screen.getByRole("button", { name: "Remove Layer" }));
164
+ expect(
165
+ container.querySelectorAll('[data-slot="collection-item-group"]'),
166
+ ).toHaveLength(1);
167
+ });
168
+
80
169
  it("renders collection item controls beyond color lists", () => {
81
170
  const schema = defineToolcraft({
82
171
  canvas: { enabled: false },