@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.
- package/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +104 -14
- package/templates/runtime/contracts/component-contracts.ts +54 -22
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/react/controls-panel.test.tsx +374 -13
- package/templates/runtime/react/controls-panel.tsx +65 -4
- package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
- package/templates/runtime/schema/define-toolcraft.ts +25 -1
- package/templates/runtime/schema/types.ts +3 -0
- package/templates/runtime/testing/performance.test.ts +134 -0
- package/templates/runtime/testing/performance.ts +34 -4
- package/templates/starter/AGENTS.md +4 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
- package/templates/starter/docs/toolcraft/component-rules.md +32 -11
- package/templates/starter/docs/toolcraft/performance.md +7 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +2 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +739 -66
- package/templates/starter/src/app/starter-acceptance.ts +471 -12
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/index.ts +2 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
- 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
|
-
(
|
|
1594
|
-
scenario.interaction === "control-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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"`
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 `
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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 `
|
|
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
|
|