@pixel-point/toolcraft 0.0.8 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +42 -5
  4. package/src/generate.test.mjs +40 -0
  5. package/src/package-json.mjs +15 -0
  6. package/src/package-json.test.mjs +14 -1
  7. package/templates/runtime/contracts/component-contracts.test.ts +251 -47
  8. package/templates/runtime/contracts/component-contracts.ts +130 -57
  9. package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +22 -9
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +916 -108
  17. package/templates/runtime/react/controls-panel.tsx +136 -30
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
  27. package/templates/runtime/schema/define-toolcraft.ts +140 -246
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +44 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +810 -21
  41. package/templates/runtime/testing/performance.ts +823 -53
  42. package/templates/starter/AGENTS.md +24 -18
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +75 -42
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +37 -11
  50. package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/gitignore +1 -0
  56. package/templates/starter/index.html +1 -0
  57. package/templates/starter/package.json +3 -0
  58. package/templates/starter/playwright.config.ts +1 -1
  59. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  60. package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
  61. package/templates/starter/scripts/toolcraft-port.mjs +280 -0
  62. package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
  63. package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
  64. package/templates/starter/src/app/starter-acceptance.ts +1453 -97
  65. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  66. package/templates/starter/src/app/starter-performance.ts +5 -0
  67. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  68. package/templates/starter/src/app/starter-schema.ts +6 -2
  69. package/templates/starter/vite.config.ts +58 -2
  70. package/templates/ui/components/control-layout/index.tsx +8 -3
  71. package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
  72. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  73. package/templates/ui/components/controls/color/index.ts +4 -1
  74. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  75. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  76. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  77. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  78. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  79. package/templates/ui/components/controls/index.ts +4 -0
  80. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  81. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  82. package/templates/ui/components/controls/select/select-control.tsx +8 -25
  83. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  84. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  85. package/templates/ui/components/controls/vector/index.ts +1 -0
  86. package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
  87. package/templates/ui/components/panel/panel-actions.tsx +1 -1
  88. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -6,13 +6,12 @@ Edit `src/app/app-schema.ts` as the public product surface.
6
6
 
7
7
  - Use `defineToolcraft`.
8
8
  - Configure `canvas`, `panels`, `toolbar`, and `panelActions` through the schema instead of composing those surfaces by hand.
9
- - Use `settingsTransfer: "auto"` for complex apps that should let users import/export control settings.
9
+ - New generated apps keep a controls panel so runtime `Setup` always provides `Export Settings` / `Import Settings` from the first run.
10
10
  - Bind every control to a schema `target`.
11
11
  - Use `defaultValue` for reset behavior.
12
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
- - Use `disabled: true` only when the control is intentionally unavailable; the runtime renders the disabled visual and interaction state.
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
- - Use `disabledWhen` when a control belongs to the current entity but is temporarily unavailable in the selected state. The value is preserved while disabled.
13
+ - Use `visibleWhen` when a control or section exists only for a specific template, type, source, include state, mode, variant, or count. Hidden values are preserved. A section with no visible controls is hidden automatically.
14
+ - Do not use `disabled: true` or `disabledWhen` for generated product controls. Product panels should show only controls usable in the current state. Runtime primitives may still have disabled styling internally, but app schemas should model product availability with `visibleWhen`.
16
15
  - Conditions support `equals`, `notEquals`, `oneOf`, `notOneOf`, `greaterThan`, `greaterThanOrEqual`, `lessThan`, and `lessThanOrEqual`.
17
16
  - Use `orderRole` to make control order testable.
18
17
  - Use `performanceRole` and `performanceReason` on every visible non-action control so performance coverage can be derived from the schema.
@@ -20,8 +19,10 @@ Edit `src/app/app-schema.ts` as the public product surface.
20
19
  - Use `panelActions` only for sticky footer product actions.
21
20
  - Do not use `panelActions` for settings import/export; `settingsTransfer` owns that body section.
22
21
  - Do not use `panelActions` for reset. The controls panel header owns reset, and footer actions with `label`, `value`, or `command` containing reset fail acceptance.
22
+ - Use `media.defaultAssets` for predefined file/image attachments; never hard-code those files inside `canvasContent` or the renderer.
23
23
  - Still-output product apps expose `Export PNG`.
24
24
  - Animated product apps expose `Export Video` and `Export PNG`.
25
+ - Export PNG and Export Video use `icon: "upload-simple"` to match the runtime `Export Settings` action.
25
26
  - `Copy PNG` can be secondary, but it never replaces export.
26
27
  - If an odd number of footer actions leaves one action alone in the final row, that final action spans the full row.
27
28
  - Use `ToolcraftApp onPanelAction` for product-specific actions.
@@ -45,7 +46,7 @@ export: {
45
46
  - `appearance.background` or `scene.background` as a `color` control;
46
47
  - `export.includeBackground` as a boolean/options control.
47
48
 
48
- PNG exporters should call `createToolcraftPngExportCanvas({ background, includeBackground, resolution, state, render })`, where `background`, `includeBackground`, and `resolution` come from runtime state. Live preview renderers should call `shouldIncludeToolcraftPreviewBackground(state)` and hide only the product-rendered background when it returns false; do not hide or replace the Toolcraft canvas shell/backing. For every app with `Export PNG`, `resolution` comes from `export.image.resolution`: `2k`, `4k`, and `8k` render actual 2048/4096/8192px long-edge PNGs. `current` or omitted resolution falls back to retina sizing. Video export always includes the product background, uses `getToolcraftRetinaExportSize`, and must prove exported metadata duration matches the runtime timeline duration.
49
+ PNG exporters should call `createToolcraftPngExportCanvas({ background, includeBackground, resolution, state, render })`, where `background`, `includeBackground`, and `resolution` come from runtime state. Live preview renderers should call `shouldIncludeToolcraftPreviewBackground(state)` and hide only the product-rendered background when it returns false; do not hide or replace the Toolcraft canvas shell/backing. For every app with `Export PNG`, `resolution` comes from `export.image.resolution`: `2k`, `4k`, and `8k` render actual 2048/4096/8192px long-edge PNGs. `current` or omitted resolution falls back to retina sizing. Video export always includes the product background, uses `getToolcraftVideoExportSize({ resolution: state.values["export.video.resolution"], state })`, and must prove exported metadata duration matches the runtime timeline duration.
49
50
 
50
51
  Every app with `Export PNG` exposes a separate `Image Export` controls section. For still-output apps it sits directly above sticky footer actions. For animated apps with both `Export PNG` and `Export Video`, it sits immediately before `Video Export`:
51
52
 
@@ -85,7 +86,7 @@ Every app with `Export PNG` exposes a separate `Image Export` controls section.
85
86
  }
