@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
@@ -0,0 +1,95 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { ControlsPanelFileDropItemControls } from "./controls-panel-file-drop-item-controls";
5
+
6
+ describe("controls-panel FileDrop item controls", () => {
7
+ it("renders one file's built-in settings and commits a mediaId-keyed record", () => {
8
+ const setControlValue = vi.fn();
9
+
10
+ render(
11
+ <ControlsPanelFileDropItemControls
12
+ control={{
13
+ assetKind: "file",
14
+ defaultValue: [],
15
+ itemControls: {
16
+ depth: {
17
+ defaultValue: 0.2,
18
+ label: "Strength",
19
+ max: 1,
20
+ min: 0,
21
+ step: 0.1,
22
+ type: "slider",
23
+ },
24
+ invert: {
25
+ defaultValue: false,
26
+ label: "Invert",
27
+ type: "switch",
28
+ },
29
+ levels: {
30
+ defaultValue: [0, 1],
31
+ label: "Levels",
32
+ max: 1,
33
+ min: 0,
34
+ step: 0.01,
35
+ type: "rangeSlider",
36
+ },
37
+ },
38
+ multiple: true,
39
+ target: "media.bumpMaps",
40
+ type: "fileDrop",
41
+ variant: "collection-actions",
42
+ }}
43
+ index={1}
44
+ mediaId="bump-two"
45
+ name="Bump maps"
46
+ setControlValue={setControlValue}
47
+ value={[
48
+ {
49
+ depth: 0.4,
50
+ invert: true,
51
+ levels: [0.1, 0.8],
52
+ mediaId: "bump-one",
53
+ },
54
+ {
55
+ depth: 0.6,
56
+ invert: false,
57
+ levels: [0.2, 0.9],
58
+ mediaId: "bump-two",
59
+ },
60
+ ]}
61
+ />,
62
+ );
63
+
64
+ expect(screen.getByText("Strength")).toBeTruthy();
65
+ expect(screen.getByText("Invert")).toBeTruthy();
66
+ expect(screen.getByText("Levels")).toBeTruthy();
67
+
68
+ fireEvent.click(
69
+ screen.getByRole("button", { name: "Edit Strength value" }),
70
+ );
71
+ const editor = screen.getByRole("textbox", { name: "Strength value" });
72
+ editor.textContent = "0.7";
73
+ fireEvent.keyDown(editor, { key: "Enter" });
74
+
75
+ expect(setControlValue).toHaveBeenCalledWith(
76
+ "media.bumpMaps",
77
+ [
78
+ {
79
+ depth: 0.4,
80
+ invert: true,
81
+ levels: [0.1, 0.8],
82
+ mediaId: "bump-one",
83
+ },
84
+ {
85
+ depth: 0.7,
86
+ invert: false,
87
+ levels: [0.2, 0.9],
88
+ mediaId: "bump-two",
89
+ },
90
+ ],
91
+ "Bump maps 2 Strength",
92
+ undefined,
93
+ );
94
+ });
95
+ });
@@ -0,0 +1,82 @@
1
+ import type { ControlChangeMeta } from "@repo/ui";
2
+
3
+ import type { ToolcraftControlSchema } from "../../../schema/types";
4
+ import { ControlsPanelCollectionItemFields } from "./controls-panel-collection-item-fields";
5
+
6
+ type FileDropItemValue = Readonly<Record<string, unknown>> & {
7
+ mediaId: string;
8
+ };
9
+
10
+ export type ControlsPanelFileDropItemControlsProps = {
11
+ control: ToolcraftControlSchema;
12
+ index: number;
13
+ mediaId: string;
14
+ name: string;
15
+ setControlValue: (
16
+ target: string,
17
+ value: unknown,
18
+ label?: string,
19
+ meta?: ControlChangeMeta,
20
+ ) => void;
21
+ value: unknown;
22
+ };
23
+
24
+ function asFileDropItemValues(value: unknown): readonly FileDropItemValue[] {
25
+ if (!Array.isArray(value)) {
26
+ return [];
27
+ }
28
+
29
+ return value.filter(
30
+ (item): item is FileDropItemValue =>
31
+ typeof item === "object" &&
32
+ item !== null &&
33
+ typeof (item as { mediaId?: unknown }).mediaId === "string",
34
+ );
35
+ }
36
+
37
+ export function ControlsPanelFileDropItemControls({
38
+ control,
39
+ index,
40
+ mediaId,
41
+ name,
42
+ setControlValue,
43
+ value,
44
+ }: ControlsPanelFileDropItemControlsProps): React.JSX.Element | null {
45
+ const itemControls = control.itemControls;
46
+
47
+ if (!itemControls || Object.keys(itemControls).length === 0) {
48
+ return null;
49
+ }
50
+
51
+ const itemValues = asFileDropItemValues(value);
52
+ const storedItemValue = itemValues.find((item) => item.mediaId === mediaId);
53
+
54
+ return (
55
+ <ControlsPanelCollectionItemFields
56
+ controls={itemControls}
57
+ id={`${control.target}:${mediaId}`}
58
+ onChange={(nextValue, fieldName, meta) => {
59
+ const nextItemValue: FileDropItemValue = { ...nextValue, mediaId };
60
+ const existingIndex = itemValues.findIndex(
61
+ (item) => item.mediaId === mediaId,
62
+ );
63
+ const nextItems = [...itemValues];
64
+
65
+ if (existingIndex >= 0) {
66
+ nextItems[existingIndex] = nextItemValue;
67
+ } else {
68
+ nextItems.push(nextItemValue);
69
+ }
70
+
71
+ setControlValue(
72
+ control.target,
73
+ nextItems,
74
+ `${name} ${index + 1} ${fieldName}`,
75
+ meta,
76
+ );
77
+ }}
78
+ target={control.target}
79
+ value={storedItemValue}
80
+ />
81
+ );
82
+ }
@@ -52,6 +52,7 @@ describe("controls-panel media renderer", () => {
52
52
  dispatchCommand,
53
53
  id: "source-model",
54
54
  mediaAssets: [],
55
+ name: "Model",
55
56
  sourceAssetCoordinator,
56
57
  });
