@pixel-point/toolcraft 0.0.8 → 0.0.9

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/generate.mjs +34 -5
  3. package/src/generate.test.mjs +12 -0
  4. package/src/package-json.mjs +15 -0
  5. package/src/package-json.test.mjs +14 -1
  6. package/templates/runtime/contracts/component-contracts.test.ts +104 -14
  7. package/templates/runtime/contracts/component-contracts.ts +54 -22
  8. package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
  9. package/templates/runtime/contracts/decision-contracts.ts +3 -3
  10. package/templates/runtime/react/controls-panel.test.tsx +374 -13
  11. package/templates/runtime/react/controls-panel.tsx +65 -4
  12. package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
  13. package/templates/runtime/schema/define-toolcraft.ts +25 -1
  14. package/templates/runtime/schema/types.ts +3 -0
  15. package/templates/runtime/testing/performance.test.ts +134 -0
  16. package/templates/runtime/testing/performance.ts +34 -4
  17. package/templates/starter/AGENTS.md +4 -4
  18. package/templates/starter/docs/toolcraft/README.md +1 -1
  19. package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
  20. package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
  21. package/templates/starter/docs/toolcraft/component-rules.md +32 -11
  22. package/templates/starter/docs/toolcraft/performance.md +7 -1
  23. package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
  24. package/templates/starter/gitignore +1 -0
  25. package/templates/starter/package.json +2 -0
  26. package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
  27. package/templates/starter/scripts/toolcraft-port.mjs +102 -0
  28. package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
  29. package/templates/starter/src/app/starter-acceptance.test.ts +739 -66
  30. package/templates/starter/src/app/starter-acceptance.ts +471 -12
  31. package/templates/ui/components/control-layout/index.tsx +8 -3
  32. package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
  33. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  34. package/templates/ui/components/controls/color/index.ts +4 -1
  35. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  36. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  37. package/templates/ui/components/controls/index.ts +2 -0
  38. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  39. package/templates/ui/components/controls/select/select-control.tsx +9 -4
  40. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  41. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  42. package/templates/ui/components/controls/vector/index.ts +1 -0
  43. package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
  44. package/templates/ui/components/panel/panel-section.tsx +29 -5
