@pixel-point/toolcraft 0.0.11 → 0.0.13

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.
@@ -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",
@@ -34,6 +34,7 @@
34
34
  "class-variance-authority": "^0.7.1",
35
35
  "clsx": "^2.1.1",
36
36
  "cmdk": "^1.1.1",
37
+ "cross-spawn": "^7.0.6",
37
38
  "lucide-react": "^0.511.0",
38
39
  "motion": "^11.16.3",
39
40
  "react": "^19.2.0",
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from "node:child_process";
3
+ import { access } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import spawn from "cross-spawn";
4
8
 
5
9
  import {
6
10
  findAvailablePort,
@@ -14,6 +18,8 @@ import {
14
18
  } from "./toolcraft-port.mjs";
15
19
 
16
20
  const viteCommand = process.argv[2] ?? "dev";
21
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
22
+ const projectDir = join(scriptDir, "..");
17
23
  const restartArg = "--toolcraft-restart";
18
24
  const rawPassthroughArgs = process.argv.slice(3).filter((arg) => arg !== "--");
19
25
  const restartSamePort =
@@ -49,7 +55,7 @@ if (savedPort) {
49
55
  process.exit(0);
50
56
  } catch (error) {
51
57
  console.error(
52
- `Saved Toolcraft port ${savedPort} is busy, but it is not serving this app. Use pnpm dev:restart or pnpm preview:restart to reclaim the saved port, or remove .toolcraft/server-port.json to assign a new port. ${error?.message ?? ""}`.trim(),
58
+ `Saved Toolcraft port ${savedPort} is busy, but it is not serving this app. Use the restart script to reclaim the saved port, or remove .toolcraft/server-port.json to assign a new port. ${error?.message ?? ""}`.trim(),
53
59
  );
54
60
  process.exit(1);
55
61
  }
@@ -66,12 +72,25 @@ if (!port) {
66
72
  }
67
73
  }
68
74
 