57
58
 
@@ -102,6 +103,7 @@ describe("controls-panel media renderer", () => {
102
103
  dispatchCommand: vi.fn(),
103
104
  id: "source-files",
104
105
  mediaAssets: [],
106
+ name: "Source files",
105
107
  sourceAssetCoordinator,
106
108
  })}
107
109
  </>,
@@ -113,7 +115,75 @@ describe("controls-panel media renderer", () => {
113
115
  undefined,
114
116
  );
115
117
  expect(legacyFileDrop.mock.calls[0]?.[0]).toEqual(
116
- expect.objectContaining({ onFolderSelect: undefined }),
118
+ expect.objectContaining({
119
+ collectionActions: undefined,
120
+ onFolderSelect: undefined,
121
+ }),
122
+ );
123
+ });
124
+
125
+ it("maps the collection-actions variant to compact FileDrop cardinality controls", () => {
126
+ const control = {
127
+ assetKind: "file" as const,
128
+ itemLabel: "bump map",
129
+ multiple: true,
130
+ target: "surface.bumpMaps",
131
+ type: "fileDrop" as const,
132
+ variant: "collection-actions",
133
+ };
134
+ const operation = { phase: "idle" as const, target: control.target };
135
+ const presentation = {
136
+ accept: "image/*",
137
+ allowsFileBatch: true,
138
+ allowsFolderSelection: false,
139
+ emptyDescription: "Drop bump maps",
140
+ emptyTitle: "Upload bump maps",
141
+ items: [{
142
+ assetKind: "file" as const,
143
+ fileName: "grain.png",
144
+ id: "bump-grain",
145
+ }],
146
+ presenter: "file-list" as const,
147
+ secondaryActions: [],
148
+ };
149
+ const sourceAssetCoordinator = {
150
+ cancelTarget: vi.fn(),
151
+ clearPresentationFeedback: vi.fn(),
152
+ dispose: vi.fn(async () => undefined),
153
+ getOperation: vi.fn(() => operation),
154
+ getPresentation: vi.fn(() => presentation),
155
+ hydrateBinaryMedia: vi.fn(async () => undefined),
156
+ hydrateModels: vi.fn(async () => undefined),
157
+ importBatch: vi.fn(),
158
+ repairModel: vi.fn(),
159
+ reportPresentationFeedback: vi.fn(),
160
+ subscribe: () => () => undefined,
161
+ } satisfies ToolcraftSourceAssetCoordinator;
162
+
163
+ render(
164
+ <>
165
+ {renderFileDropControl({
166
+ canvasSize: { height: 720, unit: "px", width: 1280 },
167
+ control,
168
+ dispatchCommand: vi.fn(),
169
+ id: "surface-bump-maps",
170
+ mediaAssets: [],
171
+ name: "Bump maps",
172
+ sourceAssetCoordinator,
173
+ })}
174
+ </>,
175
+ );
176
+
177
+ expect(legacyFileDrop.mock.calls[0]?.[0]).toEqual(
178
+ expect.objectContaining({
179
+ collectionActions: {
180
+ addLabel: "Add bump map",
181
+ canAdd: true,
182
+ itemLabel: "bump map",
183
+ name: "Bump maps",
184
+ removeLabel: "Remove bump map",
185
+ },
186
+ }),
117
187
  );
118
188
  });
