@pixel-point/toolcraft 0.0.17 → 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 (196) hide show
  1. package/package.json +1 -1
  2. package/src/generate-test-doc-assertions.mjs +58 -68
  3. package/src/generate-test-doc-assertions.test.mjs +43 -0
  4. package/src/generate-test-doc-policy-assertions.mjs +92 -0
  5. package/src/generate-test-file-assertions.mjs +26 -75
  6. package/src/generate-test-product-iteration.test.mjs +17 -0
  7. package/src/generate-test-runtime-boundary-assertions.mjs +158 -0
  8. package/src/generate-test-standalone-assertions.mjs +10 -0
  9. package/templates/runtime/contracts/component-contracts.choices.test.ts +47 -3
  10. package/templates/runtime/contracts/component-contracts.choices.ts +48 -3
  11. package/templates/runtime/contracts/component-contracts.inputs.test.ts +6 -6
  12. package/templates/runtime/contracts/component-contracts.inputs.ts +6 -6
  13. package/templates/runtime/contracts/component-contracts.media-custom.test.ts +8 -2
  14. package/templates/runtime/contracts/component-contracts.media-custom.ts +8 -2
  15. package/templates/runtime/contracts/component-contracts.runtime-acceptance.test.ts +4 -4
  16. package/templates/runtime/contracts/component-contracts.runtime-labels.test.ts +7 -1
  17. package/templates/runtime/contracts/component-contracts.runtime-panels.test.ts +3 -0
  18. package/templates/runtime/contracts/component-contracts.runtime-shell.test.ts +3 -0
  19. package/templates/runtime/contracts/component-contracts.runtime.ts +12 -8
  20. package/templates/runtime/contracts/component-contracts.test.ts +2 -0
  21. package/templates/runtime/contracts/decision-contracts.test.ts +18 -0
  22. package/templates/runtime/contracts/decision-contracts.ts +10 -10
  23. package/templates/runtime/export/export-background.ts +0 -19
  24. package/templates/runtime/export/export.test.ts +45 -42
  25. package/templates/runtime/export/index.ts +44 -16
  26. package/templates/runtime/index.ts +2 -0
  27. package/templates/runtime/model-import/model-import-limits.ts +15 -4
  28. package/templates/runtime/react/app-shell/runtime-public-api.test.tsx +11 -1
  29. package/templates/runtime/react/app-shell/toolcraft-app.scene-export.test.tsx +29 -0
  30. package/templates/runtime/react/app-shell/toolcraft-app.tsx +18 -8
  31. package/templates/runtime/react/canvas/canvas-shell-rendering.test.tsx +76 -0
  32. package/templates/runtime/react/canvas/canvas-shell-test-utils.tsx +13 -1
  33. package/templates/runtime/react/canvas/canvas-shell.tsx +16 -1
  34. package/templates/runtime/react/canvas/product-scene-surface.test.tsx +195 -0
  35. package/templates/runtime/react/canvas/product-scene-surface.tsx +155 -0
  36. package/templates/runtime/react/canvas/runtime-scene-export.ts +1 -1
  37. package/templates/runtime/react/controls-panel/__tests__/control-conditions.test.ts +61 -0
  38. package/templates/runtime/react/controls-panel/__tests__/controls-panel.collection-actions.test.tsx +89 -0
  39. package/templates/runtime/react/controls-panel/__tests__/controls-panel.control-visibility.test.tsx +40 -19
  40. package/templates/runtime/react/controls-panel/__tests__/controls-panel.scene-export-actions.test.tsx +1 -1
  41. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-collection.test.tsx +62 -0
  42. package/templates/runtime/react/controls-panel/__tests__/controls-panel.source-color-bank.test.tsx +67 -0
  43. package/templates/runtime/react/controls-panel/actions/controls-panel-actions.ts +2 -4
  44. package/templates/runtime/react/controls-panel/actions/export-action-runner.ts +10 -4
  45. package/templates/runtime/react/controls-panel/conditions/control-conditions.ts +32 -119
  46. package/templates/runtime/react/controls-panel/layout/controls-panel-control-group.tsx +5 -2
  47. package/templates/runtime/react/controls-panel/layout/controls-panel-layout.tsx +10 -8
  48. package/templates/runtime/react/controls-panel/layout/controls-panel-section.tsx +3 -3
  49. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.test.tsx +63 -0
  50. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-item-fields.tsx +138 -0
  51. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-items.tsx +104 -0
  52. package/templates/runtime/react/controls-panel/renderers/controls-panel-collection-renderer.tsx +115 -236
  53. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.test.tsx +95 -0
  54. package/templates/runtime/react/controls-panel/renderers/controls-panel-file-drop-item-controls.tsx +82 -0
  55. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.test.tsx +73 -1
  56. package/templates/runtime/react/controls-panel/renderers/controls-panel-media-renderer.tsx +50 -1
  57. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.test.ts +1 -1
  58. package/templates/runtime/react/controls-panel/renderers/controls-panel-renderer-registry.ts +2 -1
  59. package/templates/runtime/react/controls-panel/values/controls-panel-collection-values.ts +38 -6
  60. package/templates/runtime/react/controls-panel/values/controls-panel-value-labels.ts +1 -0
  61. package/templates/runtime/react/index.ts +4 -0
  62. package/templates/runtime/react/model-rendering/model-export.ts +1 -1
  63. package/templates/runtime/react/orientation-gizmo/orientation-gizmo-selection.ts +5 -5
  64. package/templates/runtime/react/orientation-gizmo/use-toolcraft-orientation-control-selection.ts +2 -2
  65. package/templates/runtime/scene/scene-bounds.test.ts +23 -0
  66. package/templates/runtime/scene/scene-bounds.ts +31 -17
  67. package/templates/runtime/schema/app-capabilities.test.ts +4 -0
  68. package/templates/runtime/schema/collection-item-controls.ts +29 -0
  69. package/templates/runtime/schema/control-applicability.test.ts +127 -0
  70. package/templates/runtime/schema/control-applicability.ts +433 -0
  71. package/templates/runtime/schema/control-schema-normalization.ts +177 -11
  72. package/templates/runtime/schema/controls-panel-actions.ts +2 -2
  73. package/templates/runtime/schema/controls-panel-section-layout.ts +14 -9
  74. package/templates/runtime/schema/controls-panel-section-roundtrip.test.ts +59 -0
  75. package/templates/runtime/schema/define-toolcraft.collection-actions-item-controls.test.ts +127 -0
  76. package/templates/runtime/schema/define-toolcraft.control-sections.test.ts +10 -4
  77. package/templates/runtime/schema/define-toolcraft.file-drop-item-controls.test.ts +98 -0
  78. package/templates/runtime/schema/define-toolcraft.file-drop-normalization.test.ts +27 -0
  79. package/templates/runtime/schema/define-toolcraft.segmented-control-fit.test.ts +20 -2
  80. package/templates/runtime/schema/define-toolcraft.setup-canvas.test.ts +16 -3
  81. package/templates/runtime/schema/define-toolcraft.source-collection.test.ts +79 -0
  82. package/templates/runtime/schema/runtime-setup-section.ts +24 -5
  83. package/templates/runtime/schema/types.ts +68 -13
  84. package/templates/starter/AGENTS.md +12 -10
  85. package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -31
  86. package/templates/starter/docs/toolcraft/agent-worklog.md +15 -0
  87. package/templates/starter/docs/toolcraft/assembly-workflow.md +10 -3
  88. package/templates/starter/docs/toolcraft/component-rules.md +8 -6
  89. package/templates/starter/docs/toolcraft/core/control-selection.md +9 -3
  90. package/templates/starter/docs/toolcraft/core/layout.md +7 -7
  91. package/templates/starter/docs/toolcraft/core/media-upload.md +2 -0
  92. package/templates/starter/docs/toolcraft/core/runtime-boundary.md +13 -2
  93. package/templates/starter/docs/toolcraft/core/setup-export.md +33 -7
  94. package/templates/starter/docs/toolcraft/custom-controls.md +6 -4
  95. package/templates/starter/docs/toolcraft/decision-contract.md +3 -1
  96. package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
  97. package/templates/starter/docs/toolcraft/schema-reference.md +21 -19
  98. package/templates/starter/e2e/app-browser-control-applicability-requirements.spec.ts +163 -0
  99. package/templates/starter/e2e/app-browser-infinity-canvas-evidence.spec.ts +7 -2
  100. package/templates/starter/e2e/app-browser-orientation-evidence.spec.ts +174 -21
  101. package/templates/starter/e2e/app-browser-runtime-requirements.spec.ts +1 -33
  102. package/templates/starter/e2e/app-browser-semantic-evidence.spec.ts +61 -15
  103. package/templates/starter/e2e/{browser-conditional-output-evidence-helpers.ts → browser-background-output-evidence.ts} +0 -68
  104. package/templates/starter/e2e/browser-control-applicability-evidence.ts +114 -0
  105. package/templates/starter/e2e/browser-infinity-canvas-evidence.ts +20 -11
  106. package/templates/starter/e2e/browser-orientation-gizmo-evidence-helpers.ts +4 -0
  107. package/templates/starter/e2e/browser-orientation-gizmo-live-preconditions.ts +86 -0
  108. package/templates/starter/e2e/browser-runtime-applicability-requirements.ts +84 -0
  109. package/templates/starter/e2e/browser-runtime-evidence-reporter.ts +9 -2
  110. package/templates/starter/e2e/browser-runtime-evidence-requirements.ts +29 -26
  111. package/templates/starter/e2e/browser-semantic-evidence-test-helpers.ts +25 -0
  112. package/templates/starter/scripts/toolcraft-contract-manifest.json +1 -0
  113. package/templates/starter/scripts/toolcraft-product-boundary-ast.mjs +85 -115
  114. package/templates/starter/scripts/toolcraft-product-boundary-module-policy.mjs +101 -0
  115. package/templates/starter/scripts/toolcraft-product-boundary.test.mjs +0 -72
  116. package/templates/starter/scripts/toolcraft-product-control-bindings.mjs +182 -0
  117. package/templates/starter/scripts/toolcraft-product-control-boundary.mjs +146 -0
  118. package/templates/starter/scripts/toolcraft-product-control-boundary.test.mjs +186 -0
  119. package/templates/starter/scripts/toolcraft-product-export-boundary.mjs +6 -17
  120. package/templates/starter/scripts/toolcraft-product-export-boundary.test.mjs +196 -0
  121. package/templates/starter/scripts/toolcraft-typescript-source-evidence-structure.test.mjs +20 -0
  122. package/templates/starter/scripts/toolcraft-typescript-source-evidence.mjs +1 -0
  123. package/templates/starter/scripts/toolcraft-unavailable-resource-evidence-facade.test.mjs +2 -0
  124. package/templates/starter/src/app/acceptance/artifact-export-intent.ts +130 -0
  125. package/templates/starter/src/app/acceptance/conditions.ts +1 -1
  126. package/templates/starter/src/app/acceptance/control-acceptance-context.ts +2 -2
  127. package/templates/starter/src/app/acceptance/control-acceptance-coverage.ts +6 -20
  128. package/templates/starter/src/app/acceptance/control-acceptance-kind-rules.ts +24 -5
  129. package/templates/starter/src/app/acceptance/control-acceptance-policy.ts +2 -13
  130. package/templates/starter/src/app/acceptance/control-applicability-cases.test.ts +321 -0
  131. package/templates/starter/src/app/acceptance/control-applicability-cases.ts +290 -0
  132. package/templates/starter/src/app/acceptance/control-applicability.ts +332 -0
  133. package/templates/starter/src/app/acceptance/control-layout-dependency-rules.ts +20 -9
  134. package/templates/starter/src/app/acceptance/control-layout-entity-rules.ts +4 -6
  135. package/templates/starter/src/app/acceptance/control-layout-model.ts +4 -4
  136. package/templates/starter/src/app/acceptance/control-layout-section-rules.ts +3 -10
  137. package/templates/starter/src/app/acceptance/control-parts.ts +8 -0
  138. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.test.ts +215 -0
  139. package/templates/starter/src/app/acceptance/control-section-entity-cohesion.ts +132 -0
  140. package/templates/starter/src/app/acceptance/control-section-inventory.test.ts +87 -0
  141. package/templates/starter/src/app/acceptance/control-section-inventory.ts +74 -44
  142. package/templates/starter/src/app/acceptance/custom-controls.ts +11 -3
  143. package/templates/starter/src/app/acceptance/inline-layout.ts +64 -9
  144. package/templates/starter/src/app/acceptance/media-upload.ts +5 -1
  145. package/templates/starter/src/app/acceptance/orientation-gizmo.ts +18 -11
  146. package/templates/starter/src/app/acceptance/output-background-rules.ts +2 -2
  147. package/templates/starter/src/app/acceptance/output-export-layout-rules.ts +18 -10
  148. package/templates/starter/src/app/acceptance/output-export-model.ts +54 -11
  149. package/templates/starter/src/app/acceptance/output-export.ts +22 -10
  150. package/templates/starter/src/app/acceptance/output-video-export-rules.ts +99 -0
  151. package/templates/starter/src/app/acceptance/types.ts +23 -8
  152. package/templates/starter/src/app/acceptance/validate-coverage.ts +19 -2
  153. package/templates/starter/src/app/starter-acceptance.artifact-export-intent.test.ts +239 -0
  154. package/templates/starter/src/app/starter-acceptance.background-export.test.ts +33 -0
  155. package/templates/starter/src/app/starter-acceptance.canvas-sizing.test.ts +16 -1
  156. package/templates/starter/src/app/starter-acceptance.compound-part-coverage.test.ts +65 -0
  157. package/templates/starter/src/app/starter-acceptance.contract-fixtures.test.ts +41 -0
  158. package/templates/starter/src/app/starter-acceptance.contract-fixtures.ts +112 -16
  159. package/templates/starter/src/app/starter-acceptance.control-applicability.test.ts +224 -0
  160. package/templates/starter/src/app/starter-acceptance.control-order.test.ts +58 -3
  161. package/templates/starter/src/app/starter-acceptance.control-state.test.ts +0 -111
  162. package/templates/starter/src/app/starter-acceptance.custom-control-collections.test.ts +3 -1
  163. package/templates/starter/src/app/starter-acceptance.custom-control-valid.test.ts +1 -1
  164. package/templates/starter/src/app/starter-acceptance.export-test-utils.ts +22 -0
  165. package/templates/starter/src/app/{starter-acceptance.image-video-export-settings.test.ts → starter-acceptance.image-export-settings.test.ts} +37 -9
  166. package/templates/starter/src/app/starter-acceptance.interaction-ownership.test.ts +12 -0
  167. package/templates/starter/src/app/starter-acceptance.media-upload.fixtures.ts +11 -1
  168. package/templates/starter/src/app/starter-acceptance.media-upload.test.ts +28 -0
  169. package/templates/starter/src/app/starter-acceptance.output-export-intent.test.ts +384 -0
  170. package/templates/starter/src/app/starter-acceptance.output-export-rules.test.ts +4 -0
  171. package/templates/starter/src/app/starter-acceptance.product-output-export.test.ts +112 -105
  172. package/templates/starter/src/app/starter-acceptance.product-readiness.test.ts +12 -0
  173. package/templates/starter/src/app/starter-acceptance.section-cohesion.test.ts +1 -1
  174. package/templates/starter/src/app/starter-acceptance.section-dependencies.test.ts +35 -24
  175. package/templates/starter/src/app/starter-acceptance.section-size.test.ts +72 -0
  176. package/templates/starter/src/app/starter-acceptance.ts +11 -2
  177. package/templates/starter/src/app/starter-acceptance.video-export-settings.test.ts +336 -0
  178. package/templates/starter/src/app/starter-acceptance.view-interaction.test.ts +8 -0
  179. package/templates/starter/src/app/test-evidence/browser-runtime-contract.ts +2 -2
  180. package/templates/starter/src/routes/index.tsx +1 -0
  181. package/templates/ui/components/composites/tabs.tsx +2 -2
  182. package/templates/ui/components/controls/collection-actions/collection-item-groups.test.tsx +38 -0
  183. package/templates/ui/components/controls/collection-actions/collection-item-groups.tsx +36 -0
  184. package/templates/ui/components/controls/collection-actions/index.ts +2 -0
  185. package/templates/ui/components/controls/file-drop/file-drop-collection-control.tsx +137 -0
  186. package/templates/ui/components/controls/file-drop/file-drop-control.test.tsx +189 -0
  187. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +81 -88
  188. package/templates/ui/components/controls/file-drop/file-drop-presentation.tsx +131 -9
  189. package/templates/ui/components/controls/file-drop/file-drop-types.ts +36 -22
  190. package/templates/ui/components/controls/file-drop/index.ts +1 -0
  191. package/templates/ui/components/controls/index.ts +3 -0
  192. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +1 -1
  193. package/templates/ui/components/controls/range-slider/range-slider-value.test.ts +14 -0
  194. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -2
  195. package/templates/starter/src/app/acceptance/condition-exclusivity.test.ts +0 -60
  196. package/templates/starter/src/app/acceptance/condition-exclusivity.ts +0 -221