86
87
  ```
87
88
 
88
- Animated apps with `Export Video` also expose a separate `Video Export` controls section. Do not mix video export settings into renderer/effect sections. Place this section after `Image Export` as the final authored controls section directly above sticky footer export buttons. `Format` and `Resolution` are a compact semantic pair, so use an inline two-column layout by default; stack them only when labels or selected values would clip.
89
+ Animated apps with `Export Video` must enable the top Toolcraft timeline and also expose a separate `Video Export` controls section. Do not mix video export settings into renderer/effect sections. Place this section after `Image Export` as the final authored controls section directly above sticky footer export buttons. `Format` and `Resolution` are a compact semantic pair, so use an inline two-column layout by default; stack them only when labels or selected values would clip.
89
90
 
90
91
  ```ts
91
92
  {
@@ -122,25 +123,35 @@ Animated apps with `Export Video` also expose a separate `Video Export` controls
122
123
  }
123
124
  ```
124
125
 
125
- Use `MediaRecorder.isTypeSupported(...)` or an explicit encoder/transcoder capability check before choosing the actual MIME/container. `MOV` and `ProRes` are not baseline browser outputs; use them only with a custom encoder/transcoder and dedicated acceptance plus performance coverage. `4K` is an export resolution target, not a reason to lock `canvas.size`. Offline rendered-frame video export must write timeline-based timestamps; `canvas.captureStream()` plus `MediaRecorder` records wall-clock time and cannot be the only duration mechanism for heavy renderers.
126
+ Use `MediaRecorder.isTypeSupported(...)` or an explicit encoder/transcoder capability check before choosing the actual MIME/container. `MOV` and `ProRes` are not baseline browser outputs; use them only with a custom encoder/transcoder and dedicated acceptance plus performance coverage. `4K` is an export resolution target, not a reason to lock `canvas.size` and not PNG-style 4096px long-edge sizing. Use `getToolcraftVideoExportSize`: `current` uses the current canvas/output size with even encoder-safe rounding, while `4k` fits inside an encoder-safe 3840x2160 box, preserves aspect ratio, and returns even pixel dimensions. Set recording canvas dimensions before `captureStream`, `MediaRecorder`, `VideoEncoder`, or equivalent setup, and reject recorder/encoder errors instead of returning corrupt blobs. Offline rendered-frame video export must write timeline-based timestamps; `canvas.captureStream()` plus `MediaRecorder` records wall-clock time and cannot be the only duration mechanism for heavy renderers.
126
127
 
127
128
  ## Canvas Sizing
128
129
 
129
130
  Choose sizing from product context:
130
131
 
131
- - `intrinsic-media`: a single uploaded or generated source defines `canvas.size`.
132
- - `editable-output`: exportable output where users should edit width and height.
133
- - `fixed-output`: product-defined output size that users must not edit.
132
+ - `intrinsic-media`: an explicit media-viewer/source-native product where a single uploaded or generated source defines `canvas.size`.
133
+ - `editable-output`: product/export output where users always see aspect ratio, width, and height.
134
+ - `fixed-output`: non-product/internal output size that users must not edit.
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
+ For product output, export, copy, download, shader rendering, procedural rendering, reference clones, or no single intrinsic source image, use `editable-output`. Upload without explicit sizing also resolves to `editable-output`.
136
137
 
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
+ An uploaded background/source image inside a product canvas is not source-native sizing. Use `editable-output`, keep the current `canvas.size`, keep `Setup` canvas controls visible, and render the image as cover/crop inside the current canvas bounds without letterbox or aspect distortion. Use `intrinsic-media` only when the product is truly a media viewer/source-native tool where imported media natural dimensions intentionally own `canvas.size`, and prove that with `canvasSizingCoverage: "intrinsic-media-size"` acceptance.
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
+ A prompt-provided, reference, fixed-format, or 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. Generated product/export apps do not use `fixed-output` to preserve a reference baseline; fixed dimensions stay visible as editable defaults.
141
+
142
+ Resolved `canvas.size` exists for every canvas app, but visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are mandatory through `editable-output` sizing for product/export apps. They live in the first visible headerless `Setup` controls block after `Export Settings` and `Import Settings`. Do not hand-build a duplicate size selector.
140
143
 
141
144
  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
145
 
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.
146
+ For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderScale: true`. Runtime appends `Resolution scale` after canvas sizing in `Setup`. The slider ranges from `1` to `2`, defaults to `2`, 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. A full browser performance checkpoint is required for the first working product version and explicit performance complaints; use the agent-controlled browser first and `pnpm verify:perf` only as fallback. 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.
147
+
148
+ When `panels.timeline` is enabled, runtime appends a `Timeline` switch as the last control in `Setup`, after `Resolution scale` when present. The switch controls runtime presentation only: off shows compact Play-only transport, on shows the extended timeline with scrubber, duration, loop, and keyframe UI. It does not pause playback, change keyframes, alter export, write product `values`, or reset with `Reset controls`. If `persistence.include` contains `"panels"`, the extended/compact state can restore as a UI preference.
149
+
150
+ ## Media Defaults
151
+
152
+ Use `media.defaultAssets` when the app starts with predefined files, source images, masks, symbol sets, or background images. Each item should set `sourceTarget` to the matching `fileDrop` control target. The runtime treats these as attached files: users see them in the uploader, can remove them to get an empty source/canvas state, and Reset restores them.
153
+
154
+ If removal, reorder, or transforms of predefined media should survive reload, add `"media"` to `persistence.include`; do not mirror the file list into product `values` and do not hard-code the file in `canvasContent`.
144
155
 
145
156
  ## Panels
146
157
 