119
189
 
@@ -157,6 +227,7 @@ describe("controls-panel media renderer", () => {
157
227
  dispatchCommand: vi.fn(),
158
228
  id: "source-model",
159
229
  mediaAssets: [],
230
+ name: "Model",
160
231
  sourceAssetCoordinator,
161
232
  })}
162
233
  </>,
@@ -226,6 +297,7 @@ describe("controls-panel media renderer", () => {
226
297
  dispatchCommand: vi.fn(),
227
298
  id: "source-model",
228
299
  mediaAssets: [],
300
+ name: "Model",
229
301
  sourceAssetCoordinator,
230
302
  })}
231
303
  </>,
@@ -1,5 +1,9 @@
1
1
  import * as React from "react";
2
- import { FileDrop } from "@repo/ui";
2
+ import {
3
+ FileDrop,
4
+ type ControlChangeMeta,
5
+ type FileDropPresentationItem,
6
+ } from "@repo/ui";
3
7
 
4
8
  import type { ToolcraftCanvasSize, ToolcraftControlSchema } from "../../../schema/types";
5
9
  import type { ToolcraftSourceAssetCoordinator } from "../../../source-assets/source-asset-coordinator";
@@ -11,6 +15,7 @@ import type {
11
15
  } from "../../../state/types";
12
16
  import { useToolcraftMediaPresentationUrls } from "../../app-shell/toolcraft-source-asset-context";
13
17
  import { useToolcraftSelector } from "../../app-shell/use-toolcraft";
18
+ import { ControlsPanelFileDropItemControls } from "./controls-panel-file-drop-item-controls";
14
19
 
