@pixel-point/toolcraft 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +104 -14
- package/templates/runtime/contracts/component-contracts.ts +54 -22
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/react/controls-panel.test.tsx +374 -13
- package/templates/runtime/react/controls-panel.tsx +65 -4
- package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
- package/templates/runtime/schema/define-toolcraft.ts +25 -1
- package/templates/runtime/schema/types.ts +3 -0
- package/templates/runtime/testing/performance.test.ts +134 -0
- package/templates/runtime/testing/performance.ts +34 -4
- package/templates/starter/AGENTS.md +4 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
- package/templates/starter/docs/toolcraft/component-rules.md +32 -11
- package/templates/starter/docs/toolcraft/performance.md +7 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +2 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +739 -66
- package/templates/starter/src/app/starter-acceptance.ts +471 -12
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/index.ts +2 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
- package/templates/ui/components/panel/panel-section.tsx +29 -5
package/package.json
CHANGED
package/src/generate.mjs
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
pathExists,
|
|
12
12
|
removeDirectory,
|
|
13
13
|
} from "./copy-recursive.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createGeneratedPackageJson,
|
|
16
|
+
createGeneratedTsConfig,
|
|
17
|
+
normalizeProjectTitle,
|
|
18
|
+
} from "./package-json.mjs";
|
|
15
19
|
import { rewriteGeneratedText, rewriteTextFiles } from "./rewrite-imports.mjs";
|
|
16
20
|
|
|
17
21
|
const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
@@ -21,6 +25,13 @@ function writeJson(filePath, value) {
|
|
|
21
25
|
return fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function escapeHtmlText(value) {
|
|
29
|
+
return String(value)
|
|
30
|
+
.replaceAll("&", "&")
|
|
31
|
+
.replaceAll("<", "<")
|
|
32
|
+
.replaceAll(">", ">");
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
async function readJson(filePath) {
|
|
25
36
|
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
26
37
|
}
|
|
@@ -100,6 +111,21 @@ async function restoreGeneratedGitignore(targetDir) {
|
|
|
100
111
|
await fs.rename(npmSafeGitignorePath, gitignorePath);
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
async function writeGeneratedProjectTitle(targetDir, title) {
|
|
115
|
+
const indexPath = path.join(targetDir, "index.html");
|
|
116
|
+
const source = await fs.readFile(indexPath, "utf8");
|
|
117
|
+
const nextSource = source.replace(
|
|
118
|
+
/<title>.*?<\/title>/,
|
|
119
|
+
`<title>${escapeHtmlText(title)}</title>`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (nextSource === source) {
|
|
123
|
+
throw new Error(`Unable to update generated app title in ${indexPath}.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await fs.writeFile(indexPath, nextSource);
|
|
127
|
+
}
|
|
128
|
+
|
|
103
129
|
async function removeToolcraftTestFiles(toolcraftRoot) {
|
|
104
130
|
async function visit(currentDir) {
|
|
105
131
|
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
@@ -190,6 +216,12 @@ export async function generateToolcraft(options = {}) {
|
|
|
190
216
|
await assertDirectory(sourcePaths.uiSrc, "UI package source");
|
|
191
217
|
await assertDirectory(sourcePaths.toolcraftSrc, "Toolcraft template runtime package source");
|
|
192
218
|
const starterPackageJson = await readJson(path.join(sourcePaths.starterDir, "package.json"));
|
|
219
|
+
const packageJson = createGeneratedPackageJson({
|
|
220
|
+
name: options.name ?? path.basename(targetDir),
|
|
221
|
+
starterPackageJson,
|
|
222
|
+
});
|
|
223
|
+
const projectTitle = normalizeProjectTitle(packageJson.name);
|
|
224
|
+
|
|
193
225
|
await ensureWritableTargetDirectory(targetDir, { force: options.force });
|
|
194
226
|
|
|
195
227
|
await copyDirectory(sourcePaths.starterDir, targetDir);
|
|
@@ -206,11 +238,8 @@ export async function generateToolcraft(options = {}) {
|
|
|
206
238
|
const changedFiles = await rewriteTextFiles(targetDir, (source) =>
|
|
207
239
|
rewriteGeneratedAppText(source),
|
|
208
240
|
);
|
|
241
|
+
await writeGeneratedProjectTitle(targetDir, projectTitle);
|
|
209
242
|
await writeToolcraftIntegrityManifest(toolcraftRoot);
|
|
210
|
-
const packageJson = createGeneratedPackageJson({
|
|
211
|
-
name: options.name ?? path.basename(targetDir),
|
|
212
|
-
starterPackageJson,
|
|
213
|
-
});
|
|
214
243
|
|
|
215
244
|
await writeJson(path.join(targetDir, "package.json"), packageJson);
|
|
216
245
|
await writeJson(path.join(targetDir, "tsconfig.json"), createGeneratedTsConfig());
|
package/src/generate.test.mjs
CHANGED
|
@@ -79,7 +79,15 @@ describe("generateToolcraft", () => {
|
|
|
79
79
|
assert.equal(packageJson.devDependencies["@playwright/test"], "^1.51.1");
|
|
80
80
|
assert.equal(packageJson.scripts["ai:check"], "node scripts/check-ai-skills.mjs");
|
|
81
81
|
assert.equal(packageJson.scripts.dev, "node scripts/run-vite-on-free-port.mjs dev");
|
|
82
|
+
assert.equal(
|
|
83
|
+
packageJson.scripts["dev:restart"],
|
|
84
|
+
"node scripts/run-vite-on-free-port.mjs dev --toolcraft-restart",
|
|
85
|
+
);
|
|
82
86
|
assert.equal(packageJson.scripts.preview, "node scripts/run-vite-on-free-port.mjs preview");
|
|
87
|
+
assert.equal(
|
|
88
|
+
packageJson.scripts["preview:restart"],
|
|
89
|
+
"node scripts/run-vite-on-free-port.mjs preview --toolcraft-restart",
|
|
90
|
+
);
|
|
83
91
|
assert.equal(packageJson.scripts["docs:check"], "node scripts/check-toolcraft-docs.mjs");
|
|
84
92
|
assert.equal(
|
|
85
93
|
packageJson.scripts.test,
|
|
@@ -98,6 +106,9 @@ describe("generateToolcraft", () => {
|
|
|
98
106
|
"pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
|
|
99
107
|
);
|
|
100
108
|
|
|
109
|
+
const indexHtmlSource = await fs.readFile(path.join(targetDir, "index.html"), "utf8");
|
|
110
|
+
assert.match(indexHtmlSource, /<title>Generated App<\/title>/);
|
|
111
|
+
|
|
101
112
|
assert.ok(await fs.stat(path.join(targetDir, "playwright.config.ts")));
|
|
102
113
|
const playwrightConfigSource = await fs.readFile(
|
|
103
114
|
path.join(targetDir, "playwright.config.ts"),
|
|
@@ -121,6 +132,7 @@ describe("generateToolcraft", () => {
|
|
|
121
132
|
assert.match(gitignoreSource, /dist/);
|
|
122
133
|
assert.match(gitignoreSource, /playwright-report/);
|
|
123
134
|
assert.match(gitignoreSource, /\.env\.\*/);
|
|
135
|
+
assert.match(gitignoreSource, /\.toolcraft/);
|
|
124
136
|
await assert.rejects(() => fs.stat(path.join(targetDir, "gitignore")), /ENOENT/);
|
|
125
137
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/check-ai-skills.mjs")));
|
|
126
138
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.mjs")));
|
package/src/package-json.mjs
CHANGED
|
@@ -9,6 +9,21 @@ export function sanitizePackageName(value) {
|
|
|
9
9
|
return sanitized || "toolcraft-app";
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function normalizeProjectTitle(value) {
|
|
13
|
+
const words = String(value ?? "")
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/^@/, "")
|
|
16
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
17
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
|
|
20
|
+
const title = words
|
|
21
|
+
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`)
|
|
22
|
+
.join(" ");
|
|
23
|
+
|
|
24
|
+
return title || "Toolcraft App";
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
function isWorkspaceDependency(name, specifier) {
|
|
13
28
|
return name.startsWith("@repo/") || String(specifier).startsWith("workspace:");
|
|
14
29
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createGeneratedPackageJson,
|
|
6
|
+
normalizeProjectTitle,
|
|
7
|
+
sanitizePackageName,
|
|
8
|
+
} from "./package-json.mjs";
|
|
5
9
|
|
|
6
10
|
describe("sanitizePackageName", () => {
|
|
7
11
|
it("normalizes package names for generated apps", () => {
|
|
@@ -11,6 +15,15 @@ describe("sanitizePackageName", () => {
|
|
|
11
15
|
});
|
|
12
16
|
});
|
|
13
17
|
|
|
18
|
+
describe("normalizeProjectTitle", () => {
|
|
19
|
+
it("creates a human-readable title from generated project names", () => {
|
|
20
|
+
assert.equal(normalizeProjectTitle("mesh-gradient"), "Mesh Gradient");
|
|
21
|
+
assert.equal(normalizeProjectTitle("mesh_gradient.app"), "Mesh Gradient App");
|
|
22
|
+
assert.equal(normalizeProjectTitle("@pixel-point/mesh-gradient"), "Pixel Point Mesh Gradient");
|
|
23
|
+
assert.equal(normalizeProjectTitle(""), "Toolcraft App");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
14
27
|
describe("createGeneratedPackageJson", () => {
|
|
15
28
|
it("uses the starter package manifest as source of truth", () => {
|
|
16
29
|
const packageJson = createGeneratedPackageJson({
|
|
@@ -82,6 +82,12 @@ describe("Toolcraft template component contracts", () => {
|
|
|
82
82
|
|
|
83
83
|
expect(segmented?.strictness).toBe("best-fit");
|
|
84
84
|
expect(segmented?.acceptableAlternatives?.join(" ")).toMatch(/Select/i);
|
|
85
|
+
expect(segmented?.layoutConstraints).toContain(
|
|
86
|
+
"Segmented controls are full-width controls and must not be placed in two-column inline or half-width layout groups.",
|
|
87
|
+
);
|
|
88
|
+
expect(getToolcraftComponentContract("segmented").aiUsageRules).toContain(
|
|
89
|
+
"Do not place Segmented beside Switch, Color, Select, or another control in an inline row; use Select when a finite choice must occupy a half-width column.",
|
|
90
|
+
);
|
|
85
91
|
expect(select?.useWhen.join(" ")).toMatch(/long labels|many options/i);
|
|
86
92
|
expect(select?.layoutConstraints).toContain(
|
|
87
93
|
"Prefer compact two-column inline layout for related short Select pairs that tune one workflow or entity.",
|
|
@@ -150,6 +156,15 @@ describe("Toolcraft template component contracts", () => {
|
|
|
150
156
|
expect(contract.aiUsageRules).toContain(
|
|
151
157
|
'Use the default vector variant for spatial values such as position, offset, direction, focus, anchor, and light direction.',
|
|
152
158
|
);
|
|
159
|
+
expect(contract.aiUsageRules).toContain(
|
|
160
|
+
'Default spatial vector pads use coordinateMode: "screen": dragging left/up makes vector.x and vector.y smaller so canvas objects move left/up without renderer-side Y inversion.',
|
|
161
|
+
);
|
|
162
|
+
expect(contract.aiUsageRules).toContain(
|
|
163
|
+
"Holding Shift while dragging a vector pad locks movement to the dominant axis; do not build a custom pad just to support axis-constrained movement.",
|
|
164
|
+
);
|
|
165
|
+
expect(contract.aiUsageRules).toContain(
|
|
166
|
+
'Use coordinateMode: "cartesian" only when the product intentionally exposes mathematical Y-up coordinates instead of canvas/screen movement.',
|
|
167
|
+
);
|
|
153
168
|
expect(contract.aiUsageRules).toContain(
|
|
154
169
|
"Vector is a compound control; acceptance must prove vector.x and vector.y both affect the product output.",
|
|
155
170
|
);
|
|
@@ -192,7 +207,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
192
207
|
"When the nearest section title already names the switch context, do not duplicate that title as the visible switch label. Use label false for a visual-only toggle and keep the meaning in target/description.",
|
|
193
208
|
);
|
|
194
209
|
expect(switchContract.aiUsageRules).toContain(
|
|
195
|
-
'A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. That row uses equal-width columns; never shrink the switch column to intrinsic width. In section-owned rows, use a short visible label such as "Include" instead of repeating the section title, such as "Include background" inside Background.',
|
|
210
|
+
'A Switch may share an inline row with one related parameter control when the visible switch label is short enough to fit. That row uses equal-width columns and the same horizontal column gap as paired Select controls; never shrink the switch column to intrinsic width. The non-switch parameter uses label false in that row; if its label is needed, stack the controls instead. In section-owned rows, use a short visible switch label such as "Include" instead of repeating the section title, such as "Include background" inside Background.',
|
|
196
211
|
);
|
|
197
212
|
expect(checkboxContract.aiUsageRules).toContain(
|
|
198
213
|
'Checkbox labels name the setting context only; do not prefix labels with "Enable" or "Disable" because the checkbox already communicates enabled/selected state.',
|
|
@@ -207,7 +222,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
207
222
|
"Two adjacent Checkbox controls for the same product entity must share one inline row when every visible label fits without truncation. Keep paired labels to short one- or two-word names; the runtime auto-pairs safe adjacent checkboxes by target entity, and generated schemas should stack checkboxes only when any label would truncate.",
|
|
208
223
|
);
|
|
209
224
|
expect(checkboxContract.aiUsageRules).toContain(
|
|
210
|
-
"A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. That row uses equal-width columns; never shrink the checkbox column to intrinsic width. Hide the checkbox label when the section title provides the visible context.",
|
|
225
|
+
"A Checkbox may share an inline row with one related parameter control when the visible checkbox label is short enough to fit. That row uses equal-width columns and the same horizontal column gap as paired Select controls; never shrink the checkbox column to intrinsic width. The non-checkbox parameter uses label false in that row; if its label is needed, stack the controls instead. Hide the checkbox label when the section title provides the visible context.",
|
|
211
226
|
);
|
|
212
227
|
});
|
|
213
228
|
|
|
@@ -261,10 +276,28 @@ describe("Toolcraft template component contracts", () => {
|
|
|
261
276
|
"Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
|
|
262
277
|
);
|
|
263
278
|
expect(contract.decisionCatalog?.layoutConstraints).toContain(
|
|
264
|
-
"
|
|
279
|
+
"Do not set an Actions control label to the exact same visible text as its only button; use a short one- or two-word context label such as Ink wash, Palette action, or Current layer while the button keeps the command verb.",
|
|
280
|
+
);
|
|
281
|
+
expect(contract.decisionCatalog?.layoutConstraints).toContain(
|
|
282
|
+
"Actions never use a side-label layout. If a visible label exists, it sits above the buttons.",
|
|
283
|
+
);
|
|
284
|
+
expect(contract.decisionCatalog?.layoutConstraints).toContain(
|
|
285
|
+
"Actions buttons render as a two-column grid. One visible button occupies the left half of the section; two buttons occupy one half each; more than two buttons wrap into additional 50% cells.",
|
|
286
|
+
);
|
|
287
|
+
expect(contract.decisionCatalog?.layoutConstraints).toContain(
|
|
288
|
+
"Do not center or right-align a partial final Actions row; an odd trailing button stays in the left 50% cell.",
|
|
265
289
|
);
|
|
266
290
|
expect(contract.aiUsageRules).toContain(
|
|
267
|
-
"
|
|
291
|
+
"For a single visible Actions button, the control label and button label must not be identical; make the control label a concise context and the button label the command.",
|
|
292
|
+
);
|
|
293
|
+
expect(contract.aiUsageRules).toContain(
|
|
294
|
+
"Render the Actions label above the buttons; do not put the label on the left with buttons on the right.",
|
|
295
|
+
);
|
|
296
|
+
expect(contract.aiUsageRules).toContain(
|
|
297
|
+
"Render Actions buttons in 50% cells: one button uses the left half, two buttons fill one row, and larger groups continue in two columns.",
|
|
298
|
+
);
|
|
299
|
+
expect(contract.aiUsageRules).toContain(
|
|
300
|
+
"Do not stretch an odd trailing Actions button full-width.",
|
|
268
301
|
);
|
|
269
302
|
expect(contract.aiUsageRules).toContain(
|
|
270
303
|
'For local reset-like actions, use product-specific values such as "reset-current-layer" or "reset-palette" and handle them through ToolcraftApp onPanelAction; do not use a bare "reset" value unless the action intentionally runs controls.reset.',
|
|
@@ -374,13 +407,16 @@ describe("Toolcraft template component contracts", () => {
|
|
|
374
407
|
"Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
|
|
375
408
|
);
|
|
376
409
|
expect(contract.aiUsageRules).toContain(
|
|
377
|
-
"Use editable-output
|
|
410
|
+
"Use editable-output for generated, exportable, shader, poster, badge, wall, banner, thumbnail, procedural, reference-clone, and product-output apps so users always see Aspect ratio, Canvas width, and Canvas height.",
|
|
411
|
+
);
|
|
412
|
+
expect(contract.aiUsageRules).toContain(
|
|
413
|
+
"A user-provided, reference, fixed-format, or base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output so the size is an initial value, not a hidden lock.",
|
|
378
414
|
);
|
|
379
415
|
expect(contract.aiUsageRules).toContain(
|
|
380
|
-
"
|
|
416
|
+
"Do not use fixed-output for generated product/output apps with export actions. Reserve fixed-output for non-product internal fixtures where width and height truly must never be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
|
|
381
417
|
);
|
|
382
418
|
expect(contract.aiUsageRules).toContain(
|
|
383
|
-
"
|
|
419
|
+
"A reference or previous app lacking a size editor, or defining a fixed-size baseline, is not a fixed-output reason for a generated product app; product-output clones still use editable-output.",
|
|
384
420
|
);
|
|
385
421
|
expect(contract.aiUsageRules).toContain(
|
|
386
422
|
"Resolved canvas.size exists for every canvas app, but visible Canvas width and Canvas height controls are mandatory only for editable-output sizing and do not depend on settingsTransfer.",
|
|
@@ -398,7 +434,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
398
434
|
"Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
|
|
399
435
|
);
|
|
400
436
|
expect(contract.aiUsageRules).toContain(
|
|
401
|
-
"For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from
|
|
437
|
+
"For non-vector raster, Canvas 2D, WebGL, and WebGPU previews, set canvas.renderScale: true so the runtime adds Resolution scale after canvas sizing. The scale changes backing pixels from 1 to 2 without changing visible canvas size, and adding/enabling it requires targeted browser evidence that the canvas stays responsive at the selected scale.",
|
|
402
438
|
);
|
|
403
439
|
expect(contract.aiUsageRules).toContain(
|
|
404
440
|
"After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
|
|
@@ -464,6 +500,24 @@ describe("Toolcraft template component contracts", () => {
|
|
|
464
500
|
const slider = getToolcraftComponentContract("slider");
|
|
465
501
|
const rangeSlider = getToolcraftComponentContract("rangeSlider");
|
|
466
502
|
|
|
503
|
+
expect(slider.decisionCatalog.requiredAcceptance).toContain(
|
|
504
|
+
"Prove dragging the slider changes product output or the intended runtime side effect while the drag is in progress, not only after pointer release, blur, Apply, or a final commit.",
|
|
505
|
+
);
|
|
506
|
+
expect(slider.aiUsageRules).toContain(
|
|
507
|
+
"Sliders are live canvas controls: dragging must update runtime state and product output in real time by default.",
|
|
508
|
+
);
|
|
509
|
+
expect(slider.aiUsageRules).toContain(
|
|
510
|
+
"Do not implement slider values as deferred local drafts, Apply-only updates, pointer-up-only commits, or renderer changes that appear only after the user asks again.",
|
|
511
|
+
);
|
|
512
|
+
expect(slider.aiUsageRules).toContain(
|
|
513
|
+
"Slider performance coverage must use a real control-drag scenario; control-change coverage is not enough to prove live canvas feedback or drag smoothness.",
|
|
514
|
+
);
|
|
515
|
+
expect(slider.aiUsageRules).toContain(
|
|
516
|
+
"If a live slider causes jank, optimize the renderer path first: update uniforms or stable buffers, cache expensive inputs, coalesce preview work to requestAnimationFrame, cancel stale async renders, move heavy work off React, or switch renderer strategy.",
|
|
517
|
+
);
|
|
518
|
+
expect(slider.aiUsageRules).toContain(
|
|
519
|
+
"Only in an extreme documented performance ceiling may a slider use a degraded live preview or delayed heavy refinement; the user must still see immediate canvas feedback while dragging and the worklog must record the measured reason.",
|
|
520
|
+
);
|
|
467
521
|
expect(slider.aiUsageRules).toContain(
|
|
468
522
|
"Slider step means numeric snapping only; it does not make the slider visually discrete by itself.",
|
|
469
523
|
);
|
|
@@ -480,16 +534,19 @@ describe("Toolcraft template component contracts", () => {
|
|
|
480
534
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
481
535
|
);
|
|
482
536
|
expect(slider.aiUsageRules).toContain(
|
|
483
|
-
"Use slider unit only for measurement
|
|
537
|
+
"Use slider unit only for real measurement suffixes such as %, px, °, s, ms, fps, rows/cols, or similar domain units.",
|
|
484
538
|
);
|
|
485
539
|
expect(slider.aiUsageRules).toContain(
|
|
486
540
|
"Do not use unit for repeated entity nouns already named by the section or label, such as Letters + letters, Shape Density / Count + shapes, Words + words, Symbols + symbols, Items + items, Particles + particles, or Layers + layers.",
|
|
487
541
|
);
|
|
542
|
+
expect(slider.aiUsageRules).toContain(
|
|
543
|
+
'Do not use unit: "x"; scale, multiplier, intensity, opacity, strength, depth, and shader amount sliders display plain numbers unless a real measurement unit applies.',
|
|
544
|
+
);
|
|
488
545
|
expect(slider.aiUsageRules).toContain(
|
|
489
546
|
"When the value needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
|
|
490
547
|
);
|
|
491
548
|
expect(slider.aiUsageRules).toContain(
|
|
492
|
-
"Compact symbol/CSS units render tight, such as 70%, 24px,
|
|
549
|
+
"Compact symbol/CSS units render tight, such as 70%, 24px, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
|
|
493
550
|
);
|
|
494
551
|
expect(slider.aiUsageRules).toContain(
|
|
495
552
|
"Slider valueLabel is editable only when it contains a numeric value; textual state labels such as Normal are display-only and must not expose hover or click editing affordances.",
|
|
@@ -515,6 +572,24 @@ describe("Toolcraft template component contracts", () => {
|
|
|
515
572
|
expect(slider.aiUsageRules).toContain(
|
|
516
573
|
"Do not leave a mode-dependent slider active while making the renderer ignore it; the UI must expose the unavailable state.",
|
|
517
574
|
);
|
|
575
|
+
expect(rangeSlider.decisionCatalog.requiredAcceptance).toContain(
|
|
576
|
+
"Prove dragging rangeSlider.lower and rangeSlider.upper both affect product output while the drag is in progress, not only after pointer release, blur, Apply, or a final commit.",
|
|
577
|
+
);
|
|
578
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
579
|
+
"Range sliders are live canvas controls: dragging either thumb must update runtime state and product output in real time by default.",
|
|
580
|
+
);
|
|
581
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
582
|
+
"Do not implement range slider values as deferred local drafts, Apply-only updates, pointer-up-only commits, or renderer changes that appear only after the user asks again.",
|
|
583
|
+
);
|
|
584
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
585
|
+
"Range slider performance coverage must use a real control-drag scenario; control-change coverage is not enough to prove live canvas feedback or drag smoothness.",
|
|
586
|
+
);
|
|
587
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
588
|
+
"If a live range slider causes jank, optimize the renderer path first: update uniforms or stable buffers, cache expensive inputs, coalesce preview work to requestAnimationFrame, cancel stale async renders, move heavy work off React, or switch renderer strategy.",
|
|
589
|
+
);
|
|
590
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
591
|
+
"Only in an extreme documented performance ceiling may a range slider use a degraded live preview or delayed heavy refinement; the user must still see immediate canvas feedback while dragging and the worklog must record the measured reason.",
|
|
592
|
+
);
|
|
518
593
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
519
594
|
"Range slider step means numeric snapping only; it does not make the range slider visually discrete by itself.",
|
|
520
595
|
);
|
|
@@ -531,7 +606,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
531
606
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
532
607
|
);
|
|
533
608
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
534
|
-
"Use rangeSlider unit only for measurement
|
|
609
|
+
"Use rangeSlider unit only for real measurement suffixes; do not use it for repeated entity nouns already named by the section or label, and do not use x as a unit.",
|
|
535
610
|
);
|
|
536
611
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
537
612
|
"When a range label needs an entity noun to make sense, improve the label or section title instead of appending that noun as the value unit.",
|
|
@@ -590,7 +665,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
590
665
|
"When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
|
|
591
666
|
);
|
|
592
667
|
expect(color.aiUsageRules).toContain(
|
|
593
|
-
'Mixed inline rows require visible labels on both controls.
|
|
668
|
+
'Mixed inline rows usually require visible labels on both controls. Toggle-plus-parameter rows are the section-owned exception: keep the Switch/Checkbox label visible and set the non-toggle parameter label to false; if the parameter label is needed, stack the controls instead. All 50/50 inline rows use the same horizontal column gap as paired Select controls. The required Background row uses the Switch label "Include" and sets the background Color control label to false. Palette variation color banks are the other exception when the group/section label already names the color bank.',
|
|
594
669
|
);
|
|
595
670
|
expect(color.aiUsageRules).toContain(
|
|
596
671
|
"Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
|
|
@@ -1239,6 +1314,12 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1239
1314
|
expect(contract.aiUsageRules).toContain(
|
|
1240
1315
|
'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
|
|
1241
1316
|
);
|
|
1317
|
+
expect(contract.aiUsageRules).toContain(
|
|
1318
|
+
"When uploaded/imported content is part of the source-material flow, the canvas must not show agent-invented artwork, CTA text, fake sample output, decorative placeholders, or preset source designs before real content exists; keep the canvas neutral/runtime-backed and put upload affordance in fileDrop.",
|
|
1319
|
+
);
|
|
1320
|
+
expect(contract.aiUsageRules).toContain(
|
|
1321
|
+
"Do not add procedural Source Preset modes only to avoid an empty canvas. A default procedural or reference source is allowed only when the prompt/reference explicitly defines it and the worklog records that evidence.",
|
|
1322
|
+
);
|
|
1242
1323
|
expect(contract.aiUsageRules).toContain(
|
|
1243
1324
|
"In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
|
|
1244
1325
|
);
|
|
@@ -1310,10 +1391,16 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1310
1391
|
"CodeTextarea is the multiline text input for any potentially long value, not only source code.",
|
|
1311
1392
|
);
|
|
1312
1393
|
expect(contract.aiUsageRules).toContain(
|
|
1313
|
-
"
|
|
1394
|
+
"Do not use CodeTextarea for short single-line canvas text, button labels, names, titles, captions, badges, or short tokens; use TextInput.",
|
|
1395
|
+
);
|
|
1396
|
+
expect(contract.aiUsageRules).toContain(
|
|
1397
|
+
"Use text for short single-line strings such as names, button labels, small numeric values, compact prompts, titles, captions, and short tokens.",
|
|
1314
1398
|
);
|
|
1315
1399
|
expect(contract.aiUsageRules).toContain(
|
|
1316
|
-
"Use code when the user may enter long prompts, multiline text, JSON, CSS, shader code, scripts, templates, or other long structured data.",
|
|
1400
|
+
"Use code only when the user may enter long prompts, multiline text, instructions, JSON, CSS, shader code, scripts, templates, or other long structured data.",
|
|
1401
|
+
);
|
|
1402
|
+
expect(contract.aiUsageRules).toContain(
|
|
1403
|
+
"If CodeTextarea has a short single-line default value, the schema description must make the long, multiline, or structured-content reason explicit.",
|
|
1317
1404
|
);
|
|
1318
1405
|
expect(contract.aiUsageRules).toContain(
|
|
1319
1406
|
"CodeTextarea is a content editor and applies values while typing; do not wait for blur, Enter, or Cmd/Ctrl+Enter to update runtime state.",
|
|
@@ -1329,6 +1416,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1329
1416
|
expect(contract.visualComponent).toBe("TextInput");
|
|
1330
1417
|
expect(contract.defaultSectionLayout).toBe("grouped");
|
|
1331
1418
|
expect(contract.labelPolicy).toBe("required");
|
|
1419
|
+
expect(contract.aiUsageRules).toContain(
|
|
1420
|
+
"TextInput owns short single-line product text: button labels, labels on the canvas, names, titles, captions, badges, short tokens, and compact prompts.",
|
|
1421
|
+
);
|
|
1332
1422
|
expect(contract.aiUsageRules).toContain(
|
|
1333
1423
|
'TextInput commitMode defaults to "content": text content, prompts, names, tokens, titles, and instructions apply while typing.',
|
|
1334
1424
|
);
|