@@ -148,7 +159,8 @@ For non-vector raster, Canvas 2D, WebGL, or WebGPU previews, set `canvas.renderS
148
159
  - Controls panel is the primary editing panel once the product has schema controls.
149
160
  - Layers are optional. Enable only for multiple editable objects, media objects, groups, visibility, selection, reorder, or selected-layer controls.
150
161
  - Do not use `selectedLayer.*` targets when layers are disabled.
151
- - Timeline is optional. Use no timeline, playback, keyframes, or custom reference timeline from product transport behavior.
162
+ - Timeline is optional only for autonomous decorative animation with no video export. Use playback, keyframes, or custom reference timeline from product transport behavior.
163
+ - Timeline compact/extended presentation is runtime panel UI state controlled by the auto-injected `Setup` switch, not a product control target.
152
164
  - Do not add right-panel Play, Pause, Animate, or Restart controls for app-wide transport. Use the top timeline.
153
165
 
154
166
  ## Built-In Control Types
@@ -157,7 +169,7 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
157
169
 
158
170
  | `type` | Renders | Key fields |
159
171
  | --- | --- | --- |
160
- | `actions` | Inline local action buttons for the current section or nearby entity | `actions`, `target`, `label` |
172
+ | `actions` | Local action buttons for the current section or nearby entity, rendered below the label in a two-column grid | `actions`, `target`, `label` |
161
173
  | `anchorGrid` | Anchor picker | `defaultValue`, `target` |
162
174
  | `channelMixer` | RGB-only channel matrix mixer with R/G/B tabs and Red/Green/Blue source sliders | `defaultValue`, `target`, `label` |
163
175
  | `checkbox` | Checkbox field | `defaultValue`, `target`, `label` |
@@ -166,7 +178,7 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
166
178
  | `color` | Hex color picker | `defaultValue: { hex }`, `target`, `label` |
167
179
  | `colorOpacity` | Hex color picker plus opacity percent input | `defaultValue: { hex, opacity }`, `target`, `label` |
168
180
  | `curves` | RGB or single curve editor | `defaultValue`, `target`, `variant: "single"`, `interpolation: "smooth" \| "monotone"` |
169
- | `fileDrop` | Upload/drop input; `assetKind: "image"` owns image previews and `assetKind: "file"` owns sortable arbitrary file lists | `assetKind`, `accept`, `multiple`, `defaultValue`, `target` |
181
+ | `fileDrop` | Upload/drop input; `assetKind: "image"` owns image previews, rotate/flip actions, selection for multi-image transforms, and cover/crop canvas source behavior; `assetKind: "file"` owns sortable arbitrary file lists | `assetKind`, `accept`, `multiple`, `defaultValue`, `target` |
170
182
  | `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`; default text color is `#FFFFFF` at `100` opacity | `defaultValue: { fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, opacity }`, `target` |
171
183
  | `gradient` | Gradient editor | `defaultValue: { angle, gradientType, stops }`, `target` |
172
184
  | `imagePicker` | Image choice grid | `items`, `defaultValue`, `target` |
@@ -179,14 +191,22 @@ Use built-ins before custom controls. Unknown `type` values render nothing unles
179
191
  | `slider` | Single-value slider | `defaultValue`, `min`, `max`, `step`, `unit`, `variant` |
180
192
  | `switch` | Binary switch | `defaultValue`, `target`, `label` |
181
193
  | `text` | Single-line input | `defaultValue`, `target`, `label`, `commitMode` |
182
- | `vector` | X/Y vector pad and fields | `defaultValue: { x, y }`, `xLabel`, `yLabel`, `variant` |
194
+ | `vector` | X/Y vector pad and fields | `defaultValue: { x, y }`, `xLabel`, `yLabel`, `variant`, `coordinateMode` |
183
195
 
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.
196
+ For `select`, standalone controls render stacked and full-width with the label above the dropdown. Do not use the old compact side-label row with label left and dropdown right. Use compact two-column inline layout only for related short select pairs that tune one workflow or entity, such as export `Format` and `Resolution`.
185
197
 
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`).
198
+ `text` defaults to `commitMode: "content"` and applies while typing for short real content such as button labels, canvas labels, names, titles, captions, tokens, compact prompts, and other one-line 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 for long, multiline, or structured values, applies while typing, and is capped at 12 visible lines. Long content scrolls inside the textarea instead of making the controls panel taller. Do not use `code` for short single-line button/canvas text unless `description` explicitly proves the field is intended for long or structured input.
199
+
200
+ For `slider` and `rangeSlider`, `unit` is a real measurement suffix, not the entity being counted and not a generic multiplier. Use units such as `%`, `px`, `°`, `s`, `ms`, `fps`, `rows`, or `cols` only when they clarify the number. Do not use `unit: "x"`; scale, multiplier, intensity, opacity, strength, depth, and shader amount values display plain numbers unless a real measurement unit applies. 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`).
201
+
202
+ `slider` and `rangeSlider` are live controls. Dragging must update runtime state and product output while the drag is in progress, not only on pointer release, blur, Apply, or a final commit. Treat a non-live slider as a broken product mapping unless an extreme measured performance ceiling is documented; even then, keep immediate lightweight canvas feedback and refine the heavy output after coalescing/caching.
203
+
204
+ For `vector`, the default/spatial variant uses `coordinateMode: "screen"` by default: dragging left/up lowers `x` and `y`, so canvas objects move left/up without renderer-side Y inversion. Use `coordinateMode: "cartesian"` only for intentional mathematical Y-up coordinates. Color variants keep their color-axis semantics by default. Vector pad value labels show compact rounded coordinates; raw floating-point tails must never appear in the controls panel. Double-clicking the pad resets both axes to the control default through the normal runtime value update, matching the section header reset; if no default is defined, the fallback is `0,0`. Holding Shift while dragging locks movement to the dominant axis and must not select text or page content.
187
205
 
