@pixel-point/toolcraft 0.0.7 → 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 +185 -23
- package/templates/runtime/contracts/component-contracts.ts +91 -36
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +4 -4
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +774 -17
- package/templates/runtime/react/controls-panel.tsx +155 -8
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
- package/templates/runtime/schema/define-toolcraft.ts +29 -3
- package/templates/runtime/schema/types.ts +7 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1424 -56
- package/templates/runtime/testing/performance.ts +710 -43
- package/templates/starter/AGENTS.md +10 -9
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
- package/templates/starter/docs/toolcraft/component-rules.md +46 -16
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +54 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +5 -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 +1288 -105
- package/templates/starter/src/app/starter-acceptance.ts +740 -24
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
- 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/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +3 -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 +82 -6
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
|
|
|
@@ -224,6 +239,12 @@ describe("Toolcraft template component contracts", () => {
|
|
|
224
239
|
expect(contract.aiUsageRules).toContain(
|
|
225
240
|
"Do not use a custom control to recreate a built-in Slider, RangeSlider, Select, Segmented, Switch, Checkbox, Color, ColorOpacity, Gradient, FontPicker, ImagePicker, FileDrop, TextInput, CodeTextarea, RangeInput, Palette, Actions, CollectionActions, Curves, AnchorGrid, ChannelMixer, Vector, or PanelActions control.",
|
|
226
241
|
);
|
|
242
|
+
expect(contract.aiUsageRules).toContain(
|
|
243
|
+
"When a custom control owns a growable, removable, selectable, or reorderable runtime item set, its builtInFitCheck must explicitly check collectionActions and actions before choosing custom; this is based on the value model and user workflow, not on entity names such as masks or glyphs.",
|
|
244
|
+
);
|
|
245
|
+
expect(contract.aiUsageRules).toContain(
|
|
246
|
+
"Do not justify custom controls with icons, layout, styling, compactness, or custom buttons alone. The fit check must name the product interaction or value model that built-ins cannot express.",
|
|
247
|
+
);
|
|
227
248
|
expect(contract.aiUsageRules).toContain(
|
|
228
249
|
"Custom controls may use Toolcraft primitives for small app-specific chrome, but must not import or render low-level runtime surfaces or duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.",
|
|
229
250
|
);
|
|
@@ -254,6 +275,30 @@ describe("Toolcraft template component contracts", () => {
|
|
|
254
275
|
expect(contract.aiUsageRules).toContain(
|
|
255
276
|
"Do not use Actions for animation transport; Play, Pause, Resume, Restart, and Scrub belong to the top timeline when timeline behavior exists.",
|
|
256
277
|
);
|
|
278
|
+
expect(contract.decisionCatalog?.layoutConstraints).toContain(
|
|
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.",
|
|
289
|
+
);
|
|
290
|
+
expect(contract.aiUsageRules).toContain(
|
|
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.",
|
|
301
|
+
);
|
|
257
302
|
expect(contract.aiUsageRules).toContain(
|
|
258
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.',
|
|
259
304
|
);
|
|
@@ -286,6 +331,10 @@ describe("Toolcraft template component contracts", () => {
|
|
|
286
331
|
"recommendedMaxItems is advisory only and must not disable the plus button. Use hardMaxItems only when a real product, algorithm, API, export, or measured performance limit requires it.",
|
|
287
332
|
);
|
|
288
333
|
expect(contract.aiUsageRules.join(" ")).toMatch(/TextInput/);
|
|
334
|
+
expect(contract.aiUsageRules.join(" ")).toMatch(/FontPicker/);
|
|
335
|
+
expect(contract.aiUsageRules).toContain(
|
|
336
|
+
"Use FontPicker as the collection item control when each repeated item is a typography/text-style entity; do not split its font, color, opacity, size, case, letter-spacing, or line-height into sibling collection fields.",
|
|
337
|
+
);
|
|
289
338
|
});
|
|
290
339
|
|
|
291
340
|
it("documents Palette as a constrained design-token color control", () => {
|
|
@@ -358,13 +407,16 @@ describe("Toolcraft template component contracts", () => {
|
|
|
358
407
|
"Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
|
|
359
408
|
);
|
|
360
409
|
expect(contract.aiUsageRules).toContain(
|
|
361
|
-
"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.",
|
|
362
411
|
);
|
|
363
412
|
expect(contract.aiUsageRules).toContain(
|
|
364
|
-
"A user-provided base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output
|
|
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.",
|
|
365
414
|
);
|
|
366
415
|
expect(contract.aiUsageRules).toContain(
|
|
367
|
-
"
|
|
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.",
|
|
417
|
+
);
|
|
418
|
+
expect(contract.aiUsageRules).toContain(
|
|
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.",
|
|
368
420
|
);
|
|
369
421
|
expect(contract.aiUsageRules).toContain(
|
|
370
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.",
|
|
@@ -382,7 +434,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
382
434
|
"Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
|
|
383
435
|
);
|
|
384
436
|
expect(contract.aiUsageRules).toContain(
|
|
385
|
-
"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.",
|
|
386
438
|
);
|
|
387
439
|
expect(contract.aiUsageRules).toContain(
|
|
388
440
|
"After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
|
|
@@ -448,6 +500,24 @@ describe("Toolcraft template component contracts", () => {
|
|
|
448
500
|
const slider = getToolcraftComponentContract("slider");
|
|
449
501
|
const rangeSlider = getToolcraftComponentContract("rangeSlider");
|
|
450
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
|
+
);
|
|
451
521
|
expect(slider.aiUsageRules).toContain(
|
|
452
522
|
"Slider step means numeric snapping only; it does not make the slider visually discrete by itself.",
|
|
453
523
|
);
|
|
@@ -464,16 +534,19 @@ describe("Toolcraft template component contracts", () => {
|
|
|
464
534
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
465
535
|
);
|
|
466
536
|
expect(slider.aiUsageRules).toContain(
|
|
467
|
-
"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.",
|
|
468
538
|
);
|
|
469
539
|
expect(slider.aiUsageRules).toContain(
|
|
470
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.",
|
|
471
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
|
+
);
|
|
472
545
|
expect(slider.aiUsageRules).toContain(
|
|
473
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.",
|
|
474
547
|
);
|
|
475
548
|
expect(slider.aiUsageRules).toContain(
|
|
476
|
-
"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.",
|
|
477
550
|
);
|
|
478
551
|
expect(slider.aiUsageRules).toContain(
|
|
479
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.",
|
|
@@ -499,6 +572,24 @@ describe("Toolcraft template component contracts", () => {
|
|
|
499
572
|
expect(slider.aiUsageRules).toContain(
|
|
500
573
|
"Do not leave a mode-dependent slider active while making the renderer ignore it; the UI must expose the unavailable state.",
|
|
501
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
|
+
);
|
|
502
593
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
503
594
|
"Range slider step means numeric snapping only; it does not make the range slider visually discrete by itself.",
|
|
504
595
|
);
|
|
@@ -515,7 +606,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
515
606
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
516
607
|
);
|
|
517
608
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
518
|
-
"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.",
|
|
519
610
|
);
|
|
520
611
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
521
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.",
|
|
@@ -568,22 +659,31 @@ describe("Toolcraft template component contracts", () => {
|
|
|
568
659
|
'Pair renderer-owned output background color with export.includeBackground in one Background section directly before export settings. Use an equal-width inline row with the export.includeBackground Switch labeled "Include" on the left and the background Color parameter with label false on the right; each control occupies one half of the row.',
|
|
569
660
|
);
|
|
570
661
|
expect(color.aiUsageRules).toContain(
|
|
571
|
-
"Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls
|
|
662
|
+
"Preview, PNG export, and video export must read the runtime background color value instead of hardcoding that background in CSS, Canvas fillStyle, or WebGL clearColor. export.includeBackground controls live preview product background and PNG alpha; it must not make the Toolcraft canvas shell/backing or video output transparent.",
|
|
572
663
|
);
|
|
573
664
|
expect(color.aiUsageRules).toContain(
|
|
574
665
|
"When one short numeric/text field and one Color field configure the same entity, keep them in one two-column inline layout group.",
|
|
575
666
|
);
|
|
576
667
|
expect(color.aiUsageRules).toContain(
|
|
577
|
-
'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.',
|
|
578
669
|
);
|
|
579
670
|
expect(color.aiUsageRules).toContain(
|
|
580
671
|
"Plain Color popovers must not show opacity controls. If opacity is editable, use ColorOpacity instead.",
|
|
581
672
|
);
|
|
582
673
|
expect(color.aiUsageRules).toContain(
|
|
583
|
-
"
|
|
674
|
+
"Decide color label visibility from the user's point of view. Omit labels for color banks that only add palette variety, such as Accent Shades, Bead Colors, or palette.accent1..5.",
|
|
675
|
+
);
|
|
676
|
+
expect(color.aiUsageRules).toContain(
|
|
677
|
+
"Make color label visibility a group-level decision: do not mix labeled and unlabeled items inside one semantic color bank.",
|
|
584
678
|
);
|
|
585
679
|
expect(color.aiUsageRules).toContain(
|
|
586
|
-
"
|
|
680
|
+
"Keep visible labels when colors edit distinct user-facing entities or roles, such as Fill, Stroke, Background, Connector, Object, or Highlight.",
|
|
681
|
+
);
|
|
682
|
+
expect(color.aiUsageRules).toContain(
|
|
683
|
+
"A color bank can share a section with distribution controls such as Spread, Mix, or Randomness and still keep each color item unlabeled when the section title names the palette context.",
|
|
684
|
+
);
|
|
685
|
+
expect(color.aiUsageRules).toContain(
|
|
686
|
+
"If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
|
|
587
687
|
);
|
|
588
688
|
expect(gradient.aiUsageRules).toContain(
|
|
589
689
|
"Gradient is a compound control; acceptance must prove gradient.gradientType, gradient.angle, gradient.stops.position, gradient.stops.color, and gradient.stops.opacity all affect the product output when visible.",
|
|
@@ -600,6 +700,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
600
700
|
expect(fontPicker.aiUsageRules).toContain(
|
|
601
701
|
'Do not recreate FontPicker with a plain Select plus separate sliders; use type: "fontPicker" so the popup mechanics and footer controls stay intact.',
|
|
602
702
|
);
|
|
703
|
+
expect(fontPicker.aiUsageRules).toContain(
|
|
704
|
+
"FontPicker standard/default text color is #FFFFFF with opacity 100; omit color/opacity or use those values unless the prompt or reference explicitly requires a different initial text color.",
|
|
705
|
+
);
|
|
603
706
|
expect(fontPicker.aiUsageRules).toContain(
|
|
604
707
|
"Any product text controlled by FontPicker must render fontId, fontWeight, fontSize, letterSpacing, lineHeight, textCase, color, and opacity in preview and export; do not leave typography values as panel-only runtime state.",
|
|
605
708
|
);
|
|
@@ -743,14 +846,20 @@ describe("Toolcraft template component contracts", () => {
|
|
|
743
846
|
"Custom renderers must define performance budgets for media import, preview updates, control drags, and export/copy before implementation.",
|
|
744
847
|
);
|
|
745
848
|
expect(contract.aiUsageRules).toContain(
|
|
746
|
-
"Controls that change renderer workload
|
|
849
|
+
"Controls that change renderer workload by changing output dimensions, element count, density, sample count, iteration count, blur/filter radius, shader branch cost, media processing, text layout, or export quality must be tested at min, default, and max values.",
|
|
747
850
|
);
|
|
748
851
|
expect(contract.aiUsageRules).toContain(
|
|
749
|
-
"
|
|
852
|
+
"For workload control scenarios, stressFixture is the tested control value. If the app has an independent heavy baseline such as large media, long text, many items, high render scale, or dense source state, declare workloadFixture and apply it before the measured control interaction.",
|
|
853
|
+
);
|
|
854
|
+
expect(contract.aiUsageRules).toContain(
|
|
855
|
+
"Hash differs is not enough for workload controls; tests must assert semantic direction, such as density increasing item count or size changes reducing/increasing rendered cells.",
|
|
750
856
|
);
|
|
751
857
|
expect(contract.aiUsageRules).toContain(
|
|
752
858
|
"Performance tests must use representative fixtures and the same renderer/export path as the running app, not only tiny 32px fixtures or isolated helper state.",
|
|
753
859
|
);
|
|
860
|
+
expect(contract.aiUsageRules).toContain(
|
|
861
|
+
'Media-import workload fixtures and media workload baselines must use fixture kind "media" with numeric width and height at least 1920x1080-equivalent; 640x480 preview fixtures cannot satisfy upload, effect-control, or image-processing performance coverage.',
|
|
862
|
+
);
|
|
754
863
|
expect(contract.aiUsageRules).toContain(
|
|
755
864
|
"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.",
|
|
756
865
|
);
|
|
@@ -790,8 +899,19 @@ describe("Toolcraft template component contracts", () => {
|
|
|
790
899
|
expect(contract.aiUsageRules).toContain(
|
|
791
900
|
'productRepresentation "mixed" is valid only when rendererTechnique.layers proves at least two different content families.',
|
|
792
901
|
);
|
|
902
|
+
expect(contract.aiUsageRules).toContain(
|
|
903
|
+
"Custom renderer apps must declare rendererPipeline with render passes, cache keys, execution location, preview/export quality, and interaction invalidation before implementation.",
|
|
904
|
+
);
|
|
905
|
+
expect(contract.aiUsageRules).toContain(
|
|
906
|
+
"Render Pipeline Inventory must explain which runtime targets invalidate each expensive pass; high-frequency interactions such as animation frames, drag, zoom, pan, timeline playback, and mask movement must not invalidate upstream decode/preprocess/pixel-transform work unless that target truly changes the upstream result.",
|
|
907
|
+
);
|
|
908
|
+
expect(contract.aiUsageRules).toContain(
|
|
909
|
+
"Cache-sensitive render passes such as decode, preprocess, pixel-transform, text-layout, rasterize, and composite must declare cache keys so tests can reject full recomputation on every control change.",
|
|
910
|
+
);
|
|
793
911
|
expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique/);
|
|
794
912
|
expect(contract.aiUsageRules.join("\n")).toMatch(/rendererTechnique\.layers/);
|
|
913
|
+
expect(contract.aiUsageRules.join("\n")).toMatch(/rendererPipeline/);
|
|
914
|
+
expect(contract.aiUsageRules.join("\n")).toMatch(/Render Pipeline Inventory/);
|
|
795
915
|
expect(contract.aiUsageRules.join("\n")).toMatch(/sourceRepresentation/);
|
|
796
916
|
expect(contract.aiUsageRules.join("\n")).toMatch(/productRepresentation/);
|
|
797
917
|
expect(contract.aiUsageRules.join("\n")).toMatch(/previewRenderer/);
|
|
@@ -809,11 +929,14 @@ describe("Toolcraft template component contracts", () => {
|
|
|
809
929
|
"Text-output and vector-output visible previews must preserve native output fidelity. Do not render a low-resolution offscreen canvas or texture and upscale it to the product size.",
|
|
810
930
|
);
|
|
811
931
|
expect(contract.aiUsageRules).toContain(
|
|
812
|
-
"Pixel-output renderers must
|
|
932
|
+
"Pixel-output renderers must treat WebGL/WebGPU as the default candidate even when the scene is static; Canvas 2D is allowed only when measured worst-case evidence shows the CPU path preserves quality and remains responsive.",
|
|
813
933
|
);
|
|
814
934
|
expect(contract.aiUsageRules).toContain(
|
|
815
935
|
"Procedural pixel renderers, shader-like effects, animated mesh gradients, and large exportable previews should use WebGL or WebGPU for pixel work instead of main-thread ImageData loops.",
|
|
816
936
|
);
|
|
937
|
+
expect(contract.aiUsageRules).toContain(
|
|
938
|
+
"Detail-heavy Canvas 2D pixel/media renderers may stay on CPU only when rendererTechnique records measured stress evidence for rejecting WebGL/WebGPU; if the heavy media stress fails, move pixel work to GPU instead of lowering quality.",
|
|
939
|
+
);
|
|
817
940
|
expect(contract.aiUsageRules).toContain(
|
|
818
941
|
"WebGL and WebGPU renderers must initialize contexts, programs, shaders, pipelines, textures, and large buffers once, then update uniforms or stable buffers when controls change.",
|
|
819
942
|
);
|
|
@@ -830,11 +953,14 @@ describe("Toolcraft template component contracts", () => {
|
|
|
830
953
|
"Animated preview renderers must suspend or coalesce non-essential animation work while the user drags, pans, pinches, zooms, or centers the canvas viewport, then resume from the correct timeline or autonomous time without changing the user's play/pause state.",
|
|
831
954
|
);
|
|
832
955
|
expect(contract.aiUsageRules).toContain(
|
|
833
|
-
"If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless the
|
|
956
|
+
"If a generated app uses ImageData, getImageData, or putImageData for procedural output, performance validation must fail unless rendererTechnique records measured WebGL/WebGPU comparison evidence that the CPU path preserves quality and responsiveness, or the renderer is moved to GPU.",
|
|
834
957
|
);
|
|
835
958
|
expect(contract.aiUsageRules).toContain(
|
|
836
959
|
"Performance matrices must declare rendererStrategy so tests can distinguish none, dom, svg, canvas-2d, webgl, and webgpu renderer paths.",
|
|
837
960
|
);
|
|
961
|
+
expect(contract.aiUsageRules).toContain(
|
|
962
|
+
"If a renderer cannot meet the budget, first optimize renderer technique, caching, invalidation, scheduling, and critical-path work. Only change exposed product ranges, work units, or controls after recording measured evidence that the requested quality ceiling is impossible; do not silently reduce product quality to pass budgets.",
|
|
963
|
+
);
|
|
838
964
|
});
|
|
839
965
|
|
|
840
966
|
it("documents reference runtime clone mode as a tested composition contract", () => {
|
|
@@ -1156,10 +1282,10 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1156
1282
|
'Product-output apps must expose a dedicated "Background" section directly before the first export settings section. With PNG export that first section is Image Export; with video-only export it is Video Export.',
|
|
1157
1283
|
);
|
|
1158
1284
|
expect(contract.aiUsageRules).toContain(
|
|
1159
|
-
"Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas
|
|
1285
|
+
"Product-output apps must pass the includeBackground runtime value to createToolcraftPngExportCanvas for PNG alpha and call shouldIncludeToolcraftPreviewBackground(state) for live preview product background.",
|
|
1160
1286
|
);
|
|
1161
1287
|
expect(contract.aiUsageRules).toContain(
|
|
1162
|
-
"PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently
|
|
1288
|
+
"PNG export must use createToolcraftPngExportCanvas so background transparency and selected image dimensions or retina fallback are applied consistently; turning Include off makes preview product background and PNG alpha transparent without hiding the Toolcraft canvas backing or video background.",
|
|
1163
1289
|
);
|
|
1164
1290
|
expect(contract.aiUsageRules).toContain(
|
|
1165
1291
|
"Video export must keep product background and use getToolcraftRetinaExportSize for retina dimensions.",
|
|
@@ -1184,15 +1310,39 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1184
1310
|
it("documents file upload ownership across single-layer and multi-layer apps", () => {
|
|
1185
1311
|
const contract = getToolcraftComponentContract("fileDrop");
|
|
1186
1312
|
|
|
1187
|
-
expect(contract.commands).toEqual(["media.delete", "media.import"]);
|
|
1313
|
+
expect(contract.commands).toEqual(["media.delete", "media.import", "media.reorder"]);
|
|
1314
|
+
expect(contract.aiUsageRules).toContain(
|
|
1315
|
+
'Use fileDrop with assetKind: "image" for image-only source media and assetKind: "file" for arbitrary uploaded files.',
|
|
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
|
+
);
|
|
1188
1323
|
expect(contract.aiUsageRules).toContain(
|
|
1189
1324
|
"In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
|
|
1190
1325
|
);
|
|
1326
|
+
expect(contract.aiUsageRules).toContain(
|
|
1327
|
+
"In file mode, the runtime shows uploaded files as a sortable list with paperclip icons, file names, remove buttons, and --border/5 separators.",
|
|
1328
|
+
);
|
|
1329
|
+
expect(contract.aiUsageRules).toContain(
|
|
1330
|
+
"In single-layer apps, global Reset controls and section reset must remove uploaded fileDrop source media and return the fileDrop target to defaultValue.",
|
|
1331
|
+
);
|
|
1191
1332
|
expect(contract.aiUsageRules).toContain(
|
|
1192
1333
|
"Use fileDrop with multiple: true when the app needs several uploaded images as one source set; do not build a custom thumbnail uploader for this.",
|
|
1193
1334
|
);
|
|
1194
1335
|
expect(contract.aiUsageRules).toContain(
|
|
1195
|
-
"When multiple uploaded images are present, the runtime appends media, shows a four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
|
|
1336
|
+
"When multiple uploaded images are present, the runtime appends media, shows a sortable four-column preview grid, puts the add-more tile last, and exposes per-image removal.",
|
|
1337
|
+
);
|
|
1338
|
+
expect(contract.aiUsageRules).toContain(
|
|
1339
|
+
"Canvas drops route to the first visible matching fileDrop target by asset kind: image files prefer image uploaders, non-image files prefer file uploaders, and file uploaders accept images only when no image uploader matches.",
|
|
1340
|
+
);
|
|
1341
|
+
expect(contract.aiUsageRules).toContain(
|
|
1342
|
+
"Dragging thumbnails reorders runtime mediaAssets; preview, export, and renderer mapping must consume that media order instead of maintaining a separate product-only order.",
|
|
1343
|
+
);
|
|
1344
|
+
expect(contract.aiUsageRules).toContain(
|
|
1345
|
+
"Do not create custom upload buttons, file lists, or file sorting for generic source uploads when fileDrop can represent the source set.",
|
|
1196
1346
|
);
|
|
1197
1347
|
expect(contract.aiUsageRules).toContain(
|
|
1198
1348
|
"In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
|
|
@@ -1226,6 +1376,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1226
1376
|
expect(contract.aiUsageRules).toContain(
|
|
1227
1377
|
"Render multiple related color fields in one section with at most two colors per row.",
|
|
1228
1378
|
);
|
|
1379
|
+
expect(contract.aiUsageRules).toContain(
|
|
1380
|
+
"If a multi-color bank has an odd trailing plain Color, keep that last Color at half width; only ColorOpacity or intentionally full-width compound controls occupy a full row.",
|
|
1381
|
+
);
|
|
1229
1382
|
});
|
|
1230
1383
|
|
|
1231
1384
|
it("documents CodeTextarea as generic multiline text input", () => {
|
|
@@ -1238,10 +1391,16 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1238
1391
|
"CodeTextarea is the multiline text input for any potentially long value, not only source code.",
|
|
1239
1392
|
);
|
|
1240
1393
|
expect(contract.aiUsageRules).toContain(
|
|
1241
|
-
"
|
|
1394
|
+
"Do not use CodeTextarea for short single-line canvas text, button labels, names, titles, captions, badges, or short tokens; use TextInput.",
|
|
1242
1395
|
);
|
|
1243
1396
|
expect(contract.aiUsageRules).toContain(
|
|
1244
|
-
"Use
|
|
1397
|
+
"Use text for short single-line strings such as names, button labels, small numeric values, compact prompts, titles, captions, and short tokens.",
|
|
1398
|
+
);
|
|
1399
|
+
expect(contract.aiUsageRules).toContain(
|
|
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.",
|
|
1245
1404
|
);
|
|
1246
1405
|
expect(contract.aiUsageRules).toContain(
|
|
1247
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.",
|
|
@@ -1257,6 +1416,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1257
1416
|
expect(contract.visualComponent).toBe("TextInput");
|
|
1258
1417
|
expect(contract.defaultSectionLayout).toBe("grouped");
|
|
1259
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
|
+
);
|
|
1260
1422
|
expect(contract.aiUsageRules).toContain(
|
|
1261
1423
|
'TextInput commitMode defaults to "content": text content, prompts, names, tokens, titles, and instructions apply while typing.',
|
|
1262
1424
|
);
|