@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
@@ -146,7 +146,9 @@ Async product actions such as Export, Download, Copy, Generate, or Apply must re
146
146
 
147
147
  For complex apps, use schema `settingsTransfer: "auto"` or `true` for settings import/export. Recalculate settings-transfer eligibility after adding, removing, or reorganizing controls, sections, timeline, or layers. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Do not put Import Settings or Export Settings in sticky footer `panelActions`; runtime inserts the technical `Setup` settings-transfer section first without a visible section heading.
148
148
 
149
- If the app also uses `editable-output` canvas sizing, that first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, and `Canvas height` in that order. Do not split the canvas size fields and settings-transfer actions into app-authored sections.
149
+ If the app also uses `editable-output` canvas sizing, that first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split the canvas size fields and settings-transfer actions into app-authored sections.
150
+
151
+ For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` to the same first technical section. The slider changes backing resolution from `1x` to `2x` without changing visible canvas size; DOM/SVG/vector-native previews should not use it.
150
152
 
151
153
  If a controls panel shows only `Export Settings` and `Import Settings` in the first runtime section, check the canvas sizing decision. Product-output apps usually need `editable-output`; intrinsic media and explicitly fixed output are the cases where visible canvas size inputs are absent.
152
154
 
@@ -188,8 +190,8 @@ Use these tiers:
188
190
  | Tier 0 — docs/copy | Documentation, comments, copy, labels, or titles change without schema targets, values, runtime behavior, renderer output, or layout mechanics. | Targeted docs/typecheck or targeted app test. Browser is not required unless visual text fitting is the risk. |
189
191
  | Tier 1 — local control presentation | One control or panel visual state changes: spacing, hover, focus, disabled, marker visibility, label fit, or component variant display. Runtime state shape and product renderer are unchanged. | Targeted unit/component test plus one focused browser check for the affected control or panel. |
190
192
  | Tier 2 — schema/product behavior | Controls, sections, defaults, persistence, panel actions, export actions, acceptance rows, or product behavior mapping changes. | `pnpm verify:quick` plus relevant browser acceptance. Run perf only when the changed control affects renderer workload or responsiveness. |
191
- | Tier 3 — renderer/canvas/runtime feature | Custom renderer, animation loop, canvas sizing, upload/media, timeline, layers, toolbar, export bytes, WebGL/Canvas/SVG output, zoom, radar, history, heavy control behavior changes, or a post-generation iteration that touches renderer workload or viewport stability. | `pnpm verify:quick`, targeted browser acceptance, and relevant `pnpm verify:perf` scenarios for touched workload/viewport/export paths. |
192
- | Tier 4 — final delivery/template architecture | Fresh generated app completion, folder export, commit-ready delivery, dependency changes, runtime/template/contract/CLI changes, broad refactors, or major post-generation iterations that rewrite renderer, canvas, animation, timeline/keyframes, layers, media, export, or control mapping. | Fresh folders run `pnpm install` once, then `pnpm verify:final`, then start `pnpm dev` to provide the local URL. |
193
+ | Tier 3 — renderer/canvas/runtime feature | Custom renderer, animation loop, canvas sizing, upload/media, timeline, layers, toolbar, export bytes, WebGL/Canvas/SVG output, zoom, radar, history, heavy control behavior changes, or a post-generation iteration that touches renderer workload or viewport stability. | `pnpm verify:quick`, targeted browser acceptance, and targeted performance scenarios only for touched workload/viewport/export paths. |
194
+ | Tier 4 — final delivery/template architecture | Fresh generated app completion, folder export, commit-ready delivery, dependency changes, runtime/template/contract/CLI changes, broad refactors, or major post-generation iterations that rewrite renderer, canvas, animation, timeline/keyframes, layers, media, export, or control mapping. | Fresh folders run `pnpm install` once, then `pnpm verify:final`; add `pnpm verify:perf` only for the first working app version or explicit performance complaints, then start `pnpm dev` to provide the local URL. |
193
195
 
194
196
  Choose the tier by blast radius, not by line count. If uncertain, move one tier higher, not automatically to Tier 4.
195
197
 
@@ -197,9 +199,9 @@ Do not rerun `pnpm install` after every edit. Run it after fresh export, depende
197
199
 
198
200
  Use `pnpm verify:ui` when a tier calls for the browser acceptance suite without the performance suite. Use a focused named Playwright test instead when only one entity changed and the relevant test is already known.
199
201
 
200
- Run a full performance checkpoint with `pnpm verify:perf` when the first working version of the app exists, when renderer/canvas/animation/export/timeline/layers change, after fixing a bug that previously broke functionality, after any performance optimization, or when the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
202
+ Run a full performance checkpoint with `pnpm verify:perf` only when the first working version of the app exists, or when the user explicitly asks to optimize performance, fix lag, remove jank, speed up animation, stabilize drag/zoom, or otherwise complains about performance.
201
203
 
202
- Fast feature loops may defer full performance only when none of the checkpoint triggers apply. Record the deferred check and reason in the worklog.
204
+ Feature loops after the first working version do not run the full performance suite by default. Renderer, canvas, animation, export, timeline, layers, `canvas.renderScale`, bug fixes, and performance-sensitive controls still need targeted functional/browser checks first, plus targeted performance scenarios only when they directly exercise the touched path. Record any skipped full performance run and reason in the worklog.
203
205
 
204
206
  For final delivery, run:
205
207
 
@@ -208,6 +210,6 @@ pnpm verify:final
208
210
  pnpm dev
209
211
  ```