@@ -524,6 +524,10 @@ function getControlSectionLayout(
524
524
  control: ToolcraftControlSchema,
525
525
  entries: readonly [string, ToolcraftControlSchema][],
526
526
  ): "grouped" | "standalone" {
527
+ if (isControlGatedBySameSectionControl(control, entries)) {
528
+ return "grouped";
529
+ }
530
+
527
531
  if (
528
532
  (control.type === "color" || control.type === "colorOpacity") &&
529
533
  entries.some(
@@ -539,6 +543,24 @@ function getControlSectionLayout(
539
543
  return getControlDefaultSectionLayout(control);
540
544
  }
541
545
 
546
+ function isControlGatedBySameSectionControl(
547
+ control: ToolcraftControlSchema,
548
+ entries: readonly [string, ToolcraftControlSchema][],
549
+ ): boolean {
550
+ const gateTargets = [control.visibleWhen?.target, control.disabledWhen?.target].filter(
551
+ (target): target is string => Boolean(target),
552
+ );
553
+
554
+ if (gateTargets.length === 0) {
555
+ return false;
556
+ }
557
+
558
+ return entries.some(([, entryControl]) =>
559
+ gateTargets.includes(entryControl.target) &&
560
+ getControlDefaultSectionLayout(entryControl) === "grouped",
561
+ );
562
+ }
563
+
542
564
  function createControlsRecord(
543
565
  entries: readonly [string, ToolcraftControlSchema][],
544
566
  ): Record<string, ToolcraftControlSchema> {
@@ -1428,7 +1450,6 @@ function normalizePanels({
1428
1450
  step: canvas.renderScale.step,
1429
1451
  target: canvasRenderScaleTarget,
1430
1452
  type: "slider",
1431
- unit: "x",
1432
1453
  variant: "discrete",
1433
1454
  }
1434
1455
  : undefined;
@@ -1460,6 +1481,7 @@ function normalizePanels({
1460
1481
  const aspectRatioControl: ToolcraftControlSchema = {
1461
1482
  defaultValue: getCanvasAspectRatioDefaultValue(canvas.size),
1462
1483
  label: "Aspect ratio",
1484
+ orderRole: "input",
1463
1485
  performanceReason: "Aspect ratio changes output dimensions and renderer workload.",
1464
1486
  performanceRole: "workload",
1465
1487
  target: canvasAspectRatioTarget,
@@ -1470,6 +1492,7 @@ function normalizePanels({
1470
1492
  sizeControls.canvasWidth = {
1471
1493
  defaultValue: canvas.size.width,
1472
1494
  label: "Canvas width",
1495
+ orderRole: "input",
1473
1496
  performanceReason: "Canvas width changes output dimensions and renderer workload.",
1474
1497
  performanceRole: "workload",
1475
1498
  target: canvasSizeControlTargets.width,
@@ -1482,6 +1505,7 @@ function normalizePanels({
1482
1505
  sizeControls.canvasHeight = {
1483
1506
  defaultValue: canvas.size.height,
1484
1507
  label: "Canvas height",
1508
+ orderRole: "input",
1485
1509
  performanceReason: "Canvas height changes output dimensions and renderer workload.",
1486
1510
  performanceRole: "workload",
1487
1511
  target: canvasSizeControlTargets.height,
@@ -323,12 +323,15 @@ export type ToolcraftFontPickerValueSchema = {
323
323
 
324
324
  export type ToolcraftCurveInterpolation = "monotone" | "smooth";
325
325
 
326
+ export type ToolcraftVectorCoordinateMode = "cartesian" | "screen";
327
+
326
328
  export type ToolcraftControlSchema = {
327
329
  accept?: string;
328
330
  actions?: readonly (ToolcraftActionSchema | string)[];
329
331
  assetKind?: ToolcraftFileDropAssetKind;
330
332
  addLabel?: string;
331
333
  commitMode?: "content" | "setting";
334
+ coordinateMode?: ToolcraftVectorCoordinateMode;
332
335
  defaultValue?: unknown;
333
336
  description?: string;
334
337
  disabled?: boolean;
@@ -76,6 +76,34 @@ const ordinarySliderSchema = defineToolcraft({
76
76
  },
77
77
  });
78
78
 
79
+ const rangeSliderSchema = defineToolcraft({
80
+ canvas: {
81
+ enabled: true,
82
+ sizing: { mode: "intrinsic-media" },
83
+ },
84
+ panels: {
85
+ controls: {
86
+ sections: [
87
+ {
88
+ controls: {
89
+ band: {
90
+ defaultValue: [20, 80],
91
+ label: "Band",
92
+ max: 100,
93
+ min: 0,
94
+ performanceReason: "Band changes a lightweight bounded interval.",
95
+ performanceRole: "responsiveness",
96
+ target: "render.band",
97
+ type: "rangeSlider",
98
+ },
99
+ },
100
+ },
101
+ ],
102
+ title: "Runtime Controls",
103
+ },
104
+ },
105
+ });
106
+
79
107
  const largeTextSchema = defineToolcraft({
80
108
  canvas: {
81
109
  enabled: true,
@@ -714,6 +742,64 @@ describe("Toolcraft template performance contract", () => {
714
742
  expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toEqual([]);
715
743
  });
716
744
 
745
+ it("rejects slider performance coverage that does not drag the real control", () => {
746
+ const config = defineToolcraftPerformance({
747
+ rendererStrategy: "none",
748
+ rendererWorkload: "none",
749
+ scenarios: [
750
+ {
751
+ automated: true,
752
+ automatedTestName: "perf: opacity change stays responsive",
753
+ browser: true,
754
+ browserTestName: "browser perf: opacity change stays responsive",
755
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
756
+ controlLabel: "Opacity",
757
+ expectedObservable: "Changing Opacity stays responsive.",
758
+ fixture: "opacity responsiveness fixture",
759
+ id: "opacity-change",
760
+ interaction: "control-change",
761
+ target: "render.opacity",
762
+ workload: false,
763
+ },
764
+ ],
765
+ usesCustomRenderer: false,
766
+ workloadTargets: [],
767
+ });
768
+
769
+ expect(validateToolcraftPerformanceCoverage(ordinarySliderSchema, config)).toContain(
770
+ "render.opacity is a slider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
771
+ );
772
+ });
773
+
774
+ it("rejects range slider performance coverage that does not drag the real control", () => {
775
+ const config = defineToolcraftPerformance({
776
+ rendererStrategy: "none",
777
+ rendererWorkload: "none",
778
+ scenarios: [
779
+ {
780
+ automated: true,
781
+ automatedTestName: "perf: band change stays responsive",
782
+ browser: true,
783
+ browserTestName: "browser perf: band change stays responsive",
784
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
785
+ controlLabel: "Band",
786
+ expectedObservable: "Changing Band stays responsive.",
787
+ fixture: "band responsiveness fixture",
788
+ id: "band-change",
789
+ interaction: "control-change",
790
+ target: "render.band",
791
+ workload: false,
792
+ },
793
+ ],
794
+ usesCustomRenderer: false,
795
+ workloadTargets: [],
796
+ });
797
+
798
+ expect(validateToolcraftPerformanceCoverage(rangeSliderSchema, config)).toContain(
799
+ "render.band is a rangeSlider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
800
+ );
801
+ });
802
+
717
803
  it("requires performance-sensitive controls to be listed in workloadTargets", () => {
718
804
  const config = defineToolcraftPerformance({
719
805
  rendererStrategy: "none",
@@ -758,6 +844,54 @@ describe("Toolcraft template performance contract", () => {
758
844
  );
759
845
  });
760
846
 
847
+ it("requires workload slider coverage to use real control drag", () => {
848
+ const config = defineToolcraftPerformance({
849
+ rendererStrategy: "none",
850
+ rendererWorkload: "none",
851
+ scenarios: [
852
+ {
853
+ automated: true,
854
+ automatedTestName: "perf: density change stays responsive",
855
+ browser: true,
856
+ browserTestName: "browser perf: density change stays responsive",
857
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
858
+ controlLabel: "Density",
859
+ expectedObservable: "Changing Density updates the product without blocking the UI.",
860
+ fixture: "runtime density fixture",
861
+ id: "density-change",
862
+ interaction: "control-change",
863
+ target: "render.density",
864
+ values: { default: 4, max: 12, min: 1 },
865
+ workload: true,
866
+ },
867
+ {
868
+ automated: true,
869
+ automatedTestName: "perf: mode change stays responsive",
870
+ browser: true,
871
+ browserTestName: "browser perf: mode change stays responsive",
872
+ budget: { maxFrameGapMs: 80, maxInteractionMs: 500 },
873
+ controlLabel: "Mode",
874
+ expectedObservable: "Changing Mode updates the product without blocking the UI.",
875
+ fixture: "runtime mode fixture",
876
+ id: "mode-change",
877
+ interaction: "control-change",
878
+ target: "render.mode",
879
+ values: { default: "soft", max: "sharp", min: "soft" },
880
+ workload: false,
881
+ },
882
+ ],
883
+ usesCustomRenderer: false,
884
+ workloadTargets: ["render.density"],
885
+ });
886
+
887
+ expect(validateToolcraftPerformanceCoverage(testSchema, config)).toEqual(
888
+ expect.arrayContaining([
889
+ "render.density must have min/default/max workload performance coverage through a real control-drag scenario.",
890
+ "render.density is a slider and must have a control-drag performance scenario proving live canvas/product feedback while dragging.",
891
+ ]),
892
+ );
893
+ });
894
+
761
895
  it("rejects responsiveness role on semantically workload controls", () => {
762
896
  const misclassifiedSchema = defineToolcraft({
763
897
  canvas: {
@@ -455,6 +455,20 @@ function hasMinDefaultMax(values: ToolcraftPerformanceScenario["values"]): boole
455
455
  return values !== undefined && "default" in values && "min" in values && "max" in values;
456
456
  }
457
457
 
458
+ function getSliderDragControlType(
459
+ control: ToolcraftControlSchema | undefined,
460
+ ): "rangeSlider" | "slider" | null {
461
+ if (control?.type === "slider" || control?.type === "rangeSlider") {
462
+ return control.type;
463
+ }
464
+
465
+ return null;
466
+ }
467
+
468
+ function hasControlDragScenario(scenarios: readonly ToolcraftPerformanceScenario[]): boolean {
469
+ return scenarios.some((scenario) => scenario.interaction === "control-drag");
470
+ }
471
+
458
472
  function hasPerformanceFixtureValue(
459
473
  fixture: ToolcraftPerformanceFixture,
460
474
  ): fixture is ToolcraftPerformanceFixture & { value: unknown } {
@@ -1587,24 +1601,40 @@ export function validateToolcraftPerformanceCoverage(
1587
1601
  }
1588
1602
 
1589
1603
  const targetScenarios = scenariosByTarget.get(target) ?? [];
1604
+ const targetControl = controlsByTarget.get(target);
1605
+ const targetRequiresDrag = getSliderDragControlType(targetControl) !== null;
1590
1606
  const hasWorkloadCoverage = targetScenarios.some(
1591
1607
  (scenario) =>
1592
1608
  scenario.workload &&
1593
- (scenario.interaction === "control-drag" ||
1594
- scenario.interaction === "control-change") &&
1609
+ (targetRequiresDrag
1610
+ ? scenario.interaction === "control-drag"
1611
+ : scenario.interaction === "control-drag" ||
1612
+ scenario.interaction === "control-change") &&
1595
1613
  hasMinDefaultMax(scenario.values),
1596
1614
  );
1597
1615
 
1598
1616
  if (!hasWorkloadCoverage) {
1599
- errors.push(`${target} must have min/default/max workload performance coverage.`);
1617
+ errors.push(
1618
+ targetRequiresDrag
1619
+ ? `${target} must have min/default/max workload performance coverage through a real control-drag scenario.`
1620
+ : `${target} must have min/default/max workload performance coverage.`,
1621
+ );
1600
1622
  }
1601
1623
  }
1602
1624
 
1603
1625
  for (const target of getVisiblePerformanceControlTargets(schema)) {
1604
- if (!scenariosByTarget.has(target)) {
1626
+ const targetScenarios = scenariosByTarget.get(target) ?? [];
1627
+ const targetControl = controlsByTarget.get(target);
1628
+ const sliderControlType = getSliderDragControlType(targetControl);
1629
+
1630
+ if (targetScenarios.length === 0) {
1605
1631
  errors.push(
1606
1632
  `${target} must have a performance scenario because every visible control can affect app responsiveness.`,
1607
1633
  );
1634
+ } else if (sliderControlType && !hasControlDragScenario(targetScenarios)) {
1635
+ errors.push(
1636
+ `${target} is a ${sliderControlType} and must have a control-drag performance scenario proving live canvas/product feedback while dragging.`,
1637
+ );
1608
1638
  }
1609
1639
  }
1610
1640
 
@@ -14,16 +14,16 @@ Then follow `workflow.md` to choose the required contract docs and verification
14
14
 
15
15
  1. Build through `defineToolcraft` and `ToolcraftApp`.
16
16
  2. Keep app state in Toolcraft runtime schema and commands.
17
- 3. Keep product output in `canvasContent`; never render app UI there.
17
+ 3. Keep product output in `canvasContent`; never render app UI there. If upload/import is part of the source-material flow, do not invent canvas placeholder artwork, CTA copy, helper text, fake sample output, or preset source designs before real content exists.
18
18
  4. Use built-in Toolcraft controls before custom controls.
19
19
  5. Do not hand-compose runtime surfaces or render built-in control components directly in app code; use `ToolcraftApp`, schema controls, `canvasContent`, `controlRenderers`, `onPanelAction`, and runtime commands.
20
- 6. Before writing controls, make a Control Section Inventory that groups controls by product entity or workflow stage, not by UI component type.
20
+ 6. Before writing controls, make and export `starterControlSectionInventory`: each product controls section declares its title, product entity or workflow stage, targets, and grouping reason. Group by product meaning, not UI component type.
21
21
  7. Keep control `label` short but semantically sufficient with the nearest visible section/group context, and put product-specific behavior help in schema `description`; runtime renders the label help tooltip only when that description adds meaning beyond the label.
22
22
  8. Enable layers and timeline only when product behavior requires them, then test the real UI.
23
23
  9. Animated preview renderers suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions, then resume without changing user playback state.
24
24
  10. If a Figma URL is provided, inspect the Figma file through MCP and rebuild from its structure; never implement from a screenshot or by eye.
25
25
  11. Choose an explicit persistence policy; use schema `persistence` for user-edited app settings that should survive reload, and test real reload restoration when localStorage is enabled.
26
- 12. Use schema `settingsTransfer: "auto"` for complex apps that need import/export of control settings; never implement settings import/export through `panelActions` or route-local file inputs. After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are owned by `editable-output` canvas sizing, not by settings transfer. Runtime aspect presets apply canonical canvas sizes, with `16:9` equal to `1920x1080`; manual Canvas width/height edits keep the typed dimension, keep the other dimension unchanged, switch Aspect ratio to Custom, and show the reduced current ratio in custom ratio inputs; when no explicit product size is provided, the runtime default canvas size is also `1920x1080`. Non-vector raster, Canvas 2D, WebGL, and WebGPU previews set `canvas.renderScale: true`; the first technical runtime section then appends `Resolution scale` after canvas sizing so backing pixels can increase up to 2x without changing CSS/output size. Performance fixes must preserve the selected render scale and keep canvas preview responsive to sliders/high-frequency controls at that scale; diagnose the bottleneck before reducing quality. Do not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping `canvas.renderScale` below the user's chosen value. When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and, for raster outputs, `Resolution scale` in that order and renders without a visible section heading.
26
+ 12. Use schema `settingsTransfer: "auto"` for complex apps that need import/export of control settings; never implement settings import/export through `panelActions` or route-local file inputs. After adding, removing, or reorganizing controls, sections, timeline, or layers, recalculate settings-transfer eligibility. The runtime threshold is 12 product controls, 5 product sections, or weighted score 18. Visible `Aspect ratio`, `Canvas width`, and `Canvas height` controls are owned by `editable-output` canvas sizing, not by settings transfer. Runtime aspect presets apply canonical canvas sizes, with `16:9` equal to `1920x1080`; manual Canvas width/height edits keep the typed dimension, keep the other dimension unchanged, switch Aspect ratio to Custom, and show the reduced current ratio in custom ratio inputs; when no explicit product size is provided, the runtime default canvas size is also `1920x1080`. Product-output, exportable, shader, procedural, and reference-clone apps use `editable-output`; fixed/reference/base dimensions are initial `canvas.size` values, not reasons to hide `Aspect ratio`, `Canvas width`, or `Canvas height`. Non-vector raster, Canvas 2D, WebGL, and WebGPU previews set `canvas.renderScale: true`; the first technical runtime section then appends `Resolution scale` after canvas sizing so backing pixels can increase up to scale 2 without changing CSS/output size. Performance fixes must preserve the selected render scale and keep canvas preview responsive to sliders/high-frequency controls at that scale; diagnose the bottleneck before reducing quality. Do not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping `canvas.renderScale` below the user's chosen value. When settings transfer and editable-output canvas sizing are both enabled, the first technical `Setup` runtime section contains `Export Settings`, `Import Settings`, `Aspect ratio`, `Canvas width`, `Canvas height`, and, for raster outputs, `Resolution scale` in that order and renders without a visible section heading.
27
27
  13. Product apps expose a required `Background` section directly before export settings. It contains a Switch labeled `Include` and a background color control with `label: false` in one equal-width inline row; PNG export wires those runtime values into the standard export helper, live preview uses `shouldIncludeToolcraftPreviewBackground(state)` so Include can hide the product background, and video export keeps the background. Every app with `Export PNG` exposes `Image Export` with `export.image.format` and `export.image.resolution` as two `select` controls in one compact two-column inline row, and passes the selected resolution to `createToolcraftPngExportCanvas({ resolution })` so 2K/4K/8K change actual PNG dimensions. Animated apps with both PNG and video export place `Image Export` immediately before `Video Export`.
28
28
  14. Keep `docs/toolcraft/agent-worklog.md` current with a decision trail, product decisions, evidence, verification, and risks.
29
29
  15. Prove every visible entity through acceptance, browser, and performance coverage.
@@ -164,7 +164,7 @@ Use `pnpm install` before this final gate when the folder is fresh or dependenci
164
164
 
165
165
  `pnpm test` must include `node scripts/check-toolcraft-docs.mjs`, `node scripts/check-toolcraft-integrity.mjs`, and app tests. `pnpm verify:ui` / `pnpm test:browser` must run against the real app UI and product output. `pnpm verify:perf` / `pnpm test:browser:perf` remains available for the two full-performance triggers and must run the performance browser suite sequentially so budgets are measured without parallel e2e noise.
166
166
 
167
- Do not stop or kill existing local servers to free a port. `pnpm dev`, `pnpm preview`, and browser verification prefer port `3002`, but automatically move to the next free port when it is busy. Use `TOOLCRAFT_PORT`, `TOOLCRAFT_DEV_PORT`, or `TOOLCRAFT_TEST_PORT` only to change the preferred starting port.
167
+ Do not stop or kill existing local servers to free a port during a first start. `pnpm dev`, `pnpm preview`, and browser verification prefer port `3002`, but automatically move to the next free port when it is busy. Use `TOOLCRAFT_PORT`, `TOOLCRAFT_DEV_PORT`, or `TOOLCRAFT_TEST_PORT` only to change the preferred starting port. When deliberately restarting this app server, use `pnpm dev:restart` or `pnpm preview:restart`; restart mode reuses the previously saved app port, stops the listener on that exact port if it is still running, force-stops it if it does not release the port, and starts on the same port again.
168
168
 
169
169
  ## App Completion Bar
170
170
 
@@ -39,4 +39,4 @@ pnpm verify:perf # first working version or explicit performance complaint only
39
39
  pnpm dev
40
40
  ```
41
41
 
42
- Do not kill existing local servers to free `3002`. Dev, preview, and browser verification prefer `3002`, then move to the next free port automatically.
42
+ Do not kill existing local servers to free `3002` during a first start. Dev, preview, and browser verification prefer `3002`, then move to the next free port automatically. When restarting the same app server, use `pnpm dev:restart` or `pnpm preview:restart`; restart mode reuses the saved app port and stops only the listener on that exact port before starting again, forcing it only when the soft stop does not release the port.
@@ -53,12 +53,14 @@ Each row should name:
53
53
  - exact `automatedTestName`;
54
54
  - exact `browserTestName`.
55
55
  - `controlPartCoverage` when the control is compound.
56
- - `canvasSizingCoverage: "fixed-output-size"` when `canvas.sizing.mode` is `fixed-output`.
56
+ - `canvasSizingCoverage: "fixed-output-size"` only for non-product/internal `fixed-output` fixtures.
57
57
  - `persistenceCoverage: "reload"` when schema `persistence.storage` is `"localStorage"`.
58
58
 
59
59
  The test gate rejects rows without matching automated and browser test names.
60
60
 
61
- `fixed-output` canvas sizing must be deliberate. Its runtime acceptance row must explain why width, height, and aspect ratio are non-editable. A default size from the prompt should use `editable-output`, which keeps the runtime Aspect ratio, Canvas width, and Canvas height controls.
61
+ Slider and range slider rows must prove live behavior. Browser tests should drag the real thumb and assert the runtime value and product-level canvas observable update during the drag, not only after pointer release, blur, an Apply action, or a final commit. Performance-sensitive sliders still need this live acceptance; jank is handled through renderer optimization and targeted performance coverage, not by making the slider deferred by default.
62
+
63
+ `fixed-output` canvas sizing must be deliberate and is not valid for generated product/output apps with export actions. A default, reference, or fixed-format size from the prompt should use `editable-output`, which keeps the runtime Aspect ratio, Canvas width, and Canvas height controls.
62
64
 
63
65
  When localStorage persistence is enabled, add a runtime acceptance row that proves reload behavior. The browser test must change a real user-facing setting, wait for persistence to write, call a real page reload, and verify the restored control value or product output. Importing a settings JSON file is not persistence coverage.
64
66
 
@@ -152,7 +154,7 @@ Animated app acceptance must also exercise the separate `Video Export` section:
152
154
 
153
155
  Footer action acceptance must not include Reset. Reset is already available in the controls panel header and uses schema `defaultValue`; duplicating it in sticky `panelActions` fails acceptance.
154
156
 
155
- Local `actions` acceptance must click every visible action and prove the nearby entity changed through runtime state or product output. A section-level `Randomize palette` must change palette output, `Normalize weights` must change weights/output, and `Clear selection` must clear only the scoped selection. Do not accept a test that only proves the button rendered.
157
+ Local `actions` acceptance must click every visible action and prove the nearby entity changed through runtime state or product output. A section-level `Randomize palette` must change palette output, `Normalize weights` must change weights/output, and `Clear selection` must clear only the scoped selection. Do not accept a test that only proves the button rendered. A single-button `actions` control fails validation when the control label duplicates the button label; the label must add concise context. Visual acceptance rejects side-label actions; labels sit above a two-column button grid where each button cell is 50% width.
156
158
 
157
159
  `collectionActions` acceptance must click plus and minus in the real panel, prove the runtime target array length changes, prove `minItems` prevents invalid removal, prove `recommendedMaxItems` is not a hidden hard limit, and prove preview/export consumes the changed item list.
158
160
 
@@ -51,7 +51,7 @@ Do not leave `mode: "starter"` in a renamed product folder or after adding produ
51
51
 
52
52
  ## Control Sections
53
53
 
54
- Before writing the schema, make a Control Section Inventory. Each section needs a product entity or workflow stage, included targets, and a reason for grouping. Do not group by control type.
54
+ Before writing the schema, make and export `starterControlSectionInventory`. Each product controls section needs a product entity or workflow stage, included targets, and a reason for grouping. Do not group by control type. The exported inventory must match the schema targets exactly; if one target entity is intentionally split across sections, every split section needs `workflowStage` and `splitReason`.
55
55
 
56
56
  Bad section titles: `Controls`, `Settings`, `Options`, `Sliders`, `Inputs`, `Buttons`, `Color`, `Colors`.
57
57
 
@@ -67,6 +67,8 @@ Ordinary controls-panel body sections use 8px top spacing and 24px bottom spacin
67
67
 
68
68
  If a color, slider, input, or selector edits the same entity as nearby controls, keep it in that entity section. Split only when the product has a real workflow split and cover that decision in acceptance.
69
69
 
70
+ When a selector controls visibility or enabled state for another control through `visibleWhen` or `disabledWhen`, treat both as one dependency group if they share the same target entity or selected branch. Keep the selector and its gated branch controls in the same section; do not create a separate section that merely mirrors one selector option unless that branch is a genuinely separate product entity with its own workflow evidence.
71
+
70
72
  Before choosing the concrete control type for each target, check `component-rules.md` and `schema-reference.md`. Built-in compound controls must stay compound: for example, typography with font choice, weight, size, color/opacity, and text rhythm uses `fontPicker`, not a plain `select` plus separate inputs/sliders. The product renderer and acceptance rows must cover every semantic value part of the chosen component.
71
73
 
72
74
  For custom renderers, write the Renderer Technique Decision Matrix and Render Pipeline Inventory before code. The implementation plan must map every performance-sensitive control to the pass it invalidates.
@@ -88,6 +90,8 @@ Do not implement a Figma design by eye from an image, screenshot, exported PNG,
88
90
 
89
91
  Use `canvasContent` only for product output: WebGL, Canvas 2D, SVG, DOM product text, shader previews, generated previews, export previews, or product editing handles.
90
92
 
93
+ If upload/import is part of the source-material flow, do not invent a design on the canvas before real content exists. The pre-content canvas stays neutral and runtime-backed; upload affordance belongs in `fileDrop`, not in canvas CTA text, helper copy, fake sample output, decorative placeholders, or agent-made source presets. A default procedural/reference source is allowed only when the prompt or reference explicitly defines it, and the worklog must record that evidence.
94
+
91
95
  ```tsx
92
96
  <ToolcraftApp
93
97
  canvasContent={<ProductRenderer />}
@@ -131,7 +135,7 @@ Animated preview renderers must prioritize viewport interactions. During canvas
131
135
 
132
136
  ## Canvas Sizing And Background
133
137
 
134
- A base/default size in the prompt is the initial output size. It does not remove user-facing size controls. Use `editable-output` unless the reference or product explicitly locks dimensions, and cover any `fixed-output` choice with `canvasSizingCoverage: "fixed-output-size"`.
138
+ A base/default, reference, or fixed-format size in the prompt is the initial output size. It does not remove user-facing size controls. Product-output, exportable, shader, procedural, and reference-clone apps use `editable-output`; keep fixed dimensions as editable `canvas.size` defaults instead of switching to `fixed-output`.
135
139
 
136
140
  Every product app exposes output background controls:
137
141
 
@@ -150,9 +154,9 @@ For complex apps, use schema `settingsTransfer: "auto"` or `true` for settings i
150
154
 
151
155
  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.
152
156
 
153
- 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.
157
+ 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 `1` to `2` without changing visible canvas size; DOM/SVG/vector-native previews should not use it.
154
158
 
155
- 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.
159
+ If a controls panel shows only `Export Settings` and `Import Settings` in the first runtime section, check the canvas sizing decision. Product-output apps need `editable-output`; only intrinsic media and non-product/internal fixed fixtures should omit visible canvas size inputs.
156
160
 
157
161
  For user-edited settings that should survive reload, use schema `persistence` with a stable app-specific key. When localStorage persistence is enabled, acceptance must prove a user setting restores after a real browser reload. Do not use settings import/export as a workaround for broken persistence.
158
162
 
@@ -214,4 +218,4 @@ pnpm dev
214
218
 
215
219
  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.
216
220
 
217
- 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.
221
+ Do not stop existing local servers to free `3002` during a first start. `pnpm dev`, `pnpm preview`, and browser verification prefer `3002`, then automatically use the next free port when it is occupied. When restarting an app server you already started, use `pnpm dev:restart` or `pnpm preview:restart`; restart mode reuses the previously saved app port, stops the listener on that exact port if needed, force-stops it if the port is still occupied, and starts on the same port again.
@@ -12,7 +12,7 @@ If a built-in owner is discovered after a custom workaround, replace the workaro
12
12
 
13
13
  Common exact-owner choices:
14
14
 
15
- - Use `gradient` for adjustable gradients, color transitions, gradient fills, stops, type, and angle. Do not replace it with two `color` controls. The built-in Gradient owns type/angle, the draggable stop track, and the Stops list; the full Gradient control uses content-width internal dividers only when it shares a section with sibling controls, with 18px between each divider and the control content. If Gradient is the first control in that section, only the bottom internal divider renders.
15
+ - Use `gradient` for adjustable gradients, color transitions, gradient fills, stops, type, and angle. Do not replace it with two `color` controls. The built-in Gradient owns type/angle, the draggable stop track, and the Stops list; the full Gradient control uses content-width internal dividers only when it shares a section with sibling controls, with 18px between each divider and the control content. If Gradient is the first control in that section, only the bottom internal divider renders; if it is last, only the top internal divider renders.
16
16
  - Use `fontPicker` for typography that includes font family, weight, size, text case, text color/opacity, letter spacing, or line height.
17
17
  - Use `colorOpacity` when one product entity owns both color and opacity.
18
18
  - Use `rangeSlider` or `rangeInput` for lower/upper bounds or from/to ranges.
@@ -23,9 +23,14 @@ Common exact-owner choices:
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
25
  - Use `collectionActions` for repeatable product entities whose actual item list can grow or shrink, such as colors, glyphs, symbols, points, rules, variants, object entries, or typography style 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`, `rangeInput`, or `fontPicker` before any custom renderer. Use `fontPicker` as the item control when each item is a text style or typography entity; do not split its owned fields into neighboring collection controls.
26
- - `actions` buttons stay beside the label when they fit. If the button group wraps to a second row, the wrapped row starts from the left edge of the control content; do not center it or keep it right-aligned.
26
+ - For a single `actions` button, the control label and the button label must not be identical. Keep the button as the command verb and make the control label a concise one- or two-word context such as `Ink wash`, `Palette action`, or `Current layer`.
27
+ - If an `actions` control has a visible label, the label is always above the buttons. Do not use a side-label layout with buttons on the right.
28
+ - Actions render in 50% cells: one button uses the left half, two buttons fill one row, and larger groups continue in two columns.
29
+ - Do not stretch an odd trailing action full-width or center it; keep it in the left 50% cell.
27
30
  - Use `panelActions` for sticky final product actions such as export, copy, generate, apply, or download.
28
31
 
32
+ When upload/import is part of the source-material flow, `fileDrop` owns the empty/upload state. Do not put a custom pre-upload design, CTA, helper copy, fake sample output, decorative placeholder, or agent-made source preset on the canvas. A default procedural/reference source is allowed only when the prompt or reference explicitly defines it and the worklog records that evidence.
33
+
29
34
  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.
30
35
 
31
36
  For local reset-like `actions`, use product-specific values such as `reset-current-layer`, `reset-palette`, or `reset-current-stop` and handle them through `ToolcraftApp onPanelAction`. Do not use a bare `reset` value unless the action intentionally runs global `controls.reset`.
@@ -33,7 +38,7 @@ For local reset-like `actions`, use product-specific values such as `reset-curre
33
38
  ## Dividers
34
39
 
35
40
  - Full-width dividers belong only to panel sections.
36
- - Large built-in compound controls inside a section render content-width internal dividers only when their parent section contains more than one visible control item. Keep 18px between each rendered internal divider and the compound control content. If the compound control is the first item in that section, render only its bottom internal divider and remove the top internal padding. This applies to `gradient`, `fontPicker`, RGB `curves`, `channelMixer`, and `palette`. Single `curves` are one labeled control and do not render internal dividers.
41
+ - Large built-in compound controls inside a section render content-width internal dividers only when their parent section contains more than one visible control item. Keep 18px between each rendered internal divider and the compound control content. If the 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, render only its top internal divider and remove the bottom internal padding. This applies to `gradient`, `fontPicker`, RGB `curves`, `channelMixer`, and `palette`. Single `curves` are one labeled control and do not render internal dividers.
37
42
  - If a section contains exactly one control, whether simple or compound, render only the parent section dividers.
38
43
  - Do not add full-width borders inside a compound control, and do not put dividers only around an internal subsection such as Gradient Stops.
39
44
  - Small compound fields such as `colorOpacity` and `rangeInput` stay inline fields without section dividers.
@@ -53,7 +58,7 @@ Visual discrete sliders must declare `step`; the runtime derives one marker per
53
58
 
54
59
  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.
55
60
 
56
- 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.
61
+ Use slider `unit` only for real measurement suffixes: `%`, `px`, `°`, `s`, `ms`, `fps`, `rows`, `cols`, or a similarly useful domain unit. Do not use `unit: "x"`; scale, multiplier, intensity, opacity, strength, depth, and shader amount sliders display plain numbers unless a real measurement unit applies. 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`, `8s`); word or acronym units render with a space (`5 cols`, `17 fps`) only when they are truly needed.
57
62
 
58
63
  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.
59
64
 
@@ -61,7 +66,7 @@ Range sliders are always full-width two-thumb controls. Do not put a `rangeSlide
61
66
 
62
67
  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.
63
68
 
64
- Discrete sliders must still drag smoothly. Heavy preview work must be debounced, coalesced, or deferred.
69
+ Discrete sliders must still drag smoothly. Heavy preview work may be coalesced, cached, or split into lightweight live feedback plus heavier refinement, but the canvas/product output must not stay unchanged until pointer release.
65
70
 
66
71
  When a slider or range slider is intentionally unavailable, use schema `disabled: true`. Do not draw custom disabled-looking slider rows or disable only the renderer response while leaving the control active.
67
72
 
@@ -83,6 +88,8 @@ The disabled value is preserved. When the user switches back to a mode where the
83
88
 
84
89
  Use `visibleWhen` instead of `disabledWhen` when a control or section belongs only to another template, type, mode, variant, or count. Example: in a co-brand lockup, `Partner` belongs to text identity mode and `Partner logo` belongs to logo identity mode. For count-controlled banks, hide inactive siblings: if `Shades` is `2`, `Shade 3`, `Shade 4`, and `Shade 5` are not visible. Do not keep inactive controls visible and enabled while making the renderer ignore them.
85
90
 
91
+ If `visibleWhen` or `disabledWhen` points to a selector for the same target entity or selected branch, keep the selector and the dependent controls in the same semantic section. A section that exists only because one selector option is active is not a separate product section just because the branch uses a standalone control. Use one section with conditional controls; split only when the dependent branch is a separate product entity with its own workflow and acceptance evidence.
92
+
86
93
  ## Palette
87
94
 
88
95
  Use `palette` only when the product needs a constrained token palette: family plus shade. It is for design-system color tokens, not for arbitrary color entry.
@@ -93,6 +100,8 @@ Use `color` for free hex colors, `colorOpacity` when opacity belongs to the same
93
100
 
94
101
  Use segmented controls only for compact mode choices that preserve every cell's internal padding.
95
102
 
103
+ Segmented controls are full-width. Do not place `segmented` beside Switch, Color, Select, or another control in a two-column inline row; use `select` when a finite choice must occupy a half-width column.
104
+
96
105
  Limits:
97
106
 
98
107
  - at most four options;
@@ -101,10 +110,18 @@ Limits:
101
110
 
102
111
  If cells clip, collide, lose padding, or force labels into adjacent cells, shorten labels first. If compact labels still fail, use `select`.
103
112
 
113
+ ## Sliders
114
+
115
+ Slider and range slider controls are live canvas controls. Dragging a thumb must update runtime state and product output while the drag is in progress, not only on pointer release, blur, an Apply action, or a final commit. Browser acceptance should drag the real control and prove the canvas/product observable changes during the interaction.
116
+
117
+ If live slider updates are slow, fix the renderer path first: update uniforms or stable buffers, cache decoded media and expensive derived inputs, coalesce preview work to `requestAnimationFrame`, cancel stale async renders, move heavy work off React renders, or change renderer strategy. Only in an extreme measured performance ceiling may the app use a degraded live preview or delayed heavy refinement; even then, the user must see immediate canvas feedback during drag and the worklog must record the evidence.
118
+
104
119
  ## Sections
105
120
 
106
121
  Build controls-panel sections from product entities and workflow stages, not component types. Keep sections discrete: two to seven product controls is the normal size. When a section grows past seven controls or mixes several meanings, split it into specific sections such as `Flow Motion`, `Flow Geometry`, `Letter Burst`, `Shape Colors`, `Logo Glow`, `Logo Plate`, or `Text Block`. Do not reuse the same section title for multiple sections.
107
122
 
123
+ Section splitting must preserve dependency cohesion. A selector that chooses a mode, type, source, variant, or include state stays with the controls it gates when they share the same product entity. Prefer internal compound-control dividers, tighter labels, or a more specific section title before moving a gated branch into a separate section.
124
+
108
125
  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. Do not omit a title on app-authored body sections to avoid naming decisions; choose the nearest honest product context instead.
109
126
 
110
127
  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.
@@ -133,7 +150,7 @@ Use `colorOpacity` when one product entity owns both color and opacity, such as
133
150
 
134
151
  When one short numeric/text field and one plain `color` field configure the same entity, they can share a two-column inline row. Example: `Mask size` and `Color` belong in the same `Mask` row instead of two stacked rows. Do not put `colorOpacity` in inline rows.
135
152
 
136
- Mixed inline rows require label parity: every field in that row has a visible label. The required `Background` section row is the section-title-owned exception: use the switch label `Include` beside the background color parameter with `label: false`. Palette variation color banks are the other exception when the group or section label already names the color bank.
153
+ Mixed inline rows usually require label parity: every field in that row has a visible label. Toggle-plus-parameter rows are the section-owned exception: keep the `switch`/`checkbox` label visible and set the non-toggle parameter to `label: false`; if the parameter label is needed, stack the controls instead. All 50/50 inline rows use the same horizontal column gap as paired `select` controls; do not give toggle-plus-parameter rows a separate wider or narrower gap. The required `Background` section row uses the switch label `Include` beside the background color parameter with `label: false`. Palette variation color banks are the other exception when the group or section label already names the color bank.
137
154
 
138
155
  Renderer-owned output background is a base product control. Use a schema `color` target such as `appearance.background` or `scene.background`, add an `export.includeBackground` control for PNG transparency, and make preview/export read those runtime values. Keep them in one required `Background` section directly before the first export settings section. With PNG export, that first section is `Image Export`; with video-only export, it is `Video Export`. Use one equal-width inline row with `export.includeBackground` on the left and `appearance.background` on the right when no other fit rule is violated. The switch label is `Include`, not `Include background`; the background color control uses `label: false`. Each control occupies one half of the row; do not shrink the toggle column to intrinsic width. `export.includeBackground` controls PNG alpha and live preview product-background visibility through `shouldIncludeToolcraftPreviewBackground(state)`; it must not make the Toolcraft canvas shell/backing or video output transparent. Do not hardcode a configurable background in CSS, Canvas `fillStyle`, or WebGL clear color.
139
156
 
@@ -193,6 +210,10 @@ Use variants by product meaning:
193
210
  - `chromaOffset`: RGB or chromatic offset;
194
211
  - `toneBias`: split-tone, duotone, or color-grading bias.
195
212
 
213
+ Default/spatial vector pads use screen-coordinate movement. Dragging the pad left/up lowers `vector.x` and `vector.y`, so an object on the canvas moves left/up without renderer-side Y inversion. Use `coordinateMode: "cartesian"` only when the product intentionally exposes mathematical Y-up coordinates.
214
+
215
+ Holding Shift while dragging a vector pad locks movement to the dominant axis. Use the built-in `vector` control for constrained two-axis movement instead of creating a custom pad.
216
+
196
217
  Do not add custom vector sizing props. Choose the right number, variant, and section grouping, then let runtime sizing handle the pad.
197
218
 
198
219
  ## Curves
@@ -216,11 +237,11 @@ Acceptance for curves should include an off-center control point near an edge so
216
237
 
217
238
  ## Text And Code
218
239
 
219
- Use `text` for short single-line strings: names, small values, compact prompts, titles, and tokens.
240
+ Use `text` for short single-line strings: button labels, canvas labels, names, small values, compact prompts, titles, captions, badges, and tokens.
220
241
 
221
242
  For `text`, separate content from settings. `commitMode` defaults to `"content"`: content strings such as prompts, names, titles, tokens, and short text update while the user types. 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 are runtime-owned editable-size fields and always commit on blur or Enter.
222
243
 
223
- Use `code` / `CodeTextarea` as the base multiline content editor for any potentially long value: prompts, instructions, JSON, CSS, shader code, scripts, templates, or other structured text. It applies while typing, is capped at 12 visible lines, and long content scrolls inside the textarea instead of making the controls panel taller. Do not name a section `Code` unless the product value is actually code.
244
+ Use `code` / `CodeTextarea` as the base multiline content editor for any potentially long value: long prompts, multiline text, instructions, JSON, CSS, shader code, scripts, templates, or other structured text. It applies while typing, is capped at 12 visible lines, and long content scrolls inside the textarea instead of making the controls panel taller. Do not use it for short one-line button/canvas text such as `Glass`, `Submit`, `Title`, or `Badge`; use `text`. If the default value is short but the intended input is long or structured, make that reason explicit in `description`. Do not name a section `Code` unless the product value is actually code.
224
245
 
225
246
  ## Labels
226
247
 
@@ -240,7 +261,7 @@ Switch and checkbox labels name the setting context, not the action. Do not pref
240
261
 
241
262
  Two adjacent `switch` or `checkbox` controls for the same product entity must share one inline row when every visible label fits without truncation. Use short one- or two-word labels such as `Snap X` and `Snap Y`, or `Glow` and `Loop`. The runtime auto-pairs safe adjacent toggles by target entity; use explicit layout groups only when pairing a toggle with a non-toggle parameter. If either label would truncate in half-width, remove the inline group and let the toggles stack.
242
263
 
243
- A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and the controls edit the same entity. This row is always equal-width: each control occupies one half. Example: `Loop` plus `Duration`, or `Include` plus unlabeled background color inside the required `Background` section. If the section title already names the toggle context, shorten the label instead of repeating the title.
264
+ A single `switch` or `checkbox` may share an inline row with one related parameter control when the toggle label fits and the controls edit the same entity. This row is always equal-width: each control occupies one half, using the same horizontal column gap as a paired `select` row. The non-toggle parameter uses `label: false`; if that parameter label is needed for clarity, stack the controls instead. Example: `Loop` plus an unlabeled duration field, or `Include` plus unlabeled background color inside the required `Background` section. If the section title already names the toggle context, shorten the toggle label instead of repeating the title.
244
265
 
245
266
  ## Layers
246
267
 
@@ -290,11 +311,11 @@ Recalculate settings-transfer eligibility after adding, removing, or reorganizin
290
311
 
291
312
  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.
292
313
 
293
- 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.
314
+ 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, fix the canvas sizing decision instead of adding hand-built size fields. A reference, previous app, fixed-format baseline, or user-provided default size does not justify hiding size controls; keep those dimensions as editable `canvas.size` defaults.
294
315
 
295
316
  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.
296
317
 
297
- 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.
318
+ 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 `2` 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.
298
319
 
299
320
  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.
300
321