188
206
  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`, `rangeInput`, and `fontPicker`; item controls still follow normal density rules, so plain colors use equal 50% columns when they fit. Use `fontPicker` as the item control when each repeated item is a text style or typography entity; do not split its font, weight, size, case, color/opacity, letter spacing, or line height into sibling collection fields.
189
207
 
208
+ Segmented controls are full-width compact choices. Do not place `segmented` in inline half-width rows beside Switch, Color, Select, or another control; use `select` when a finite choice must occupy a half-width column.
209
+
190
210
  ## Control Selection Inventory
191
211
 
192
212
  Before writing schema controls, map product needs to built-ins by value model, not visual similarity.
@@ -207,42 +227,152 @@ If a built-in exact owner matches the value model, use that built-in. If several
207
227
 
208
228
  For `curves`, choose the variant explicitly. Use `variant: "single"` for acceleration, bend, easing, response, depth, mask, opacity, threshold, or remap curves. Omit it only for RGB/color-correction or channel-specific curves that intentionally need RGB/R/G/B tabs.
209
229
 
230
+ ## Video Reference Study
231
+
232
+ When a video, GIF, screen recording, contact sheet, or extracted-frame sequence is supplied as a reference, declare `starterTransferMode.videoReferenceStudy` before implementation. This is independent of whether the app is a new Toolcraft app or a reference-runtime clone.
233
+
234
+ ```ts
235
+ export const starterTransferMode = {
236
+ mode: "new-toolcraft-app",
237
+ videoReferenceStudy: {
238
+ acceptanceMapping: [
239
+ {
240
+ acceptanceId: "reference.video.motion",
241
+ behavior: "Body motion preserves planted contact points before retargeting.",
242
+ frameIds: ["f000", "f012", "f024"],
243
+ },
244
+ ],
245
+ behaviorDecomposition:
246
+ "The reference decomposes into moving body state, persistent anchors, delayed release, and retargeting behavior.",
247
+ extractionEvidence:
248
+ "Extracted frames with ffmpeg and reviewed a contact sheet before implementation.",
249
+ referenceLocation: "/path/to/reference.mp4",
250
+ storyboard: [
251
+ {
252
+ behaviorObservation: "Several endpoints remain planted while the body moves.",
253
+ frameId: "f000",
254
+ frameSource: "frames/frame_000.png",
255
+ timeSeconds: 0,
256
+ visualObservation: "The body is left of center with legs spread outward.",
257
+ },
258
+ ],
259
+ transitionAnalysis: [
260
+ {
261
+ behaviorDelta:
262
+ "Between f000 and f012, body position changes while endpoints stay near their previous canvas positions.",
263
+ fromFrameId: "f000",
264
+ id: "f000-f012",
265
+ toFrameId: "f012",
266
+ },
267
+ ],
268
+ },
269
+ } satisfies ToolcraftTransferMode;
270
+ ```
271
+
272
+ The real study must include at least four storyboard frames, at least three frame-to-frame transition rows, behavior decomposition, and acceptance mapping to automated browser-backed tests. Do not implement from a single screenshot or static summary when the reference is temporal.
273
+
274
+ ## Reference Transfer Mode
275
+
276
+ Reference clones must declare `referenceFeatureInventory` beside `behaviorCoverage`. The inventory is the source checklist for the port and is validated against acceptance rows.
277
+
278
+ ```ts
279
+ export const starterTransferMode = {
280
+ behaviorCoverage: ["canvas-sizing", "control-mapping", "renderer-state"],
281
+ mode: "reference-runtime-clone",
282
+ referenceFeatureInventory: [
283
+ {
284
+ acceptanceId: "reference.rendererState",
285
+ behaviorEvidence:
286
+ "Observed the reference renderer keep particle state across multiple frames.",
287
+ featureName: "Renderer state",
288
+ id: "renderer-state",
289
+ referenceBehavior:
290
+ "The reference renderer keeps mutable particle state across frames.",
291
+ sourceEvidence: "Inspected reference/src/renderer.ts frame loop.",
292
+ status: "ported",
293
+ toolcraftMapping:
294
+ "The Toolcraft renderer keeps equivalent state and invalidation keys.",
295
+ },
296
+ ],
297
+ referenceName: "Original app",
298
+ referenceStudy: {
299
+ behaviorEvidence:
300
+ "Ran the original app in a local browser and verified controls, renderer state, export, and media behavior.",
301
+ referenceLocation: "/path/to/original-app",
302
+ reproductionSteps:
303
+ "Installed dependencies, started the reference app, opened it in the browser, and compared behavior against the Toolcraft port.",
304
+ sourceEvidence:
305
+ "Inspected routes, renderer, control state, timeline/export handlers, and media lifecycle files.",
306
+ status: "ran-original",
307
+ },
308
+ referenceTimeline: { behaviorCoverage: [], mode: "none" },
309
+ sourceOfTruth: "reference-runtime",
310
+ } satisfies ToolcraftTransferMode;
311
+ ```
312
+
313
+ If the original cannot run as-is but behavior can be reconstructed, use `status: "restored-local"` and describe the restoration steps. Use `status: "source-inspection-only"` only with `sourceOnlyReason` explaining the concrete blocker that made running or restoring unavailable.
314
+
315
+ Use `status: "ported"` when the behavior is carried over directly and `status: "toolcraft-native"` when Toolcraft owns the same behavior, such as canvas sizing or export shell. Use `status: "intentionally-changed"` only with `userApprovedChangeReason` that cites explicit user approval or redesign/change-request evidence.
316
+
210
317
  ## Animation Intent
211
318
 
212
319
  Before adding animation controls, decide the animation owner:
213
320
 
214
321
  - `timeline-playback`: product time controlled by the top timeline.
215
322
  - `timeline-keyframes`: property animation controlled by keyframe diamonds and rows.
216
- - `autonomous`: decorative/self-running output with no user-facing transport.
323
+ - `autonomous`: decorative/self-running output with no user-facing transport and no video export.
217
324
 
218
325
  In keyframes mode, renderer code reads evaluated values from the runtime keyframe evaluator. Do not parse timeline labels and do not use raw `state.values` for targets with keyframes.
219
326
 
220
- If the user asks for product animation and does not explicitly say it is decorative autoplay, use `panels.timeline: { mode: "playback" }`. If no timeline is used while animation controls remain visible, `starterTransferMode.animationIntent` must declare `mode: "autonomous"`, include a concrete reason, and include behavior coverage for no transport, no play/pause, no scrub, no duration control, no loop control, and no export-at-time.
327
+ If the product output is animated, use the top Toolcraft timeline. Use `panels.timeline: { mode: "playback", defaultDurationSeconds }` for playback animation and set `defaultDurationSeconds` to the product loop duration when it is known. When `panels.timeline` is enabled for a new Toolcraft app, `starterTransferMode.animationIntent` must match it: `mode: "timeline-playback"` for playback, or `mode: "timeline-keyframes"` for keyframes. `starterTransferMode.animationIntent` must declare `loopDuration: { source, seconds, evidence }` for playback/keyframe animation; valid sources are `reference`, `user-request`, and `product-derived`, never runtime/template fallback 8s. Reference clones that choose `referenceTimeline.mode: "toolcraft-playback"` or `"toolcraft-keyframes"` declare the same shape on `starterTransferMode.referenceTimeline.loopDuration`. `defaultDurationSeconds` must equal the declared `loopDuration.seconds`. Any app with `Export Video` must enable the top Toolcraft timeline and use runtime timeline time for preview/export duration and seamless forward-loop behavior. Product loops must advance in one direction and stitch first/last frames at any timeline duration; mirror, yoyo, ping-pong, or reverse loops require explicit user request. Playback renderers should use `getToolcraftTimelineLoopTime` or `getToolcraftTimelineLoopProgress` so `state.timeline.durationSeconds` remains the active loop period after the user edits duration. If no timeline is used while animation controls remain visible, `starterTransferMode.animationIntent` must declare `mode: "autonomous"`, include a concrete reason, include behavior coverage for no transport, no play/pause, no scrub, no duration control, no loop control, and no export-at-time, and prove there is no product animation and no video export.
221
328
 
222
329
  ## Control Section Inventory
223
330
 
224
- Before editing `panels.controls.sections`, write a short inventory in the spec or plan:
331
+ Before editing `panels.controls.sections`, define and export `starterControlSectionInventory` beside `starterAcceptance` in `src/app/starter-acceptance.ts`. This is the machine-checkable version of the section plan.
225
332
 
226
333
  - section title;
227
334
  - product entity or workflow stage;
228
335
  - included schema targets;
229
336
  - reason these controls belong together or reason for a real workflow split.
230
337
 
338
+ ```ts
339
+ export const starterControlSectionInventory = [
340
+ {
341
+ entity: "Text block",
342
+ groupingReason:
343
+ "These controls edit the text content, typography, and visible text fill together.",
344
+ targets: ["text.content", "text.font"],
345
+ title: "Text",
346
+ },
347
+ {
348
+ entity: "Object shape",
349
+ groupingReason: "Structure controls tune the physical footprint of the object.",
350
+ splitReason:
351
+ "Structure and density are separate workflow stages in this editor.",
352
+ targets: ["object.shape.size"],
353
+ title: "Shape Structure",
354
+ workflowStage: "structure",
355
+ },
356
+ ] as const;
357
+ ```
358
+
231
359
  Group controls by product meaning, not by component type. Do not create sections named `Controls`, `Settings`, `Options`, `Sliders`, `Inputs`, `Buttons`, `Color`, or `Colors`.
232
360
 
233
- Every app-authored controls-panel body section must have a short meaningful visible title. Runtime-created setup/settings sections use the technical title `Setup` but render without a visible heading; sticky footer action sections use the technical title `Export` but render without a visible heading.
361
+ The inventory must match the rendered schema: every product control target in every product section appears exactly once, and every inventory target renders in the section named by `title`. Runtime `Setup` controls, sticky footer `Export` actions, `settingsTransfer`, and runtime canvas sizing controls do not need inventory entries. If one target entity is split across sections, every split section must declare `workflowStage` and a concrete `splitReason`; otherwise the validator treats the split as accidental section drift.
362
+
363
+ Every app-authored controls-panel body section must have a short meaningful visible title. Runtime-created `Setup` renders as the first visible headerless controls block with no title, reset action, collapse button, or collapsed state; sticky footer action sections use the technical title `Export` but render without a visible heading.
234
364
 
235
- Every visible section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon. Do not hand-build section headers in generated apps.
365
+ Every visible app-authored section title renders through the standard 36px collapsible header row with vertically centered text and the runtime collapse icon. Do not hand-build section headers in generated apps.
236
366
 
237
367
  Section expand/collapse uses the standard runtime height/opacity animation. Do not replace it with instant custom section visibility.
238
368
 
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.
369
+ 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 `Setup` is not collapsible; sticky footer `Export` sections are not collapsible.
240
370
 
241
371
  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
372
 
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.
373
+ Runtime `Setup` and ordinary controls-panel body sections use 8px top spacing and 24px bottom spacing for their control content. Sticky footer action sections keep their dedicated spacing.
244
374
 
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.
375
+ 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 it is the last item in that section, render only its top internal divider and remove the bottom 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.
246
376
 
247
377
  Controls for the same product entity stay in the same section. For example, `squares.right.connections`, `squares.right.hoverRadius`, and `squares.right.color` belong in `Square 1 (Right)` with `Color` as the field label. A standalone color section is only valid when the color is the whole product entity, such as `Background`, `Accent`, `Connector`, or `Brand`.
248
378
 
@@ -254,7 +384,7 @@ If a target prefix has to be split across sections, the spec must name the workf
254
384
 
255
385
  Switch and checkbox labels name the setting context only. Do not prefix them with `Enable` or `Disable`; use `CRT`, `Glow`, `Loop`, or `Guides` instead. If the nearest section title already names the context, do not duplicate it as the visible toggle label. Use a short contextual label such as `Include` or, only for icon-only visual toggles, `label: false` with the product meaning in `target` and `description`.
256
386
 
257
- Inline two-column groups are preferred when controls tune one close product meaning and labels/values fit. Short numeric text pairs can be inline. Related short `select` pairs can be inline, especially workflow pairs such as `Format` + `Resolution`, `Codec` + `Profile`, or `Width unit` + `Height unit`. Use stacked one-control rows only as a fit fallback when a label, selected value, or option text would clip, truncate, or lose padding; record that fallback reason in the spec or worklog. A short numeric/text field may also pair with one related plain `color` field when both configure the same entity, such as `Mask size` and `Color` inside `Mask`. `colorOpacity` never renders in inline two-column groups; if either color control has opacity, keep the controls stacked. Color labels are semantic, not automatic: decide once for the whole color group, omit per-item labels such as `Color 1` for palette variation banks like `Accent Shades` or `Bead Colors`, and do not mix labeled and unlabeled items inside that bank. Sibling controls like `Spread` or `Randomness` do not force item labels; keep visible labels only when colors edit distinct roles such as `Fill`, `Stroke`, `Background`, `Connector`, or `Object`. Related plain color banks render two per row, and an odd trailing plain color remains half-width instead of stretching to a full row. Mixed inline rows require visible labels on every field except the required Background row and palette variation color banks whose group/section label already names the bank. Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when both visible labels fit without truncation; the runtime auto-pairs safe adjacent toggles by target entity, and schemas should stack them only when either label is too long. A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and both controls edit the same entity; shorten the toggle label when the section title already supplies context. Toggle plus parameter rows are equal-width two-column rows: each control occupies one half, never intrinsic toggle width plus remaining space. The required Background row uses `Include` plus unlabeled background color. Schema `slider` and `rangeSlider` controls always stay stacked at full width; the only built-in exception is the paired letter-spacing and line-height footer sliders inside `fontPicker`. Do not place sibling controls for case, color, opacity, size, weight, letter spacing, or line height when the same text entity already uses `fontPicker`.
387
+ Inline two-column groups are preferred when controls tune one close product meaning and labels/values fit. Short numeric text pairs can be inline. Related short `select` pairs can be inline, especially workflow pairs such as `Format` + `Resolution`, `Codec` + `Profile`, or `Width unit` + `Height unit`. Use stacked one-control rows only as a fit fallback when a label, selected value, or option text would clip, truncate, or lose padding; record that fallback reason in the spec or worklog. A short numeric/text field may also pair with one related plain `color` field when both configure the same entity, such as `Mask size` and `Color` inside `Mask`. `colorOpacity` never renders in inline two-column groups; if either color control has opacity, keep the controls stacked. Color labels are semantic, not automatic: decide once for the whole color group, omit per-item labels such as `Color 1` for palette variation banks like `Accent Shades` or `Bead Colors`, and do not mix labeled and unlabeled items inside that bank. Sibling controls like `Spread` or `Randomness` do not force item labels; keep visible labels only when colors edit distinct roles such as `Fill`, `Stroke`, `Background`, `Connector`, or `Object`. Related plain color banks render two per row, and an odd trailing plain color remains half-width instead of stretching to a full row. Mixed inline rows usually require visible labels on every field, except toggle-plus-parameter rows, the required Background row, and palette variation color banks whose group/section label already names the bank. All 50/50 inline rows use the same horizontal column gap as paired `select` controls; do not create a wider or narrower gap for toggle-plus-parameter rows. Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when both visible labels fit without truncation; the runtime auto-pairs safe adjacent toggles by target entity, and schemas should stack them only when either label is too long. A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and both controls edit the same entity; the non-toggle parameter uses `label: false`, and if that label is needed, stack the controls instead. Toggle plus parameter rows are equal-width two-column rows: each control occupies one half, never intrinsic toggle width plus remaining space. The required Background row uses `Include` plus unlabeled background color. Schema `slider` and `rangeSlider` controls always stay stacked at full width; the only built-in exception is the paired letter-spacing and line-height footer sliders inside `fontPicker`. Do not place sibling controls for case, color, opacity, size, weight, letter spacing, or line height when the same text entity already uses `fontPicker`.
258
388
 
259
389
  `rangeSlider` is always a full-width two-thumb control. Do not include it in `layoutGroups`. Its `defaultValue` must start with different lower and upper values, such as `[20, 80]`, so the two handles do not collapse into one apparent slider. Manual range labels accept built-in separators such as slash, hyphen, spaces, and dashes.
260
390
 
@@ -272,7 +402,7 @@ Order controls by decision flow inside each section:
272
402
 
273
403
  A selector that changes how later controls are interpreted must use `orderRole: "mode"` and sit above dependent parameters.
274
404
 
275
- Use `visibleWhen` for mode-, type-, variant-, or count-exclusive controls and sections. Example: `Partner` is visible when `coBrand.identityMode` is `text`; `Partner logo` is visible when it is `logo`. Count-controlled banks use numeric conditions: `Shade 4` is visible when `shapes.shadeCount` is `greaterThanOrEqual: 4`. When every control in a section is hidden by `visibleWhen`, the whole section is hidden automatically. Use `disabledWhen` only when the control remains part of the current entity but temporarily has no effect.
405
+ Use `visibleWhen` for mode-, type-, source-, include-, variant-, or count-exclusive controls and sections. Example: `Partner` is visible when `coBrand.identityMode` is `text`; `Partner logo` is visible when it is `logo`. Count-controlled banks use numeric conditions: `Shade 4` is visible when `shapes.shadeCount` is `greaterThanOrEqual: 4`. When every control in a section is hidden by `visibleWhen`, the whole section is hidden automatically. If a switch/select/segmented/imagePicker/checkbox chooses a branch for the same product entity, controls outside the current branch use `visibleWhen`, not `disabledWhen`.
276
406
 
277
407
  If a selector says “use the first N”, “number of colors”, “number of stops”, “active slots”, or similar, dependent sibling controls must be hidden with `visibleWhen` when they are outside the current count. Do not leave all possible controls visible while making the renderer ignore the inactive ones.
278
408
 
@@ -282,15 +412,15 @@ App schema tests must assert visible control order with `getToolcraftControlOrde
282
412
 