210
212
 
211
- Browser verification must use the real Toolcraft shell plus renderer output. `pnpm verify:final` runs the full static, build, browser, and browser performance gate. `pnpm dev` is intentionally separate because it keeps the local server running.
213
+ Browser verification must use the real Toolcraft shell plus renderer output. `pnpm verify:final` runs the full static, build, and browser functional gate. `pnpm verify:perf` is intentionally separate and only runs for the two full-performance triggers. `pnpm dev` is intentionally separate because it keeps the local server running.
212
214
 
213
215
  Do not stop existing local servers to free `3002`. `pnpm dev`, `pnpm preview`, and browser verification prefer `3002`, then automatically use the next free port when it is occupied.
@@ -22,6 +22,7 @@ Common exact-owner choices:
22
22
  - Use `imagePicker` for choosing one visual option from a set.
23
23
  - Use `palette` only for constrained design-token color choices with both family and shade: brand palette, Tailwind-like token color, style-guide color scale, semantic palette family, or theme accent token.
24
24
  - Use `actions` for local section commands that affect only the nearby entity, such as randomize palette, normalize weights, sort glyphs, clear selection, duplicate item, or reset current stop.
25
+ - Use `collectionActions` for repeatable product entities whose actual item list can grow or shrink, such as colors, glyphs, symbols, points, rules, variants, or object entries. Use it instead of a count slider when the user edits the actual set. The item list must be runtime state that changes preview/export, not panel-only row chrome. The collection control shows the collection `label` on the left and remove/add icon buttons on the right. Homogeneous repeated items do not show visible per-item labels like `Color 1`, `Color 2`, `Item 1`, or `Item 2` when the collection label already names the group. Item controls should use built-ins such as `color`, `colorOpacity`, `text`, `select`, `segmented`, `slider`, `switch`, `checkbox`, or `rangeInput` before any custom renderer.
25
26
  - Use `panelActions` for sticky final product actions such as export, copy, generate, apply, or download.
26
27
 
27
28
  Small action buttons inside custom controls are for item-level actions such as remove, reorder, add stop, or delete stop. Use schema `actions` for section-level local commands. Keep final product actions in `panelActions`, keep timeline transport in the top timeline, and keep global reset in the controls panel header.
@@ -35,6 +36,7 @@ For local reset-like `actions`, use product-specific values such as `reset-curre
35
36
  - If a section contains exactly one control, whether simple or compound, render only the parent section dividers.
36
37
  - Do not add full-width borders inside a compound control, and do not put dividers only around an internal subsection such as Gradient Stops.
37
38
  - Small compound fields such as `colorOpacity` and `rangeInput` stay inline fields without section dividers.
39
+ - `collectionActions` is a compound control when it shares a section with generated item controls, so it follows the same content-width divider rules. Place it at the start of the controlled section. Generated item controls still follow normal density rules: plain colors use equal 50% columns when they fit, while color+opacity items stay stacked.
38
40
 
39
41
  ## Sliders
40
42
 
@@ -50,6 +52,10 @@ Visual discrete sliders must declare `step`; the runtime derives one marker per
50
52
 
51
53
  Schema sliders always render stacked at full width. Do not put `slider` or `rangeSlider` controls in two-column inline rows. The only built-in exception is `fontPicker`, whose letter-spacing and line-height footer sliders stay paired inside that component.
52
54
 
55
+ Use slider `unit` only for measurement or scale suffixes: `%`, `px`, `°`, `x`, `s`, `ms`, `fps`, `rows`, `cols`, or a similarly useful domain unit. Do not use `unit` to repeat the entity already named by the section or label. Avoid `Letters` + `letters`, `Shape Density / Count` + `shapes`, `Words` + `words`, `Symbols` + `symbols`, `Items` + `items`, `Particles` + `particles`, and `Layers` + `layers`. If the numeric value needs an entity noun to make sense, rename the label or section instead of appending the noun to the value. Compact units render tight (`70%`, `24px`, `1.2x`, `8s`); word or acronym units render with a space (`5 cols`, `17 fps`) only when they are truly needed.
56
+
57
+ Slider value labels are editable only when they contain a numeric value. Textual state labels such as `Normal` are display-only and must not expose hover or click editing affordances.
58
+
53
59
  Range sliders are always full-width two-thumb controls. Do not put a `rangeSlider` in an inline row. Its `defaultValue` must start with different lower and upper values, such as `[20, 80]`, so the control does not collapse into a single-value slider.
54
60
 
55
61
  Range slider value editing accepts common range separators such as `20/80`, `20-80`, `20 - 80`, `20 80`, and en-dash ranges. Use the built-in parser instead of adding custom label parsing.
@@ -106,6 +112,8 @@ Section expand/collapse uses the standard runtime height/opacity animation. Do n
106
112
 
107
113
  Ordinary section collapsed/expanded state persists as a per-app runtime UI preference. 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.
108
114
 
115
+ Ordinary 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`.
116
+
109
117
  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.
