@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
@@ -94,3 +94,192 @@ describe("FileDropControl folder selection", () => {
94
94
  expect(screen.queryByLabelText("Upload file folder")).toBeNull();
95
95
  });
96
96
  });
97
+
98
+ describe("FileDropControl collection actions", () => {
99
+ const filePresentation = {
100
+ accept: "image/*",
101
+ allowsFileBatch: true,
102
+ allowsFolderSelection: false,
103
+ emptyDescription: "or drop bump maps",
104
+ emptyTitle: "Upload bump maps",
105
+ items: [],
106
+ presenter: "file-list",
107
+ secondaryActions: [],
108
+ } satisfies FileDropPresentation<"file">;
109
+
110
+ it("adds an empty upload slot before selecting another file", () => {
111
+ const onFilesSelect = vi.fn();
112
+
113
+ render(
114
+ <FileDropControl
115
+ collectionActions={{
116
+ addLabel: "Add bump map",
117
+ itemLabel: "bump map",
118
+ name: "Bump maps",
119
+ removeLabel: "Remove bump map",
120
+ }}
121
+ onFilesSelect={onFilesSelect}
122
+ presentation={{
123
+ ...filePresentation,
124
+ items: [{
125
+ assetKind: "file",
126
+ fileName: "broad.png",
127
+ id: "bump-broad",
128
+ }],
129
+ }}
130
+ />,
131
+ );
132
+
133
+ fireEvent.click(screen.getByRole("button", { name: "Add bump map" }));
134
+ expect(onFilesSelect).not.toHaveBeenCalled();
135
+ expect(
136
+ (screen.getByRole("button", { name: "Add bump map" }) as HTMLButtonElement)
137
+ .disabled,
138
+ ).toBe(true);
139
+
140
+ const input = screen.getByLabelText("Upload bump map 2") as HTMLInputElement;
141
+ const file = new File([new Uint8Array([1])], "grain.png", {
142
+ type: "image/png",
143
+ });
144
+ fireEvent.change(input, { target: { files: [file] } });
145
+
146
+ expect(onFilesSelect).toHaveBeenCalledWith([file]);
147
+ });
148
+
149
+ it("removes the final item and suppresses duplicate list cardinality actions", () => {
150
+ const onItemRemove = vi.fn();
151
+ const finalItem = {
152
+ assetKind: "file" as const,
153
+ fileName: "fine.png",
154
+ id: "bump-fine",
155
+ };
156
+
157
+ render(
158
+ <FileDropControl
159
+ collectionActions={{
160
+ addLabel: "Add bump map",
161
+ itemLabel: "bump map",
162
+ name: "Bump maps",
163
+ removeLabel: "Remove bump map",
164
+ }}
165
+ onFilesSelect={vi.fn()}
166
+ onItemRemove={onItemRemove}
167
+ presentation={{
168
+ ...filePresentation,
169
+ items: [
170
+ {
171
+ assetKind: "file",
172
+ fileName: "broad.png",
173
+ id: "bump-broad",
174
+ },
175
+ finalItem,
176
+ ],
177
+ }}
178
+ />,
179
+ );
180
+
181
+ expect(screen.queryByRole("button", { name: "Add a new file" })).toBeNull();
182
+ expect(screen.queryByRole("button", { name: "Remove broad.png" })).toBeNull();
183
+ expect(screen.queryByRole("button", { name: "Remove fine.png" })).toBeNull();
184
+
185
+ fireEvent.click(screen.getByRole("button", { name: "Remove bump map" }));
186
+ expect(onItemRemove).toHaveBeenCalledWith(finalItem, 1);
187
+ });
188
+
189
+ it("removes a pending final slot before deleting an attached file", () => {
190
+ const onItemRemove = vi.fn();
191
+
192
+ render(
193
+ <FileDropControl
194
+ collectionActions={{
195
+ addLabel: "Add bump map",
196
+ itemLabel: "bump map",
197
+ name: "Bump maps",
198
+ removeLabel: "Remove bump map",
199
+ }}
200
+ onFilesSelect={vi.fn()}
201
+ onItemRemove={onItemRemove}
202
+ presentation={{
203
+ ...filePresentation,
204
+ items: [{
205
+ assetKind: "file",
206
+ fileName: "broad.png",
207
+ id: "bump-broad",
208
+ }],
209
+ }}
210
+ />,
211
+ );
212
+
213
+ fireEvent.click(screen.getByRole("button", { name: "Add bump map" }));
214
+ expect(screen.getByLabelText("Upload bump map 2")).toBeTruthy();
215
+
216
+ fireEvent.click(screen.getByRole("button", { name: "Remove bump map" }));
217
+ expect(screen.queryByLabelText("Upload bump map 2")).toBeNull();
218
+ expect(onItemRemove).not.toHaveBeenCalled();
219
+ });
220
+
221
+ it("renders item-owned settings only under an attached upload", () => {
222
+ const renderItemContent = vi.fn((item: { id: string }) => (
223
+ <div>Settings for {item.id}</div>
224
+ ));
225
+
226
+ render(
227
+ <FileDropControl
228
+ collectionActions={{
229
+ addLabel: "Add bump map",
230
+ itemLabel: "bump map",
231
+ name: "Bump maps",
232
+ renderItemContent,
233
+ }}
234
+ onFilesSelect={vi.fn()}
235
+ presentation={{
236
+ ...filePresentation,
237
+ items: [
238
+ {
239
+ assetKind: "file",
240
+ fileName: "broad.png",
241
+ id: "bump-broad",
242
+ },
243
+ ],
244
+ }}
245
+ />,
246
+ );
247
+
248
+ expect(screen.getByText("Settings for bump-broad")).toBeTruthy();
249
+ fireEvent.click(screen.getByRole("button", { name: "Add bump map" }));
250
+ expect(screen.getByLabelText("Upload bump map 2")).toBeTruthy();
251
+ expect(renderItemContent).toHaveBeenCalled();
252
+ expect(
253
+ renderItemContent.mock.calls.every(([item]) => item.id === "bump-broad"),
254
+ ).toBe(true);
255
+ });
256
+
257
+ it("separates attached logical file groups with one between-group line", () => {
258
+ const { container } = render(
259
+ <FileDropControl
260
+ collectionActions={{
261
+ itemLabel: "bump map",
262
+ name: "Bump maps",
263
+ renderItemContent: (item) => <div>Settings for {item.id}</div>,
264
+ }}
265
+ onFilesSelect={vi.fn()}
266
+ onItemRemove={vi.fn()}
267
+ presentation={{
268
+ ...filePresentation,
269
+ items: [
270
+ { assetKind: "file", fileName: "broad.png", id: "broad" },
271
+ { assetKind: "file", fileName: "fine.png", id: "fine" },
272
+ ],
273
+ }}
274
+ />,
275
+ );
276
+ const groups = container.querySelectorAll(
277
+ '[data-slot="collection-item-group"]',
278
+ );
279
+
280
+ expect(groups).toHaveLength(2);
281
+ expect(groups[0]?.className).not.toContain("border-t");
282
+ expect(groups[1]?.className).toContain("border-t");
283
+ expect(container.textContent).not.toMatch(/Item 1|Item 2/u);
284
+ });
285
+ });
@@ -11,12 +11,9 @@ import {
11
11
  import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
12
12
 
13
13
  import { cn } from "../../../lib/utils";
14
- import {
15
- ActionsControl,
16
- type ActionControlObjectOption,
17
- type ActionsControlItemState,
18
- } from "../actions/actions-control";
14
+ import { ActionsControl } from "../actions/actions-control";
19
15
  import { Field } from "../../primitives";
16
+ import { FileDropCollectionControl } from "./file-drop-collection-control";
20
17
  import { FileDropEmptyState } from "./file-drop-empty-state";
21
18
  import { FileDropFileList } from "./file-drop-file-list";
22
19
  import { FileDropFolderInput } from "./file-drop-folder-input";
@@ -24,11 +21,13 @@ import { FileDropImageGrid } from "./file-drop-image-grid";
24
21
  import { FileDropPresentationMessages } from "./file-drop-presentation-messages";
25
22
  import {
26
23
  createLegacyFileDropPresentation,
24
+ createFileDropActionModel,
25
+ createFileDropItemView,
26
+ createFileDropTargetHandlers,
27
27
  getFileInputAccept,
28
28
  getPresentationItemKey,
29
29
  getPreviewKey,
30
30
  imageTransformActions,
31
- isDragLeavingCurrentTarget,
32
31
  reorderFileDropPresentationItems,
33
32
  } from "./file-drop-presentation";
34
33
  import { FileDropSinglePreview } from "./file-drop-single-preview";
@@ -37,19 +36,19 @@ import type {
37
36
  FileDropControlProps,
38
37
  FileDropImageTransformOperation,
39
38
  FileDropPresentation,
40
- FileDropPresentationAction,
41
- FileDropPresentationEntry,
42
39
  FileDropPresentationItem,
43
40
  FileDropPreview,
44
41
  } from "./file-drop-types";
45
42
 
46
- export function FileDropControl<
43
+ function FileDropControlBase<
47
44
  AssetKind extends string = FileDropAssetKind,
48
45
  Feedback = unknown,
49
46
  Status = unknown,
50
47
  >({
51
48
  accept,
52
49
  assetKind = "image",
50
+ collectionActions,
51
+ collectionSlot = false,
53
52
  multiple = false,
54
53
  onAction,
55
54
  onClear,
@@ -65,11 +64,15 @@ export function FileDropControl<
65
64
  presentation: providedPresentation,
66
65
  preview,
67
66
  previews,
68
- }: FileDropControlProps<AssetKind, Feedback, Status>): React.JSX.Element {
67
+ }: FileDropControlProps<AssetKind, Feedback, Status> & {
68
+ collectionSlot?: boolean;
69
+ }): React.JSX.Element {
69
70
  const inputRef = React.useRef<HTMLInputElement>(null);
70
71
  const folderInputRef = React.useRef<HTMLInputElement>(null);
71
72
  const [dragOver, setDragOver] = React.useState(false);
72
- const [selectedItemKey, setSelectedItemKey] = React.useState<string | null>(null);
73
+ const [selectedItemKey, setSelectedItemKey] = React.useState<string | null>(
74
+ null,
75
+ );
73
76
  const sensors = useSensors(
74
77
  useSensor(PointerSensor, {
75
78
  activationConstraint: {
@@ -88,32 +91,21 @@ export function FileDropControl<
88
91
  previews,
89
92
  });
90
93
  const usesLegacyPresentation = providedPresentation === undefined;
91
- const presentation = (providedPresentation ?? legacy.presentation) as FileDropPresentation<
92
- string,
93
- unknown,
94
- unknown
95
- >;
94
+ const presentation = (providedPresentation ??
95
+ legacy.presentation) as FileDropPresentation<string, unknown, unknown>;
96
96
  const isImagePresenter =
97
97
  presentation.presenter === "image-grid" ||
98
98
  presentation.presenter === "single-image";
99
99
  const requiresPreviewSource = usesLegacyPresentation || isImagePresenter;
100
- const itemEntries: readonly FileDropPresentationEntry[] =
101
- presentation.items.flatMap((item, sourceIndex) =>
102
- requiresPreviewSource && !item.previewSrc
103
- ? []
104
- : [
105
- {
106
- item,
107
- key: getPresentationItemKey(item, sourceIndex),
108
- sourceIndex,
109
- },
110
- ],
111
- );
112
- const renderableItems = itemEntries.map((entry) => entry.item);
113
- const hasContent = itemEntries.length > 0;
114
- const isListPresenter =
115
- presentation.presenter === "file-list" || presentation.presenter === "model-item";
116
- const itemKeys = itemEntries.map((entry) => entry.key);
100
+ const {
101
+ entries: itemEntries,
102
+ hasContent,
103
+ isListPresenter,
104
+ itemKeys,
105
+ items: renderableItems,
106
+ } = createFileDropItemView(presentation, requiresPreviewSource);
107
+ const isAttachedCollectionSlot = collectionSlot && hasContent;
108
+ const showsListCardinalityActions = !collectionSlot;
117
109
  const legacyPreviewByKey = new Map<string, FileDropPreview>(
118
110
  legacy.previews.map((item, index) => [getPreviewKey(item, index), item]),
119
111
  );
@@ -135,22 +127,12 @@ export function FileDropControl<
135
127
  Boolean(onPreviewTransform) &&
136
128
  Boolean(selectedItemEntry) &&
137
129
  (!requiresItemSelection || selectedItemKey !== null);
138
- const secondaryActions: readonly FileDropPresentationAction[] =
139
- shouldRenderLegacyImageActions
140
- ? imageTransformActions
141
- : presentation.secondaryActions;
142
- const actionOptions: readonly ActionControlObjectOption[] = secondaryActions.map(
143
- ({ state: _state, ...action }) => action,
144
- );
145
- const actionStates = secondaryActions.reduce<Record<string, ActionsControlItemState>>(
146
- (states, action) => {
147
- if (action.state) {
148
- states[action.value] = action.state;
149
- }
150
-
151
- return states;
152
- },
153
- {},
130
+ const secondaryActions = shouldRenderLegacyImageActions
131
+ ? imageTransformActions
132
+ : presentation.secondaryActions;
133
+ const actionModel = createFileDropActionModel(
134
+ secondaryActions,
135
+ Boolean(shouldRenderLegacyImageActions || onAction),
154
136
  );
155
137
  const hasReorderHandler = usesLegacyPresentation
156
138
  ? Boolean(onPreviewReorder)
@@ -158,6 +140,11 @@ export function FileDropControl<
158
140
  const canRemoveCollectionItem = usesLegacyPresentation
159
141
  ? Boolean(onPreviewRemove)
160
142
  : Boolean(onItemRemove || (onClear && renderableItems.length === 1));
143
+ const dropTargetHandlers = createFileDropTargetHandlers({
144
+ enabled: !isAttachedCollectionSlot,
145
+ onDragOverChange: setDragOver,
146
+ onFiles: handleFiles,
147
+ });
161
148
 
162
149
  if (
163
150
  !usesLegacyPresentation &&
@@ -233,12 +220,14 @@ export function FileDropControl<
233
220
  }
234
221
 
235
222
  if (usesLegacyPresentation) {
236
- const orderedPreviews = orderedPresentationItems.flatMap((item, index) => {
237
- const key = getPresentationItemKey(item, index);
238
- const legacyPreview = legacyPreviewByKey.get(key);
223
+ const orderedPreviews = orderedPresentationItems.flatMap(
224
+ (item, index) => {
225
+ const key = getPresentationItemKey(item, index);
226
+ const legacyPreview = legacyPreviewByKey.get(key);
239
227
 
240
- return legacyPreview ? [legacyPreview] : [];
241
- });
228
+ return legacyPreview ? [legacyPreview] : [];
229
+ },
230
+ );
242
231
 
243
232
  if (orderedPreviews.length === legacy.previews.length) {
244
233
  onPreviewReorder?.(orderedPreviews);
@@ -256,7 +245,9 @@ export function FileDropControl<
256
245
  index: number,
257
246
  ): void {
258
247
  if (usesLegacyPresentation) {
259
- const legacyPreview = legacyPreviewByKey.get(getPresentationItemKey(item, index));
248
+ const legacyPreview = legacyPreviewByKey.get(
249
+ getPresentationItemKey(item, index),
250
+ );
260
251
 
261
252
  if (legacyPreview) {
262
253
  onPreviewRemove?.(legacyPreview, index);
@@ -293,7 +284,9 @@ export function FileDropControl<
293
284
  onClear?.();
294
285
  }
295
286
 
296
- function runLegacyImageTransform(operation: FileDropImageTransformOperation): void {
287
+ function runLegacyImageTransform(
288
+ operation: FileDropImageTransformOperation,
289
+ ): void {
297
290
  if (!selectedItemEntry) {
298
291
  return;
299
292
  }
@@ -314,6 +307,20 @@ export function FileDropControl<
314
307
  onAction?.(value);
315
308
  }
316
309
 
310
+ if (collectionActions) {
311
+ return (
312
+ <FileDropCollectionControl
313
+ actions={collectionActions}
314
+ canRemoveAttachedItem={canRemoveCollectionItem}
315
+ entries={itemEntries}
316
+ onFilesSelect={onFilesSelect}
317
+ onRemoveAttachedItem={handleCollectionItemRemove}
318
+ presentation={presentation}
319
+ SlotControl={FileDropControlBase}
320
+ />
321
+ );
322
+ }
323
+
317
324
  return (
318
325
  <Field className="min-w-0" style={{ gap: "6px" }}>
319
326
  <input
@@ -353,25 +360,8 @@ export function FileDropControl<
353
360
  : "p-0",
354
361
  )}
355
362
  data-drag-over={dragOver}
356
- onDragEnter={(event) => {
357
- event.preventDefault();
358
- setDragOver(true);
359
- }}
360
- onDragLeave={(event) => {
361
- if (isDragLeavingCurrentTarget(event)) {
362
- setDragOver(false);
363
- }
364
- }}
365
- onDragOver={(event) => {
366
- event.preventDefault();
367
- setDragOver(true);
368
- }}
369
- onDrop={(event) => {
370
- event.preventDefault();
371
- setDragOver(false);
372
- handleFiles(event.dataTransfer?.files ?? undefined);
373
- }}
374
363
  role="group"
364
+ {...dropTargetHandlers}
375
365
  >
376
366
  {!hasContent ? (
377
367
  <FileDropEmptyState
@@ -385,10 +375,12 @@ export function FileDropControl<
385
375
  ) : presentation.presenter === "file-list" ? (
386
376
  <FileDropFileList
387
377
  isSortable={hasReorderHandler && itemEntries.length > 1}
388
- onAddFile={openFileDialog}
378
+ onAddFile={showsListCardinalityActions ? openFileDialog : undefined}
389
379
  onDragEnd={handleItemDragEnd}
390
380
  onItemRemove={
391
- canRemoveCollectionItem ? handleCollectionItemRemove : undefined
381
+ showsListCardinalityActions && canRemoveCollectionItem
382
+ ? handleCollectionItemRemove
383
+ : undefined
392
384
  }
393
385
  previewEntries={itemEntries}
394
386
  previewKeys={itemKeys}
@@ -405,7 +397,9 @@ export function FileDropControl<
405
397
  onPreviewSelect={
406
398
  canSelectImageItem
407
399
  ? (key) => {
408
- const entry = itemEntries.find((candidate) => candidate.key === key);
400
+ const entry = itemEntries.find(
401
+ (candidate) => candidate.key === key,
402
+ );
409
403
 
410
404
  if (!entry) {
411
405
  return;
@@ -465,17 +459,8 @@ export function FileDropControl<
465
459
  />
466
460
  {secondaryActions.length > 0 ? (
467
461
  <ActionsControl
468
- actionStates={
469
- shouldRenderLegacyImageActions || onAction
470
- ? actionStates
471
- : Object.fromEntries(
472
- secondaryActions.map((action) => [
473
- action.value,
474
- { ...action.state, disabled: true },
475
- ]),
476
- )
477
- }
478
- actions={actionOptions}
462
+ actionStates={actionModel.states}
463
+ actions={actionModel.options}
479
464
  buttonColumns={isImagePresenter ? 3 : undefined}
480
465
  name={isImagePresenter ? "Image transforms" : "File actions"}
481
466
  onAction={handleSecondaryAction}
@@ -485,3 +470,11 @@ export function FileDropControl<
485
470
  </Field>
486
471
  );
487
472
  }
473
+
474
+ export function FileDropControl<
475
+ AssetKind extends string = FileDropAssetKind,
476
+ Feedback = unknown,
477
+ Status = unknown,
478
+ >(props: FileDropControlProps<AssetKind, Feedback, Status>): React.JSX.Element {
479
+ return <FileDropControlBase {...props} />;
480
+ }
@@ -3,13 +3,18 @@
3
3
  import * as React from "react";
4
4
 
5
5
  import { cn } from "../../../lib/utils";
6
- import type { ActionControlOption } from "../actions/actions-control";
6
+ import type {
7
+ ActionControlObjectOption,
8
+ ActionControlOption,
9
+ ActionsControlItemState,
10
+ } from "../actions/actions-control";
7
11
  import type {
8
12
  FileDropAssetKind,
9
13
  FileDropImageSize,
10
14
  FileDropImageTransform,
11
15
  FileDropImageTransformOperation,
12
16
  FileDropPresentation,
17
+ FileDropPresentationAction,
13
18
  FileDropPresentationEntry,
14
19
  FileDropPresentationItem,
15
20
  FileDropPreview,
@@ -22,7 +27,44 @@ export function isDragLeavingCurrentTarget(
22
27
  ): boolean {
23
28
  const nextTarget = event.relatedTarget;
24
29
 
25
- return !(nextTarget instanceof Node && event.currentTarget.contains(nextTarget));
30
+ return !(
31
+ nextTarget instanceof Node && event.currentTarget.contains(nextTarget)
32
+ );
33
+ }
34
+
35
+ export function createFileDropTargetHandlers({
36
+ enabled,
37
+ onDragOverChange,
38
+ onFiles,
39
+ }: Readonly<{
40
+ enabled: boolean;
41
+ onDragOverChange: (active: boolean) => void;
42
+ onFiles: (files: FileList | readonly File[] | undefined) => void;
43
+ }>): React.HTMLAttributes<HTMLDivElement> {
44
+ if (!enabled) {
45
+ return {};
46
+ }
47
+
48
+ return {
49
+ onDragEnter: (event) => {
50
+ event.preventDefault();
51
+ onDragOverChange(true);
52
+ },
53
+ onDragLeave: (event) => {
54
+ if (isDragLeavingCurrentTarget(event)) {
55
+ onDragOverChange(false);
56
+ }
57
+ },
58
+ onDragOver: (event) => {
59
+ event.preventDefault();
60
+ onDragOverChange(true);
61
+ },
62
+ onDrop: (event) => {
63
+ event.preventDefault();
64
+ onDragOverChange(false);
65
+ onFiles(event.dataTransfer?.files);
66
+ },
67
+ };
26
68
  }
27
69
 
28
70
  export function getFileInputAccept(accept: string): string {
@@ -59,9 +101,12 @@ export function getFileInputAccept(accept: string): string {
59
101
  export function normalizeImageRotation(
60
102
  rotationDeg: number | undefined,
61
103
  ): 0 | 90 | 180 | 270 {
62
- const normalized = ((Math.round((rotationDeg ?? 0) / 90) * 90) % 360 + 360) % 360;
104
+ const normalized =
105
+ (((Math.round((rotationDeg ?? 0) / 90) * 90) % 360) + 360) % 360;
63
106
 
64
- return normalized === 90 || normalized === 180 || normalized === 270 ? normalized : 0;
107
+ return normalized === 90 || normalized === 180 || normalized === 270
108
+ ? normalized
109
+ : 0;
65
110
  }
66
111
 
67
112
  export function getImageTransformStyle(
@@ -144,13 +189,73 @@ export function getPresentationItemKey(
144
189
  return item.id;
145
190
  }
146
191
 
192
+ export function createFileDropItemView(
193
+ presentation: FileDropPresentation<string, unknown, unknown>,
194
+ requiresPreviewSource: boolean,
195
+ ): Readonly<{
196
+ entries: readonly FileDropPresentationEntry[];
197
+ hasContent: boolean;
198
+ isListPresenter: boolean;
199
+ itemKeys: string[];
200
+ items: readonly FileDropPresentationItem[];
201
+ }> {
202
+ const entries: readonly FileDropPresentationEntry[] =
203
+ presentation.items.flatMap((item, sourceIndex) =>
204
+ requiresPreviewSource && !item.previewSrc
205
+ ? []
206
+ : [
207
+ {
208
+ item,
209
+ key: getPresentationItemKey(item, sourceIndex),
210
+ sourceIndex,
211
+ },
212
+ ],
213
+ );
214
+
215
+ return {
216
+ entries,
217
+ hasContent: entries.length > 0,
218
+ isListPresenter:
219
+ presentation.presenter === "file-list" ||
220
+ presentation.presenter === "model-item",
221
+ itemKeys: entries.map((entry) => entry.key),
222
+ items: entries.map((entry) => entry.item),
223
+ };
224
+ }
225
+
226
+ export function createFileDropActionModel(
227
+ actions: readonly FileDropPresentationAction[],
228
+ enabled: boolean,
229
+ ): Readonly<{
230
+ options: readonly ActionControlObjectOption[];
231
+ states: Readonly<Record<string, ActionsControlItemState>>;
232
+ }> {
233
+ return {
234
+ options: actions.map(({ state: _state, ...action }) => action),
235
+ states: actions.reduce<Record<string, ActionsControlItemState>>(
236
+ (states, action) => {
237
+ if (!enabled) {
238
+ states[action.value] = { ...action.state, disabled: true };
239
+ } else if (action.state) {
240
+ states[action.value] = action.state;
241
+ }
242
+
243
+ return states;
244
+ },
245
+ {},
246
+ ),
247
+ };
248
+ }
249
+
147
250
  export function reorderFileDropPresentationItems<AssetKind extends string>(
148
251
  items: readonly FileDropPresentationItem<AssetKind>[],
149
252
  visibleEntries: readonly FileDropPresentationEntry<AssetKind>[],
150
253
  activeKey: string,
151
254
  overKey: string,
152
255
  ): FileDropPresentationItem<AssetKind>[] | null {
153
- const activeIndex = visibleEntries.findIndex((entry) => entry.key === activeKey);
256
+ const activeIndex = visibleEntries.findIndex(
257
+ (entry) => entry.key === activeKey,
258
+ );
154
259
  const overIndex = visibleEntries.findIndex((entry) => entry.key === overKey);
155
260
 
156
261
  if (activeIndex < 0 || overIndex < 0 || activeIndex === overIndex) {
@@ -225,7 +330,9 @@ export function createLegacyFileDropPresentation({
225
330
  ? "image-grid"
226
331
  : "single-image";
227
332
  const emptyTitle =
228
- assetKind === "file" ? "Click to upload a file" : "Click to upload an image";
333
+ assetKind === "file"
334
+ ? "Click to upload a file"
335
+ : "Click to upload an image";
229
336
  const dropTargetLabel = hasContent
230
337
  ? multiple
231
338
  ? assetKind === "file"
@@ -271,14 +378,29 @@ export function FileDropPlusGlyph({
271
378
  viewBox="0 0 14 14"
272
379
  xmlns="http://www.w3.org/2000/svg"
273
380
  >
274
- <path d="M7 2.5V11.5" stroke="currentColor" strokeLinecap="round" strokeWidth="1" />
275
- <path d="M2.5 7H11.5" stroke="currentColor" strokeLinecap="round" strokeWidth="1" />
381
+ <path
382
+ d="M7 2.5V11.5"
383
+ stroke="currentColor"
384
+ strokeLinecap="round"
385
+ strokeWidth="1"
386
+ />
387
+ <path
388
+ d="M2.5 7H11.5"
389
+ stroke="currentColor"
390
+ strokeLinecap="round"
391
+ strokeWidth="1"
392
+ />
276
393
  </svg>
277
394
  );
278
395
  }
279
396
 
280
397
  export const imageTransformActions = [
281
- { ariaLabel: "90° Right", icon: "rotate-cw", label: "90°", value: "rotate-right" },
398
+ {
399
+ ariaLabel: "90° Right",
400
+ icon: "rotate-cw",
401
+ label: "90°",
402
+ value: "rotate-right",
403
+ },
282
404
  {
283
405
  ariaLabel: "Flip horizontal",
284
406
  icon: "flip-horizontal",