283
413
  State persistence is a product policy, not a hidden side effect.
284
414
 
285
- Use `persistence: { storage: "localStorage", key, version, include }` when user-edited app settings should survive reload. Typical product editors persist `values`, `canvas`, and `panels`. If localStorage persistence is enabled and any runtime panel is visible, `include` must contain `"panels"` so dragged panel positions survive reload in that specific app. Add `timeline` when playback position, duration, loop, expansion, or keyframes should survive reload. Add `layers` only when the app has a real layer model.
415
+ Use `persistence: { storage: "localStorage", key, version, include }` when user-edited app settings should survive reload. Typical product editors persist `values`, `canvas`, and `panels`. If localStorage persistence is enabled and any runtime panel is visible, `include` must contain `"panels"` so dragged panel positions survive reload in that specific app. Add `timeline` when playback position, duration, loop, expansion, or keyframes should survive reload. Add `layers` only when the app has a real layer model. Add `media` only when runtime media state must survive reload, such as predefined attached files that users can delete, reorder, or transform.
286
416
 
287
- Do not persist media blobs, files, generated images, or history stacks. Theme preference is runtime-owned separately. Do not write runtime state to `localStorage` directly from app code.
417
+ Do not write media state directly to storage and do not use product values to mirror the attached file list. Use `media.defaultAssets` for predefined source files/background images; they render as ordinary `fileDrop` attachments, can be removed to produce an empty source/canvas state, and Reset restores them. Theme preference is runtime-owned separately. Do not write runtime state to `localStorage` directly from app code.
288
418
 