110
118
 
111
119
  ## Colors
@@ -134,6 +142,8 @@ Use `fileDrop` for source material uploads in the controls panel. Do not place u
134
142
 
135
143
  In single-layer apps, the runtime shows uploaded image preview and clear button in the file control. Clearing removes source material from the renderer and canvas.
136
144
 
145
+ Use `multiple: true` when the app needs several uploaded images as one source set. The runtime appends media, switches to a four-column thumbnail grid when more than one image is present, puts the add-more tile last, and keeps per-image removal inside the file control.
146
+
137
147
  In multi-layer apps, deletion and visibility belong to the Layers panel; `fileDrop` stays an upload target.
138
148
 
139
149
  ## Image Picker
@@ -211,6 +221,8 @@ Short labels must still be semantically sufficient with nearby context. `Animati
211
221
 
212
222
  Visible control labels can get a runtime-owned filled Phosphor question tooltip icon. Put a concise product-specific explanation in `description` only when it adds meaning beyond the label. Do not write recaps like `Adjusts Opacity`, and do not build custom help icons beside built-in labels.
213
223
 
224
+ Do not add `description` to obvious color clusters. If a section title already names the palette/color context, sequential labels such as `Color 1`, `Color 2`, or simple palette controls such as `Spread` do not need help icons. Keep the whole obvious group clean unless the tooltip explains a non-obvious product behavior.
225
+
214
226
  For compound controls such as `fontPicker`, `description` must not enumerate owned fields like font, weight, size, case, color, opacity, letter spacing, or line height. The component already labels those fields.
215
227
 
216
228
  If a source label is unavoidably long, keep the visible label concise and rely on native `title` for the full text.
@@ -267,10 +279,14 @@ Use schema `settingsTransfer` for settings import/export. Do not add Import Sett
267
279
 
268
280
  Recalculate settings-transfer eligibility after adding, removing, or reorganizing controls, sections, timeline, or layers. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. If the threshold is reached, use `settingsTransfer: "auto"` / `true` or document a product-specific opt-out through `runtime.settingsTransfer` acceptance evidence.
269
281
 
270
- When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, and `Canvas height` in that order. Do not split these into separate app-authored sections, rename the controls, or rebuild the block by hand.
282
+ When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split these into separate app-authored sections, rename the controls, or rebuild the block by hand.
271
283
 
272
284
  If only `Export Settings` and `Import Settings` appear in that section, the schema is not using `editable-output` canvas sizing or already owns `canvas.size.width` / `canvas.size.height` controls. For product-output apps, prefer fixing the canvas sizing decision over adding hand-built size fields.
273
285
 
286
+ Manual `Canvas width` or `Canvas height` edits are exact output-size edits. They keep the other dimension unchanged, switch `Aspect ratio` to `Custom`, and update the custom ratio inputs to the reduced current ratio. Do not recreate the old behavior where typing one size field stays locked to the previous aspect preset.
287
+
288
+ Enable `canvas.renderScale: true` for non-vector raster previews such as Canvas 2D, WebGL, or WebGPU output. Runtime adds a `Resolution scale` slider after canvas sizing; it defaults to `2x` and lets users trade preview quality/performance without changing output size. Adding or enabling this slider requires targeted browser evidence that the canvas stays responsive while dragging sliders or other high-frequency controls at the selected scale. Full `pnpm verify:perf` is required only for the first working app version or explicit performance complaints. Performance fixes must preserve the selected scale and keep canvas preview responsive. Diagnose the actual bottleneck before lowering quality; do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews.
289
+
274
290
  Reset belongs to the controls panel header reset button. Do not add a footer action with `label`, `value`, or `command` containing reset; acceptance treats that as a duplicate Reset.
275
291
 
276
292
  Still-output product apps include one primary `Export PNG` action.
@@ -2,11 +2,11 @@
2
2
 
3
3
  Use a custom control only when no built-in Toolcraft control represents the product interaction.
4
4
 
5
- Built-ins come first: `slider`, `rangeSlider`, `select`, `segmented`, `switch`, `checkbox`, `color`, `colorOpacity`, `vector`, `gradient`, `curves`, `fontPicker`, `imagePicker`, `fileDrop`, `text`, `code`, `rangeInput`, `palette`, `actions`, and `panelActions`.
5
+ Built-ins come first: `slider`, `rangeSlider`, `select`, `segmented`, `switch`, `checkbox`, `color`, `colorOpacity`, `vector`, `gradient`, `curves`, `fontPicker`, `imagePicker`, `fileDrop`, `text`, `code`, `rangeInput`, `palette`, `actions`, `collectionActions`, and `panelActions`.
6
6
 
7
7
  Register custom renderers through `ToolcraftApp controlRenderers`.
8
8
 
9
- Do not use `controlRenderers` to recreate a built-in control. If the product needs a slider, select, segmented mode picker, color input, gradient editor, font picker, upload, textarea, local action group, or footer action, declare the matching schema control instead of rendering the component manually.
9
+ Do not use `controlRenderers` to recreate a built-in control. If the product needs a slider, select, segmented mode picker, color input, gradient editor, font picker, upload, textarea, local action group, repeatable item add/remove, or footer action, declare the matching schema control instead of rendering the component manually.
10
10
 
11
11
  Do not edit `ControlsPanel`, copied `src/toolcraft`, or Toolcraft internals inside a generated app.
12
12
 
@@ -55,6 +55,8 @@ Ordinary controls still need lightweight responsiveness checks. They should not
55
55
  - panel scroll affecting canvas zoom;
56
56
  - timeline or layer interactions destabilizing the viewport.
57
57
 
58
+ When `canvas.renderScale` / `Resolution scale` is enabled, responsiveness coverage must include slider or other high-frequency control drags at the selected scale. If the canvas lags, diagnose the source before changing quality: renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling.
59
+
58
60
  ## Renderer Performance
59
61
 
60
62
  Custom renderers should:
@@ -99,14 +101,13 @@ Animation checks should sample enough frames to catch jank. Interaction budgets
99
101
 
100
102
  Use `app-performance.ts` as the single budget and fixture source. Browser performance tests must call `getToolcraftPerformanceStressValue(appPerformance, scenarioId)` for workload values and `expectToolcraftScenarioPerformanceBudget(..., appPerformance, scenarioId)` for budgets.
101
103
 
102
- Run `pnpm verify:perf` for Tier 3 performance-sensitive edits and inside `pnpm verify:final` before delivery. It runs `e2e/app-performance.spec.ts` and every `browser perf:` scenario with one worker so budget failures are not hidden or created by unrelated parallel browser tests.
104
+ Run targeted performance scenarios for Tier 3 performance-sensitive edits when they directly exercise the touched workload, viewport, or export path. `pnpm verify:perf` is the full performance suite; it runs `e2e/app-performance.spec.ts` and every `browser perf:` scenario with one worker so budget failures are not hidden or created by unrelated parallel browser tests.
103
105
 
104
- Run a full performance checkpoint with `pnpm verify:perf` when:
106
+ Run a full performance checkpoint with `pnpm verify:perf` only when:
105
107
 
106
108
  - the first working version of the app exists;
107
- - renderer, canvas, animation, export, timeline, or layers change;
108
- - a bug that previously broke functionality is fixed;
109
- - a performance optimization lands;
110
- - the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
109
+ - the user explicitly asks to optimize performance, fix lag, remove jank, speed up animation, stabilize drag/zoom, or otherwise complains about performance.
110
+
111
+ Performance fixes must preserve selected output and preview quality. Do not pass budgets by lowering image quality, selected `canvas.renderScale`, export resolution, source media fidelity, or canvas backing pixels unless the user explicitly chooses that lower-quality value through a visible control. Prefer coalescing slider updates, caching expensive inputs, moving work off the React render path, reusing GPU resources, or changing renderer strategy over reducing visual fidelity.
111
112
 
112
- Do not use the full performance suite as the default loop for Tier 0-2 edits. Those edits still need the targeted checks named by the verification tier, but they should not pay for renderer and viewport stress tests unless a checkpoint trigger applies. If a fast feature loop defers full performance, record the deferred check and reason in the worklog.
113
+ Do not use the full performance suite as the default loop for feature work after the first working version. Those edits still need the targeted checks named by the verification tier, but they should not pay for every renderer and viewport stress test unless one of the two full-performance triggers applies. If a feature loop skips full performance, record the reason in the worklog.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Choose render technology per product layer. Do not choose a renderer because it is convenient; choose it from product output semantics, reference behavior, fidelity, and workload.
4
4
 
5
- The initial renderer choice is provisional. It becomes accepted only after the app passes performance checks with the largest useful product canvas and the heaviest useful values for its own controls. If those checks show frame gaps, long tasks, viewport shaking, slow export, or interaction jank, revise the renderer strategy from that evidence before delivery.
5
+ The initial renderer choice is provisional. It becomes accepted only after the app passes performance checks with the largest useful product canvas and the heaviest useful values for its own controls. If those checks show frame gaps, long tasks, viewport shaking, slow export, or interaction jank, revise the renderer strategy from that evidence before delivery. Do not make a renderer look fast by silently reducing the selected preview scale, backing pixels, source media quality, or export fidelity.
6
6
 
7
7
  ## Strategy Guide
8
8
 
@@ -9,7 +9,7 @@ Edit `src/app/app-schema.ts` as the public product surface.
9
9
  - Use `settingsTransfer: "auto"` for complex apps that should let users import/export control settings.
10
10
  - Bind every control to a schema `target`.
11
11
  - Use `defaultValue` for reset behavior.
12
- - Use `description` for product-specific help beside a visible label. Keep `label` short. Omit `description` instead of writing label recaps like `Adjusts Opacity`; compound controls such as `fontPicker` must not use `description` to list their own fields.
12
+ - Use `description` for product-specific help beside a visible label. Keep `label` short. Omit `description` instead of writing label recaps like `Adjusts Opacity`; also omit it for obvious color clusters such as `Color 1` / `Color 2` inside a color section. Compound controls such as `fontPicker` must not use `description` to list their own fields.
13
13
  - Use `disabled: true` only when the control is intentionally unavailable; the runtime renders the disabled visual and interaction state.
14
14
  - Use `visibleWhen` when a control or section exists only for a specific template, type, mode, variant, or count. Hidden values are preserved. A section with no visible controls is hidden automatically.
15
15
  - Use `disabledWhen` when a control belongs to the current entity but is temporarily unavailable in the selected state. The value is preserved while disabled.
@@ -134,10 +134,14 @@ Choose sizing from product context:
134
134
 
135
135
  For product output, export, copy, download, shader rendering, procedural rendering, or no single intrinsic source image, use `editable-output` unless the product explicitly needs `fixed-output`.
136
136
 
137
- A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
137
+ A prompt-provided base/default size is only the initial `canvas.size`. It must not remove the runtime Aspect ratio, Canvas width, and Canvas height controls. Aspect presets use canonical output sizes (`16:9` is `1920x1080`; the other presets are derived around a 1080px short edge or matching portrait long edge). When no explicit product size is provided, runtime defaults to `16:9` / `1920x1080`; choose another preset only when the product meaning calls for it. Use `fixed-output` only when the reference or product explicitly locks dimensions, and add runtime acceptance with `canvasSizingCoverage: "fixed-output-size"`.
138
138
 
139
139
  Resolved `canvas.size` exists for every canvas app, but visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are mandatory only for `editable-output` sizing. They do not depend on `settingsTransfer`: when settings transfer is off, the runtime prepends a technical `Setup` canvas size section without a visible heading; when settings transfer is on, the controls merge into the first technical `Setup` runtime settings section without a visible heading. Do not hand-build a duplicate size selector.
140
140
 
141
+ When the user manually edits `Canvas width` or `Canvas height`, the runtime treats that as an exact custom output size. It 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. Only selecting an aspect preset may resize both dimensions from a canonical preset.
142
+
143
+ For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` after canvas sizing in the first technical `Setup` section. The slider ranges from `1x` to `2x`, defaults to `2x`, and changes the renderer backing pixels without changing the visible CSS size or product output dimensions. Adding or enabling this slider requires targeted browser evidence that the canvas stays responsive while dragging sliders or other high-frequency controls at the selected scale. Full `pnpm verify:perf` is required only for the first working app version or explicit performance complaints. Performance fixes must preserve the selected scale. Diagnose whether lag comes from renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, stale async renders, or animation scheduling before reducing quality. Do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp `canvas.renderScale` below the user's chosen value. Do not enable it for DOM/SVG/vector-native previews; use native vector rendering for those.
144
+
141
145
  ## Panels
