@pixel-point/toolcraft 0.0.11 → 0.0.12
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 +2 -1
- package/src/cli.mjs +4 -2
- package/src/cli.test.mjs +5 -2
- package/src/generate.test.mjs +5 -2
- package/templates/runtime/contracts/component-contracts.test.ts +27 -2
- package/templates/runtime/contracts/component-contracts.ts +11 -7
- package/templates/runtime/react/controls-panel.test.tsx +4 -0
- package/templates/starter/AGENTS.md +2 -2
- package/templates/starter/docs/toolcraft/acceptance-testing.md +3 -1
- package/templates/starter/docs/toolcraft/assembly-workflow.md +1 -1
- package/templates/starter/docs/toolcraft/component-rules.md +10 -1
- package/templates/starter/docs/toolcraft/performance.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +1 -1
- package/templates/starter/e2e/app-performance.spec.ts +3 -3
- package/templates/starter/package.json +2 -2
- package/templates/starter/src/app/starter-performance.test.ts +20 -2
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +18 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixel-point/toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@clack/prompts": "^1.6.0",
|
|
27
|
+
"cross-spawn": "^7.0.6",
|
|
27
28
|
"skills": "^1.5.12"
|
|
28
29
|
}
|
|
29
30
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { createRequire } from "node:module";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import readline from "node:readline/promises";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
|
|
7
|
+
import spawn from "cross-spawn";
|
|
8
8
|
import {
|
|
9
9
|
cancel,
|
|
10
10
|
confirm,
|
|
@@ -364,7 +364,7 @@ function runCommand(command, args, options = {}, context = {}) {
|
|
|
364
364
|
const child = spawn(command, args, {
|
|
365
365
|
cwd: options.cwd,
|
|
366
366
|
env: options.env,
|
|
367
|
-
shell:
|
|
367
|
+
shell: options.shell ?? false,
|
|
368
368
|
stdio: options.stdio ?? "inherit",
|
|
369
369
|
});
|
|
370
370
|
|
|
@@ -439,6 +439,7 @@ async function installToolcraftSkills(parsedOptions, targetDir, context = {}) {
|
|
|
439
439
|
...process.env,
|
|
440
440
|
...(context.env ?? {}),
|
|
441
441
|
},
|
|
442
|
+
shell: false,
|
|
442
443
|
stdio: "inherit",
|
|
443
444
|
},
|
|
444
445
|
context,
|
|
@@ -493,6 +494,7 @@ export async function runCreateCommand(parsedOptions, context = {}) {
|
|
|
493
494
|
...process.env,
|
|
494
495
|
...(context.env ?? {}),
|
|
495
496
|
},
|
|
497
|
+
shell: false,
|
|
496
498
|
stdio: "inherit",
|
|
497
499
|
},
|
|
498
500
|
context,
|
package/src/cli.test.mjs
CHANGED
|
@@ -259,7 +259,7 @@ describe("runToolcraftCli", () => {
|
|
|
259
259
|
throwOnError: true,
|
|
260
260
|
toolcraftSkillsSourceDir: skillsSourceDir,
|
|
261
261
|
async runCommand(command, args, options) {
|
|
262
|
-
installs.push({ args, command, cwd: options.cwd });
|
|
262
|
+
installs.push({ args, command, cwd: options.cwd, shell: options.shell });
|
|
263
263
|
},
|
|
264
264
|
});
|
|
265
265
|
|
|
@@ -268,11 +268,13 @@ describe("runToolcraftCli", () => {
|
|
|
268
268
|
args: ["install"],
|
|
269
269
|
command: "pnpm",
|
|
270
270
|
cwd: path.join(tempRoot, "install-app"),
|
|
271
|
+
shell: false,
|
|
271
272
|
},
|
|
272
273
|
{
|
|
273
274
|
args: [skillsCliPath, "add", skillsSourceDir, "--skill", "*", "--yes"],
|
|
274
275
|
command: process.execPath,
|
|
275
276
|
cwd: path.join(tempRoot, "install-app"),
|
|
277
|
+
shell: false,
|
|
276
278
|
},
|
|
277
279
|
]);
|
|
278
280
|
assert.match(stdout.text, /Installing dependencies with pnpm/);
|
|
@@ -308,7 +310,7 @@ describe("runToolcraftCli", () => {
|
|
|
308
310
|
throwOnError: true,
|
|
309
311
|
toolcraftSkillsSourceDir: skillsSourceDir,
|
|
310
312
|
async runCommand(command, args, options) {
|
|
311
|
-
skillsCommands.push({ args, command, cwd: options.cwd });
|
|
313
|
+
skillsCommands.push({ args, command, cwd: options.cwd, shell: options.shell });
|
|
312
314
|
},
|
|
313
315
|
},
|
|
314
316
|
);
|
|
@@ -331,6 +333,7 @@ describe("runToolcraftCli", () => {
|
|
|
331
333
|
],
|
|
332
334
|
command: process.execPath,
|
|
333
335
|
cwd: path.join(tempRoot, "skills-options-app"),
|
|
336
|
+
shell: false,
|
|
334
337
|
},
|
|
335
338
|
]);
|
|
336
339
|
});
|
package/src/generate.test.mjs
CHANGED
|
@@ -93,10 +93,13 @@ describe("generateToolcraft", () => {
|
|
|
93
93
|
packageJson.scripts.test,
|
|
94
94
|
"node scripts/check-toolcraft-docs.mjs && node scripts/check-toolcraft-integrity.mjs && node --test scripts/*.test.mjs && vitest run src --passWithNoTests",
|
|
95
95
|
);
|
|
96
|
-
assert.equal(
|
|
96
|
+
assert.equal(
|
|
97
|
+
packageJson.scripts["test:browser"],
|
|
98
|
+
'playwright install chromium && playwright test --grep-invert "browser perf:"',
|
|
99
|
+
);
|
|
97
100
|
assert.equal(
|
|
98
101
|
packageJson.scripts["test:browser:perf"],
|
|
99
|
-
"playwright install chromium && playwright test
|
|
102
|
+
"playwright install chromium && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
|
|
100
103
|
);
|
|
101
104
|
assert.equal(packageJson.scripts["verify:quick"], "pnpm ai:check && pnpm test");
|
|
102
105
|
assert.equal(packageJson.scripts["verify:ui"], "pnpm test:browser");
|
|
@@ -153,6 +153,28 @@ describe("Toolcraft template component contracts", () => {
|
|
|
153
153
|
expect(contract.aiUsageRules).toContain(
|
|
154
154
|
"Multiple vector controls should live in separate semantic sections unless they intentionally belong to the same entity with other related controls.",
|
|
155
155
|
);
|
|
156
|
+
expect(contract.decisionCatalog?.ownsValueModel).toEqual(
|
|
157
|
+
expect.arrayContaining([
|
|
158
|
+
"user-authored stable position",
|
|
159
|
+
"user-authored stable offset",
|
|
160
|
+
"user-authored stable direction",
|
|
161
|
+
]),
|
|
162
|
+
);
|
|
163
|
+
expect(contract.decisionCatalog?.useWhen).toContain(
|
|
164
|
+
"Use Vector for paired X/Y values only when the user is meant to manually author a stable two-axis product parameter such as position, offset, direction, focus, anchor, light direction, or color-balance movement.",
|
|
165
|
+
);
|
|
166
|
+
expect(contract.decisionCatalog?.doNotReplaceWith).toContain(
|
|
167
|
+
"Do not replace animation, keyboard input, pointer input, physics, timeline phase, velocity, or simulated pose state with Vector just because the internal value has x/y coordinates.",
|
|
168
|
+
);
|
|
169
|
+
expect(contract.decisionCatalog?.acceptableAlternatives).toContain(
|
|
170
|
+
"Use timeline, keyboard/pointer handlers, motion sliders, path/step controls, or renderer simulation state when movement is generated by animation or user input rather than authored as a stable panel value.",
|
|
171
|
+
);
|
|
172
|
+
expect(contract.aiUsageRules).toContain(
|
|
173
|
+
"Use Vector only for user-authored stable two-axis product parameters. Do not expose Vector for current animation state, keyboard movement, pointer movement, physics state, timeline phase, velocity, target pose, current pose, or simulated position/direction.",
|
|
174
|
+
);
|
|
175
|
+
expect(contract.aiUsageRules).toContain(
|
|
176
|
+
"Before adding a Vector control to an animated or interactive product, classify movement ownership as direct-authored, timeline-driven, keyboard/pointer-driven, or simulation-owned. Only direct-authored movement may become a visible Vector control; the other ownership modes stay in renderer/runtime interaction state and use controls such as Speed, Step, Spread, Path, Duration, or Timeline when the user needs tuning.",
|
|
177
|
+
);
|
|
156
178
|
expect(contract.aiUsageRules).toContain(
|
|
157
179
|
'Use variant: "whiteBalance" for temperature/tint pads: X maps cool blue to warm amber, Y maps green to magenta.',
|
|
158
180
|
);
|
|
@@ -178,7 +200,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
178
200
|
'Use coordinateMode: "cartesian" only when the product intentionally exposes mathematical Y-up coordinates instead of canvas/screen movement.',
|
|
179
201
|
);
|
|
180
202
|
expect(contract.aiUsageRules).toContain(
|
|
181
|
-
"Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output.",
|
|
203
|
+
"Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output and that the vector represents a user-authored stable two-axis parameter rather than current animation, input, or simulation state.",
|
|
182
204
|
);
|
|
183
205
|
});
|
|
184
206
|
|
|
@@ -652,7 +674,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
652
674
|
"RangeSlider defaultValue must start with different lower and upper values so the two-thumb control does not collapse into a single-value slider.",
|
|
653
675
|
);
|
|
654
676
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
655
|
-
"Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes
|
|
677
|
+
"Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes, including when values include unit suffixes such as 30%-150% or 30% - 90%; do not create custom parsers for RangeSlider labels.",
|
|
656
678
|
);
|
|
657
679
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
658
680
|
"Visual discrete sliders must still drag smoothly; browser performance tests should use expectToolcraftDiscreteSliderDragSmoothness for real pointer drag.",
|
|
@@ -794,6 +816,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
794
816
|
expect(anchorGrid.aiUsageRules).toContain(
|
|
795
817
|
"AnchorGrid is a position selector; acceptance must prove anchorGrid.position changes product placement, not only selected button state.",
|
|
796
818
|
);
|
|
819
|
+
expect(anchorGrid.decisionCatalog?.acceptableAlternatives).toContain(
|
|
820
|
+
"Use Vector only for stable direct-authored continuous position or direction parameters.",
|
|
821
|
+
);
|
|
797
822
|
expect(channelMixer.aiUsageRules).toContain(
|
|
798
823
|
"ChannelMixer is RGB-specific: it renders R/G/B tabs and Red, Green, Blue sliders for an RGB channel matrix.",
|
|
799
824
|
);
|
|
@@ -115,7 +115,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
115
115
|
"Compact symbol/CSS units render tight, such as 20% – 80% or 12px – 48px; word units render with a space when truly needed.",
|
|
116
116
|
"RangeSlider is always a full-width two-thumb control; never place it in an inline two-column layout group with another slider or range slider.",
|
|
117
117
|
"RangeSlider defaultValue must start with different lower and upper values so the two-thumb control does not collapse into a single-value slider.",
|
|
118
|
-
"Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes
|
|
118
|
+
"Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes, including when values include unit suffixes such as 30%-150% or 30% - 90%; do not create custom parsers for RangeSlider labels.",
|
|
119
119
|
'Specs, plans, and app-schema tests must assert explicit discrete range sliders render as variant: "discrete" with markers derived from min, max, and step.',
|
|
120
120
|
"Visual discrete sliders must still drag smoothly; browser performance tests should use expectToolcraftDiscreteSliderDragSmoothness for real pointer drag.",
|
|
121
121
|
"Use visibleWhen for range sliders that are meaningful only in some mode/type/source/include/count states; inactive branches should disappear so the panel shows only controls usable in the current state.",
|
|
@@ -592,21 +592,23 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
592
592
|
strictness: "exact-owner",
|
|
593
593
|
ownsValueModel: [
|
|
594
594
|
"x/y vector",
|
|
595
|
-
"position",
|
|
596
|
-
"offset",
|
|
597
|
-
"direction",
|
|
595
|
+
"user-authored stable position",
|
|
596
|
+
"user-authored stable offset",
|
|
597
|
+
"user-authored stable direction",
|
|
598
598
|
"focus point",
|
|
599
599
|
"light vector",
|
|
600
600
|
"color balance pad",
|
|
601
601
|
],
|
|
602
602
|
useWhen: [
|
|
603
|
-
"Use Vector for paired X/Y values such as position, offset, direction, focus, anchor, light direction, or color-balance movement.",
|
|
603
|
+
"Use Vector for paired X/Y values only when the user is meant to manually author a stable two-axis product parameter such as position, offset, direction, focus, anchor, light direction, or color-balance movement.",
|
|
604
604
|
],
|
|
605
605
|
doNotReplaceWith: [
|
|
606
606
|
"Do not replace Vector with two unrelated sliders or text inputs when direct two-axis editing is the product interaction.",
|
|
607
|
+
"Do not replace animation, keyboard input, pointer input, physics, timeline phase, velocity, or simulated pose state with Vector just because the internal value has x/y coordinates.",
|
|
607
608
|
],
|
|
608
609
|
acceptableAlternatives: [
|
|
609
610
|
"Use two numeric text fields only when exact numeric entry is the primary product requirement.",
|
|
611
|
+
"Use timeline, keyboard/pointer handlers, motion sliders, path/step controls, or renderer simulation state when movement is generated by animation or user input rather than authored as a stable panel value.",
|
|
610
612
|
],
|
|
611
613
|
layoutConstraints: [
|
|
612
614
|
"One vector renders as a square pad; multiple vectors render compact pads.",
|
|
@@ -619,6 +621,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
619
621
|
"If the controls panel contains exactly one vector control, the runtime renders the vector pad as a square.",
|
|
620
622
|
"If the controls panel contains multiple vector controls, the runtime renders compact vector pads.",
|
|
621
623
|
"Multiple vector controls should live in separate semantic sections unless they intentionally belong to the same entity with other related controls.",
|
|
624
|
+
"Use Vector only for user-authored stable two-axis product parameters. Do not expose Vector for current animation state, keyboard movement, pointer movement, physics state, timeline phase, velocity, target pose, current pose, or simulated position/direction.",
|
|
625
|
+
"Before adding a Vector control to an animated or interactive product, classify movement ownership as direct-authored, timeline-driven, keyboard/pointer-driven, or simulation-owned. Only direct-authored movement may become a visible Vector control; the other ownership modes stay in renderer/runtime interaction state and use controls such as Speed, Step, Spread, Path, Duration, or Timeline when the user needs tuning.",
|
|
622
626
|
'Use variant: "whiteBalance" for temperature/tint pads: X maps cool blue to warm amber, Y maps green to magenta.',
|
|
623
627
|
'Use variant: "colorBalance" for paired color-balance axes such as cyan/red and blue/yellow correction.',
|
|
624
628
|
'Use variant: "chromaOffset" for RGB/chromatic offset vectors where the X/Y movement controls channel separation.',
|
|
@@ -630,7 +634,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
630
634
|
"Holding Shift while dragging a vector pad locks movement to the dominant axis and must not select text or page content; do not build a custom pad just to support axis-constrained movement.",
|
|
631
635
|
'Use coordinateMode: "cartesian" only when the product intentionally exposes mathematical Y-up coordinates instead of canvas/screen movement.',
|
|
632
636
|
"Do not add custom vector sizing props in generated schemas; choose the number, variant, and section grouping from product need and let the runtime size the pads.",
|
|
633
|
-
"Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output.",
|
|
637
|
+
"Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output and that the vector represents a user-authored stable two-axis parameter rather than current animation, input, or simulation state.",
|
|
634
638
|
],
|
|
635
639
|
},
|
|
636
640
|
color: {
|
|
@@ -832,7 +836,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
832
836
|
"Do not use AnchorGrid for freeform two-axis movement; use Vector.",
|
|
833
837
|
],
|
|
834
838
|
acceptableAlternatives: [
|
|
835
|
-
"Use Vector for continuous position or direction.",
|
|
839
|
+
"Use Vector only for stable direct-authored continuous position or direction parameters.",
|
|
836
840
|
],
|
|
837
841
|
layoutConstraints: [
|
|
838
842
|
"AnchorGrid is a standalone position selector.",
|
|
@@ -2782,9 +2782,13 @@ describe("ControlsPanel", () => {
|
|
|
2782
2782
|
|
|
2783
2783
|
for (const [draftValue, expectedValue, expectedLabel] of [
|
|
2784
2784
|
["0/1", '"shape.range":[0,1]', "0% – 1%"],
|
|
2785
|
+
["3/5", '"shape.range":[3,5]', "3% – 5%"],
|
|
2785
2786
|
["1-6", '"shape.range":[1,6]', "1% – 6%"],
|
|
2786
2787
|
["2-3", '"shape.range":[2,3]', "2% – 3%"],
|
|
2787
2788
|
["4 - 5", '"shape.range":[4,5]', "4% – 5%"],
|
|
2789
|
+
["30%-150%", '"shape.range":[30,100]', "30% – 100%"],
|
|
2790
|
+
["30% - 90%", '"shape.range":[30,90]', "30% – 90%"],
|
|
2791
|
+
["30 % - 90 %", '"shape.range":[30,90]', "30% – 90%"],
|
|
2788
2792
|
["6–7", '"shape.range":[6,7]', "6% – 7%"],
|
|
2789
2793
|
["7—8", '"shape.range":[7,8]', "7% – 8%"],
|
|
2790
2794
|
["8−9", '"shape.range":[8,9]', "8% – 9%"],
|
|
@@ -114,7 +114,7 @@ AI must work on this app through the required workflow skills when the environme
|
|
|
114
114
|
- Before editing code from an approved spec, use `writing-plans` to produce a deterministic implementation plan focused on app files, tests, build, and browser verification.
|
|
115
115
|
- Before fixing any broken control, failed test, build failure, visual mismatch, export issue, or runtime regression, use `systematic-debugging` to find the root cause first.
|
|
116
116
|
- When the prompt includes a Figma URL, use Figma MCP/design context before implementation. Read the actual node, layer, component, variable, and asset structure; screenshots are only for final visual QA, not the source of truth.
|
|
117
|
-
- After implementation, use the `browser` workflow or equivalent local browser verification to test the running app, not only typecheck/build output. The default browser gate is `pnpm test:browser`; it excludes
|
|
117
|
+
- After implementation, use the `browser` workflow or equivalent local browser verification to test the running app, not only typecheck/build output. The default browser gate is `pnpm test:browser`; it excludes every Playwright test whose name contains `browser perf:`, including performance audit and budget scenarios. `pnpm test:browser:perf` is reserved for full performance checkpoints.
|
|
118
118
|
- Run `pnpm ai:check` before app generation or major changes.
|
|
119
119
|
- If a required skill is missing and the environment supports skill installation, install it before implementation and restart or refresh the session if the skill list does not update.
|
|
120
120
|
- If skill installation is not available, stop before implementation and tell the user exactly which required skills are missing.
|
|
@@ -168,7 +168,7 @@ For the first working product delivery, run the browser performance checkpoint a
|
|
|
168
168
|
|
|
169
169
|
Use `pnpm install` before this final gate when the folder is fresh or dependencies changed.
|
|
170
170
|
|
|
171
|
-
`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 but must not run
|
|
171
|
+
`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 but must not run any Playwright test whose name contains `browser perf:`. `pnpm verify:perf` / `pnpm test:browser:perf` remains available as the Playwright fallback for the two full-performance triggers and must run the performance audit plus browser budget suite sequentially so budgets are measured without parallel e2e noise.
|
|
172
172
|
|
|
173
173
|
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 only while assigning this app's first saved port. After a saved port exists, normal `pnpm dev` / `pnpm preview` uses that same port; if that port is already serving this app, report that existing URL instead of starting a duplicate. Use `TOOLCRAFT_PORT`, `TOOLCRAFT_DEV_PORT`, or `TOOLCRAFT_TEST_PORT` only to change the preferred starting port before a saved port exists. A dev/preview launch is successful only after the selected port serves this app's Toolcraft server identity endpoint plus the `toolcraft-app-title` marker from `index.html`; never report a URL just because some server is listening there. 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, starts on the same port again, and verifies the identity before saving/reporting the port.
|
|
174
174
|
|
|
@@ -95,6 +95,8 @@ For `curves`, the acceptance row must match the intended variant. Semantic one-d
|
|
|
95
95
|
|
|
96
96
|
For `fontPicker`, product output evidence must come from actual rendered/exported product text after changing the font, weight, size, letter spacing, line height, text case, color, and opacity. Runtime value changes, selected labels, or popup font previews are preflight checks, not final acceptance.
|
|
97
97
|
|
|
98
|
+
For `vector`, acceptance must prove both axes affect output and that the pad represents a user-authored stable two-axis parameter. Do not accept `vector` controls for current animation state, keyboard/pointer movement, physics state, timeline phase, velocity, target pose, current pose, or simulated position/direction; those belong to timeline/input/simulation state plus higher-level tuning controls such as Speed, Step, Spread, Path, Duration, or Timeline.
|
|
99
|
+
|
|
98
100
|
## Control Selection Gates
|
|
99
101
|
|
|
100
102
|
Acceptance must catch wrong-substitution failures. If the prompt, spec, or app behavior needs a value model owned by a built-in control, the schema must use that built-in or include a documented built-in fit check.
|
|
@@ -108,7 +110,7 @@ High-confidence wrong-substitution cases:
|
|
|
108
110
|
- repeatable user-editable item sets without `collectionActions` or another justified collection owner;
|
|
109
111
|
- from/to range without `rangeSlider` or `rangeInput`;
|
|
110
112
|
- curve, remap, easing, or response without `curves`;
|
|
111
|
-
- position, direction, focus, or vector without `vector`;
|
|
113
|
+
- manual stable two-axis position, direction, focus, anchor, light, or vector parameters without `vector`;
|
|
112
114
|
- source upload without `fileDrop`;
|
|
113
115
|
- app-wide transport in the controls panel instead of timeline;
|
|
114
116
|
- segmented choices that clip instead of falling back to `select`;
|
|
@@ -247,6 +247,6 @@ pnpm verify:final
|
|
|
247
247
|
pnpm dev
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
-
Browser verification must use the real Toolcraft shell plus renderer output. `pnpm verify:final` runs the full static, build, and browser functional gate. The browser performance checkpoint is intentionally separate and only runs for the two full-performance triggers; `pnpm verify:perf` is the Playwright fallback command for that checkpoint. `pnpm dev` is intentionally separate because it keeps the local server running.
|
|
250
|
+
Browser verification must use the real Toolcraft shell plus renderer output. `pnpm verify:final` runs the full static, build, and browser functional gate. The default `pnpm test:browser` / `pnpm verify:ui` gate excludes every Playwright test whose name contains `browser perf:`, including performance audit and budget scenarios. The browser performance checkpoint is intentionally separate and only runs for the two full-performance triggers; `pnpm verify:perf` is the Playwright fallback command for that checkpoint. `pnpm dev` is intentionally separate because it keeps the local server running.
|
|
251
251
|
|
|
252
252
|
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 only while assigning the app's first saved port. After that, normal dev/preview starts use the saved port; if that port already serves the same app, report the existing URL instead of creating a second server. A launch is valid only after the selected port serves the current app root through the Toolcraft server identity endpoint and the app title marker from `index.html`; a random listener on that port is not enough. 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, starts on the same port again, and verifies the same app identity before saving/reporting the port.
|
|
@@ -208,13 +208,22 @@ Use variants by product meaning:
|
|
|
208
208
|
- `chromaOffset`: RGB or chromatic offset;
|
|
209
209
|
- `toneBias`: split-tone, duotone, or color-grading bias.
|
|
210
210
|
|
|
211
|
+
Use Vector only when the user is meant to manually author a stable two-axis product parameter. Do not expose a pad for current animation state, keyboard movement, pointer movement, physics state, timeline phase, velocity, target pose, current pose, or simulated position/direction just because the internal value has `x` and `y`.
|
|
212
|
+
|
|
213
|
+
Before adding a Vector control to an animated or interactive product, classify movement ownership:
|
|
214
|
+
|
|
215
|
+
- `direct-authored`: a stable parameter the user manually edits, such as light direction, focus, anchor, or object offset. This can be Vector.
|
|
216
|
+
- `timeline-driven`: movement comes from playback/keyframes. Use timeline, speed, duration, path, step, or amplitude controls instead.
|
|
217
|
+
- `keyboard/pointer-driven`: movement comes from user input on the canvas/app. Keep position/direction in interaction state and expose only useful tuning controls.
|
|
218
|
+
- `simulation-owned`: movement comes from physics/procedural state. Keep current pose/velocity internal and expose high-level tuning controls.
|
|
219
|
+
|
|
211
220
|
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.
|
|
212
221
|
|
|
213
222
|
Vector pad value labels are compact UI labels, not raw state dumps. They show rounded normalized coordinates and must never expose floating-point tails such as `-0.07070312499999998`.
|
|
214
223
|
|
|
215
224
|
Double-clicking the vector pad resets both axes to the control default through the normal runtime value update, matching the reset button in the section header. If no default is defined, the fallback is `0,0`. Do not add a separate custom reset button for basic pad reset behavior.
|
|
216
225
|
|
|
217
|
-
Holding Shift while dragging a vector pad locks movement to the dominant axis and must not select text or page content. Use the built-in `vector` control for constrained two-axis
|
|
226
|
+
Holding Shift while dragging a vector pad locks movement to the dominant axis and must not select text or page content. Use the built-in `vector` control for constrained two-axis direct-authored parameters instead of creating a custom pad.
|
|
218
227
|
|
|
219
228
|
Do not add custom vector sizing props. Choose the right number, variant, and section grouping, then let runtime sizing handle the pad.
|
|
220
229
|
|
|
@@ -167,7 +167,7 @@ Animation checks should sample enough frames to catch jank. Interaction budgets
|
|
|
167
167
|
|
|
168
168
|
Use `app-performance.ts` as the single budget and fixture source. Browser performance tests must call `getToolcraftPerformanceStressValue(appPerformance, scenarioId)` for workload values and `expectToolcraftScenarioPerformanceBudget(..., appPerformance, scenarioId)` for budgets.
|
|
169
169
|
|
|
170
|
-
Run targeted performance scenarios for Tier 3 performance-sensitive edits when they directly exercise the touched workload, viewport, or export path. The full performance checkpoint prefers the current AI agent's controlled browser. `pnpm verify:perf` is the Playwright fallback suite; it runs
|
|
170
|
+
Run targeted performance scenarios for Tier 3 performance-sensitive edits when they directly exercise the touched workload, viewport, or export path. The default `pnpm test:browser` / `pnpm verify:ui` gate excludes every Playwright test whose name contains `browser perf:`, including the performance audit and budget scenarios. The full performance checkpoint prefers the current AI agent's controlled browser. `pnpm verify:perf` is the Playwright fallback suite; it runs every `browser perf:` scenario with one worker so budget failures are not hidden or created by unrelated parallel browser tests.
|
|
171
171
|
|
|
172
172
|
Run a full performance checkpoint only when:
|
|
173
173
|
|
|
@@ -201,7 +201,7 @@ For `slider` and `rangeSlider`, `unit` is a real measurement suffix, not the ent
|
|
|
201
201
|
|
|
202
202
|
`slider` and `rangeSlider` are live controls. Dragging must update runtime state and product output while the drag is in progress, not only on pointer release, blur, Apply, or a final commit. Treat a non-live slider as a broken product mapping unless an extreme measured performance ceiling is documented; even then, keep immediate lightweight canvas feedback and refine the heavy output after coalescing/caching.
|
|
203
203
|
|
|
204
|
-
For `vector`, the default/spatial variant uses `coordinateMode: "screen"` by default: dragging left/up lowers `x` and `y`, so canvas objects move left/up without renderer-side Y inversion. Use `coordinateMode: "cartesian"` only for intentional mathematical Y-up coordinates. Color variants keep their color-axis semantics by default. Vector pad value labels show compact rounded coordinates; raw floating-point tails must never appear in the controls panel. Double-clicking the pad resets both axes to the control default through the normal runtime value update, matching the section header reset; if no default is defined, the fallback is `0,0`. Holding Shift while dragging locks movement to the dominant axis and must not select text or page content.
|
|
204
|
+
For `vector`, the default/spatial variant uses `coordinateMode: "screen"` by default: dragging left/up lowers `x` and `y`, so canvas objects move left/up without renderer-side Y inversion. Use it only for stable user-authored two-axis parameters. Do not expose Vector for current animation state, keyboard movement, pointer movement, physics state, timeline phase, velocity, target pose, current pose, or simulated position/direction just because the internal value has `x` and `y`. Use `coordinateMode: "cartesian"` only for intentional mathematical Y-up coordinates. Color variants keep their color-axis semantics by default. Vector pad value labels show compact rounded coordinates; raw floating-point tails must never appear in the controls panel. Double-clicking the pad resets both axes to the control default through the normal runtime value update, matching the section header reset; if no default is defined, the fallback is `0,0`. Holding Shift while dragging locks movement to the dominant axis and must not select text or page content.
|
|
205
205
|
|
|
206
206
|
Use `collectionActions` when the product owns a growable/shrinkable item list. `minItems` protects the smallest valid output, `recommendedMaxItems` is only a design recommendation, and `hardMaxItems` is valid only for a real product or technical limit. Adding/removing items must update the runtime array and the renderer/export must consume that same array. Do not pair a count slider with hidden fixed item controls when the user needs to add or remove actual entities. The collection label is on the left and remove/add buttons stay on the right. Homogeneous repeated items do not show visible per-item labels when the collection label already names the group. `itemControl.type` supports normal item built-ins such as `color`, `colorOpacity`, `text`, `select`, `segmented`, `slider`, `switch`, `checkbox`, `rangeInput`, and `fontPicker`; item controls still follow normal density rules, so plain colors use equal 50% columns when they fit. Use `fontPicker` as the item control when each repeated item is a text style or typography entity; do not split its font, weight, size, case, color/opacity, letter spacing, or line height into sibling collection fields.
|
|
207
207
|
|
|
@@ -184,7 +184,7 @@ function getFirstMatchIndex(source: string, pattern: RegExp): number {
|
|
|
184
184
|
return match?.index ?? -1;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
test("browser performance matrix points at real fallback browser tests", () => {
|
|
187
|
+
test("browser perf: performance matrix points at real fallback browser tests", () => {
|
|
188
188
|
const browserTestSources = readSiblingBrowserTestSources();
|
|
189
189
|
|
|
190
190
|
for (const scenario of starterPerformance.scenarios) {
|
|
@@ -199,7 +199,7 @@ test("browser performance matrix points at real fallback browser tests", () => {
|
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
test("browser performance tests use real Toolcraft interactions", () => {
|
|
202
|
+
test("browser perf: performance tests use real Toolcraft interactions", () => {
|
|
203
203
|
if (starterPerformance.scenarios.length === 0) {
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
@@ -493,7 +493,7 @@ test("browser performance tests use real Toolcraft interactions", () => {
|
|
|
493
493
|
}
|
|
494
494
|
});
|
|
495
495
|
|
|
496
|
-
test("declared renderer layer selectors are present", async ({ page }) => {
|
|
496
|
+
test("browser perf: declared renderer layer selectors are present", async ({ page }) => {
|
|
497
497
|
if (!starterPerformance.usesCustomRenderer) {
|
|
498
498
|
return;
|
|
499
499
|
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"preview:restart": "node scripts/run-vite-on-free-port.mjs preview --toolcraft-restart",
|
|
13
13
|
"docs:check": "node scripts/check-toolcraft-docs.mjs",
|
|
14
14
|
"test": "node scripts/check-toolcraft-docs.mjs && node scripts/check-toolcraft-integrity.mjs && node --test scripts/*.test.mjs && vitest run src --passWithNoTests",
|
|
15
|
-
"test:browser": "playwright install chromium && playwright test",
|
|
16
|
-
"test:browser:perf": "playwright install chromium && playwright test
|
|
15
|
+
"test:browser": "playwright install chromium && playwright test --grep-invert \"browser perf:\"",
|
|
16
|
+
"test:browser:perf": "playwright install chromium && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
|
|
17
17
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
18
|
"verify:quick": "pnpm ai:check && pnpm test",
|
|
19
19
|
"verify:ui": "pnpm test:browser",
|
|
@@ -317,16 +317,20 @@ function sourceUsesDirectStorageApi(): boolean {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
describe("Toolcraft template app performance coverage", () => {
|
|
320
|
-
it("publishes
|
|
320
|
+
it("publishes separate browser acceptance and performance fallback gates", () => {
|
|
321
321
|
const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf8")) as {
|
|
322
322
|
scripts?: Record<string, string>;
|
|
323
323
|
};
|
|
324
324
|
|
|
325
|
+
expect(
|
|
326
|
+
packageJson.scripts?.["test:browser"],
|
|
327
|
+
"Generated apps must keep full performance scenarios out of the default browser acceptance gate.",
|
|
328
|
+
).toBe('playwright install chromium && playwright test --grep-invert "browser perf:"');
|
|
325
329
|
expect(
|
|
326
330
|
packageJson.scripts?.["test:browser:perf"],
|
|
327
331
|
"Generated apps must expose a sequential Playwright fallback so perf budgets can be measured when no agent browser is available.",
|
|
328
332
|
).toBe(
|
|
329
|
-
'playwright install chromium && playwright test
|
|
333
|
+
'playwright install chromium && playwright test --grep "browser perf:" --workers=1 --pass-with-no-tests',
|
|
330
334
|
);
|
|
331
335
|
expect(packageJson.scripts?.["verify:quick"]).toBe("pnpm ai:check && pnpm test");
|
|
332
336
|
expect(packageJson.scripts?.["verify:ui"]).toBe("pnpm test:browser");
|
|
@@ -339,6 +343,20 @@ describe("Toolcraft template app performance coverage", () => {
|
|
|
339
343
|
);
|
|
340
344
|
});
|
|
341
345
|
|
|
346
|
+
it("keeps the Playwright performance audit inside the browser perf tag", () => {
|
|
347
|
+
const source = readFileSync(join(e2eDir, "app-performance.spec.ts"), "utf8");
|
|
348
|
+
const testNames = [...source.matchAll(/test\(\s*(["'`])([^"'`]+)\1/g)].map(
|
|
349
|
+
(match) => match[2],
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
expect(source).not.toContain("TOOLCRAFT_PERF_CHECK");
|
|
353
|
+
expect(testNames.length).toBeGreaterThan(0);
|
|
354
|
+
expect(
|
|
355
|
+
testNames.every((name) => name.includes("browser perf:")),
|
|
356
|
+
`app-performance.spec.ts tests must all be tagged for the dedicated perf checkpoint: ${testNames.join(", ")}`,
|
|
357
|
+
).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
|
|
342
360
|
it("declares agent browser as the preferred performance runner", () => {
|
|
343
361
|
expect(starterPerformance.browserCheckPolicy).toEqual({
|
|
344
362
|
fallbackRunner: "playwright",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { applySliderValueLabelUnit } from "../slider/slider-value";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const signedNumberPattern = "[-−]?\\d+(?:[.,]\\d+)?";
|
|
4
|
+
const rangePairPattern = new RegExp(
|
|
5
|
+
`(${signedNumberPattern})(?:\\s*[\\p{L}%°]+)?\\s*(?:/|[-‐‑‒–—−])\\s*(${signedNumberPattern})`,
|
|
6
|
+
"u",
|
|
7
|
+
);
|
|
4
8
|
|
|
5
9
|
export function formatRangeSliderValue(
|
|
6
10
|
value: readonly number[],
|
|
@@ -46,11 +50,15 @@ export function parseRangeSliderDraft(
|
|
|
46
50
|
step: number;
|
|
47
51
|
},
|
|
48
52
|
): [number, number] | null {
|
|
49
|
-
const
|
|
50
|
-
const values =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const pairMatch = draftValue.match(rangePairPattern);
|
|
54
|
+
const values = pairMatch
|
|
55
|
+
? [pairMatch[1], pairMatch[2]]
|
|
56
|
+
.map((match) => parseRangeSliderDraftNumber(match))
|
|
57
|
+
.filter(Number.isFinite)
|
|
58
|
+
: Array.from(
|
|
59
|
+
draftValue.matchAll(/[-−]?\d+(?:[.,]\d+)?/g),
|
|
60
|
+
([match]) => parseRangeSliderDraftNumber(match),
|
|
61
|
+
).filter(Number.isFinite);
|
|
54
62
|
|
|
55
63
|
if (values.length === 0) {
|
|
56
64
|
return null;
|
|
@@ -64,3 +72,7 @@ export function parseRangeSliderDraft(
|
|
|
64
72
|
|
|
65
73
|
return sortedValues;
|
|
66
74
|
}
|
|
75
|
+
|
|
76
|
+
function parseRangeSliderDraftNumber(value: string | undefined): number {
|
|
77
|
+
return Number(value?.replace("−", "-").replace(",", "."));
|
|
78
|
+
}
|