@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-point/toolcraft",
3
- "version": "0.0.11",
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: process.platform === "win32",
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
  });
@@ -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(packageJson.scripts["test:browser"], "playwright install chromium && playwright test");
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 e2e/app-performance.spec.ts --workers=1 && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
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; do not create custom parsers for RangeSlider labels.",
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; do not create custom parsers for RangeSlider labels.",
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 `browser perf:` budget scenarios and leaves the performance audit disabled unless the perf checkpoint runner sets `TOOLCRAFT_PERF_CHECK=1`. `pnpm test:browser:perf` is reserved for full performance checkpoints.
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 `browser perf:` budget scenarios or the performance audit. `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.
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 movement instead of creating a custom pad.
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 `e2e/app-performance.spec.ts` and every `browser perf:` scenario with one worker so budget failures are not hidden or created by unrelated parallel browser tests.
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 e2e/app-performance.spec.ts --workers=1 && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
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 a sequential Playwright browser performance fallback", () => {
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 e2e/app-performance.spec.ts --workers=1 && playwright test --grep "browser perf:" --workers=1 --pass-with-no-tests',
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 rangeSeparatorPattern = /(\d)\s*[-‐‑‒–—−]\s*(?=\d)/g;
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 normalizedDraftValue = draftValue.replace(rangeSeparatorPattern, "$1 ");
50
- const values = Array.from(
51
- normalizedDraftValue.matchAll(/-?\d+(?:[.,]\d+)?/g),
52
- ([match]) => Number(match.replace(",", ".")),
53
- ).filter(Number.isFinite);
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
+ }