15
20
  export type FileDropControlRenderArgs = {
16
21
  canvasSize: ToolcraftCanvasSize;
@@ -19,7 +24,15 @@ export type FileDropControlRenderArgs = {
19
24
  id: string;
20
25
  layerSelectionEnabled?: boolean;
21
26
  mediaAssets: readonly ToolcraftMediaAsset[];
27
+ name: string;
28
+ setControlValue?: (
29
+ target: string,
30
+ value: unknown,
31
+ label?: string,
32
+ meta?: ControlChangeMeta,
33
+ ) => void;
22
34
  sourceAssetCoordinator: ToolcraftSourceAssetCoordinator;
35
+ value?: unknown;
23
36
  };
24
37
 
25
38
  const IMAGE_TRANSFORM_OPERATIONS = new Set<ToolcraftMediaTransformOperation>([
@@ -43,8 +56,11 @@ function FileDropControlRenderer({
43
56
  id,
44
57
  layerSelectedImageAsset = null,
45
58
  mediaAssets,
59
+ name,
60
+ setControlValue = () => undefined,
46
61
  sourceAssetCoordinator,
47
62
  suppressMediaCollection = false,
63
+ value,
48
64
  }: Omit<
49
65
  FileDropControlRenderArgs,
50
66
  "canvasSize" | "layerSelectionEnabled"
@@ -112,6 +128,10 @@ function FileDropControlRenderer({
112
128
  ? { ...basePresentation, secondaryActions: [] }
113
129
  : basePresentation
114
130
  : null;
131
+ const usesCollectionActions = control.variant === "collection-actions";
132
+ const canAddCollectionItem =
133
+ typeof control.hardMaxItems !== "number" ||
134
+ (basePresentation?.items.length ?? 0) < control.hardMaxItems;
115
135
 
116
136
  React.useEffect(() => {
117
137
  if (selectedMediaId && !previewMediaIds.includes(selectedMediaId)) {
@@ -156,6 +176,35 @@ function FileDropControlRenderer({
156
176
 
157
177
  return (
158
178
  <FileDrop
179
+ collectionActions={
180
+ usesCollectionActions
181
+ ? {
182
+ addLabel: control.addLabel ?? `Add ${control.itemLabel ?? "file"}`,
183
+ canAdd: canAddCollectionItem,
184
+ itemLabel: control.itemLabel ?? "file",
185
+ name,
186
+ removeLabel:
187
+ control.removeLabel ?? `Remove ${control.itemLabel ?? "file"}`,
188
+ ...(control.itemControls
189
+ ? {
190
+ renderItemContent: (
191
+ item: FileDropPresentationItem,
192
+ index: number,
193
+ ) => (
194
+ <ControlsPanelFileDropItemControls
195
+ control={control}
196
+ index={index}
197
+ mediaId={item.id}
198
+ name={name}
199
+ setControlValue={setControlValue}
200
+ value={value}
201
+ />
202
+ ),
203
+ }
204
+ : {}),
205
+ }
206
+ : undefined
207
+ }
159
208
  key={id}
160
209
  onAction={
161
210
  activeImageId
@@ -30,7 +30,7 @@ describe("Toolcraft controls-panel renderer registry", () => {
30
30
  "canvas-handle": [],
31
31
  collection: ["value"],
32
32
  compound: ["value"],
33
- media: ["canvas.size", "mediaAssets"],
33
+ media: ["canvas.size", "mediaAssets", "value"],
34
34
  settings: [],
35
35
  } as const;
36
36
 
@@ -23,7 +23,7 @@ export const TOOLCRAFT_CONTROL_RENDERER_STATE_DEPENDENCIES = {
23
23
  "canvas-handle": [],
24
24
  collection: ["value"],
25
25
  compound: ["value"],
26
- media: ["canvas.size", "mediaAssets"],
26
+ media: ["canvas.size", "mediaAssets", "value"],
27
27
  settings: [],
28
28
  } as const satisfies Record<
29
29
  ToolcraftControlRendererKind,
@@ -38,6 +38,7 @@ export const TOOLCRAFT_CONTROL_RENDERER_REGISTRY = {
38
38
  checkbox: "basic",
39
39
  code: "basic",
40
40
  collectionActions: "collection",
41
+ sourceCollection: "collection",
41
42
  color: "compound",
42
43
  colorOpacity: "compound",
43
44
  curves: "compound",
@@ -1,8 +1,18 @@
1
+ import type { ToolcraftCollectionItemControlType } from "../../../schema/collection-item-controls";
1
2
  import type { ToolcraftControlSchema } from "../../../schema/types";
2
3
  import { asFontPickerValue } from "./controls-panel-font-values";
3
4
  import { asNumber } from "./controls-panel-value-primitives";
4
5
 
5
- export function asCollectionItems(value: unknown, fallback: unknown): unknown[] {
6
+ function assertNeverCollectionItemControl(value: never): never {
7
+ throw new Error(
8
+ `Unsupported Toolcraft collection item control: ${String(value)}`,
9
+ );
10
+ }
11
+
12
+ export function asCollectionItems(
13
+ value: unknown,
14
+ fallback: unknown,
15
+ ): unknown[] {
6
16
  if (Array.isArray(value)) {
7
17
  return [...value];
8
18
  }
@@ -17,18 +27,25 @@ export function getCollectionMinItems(control: ToolcraftControlSchema): number {
17
27
  export function getCollectionHardMaxItems(
18
28
  control: ToolcraftControlSchema,
19
29
  ): number | null {
20
- if (typeof control.hardMaxItems !== "number" || !Number.isFinite(control.hardMaxItems)) {
30
+ if (
31
+ typeof control.hardMaxItems !== "number" ||
32
+ !Number.isFinite(control.hardMaxItems)
33
+ ) {
21
34
  return null;
22
35
  }
23
36
 
24
37
  return Math.max(0, Math.floor(control.hardMaxItems));
25
38
  }
26
39
 
27
- export function getCollectionItemType(control: ToolcraftControlSchema): string {
40
+ export function getCollectionItemType(
41
+ control: ToolcraftControlSchema,
42
+ ): ToolcraftCollectionItemControlType {
28
43
  return control.itemControl?.type ?? "color";
29
44
  }
30
45
 
31
- export function getCollectionItemBaseLabel(control: ToolcraftControlSchema): string {
46
+ export function getCollectionItemBaseLabel(
47
+ control: ToolcraftControlSchema,
48
+ ): string {
32
49
  if (control.itemLabel) {
33
50
  return control.itemLabel;
34
51
  }
@@ -54,6 +71,15 @@ export function getCollectionItemName(
54
71
  export function getCollectionItemDefaultValue(
55
72
  control: ToolcraftControlSchema,
56
73
  ): unknown {
74
+ if (control.itemControls) {
75
+ return Object.fromEntries(
76
+ Object.entries(control.itemControls).map(([id, itemControl]) => [
77
+ id,
78
+ itemControl.defaultValue,
79
+ ]),
80
+ );
81
+ }
82
+
57
83
  if ("itemDefaultValue" in control) {
58
84
  return control.itemDefaultValue;
59
85
  }
@@ -62,7 +88,9 @@ export function getCollectionItemDefaultValue(
62
88
  return control.itemControl.defaultValue;
63
89
  }
64
90
 
65
- switch (getCollectionItemType(control)) {
91
+ const itemType = getCollectionItemType(control);
92
+
93
+ switch (itemType) {
66
94
  case "color":
67
95
  return { hex: "#C1FF00" };
68
96
  case "colorOpacity":
@@ -74,6 +102,8 @@ export function getCollectionItemDefaultValue(
74
102
  return false;
75
103
  case "rangeInput":
76
104
  return { end: "100%", start: "0%" };
105
+ case "rangeSlider":
106
+ return [control.itemControl?.min ?? 0, control.itemControl?.max ?? 100];
77
107
  case "select":
78
108
  case "segmented":
79
109
  return control.itemControl?.options?.[0]?.value ?? "";
@@ -81,7 +111,9 @@ export function getCollectionItemDefaultValue(
81
111
  return control.itemControl?.min ?? 0;
82
112
  case "text":
83
113
  return "";
114
+ case "vector":
115
+ return { x: "0", y: "0" };
84
116
  default:
85
- return "";
117
+ return assertNeverCollectionItemControl(itemType);
86
118
  }
87
119
  }
@@ -38,6 +38,7 @@ export function formatControlValueLabel(
38
38
  return `${colorOpacityValue.hex} ${colorOpacityValue.opacity}%`;
39
39
  }
40
40
  case "collectionActions":
41
+ case "sourceCollection":
41
42
  return `${asCollectionItems(value, control.defaultValue).length} items`;
42
43
  case "fontPicker":
43
44
  return asFontPickerValue(value).fontId;
@@ -6,6 +6,10 @@ export { useToolcraftMediaPresentationUrls } from "./app-shell/toolcraft-media-p
6
6
  export * from "./app-shell/use-toolcraft-pipeline";
7
7
  export * from "./app-shell/use-toolcraft-pipeline-pass";
8
8
  export * from "./canvas/canvas-shell";
9
+ export {
10
+ useToolcraftProductSceneFrame,
11
+ type ToolcraftProductSceneFrame,
12
+ } from "./canvas/product-scene-surface";
9
13
  export * from "./controls-panel/control-renderers";
10
14
  export * from "./controls-panel/controls-panel";
11
15
  export * from "./layers/layers-panel";
@@ -2,7 +2,7 @@ import {
2
2
  resolveToolcraftExportFrame,
3
3
  ToolcraftSceneExportError,
4
4
  type ToolcraftExportFrame,
5
- } from "../../export";
5
+ } from "../../export/export-frame";
6
6
  import { getToolcraftCanvasFrame, type ToolcraftCanvasFrame } from "../../state/canvas-frame";
7
7
  import type { ToolcraftState } from "../../state/types";
8
8
  import { getSceneElementPresentationRect } from "../canvas/scene-element-presentation-rect";
@@ -1,6 +1,6 @@
1
1
  import type {
2
- ToolcraftControlSchema,
3
- ToolcraftControlSectionSchema,
2
+ ResolvedToolcraftControlSchema,
3
+ ResolvedToolcraftControlSectionSchema,
4
4
  } from "../../schema/types";
5
5
  import type { ToolcraftState } from "../../state/types";
6
6
  import {
@@ -9,13 +9,13 @@ import {
9
9
  } from "../controls-panel/conditions/control-conditions";
10
10
 
11
11
  export type ToolcraftOrientationControlEntry = {
12
- control: ToolcraftControlSchema;
12
+ control: ResolvedToolcraftControlSchema;
13
13
  id: string;
14
- section: ToolcraftControlSectionSchema;
14
+ section: ResolvedToolcraftControlSectionSchema;
15
15
  };
16
16
 
17
17
  export function getToolcraftOrientationControlEntries(
18
- sections: readonly ToolcraftControlSectionSchema[],
18
+ sections: readonly ResolvedToolcraftControlSectionSchema[],
19
19
  ): ToolcraftOrientationControlEntry[] {
20
20
  return sections.flatMap((section) =>
21
21
  Object.entries(section.controls).flatMap(([id, control]) =>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
 
5
- import type { ToolcraftControlSchema } from "../../schema/types";
5
+ import type { ResolvedToolcraftControlSchema } from "../../schema/types";
6
6
  import type { ToolcraftStoreDependency } from "../../state/toolcraft-external-store-dependencies";
7
7
  import type { ToolcraftState } from "../../state/types";
8
8
  import { useToolcraftDependencySelector } from "../app-shell/toolcraft-selectors";
@@ -18,7 +18,7 @@ import {
18
18
  } from "./orientation-gizmo-selection";
19
19
 
20
20
  export type ToolcraftOrientationControlSelection = Readonly<{
21
- control: ToolcraftControlSchema | null;
21
+ control: ResolvedToolcraftControlSchema | null;
22
22
  id: string | null;
23
23
  value: unknown;
24
24
  }>;
@@ -6,6 +6,7 @@ import { createToolcraftState } from "../state/create-template-state";
6
6
  import {
7
7
  getToolcraftSceneElementRect,
8
8
  outwardRoundToolcraftSceneRect,
9
+ resolveToolcraftProductSceneBounds,
9
10
  resolveToolcraftSceneBounds,
10
11
  unionToolcraftSceneRects,
11
12
  } from "./scene-bounds";
@@ -17,6 +18,28 @@ function emptyState() {
17
18
  }
18
19
 
19
20
  describe("scene bounds", () => {
21
+ it("resolves one exact product frame before runtime contributors are added", () => {
22
+ expect(
23
+ resolveToolcraftProductSceneBounds([
24
+ { height: 20, width: 40, x: -10, y: -5 },
25
+ { height: 30, width: 10, x: 35, y: 15 },
26
+ ]),
27
+ ).toEqual({
28
+ bounds: { height: 50, width: 55, x: -10, y: -5 },
29
+ ok: true,
30
+ });
31
+ expect(resolveToolcraftProductSceneBounds([])).toMatchObject({
32
+ code: "empty-scene",
33
+ ok: false,
34
+ });
35
+ expect(
36
+ resolveToolcraftProductSceneBounds([
37
+ { height: 20, width: 40, x: -10, y: -5 },
38
+ { height: 30, width: Number.NaN, x: 35, y: 15 },
39
+ ]),
40
+ ).toMatchObject({ code: "scene-bounds-unavailable", ok: false });
41
+ });
42
+
20
43
  it("projects center-anchored frames and unions negative coordinates", () => {
21
44
  expect(
22
45
  getToolcraftSceneElementRect({
@@ -48,6 +48,18 @@ export type ToolcraftSceneBoundsResult =
48
48
  ok: false;
49
49
  }>;
50
50
 
51
+ const emptySceneBoundsResult: ToolcraftSceneBoundsResult = Object.freeze({
52
+ code: "empty-scene",
53
+ message: "The scene has no visible exportable elements.",
54
+ ok: false,
55
+ });
56
+
57
+ const unavailableSceneBoundsResult: ToolcraftSceneBoundsResult = Object.freeze({
58
+ code: "scene-bounds-unavailable",
59
+ message: "Product scene bounds are unavailable or invalid.",
60
+ ok: false,
61
+ });
62
+
51
63
  export function getToolcraftSceneElementRect(
52
64
  element: ToolcraftSceneElementFrame,
53
65
  ): ToolcraftSceneRect {
@@ -158,32 +170,34 @@ export function outwardRoundToolcraftSceneRect(
158
170
  };
159
171
  }
160
172
 
161
- export function resolveToolcraftSceneBounds(
162
- state: ToolcraftState,
163
- productRects: unknown = [],
164
- visibility: ToolcraftRuntimeSceneVisibility = defaultToolcraftRuntimeSceneVisibility,
173
+ export function resolveToolcraftProductSceneBounds(
174
+ productRects: unknown,
165
175
  ): ToolcraftSceneBoundsResult {
166
176
  if (
167
177
  !Array.isArray(productRects) ||
168
178
  !productRects.every(isFiniteSceneRect)
169
179
  ) {
170
- return {
171
- code: "scene-bounds-unavailable",
172
- message: "Product scene bounds are unavailable or invalid.",
173
- ok: false,
174
- };
180
+ return unavailableSceneBoundsResult;
181
+ }
182
+
183
+ const bounds = unionToolcraftSceneRects(productRects);
184
+ return bounds ? { bounds, ok: true } : emptySceneBoundsResult;
185
+ }
186
+
187
+ export function resolveToolcraftSceneBounds(
188
+ state: ToolcraftState,
189
+ productRects: unknown = [],
190
+ visibility: ToolcraftRuntimeSceneVisibility = defaultToolcraftRuntimeSceneVisibility,
191
+ ): ToolcraftSceneBoundsResult {
192
+ const productBounds = resolveToolcraftProductSceneBounds(productRects);
193
+ if (!productBounds.ok && productBounds.code === "scene-bounds-unavailable") {
194
+ return productBounds;
175
195
  }
176
196
 
177
197
  const bounds = unionToolcraftSceneRects([
178
198
  ...getRuntimeSceneRects(state, visibility),
179
- ...productRects,
199
+ ...(productBounds.ok ? [productBounds.bounds] : []),
180
200
  ]);
181
201
 
182
- return bounds
183
- ? { bounds, ok: true }
184
- : {
185
- code: "empty-scene",
186
- message: "The scene has no visible exportable elements.",
187
- ok: false,
188
- };
202
+ return bounds ? { bounds, ok: true } : emptySceneBoundsResult;
189
203
  }