@pixel-point/toolcraft 0.0.9 → 0.0.11

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 (75) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +14 -6
  4. package/src/generate.test.mjs +28 -0
  5. package/templates/runtime/contracts/component-contracts.test.ts +148 -34
  6. package/templates/runtime/contracts/component-contracts.ts +79 -38
  7. package/templates/runtime/contracts/decision-contracts.test.ts +33 -1
  8. package/templates/runtime/contracts/decision-contracts.ts +19 -6
  9. package/templates/runtime/export/export.test.ts +63 -0
  10. package/templates/runtime/export/export.ts +55 -0
  11. package/templates/runtime/index.ts +1 -0
  12. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  13. package/templates/runtime/react/canvas-shell.tsx +31 -9
  14. package/templates/runtime/react/controls-panel.test.tsx +474 -27
  15. package/templates/runtime/react/controls-panel.tsx +71 -26
  16. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  17. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  18. package/templates/runtime/react/settings-transfer.ts +6 -1
  19. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  20. package/templates/runtime/react/timeline-panel.tsx +44 -7
  21. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  22. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  23. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  24. package/templates/runtime/schema/define-toolcraft.test.ts +225 -173
  25. package/templates/runtime/schema/define-toolcraft.ts +117 -247
  26. package/templates/runtime/schema/runtime-targets.ts +21 -0
  27. package/templates/runtime/schema/types.ts +41 -0
  28. package/templates/runtime/state/create-template-state.test.ts +156 -0
  29. package/templates/runtime/state/create-template-state.ts +38 -8
  30. package/templates/runtime/state/media-defaults.ts +105 -0
  31. package/templates/runtime/state/persistence.test.ts +58 -0
  32. package/templates/runtime/state/persistence.ts +105 -1
  33. package/templates/runtime/state/reducer.test.ts +280 -4
  34. package/templates/runtime/state/reducer.ts +195 -9
  35. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  36. package/templates/runtime/state/timeline-loop.ts +35 -0
  37. package/templates/runtime/state/types.ts +27 -0
  38. package/templates/runtime/testing/performance.test.ts +657 -2
  39. package/templates/runtime/testing/performance.ts +789 -49
  40. package/templates/starter/AGENTS.md +22 -16
  41. package/templates/starter/docs/toolcraft/README.md +8 -4
  42. package/templates/starter/docs/toolcraft/acceptance-testing.md +38 -7
  43. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  44. package/templates/starter/docs/toolcraft/assembly-workflow.md +51 -20
  45. package/templates/starter/docs/toolcraft/component-rules.md +49 -37
  46. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  47. package/templates/starter/docs/toolcraft/performance.md +30 -10
  48. package/templates/starter/docs/toolcraft/schema-reference.md +134 -33
  49. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  50. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  51. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  52. package/templates/starter/e2e/performance-helpers.ts +45 -0
  53. package/templates/starter/index.html +1 -0
  54. package/templates/starter/package.json +1 -0
  55. package/templates/starter/playwright.config.ts +1 -1
  56. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  57. package/templates/starter/scripts/run-vite-on-free-port.mjs +82 -16
  58. package/templates/starter/scripts/toolcraft-port.mjs +178 -0
  59. package/templates/starter/scripts/toolcraft-port.test.mjs +147 -0
  60. package/templates/starter/src/app/starter-acceptance.test.ts +2573 -313
  61. package/templates/starter/src/app/starter-acceptance.ts +978 -81
  62. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  63. package/templates/starter/src/app/starter-performance.ts +5 -0
  64. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  65. package/templates/starter/src/app/starter-schema.ts +6 -2
  66. package/templates/starter/vite.config.ts +58 -2
  67. package/templates/ui/components/controls/actions/actions-control.tsx +46 -3
  68. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  69. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  70. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  71. package/templates/ui/components/controls/index.ts +2 -0
  72. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  73. package/templates/ui/components/controls/select/select-control.tsx +4 -26
  74. package/templates/ui/components/controls/vector/vector-control.tsx +25 -4
  75. package/templates/ui/components/panel/panel-actions.tsx +1 -1
