@pixel-point/toolcraft 0.0.4 → 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 (49) hide show
  1. package/package.json +1 -1
  2. package/scripts/prepare-pack.mjs +5 -0
  3. package/src/generate.mjs +13 -0
  4. package/src/generate.test.mjs +7 -1
  5. package/templates/runtime/contracts/component-contracts.test.ts +96 -3
  6. package/templates/runtime/contracts/component-contracts.ts +75 -3
  7. package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
  8. package/templates/runtime/contracts/decision-contracts.ts +1 -1
  9. package/templates/runtime/react/canvas-shell.test.tsx +7 -7
  10. package/templates/runtime/react/controls-panel.test.tsx +455 -1
  11. package/templates/runtime/react/controls-panel.tsx +515 -30
  12. package/templates/runtime/react/settings-transfer.test.ts +3 -3
  13. package/templates/runtime/react/timeline-panel.test.tsx +69 -0
  14. package/templates/runtime/react/timeline-panel.tsx +98 -10
  15. package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
  16. package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
  17. package/templates/runtime/schema/define-toolcraft.test.ts +78 -1
  18. package/templates/runtime/schema/define-toolcraft.ts +145 -6
  19. package/templates/runtime/schema/runtime-targets.ts +1 -0
  20. package/templates/runtime/schema/types.ts +46 -1
  21. package/templates/runtime/state/canvas-zoom.ts +1 -1
  22. package/templates/runtime/state/create-template-state.test.ts +6 -6
  23. package/templates/runtime/state/reducer.test.ts +139 -8
  24. package/templates/runtime/state/reducer.ts +88 -22
  25. package/templates/runtime/state/types.ts +3 -0
  26. package/templates/starter/AGENTS.md +8 -8
  27. package/templates/starter/docs/toolcraft/README.md +4 -3
  28. package/templates/starter/docs/toolcraft/acceptance-testing.md +6 -2
  29. package/templates/starter/docs/toolcraft/assembly-workflow.md +8 -6
  30. package/templates/starter/docs/toolcraft/component-rules.md +17 -1
  31. package/templates/starter/docs/toolcraft/custom-controls.md +2 -2
  32. package/templates/starter/docs/toolcraft/performance.md +8 -7
  33. package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
  34. package/templates/starter/docs/toolcraft/schema-reference.md +15 -4
  35. package/templates/starter/docs/toolcraft/workflow.md +5 -8
  36. package/templates/starter/gitignore +36 -0
  37. package/templates/starter/package.json +1 -1
  38. package/templates/starter/src/app/starter-acceptance.test.ts +55 -0
  39. package/templates/starter/src/app/starter-acceptance.ts +67 -1
  40. package/templates/starter/src/app/starter-performance.test.ts +1 -1
  41. package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
  42. package/templates/ui/components/controls/collection-actions/index.ts +4 -0
  43. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
  44. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -6
  45. package/templates/ui/components/controls/index.ts +8 -0
  46. package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
  47. package/templates/ui/components/controls/slider/slider-value.ts +48 -5
  48. package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
  49. 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.4",
3
+ "version": "0.0.7",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import fs from "node:fs/promises";
3
4
 
4
5
  import { copyDirectory, removeDirectory } from "../src/copy-recursive.mjs";
5
6
 
@@ -27,3 +28,7 @@ await removeDirectory(templatesRoot);
27
28
  for (const source of sources) {
28
29
  await copyDirectory(source.from, source.to);
29
30
  }
31
+
32
+ const starterGitignorePath = path.join(templatesRoot, "starter/.gitignore");
33
+ const starterPackGitignorePath = path.join(templatesRoot, "starter/gitignore");
34
+ await fs.copyFile(starterGitignorePath, starterPackGitignorePath);
package/src/generate.mjs CHANGED
@@ -88,6 +88,18 @@ async function renameGeneratedAppFiles(targetDir) {
88
88
  }
89
89
  }
90
90
 
