@pixel-point/toolcraft 0.0.6 → 0.0.7

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/generate.test.mjs +1 -1
  3. package/templates/runtime/contracts/component-contracts.test.ts +39 -3
  4. package/templates/runtime/contracts/component-contracts.ts +54 -3
  5. package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
  6. package/templates/runtime/contracts/decision-contracts.ts +1 -1
  7. package/templates/runtime/react/controls-panel.test.tsx +186 -1
  8. package/templates/runtime/react/controls-panel.tsx +382 -6
  9. package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
  10. package/templates/runtime/schema/define-toolcraft.ts +7 -1
  11. package/templates/runtime/schema/types.ts +23 -0
  12. package/templates/runtime/state/reducer.test.ts +53 -0
  13. package/templates/runtime/state/reducer.ts +47 -0
  14. package/templates/runtime/state/types.ts +2 -0
  15. package/templates/starter/AGENTS.md +7 -7
  16. package/templates/starter/docs/toolcraft/README.md +4 -3
  17. package/templates/starter/docs/toolcraft/acceptance-testing.md +6 -2
  18. package/templates/starter/docs/toolcraft/assembly-workflow.md +5 -5
  19. package/templates/starter/docs/toolcraft/component-rules.md +5 -1
  20. package/templates/starter/docs/toolcraft/custom-controls.md +2 -2
  21. package/templates/starter/docs/toolcraft/performance.md +4 -8
  22. package/templates/starter/docs/toolcraft/schema-reference.md +6 -1
  23. package/templates/starter/docs/toolcraft/workflow.md +5 -8
  24. package/templates/starter/package.json +1 -1
  25. package/templates/starter/src/app/starter-performance.test.ts +1 -1
  26. package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
  27. package/templates/ui/components/controls/collection-actions/index.ts +4 -0
  28. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -6
  29. package/templates/ui/components/controls/index.ts +8 -0
  30. package/templates/ui/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-point/toolcraft",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "bin": {