142
146
 
143
147
  - Use `panels: {}` for the neutral starter or for products that have no user-facing panels yet.
@@ -157,11 +161,12 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
157
161
  | `anchorGrid` | Anchor picker | `defaultValue`, `target` |
158
162
  | `channelMixer` | RGB-only channel matrix mixer with R/G/B tabs and Red/Green/Blue source sliders | `defaultValue`, `target`, `label` |
159
163
  | `checkbox` | Checkbox field | `defaultValue`, `target`, `label` |
164
+ | `collectionActions` | Add/remove buttons for repeatable product entities and their runtime-backed item controls | `defaultValue: []`, `itemControl`, `itemDefaultValue`, `itemLabel`, `minItems`, `recommendedMaxItems`, `hardMaxItems`, `target` |
160
165
  | `code` | Multiline textarea | `defaultValue`, `target`, `label` |
161
166
  | `color` | Hex color picker | `defaultValue: { hex }`, `target`, `label` |
162
167
  | `colorOpacity` | Hex color picker plus opacity percent input | `defaultValue: { hex, opacity }`, `target`, `label` |
163
168
  | `curves` | RGB or single curve editor | `defaultValue`, `target`, `variant: "single"`, `interpolation: "smooth" \| "monotone"` |
164
- | `fileDrop` | Upload/drop input | `accept`, `target` |
169
+ | `fileDrop` | Upload/drop input | `accept`, `multiple`, `target` |
165
170
  | `fontPicker` | Font preview select with popup, category search, weight, size, text case, text color/opacity, letter spacing, and line height; product text must consume `fontId`, `fontWeight`, `fontSize`, `letterSpacing`, `lineHeight`, `textCase`, `color`, and `opacity` | `defaultValue: { fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, opacity }`, `target` |
