@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.
- package/README.md +4 -2
- package/package.json +2 -1
- package/src/cli.mjs +19 -44
- package/src/cli.test.mjs +146 -8
- package/src/command-runner.mjs +71 -0
- package/src/dependency-install.mjs +87 -0
- package/src/generate.mjs +32 -4
- package/src/generate.test.mjs +72 -11
- package/src/package-json.mjs +11 -2
- package/src/package-json.test.mjs +27 -0
- package/src/package-manager.mjs +123 -0
- package/src/package-manager.test.mjs +80 -0
- package/templates/runtime/contracts/component-contracts.test.ts +28 -3
- package/templates/runtime/contracts/component-contracts.ts +12 -8
- 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 +3 -2
- package/templates/starter/scripts/run-vite-on-free-port.mjs +25 -6
- package/templates/starter/src/app/starter-acceptance.test.ts +15 -7
- package/templates/starter/src/app/starter-performance.test.ts +37 -9
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +18 -6
package/src/generate.test.mjs
CHANGED
|
@@ -73,6 +73,7 @@ describe("generateToolcraft", () => {
|
|
|
73
73
|
assert.equal(packageJson.dependencies["@repo/toolcraft-runtime"], undefined);
|
|
74
74
|
assert.equal(packageJson.dependencies["@tanstack/react-router"], "1.170.6");
|
|
75
75
|
assert.equal(packageJson.dependencies.cmdk, "^1.1.1");
|
|
76
|
+
assert.equal(packageJson.dependencies["cross-spawn"], "^7.0.6");
|
|
76
77
|
assert.equal(packageJson.dependencies["react-resizable-panels"], "^4.10.0");
|
|
77
78
|
assert.equal(packageJson.dependencies.sonner, "^2.0.7");
|
|
78
79
|
assert.equal(packageJson.devDependencies["@repo/typescript-config"], undefined);
|
|
@@ -93,18 +94,21 @@ describe("generateToolcraft", () => {
|
|
|
93
94
|
packageJson.scripts.test,
|
|
94
95
|
"node scripts/check-toolcraft-docs.mjs && node scripts/check-toolcraft-integrity.mjs && node --test scripts/*.test.mjs && vitest run src --passWithNoTests",
|
|
95
96
|
);
|
|
96
|
-
assert.equal(
|
|
97
|
+
assert.equal(
|
|
98
|
+
packageJson.scripts["test:browser"],
|
|
99
|
+
'playwright install chromium && playwright test --grep-invert "browser perf:"',
|
|
100
|
+
);
|
|
97
101
|
assert.equal(
|
|
98
102
|
packageJson.scripts["test:browser:perf"],
|
|
99
|
-
"playwright install chromium && playwright test
|
|
103
|
+
"playwright install chromium && playwright test --grep \"browser perf:\" --workers=1 --pass-with-no-tests",
|
|
100
104
|
);
|
|
101
|
-
assert.equal(packageJson.scripts["verify:quick"], "
|
|
102
|
-
assert.equal(packageJson.scripts["verify:ui"], "
|
|
103
|
-
assert.equal(packageJson.scripts["verify:perf"], "
|
|
104
|
-
assert.equal(packageJson.scripts["verify:perf:playwright"], "
|
|
105
|
+
assert.equal(packageJson.scripts["verify:quick"], "npm run ai:check && npm run test");
|
|
106
|
+
assert.equal(packageJson.scripts["verify:ui"], "npm run test:browser");
|
|
107
|
+
assert.equal(packageJson.scripts["verify:perf"], "npm run test:browser:perf");
|
|
108
|
+
assert.equal(packageJson.scripts["verify:perf:playwright"], "npm run test:browser:perf");
|
|
105
109
|
assert.equal(
|
|
106
110
|
packageJson.scripts["verify:final"],
|
|
107
|
-
"
|
|
111
|
+
"npm run ai:check && npm run test && npm run build && npm run test:browser",
|
|
108
112
|
);
|
|
109
113
|
|
|
110
114
|
const indexHtmlSource = await fs.readFile(path.join(targetDir, "index.html"), "utf8");
|
|
@@ -142,6 +146,15 @@ describe("generateToolcraft", () => {
|
|
|
142
146
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/check-toolcraft-docs.mjs")));
|
|
143
147
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/check-toolcraft-integrity.mjs")));
|
|
144
148
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/run-vite-on-free-port.mjs")));
|
|
149
|
+
const runViteSource = await fs.readFile(
|
|
150
|
+
path.join(targetDir, "scripts/run-vite-on-free-port.mjs"),
|
|
151
|
+
"utf8",
|
|
152
|
+
);
|
|
153
|
+
assert.match(runViteSource, /node_modules/);
|
|
154
|
+
assert.match(runViteSource, /\.bin/);
|
|
155
|
+
assert.match(runViteSource, /vite\.cmd/);
|
|
156
|
+
assert.match(runViteSource, /from "cross-spawn"/);
|
|
157
|
+
assert.doesNotMatch(runViteSource, /pnpmCommand|pnpm\.cmd|pnpm",/);
|
|
145
158
|
assert.ok(await fs.stat(path.join(targetDir, "LICENSE.md")));
|
|
146
159
|
assert.ok(await fs.stat(path.join(targetDir, "NOTICE.md")));
|
|
147
160
|
assert.ok(await fs.stat(path.join(targetDir, "docs/toolcraft/README.md")));
|
|
@@ -229,14 +242,14 @@ describe("generateToolcraft", () => {
|
|
|
229
242
|
assert.match(agentsSource, /Required AI Workflow Skills/);
|
|
230
243
|
assert.match(agentsSource, /Verification Tier Classifier/);
|
|
231
244
|
assert.match(agentsSource, /Verification tier: Tier N/);
|
|
232
|
-
assert.match(agentsSource, /
|
|
233
|
-
assert.match(agentsSource, /
|
|
234
|
-
assert.match(agentsSource, /
|
|
245
|
+
assert.match(agentsSource, /npm run verify:quick/);
|
|
246
|
+
assert.match(agentsSource, /npm run verify:final/);
|
|
247
|
+
assert.match(agentsSource, /npm run ai:check/);
|
|
235
248
|
assert.match(agentsSource, /use `brainstorming`/);
|
|
236
249
|
assert.match(agentsSource, /use `writing-plans`/);
|
|
237
250
|
assert.match(agentsSource, /use `systematic-debugging`/);
|
|
238
251
|
assert.match(agentsSource, /browser` workflow/);
|
|
239
|
-
assert.match(agentsSource, /
|
|
252
|
+
assert.match(agentsSource, /npm run test:browser/);
|
|
240
253
|
assert.match(agentsSource, /Do not silently skip required workflow skills/);
|
|
241
254
|
assert.match(agentsSource, /Toolcraft app contract overrides generic brainstorming approval/);
|
|
242
255
|
assert.match(agentsSource, /Do not ask the user to confirm decisions already covered/);
|
|
@@ -343,6 +356,54 @@ describe("generateToolcraft", () => {
|
|
|
343
356
|
);
|
|
344
357
|
});
|
|
345
358
|
|
|
359
|
+
it("generates package-manager-specific commands for pnpm apps", async () => {
|
|
360
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-cli-"));
|
|
361
|
+
tempRoots.push(tempRoot);
|
|
362
|
+
const targetDir = path.join(tempRoot, "pnpm-app");
|
|
363
|
+
|
|
364
|
+
await generateToolcraft({
|
|
365
|
+
cwd: tempRoot,
|
|
366
|
+
force: true,
|
|
367
|
+
name: "Pnpm App",
|
|
368
|
+
packageManager: "pnpm",
|
|
369
|
+
targetDir: "pnpm-app",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(targetDir, "package.json"), "utf8"));
|
|
373
|
+
assert.equal(packageJson.scripts["verify:quick"], "pnpm ai:check && pnpm test");
|
|
374
|
+
assert.equal(packageJson.scripts["verify:ui"], "pnpm test:browser");
|
|
375
|
+
assert.equal(packageJson.scripts["verify:perf"], "pnpm test:browser:perf");
|
|
376
|
+
assert.equal(packageJson.scripts["verify:perf:playwright"], "pnpm test:browser:perf");
|
|
377
|
+
assert.equal(
|
|
378
|
+
packageJson.scripts["verify:final"],
|
|
379
|
+
"pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const playwrightConfigSource = await fs.readFile(
|
|
383
|
+
path.join(targetDir, "playwright.config.ts"),
|
|
384
|
+
"utf8",
|
|
385
|
+
);
|
|
386
|
+
assert.match(playwrightConfigSource, /pnpm exec vite dev/);
|
|
387
|
+
assert.doesNotMatch(playwrightConfigSource, /npm exec -- vite/);
|
|
388
|
+
|
|
389
|
+
const agentsSource = await fs.readFile(path.join(targetDir, "AGENTS.md"), "utf8");
|
|
390
|
+
assert.match(agentsSource, /pnpm verify:quick/);
|
|
391
|
+
assert.match(agentsSource, /pnpm verify:final/);
|
|
392
|
+
assert.match(agentsSource, /pnpm test:browser/);
|
|
393
|
+
assert.doesNotMatch(agentsSource, /npm run verify:quick/);
|
|
394
|
+
|
|
395
|
+
const worklogSource = await fs.readFile(
|
|
396
|
+
path.join(targetDir, "docs/toolcraft/agent-worklog.md"),
|
|
397
|
+
"utf8",
|
|
398
|
+
);
|
|
399
|
+
assert.match(worklogSource, /pnpm verify:quick/);
|
|
400
|
+
assert.match(worklogSource, /pnpm verify:final/);
|
|
401
|
+
|
|
402
|
+
await execFileAsync(process.execPath, [path.join(targetDir, "scripts/check-toolcraft-docs.mjs")], {
|
|
403
|
+
cwd: targetDir,
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
346
407
|
it("writes the app identity marker when the generated title matches the template title", async () => {
|
|
347
408
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-cli-"));
|
|
348
409
|
tempRoots.push(tempRoot);
|
package/src/package-json.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEFAULT_PACKAGE_MANAGER, createVerificationScripts } from "./package-manager.mjs";
|
|
2
|
+
|
|
1
3
|
export function sanitizePackageName(value) {
|
|
2
4
|
const sanitized = String(value ?? "")
|
|
3
5
|
.trim()
|
|
@@ -44,7 +46,11 @@ function addDependencyGroup(packageJson, groupName, dependencies) {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export function createGeneratedPackageJson({
|
|
49
|
+
export function createGeneratedPackageJson({
|
|
50
|
+
name,
|
|
51
|
+
packageManager = DEFAULT_PACKAGE_MANAGER,
|
|
52
|
+
starterPackageJson,
|
|
53
|
+
}) {
|
|
48
54
|
if (!starterPackageJson || typeof starterPackageJson !== "object") {
|
|
49
55
|
throw new Error("starterPackageJson is required to create a generated package manifest.");
|
|
50
56
|
}
|
|
@@ -54,7 +60,10 @@ export function createGeneratedPackageJson({ name, starterPackageJson }) {
|
|
|
54
60
|
private: true,
|
|
55
61
|
license: starterPackageJson.license,
|
|
56
62
|
type: starterPackageJson.type ?? "module",
|
|
57
|
-
scripts:
|
|
63
|
+
scripts: {
|
|
64
|
+
...(starterPackageJson.scripts ?? {}),
|
|
65
|
+
...createVerificationScripts(packageManager),
|
|
66
|
+
},
|
|
58
67
|
};
|
|
59
68
|
|
|
60
69
|
addDependencyGroup(packageJson, "dependencies", starterPackageJson.dependencies);
|
|
@@ -58,6 +58,12 @@ describe("createGeneratedPackageJson", () => {
|
|
|
58
58
|
scripts: {
|
|
59
59
|
dev: "vite dev",
|
|
60
60
|
test: "vitest run",
|
|
61
|
+
"verify:final":
|
|
62
|
+
"npm run ai:check && npm run test && npm run build && npm run test:browser",
|
|
63
|
+
"verify:perf": "npm run test:browser:perf",
|
|
64
|
+
"verify:perf:playwright": "npm run test:browser:perf",
|
|
65
|
+
"verify:quick": "npm run ai:check && npm run test",
|
|
66
|
+
"verify:ui": "npm run test:browser",
|
|
61
67
|
},
|
|
62
68
|
dependencies: {
|
|
63
69
|
"@tanstack/react-router": "1.170.6",
|
|
@@ -77,4 +83,25 @@ describe("createGeneratedPackageJson", () => {
|
|
|
77
83
|
/starterPackageJson is required/,
|
|
78
84
|
);
|
|
79
85
|
});
|
|
86
|
+
|
|
87
|
+
it("generates verification scripts for the selected package manager", () => {
|
|
88
|
+
const packageJson = createGeneratedPackageJson({
|
|
89
|
+
name: "Generated App",
|
|
90
|
+
packageManager: "pnpm",
|
|
91
|
+
starterPackageJson: {
|
|
92
|
+
scripts: {
|
|
93
|
+
dev: "vite dev",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(packageJson.scripts, {
|
|
99
|
+
dev: "vite dev",
|
|
100
|
+
"verify:final": "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
|
|
101
|
+
"verify:perf": "pnpm test:browser:perf",
|
|
102
|
+
"verify:perf:playwright": "pnpm test:browser:perf",
|
|
103
|
+
"verify:quick": "pnpm ai:check && pnpm test",
|
|
104
|
+
"verify:ui": "pnpm test:browser",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
80
107
|
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const DEFAULT_PACKAGE_MANAGER = "npm";
|
|
2
|
+
|
|
3
|
+
const PACKAGE_MANAGER_SPECS = {
|
|
4
|
+
npm: {
|
|
5
|
+
installHelp: [" Install Node.js from https://nodejs.org/"],
|
|
6
|
+
installArgs: ["install"],
|
|
7
|
+
runScript(scriptName) {
|
|
8
|
+
return `npm run ${scriptName}`;
|
|
9
|
+
},
|
|
10
|
+
exec(binaryName, args = []) {
|
|
11
|
+
return ["npm", "exec", "--", binaryName, ...args].join(" ");
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
pnpm: {
|
|
15
|
+
installHelp: [" corepack enable", " corepack prepare pnpm@latest --activate"],
|
|
16
|
+
installArgs: ["install"],
|
|
17
|
+
runScript(scriptName) {
|
|
18
|
+
return `pnpm ${scriptName}`;
|
|
19
|
+
},
|
|
20
|
+
exec(binaryName, args = []) {
|
|
21
|
+
return ["pnpm", "exec", binaryName, ...args].join(" ");
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const PACKAGE_MANAGER_NAMES = new Set(Object.keys(PACKAGE_MANAGER_SPECS));
|
|
27
|
+
|
|
28
|
+
const GENERATED_SCRIPT_COMMANDS = [
|
|
29
|
+
"ai:check",
|
|
30
|
+
"build",
|
|
31
|
+
"dev",
|
|
32
|
+
"dev:restart",
|
|
33
|
+
"preview",
|
|
34
|
+
"preview:restart",
|
|
35
|
+
"test",
|
|
36
|
+
"test:browser",
|
|
37
|
+
"test:browser:perf",
|
|
38
|
+
"verify:final",
|
|
39
|
+
"verify:perf",
|
|
40
|
+
"verify:perf:playwright",
|
|
41
|
+
"verify:quick",
|
|
42
|
+
"verify:ui",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function getSpec(packageManager) {
|
|
46
|
+
return PACKAGE_MANAGER_SPECS[normalizePackageManager(packageManager)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function replaceAllExact(source, replacements) {
|
|
50
|
+
let next = String(source);
|
|
51
|
+
|
|
52
|
+
for (const [from, to] of replacements) {
|
|
53
|
+
next = next.split(from).join(to);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizePackageManager(value, fallback = DEFAULT_PACKAGE_MANAGER) {
|
|
60
|
+
return PACKAGE_MANAGER_NAMES.has(value) ? value : fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function detectPackageManager(env = process.env) {
|
|
64
|
+
const userAgent = String(env.npm_config_user_agent ?? "");
|
|
65
|
+
const [packageManager] = userAgent.split("/", 1);
|
|
66
|
+
return normalizePackageManager(packageManager);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createInstallCommand(packageManager) {
|
|
70
|
+
const spec = getSpec(packageManager);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
args: [...spec.installArgs],
|
|
74
|
+
command: normalizePackageManager(packageManager),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createInstallHelp(packageManager) {
|
|
79
|
+
return [...getSpec(packageManager).installHelp];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createRunScriptCommand(packageManager, scriptName) {
|
|
83
|
+
return getSpec(packageManager).runScript(scriptName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createExecCommand(packageManager, binaryName, args = []) {
|
|
87
|
+
return getSpec(packageManager).exec(binaryName, args);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createVerificationScripts(packageManager) {
|
|
91
|
+
return {
|
|
92
|
+
"verify:quick": [
|
|
93
|
+
createRunScriptCommand(packageManager, "ai:check"),
|
|
94
|
+
createRunScriptCommand(packageManager, "test"),
|
|
95
|
+
].join(" && "),
|
|
96
|
+
"verify:ui": createRunScriptCommand(packageManager, "test:browser"),
|
|
97
|
+
"verify:perf": createRunScriptCommand(packageManager, "test:browser:perf"),
|
|
98
|
+
"verify:perf:playwright": createRunScriptCommand(packageManager, "test:browser:perf"),
|
|
99
|
+
"verify:final": [
|
|
100
|
+
createRunScriptCommand(packageManager, "ai:check"),
|
|
101
|
+
createRunScriptCommand(packageManager, "test"),
|
|
102
|
+
createRunScriptCommand(packageManager, "build"),
|
|
103
|
+
createRunScriptCommand(packageManager, "test:browser"),
|
|
104
|
+
].join(" && "),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createGeneratedCommandReplacements(packageManager) {
|
|
109
|
+
const replacements = [
|
|
110
|
+
["pnpm exec vite", createExecCommand(packageManager, "vite")],
|
|
111
|
+
["pnpm install", `${normalizePackageManager(packageManager)} install`],
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const scriptName of GENERATED_SCRIPT_COMMANDS) {
|
|
115
|
+
replacements.push([`pnpm ${scriptName}`, createRunScriptCommand(packageManager, scriptName)]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return replacements.sort(([left], [right]) => right.length - left.length);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function replaceGeneratedCommandReferences(source, packageManager) {
|
|
122
|
+
return replaceAllExact(source, createGeneratedCommandReplacements(packageManager));
|
|
123
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createExecCommand,
|
|
6
|
+
createInstallCommand,
|
|
7
|
+
createRunScriptCommand,
|
|
8
|
+
createVerificationScripts,
|
|
9
|
+
detectPackageManager,
|
|
10
|
+
replaceGeneratedCommandReferences,
|
|
11
|
+
} from "./package-manager.mjs";
|
|
12
|
+
|
|
13
|
+
describe("detectPackageManager", () => {
|
|
14
|
+
it("uses the npm user agent when the CLI is launched through npx", () => {
|
|
15
|
+
assert.equal(
|
|
16
|
+
detectPackageManager({
|
|
17
|
+
npm_config_user_agent: "npm/10.9.4 node/v24.4.1 darwin arm64 workspaces/false",
|
|
18
|
+
}),
|
|
19
|
+
"npm",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("detects supported package manager launchers", () => {
|
|
24
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "pnpm/10.12.4 npm/? node/v24.4.1" }), "pnpm");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("falls back to npm when no supported package manager is detected", () => {
|
|
28
|
+
assert.equal(detectPackageManager({}), "npm");
|
|
29
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "unknown/1.0.0" }), "npm");
|
|
30
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "yarn/1.22.22 npm/? node/v24.4.1" }), "npm");
|
|
31
|
+
assert.equal(detectPackageManager({ npm_config_user_agent: "bun/1.2.0 npm/? node/v24.4.1" }), "npm");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("package manager commands", () => {
|
|
36
|
+
it("formats install, run, exec, and verification commands for npm", () => {
|
|
37
|
+
assert.deepEqual(createInstallCommand("npm"), {
|
|
38
|
+
args: ["install"],
|
|
39
|
+
command: "npm",
|
|
40
|
+
});
|
|
41
|
+
assert.equal(createRunScriptCommand("npm", "dev"), "npm run dev");
|
|
42
|
+
assert.equal(createExecCommand("npm", "vite", ["dev"]), "npm exec -- vite dev");
|
|
43
|
+
assert.deepEqual(createVerificationScripts("npm"), {
|
|
44
|
+
"verify:final":
|
|
45
|
+
"npm run ai:check && npm run test && npm run build && npm run test:browser",
|
|
46
|
+
"verify:perf": "npm run test:browser:perf",
|
|
47
|
+
"verify:perf:playwright": "npm run test:browser:perf",
|
|
48
|
+
"verify:quick": "npm run ai:check && npm run test",
|
|
49
|
+
"verify:ui": "npm run test:browser",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("formats run and exec commands for pnpm", () => {
|
|
54
|
+
assert.equal(createRunScriptCommand("pnpm", "dev"), "pnpm dev");
|
|
55
|
+
assert.equal(createExecCommand("pnpm", "vite", ["dev"]), "pnpm exec vite dev");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("replaces known generated command references for npm", () => {
|
|
59
|
+
const source = [
|
|
60
|
+
"pnpm install",
|
|
61
|
+
"pnpm verify:final",
|
|
62
|
+
"pnpm verify:perf",
|
|
63
|
+
"pnpm test:browser",
|
|
64
|
+
"pnpm dev",
|
|
65
|
+
"pnpm exec vite dev --host 127.0.0.1",
|
|
66
|
+
].join("\n");
|
|
67
|
+
|
|
68
|
+
assert.equal(
|
|
69
|
+
replaceGeneratedCommandReferences(source, "npm"),
|
|
70
|
+
[
|
|
71
|
+
"npm install",
|
|
72
|
+
"npm run verify:final",
|
|
73
|
+
"npm run verify:perf",
|
|
74
|
+
"npm run test:browser",
|
|
75
|
+
"npm run dev",
|
|
76
|
+
"npm exec -- vite dev --host 127.0.0.1",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -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
|
);
|
|
@@ -904,7 +929,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
904
929
|
"Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
|
|
905
930
|
);
|
|
906
931
|
expect(contract.aiUsageRules).toContain(
|
|
907
|
-
"A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and
|
|
932
|
+
"A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and this app's verify:perf script only as the Playwright fallback.",
|
|
908
933
|
);
|
|
909
934
|
expect(contract.aiUsageRules).toContain(
|
|
910
935
|
"Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
|
|
@@ -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.",
|
|
@@ -1182,7 +1186,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
1182
1186
|
"Expensive renderers must cache decoded media, source pixels, glyph atlases, gradients, and other reusable inputs by media id, canvas size, and stable control keys.",
|
|
1183
1187
|
"Slider drags and high-frequency controls must debounce or coalesce preview work, cancel stale async renders, and avoid re-decoding media on every control change.",
|
|
1184
1188
|
"Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
|
|
1185
|
-
"A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and
|
|
1189
|
+
"A full performance checkpoint must run only when the first working app version exists or the user requests performance, lag, jank, animation speed, drag/zoom stabilization work, or otherwise complains about performance; use the agent-controlled browser first and this app's verify:perf script only as the Playwright fallback.",
|
|
1186
1190
|
"Renderer, canvas, animation, export, timeline, layers, canvas.renderScale, bug fixes, and performance-sensitive control changes use targeted functional/browser checks first and targeted performance scenarios only for touched workload, viewport, or export paths.",
|
|
1187
1191
|
"Performance fixes must preserve selected output and preview quality; do not reduce image quality, selected renderScale, export resolution, source media fidelity, or canvas backing pixels as the hidden way to pass budgets.",
|
|
1188
1192
|
"When canvas or slider interactions lag, diagnose where the slowdown comes from before changing output quality: renderer technique, React update frequency, decoded media, shader/program setup, buffer uploads, layout work, async render cancellation, or animation scheduling.",
|
|
@@ -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
|
|