@@ -95,7 +95,7 @@ describe("generateToolcraft", () => {
95
95
  assert.equal(packageJson.scripts["verify:perf"], "pnpm test:browser:perf");
96
96
  assert.equal(
97
97
  packageJson.scripts["verify:final"],
98
- "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser && pnpm test:browser:perf",
98
+ "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
99
99
  );
100
100
 
101
101
  assert.ok(await fs.stat(path.join(targetDir, "playwright.config.ts")));
@@ -30,6 +30,7 @@ describe("Toolcraft template component contracts", () => {
30
30
  "imagePicker",
31
31
  "palette",
32
32
  "actions",
33
+ "collectionActions",
33
34
  "panelActions",
34
35
  "customControl",
35
36
  ] as const) {
@@ -221,7 +222,7 @@ describe("Toolcraft template component contracts", () => {
221
222
  "Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or status that affects the product.",
222
223
  );
223
224
  expect(contract.aiUsageRules).toContain(
224
- "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
225
+ "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
225
226
  );
226
227
  expect(contract.aiUsageRules).toContain(
227
228
  "Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
@@ -258,6 +259,35 @@ describe("Toolcraft template component contracts", () => {
258
259
  );
259
260
  });
260
261
 
262
+ it("documents CollectionActions as canvas-backed add/remove controls", () => {
263
+ const contract = getToolcraftComponentContract("collectionActions");
264
+
265
+ expect(contract.stateMode).toBe("controlled");
266
+ expect(contract.decisionCatalog?.strictness).toBe("exact-owner");
267
+ expect(contract.decisionCatalog?.ownsValueModel).toContain(
268
+ "repeatable product entity collection",
269
+ );
270
+ expect(contract.decisionCatalog?.useWhen).toContain(
271
+ "Use CollectionActions instead of a count Slider when the user edits the actual set of items rather than only a numeric amount.",
272
+ );
273
+ expect(contract.decisionCatalog?.doNotReplaceWith).toContain(
274
+ "Do not use Slider to add or remove real collection items.",
275
+ );
276
+ expect(contract.decisionCatalog?.layoutConstraints).toContain(
277
+ "recommendedMaxItems is an agent/layout/performance hint, not a hard add limit; hardMaxItems is allowed only for real algorithm, format, API, export, or proven performance limits.",
278
+ );
279
+ expect(contract.decisionCatalog?.requiredAcceptance.join(" ")).toMatch(
280
+ /canvas preview and export/i,
281
+ );
282
+ expect(contract.aiUsageRules).toContain(
283
+ "Adding or removing collection items must update the runtime target array consumed by the renderer and export; do not add panel-only items.",
284
+ );
285
+ expect(contract.aiUsageRules).toContain(
286
+ "recommendedMaxItems is advisory only and must not disable the plus button. Use hardMaxItems only when a real product, algorithm, API, export, or measured performance limit requires it.",
287
+ );
288
+ expect(contract.aiUsageRules.join(" ")).toMatch(/TextInput/);
289
+ });
290
+
261
291
  it("documents Palette as a constrained design-token color control", () => {
262
292
  const contract = getToolcraftComponentContract("palette");
263
293
 
@@ -352,7 +382,7 @@ describe("Toolcraft template component contracts", () => {
352
382
  "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
353
383
  );
354
384
  expect(contract.aiUsageRules).toContain(
355
- "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1x to 2x without changing visible canvas size, and adding/enabling it requires a full pnpm verify:perf checkpoint.",
385
+ "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1x to 2x without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
356
386
  );
357
387
  expect(contract.aiUsageRules).toContain(
358
388
  "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
@@ -728,7 +758,10 @@ describe("Toolcraft template component contracts", () => {
728
758
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
729
759
  );
730
760
  expect(contract.aiUsageRules).toContain(
731
- "A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, canvas.renderScale or the Resolution scale retina slider is added/enabled, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
761
+ "A full performance checkpoint must run with pnpm verify:perf only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance.",
762
+ );
763
+ expect(contract.aiUsageRules).toContain(
764
+ "Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
732
765
  );
733
766
  expect(contract.aiUsageRules).toContain(
734
767
  "Performance fixes must preserve selected output and preview quality; do not reduce image quality, selected renderScale, export resolution, source media fidelity, or canvas backing pixels as the hidden way to pass budgets.",
@@ -876,6 +909,9 @@ describe("Toolcraft template component contracts", () => {
876
909
  expect(contract.aiUsageRules).toContain(
877
910
  "Ordinary controls-panel section collapsed/expanded state persists as a runtime UI preference per app. It is not undo/redo state, not settings import/export state, and Reset controls must not clear it. Runtime technical Setup/settings sections and sticky footer Export sections are not collapsible.",
878
911
  );
912
+ expect(contract.aiUsageRules).toContain(
913
+ "Ordinary controls-panel section headers expose the runtime section reset action before the collapse button; it dispatches controls.resetTargets and restores only that section's control targets to their schema defaultValue.",
914
+ );
879
915
  expect(contract.aiUsageRules).toContain(
880
916
  "Ordinary controls-panel body sections use 8px top spacing and 24px bottom spacing for their control content. Runtime technical Setup/settings sections use 12px top and bottom spacing to match side padding. Sticky footer action sections keep their dedicated spacing.",
881
917
  );
@@ -392,6 +392,55 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
392
392
  "Acceptance and browser tests must click each Actions button and prove the product output or runtime state for the nearby entity changed.",
393
393
  ],
394
394
  },
395
+ collectionActions: {
396
+ ...control("collectionActions", "CollectionActions", "standalone", "component-owned"),
397
+ decisionCatalog: decisionCatalog({
398
+ strictness: "exact-owner",
399
+ ownsValueModel: [
400
+ "repeatable product entity collection",
401
+ "add/remove product items",
402
+ "dynamic list of visible controls",
403
+ "canvas-backed collection size",
404
+ ],
405
+ useWhen: [
406
+ "Use CollectionActions when users can add or remove repeated product entities such as colors, glyphs, symbols, points, rules, variants, or object entries.",
407
+ "Use CollectionActions instead of a count Slider when the user edits the actual set of items rather than only a numeric amount.",
408
+ ],
409
+ doNotReplaceWith: [
410
+ "Do not use Slider to add or remove real collection items.",
411
+ "Do not use plain Actions for add/remove collection ownership; Actions are local commands, not collection state owners.",
412
+ "Do not use CollectionActions for panel-only lists that do not affect canvas preview or export.",
413
+ ],
414
+ acceptableAlternatives: [
415
+ "Use fileDrop multiple when the repeated entities are uploaded images.",
416
+ "Use Gradient when the repeated entities are gradient stops inside an adjustable gradient.",
417
+ "Use customControl only when the repeated entity needs interactions no built-in collection item control can express.",
418
+ ],
419
+ layoutConstraints: [
420
+ "CollectionActions sits at the start of its section, renders the collection label on the left, and keeps remove/add icon buttons together on the right.",
421
+ "Homogeneous repeated item controls do not render visible per-item labels when the collection label already names the group.",
422
+ "Collection item controls follow normal density rules: plain color items use equal 50% columns when they fit, while color+opacity items stay stacked.",
423
+ "CollectionActions is a compound control and follows content-width compound divider rules when sharing a section with sibling controls.",
424
+ "recommendedMaxItems is an agent/layout/performance hint, not a hard add limit; hardMaxItems is allowed only for real algorithm, format, API, export, or proven performance limits.",
425
+ ],
426
+ requiredAcceptance: [
427
+ "Prove plus adds a runtime item and that the new item appears in or affects canvas preview and export.",
428
+ "Prove minus removes a runtime item and that the removed item disappears from or stops affecting canvas preview and export.",
429
+ "Prove minItems prevents deleting below the minimum and recommendedMaxItems does not silently block adding more items.",
430
+ ],
431
+ }),
432
+ stateMode: "controlled",
433
+ aiUsageRules: [
434
+ "Use CollectionActions for repeatable product entities whose actual item list can grow or shrink.",
435
+ "Adding or removing collection items must update the runtime target array consumed by the renderer and export; do not add panel-only items.",
436
+ "Do not model add/remove item behavior with a Slider count when users need to edit the actual items.",
437
+ "recommendedMaxItems is advisory only and must not disable the plus button. Use hardMaxItems only when a real product, algorithm, API, export, or measured performance limit requires it.",
438
+ "CollectionActions item controls use built-in controls whenever possible, such as Color, ColorOpacity, TextInput, Select, Segmented, Slider, Switch, Checkbox, or RangeInput.",
439
+ "Do not add visible labels like Color 1, Color 2, Item 1, or Item 2 for homogeneous collection items when the collection label already explains the group.",
440
+ "Use compact half-width item layout whenever the child control is allowed to fit in a half row; color items without opacity are the default two-column case.",
441
+ "Acceptance must add and remove items through the browser UI and prove canvas/export output follows the changed collection.",
442
+ ],
443
+ },
395
444
  panelActions: {
396
445
  ...control("panelActions", "PanelActions", "standalone", "component-owned"),
397
446
  decisionCatalog: decisionCatalog({
@@ -928,7 +977,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
928
977
  }),
929
978
  aiUsageRules: [
930
979
  "Use custom controls only for product interactions that built-in controls cannot express.",
931
- "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
980
+ "Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
932
981
  "Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
933
982
  "Custom controls must render the minimum UI needed to understand the value, context, and available actions; avoid decorative metadata and text that repeats what the section, label, or visible item already explains.",
934
983
  "Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or status that affects the product.",
@@ -961,7 +1010,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
961
1010
  "The runtime Canvas width and Canvas height block uses the technical Setup section and renders without a visible section heading; do not add a separate Canvas section label above these fields.",
962
1011
  "When the user manually edits Canvas width or Canvas height, the runtime keeps the typed dimension, keeps the other dimension unchanged, switches Aspect ratio to Custom, and shows the reduced current ratio in the custom ratio inputs.",
963
1012
  "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
964
- "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1x to 2x without changing visible canvas size, and adding/enabling it requires a full pnpm verify:perf checkpoint.",
1013
+ "For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1x to 2x without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
965
1014
  "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
966
1015
  "Performance fixes for canvas.renderScale must preserve the selected visual quality; do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp canvas.renderScale below the user's chosen value to pass budgets.",
967
1016
  "Do not enable canvas.renderScale for DOM/SVG/vector-native previews; preserve vector fidelity through native vector rendering instead of raster supersampling.",
@@ -1058,7 +1107,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
1058
1107
  "Expensive renderers must cache decoded media, source pixels, glyph atlases, gradients, and other reusable inputs by media id, canvas size, and stable control keys.",
1059
1108
  "Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
1060
1109
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
1061
- "A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, canvas.renderScale or the Resolution scale retina slider is added/enabled, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
1110
+ "A full performance checkpoint must run with pnpm verify:perf only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance.",
1111
+ "Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
1062
1112
  "Performance fixes must preserve selected output and preview quality; do not reduce image quality, selected renderScale, export resolution, source media fidelity, or canvas backing pixels as the hidden way to pass budgets.",
1063
1113
  "When canvas or slider interactions lag, diagnose where the slowdown comes from before changing output quality: renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, async render cancellation, or animation scheduling.",
1064
1114
  "Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
@@ -1138,6 +1188,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
1138
1188
  "Every visible controls-panel section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon; generated apps must not hand-build section headers.",
1139
1189
  "Controls-panel section expand and collapse uses the standard runtime height/opacity animation; generated apps must not replace it with instant custom section visibility.",
1140
1190
  "Ordinary controls-panel section collapsed/expanded state persists as a runtime UI preference per app. It is not undo/redo state, not settings import/export state, and Reset controls must not clear it. Runtime technical Setup/settings sections and sticky footer Export sections are not collapsible.",
1191
+ "Ordinary controls-panel section headers expose the runtime section reset action before the collapse button; it dispatches controls.resetTargets and restores only that section's control targets to their schema defaultValue.",
1141
1192
  "Ordinary controls-panel body sections use 8px top spacing and 24px bottom spacing for their control content. Runtime technical Setup/settings sections use 12px top and bottom spacing to match side padding. Sticky footer action sections keep their dedicated spacing.",
1142
1193
  "Broad section titles such as Flow, Icon, Shapes, Scene, Text, Typography, or Motion are only valid for small cohesive groups; use specific titles such as Flow Motion, Flow Geometry, Letter Burst, Shape Colors, Logo Glow, Logo Plate, or Text Block for larger groups.",
1143
1194
  "Section titles in one controls panel must be unique.",
@@ -89,9 +89,10 @@ describe("Toolcraft template decision contract", () => {
89
89
  expect(rule?.desiredBehavior).toMatch(/canvas drag, pan, pinch, zoom, and radar\/center/i);
90
90
  expect(rule?.desiredBehavior).toMatch(/playback state/i);
91
91
  expect(rule?.desiredBehavior).toMatch(/first working version/i);
92
- expect(rule?.desiredBehavior).toMatch(/renderer\/canvas\/animation\/export\/timeline\/layers/i);
93
- expect(rule?.desiredBehavior).toMatch(/bug that previously broke functionality/i);
94
92
  expect(rule?.desiredBehavior).toMatch(/optimize performance/i);
93
+ expect(rule?.desiredBehavior).toMatch(/otherwise complains about performance/i);
94
+ expect(rule?.desiredBehavior).toMatch(/targeted functional\/browser checks/i);
95
+ expect(rule?.desiredBehavior).toMatch(/touched workload\/viewport\/export paths/i);
95
96
  expect(rule?.desiredBehavior).toMatch(/full performance checkpoint/i);
96
97
  });
97
98
 
@@ -237,7 +237,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
237
237
  currentConstraint:
238
238
  "Performance coverage currently asks every visible non-action control for a performance scenario.",
239
239
  desiredBehavior:
240
- "Heavy workload controls get min/default/max workload coverage; ordinary controls get lightweight responsiveness coverage so they cannot hang or break input. Animated previews suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions without changing user playback state. A full performance checkpoint is required when the first working version of an app exists, when renderer/canvas/animation/export/timeline/layers change, when canvas.renderScale or the Resolution scale retina slider is added/enabled, after fixing a bug that previously broke functionality, after any performance optimization, and whenever the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom. Performance fixes must preserve the selected render scale and must not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping canvas.renderScale below the user's chosen value. Browser performance tests read budgets from typed performance config and run sequentially for stable measurements.",
240
+ "Heavy workload controls get min/default/max workload coverage; ordinary controls get lightweight responsiveness coverage so they cannot hang or break input. Animated previews suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions without changing user playback state. A full performance checkpoint is required only when the first working version of an app exists, or whenever the user explicitly asks to optimize performance, fix lag, remove jank, speed up animation, stabilize drag/zoom, or otherwise complains about performance. Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive controls need targeted functional/browser checks first and targeted performance scenarios only for touched workload/viewport/export paths. Performance fixes must preserve the selected render scale and must not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping canvas.renderScale below the user's chosen value. Browser performance tests read budgets from typed performance config and run sequentially for stable measurements.",
241
241
  enforcement: ["performance-validator", "browser-helper", "starter-agents"],
242
242
  id: "performance-coverage-levels",
243
243
  level: "invariant",
@@ -1271,6 +1271,24 @@ describe("ControlsPanel", () => {
1271
1271
  });
1272
1272
  });
1273
1273
 
1274
+ fireEvent.change(fontSizeInput, {
1275
+ target: { value: "768" },
1276
+ });
1277
+ fireEvent.blur(fontSizeInput);
1278
+
1279
+ await waitFor(() => {
1280
+ expect(JSON.parse(screen.getByTestId("font-value").textContent ?? "{}")).toEqual({
1281
+ color: "#FFFFFF",
1282
+ fontId: "roboto",
1283
+ fontSize: 768,
1284
+ fontWeight: "400",
1285
+ letterSpacing: "wider",
1286
+ lineHeight: "relaxed",
1287
+ opacity: 100,
1288
+ textCase: "uppercase",
1289
+ });
1290
+ });
1291
+
1274
1292
  const colorHexInput = screen.getByLabelText("Color hex") as HTMLInputElement;
1275
1293
  fireEvent.change(colorHexInput, {
1276
1294
  target: { value: "#C1FF00" },
@@ -1286,7 +1304,7 @@ describe("ControlsPanel", () => {
1286
1304
  expect(JSON.parse(screen.getByTestId("font-value").textContent ?? "{}")).toEqual({
1287
1305
  color: "#C1FF00",
1288
1306
  fontId: "roboto",
1289
- fontSize: 24,
1307
+ fontSize: 768,
1290
1308
  fontWeight: "400",
1291
1309
  letterSpacing: "wider",
1292
1310
  lineHeight: "relaxed",
@@ -3264,6 +3282,173 @@ describe("ControlsPanel", () => {
3264
3282
  expect(screen.getByTestId("enabled-value").textContent).toBe("true");
3265
3283
  });
3266
3284
 
3285
+ it("resets only controls from the selected section header action", () => {
3286
+ const schema = defineToolcraft({
3287
+ canvas: { enabled: false },
3288
+ panels: {
3289
+ controls: {
3290
+ sections: [
3291
+ {
3292
+ controls: {
3293
+ toneName: {
3294
+ defaultValue: "Soft",
3295
+ label: "Tone name",
3296
+ target: "tone.name",
3297
+ type: "text",
3298
+ },
3299
+ },
3300
+ title: "Tone",
3301
+ },
3302
+ {
3303
+ controls: {
3304
+ prompt: {
3305
+ defaultValue: "Poster",
3306
+ label: "Prompt",
3307
+ target: "generation.prompt",
3308
+ type: "text",
3309
+ },
3310
+ },
3311
+ title: "Output",
3312
+ },
3313
+ ],
3314
+ title: "Controls",
3315
+ },
3316
+ },
3317
+ });
3318
+ const { container } = renderControlsPanelWithSchema(schema);
3319
+
3320
+ fireEvent.change(screen.getByDisplayValue("Soft"), {
3321
+ target: { value: "Sharp" },
3322
+ });
3323
+ fireEvent.change(screen.getByDisplayValue("Poster"), {
3324
+ target: { value: "Banner" },
3325
+ });
3326
+
3327
+ const toneHeader = screen
3328
+ .getByText("Tone")
3329
+ .closest<HTMLElement>('[data-slot="control-section-header"]');
3330
+ const headerButtons = toneHeader?.querySelectorAll("button");
3331
+
3332
+ expect(headerButtons?.[0]?.getAttribute("data-control-section-reset-button")).toBe("");
3333
+ expect(headerButtons?.[1]?.getAttribute("data-control-section-collapse-button")).toBe("");
3334
+
3335
+ fireEvent.click(screen.getByRole("button", { name: "Reset Tone section" }));
3336
+
3337
+ const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
3338
+
3339
+ expect(values["tone.name"]).toBe("Soft");
3340
+ expect(values["generation.prompt"]).toBe("Banner");
3341
+ expect(container.querySelector('[data-control-section-reset-button]')).toBeTruthy();
3342
+ });
3343
+
3344
+ it("renders collection actions as add/remove controls for runtime item arrays", () => {
3345
+ const schema = defineToolcraft({
3346
+ canvas: { enabled: false },
3347
+ panels: {
3348
+ controls: {
3349
+ sections: [
3350
+ {
3351
+ controls: {
3352
+ colors: {
3353
+ defaultValue: [{ hex: "#DFFF1A" }, { hex: "#8CFF3A" }],
3354
+ itemControl: {
3355
+ defaultValue: { hex: "#C1FF00" },
3356
+ label: "Color",
3357
+ type: "color",
3358
+ },
3359
+ label: "Colors",
3360
+ minItems: 1,
3361
+ recommendedMaxItems: 2,
3362
+ target: "bead.colors",
3363
+ type: "collectionActions",
3364
+ },
3365
+ },
3366
+ title: "Bead Colors",
3367
+ },
3368
+ ],
3369
+ title: "Controls",
3370
+ },
3371
+ },
3372
+ });
3373
+
3374
+ const { container } = renderControlsPanelWithSchema(schema);
3375
+
3376
+ const addButton = screen.getByRole("button", { name: "Add Color" });
3377
+ const removeButton = screen.getByRole("button", { name: "Remove Color" });
3378
+ const collectionHeader = container.querySelector(
3379
+ '[data-slot="collection-actions-control-header"]',
3380
+ );
3381
+ const colorGrid = container.querySelector(
3382
+ '[data-slot="collection-actions-items-grid"]',
3383
+ );
3384
+
3385
+ expect(collectionHeader?.textContent).toContain("Colors");
3386
+ expect(collectionHeader?.lastElementChild?.contains(removeButton)).toBe(true);
3387
+ expect(collectionHeader?.lastElementChild?.contains(addButton)).toBe(true);
3388
+ expect(colorGrid).toBeTruthy();
3389
+ expect(colorGrid?.children).toHaveLength(2);
3390
+ expect(screen.queryByText("Color 1")).toBeNull();
3391
+ expect(screen.queryByText("Color 2")).toBeNull();
3392
+
3393
+ fireEvent.click(addButton);
3394
+ fireEvent.click(addButton);
3395
+
3396
+ let values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
3397
+
3398
+ expect(values["bead.colors"]).toHaveLength(4);
3399
+ expect(screen.queryByText("Color 4")).toBeNull();
3400
+ expect(colorGrid?.children).toHaveLength(4);
3401
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
3402
+
3403
+ fireEvent.click(removeButton);
3404
+ fireEvent.click(removeButton);
3405
+ fireEvent.click(removeButton);
3406
+
3407
+ values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
3408
+
3409
+ expect(values["bead.colors"]).toHaveLength(1);
3410
+ expect((removeButton as HTMLButtonElement).disabled).toBe(true);
3411
+ });
3412
+
3413
+ it("renders collection item controls beyond color lists", () => {
3414
+ const schema = defineToolcraft({
3415
+ canvas: { enabled: false },
3416
+ panels: {
3417
+ controls: {
3418
+ sections: [
3419
+ {
3420
+ controls: {
3421
+ labels: {
3422
+ defaultValue: ["Alpha"],
3423
+ itemControl: {
3424
+ defaultValue: "New label",
3425
+ label: "Label",
3426
+ type: "text",
3427
+ },
3428
+ minItems: 1,
3429
+ target: "glyph.labels",
3430
+ type: "collectionActions",
3431
+ },
3432
+ },
3433
+ title: "Glyph Labels",
3434
+ },
3435
+ ],
3436
+ title: "Controls",
3437
+ },
3438
+ },
3439
+ });
3440
+
3441
+ renderControlsPanelWithSchema(schema);
3442
+
3443
+ fireEvent.click(screen.getByRole("button", { name: "Add Label" }));
3444
+
3445
+ const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
3446
+
3447
+ expect(values["glyph.labels"]).toEqual(["Alpha", "New label"]);
3448
+ expect(screen.getByDisplayValue("Alpha")).toBeTruthy();
3449
+ expect(screen.getByDisplayValue("New label")).toBeTruthy();
3450
+ });
3451
+
3267
3452
  it("commits canvas size controls to runtime state on blur or Enter", () => {
3268
3453
  const { container } = renderControlsPanel();
3269
3454