166
171
  | `gradient` | Gradient editor | `defaultValue: { angle, gradientType, stops }`, `target` |
167
172
  | `imagePicker` | Image choice grid | `items`, `defaultValue`, `target` |
@@ -178,6 +183,10 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
178
183
 
179
184
  `text` defaults to `commitMode: "content"` and applies while typing for real content such as prompts, names, titles, tokens, and short text. Use `commitMode: "setting"` for text inputs that edit settings such as font size, numeric-like style values, dimensions, ids, or configuration fields; setting text commits on blur or Enter. Canvas width and Canvas height always commit on blur or Enter. `code` / `CodeTextarea` is a content editor, applies while typing, and is capped at 12 visible lines. Long content scrolls inside the textarea instead of making the controls panel taller.
180
185
 
186
+ For `slider` and `rangeSlider`, `unit` is a measurement or scale suffix, not the entity being counted. Use units such as `%`, `px`, `°`, `x`, `s`, `ms`, `fps`, `rows`, or `cols` only when they clarify the number. Do not add repeated nouns such as `letters`, `shapes`, `words`, `symbols`, `items`, `particles`, or `layers` when the label or section already names that entity. If the value needs a noun, rename the label or section. Word or acronym units, when truly needed, render with a space (`5 cols`, `17 fps`); compact symbol/CSS units stay tight (`70%`, `24px`).
187
+
188
+ Use `collectionActions` when the product owns a growable/shrinkable item list. `minItems` protects the smallest valid output, `recommendedMaxItems` is only a design recommendation, and `hardMaxItems` is valid only for a real product or technical limit. Adding/removing items must update the runtime array and the renderer/export must consume that same array. Do not pair a count slider with hidden fixed item controls when the user needs to add or remove actual entities. The collection label is on the left and remove/add buttons stay on the right. Homogeneous repeated items do not show visible per-item labels when the collection label already names the group. `itemControl.type` supports normal item built-ins such as `color`, `colorOpacity`, `text`, `select`, `segmented`, `slider`, `switch`, `checkbox`, and `rangeInput`; item controls still follow normal density rules, so plain colors use equal 50% columns when they fit.
189
+
181
190
  ## Control Selection Inventory