69
- const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
75
+ const viteBinPath = join(
76
+ projectDir,
77
+ "node_modules",
78
+ ".bin",
79
+ process.platform === "win32" ? "vite.cmd" : "vite",
80
+ );
81
+
82
+ try {
83
+ await access(viteBinPath);
84
+ } catch {
85
+ console.error(
86
+ "Vite is not installed. Run this app's dependency installation command before starting the dev server.",
87
+ );
88
+ process.exit(1);
89
+ }
90
+
70
91
  const child = spawn(
71
- pnpmCommand,
92
+ viteBinPath,
72
93
  [
73
- "exec",
74
- "vite",
75
94
  viteCommand,
76
95
  "--host",
77
96
  "127.0.0.1",
@@ -554,8 +554,12 @@ function escapeRegExp(value: string): string {
554
554
 
555
555
  const incompleteRequiredCheckPattern =
556
556
  /\b(?:fail(?:ed|s|ing)?|not complete|not completed|incomplete|pending|blocked|planned|todo|to do|to run|will run|not run|not started|not yet|queued)\b/i;
557
+ const packageScriptPrefixPattern = String.raw`(?:pnpm\s+|npm\s+run\s+)?`;
557
558
  const requiredCheckNamePattern =
558
- /\b(?:(?:pnpm\s+)?(?:verify:final|verify:perf|test:browser:perf|test:browser|verify:quick|test|build)|browser performance checkpoint|agent-browser|playwright-fallback)\b/i;
559
+ new RegExp(
560
+ String.raw`\b(?:${packageScriptPrefixPattern}(?:verify:final|verify:perf|test:browser:perf|test:browser|verify:quick|test|build)|browser performance checkpoint|agent-browser|playwright-fallback)\b`,
561
+ "i",
562
+ );
559
563
  const browserPerformanceCheckpointPattern =
560
564
  /\b(?:agent-browser|browser performance checkpoint|performance checkpoint|verify:perf|test:browser:perf|playwright-fallback)\b/i;
561
565
  const fallbackPerfReasonPattern =
@@ -599,7 +603,7 @@ function isPassedAgentBrowserPerfRunLine(line: string): boolean {
599
603
  function isPassedPlaywrightFallbackPerfRunLine(line: string): boolean {
600
604
  return (
601
605
  isVerificationEvidenceLine(line) &&
602
- (/\bpnpm\s+verify:perf\b/i.test(line) ||
606
+ (/\b(?:pnpm\s+verify:perf|npm\s+run\s+verify:perf|verify:perf)\b/i.test(line) ||
603
607
  (/\bplaywright-fallback\b/i.test(line) &&
604
608
  /\b(?:performance|perf|checkpoint)\b/i.test(line))) &&
605
609
  !isIncompleteOrSkippedLine(line)
@@ -643,7 +647,7 @@ function getAgentWorklogVerificationGateErrors(source: string): string[] {
643
647
 
644
648
  if (!hasPassedPerfRun && !hasAllowedPerfSkip) {
645
649
  errors.push(
646
- "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
650
+ "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
647
651
  );
648
652
  }
649
653
 
@@ -721,7 +725,11 @@ function getAgentWorklogValidationErrors(source: string): string[] {
721
725
  );
722
726
  }
723
727
 
724
- if (!/\bpnpm\s+(verify|test|build|typecheck)|browser|Playwright|perf/i.test(verificationBody)) {
728
+ if (
729
+ !/\b(?:pnpm\s+(?:verify|test|build|typecheck)|npm\s+run\s+(?:verify|test|build|typecheck)|browser|Playwright|perf)\b/i.test(
730
+ verificationBody,
731
+ )
732
+ ) {
725
733
  errors.push("agent-worklog.md Verification must list concrete test/build/browser/performance checks.");
726
734
  }
727
735
 
@@ -3689,7 +3697,7 @@ describe("Toolcraft template app acceptance coverage", () => {
3689
3697
  expect.arrayContaining([
3690
3698
  "agent-worklog.md required checks must be passed before final delivery; do not report failed, incomplete, pending, or blocked verification.",
3691
3699
  "agent-worklog.md must not list required checks as skipped unless they are explicitly not required for a post-first-working non-performance edit.",
3692
- "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
3700
+ "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
3693
3701
  ]),
3694
3702
  );
3695
3703
  });
@@ -3775,7 +3783,7 @@ describe("Toolcraft template app acceptance coverage", () => {
3775
3783
  `;
3776
3784
 
3777
3785
  expect(getAgentWorklogValidationErrors(missingPerfWorklog)).toContain(
3778
- "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
3786
+ "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
3779
3787
  );
3780
3788
  });
3781
3789
 
@@ -3992,7 +4000,7 @@ describe("Toolcraft template app acceptance coverage", () => {
3992
4000
  `;
3993
4001
 
3994
4002
  expect(getAgentWorklogValidationErrors(missingFallbackReasonWorklog)).toContain(
3995
- "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or `pnpm verify:perf` with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
4003
+ "agent-worklog.md Verification must record the browser performance checkpoint for first working product delivery: use agent-browser when available, or this app's verify:perf script with an explicit fallback reason when no agent browser or CI/non-agent automation is used.",
3996
4004
  );
3997
4005
  });
3998
4006
 
@@ -317,26 +317,54 @@ 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
+ const runScriptPattern = (scriptName: string) =>
325
+ `(?:pnpm ${scriptName}|npm run ${scriptName})`;
324
326
 
327
+ expect(
328
+ packageJson.scripts?.["test:browser"],
329
+ "Generated apps must keep full performance scenarios out of the default browser acceptance gate.",
330
+ ).toBe('playwright install chromium && playwright test --grep-invert "browser perf:"');
325
331
  expect(
326
332
  packageJson.scripts?.["test:browser:perf"],
327
333
  "Generated apps must expose a sequential Playwright fallback so perf budgets can be measured when no agent browser is available.",
328
334
  ).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',
335
+ 'playwright install chromium && playwright test --grep "browser perf:" --workers=1 --pass-with-no-tests',
336
+ );
337
+ expect(packageJson.scripts?.["verify:quick"]).toMatch(
338
+ new RegExp(`^${runScriptPattern("ai:check")} && ${runScriptPattern("test")}$`),
339
+ );
340
+ expect(packageJson.scripts?.["verify:ui"]).toMatch(
341
+ new RegExp(`^${runScriptPattern("test:browser")}$`),
342
+ );
343
+ expect(packageJson.scripts?.["verify:perf"]).toMatch(
344
+ new RegExp(`^${runScriptPattern("test:browser:perf")}$`),
345
+ );
346
+ expect(packageJson.scripts?.["verify:perf:playwright"]).toMatch(
347
+ new RegExp(`^${runScriptPattern("test:browser:perf")}$`),
330
348
  );
331
- expect(packageJson.scripts?.["verify:quick"]).toBe("pnpm ai:check && pnpm test");
332
- expect(packageJson.scripts?.["verify:ui"]).toBe("pnpm test:browser");
333
- expect(packageJson.scripts?.["verify:perf"]).toBe("pnpm test:browser:perf");
334
- expect(packageJson.scripts?.["verify:perf:playwright"]).toBe(
335
- "pnpm test:browser:perf",
349
+ expect(packageJson.scripts?.["verify:final"]).toMatch(
350
+ new RegExp(
351
+ `^${runScriptPattern("ai:check")} && ${runScriptPattern("test")} && ${runScriptPattern("build")} && ${runScriptPattern("test:browser")}$`,
352
+ ),
336
353
  );
337
- expect(packageJson.scripts?.["verify:final"]).toBe(
338
- "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
354
+ });
355
+
356
+ it("keeps the Playwright performance audit inside the browser perf tag", () => {
357
+ const source = readFileSync(join(e2eDir, "app-performance.spec.ts"), "utf8");
358
+ const testNames = [...source.matchAll(/test\(\s*(["'`])([^"'`]+)\1/g)].map(
359
+ (match) => match[2],
339
360
  );
361
+
362
+ expect(source).not.toContain("TOOLCRAFT_PERF_CHECK");
363
+ expect(testNames.length).toBeGreaterThan(0);
364
+ expect(
365
+ testNames.every((name) => name.includes("browser perf:")),
366
+ `app-performance.spec.ts tests must all be tagged for the dedicated perf checkpoint: ${testNames.join(", ")}`,
367
+ ).toBe(true);
340
368
  });
341
369
 
342
370
  it("declares agent browser as the preferred performance runner", () => {
@@ -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
+ }