@@ -1,10 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { toolcraftRuntimeCommandTypes } from "../state/types";
4
- import {
5
- defineToolcraft,
6
- getToolcraftSettingsTransferEligibility,
7
- } from "./define-toolcraft";
4
+ import { defineToolcraft } from "./define-toolcraft";
8
5
  import {
9
6
  toolcraftReservedTargets,
10
7
  toolcraftRuntimeOwnedTargets,
@@ -27,17 +24,8 @@ describe("defineToolcraft", () => {
27
24
  );
28
25
  }
29
26
 
30
- function createSections(count: number) {
31
- return Array.from({ length: count }, (_, index) => ({
32
- controls: {
33
- opacity: {
34
- defaultValue: index,
35
- target: `section${index}.opacity`,
36
- type: "slider",
37
- },
38
- },
39
- title: `Section ${index + 1}`,
40
- }));
27
+ function getProductSections(app: ReturnType<typeof defineToolcraft>) {
28
+ return app.panels.controls?.sections.filter((section) => section.title !== "Setup") ?? [];
41
29
  }
42
30
 
43
31
  it("defaults enabled canvas to draggable with canvas toolbar features", () => {
@@ -53,6 +41,30 @@ describe("defineToolcraft", () => {
53
41
  expect(app.toolbar).toEqual({ history: true, radar: true, theme: true, zoom: true });
54
42
  });
55
43
 
44
+ it("defaults upload canvases to editable output unless intrinsic media is explicit", () => {
45
+ const app = defineToolcraft({
46
+ canvas: { enabled: true, upload: true },
47
+ panels: {},
48
+ });
49
+
50
+ expect(app.canvas.size).toEqual({ height: 1080, unit: "px", width: 1920 });
51
+ expect(app.canvas.sizeSource).toBe("runtime-default");
52
+ expect(app.canvas.sizing).toEqual({ mode: "editable-output" });
53
+ expect(app.assembly.capabilities).toContain("canvas.editableSize");
54
+ expect(app.assembly.commands).toContain("canvas.setSize");
55
+ });
56
+
57
+ it("preserves explicit intrinsic media sizing for source-native upload apps", () => {
58
+ const app = defineToolcraft({
59
+ canvas: { enabled: true, sizing: { mode: "intrinsic-media" }, upload: true },
60
+ panels: {},
61
+ });
62
+
63
+ expect(app.canvas.sizing).toEqual({ mode: "intrinsic-media" });
64
+ expect(app.assembly.capabilities).not.toContain("canvas.editableSize");
65
+ expect(app.assembly.commands).not.toContain("canvas.setSize");
66
+ });
67
+
56
68
  it("disables app state persistence by default", () => {
57
69
  const app = defineToolcraft({
58
70
  canvas: { enabled: true },
@@ -82,7 +94,7 @@ describe("defineToolcraft", () => {
82
94
  });
83
95
  });
84
96
 
85
- it("keeps settings transfer disabled for small auto schemas", () => {
97
+ it("keeps settings transfer visible for small auto schemas", () => {
86
98
  const app = defineToolcraft({
87
99
  canvas: { enabled: true, size: { height: 320, unit: "px", width: 320 } },
88
100
  panels: {
@@ -107,11 +119,15 @@ describe("defineToolcraft", () => {
107
119
 
108
120
  expect(app.settingsTransfer).toMatchObject({
109
121
  appId: "mini-app",
110
- enabled: false,
122
+ enabled: true,
111
123
  fileName: "mini-app-settings.json",
112
124
  mode: "auto",
113
125
  });
114
126
  expect(app.panels.controls?.sections[0]?.title).toBe("Setup");
127
+ expect(app.panels.controls?.sections[0]?.controls.settingsTransfer).toMatchObject({
128
+ target: "runtime.settingsTransfer",
129
+ type: "settingsTransfer",
130
+ });
115
131
  expect(app.panels.controls?.sections[0]?.controls.canvasAspectRatio).toMatchObject({
116
132
  target: "canvas.aspectRatio",
117
133
  type: "aspectRatio",
@@ -119,124 +135,7 @@ describe("defineToolcraft", () => {
119
135
  expect(app.panels.controls?.sections[0]?.controls.canvasWidth).toBeTruthy();
120
136
  });
121
137
 
122
- it("reports settings transfer eligibility from control count, sections, and weighted score", () => {
123
- const small = getToolcraftSettingsTransferEligibility({
124
- panels: {
125
- controls: {
126
- sections: [
127
- {
128
- controls: createSliderControls(1),
129
- title: "Small",
130
- },
131
- ],
132
- title: "Small App",
133
- },
134
- },
135
- });
136
- const manyControls = getToolcraftSettingsTransferEligibility({
137
- panels: {
138
- controls: {
139
- sections: [
140
- {
141
- controls: createSliderControls(12),
142
- title: "Many Controls",
143
- },
144
- ],
145
- title: "Many Controls App",
146
- },
147
- },
148
- });
149
- const manySections = getToolcraftSettingsTransferEligibility({
150
- panels: {
151
- controls: {
152
- sections: createSections(5),
153
- title: "Many Sections App",
154
- },
155
- },
156
- });
157
- const heavyControls = getToolcraftSettingsTransferEligibility({
158
- panels: {
159
- controls: {
160
- sections: [
161
- {
162
- controls: Object.fromEntries(
163
- Array.from({ length: 6 }, (_, index) => [
164
- `gradient${index}`,
165
- {
166
- label: `Gradient ${index + 1}`,
167
- target: `gradient${index}`,
168
- type: "gradient",
169
- },
170
- ]),
171
- ),
172
- title: "Gradients",
173
- },
174
- ],
175
- title: "Heavy Controls App",
176
- },
177
- },
178
- });
179
- const timelineLayersScore = getToolcraftSettingsTransferEligibility({
180
- panels: {
181
- controls: {
182
- sections: [
183
- {
184
- controls: Object.fromEntries(
185
- Array.from({ length: 5 }, (_, index) => [
186
- `gradient${index}`,
187
- {
188
- label: `Gradient ${index + 1}`,
189
- target: `gradient${index}`,
190
- type: "gradient",
191
- },
192
- ]),
193
- ),
194
- title: "Gradients",
195
- },
196
- ],
197
- title: "Timeline Layers App",
198
- },
199
- layers: true,
200
- timeline: { mode: "playback" },
201
- },
202
- });
203
-
204
- expect(small).toEqual({
205
- controlCount: 1,
206
- eligible: false,
207
- reasons: [],
208
- score: 1,
209
- sectionCount: 1,
210
- });
211
- expect(manyControls).toMatchObject({
212
- controlCount: 12,
213
- eligible: true,
214
- reasons: ["control-count"],
215
- sectionCount: 1,
216
- });
217
- expect(manySections).toMatchObject({
218
- controlCount: 5,
219
- eligible: true,
220
- reasons: ["section-count"],
221
- sectionCount: 5,
222
- });
223
- expect(heavyControls).toMatchObject({
224
- controlCount: 6,
225
- eligible: true,
226
- reasons: ["score"],
227
- score: 18,
228
- sectionCount: 1,
229
- });
230
- expect(timelineLayersScore).toMatchObject({
231
- controlCount: 5,
232
- eligible: true,
233
- reasons: ["score"],
234
- score: 19,
235
- sectionCount: 1,
236
- });
237
- });
238
-
239
- it("ignores runtime canvas setup controls in settings transfer eligibility", () => {
138
+ it("keeps runtime setup controls outside product sections", () => {
240
139
  const app = defineToolcraft({
241
140
  canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } },
242
141
  panels: {
@@ -253,16 +152,16 @@ describe("defineToolcraft", () => {
253
152
  settingsTransfer: false,
254
153
  });
255
154
 
155
+ expect(app.settingsTransfer.enabled).toBe(true);
156
+ expect(app.panels.controls?.sections[0]?.controls.settingsTransfer).toMatchObject({
157
+ target: "runtime.settingsTransfer",
158
+ type: "settingsTransfer",
159
+ });
256
160
  expect(app.panels.controls?.sections[0]?.controls.canvasAspectRatio).toBeTruthy();
257
161
  expect(app.panels.controls?.sections[0]?.controls.canvasWidth).toBeTruthy();
258
162
  expect(app.panels.controls?.sections[0]?.controls.canvasHeight).toBeTruthy();
259
- expect(getToolcraftSettingsTransferEligibility({ panels: app.panels })).toEqual({
260
- controlCount: 10,
261
- eligible: false,
262
- reasons: [],
263
- score: 10,
264
- sectionCount: 1,
265
- });
163
+ expect(getProductSections(app)).toHaveLength(1);
164
+ expect(getProductSections(app)[0]?.title).toBe("Product Controls");
266
165
  });
267
166
 
268
167
  it("injects settings transfer as the first section for complex auto schemas", () => {
@@ -335,6 +234,7 @@ describe("defineToolcraft", () => {
335
234
  "canvasAspectRatio",
336
235
  "canvasWidth",
337
236
  "canvasHeight",
237
+ "timelineExtended",
338
238
  ]);
339
239
  expect(runtimeSettingsSection?.controls.canvasAspectRatio).toMatchObject({
340
240
  defaultValue: {
@@ -362,7 +262,7 @@ describe("defineToolcraft", () => {
362
262
  expect(firstProductSection?.title).toBe("Text");
363
263
  });
364
264
 
365
- it("respects explicit settings transfer overrides", () => {
265
+ it("uses explicit settings transfer naming while keeping the setup controls mandatory", () => {
366
266
  const forced = defineToolcraft({
367
267
  canvas: { enabled: false },
368
268
  panels: {
@@ -410,10 +310,13 @@ describe("defineToolcraft", () => {
410
310
  expect(forced.panels.controls?.sections[0]?.controls.settingsTransfer?.type).toBe(
411
311
  "settingsTransfer",
412
312
  );
413
- expect(disabled.settingsTransfer.enabled).toBe(false);
414
- expect(
415
- disabled.panels.controls?.sections.some((section) => section.controls.settingsTransfer),
416
- ).toBe(false);
313
+ expect(disabled.settingsTransfer).toMatchObject({
314
+ enabled: true,
315
+ mode: false,
316
+ });
317
+ expect(disabled.panels.controls?.sections[0]?.controls.settingsTransfer?.type).toBe(
318
+ "settingsTransfer",
319
+ );
417
320
  });
418
321
 
419
322
  it("preserves conditional visibility on sections and controls", () => {
@@ -445,7 +348,7 @@ describe("defineToolcraft", () => {
445
348
  },
446
349
  });
447
350
 
448
- const [section] = app.panels.controls?.sections ?? [];
351
+ const [section] = getProductSections(app);
449
352
 
450
353
  expect(section?.visibleWhen).toEqual({
451
354
  equals: "co-brand",
@@ -540,6 +443,7 @@ describe("defineToolcraft", () => {
540
443
  "media.delete",
541
444
  "media.import",
542
445
  "media.reorder",
446
+ "media.transform",
543
447
  "layers.reorder",
544
448
  "timeline.setCurrentTime",
545
449
  "canvas.center",
@@ -548,7 +452,11 @@ describe("defineToolcraft", () => {
548
452
  );
549
453
  expect(app.assembly.surfaces.panels.layers?.defaultPlacement).toBe("left");
550
454
  expect(app.assembly.surfaces.panels.timeline?.snapEdges).toEqual(["top", "bottom"]);
551
- expect(app.panels.timeline).toEqual({ enabled: true, mode: "keyframes" });
455
+ expect(app.panels.timeline).toEqual({
456
+ defaultDurationSeconds: 8,
457
+ enabled: true,
458
+ mode: "keyframes",
459
+ });
552
460
  expect(app.assembly.surfaces.panels.toolbar.enabled).toBe(true);
553
461
  });
554
462
 
@@ -560,7 +468,11 @@ describe("defineToolcraft", () => {
560
468
  },
561
469
  });
562
470
 
563
- expect(app.panels.timeline).toEqual({ enabled: true, mode: "playback" });
471
+ expect(app.panels.timeline).toEqual({
472
+ defaultDurationSeconds: 8,
473
+ enabled: true,
474
+ mode: "playback",
475
+ });
564
476
  expect(app.assembly.components).toContain("timelinePanel");
565
477
  expect(app.assembly.capabilities).toEqual(
566
478
  expect.arrayContaining([
@@ -584,6 +496,30 @@ describe("defineToolcraft", () => {
584
496
  expect(app.assembly.commands).not.toContain("timeline.moveKeyframe");
585
497
  });
586
498
 
499
+ it("uses schema timeline default duration as the animation loop duration", () => {
500
+ const app = defineToolcraft({
501
+ canvas: { enabled: true },
502
+ panels: {
503
+ timeline: {
504
+ defaultDurationSeconds: 12,
505
+ mode: "playback",
506
+ },
507
+ },
508
+ });
509
+ const clamped = defineToolcraft({
510
+ canvas: { enabled: true },
511
+ panels: {
512
+ timeline: {
513
+ defaultDurationSeconds: 120,
514
+ mode: "playback",
515
+ },
516
+ },
517
+ });
518
+
519
+ expect(app.panels.timeline?.defaultDurationSeconds).toBe(12);
520
+ expect(clamped.panels.timeline?.defaultDurationSeconds).toBe(60);
521
+ });
522
+
587
523
  it("only publishes commands supported by the runtime command bus", () => {
588
524
  const app = defineToolcraft({
589
525
  canvas: { enabled: true, upload: true },
@@ -617,12 +553,18 @@ describe("defineToolcraft", () => {
617
553
  "canvas.renderScale",
618
554
  "canvas.size.width",
619
555
  "canvas.size.height",
556
+ "runtime.settingsTransfer",
557
+ "panels.timeline.extended",
558
+ "panels.timeline.visible",
620
559
  ]);
621
560
  expect(toolcraftReservedTargets).toEqual([
622
561
  "canvas.aspectRatio",
623
562
  "canvas.renderScale",
624
563
  "canvas.size.width",
625
564
  "canvas.size.height",
565
+ "runtime.settingsTransfer",
566
+ "panels.timeline.extended",
567
+ "panels.timeline.visible",
626
568
  "selectedLayer.opacity",
627
569
  "selectedLayer.visible",
628
570
  ]);
@@ -630,6 +572,9 @@ describe("defineToolcraft", () => {
630
572
  expect(getToolcraftCanvasSizeTargetDimension("canvas.size.height")).toBe("height");
631
573
  expect(getToolcraftCanvasSizeTargetDimension("generation.prompt")).toBeNull();
632
574
  expect(isToolcraftRuntimeOwnedTarget("canvas.aspectRatio")).toBe(true);
575
+ expect(isToolcraftRuntimeOwnedTarget("runtime.settingsTransfer")).toBe(true);
576
+ expect(isToolcraftRuntimeOwnedTarget("panels.timeline.extended")).toBe(true);
577
+ expect(isToolcraftRuntimeOwnedTarget("panels.timeline.visible")).toBe(true);
633
578
  expect(isToolcraftReservedTarget("selectedLayer.opacity")).toBe(true);
634
579
  expect(isToolcraftReservedTarget("generation.prompt")).toBe(false);
635
580
  });
@@ -823,6 +768,7 @@ describe("defineToolcraft", () => {
823
768
  });
824
769
  expect(app.assembly.capabilities).toContain("canvas.renderScale");
825
770
  expect(Object.keys(setupSection?.controls ?? {})).toEqual([
771
+ "settingsTransfer",
826
772
  "canvasAspectRatio",
827
773
  "canvasWidth",
828
774
  "canvasHeight",
@@ -841,6 +787,103 @@ describe("defineToolcraft", () => {
841
787
  });
842
788
  });
843
789
 
790
+ it("appends timeline visibility to the bottom of setup controls when timeline is enabled", () => {
791
+ const app = defineToolcraft({
792
+ canvas: {
793
+ enabled: true,
794
+ renderScale: true,
795
+ size: { height: 900, unit: "px", width: 1440 },
796
+ sizing: { mode: "editable-output" },
797
+ },
798
+ panels: {
799
+ controls: {
800
+ sections: [],
801
+ title: "Controls",
802
+ },
803
+ timeline: { mode: "playback" },
804
+ },
805
+ });
806
+
807
+ const setupSection = app.panels.controls?.sections[0];
808
+
809
+ expect(Object.keys(setupSection?.controls ?? {})).toEqual([
810
+ "settingsTransfer",
811
+ "canvasAspectRatio",
812
+ "canvasWidth",
813
+ "canvasHeight",
814
+ "canvasRenderScale",
815
+ "timelineExtended",
816
+ ]);
817
+ expect(setupSection?.controls.timelineExtended).toMatchObject({
818
+ defaultValue: false,
819
+ description:
820
+ "Shows the extended runtime timeline with scrubber, duration, loop, and keyframe controls; compact mode keeps only Play visible.",
821
+ label: "Timeline",
822
+ target: "panels.timeline.extended",
823
+ type: "switch",
824
+ });
825
+ });
826
+
827
+ it("does not let app-authored runtime targets suppress mandatory setup controls", () => {
828
+ const app = defineToolcraft({
829
+ canvas: {
830
+ enabled: true,
831
+ renderScale: true,
832
+ size: { height: 1080, unit: "px", width: 1920 },
833
+ sizing: { mode: "editable-output" },
834
+ },
835
+ panels: {
836
+ controls: {
837
+ sections: [
838
+ {
839
+ controls: {
840
+ manualWidth: {
841
+ defaultValue: 1280,
842
+ label: "Width",
843
+ target: "canvas.size.width",
844
+ type: "text",
845
+ },
846
+ manualRenderScale: {
847
+ defaultValue: 1,
848
+ label: "Scale",
849
+ max: 2,
850
+ min: 1,
851
+ target: "canvas.renderScale",
852
+ type: "slider",
853
+ },
854
+ manualTimeline: {
855
+ defaultValue: true,
856
+ label: "Timeline",
857
+ target: "panels.timeline.extended",
858
+ type: "switch",
859
+ },
860
+ },
861
+ title: "Manual Runtime Duplicates",
862
+ },
863
+ ],
864
+ title: "Controls",
865
+ },
866
+ timeline: { mode: "playback" },
867
+ },
868
+ });
869
+
870
+ const [setupSection, duplicateSection] = app.panels.controls?.sections ?? [];
871
+
872
+ expect(Object.keys(setupSection?.controls ?? {})).toEqual([
873
+ "settingsTransfer",
874
+ "canvasAspectRatio",
875
+ "canvasWidth",
876
+ "canvasHeight",
877
+ "canvasRenderScale",
878
+ "timelineExtended",
879
+ ]);
880
+ expect(duplicateSection?.controls.manualWidth?.target).toBe("canvas.size.width");
881
+ expect(duplicateSection?.controls.manualRenderScale?.target).toBe("canvas.renderScale");
882
+ expect(duplicateSection?.controls.manualTimeline?.target).toBe(
883
+ "panels.timeline.extended",
884
+ );
885
+ });
886
+
844
887
  it("can prepend render scale without editable output sizing", () => {
845
888
  const app = defineToolcraft({
846
889
  canvas: {
@@ -873,6 +916,7 @@ describe("defineToolcraft", () => {
873
916
  const intrinsicApp = defineToolcraft({
874
917
  canvas: {
875
918
  enabled: true,
919
+ sizing: { mode: "intrinsic-media" },
876
920
  upload: true,
877
921
  },
878
922
  panels: {
@@ -903,10 +947,14 @@ describe("defineToolcraft", () => {
903
947
 
904
948
  expect(intrinsicApp.canvas.sizing).toEqual({ mode: "intrinsic-media" });
905
949
  expect(fixedApp.canvas.sizing).toEqual({ mode: "fixed-output" });
906
- expect(intrinsicApp.panels.controls?.sections[0]?.title).toBe("Generation");
907
- expect(fixedApp.panels.controls?.sections[0]?.title).toBe("Generation");
950
+ expect(intrinsicApp.panels.controls?.sections[0]?.title).toBe("Setup");
951
+ expect(fixedApp.panels.controls?.sections[0]?.title).toBe("Setup");
952
+ expect(intrinsicApp.panels.controls?.sections[1]?.title).toBe("Generation");
953
+ expect(fixedApp.panels.controls?.sections.at(-1)?.title).toBe("Generation");
908
954
  expect(intrinsicApp.panels.controls?.sections[0]?.controls.canvasWidth).toBeUndefined();
909
955
  expect(fixedApp.panels.controls?.sections[0]?.controls.canvasWidth).toBeUndefined();
956
+ expect(intrinsicApp.panels.controls?.sections[0]?.controls.settingsTransfer).toBeTruthy();
957
+ expect(fixedApp.panels.controls?.sections[0]?.controls.settingsTransfer).toBeTruthy();
910
958
  });
911
959
 
912
960
  it("adds runtime layout groups only for compact numeric text pairs", () => {
@@ -970,7 +1018,7 @@ describe("defineToolcraft", () => {
970
1018
  },
971
1019
  });
972
1020
 
973
- const outputSection = app.panels.controls?.sections[0];
1021
+ const [outputSection] = getProductSections(app);
974
1022
 
975
1023
  expect(outputSection?.layoutGroups).toEqual([
976
1024
  {
@@ -1010,7 +1058,7 @@ describe("defineToolcraft", () => {
1010
1058
  },
1011
1059
  });
1012
1060
 
1013
- const maskSection = app.panels.controls?.sections[0];
1061
+ const [maskSection] = getProductSections(app);
1014
1062
 
1015
1063
  expect(maskSection?.layoutGroups).toEqual([
1016
1064
  {
@@ -1121,7 +1169,7 @@ describe("defineToolcraft", () => {
1121
1169
  },
1122
1170
  });
1123
1171
 
1124
- const [formatPairSection, outputSection] = app.panels.controls?.sections ?? [];
1172
+ const [formatPairSection, outputSection] = getProductSections(app);
1125
1173
 
1126
1174
  expect(formatPairSection?.layoutGroups).toEqual([
1127
1175
  {
@@ -1168,7 +1216,7 @@ describe("defineToolcraft", () => {
1168
1216
  },
1169
1217
  });
1170
1218
 
1171
- const [basicSection, anchorSection] = app.panels.controls?.sections ?? [];
1219
+ const [basicSection, anchorSection] = getProductSections(app);
1172
1220
 
1173
1221
  expect(Object.keys(basicSection?.controls ?? {})).toEqual(["opacity", "threshold"]);
1174
1222
  expect(basicSection?.title).toBe("Basic");
@@ -1211,8 +1259,8 @@ describe("defineToolcraft", () => {
1211
1259
  },
1212
1260
  });
1213
1261
 
1214
- expect(app.panels.controls?.sections[0]?.layout).toBe("standalone");
1215
- expect(app.panels.controls?.sections[0]?.title).toBe("Palette");
1262
+ expect(getProductSections(app)[0]?.layout).toBe("standalone");
1263
+ expect(getProductSections(app)[0]?.title).toBe("Palette");
1216
1264
  });
1217
1265
 
1218
1266
  it("adds semantic implicit titles to standalone color sections", () => {
@@ -1244,8 +1292,8 @@ describe("defineToolcraft", () => {
1244
1292
  },
1245
1293
  });
1246
1294
 
1247
- expect(app.panels.controls?.sections[0]?.layout).toBe("standalone");
1248
- expect(app.panels.controls?.sections[0]?.title).toBe("Fill & Stroke");
1295
+ expect(getProductSections(app)[0]?.layout).toBe("standalone");
1296
+ expect(getProductSections(app)[0]?.title).toBe("Fill & Stroke");
1249
1297
 
1250
1298
  const inferredLayoutApp = defineToolcraft({
1251
1299
  canvas: { enabled: true },
@@ -1268,7 +1316,7 @@ describe("defineToolcraft", () => {
1268
1316
  },
1269
1317
  });
1270
1318
 
1271
- expect(inferredLayoutApp.panels.controls?.sections[0]?.title).toBe("Fill");
1319
+ expect(getProductSections(inferredLayoutApp)[0]?.title).toBe("Fill");
1272
1320
  });
1273
1321
 
1274
1322
  it("replaces generic color section titles with a semantic fallback", () => {
@@ -1344,9 +1392,9 @@ describe("defineToolcraft", () => {
1344
1392
  },
1345
1393
  });
1346
1394
 
1347
- expect(genericColorApp.panels.controls?.sections[0]?.title).toBe("Appearance");
1348
- expect(genericColorsApp.panels.controls?.sections[0]?.title).toBe("Appearance");
1349
- expect(explicitGenericTitleApp.panels.controls?.sections[0]?.title).toBe("Appearance");
1395
+ expect(getProductSections(genericColorApp)[0]?.title).toBe("Appearance");
1396
+ expect(getProductSections(genericColorsApp)[0]?.title).toBe("Appearance");
1397
+ expect(getProductSections(explicitGenericTitleApp)[0]?.title).toBe("Appearance");
1350
1398
  });
1351
1399
 
1352
1400
  it("keeps color controls inside semantic mixed sections", () => {
@@ -1387,9 +1435,11 @@ describe("defineToolcraft", () => {
1387
1435
  },
1388
1436
  });
1389
1437
 
1390
- expect(app.panels.controls?.sections).toHaveLength(1);
1391
- expect(app.panels.controls?.sections[0]?.title).toBe("Square 1 (Right)");
1392
- expect(Object.keys(app.panels.controls?.sections[0]?.controls ?? {})).toEqual([
1438
+ const productSections = getProductSections(app);
1439
+
1440
+ expect(productSections).toHaveLength(1);
1441
+ expect(productSections[0]?.title).toBe("Square 1 (Right)");
1442
+ expect(Object.keys(productSections[0]?.controls ?? {})).toEqual([
1393
1443
  "connections",
1394
1444
  "hoverRadius",
1395
1445
  "color",
@@ -1432,10 +1482,12 @@ describe("defineToolcraft", () => {
1432
1482
  },
1433
1483
  });
1434
1484
 
1435
- expect(app.panels.controls?.sections).toHaveLength(1);
1436
- expect(app.panels.controls?.sections[0]?.title).toBe("Source");
1437
- expect(app.panels.controls?.sections[0]?.layout).toBeUndefined();
1438
- expect(Object.keys(app.panels.controls?.sections[0]?.controls ?? {})).toEqual([
1485
+ const productSections = getProductSections(app);
1486
+
1487
+ expect(productSections).toHaveLength(1);
1488
+ expect(productSections[0]?.title).toBe("Source");
1489
+ expect(productSections[0]?.layout).toBeUndefined();
1490
+ expect(Object.keys(productSections[0]?.controls ?? {})).toEqual([
1439
1491
  "sourceMode",
1440
1492
  "sourceUpload",
1441
1493
  ]);
@@ -1493,7 +1545,7 @@ describe("defineToolcraft", () => {
1493
1545
  },
1494
1546
  });
1495
1547
 
1496
- const [slidersSection, actionsSection] = app.panels.controls?.sections ?? [];
1548
+ const [slidersSection, actionsSection] = getProductSections(app);
1497
1549
 
1498
1550
  expect(slidersSection?.controls.opacity?.markerCount).toBe(101);
1499
1551
  expect(slidersSection?.controls.opacity?.variant).toBe("discrete");
@@ -1563,7 +1615,7 @@ describe("defineToolcraft", () => {
1563
1615
  },
1564
1616
  });
1565
1617
 
1566
- const controls = app.panels.controls?.sections[0]?.controls;
1618
+ const controls = getProductSections(app)[0]?.controls;
1567
1619
 
1568
1620
  expect(controls?.speed?.variant).toBeUndefined();
1569
1621
  expect(controls?.speed?.markerCount).toBe(6);