182
191
 
183
192
  Before writing schema controls, map product needs to built-ins by value model, not visual similarity.
@@ -229,6 +238,8 @@ Section expand/collapse uses the standard runtime height/opacity animation. Do n
229
238
 
230
239
  Ordinary section collapsed/expanded state persists as a per-app runtime UI preference. 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.
231
240
 
241
+ Ordinary 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`.
242
+
232
243
  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.
233
244
 
234
245
  Large built-in compound controls inside mixed sections render content-width internal dividers with 18px between each rendered divider and the control content. If a compound control is the first item in that section, render only its bottom internal divider and remove the top internal padding. If a section contains exactly one control, whether simple or compound, only the parent section dividers render. Single `curves` are not compound for dividers; RGB `curves` are compound.
@@ -296,7 +307,7 @@ When enabled, the runtime inserts a technical `Setup` settings-transfer section
296
307
 
297
308
  After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. If the threshold is reached, use `"auto"` / `true` or add an explicit `runtime.settingsTransfer` opt-out acceptance row with product evidence.
298
309
 
299
- When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, and `Canvas height` in that order. Do not split these into separate sections or recreate them manually.
310
+ When settings transfer is enabled and the canvas uses `editable-output` sizing, the first technical `Setup` runtime section is mandatory and contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and optional `Resolution scale` in that order. Do not split these into separate sections or recreate them manually.
300
311
 
301
312
  A settings-transfer section with only `Export Settings` and `Import Settings` means the canvas is not `editable-output` or the app already declares its own `canvas.size.width` / `canvas.size.height` controls. For product-output apps, treat that as a schema decision to review.
302
313
 
@@ -73,15 +73,12 @@ Choose the tier from `AGENTS.md` before editing. Use the tier to decide checks.
73
73
 
74
74
  - Tier 0-1: targeted docs/typecheck/unit plus focused browser when visual.
75
75
  - Tier 2: `pnpm verify:quick` plus relevant browser acceptance.
76
- - Tier 3: `pnpm verify:quick`, targeted browser acceptance, and relevant `pnpm verify:perf` scenarios.
77
- - Tier 4: `pnpm verify:final`, then start `pnpm dev` for the local URL.
76
+ - Tier 3: `pnpm verify:quick`, targeted browser acceptance, and targeted performance scenarios only for touched workload/viewport/export paths.
77
+ - Tier 4: `pnpm verify:final`; add `pnpm verify:perf` only for the first working app version or explicit performance complaints, then start `pnpm dev` for the local URL.
78
78
 
79
- Run a full performance checkpoint with `pnpm verify:perf` when:
79
+ Run a full performance checkpoint with `pnpm verify:perf` only when:
80
80
 
81
81
  - the first working version of the app exists;
82
- - renderer, canvas, animation, export, timeline, or layers change;
83
- - a bug that previously broke functionality is fixed;
84
- - a performance optimization lands;
85
- - the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom.
82
+ - the user explicitly asks to optimize performance, fix lag, remove jank, speed up animation, stabilize drag/zoom, or otherwise complains about performance.
86
83
 
87
- Fast feature loops may defer full performance only when none of these checkpoint triggers apply. Record the deferred check and reason in the worklog.
84
+ Fast feature loops after the first working version do not run the full performance suite by default. Renderer, canvas, animation, export, timeline, layers, `canvas.renderScale`, bug fixes, and performance-sensitive controls still need targeted functional/browser checks first, plus targeted performance scenarios only when they directly exercise the touched path. Record any skipped full performance run and reason in the worklog.
@@ -0,0 +1,36 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # Build output
5
+ dist
6
+ dist-ssr
7
+ .vite
8
+ .turbo
9
+ .output
10
+ .next
11
+ *.tsbuildinfo
12
+
13
+ # Test output
14
+ coverage
15
+ test-results
16
+ playwright-report
17
+
18
+ # Local environment
19
+ .env
20
+ .env.*
21
+ !.env.example
22
+
23
+ # Logs
24
+ logs
25
+ *.log
26
+ npm-debug.log*
27
+ pnpm-debug.log*
28
+ yarn-debug.log*
29
+
30
+ # OS and editor files
31
+ .DS_Store
32
+ Thumbs.db
33
+ .idea
34
+ .vscode/*
35
+ !.vscode/extensions.json
36
+ !.vscode/settings.json
@@ -16,7 +16,7 @@
16
16
  "verify:quick": "pnpm ai:check && pnpm test",
17
17
  "verify:ui": "pnpm test:browser",
18
18
  "verify:perf": "pnpm test:browser:perf",
19
- "verify:final": "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser && pnpm test:browser:perf"
19
+ "verify:final": "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser"
20
20
  },
21
21
  "dependencies": {
22
22
  "@base-ui/react": "^1.4.1",
@@ -5997,6 +5997,61 @@ describe("Toolcraft template app acceptance coverage", () => {
5997
5997
  );
5998
5998
  });
5999
5999
 
6000
+ it("rejects redundant descriptions in obvious color sections", () => {
6001
+ const schemaWithObviousColorHelp = defineToolcraft({
6002
+ canvas: { enabled: true },
6003
+ panels: {
6004
+ controls: {
6005
+ sections: [
6006
+ {
6007
+ controls: {
6008
+ color1: {
6009
+ defaultValue: { hex: "#DFFF1A" },
6010
+ description: "Sets the first bead color.",
6011
+ label: "Color 1",
6012
+ target: "beads.color1",
6013
+ type: "color",
6014
+ },
6015
+ color2: {
6016
+ defaultValue: { hex: "#8CFF3A" },
6017
+ description: "Sets the second bead color.",
6018
+ label: "Color 2",
6019
+ target: "beads.color2",
6020
+ type: "color",
6021
+ },
6022
+ colorSpread: {
6023
+ defaultValue: 34,
6024
+ description:
6025
+ "Controls how often beads use colors 2-5 instead of Color 1.",
6026
+ label: "Spread",
6027
+ max: 100,
6028
+ min: 0,
6029
+ target: "beads.colorSpread",
6030
+ type: "slider",
6031
+ unit: "%",
6032
+ },
6033
+ },
6034
+ title: "Bead Colors",
6035
+ },
6036
+ ],
6037
+ title: "Controls",
6038
+ },
6039
+ },
6040
+ });
6041
+
6042
+ expect(validateToolcraftAcceptanceCoverage(schemaWithObviousColorHelp, [
6043
+ makeControlAcceptance("beads.color1", "color"),
6044
+ makeControlAcceptance("beads.color2", "color"),
6045
+ makeControlAcceptance("beads.colorSpread", "slider"),
6046
+ ])).toEqual(
6047
+ expect.arrayContaining([
6048
+ "Bead Colors / color1 description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.",
6049
+ "Bead Colors / color2 description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.",
6050
+ "Bead Colors / colorSpread description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.",
6051
+ ]),
6052
+ );
6053
+ });
6054
+
6000
6055
  it("requires mode selectors to appear before dependent controls", () => {
6001
6056
  const schemaWithLateModeSelector = {
6002
6057
  ...starterSchema,
@@ -1561,14 +1561,33 @@ function getToolcraftControlDescriptionError({
1561
1561
  control,
1562
1562
  controlId,
1563
1563
  sectionLabel,
1564
+ sectionTitle,
1564
1565
  }: {
1565
1566
  control: ToolcraftControlSchema;
1566
1567
  controlId: string;
1567
1568
  sectionLabel: string;
1569
+ sectionTitle: string | undefined;
1568
1570
  }): string | undefined {
1569
1571
  const description = control.description?.trim();
1570
1572
 
1571
- if (!description || control.type !== "fontPicker") {
1573
+ if (!description) {
1574
+ return undefined;
1575
+ }
1576
+
1577
+ const label = getControlLabelText(control).trim();
1578
+
1579
+ if (
1580
+ isToolcraftObviousColorSectionControlDescription({
1581
+ control,
1582
+ description,
1583
+ label,
1584
+ sectionTitle,
1585
+ })
1586
+ ) {
1587
+ return `${sectionLabel} / ${controlId} description adds a help icon to an obvious color-section control. Omit control.description when the section title and visible label already explain the setting.`;
1588
+ }
1589
+
1590
+ if (control.type !== "fontPicker") {
1572
1591
  return undefined;
1573
1592
  }
1574
1593
 
@@ -1583,6 +1602,52 @@ function getToolcraftControlDescriptionError({
1583
1602
  return `${sectionLabel} / ${controlId} description repeats FontPicker-owned fields (${repeatedParts.join(", ")}). FontPicker help must explain only non-obvious product behavior; use section titles and visible field labels for font family, weight, size, case, color, opacity, letter spacing, and line height, or omit description.`;
1584
1603
  }
1585
1604
 
1605
+ function isToolcraftColorSectionTitle(sectionTitle: string | undefined): boolean {
1606
+ return /\b(colou?rs?|palette|palettes)\b/i.test(sectionTitle ?? "");
1607
+ }
1608
+
1609
+ function isToolcraftSequentialColorLabel(label: string): boolean {
1610
+ return /^colou?r\s+\d+$/i.test(label.trim());
1611
+ }
1612
+
1613
+ function isToolcraftSimplePaletteDistributionLabel(label: string): boolean {
1614
+ return /^(spread|mix|distribution)$/i.test(label.trim());
1615
+ }
1616
+
1617
+ function isToolcraftGenericControlHelpDescription(description: string): boolean {
1618
+ return /^(adjusts?|changes?|chooses?|controls?|defines?|selects?|sets?|updates?)\b/i.test(
1619
+ description.trim(),
1620
+ );
1621
+ }
1622
+
1623
+ function isToolcraftObviousColorSectionControlDescription({
1624
+ control,
1625
+ description,
1626
+ label,
1627
+ sectionTitle,
1628
+ }: {
1629
+ control: ToolcraftControlSchema;
1630
+ description: string;
1631
+ label: string;
1632
+ sectionTitle: string | undefined;
1633
+ }): boolean {
1634
+ if (!isToolcraftColorSectionTitle(sectionTitle)) {
1635
+ return false;
1636
+ }
1637
+
1638
+ if (
1639
+ (control.type === "color" || control.type === "colorOpacity") &&
1640
+ isToolcraftSequentialColorLabel(label)
1641
+ ) {
1642
+ return true;
1643
+ }
1644
+
1645
+ return (
1646
+ isToolcraftSimplePaletteDistributionLabel(label) &&
1647
+ isToolcraftGenericControlHelpDescription(description)
1648
+ );
1649
+ }
1650
+
1586
1651
  function getToolcraftControlSectionGroupingErrors(
1587
1652
  schema: ResolvedToolcraftAppSchema,
1588
1653
  ): string[] {
@@ -1691,6 +1756,7 @@ function getToolcraftControlSectionGroupingErrors(
1691
1756
  control,
1692
1757
  controlId,
1693
1758
  sectionLabel,
1759
+ sectionTitle,
1694
1760
  });
1695
1761
 
1696
1762
  if (descriptionError) {
@@ -299,7 +299,7 @@ describe("Toolcraft template app performance coverage", () => {
299
299
  expect(packageJson.scripts?.["verify:ui"]).toBe("pnpm test:browser");
300
300
  expect(packageJson.scripts?.["verify:perf"]).toBe("pnpm test:browser:perf");
301
301
  expect(packageJson.scripts?.["verify:final"]).toBe(
302
- "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser && pnpm test:browser:perf",
302
+ "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
303
303
  );
304
304
  });
305
305
 
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { MinusIcon, PlusIcon } from "@phosphor-icons/react";
4
+ import type * as React from "react";
5
+
6
+ import { ControlFieldLabel } from "../../control-layout";
7
+ import { Button, Field } from "../../primitives";
8
+
9
+ export type CollectionActionsControlProps = {
10
+ addLabel?: string;
11
+ canAdd?: boolean;
12
+ canRemove?: boolean;
13
+ name: string;
14
+ onAdd?: () => void;
15
+ onRemove?: () => void;
16
+ removeLabel?: string;
17
+ };
18
+
19
+ export function CollectionActionsControl({
20
+ addLabel = "Add item",
21
+ canAdd = true,
22
+ canRemove = true,
23
+ name,
24
+ onAdd,
25
+ onRemove,
26
+ removeLabel = "Remove item",
27
+ }: CollectionActionsControlProps): React.JSX.Element {
28
+ return (
29
+ <Field
30
+ aria-label={name}
31
+ className="min-w-0 items-center justify-between gap-2"
32
+ data-slot="collection-actions-control-header"
33
+ orientation="horizontal"
34
+ >
35
+ <ControlFieldLabel className="flex-1">{name}</ControlFieldLabel>
36
+ <div className="inline-flex shrink-0 items-center gap-1">
37
+ <Button
38
+ aria-label={removeLabel}
39
+ disabled={!canRemove}
40
+ onClick={onRemove}
41
+ size="icon-sm"
42
+ type="button"
43
+ variant="outline"
44
+ >
45
+ <MinusIcon />
46
+ </Button>
47
+ <Button
48
+ aria-label={addLabel}
49
+ disabled={!canAdd}
50
+ onClick={onAdd}
51
+ size="icon-sm"
52
+ type="button"
53
+ variant="outline"
54
+ >
55
+ <PlusIcon />
56
+ </Button>
57
+ </div>
58
+ </Field>
59
+ );
60
+ }
@@ -0,0 +1,4 @@
1
+ "use client";
2
+
3
+ export { CollectionActionsControl } from "./collection-actions-control";
4
+ export type { CollectionActionsControlProps } from "./collection-actions-control";