@@ -136,7 +136,7 @@ describe("ControlsPanel conditional control visibility", () => {
136
136
  ).toHaveLength(0);
137
137
  });
138
138
 
139
- it("renders only controls that match the current visibleWhen mode", async () => {
139
+ it("renders only applicable controls and preserves hidden values", async () => {
140
140
  const schema = defineToolcraft({
141
141
  canvas: { enabled: true },
142
142
  panels: {
@@ -145,6 +145,7 @@ describe("ControlsPanel conditional control visibility", () => {
145
145
  {
146
146
  controls: {
147
147
  identityMode: {
148
+ applicability: { mode: "always" },
148
149
  defaultValue: "text",
149
150
  label: "Identity",
150
151
  options: [
@@ -155,24 +156,30 @@ describe("ControlsPanel conditional control visibility", () => {
155
156
  type: "segmented",
156
157
  },
157
158
  partnerName: {
159
+ applicability: {
160
+ all: [
161
+ { equals: "text", target: "coBrand.identityMode" },
162
+ { equals: true, target: "coBrand.partnerEnabled" },
163
+ ],
164
+ mode: "conditional",
165
+ },
158
166
  defaultValue: "tRPC",
159
167
  label: "Partner",
160
168
  target: "coBrand.partnerName",
161
169
  type: "text",
162
- visibleWhen: {
163
- equals: "text",
164
- target: "coBrand.identityMode",
165
- },
166
170
  },
167
171
  partnerLogo: {
172
+ applicability: {
173
+ all: [
174
+ { equals: "logo", target: "coBrand.identityMode" },
175
+ { equals: true, target: "coBrand.partnerEnabled" },
176
+ ],
177
+ mode: "conditional",
178
+ },
168
179
  defaultValue: "logo.svg",
169
180
  label: "Partner logo",
170
181
  target: "coBrand.partnerLogo",
171
182
  type: "text",
172
- visibleWhen: {
173
- equals: "logo",
174
- target: "coBrand.identityMode",
175
- },
176
183
  },
177
184
  },
178
185
  title: "Partner Lockup",
@@ -183,7 +190,9 @@ describe("ControlsPanel conditional control visibility", () => {
183
190
  },
184
191
  });
185
192
 
186
- renderControlsPanelWithSchema(schema);
193
+ renderControlsPanelWithSchema(schema, {}, {
194
+ values: { "coBrand.partnerEnabled": true },
195
+ });
187
196
 
188
197
  const partnerInput = screen.getByDisplayValue("tRPC");
189
198
 
@@ -208,7 +217,7 @@ describe("ControlsPanel conditional control visibility", () => {
208
217
  });
209
218
  });
210
219
 
211
- it("renders count-dependent controls through numeric visibleWhen conditions", () => {
220
+ it("renders count-dependent controls through numeric applicability", () => {
212
221
  const schema = defineToolcraft({
213
222
  canvas: { enabled: true },
214
223
  panels: {
@@ -217,6 +226,7 @@ describe("ControlsPanel conditional control visibility", () => {
217
226
  {
218
227
  controls: {
219
228
  shadeCount: {
229
+ applicability: { mode: "always" },
220
230
  defaultValue: 2,
221
231
  label: "Shades",
222
232
  max: 5,
@@ -227,30 +237,41 @@ describe("ControlsPanel conditional control visibility", () => {
227
237
  variant: "discrete",
228
238
  },
229
239
  shade1: {
240
+ applicability: { mode: "always" },
230
241
  defaultValue: { hex: "#FFFFFF" },
231
242
  label: "Shade 1",
232
243
  target: "shapes.color1",
233
244
  type: "color",
234
245
  },
235
246
  shade2: {
247
+ applicability: {
248
+ all: [
249
+ {
250
+ greaterThanOrEqual: 2,
251
+ target: "shapes.shadeCount",
252
+ },
253
+ ],
254
+ mode: "conditional",
255
+ },
236
256
  defaultValue: { hex: "#DDDDDD" },
237
257
  label: "Shade 2",
238
258
  target: "shapes.color2",
239
259
  type: "color",
240
- visibleWhen: {
241
- greaterThanOrEqual: 2,
242
- target: "shapes.shadeCount",
243
- },
244
260
  },
245
261
  shade3: {
262
+ applicability: {
263
+ all: [
264
+ {
265
+ greaterThanOrEqual: 3,
266
+ target: "shapes.shadeCount",
267
+ },
268
+ ],
269
+ mode: "conditional",
270
+ },
246
271
  defaultValue: { hex: "#BBBBBB" },
247
272
  label: "Shade 3",
248
273
  target: "shapes.color3",
249
274
  type: "color",
250
- visibleWhen: {
251
- greaterThanOrEqual: 3,
252
- target: "shapes.shadeCount",
253
- },
254
275
  },
255
276
  },
256
277
  title: "Shapes",
@@ -1,4 +1,4 @@
1
- import { ToolcraftSceneExportError } from "../../../export";
1
+ import { ToolcraftSceneExportError } from "../../../export/export-frame";
2
2
  import { defaultToolcraftRuntimeSceneVisibility } from "../../../scene";
3
3
  import type { ToolcraftActionSchema } from "../../../schema/types";
4
4
  import {
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ defineToolcraft,
4
+ fireEvent,
5
+ renderControlsPanelWithSchema,
6
+ screen,
7
+ } from "../testing/controls-panel-test-utils";
8
+
9
+ describe("ControlsPanel source collection", () => {
10
+ it("renders source-sized built-in color controls without cardinality actions", () => {
11
+ const schema = defineToolcraft({
12
+ canvas: { enabled: false },
13
+ panels: {
14
+ controls: {
15
+ sections: [
16
+ {
17
+ controls: {
18
+ sections: {
19
+ defaultValue: [{ hex: "#DFFF1A" }, { hex: "#8CFF3A" }],
20
+ itemControl: {
21
+ label: "Section",
22
+ type: "color",
23
+ },
24
+ label: "Sections",
25
+ target: "fills.sections",
26
+ type: "sourceCollection",
27
+ },
28
+ },
29
+ title: "Fill Sections",
30
+ },
31
+ ],
32
+ title: "Controls",
33
+ },
34
+ },
35
+ });
36
+
37
+ const { container } = renderControlsPanelWithSchema(schema);
38
+
39
+ expect(screen.queryByRole("button", { name: /Add/u })).toBeNull();
40
+ expect(screen.queryByRole("button", { name: /Remove/u })).toBeNull();
41
+ expect(screen.getByRole("button", { name: "Pick Section 1" })).toBeTruthy();
42
+ expect(screen.getByRole("textbox", { name: "Section 2 hex" })).toBeTruthy();
43
+ expect(
44
+ container.querySelector('[data-slot="source-collection-items-grid"]')
45
+ ?.children,
46
+ ).toHaveLength(2);
47
+
48
+ const secondColor = screen.getByRole("textbox", { name: "Section 2 hex" });
49
+
50
+ fireEvent.change(secondColor, { target: { value: "#123456" } });
51
+ fireEvent.blur(secondColor);
52
+
53
+ const values = JSON.parse(
54
+ screen.getByTestId("values-json").textContent ?? "{}",
55
+ );
56
+
57
+ expect(values["fills.sections"]).toEqual([
58
+ { hex: "#DFFF1A" },
59
+ { hex: "#123456" },
60
+ ]);
61
+ });
62
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ defineToolcraft,
4
+ fireEvent,
5
+ renderControlsPanelWithSchema,
6
+ screen,
7
+ } from "../testing/controls-panel-test-utils";
8
+
9
+ const sourceColors = Array.from({ length: 19 }, (_, index) => ({
10
+ hex: index === 18 ? "#654321" : "#DFFF1A",
11
+ }));
12
+
13
+ describe("ControlsPanel source color bank", () => {
14
+ it("keeps a source-sized badge palette on built-in color controls", () => {
15
+ const schema = defineToolcraft({
16
+ canvas: { enabled: false },
17
+ panels: {
18
+ controls: {
19
+ sections: [
20
+ {
21
+ controls: {
22
+ sections: {
23
+ defaultValue: sourceColors,
24
+ itemControl: {
25
+ label: "Section",
26
+ type: "color",
27
+ },
28
+ label: "Sections",
29
+ target: "fills.sections",
30
+ type: "sourceCollection",
31
+ },
32
+ },
33
+ title: "Fill Sections",
34
+ },
35
+ ],
36
+ title: "Controls",
37
+ },
38
+ },
39
+ });
40
+
41
+ const { container } = renderControlsPanelWithSchema(schema);
42
+
43
+ expect(
44
+ screen.getAllByRole("button", { name: /^Pick Section \d+$/u }),
45
+ ).toHaveLength(19);
46
+ expect(screen.getByRole("button", { name: "Pick Section 19" })).toBeTruthy();
47
+ expect(screen.queryByRole("button", { name: "Pick Section 20" })).toBeNull();
48
+ expect(screen.queryByRole("button", { name: /Add/u })).toBeNull();
49
+ expect(screen.queryByRole("button", { name: /Remove/u })).toBeNull();
50
+ expect(
51
+ container.querySelector('[data-slot="source-collection-items-grid"]')
52
+ ?.children,
53
+ ).toHaveLength(19);
54
+
55
+ const lastColor = screen.getByRole("textbox", { name: "Section 19 hex" });
56
+ fireEvent.change(lastColor, { target: { value: "#123456" } });
57
+ fireEvent.blur(lastColor);
58
+
59
+ const values = JSON.parse(
60
+ screen.getByTestId("values-json").textContent ?? "{}",
61
+ );
62
+ expect(values["fills.sections"]).toEqual([
63
+ ...sourceColors.slice(0, 18),
64
+ { hex: "#123456" },
65
+ ]);
66
+ });
67
+ });
@@ -2,10 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
 
5
- import {
6
- ToolcraftArtifactExportError,
7
- ToolcraftSceneExportError,
8
- } from "../../../export";
5
+ import { ToolcraftArtifactExportError } from "../../../export/export-error";
6
+ import { ToolcraftSceneExportError } from "../../../export/export-frame";
9
7
  import type { ToolcraftRendererPipelineClient } from "../../../rendering";
10
8
  import { defaultToolcraftRuntimeSceneVisibility } from "../../../scene";
11
9
  import type {
@@ -1,10 +1,16 @@
1
+ import type {
2
+ ToolcraftArtifactExportRequest,
3
+ } from "../../../export/artifact-export-request";
4
+ import type { ToolcraftExportFrame } from "../../../export/export-frame";
1
5
  import {
2
6
  exportToolcraftImageArtifact,
7
+ } from "../../../export/image-artifact-export";
8
+ import type {
9
+ ToolcraftProductExportRenderer,
10
+ } from "../../../export/product-export-renderer";
11
+ import {
3
12
  exportToolcraftVideoArtifact,
4
- type ToolcraftArtifactExportRequest,
5
- type ToolcraftExportFrame,
6
- type ToolcraftProductExportRenderer,
7
- } from "../../../export";
13
+ } from "../../../export/video-artifact-export";
8
14
  import type { ToolcraftRendererPipelineClient } from "../../../rendering";
9
15
  import type {
10
16
  ToolcraftProductSceneBoundsProvider,
@@ -3,67 +3,21 @@ import {
3
3
  isToolcraftTimelinePanelExtendedTarget,
4
4
  isToolcraftTimelinePanelVisibleTarget,
5
5
  } from "../../../schema/runtime-targets";
6
+ import {
7
+ doesToolcraftApplicabilityMatch,
8
+ doesToolcraftPredicateMatchValue,
9
+ getToolcraftApplicabilityTargets,
10
+ } from "../../../schema/control-applicability";
6
11
  import type {
7
12
  ToolcraftControlConditionSchema,
8
- ToolcraftControlSchema,
9
- ToolcraftControlSectionSchema,
13
+ ResolvedToolcraftControlSchema,
14
+ ResolvedToolcraftControlSectionSchema,
10
15
  } from "../../../schema/types";
11
16
  import type { ToolcraftState } from "../../../state/types";
12
17
  import { readToolcraftCanvasRuntimeTarget } from "../../../state/canvas-frame";
13
18
 
14
- function valuesEqual(first: unknown, second: unknown): boolean {
15
- if (Object.is(first, second)) {
16
- return true;
17
- }
18
-
19
- if (
20
- (typeof first !== "object" || first === null) &&
21
- (typeof second !== "object" || second === null)
22
- ) {
23
- return false;
24
- }
25
-
26
- try {
27
- return JSON.stringify(first) === JSON.stringify(second);
28
- } catch {
29
- return false;
30
- }
31
- }
32
-
33
- function valuesInclude(value: unknown, options: readonly unknown[]): boolean {
34
- return options.some((option) => valuesEqual(value, option));
35
- }
36
-
37
- function readComparableNumber(value: unknown): number | null {
38
- if (typeof value === "number" && Number.isFinite(value)) {
39
- return value;
40
- }
41
-
42
- if (typeof value === "string") {
43
- const numberValue = Number(value.trim());
44
-
45
- return Number.isFinite(numberValue) ? numberValue : null;
46
- }
47
-
48
- return null;
49
- }
50
-
51
- function compareConditionNumbers(
52
- value: unknown,
53
- expected: number | undefined,
54
- comparator: (value: number, expected: number) => boolean,
55
- ): boolean | null {
56
- if (typeof expected !== "number" || !Number.isFinite(expected)) {
57
- return null;
58
- }
59
-
60
- const numberValue = readComparableNumber(value);
61
-
62
- return numberValue === null ? false : comparator(numberValue, expected);
63
- }
64
-
65
19
  function getControlDefaultValueByTarget(
66
- sections: readonly ToolcraftControlSectionSchema[],
20
+ sections: readonly ResolvedToolcraftControlSectionSchema[],
67
21
  target: string,
68
22
  ): unknown {
69
23
  for (const section of sections) {
@@ -119,22 +73,28 @@ export function getToolcraftConditionTargets(
119
73
  }
120
74
 
121
75
  export function getToolcraftControlConditionTargets(
122
- control: ToolcraftControlSchema,
76
+ control: ResolvedToolcraftControlSchema,
123
77
  ): string[] {
124
- return getToolcraftConditionTargets(
125
- control.visibleWhen,
126
- control.disabledWhen,
127
- );
78
+ return [
79
+ ...new Set([
80
+ ...getToolcraftApplicabilityTargets(
81
+ control.applicability,
82
+ ),
83
+ ...getToolcraftConditionTargets(control.disabledWhen),
84
+ ]),
85
+ ];
128
86
  }
129
87
 
130
88
  export function getToolcraftControlVisibilityTargets(
131
- control: ToolcraftControlSchema,
89
+ control: ResolvedToolcraftControlSchema,
132
90
  ): string[] {
133
- return getToolcraftConditionTargets(control.visibleWhen);
91
+ return getToolcraftApplicabilityTargets(
92
+ control.applicability,
93
+ );
134
94
  }
135
95
 
136
96
  export function getToolcraftSectionVisibilityTargets(
137
- section: ToolcraftControlSectionSchema,
97
+ section: ResolvedToolcraftControlSectionSchema,
138
98
  ): string[] {
139
99
  return getToolcraftConditionTargets(section.visibleWhen);
140
100
  }
@@ -143,63 +103,15 @@ export function toolcraftConditionMatches(
143
103
  state: ToolcraftState,
144
104
  condition: ToolcraftControlConditionSchema,
145
105
  ): boolean {
146
- const value = getToolcraftTargetValue(state, condition.target);
147
- const matches: boolean[] = [];
148
-
149
- if ("equals" in condition) {
150
- matches.push(valuesEqual(value, condition.equals));
151
- }
152
-
153
- if ("notEquals" in condition) {
154
- matches.push(!valuesEqual(value, condition.notEquals));
155
- }
156
-
157
- if (Array.isArray(condition.oneOf)) {
158
- matches.push(valuesInclude(value, condition.oneOf));
159
- }
160
-
161
- if (Array.isArray(condition.notOneOf)) {
162
- matches.push(!valuesInclude(value, condition.notOneOf));
163
- }
164
-
165
- const greaterThan = compareConditionNumbers(
166
- value,
167
- condition.greaterThan,
168
- (numberValue, expected) => numberValue > expected,
169
- );
170
- const greaterThanOrEqual = compareConditionNumbers(
171
- value,
172
- condition.greaterThanOrEqual,
173
- (numberValue, expected) => numberValue >= expected,
106
+ return doesToolcraftPredicateMatchValue(
107
+ condition,
108
+ getToolcraftTargetValue(state, condition.target),
174
109
  );
175
- const lessThan = compareConditionNumbers(
176
- value,
177
- condition.lessThan,
178
- (numberValue, expected) => numberValue < expected,
179
- );
180
- const lessThanOrEqual = compareConditionNumbers(
181
- value,
182
- condition.lessThanOrEqual,
183
- (numberValue, expected) => numberValue <= expected,
184
- );
185
-
186
- for (const match of [
187
- greaterThan,
188
- greaterThanOrEqual,
189
- lessThan,
190
- lessThanOrEqual,
191
- ]) {
192
- if (match !== null) {
193
- matches.push(match);
194
- }
195
- }
196
-
197
- return matches.length > 0 && matches.every(Boolean);
198
110
  }
199
111
 
200
112
  export function isToolcraftSectionVisible(
201
113
  state: ToolcraftState,
202
- section: ToolcraftControlSectionSchema,
114
+ section: ResolvedToolcraftControlSectionSchema,
203
115
  ): boolean {
204
116
  return section.visibleWhen
205
117
  ? toolcraftConditionMatches(state, section.visibleWhen)
@@ -208,16 +120,17 @@ export function isToolcraftSectionVisible(
208
120
 
209
121
  export function isToolcraftControlVisible(
210
122
  state: ToolcraftState,
211
- control: ToolcraftControlSchema,
123
+ control: ResolvedToolcraftControlSchema,
212
124
  ): boolean {
213
- return control.visibleWhen
214
- ? toolcraftConditionMatches(state, control.visibleWhen)
215
- : true;
125
+ return doesToolcraftApplicabilityMatch(
126
+ control.applicability,
127
+ (target) => getToolcraftTargetValue(state, target),
128
+ );
216
129
  }
217
130
 
218
131
  export function isToolcraftControlDisabled(
219
132
  state: ToolcraftState,
220
- control: ToolcraftControlSchema,
133
+ control: ResolvedToolcraftControlSchema,
221
134
  ): boolean {
222
135
  if (control.disabled) {
223
136
  return true;
@@ -29,7 +29,7 @@ import { LiveCustomControl } from "../live-custom-control";
29
29
  import type { ActionControlRunAction } from "../renderers/controls-panel-action-renderer";
30
30
  import { renderActionControl } from "../renderers/controls-panel-action-renderer";
31
31
  import { renderBasicControl } from "../renderers/controls-panel-basic-renderers";
32
- import { renderCollectionActionsControl } from "../renderers/controls-panel-collection-renderer";
32
+ import { renderCollectionControl } from "../renderers/controls-panel-collection-renderer";
33
33
  import {
34
34
  renderCompoundColorGroup,
35
35
  renderCompoundControl,
@@ -419,7 +419,7 @@ export const ControlsPanelControlGroup = React.memo(
419
419
  node = null;
420
420
  break;
421
421
  case "collection":
422
- node = renderCollectionActionsControl({
422
+ node = renderCollectionControl({
423
423
  control,
424
424
  name,
425
425
  setControlValue,
@@ -448,7 +448,10 @@ export const ControlsPanelControlGroup = React.memo(
448
448
  id,
449
449
  layerSelectionEnabled: !mediaPreviewEnabled,
450
450
  mediaAssets: mediaPreviewEnabled ? (selection.mediaAssets ?? []) : [],
451
+ name,
452
+ setControlValue,
451
453
  sourceAssetCoordinator,
454
+ value,
452
455
  });
453
456
  break;
454
457
  case "settings":
@@ -3,7 +3,8 @@
3
3
  import * as React from "react";
4
4
 
5
5
  import type {
6
- ToolcraftControlSectionSchema,
6
+ ResolvedToolcraftControlSectionSchema,
7
+ ResolvedToolcraftControlSchema,
7
8
  ToolcraftControlSchema,
8
9
  } from "../../../schema/types";
9
10
  import {
@@ -12,7 +13,7 @@ import {
12
13
  } from "../../../schema/runtime-targets";
13
14
  import { getToolcraftControlRendererKind } from "../renderers/controls-panel-renderer-registry";
14
15
 
15
- export type ControlEntry = [string, ToolcraftControlSchema];
16
+ export type ControlEntry = [string, ResolvedToolcraftControlSchema];
16
17
 
17
18
  export type ControlRenderGroup =
18
19
  | { entries: readonly ControlEntry[]; kind: "colorGroup" }
@@ -41,6 +42,7 @@ const hiddenDiscreteMarkerCount = 2;
41
42
  const sectionedCompoundControlTypes = new Set([
42
43
  "channelMixer",
43
44
  "collectionActions",
45
+ "sourceCollection",
44
46
  "fontPicker",
45
47
  "gradient",
46
48
  "palette",
@@ -87,7 +89,7 @@ export function getControlName(
87
89
 
88
90
  export function getControlsRecord(
89
91
  entries: readonly ControlEntry[],
90
- ): Record<string, ToolcraftControlSchema> {
92
+ ): Record<string, ResolvedToolcraftControlSchema> {
91
93
  return Object.fromEntries(entries);
92
94
  }
93
95
 
@@ -151,7 +153,7 @@ export function getControlRenderGroupIds(
151
153
  }
152
154
 
153
155
  export function countControlsByType(
154
- sections: readonly ToolcraftControlSectionSchema[],
156
+ sections: readonly ResolvedToolcraftControlSectionSchema[],
155
157
  type: string,
156
158
  ): number {
157
159
  return sections.reduce(
@@ -186,8 +188,8 @@ export function isColorOnlySection(entries: readonly ControlEntry[]): boolean {
186
188
  }
187
189
 
188
190
  export function getRenderedControlsSectionTitle(
189
- section: ToolcraftControlSectionSchema,
190
- ): ToolcraftControlSectionSchema["title"] {
191
+ section: ResolvedToolcraftControlSectionSchema,
192
+ ): ResolvedToolcraftControlSectionSchema["title"] {
191
193
  return isStickyFooterActionSection(section) ? undefined : section.title;
192
194
  }
193
195
 
@@ -196,7 +198,7 @@ export function isRuntimeSetupSection({
196
198
  section,
197
199
  }: {
198
200
  entries: readonly ControlEntry[];
199
- section: ToolcraftControlSectionSchema;
201
+ section: ResolvedToolcraftControlSectionSchema;
200
202
  }): boolean {
201
203
  return (
202
204
  section.title === "Setup" &&
@@ -205,7 +207,7 @@ export function isRuntimeSetupSection({
205
207
  }
206
208
 
207
209
  function isStickyFooterActionSection(
208
- section: ToolcraftControlSectionSchema,
210
+ section: ResolvedToolcraftControlSectionSchema,
209
211
  ): boolean {
210
212
  return (
211
213
  section.actionGroup !== undefined &&
@@ -10,7 +10,7 @@ import {
10
10
  TooltipTrigger,
11
11
  } from "@repo/ui";
12
12
 
13
- import type { ToolcraftControlSectionSchema } from "../../../schema/types";
13
+ import type { ResolvedToolcraftControlSectionSchema } from "../../../schema/types";
14
14
  import type { ToolcraftStoreDependency } from "../../../state/toolcraft-external-store-dependencies";
15
15
  import type {
16
16
  ToolcraftCommand,
@@ -56,7 +56,7 @@ export type ControlsPanelSectionGroupsArgs = {
56
56
  headerKeyframeTarget: string | null;
57
57
  keyframesSupported: boolean;
58
58
  runAction: ActionControlRunAction;
59
- section: ToolcraftControlSectionSchema;
59
+ section: ResolvedToolcraftControlSectionSchema;
60
60
  setControlValue: ControlsPanelSetControlValue;
61
61
  vectorPadShape: "compact" | "square";
62
62
  };
@@ -66,7 +66,7 @@ export type ControlsPanelSectionProps = Omit<
66
66
  "headerKeyframeTarget"
67
67
  > & {
68
68
  "data-toolcraft-panel-section": "";
69
- actionGroup: ToolcraftControlSectionSchema["actionGroup"];
69
+ actionGroup: ResolvedToolcraftControlSectionSchema["actionGroup"];
70
70
  collapsed: boolean;
71
71
  onCollapsedChange: (sectionCollapseKey: string, collapsed: boolean) => void;
72
72
  sectionCollapseKey: string;