289
- If `storage` is `"localStorage"`, add a runtime acceptance row with `persistenceCoverage: "reload"` and a browser test that changes a user-facing setting, reloads the page, and verifies the restored value or product output. Settings import/export is for preset transfer in complex apps; it is not proof that persistence works.
419
+ If `storage` is `"localStorage"`, add a runtime acceptance row with `persistenceCoverage: "reload"` and a browser test that changes a user-facing setting, reloads the page, and verifies the restored value or product output. Settings import/export is preset transfer, not proof that persistence works.
290
420
 
291
421
  ## Settings Transfer
292
422
 
293
- Use `settingsTransfer` when users should move a complex app setup between sessions or machines.
423
+ Generated apps keep a controls panel so runtime `Setup` is visible from the first run. Product controls are added after that mandatory runtime section. Use `settingsTransfer` only to customize the exported JSON identity or file name.
294
424
 
295
425
  ```ts
296
426
  settingsTransfer: "auto"
@@ -298,17 +428,17 @@ settingsTransfer: "auto"
298
428
 
299
429
  Allowed values:
300
430
 
301
- - `"auto"`: default. Runtime enables the first settings-transfer section when the app reaches the complexity threshold: 12 product controls, 5 product sections, or weighted score 18. Compound controls, layers, and timeline increase the weighted score.
302
- - `true`: force the section on.
303
- - `false`: force the section off.
431
+ - `"auto"`: default. Runtime still shows the mandatory first Setup section.
432
+ - `true`: keep the mandatory section and mark settings transfer explicitly enabled.
433
+ - `false`: keep the mandatory section; this value is retained as metadata only and does not hide `Export Settings` / `Import Settings`.
304
434
  - `{ enabled, appId, fileName }`: customize the exported JSON identity and file name.
305
435
 
306
- When enabled, the runtime inserts a technical `Setup` settings-transfer section as the first controls-panel section. It renders without a visible section heading, exports and imports control values, `canvas.size`, and timeline state, ignores unknown targets on import, and pauses playback after importing.
436
+ Runtime inserts a visible headerless `Setup` controls block as the first controls-panel block. It exports and imports control values, `canvas.size`, and timeline state, ignores unknown targets on import, and pauses playback after importing.
307
437
 
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.
438
+ Do not add settings transfer buttons manually and do not use complexity thresholds to decide whether they appear.
309
439
 
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.
440
+ When the canvas uses `editable-output` sizing, `Setup` contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, optional `Resolution scale`, and optional `Timeline` in that order. Do not split these into separate sections or recreate them manually. Product sections must not declare `runtime.settingsTransfer`, `canvas.aspectRatio`, `canvas.size.width`, `canvas.size.height`, `canvas.renderScale`, or `panels.timeline.extended`; runtime Setup owns those targets and always renders its own controls.
311
441
 
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.
442
+ A settings-transfer section with only `Export Settings` and `Import Settings` means the canvas is not `editable-output`. For product-output apps, treat that as a schema error to fix, not as a layout variant.
313
443
 
314
444
  Do not hand-write `settings-transfer.ts`, hidden file inputs, route handlers, or `panelActions` for settings import/export. Sticky footer `panelActions` remain product delivery only.
@@ -24,6 +24,7 @@ Use the smallest reading set that covers the changed surface. If a task touches
24
24
  | Task type | Read before editing |
25
25
  | --- | --- |
26
26
  | App assembly, route structure, generated app porting | `assembly-workflow.md`, `decision-contract.md` |
27
+ | Reference app study, audit, or port | `assembly-workflow.md`, `schema-reference.md`, `acceptance-testing.md`, `decision-contract.md` |
27
28
  | Schema, controls, defaults, persistence, actions | `schema-reference.md`, `component-rules.md`, `acceptance-testing.md` |
28
29
  | Custom controls | `custom-controls.md`, `component-rules.md`, `acceptance-testing.md` |
29
30
  | Renderer, canvas output, visual technique | `renderer-technique.md`, `performance.md`, `acceptance-testing.md` |
@@ -74,11 +75,13 @@ Choose the tier from `AGENTS.md` before editing. Use the tier to decide checks.
74
75
  - Tier 0-1: targeted docs/typecheck/unit plus focused browser when visual.
75
76
  - Tier 2: `pnpm verify:quick` plus relevant browser acceptance.
76
77
  - 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
+ - Tier 4: `pnpm verify:final`; for the first working product version also run and pass a browser performance checkpoint with the current AI agent's controlled browser when available, using `pnpm verify:perf` only as fallback, then start `pnpm dev` for the local URL.
78
79
 
79
- Run a full performance checkpoint with `pnpm verify:perf` only when:
80
+ Run a full performance checkpoint only when:
80
81
 
81
82
  - the first working version of the app exists;
82
83
  - the user explicitly asks to optimize performance, fix lag, remove jank, speed up animation, stabilize drag/zoom, or otherwise complains about performance.
83
84
 
84
85
  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.
86
+
87
+ The app is not complete when required checks are failed, incomplete, pending, blocked, or listed as skipped. First working product delivery must record `pnpm verify:final` and the browser performance checkpoint as passed in the worklog, including runner `agent-browser` or `playwright-fallback`. After the first working version, skipping the full performance suite is valid only when the worklog explicitly says the full performance checkpoint is not required for a post-first-working non-performance edit.
@@ -213,7 +213,7 @@ function hasProductObservableHelper(source: string): boolean {
213
213
  );
214
214
  }
215
215
 
216
- test("browser acceptance matrix points at real Playwright tests", () => {
216
+ test("browser acceptance matrix points at real fallback Playwright tests", () => {
217
217
  const browserTestSources = readSiblingBrowserTestSources();
218
218
 
219
219
  for (const entry of starterAcceptance) {
@@ -223,7 +223,7 @@ test("browser acceptance matrix points at real Playwright tests", () => {
223
223
 
224
224
  expect(
225
225
  Boolean(findNamedBrowserTestSource(browserTestSources, entry.browserTestName)),
226
- `${entry.id} must be backed by a Playwright test named "${entry.browserTestName}".`,
226
+ `${entry.id} must be backed by a fallback Playwright test named "${entry.browserTestName}".`,
227
227
  ).toBe(true);
228
228
  }
229
229
  });
@@ -376,7 +376,7 @@ test("browser canvas handle entries use handle helpers and no forbidden canvas U
376
376
 
377
377
  expect(
378
378
  browserTestSource,
379
- `${entry.id} must be backed by a Playwright test named "${entry.browserTestName}".`,
379
+ `${entry.id} must be backed by a fallback Playwright test named "${entry.browserTestName}".`,
380
380
  ).toBeDefined();
381
381
 
382
382
  if (!browserTestSource) {
@@ -148,12 +148,43 @@ function getScenarioSliderStressValuePattern(scenarioId: string): RegExp {
148
148
  );
149
149
  }
150
150
 
151
+ function fixtureValueUsesRenderScale(value: unknown, pathPrefix = ""): boolean {
152
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
153
+ return false;
154
+ }
155
+
156
+ return Object.entries(value).some(([key, itemValue]) => {
157
+ const itemPath = pathPrefix ? `${pathPrefix}.${key}` : key;
158
+
159
+ return (
160
+ /^(?:canvas\.)?renderScale$|(?:^|[._-])resolutionScale$/i.test(itemPath) ||
161
+ fixtureValueUsesRenderScale(itemValue, itemPath)
162
+ );
163
+ });
164
+ }
165
+
166
+ function scenarioUsesRenderScaleFixture(
167
+ scenario: (typeof starterPerformance.scenarios)[number],
168
+ ): boolean {
169
+ return (
170
+ scenario.target === "canvas.renderScale" ||
171
+ fixtureValueUsesRenderScale(scenario.stressFixture?.value) ||
172
+ fixtureValueUsesRenderScale(scenario.workloadFixture?.value)
173
+ );
174
+ }
175
+
176
+ function scenarioUsesLoadProfile(
177
+ scenario: (typeof starterPerformance.scenarios)[number],
178
+ ): boolean {
179
+ return Boolean(scenario.stressFixture?.loadProfile || scenario.workloadFixture?.loadProfile);
180
+ }
181
+
151
182
  function getFirstMatchIndex(source: string, pattern: RegExp): number {
152
183
  const match = pattern.exec(source);
153
184
  return match?.index ?? -1;
154
185
  }
155
186
 
156
- test("browser performance matrix points at real Playwright tests", () => {
187
+ test("browser performance matrix points at real fallback browser tests", () => {
157
188
  const browserTestSources = readSiblingBrowserTestSources();
158
189
 
159
190
  for (const scenario of starterPerformance.scenarios) {
@@ -163,7 +194,7 @@ test("browser performance matrix points at real Playwright tests", () => {
163
194
 
164
195
  expect(
165
196
  Boolean(findNamedBrowserTestSource(browserTestSources, scenario.browserTestName)),
166
- `${scenario.id} must be backed by a browser performance test named "${scenario.browserTestName}".`,
197
+ `${scenario.id} must be backed by a fallback browser performance test named "${scenario.browserTestName}".`,
167
198
  ).toBe(true);
168
199
  }
169
200
  });
@@ -292,6 +323,28 @@ test("browser performance tests use real Toolcraft interactions", () => {
292
323
  }
293
324
  }
294
325
 
326
+ if (scenarioUsesRenderScaleFixture(scenario)) {
327
+ expect(
328
+ browserTestSource,
329
+ `${scenario.id} uses canvas.renderScale and must prove the selected Resolution scale changes backing canvas pixels instead of only changing declarative state.`,
330
+ ).toMatch(/expectToolcraftCanvasBackingPixelsForRenderScale\s*\(/);
331
+ }
332
+
333
+ if (scenarioUsesLoadProfile(scenario)) {
334
+ expect(
335
+ browserTestSource,
336
+ `${scenario.id} declares a loadProfile and must apply the documented smoothTarget through appPerformance fixtures instead of recalculating a smaller browser-only target.`,
337
+ ).toMatch(
338
+ scenario.workloadFixture
339
+ ? getScenarioWorkloadFixturePattern(scenario.id)
340
+ : getScenarioStressFixturePattern(scenario.id),
341
+ );
342
+ expect(
343
+ browserTestSource,
344
+ `${scenario.id} declares a loadProfile and must keep budget assertions tied to app-performance.ts.`,
345
+ ).toMatch(getScenarioBudgetAssertionPattern(scenario.id));
346
+ }
347
+
295
348
  if (scenario.interaction === "control-change") {
296
349
  expect(
297
350
  browserTestSource,
@@ -442,6 +442,51 @@ export async function dragToolcraftSliderToPerformanceStressValue(
442
442
  await dragToolcraftSliderToValue(page, label, value);
443
443
  }
444
444
 
445
+ export async function expectToolcraftCanvasBackingPixelsForRenderScale(
446
+ page: Page,
447
+ canvasSelector: string,
448
+ renderScale: number,
449
+ ): Promise<void> {
450
+ if (!Number.isFinite(renderScale) || renderScale <= 1) {
451
+ throw new Error(
452
+ `Toolcraft render scale backing-pixel checks require a numeric renderScale greater than 1, received ${renderScale}.`,
453
+ );
454
+ }
455
+
456
+ const canvas = page.locator(canvasSelector).first();
457
+ await expect(
458
+ canvas,
459
+ `Toolcraft render scale check expected a visible canvas matching "${canvasSelector}".`,
460
+ ).toBeVisible();
461
+
462
+ const metrics = await canvas.evaluate((element) => {
463
+ if (!(element instanceof HTMLCanvasElement)) {
464
+ throw new Error("Render scale backing-pixel checks must target an HTMLCanvasElement.");
465
+ }
466
+
467
+ const rect = element.getBoundingClientRect();
468
+ return {
469
+ backingHeight: element.height,
470
+ backingWidth: element.width,
471
+ cssHeight: element.clientHeight || rect.height,
472
+ cssWidth: element.clientWidth || rect.width,
473
+ devicePixelRatio: window.devicePixelRatio || 1,
474
+ };
475
+ });
476
+
477
+ const expectedWidth = metrics.cssWidth * metrics.devicePixelRatio * renderScale;
478
+ const expectedHeight = metrics.cssHeight * metrics.devicePixelRatio * renderScale;
479
+
480
+ expect(
481
+ metrics.backingWidth,
482
+ `Expected canvas backing width to honor Resolution scale ${renderScale}.`,
483
+ ).toBeGreaterThanOrEqual(Math.floor(expectedWidth - 1));
484
+ expect(
485
+ metrics.backingHeight,
486
+ `Expected canvas backing height to honor Resolution scale ${renderScale}.`,
487
+ ).toBeGreaterThanOrEqual(Math.floor(expectedHeight - 1));
488
+ }
489
+
445
490
  export async function applyToolcraftPerformanceStressFixture(
446
491
  page: Page,
447
492
  config: ToolcraftPerformanceConfig,