91
+ async function restoreGeneratedGitignore(targetDir) {
92
+ const npmSafeGitignorePath = path.join(targetDir, "gitignore");
93
+ const gitignorePath = path.join(targetDir, ".gitignore");
94
+
95
+ if (!(await pathExists(npmSafeGitignorePath))) {
96
+ return;
97
+ }
98
+
99
+ await fs.rm(gitignorePath, { force: true });
100
+ await fs.rename(npmSafeGitignorePath, gitignorePath);
101
+ }
102
+
91
103
  async function removeToolcraftTestFiles(toolcraftRoot) {
92
104
  async function visit(currentDir) {
93
105
  const entries = await fs.readdir(currentDir, { withFileTypes: true });
@@ -181,6 +193,7 @@ export async function generateToolcraft(options = {}) {
181
193
  await ensureWritableTargetDirectory(targetDir, { force: options.force });
182
194
 
183
195
  await copyDirectory(sourcePaths.starterDir, targetDir);
196
+ await restoreGeneratedGitignore(targetDir);
184
197
  await renameGeneratedAppFiles(targetDir);
185
198
 
186
199
  const toolcraftRoot = path.join(targetDir, "src/toolcraft");
@@ -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")));
@@ -116,6 +116,12 @@ describe("generateToolcraft", () => {
116
116
  assert.ok(await fs.stat(path.join(targetDir, "e2e/performance-helpers.ts")));
117
117
  assert.ok(await fs.stat(path.join(targetDir, "e2e/product-observable-helpers.ts")));
118
118
  assert.ok(await fs.stat(path.join(targetDir, "e2e/canvas-handle-helpers.ts")));
119
+ const gitignoreSource = await fs.readFile(path.join(targetDir, ".gitignore"), "utf8");
120
+ assert.match(gitignoreSource, /node_modules/);
121
+ assert.match(gitignoreSource, /dist/);
122
+ assert.match(gitignoreSource, /playwright-report/);
123
+ assert.match(gitignoreSource, /\.env\.\*/);
124
+ await assert.rejects(() => fs.stat(path.join(targetDir, "gitignore")), /ENOENT/);
119
125
  assert.ok(await fs.stat(path.join(targetDir, "scripts/check-ai-skills.mjs")));
120
126
  assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.mjs")));
121
127
  assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.test.mjs")));
@@ -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
 
@@ -345,6 +375,24 @@ describe("Toolcraft template component contracts", () => {
345
375
  expect(contract.aiUsageRules).toContain(
346
376
  "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.",
347
377
  );
378
+ expect(contract.aiUsageRules).toContain(
379
+ "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.",
380
+ );
381
+ expect(contract.aiUsageRules).toContain(
382
+ "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
383
+ );
384
+ expect(contract.aiUsageRules).toContain(
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.",
386
+ );
387
+ expect(contract.aiUsageRules).toContain(
388
+ "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
389
+ );
390
+ expect(contract.aiUsageRules).toContain(
391
+ "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.",
392
+ );
393
+ expect(contract.aiUsageRules).toContain(
394
+ "Do not enable canvas.renderScale for DOM/SVG/vector-native previews; preserve vector fidelity through native vector rendering instead of raster supersampling.",
395
+ );
348
396
  });
349
397
 
350
398
  it("documents persistence as a runtime-owned policy instead of ad hoc localStorage", () => {
@@ -392,7 +440,7 @@ describe("Toolcraft template component contracts", () => {
392
440
  "A settings-transfer section with only Export Settings and Import Settings means canvas sizing is not editable-output or canvas size controls already exist elsewhere.",
393
441
  );
394
442
  expect(contract.aiUsageRules).toContain(
395
- "When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, and Canvas height in that order and renders without a visible section heading.",
443
+ "When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, and optional Resolution scale in that order and renders without a visible section heading.",
396
444
  );
397
445
  });
398
446
 
@@ -415,6 +463,21 @@ describe("Toolcraft template component contracts", () => {
415
463
  expect(slider.aiUsageRules).toContain(
416
464
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
417
465
  );
466
+ expect(slider.aiUsageRules).toContain(
467
+ "Use slider unit only for measurement or scale suffixes such as %, px, °, x, s, ms, fps, rows/cols, or similar domain units.",
468
+ );
469
+ expect(slider.aiUsageRules).toContain(
470
+ "Do not use unit for repeated entity nouns already named by the section or label, such as Letters + letters, Shape Density / Count + shapes, Words + words, Symbols + symbols, Items + items, Particles + particles, or Layers + layers.",
471
+ );
472
+ expect(slider.aiUsageRules).toContain(
473
+ "When the value needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
474
+ );
475
+ expect(slider.aiUsageRules).toContain(
476
+ "Compact symbol/CSS units render tight, such as 70%, 24px, 1.2x, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
477
+ );
478
+ expect(slider.aiUsageRules).toContain(
479
+ "Slider valueLabel is editable only when it contains a numeric value; textual state labels such as Normal are display-only and must not expose hover or click editing affordances.",
480
+ );
418
481
  expect(slider.aiUsageRules).toContain(
419
482
  "Schema sliders render stacked at full width; do not put sliders in two-column inline layout groups.",
420
483
  );
@@ -451,6 +514,15 @@ describe("Toolcraft template component contracts", () => {
451
514
  expect(rangeSlider.aiUsageRules).toContain(
452
515
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
453
516
  );
517
+ expect(rangeSlider.aiUsageRules).toContain(
518
+ "Use rangeSlider unit only for measurement or scale suffixes; do not use it for repeated entity nouns already named by the section or label.",
519
+ );
520
+ expect(rangeSlider.aiUsageRules).toContain(
521
+ "When a range label needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
522
+ );
523
+ expect(rangeSlider.aiUsageRules).toContain(
524
+ "Compact symbol/CSS units render tight, such as 20% – 80% or 12px – 48px; word units render with a space when truly needed.",
525
+ );
454
526
  expect(rangeSlider.aiUsageRules).toContain(
455
527
  "RangeSlider is always a full-width two-thumb control; never place it in an inline two-column layout group with another slider or range slider.",
456
528
  );
@@ -686,7 +758,16 @@ describe("Toolcraft template component contracts", () => {
686
758
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
687
759
  );
688
760
  expect(contract.aiUsageRules).toContain(
689
- "A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, 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.",
765
+ );
766
+ expect(contract.aiUsageRules).toContain(
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.",
768
+ );
769
+ expect(contract.aiUsageRules).toContain(
770
+ "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.",
690
771
  );
691
772
  expect(contract.aiUsageRules).toContain(
692
773
  "Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
@@ -828,6 +909,9 @@ describe("Toolcraft template component contracts", () => {
828
909
  expect(contract.aiUsageRules).toContain(
829
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.",
830
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
+ );
831
915
  expect(contract.aiUsageRules).toContain(
832
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.",
833
917
  );
@@ -849,6 +933,9 @@ describe("Toolcraft template component contracts", () => {
849
933
  expect(contract.aiUsageRules).toContain(
850
934
  "If there is no useful product-specific explanation, omit control.description; the runtime should not show a help tooltip for that label.",
851
935
  );
936
+ expect(contract.aiUsageRules).toContain(
937
+ "Do not add control.description to sequential colors such as Color 1, Color 2, or simple palette controls such as Spread when the section title already names the color or palette context.",
938
+ );
852
939
  expect(contract.aiUsageRules).toContain(
853
940
  "For compound controls such as FontPicker, do not use control.description to enumerate the control's owned fields. FontPicker descriptions must not recap font family, weight, size, case, color, opacity, letter spacing, or line height; use description only for non-obvious product scope or omit it.",
854
941
  );
@@ -1101,6 +1188,12 @@ describe("Toolcraft template component contracts", () => {
1101
1188
  expect(contract.aiUsageRules).toContain(
1102
1189
  "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
1103
1190
  );
1191
+ expect(contract.aiUsageRules).toContain(
1192
+ "Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
1193
+ );
1194
+ expect(contract.aiUsageRules).toContain(
1195
+ "When multiple uploaded images are present, the runtime appends media, shows a four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
1196
+ );
1104
1197
  expect(contract.aiUsageRules).toContain(
1105
1198
  "In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
1106
1199
  );
@@ -49,6 +49,11 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
49
49
  'Small semantic integer domains such as rows, cols, gaps, jitter, counts, levels, bands, passes, points, tiles, and segments must use variant: "discrete".',
50
50
  'Finite animation step domains such as flip depth, character count, glyph steps, and frame steps must use variant: "discrete" when the marker count stays within the Toolcraft visual budget.',
51
51
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
52
+ "Use slider unit only for measurement or scale suffixes such as %, px, °, x, s, ms, fps, rows/cols, or similar domain units.",
53
+ "Do not use unit for repeated entity nouns already named by the section or label, such as Letters + letters, Shape Density / Count + shapes, Words + words, Symbols + symbols, Items + items, Particles + particles, or Layers + layers.",
54
+ "When the value needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
55
+ "Compact symbol/CSS units render tight, such as 70%, 24px, 1.2x, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
56
+ "Slider valueLabel is editable only when it contains a numeric value; textual state labels such as Normal are display-only and must not expose hover or click editing affordances.",
52
57
  "Schema sliders render stacked at full width; do not put sliders in two-column inline layout groups.",
53
58
  "The fontPicker component is the only built-in exception with two internal footer sliders for letter spacing and line height.",
54
59
  "For a small named option set, prefer Select or Segmented instead of forcing a discrete Slider.",
@@ -94,6 +99,9 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
94
99
  'Small semantic integer domains such as rows, cols, gaps, jitter, counts, levels, bands, passes, points, tiles, and segments must use variant: "discrete".',
95
100
  'Finite animation step domains such as flip depth, character count, glyph steps, and frame steps must use variant: "discrete" when the marker count stays within the Toolcraft visual budget.',
96
101
  "Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
102
+ "Use rangeSlider unit only for measurement or scale suffixes; do not use it for repeated entity nouns already named by the section or label.",
103
+ "When a range label needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
104
+ "Compact symbol/CSS units render tight, such as 20% – 80% or 12px – 48px; word units render with a space when truly needed.",
97
105
  "RangeSlider is always a full-width two-thumb control; never place it in an inline two-column layout group with another slider or range slider.",
98
106
  "RangeSlider defaultValue must start with different lower and upper values so the two-thumb control does not collapse into a single-value slider.",
99
107
  "Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes; do not create custom parsers for RangeSlider labels.",
@@ -384,6 +392,55 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
384
392
  "Acceptance and browser tests must click each Actions button and prove the product output or runtime state for the nearby entity changed.",
385
393
  ],
386
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
+ },
387
444
  panelActions: {
388
445
  ...control("panelActions", "PanelActions", "standalone", "component-owned"),
389
446
  decisionCatalog: decisionCatalog({
@@ -806,6 +863,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
806
863
  ],
807
864
  layoutConstraints: [
808
865
  "FileDrop lives in the controls panel; single-layer apps use its preview and clear behavior.",
866
+ "When fileDrop has multiple: true and more than one image is present, the runtime renders a four-column thumbnail grid with the add-more tile last.",
809
867
  ],
810
868
  requiredAcceptance: [
811
869
  "Prove file import changes media state and product output; prove clear removes source material.",
@@ -814,6 +872,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
814
872
  aiUsageRules: [
815
873
  "Use fileDrop for source material uploads in the controls panel, not on the canvas.",
816
874
  "In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
875
+ "Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
876
+ "When multiple uploaded images are present, the runtime appends media, shows a four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
817
877
  "In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
818
878
  ],
819
879
  commands: ["media.delete", "media.import"],
@@ -917,7 +977,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
917
977
  }),
918
978
  aiUsageRules: [
919
979
  "Use custom controls only for product interactions that built-in controls cannot express.",
920
- "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.",
921
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.",
922
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.",
923
983
  "Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or status that affects the product.",
@@ -942,11 +1002,18 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
942
1002
  "Choose canvas.sizing.mode from product context instead of copying a universal 1024px artboard.",
943
1003
  "Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
944
1004
  "Use editable-output by default for generated, exportable, shader, poster, badge, wall, banner, thumbnail, and product-output apps where users should see or edit width and height.",
1005
+ "When no explicit product size is provided, the runtime default canvas is 16:9 at 1920x1080; do not reset a new product-output app to 1:1 unless the product meaning requires it.",
945
1006
  "A user-provided base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output unless the prompt or reference explicitly locks output dimensions.",
946
1007
  "Use fixed-output only when the product output size must not be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
947
1008
  "Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and do not depend on settingsTransfer.",
948
1009
  "If canvas.size is provided without an explicit sizing mode, defineToolcraft treats it as editable-output and adds Canvas width and Canvas height controls.",
949
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.",
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.",
1012
+ "Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
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.",
1014
+ "After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
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.",
1016
+ "Do not enable canvas.renderScale for DOM/SVG/vector-native previews; preserve vector fidelity through native vector rendering instead of raster supersampling.",
950
1017
  ],
951
1018
  capabilities: ["drag", "zoom", "radar", "upload", "editable-size"],
952
1019
  commands: [
@@ -993,7 +1060,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
993
1060
  "Do not hand-roll settings import/export through app routes, hidden file inputs, or panelActions.",
994
1061
  "Settings transfer appears as the first technical Setup controls-panel section when enabled and renders without a visible section heading; it imports and exports control values, canvas size, and timeline state.",
995
1062
  "A settings-transfer section with only Export Settings and Import Settings means canvas sizing is not editable-output or canvas size controls already exist elsewhere.",
996
- "When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, and Canvas height in that order and renders without a visible section heading.",
1063
+ "When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, and optional Resolution scale in that order and renders without a visible section heading.",
997
1064
  "Keep sticky footer panelActions for product delivery actions only, such as Export PNG, Export Video, Copy, Generate, Apply, or Download.",
998
1065
  ],
999
1066
  capabilities: ["settings-import-export"],
@@ -1040,7 +1107,10 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
1040
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.",
1041
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.",
1042
1109
  "Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
1043
- "A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, 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.",
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.",
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.",
1044
1114
  "Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
1045
1115
  "Custom renderer apps must mirror the Renderer Technique Decision Matrix in typed rendererTechnique config so validation can reject contradictory renderer choices.",
1046
1116
  "Custom renderer specs must include a Renderer Layer Inventory and mirror it in typed rendererTechnique.layers so dense raster backgrounds cannot silently rasterize semantic foreground output.",
@@ -1118,6 +1188,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
1118
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.",
1119
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.",
1120
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.",
1121
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.",
1122
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.",
1123
1194
  "Section titles in one controls panel must be unique.",
@@ -1126,6 +1197,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
1126
1197
  "Use control.description for the short help tooltip shown beside visible labels. It must describe the product behavior or output affected by the control, not restate the label.",
1127
1198
  "Do not write label-recap descriptions such as Adjusts Opacity, Controls Speed, or Sets Background.",
1128
1199
  "If there is no useful product-specific explanation, omit control.description; the runtime should not show a help tooltip for that label.",
1200
+ "Do not add control.description to sequential colors such as Color 1, Color 2, or simple palette controls such as Spread when the section title already names the color or palette context.",
1129
1201
  "For compound controls such as FontPicker, do not use control.description to enumerate the control's owned fields. FontPicker descriptions must not recap font family, weight, size, case, color, opacity, letter spacing, or line height; use description only for non-obvious product scope or omit it.",
1130
1202
  "The runtime renders a filled Phosphor question icon beside each visible ControlFieldLabel; generated apps must not hand-build their own help icon beside built-in labels.",
1131
1203
  "If a source label is unavoidably long, keep the visible label concise and rely on the native title tooltip for the full text.",
@@ -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, 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. 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",
@@ -226,7 +226,7 @@ describe("CanvasShell", () => {
226
226
  await waitFor(() => {
227
227
  expect(screen.getByTestId("canvas-offset").textContent).toBe("-8,12");
228
228
  });
229
- expect(screen.getByTestId("canvas-zoom").textContent).toBe("70");
229
+ expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
230
230
  });
231
231
 
232
232
  it("zooms the canvas world from a trackpad pinch around the pointer", async () => {
@@ -243,7 +243,7 @@ describe("CanvasShell", () => {
243
243
  const world = canvas.querySelector("[data-toolcraft-canvas-world]") as HTMLElement;
244
244
  mockCanvasRect(canvas);
245
245
 
246
- expect(world.style.transform).toBe("translate(-50%, -50%) translate(0px, 0px) scale(0.7)");
246
+ expect(world.style.transform).toBe("translate(-50%, -50%) translate(0px, 0px) scale(1)");
247
247
 
248
248
  const wheelEvent = new WheelEvent("wheel", {
249
249
  bubbles: true,
@@ -257,10 +257,10 @@ describe("CanvasShell", () => {
257
257
  expect(canvas.dispatchEvent(wheelEvent)).toBe(false);
258
258
  expect(wheelEvent.defaultPrevented).toBe(true);
259
259
  await waitFor(() => {
260
- expect(screen.getByTestId("canvas-zoom").textContent).toBe("120");
260
+ expect(screen.getByTestId("canvas-zoom").textContent).toBe("150");
261
261
  });
262
- expect(world.style.transform).toContain("translate(-142.857142857142");
263
- expect(world.style.transform).toContain("scale(1.2)");
262
+ expect(world.style.transform).toContain("translate(-100px");
263
+ expect(world.style.transform).toContain("scale(1.5)");
264
264
  });
265
265
 
266
266
  it("prevents panel pinch gestures without moving the canvas viewport", async () => {
@@ -294,7 +294,7 @@ describe("CanvasShell", () => {
294
294
 
295
295
  expect(overlay.dispatchEvent(wheelEvent)).toBe(false);
296
296
  expect(wheelEvent.defaultPrevented).toBe(true);
297
- expect(screen.getByTestId("canvas-zoom").textContent).toBe("70");
297
+ expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
298
298
  expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
299
299
  expect(overlay.closest("[data-toolcraft-canvas-world]")).toBeNull();
300
300
 
@@ -306,7 +306,7 @@ describe("CanvasShell", () => {
306
306
 
307
307
  expect(overlay.dispatchEvent(scrollEvent)).toBe(true);
308
308
  expect(scrollEvent.defaultPrevented).toBe(false);
309
- expect(screen.getByTestId("canvas-zoom").textContent).toBe("70");
309
+ expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
310
310
  expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
311
311
  });
312
312