@pixel-point/toolcraft 0.0.4 → 0.0.6
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/scripts/prepare-pack.mjs +5 -0
- package/src/generate.mjs +13 -0
- package/src/generate.test.mjs +6 -0
- package/templates/runtime/contracts/component-contracts.test.ts +59 -2
- package/templates/runtime/contracts/component-contracts.ts +23 -2
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/react/canvas-shell.test.tsx +7 -7
- package/templates/runtime/react/controls-panel.test.tsx +269 -0
- package/templates/runtime/react/controls-panel.tsx +133 -24
- package/templates/runtime/react/settings-transfer.test.ts +3 -3
- package/templates/runtime/react/timeline-panel.test.tsx +69 -0
- package/templates/runtime/react/timeline-panel.tsx +98 -10
- package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
- package/templates/runtime/schema/define-toolcraft.test.ts +77 -1
- package/templates/runtime/schema/define-toolcraft.ts +138 -5
- package/templates/runtime/schema/runtime-targets.ts +1 -0
- package/templates/runtime/schema/types.ts +23 -1
- package/templates/runtime/state/canvas-zoom.ts +1 -1
- package/templates/runtime/state/create-template-state.test.ts +6 -6
- package/templates/runtime/state/reducer.test.ts +86 -8
- package/templates/runtime/state/reducer.ts +41 -22
- package/templates/runtime/state/types.ts +1 -0
- package/templates/starter/AGENTS.md +2 -2
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +1 -1
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +13 -1
- package/templates/starter/docs/toolcraft/performance.md +5 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +10 -4
- package/templates/starter/gitignore +36 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +55 -0
- package/templates/starter/src/app/starter-acceptance.ts +67 -1
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
- package/templates/ui/components/controls/slider/slider-value.ts +48 -5
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
package/package.json
CHANGED
package/scripts/prepare-pack.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
3
4
|
|
|
4
5
|
import { copyDirectory, removeDirectory } from "../src/copy-recursive.mjs";
|
|
5
6
|
|
|
@@ -27,3 +28,7 @@ await removeDirectory(templatesRoot);
|
|
|
27
28
|
for (const source of sources) {
|
|
28
29
|
await copyDirectory(source.from, source.to);
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
const starterGitignorePath = path.join(templatesRoot, "starter/.gitignore");
|
|
33
|
+
const starterPackGitignorePath = path.join(templatesRoot, "starter/gitignore");
|
|
34
|
+
await fs.copyFile(starterGitignorePath, starterPackGitignorePath);
|
package/src/generate.mjs
CHANGED
|
@@ -88,6 +88,18 @@ async function renameGeneratedAppFiles(targetDir) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
async function restoreGeneratedGitignore(targetDir) {
|
|
92
|
+
const npmSafeGitignorePath = path.join(targetDir, "gitignore");
|
|
93
|
+
const gitignorePath = path.join(targetDir, ".gitignore");
|
|
94
|
+
|
|
95
|
+
if (!(await pathExists(npmSafeGitignorePath))) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await fs.rm(gitignorePath, { force: true });
|
|
100
|
+
await fs.rename(npmSafeGitignorePath, gitignorePath);
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
async function removeToolcraftTestFiles(toolcraftRoot) {
|
|
92
104
|
async function visit(currentDir) {
|
|
93
105
|
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
@@ -181,6 +193,7 @@ export async function generateToolcraft(options = {}) {
|
|
|
181
193
|
await ensureWritableTargetDirectory(targetDir, { force: options.force });
|
|
182
194
|
|
|
183
195
|
await copyDirectory(sourcePaths.starterDir, targetDir);
|
|
196
|
+
await restoreGeneratedGitignore(targetDir);
|
|
184
197
|
await renameGeneratedAppFiles(targetDir);
|
|
185
198
|
|
|
186
199
|
const toolcraftRoot = path.join(targetDir, "src/toolcraft");
|
package/src/generate.test.mjs
CHANGED
|
@@ -116,6 +116,12 @@ describe("generateToolcraft", () => {
|
|
|
116
116
|
assert.ok(await fs.stat(path.join(targetDir, "e2e/performance-helpers.ts")));
|
|
117
117
|
assert.ok(await fs.stat(path.join(targetDir, "e2e/product-observable-helpers.ts")));
|
|
118
118
|
assert.ok(await fs.stat(path.join(targetDir, "e2e/canvas-handle-helpers.ts")));
|
|
119
|
+
const gitignoreSource = await fs.readFile(path.join(targetDir, ".gitignore"), "utf8");
|
|
120
|
+
assert.match(gitignoreSource, /node_modules/);
|
|
121
|
+
assert.match(gitignoreSource, /dist/);
|
|
122
|
+
assert.match(gitignoreSource, /playwright-report/);
|
|
123
|
+
assert.match(gitignoreSource, /\.env\.\*/);
|
|
124
|
+
await assert.rejects(() => fs.stat(path.join(targetDir, "gitignore")), /ENOENT/);
|
|
119
125
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/check-ai-skills.mjs")));
|
|
120
126
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.mjs")));
|
|
121
127
|
assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.test.mjs")));
|
|
@@ -345,6 +345,24 @@ describe("Toolcraft template component contracts", () => {
|
|
|
345
345
|
expect(contract.aiUsageRules).toContain(
|
|
346
346
|
"The runtime Canvas width and Canvas height block uses the technical Setup section and renders without a visible section heading; do not add a separate Canvas section label above these fields.",
|
|
347
347
|
);
|
|
348
|
+
expect(contract.aiUsageRules).toContain(
|
|
349
|
+
"When the user manually edits Canvas width or Canvas height, the runtime keeps the typed dimension, keeps the other dimension unchanged, switches Aspect ratio to Custom, and shows the reduced current ratio in the custom ratio inputs.",
|
|
350
|
+
);
|
|
351
|
+
expect(contract.aiUsageRules).toContain(
|
|
352
|
+
"Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
|
|
353
|
+
);
|
|
354
|
+
expect(contract.aiUsageRules).toContain(
|
|
355
|
+
"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 1x to 2x without changing visible canvas size, and adding/enabling it requires a full pnpm verify:perf checkpoint.",
|
|
356
|
+
);
|
|
357
|
+
expect(contract.aiUsageRules).toContain(
|
|
358
|
+
"After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
|
|
359
|
+
);
|
|
360
|
+
expect(contract.aiUsageRules).toContain(
|
|
361
|
+
"Performance fixes for canvas.renderScale must preserve the selected visual quality; do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp canvas.renderScale below the user's chosen value to pass budgets.",
|
|
362
|
+
);
|
|
363
|
+
expect(contract.aiUsageRules).toContain(
|
|
364
|
+
"Do not enable canvas.renderScale for DOM/SVG/vector-native previews; preserve vector fidelity through native vector rendering instead of raster supersampling.",
|
|
365
|
+
);
|
|
348
366
|
});
|
|
349
367
|
|
|
350
368
|
it("documents persistence as a runtime-owned policy instead of ad hoc localStorage", () => {
|
|
@@ -392,7 +410,7 @@ describe("Toolcraft template component contracts", () => {
|
|
|
392
410
|
"A settings-transfer section with only Export Settings and Import Settings means canvas sizing is not editable-output or canvas size controls already exist elsewhere.",
|
|
393
411
|
);
|
|
394
412
|
expect(contract.aiUsageRules).toContain(
|
|
395
|
-
"When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width,
|
|
413
|
+
"When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, and optional Resolution scale in that order and renders without a visible section heading.",
|
|
396
414
|
);
|
|
397
415
|
});
|
|
398
416
|
|
|
@@ -415,6 +433,21 @@ describe("Toolcraft template component contracts", () => {
|
|
|
415
433
|
expect(slider.aiUsageRules).toContain(
|
|
416
434
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
417
435
|
);
|
|
436
|
+
expect(slider.aiUsageRules).toContain(
|
|
437
|
+
"Use slider unit only for measurement or scale suffixes such as %, px, °, x, s, ms, fps, rows/cols, or similar domain units.",
|
|
438
|
+
);
|
|
439
|
+
expect(slider.aiUsageRules).toContain(
|
|
440
|
+
"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.",
|
|
441
|
+
);
|
|
442
|
+
expect(slider.aiUsageRules).toContain(
|
|
443
|
+
"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.",
|
|
444
|
+
);
|
|
445
|
+
expect(slider.aiUsageRules).toContain(
|
|
446
|
+
"Compact symbol/CSS units render tight, such as 70%, 24px, 1.2x, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
|
|
447
|
+
);
|
|
448
|
+
expect(slider.aiUsageRules).toContain(
|
|
449
|
+
"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.",
|
|
450
|
+
);
|
|
418
451
|
expect(slider.aiUsageRules).toContain(
|
|
419
452
|
"Schema sliders render stacked at full width; do not put sliders in two-column inline layout groups.",
|
|
420
453
|
);
|
|
@@ -451,6 +484,15 @@ describe("Toolcraft template component contracts", () => {
|
|
|
451
484
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
452
485
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
453
486
|
);
|
|
487
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
488
|
+
"Use rangeSlider unit only for measurement or scale suffixes; do not use it for repeated entity nouns already named by the section or label.",
|
|
489
|
+
);
|
|
490
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
491
|
+
"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.",
|
|
492
|
+
);
|
|
493
|
+
expect(rangeSlider.aiUsageRules).toContain(
|
|
494
|
+
"Compact symbol/CSS units render tight, such as 20% – 80% or 12px – 48px; word units render with a space when truly needed.",
|
|
495
|
+
);
|
|
454
496
|
expect(rangeSlider.aiUsageRules).toContain(
|
|
455
497
|
"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.",
|
|
456
498
|
);
|
|
@@ -686,7 +728,13 @@ describe("Toolcraft template component contracts", () => {
|
|
|
686
728
|
"Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
|
|
687
729
|
);
|
|
688
730
|
expect(contract.aiUsageRules).toContain(
|
|
689
|
-
"A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
|
|
731
|
+
"A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, canvas.renderScale or the Resolution scale retina slider is added/enabled, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
|
|
732
|
+
);
|
|
733
|
+
expect(contract.aiUsageRules).toContain(
|
|
734
|
+
"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.",
|
|
735
|
+
);
|
|
736
|
+
expect(contract.aiUsageRules).toContain(
|
|
737
|
+
"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.",
|
|
690
738
|
);
|
|
691
739
|
expect(contract.aiUsageRules).toContain(
|
|
692
740
|
"Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
|
|
@@ -849,6 +897,9 @@ describe("Toolcraft template component contracts", () => {
|
|
|
849
897
|
expect(contract.aiUsageRules).toContain(
|
|
850
898
|
"If there is no useful product-specific explanation, omit control.description; the runtime should not show a help tooltip for that label.",
|
|
851
899
|
);
|
|
900
|
+
expect(contract.aiUsageRules).toContain(
|
|
901
|
+
"Do not add control.description to sequential colors such as Color 1, Color 2, or simple palette controls such as Spread when the section title already names the color or palette context.",
|
|
902
|
+
);
|
|
852
903
|
expect(contract.aiUsageRules).toContain(
|
|
853
904
|
"For compound controls such as FontPicker, do not use control.description to enumerate the control's owned fields. FontPicker descriptions must not recap font family, weight, size, case, color, opacity, letter spacing, or line height; use description only for non-obvious product scope or omit it.",
|
|
854
905
|
);
|
|
@@ -1101,6 +1152,12 @@ describe("Toolcraft template component contracts", () => {
|
|
|
1101
1152
|
expect(contract.aiUsageRules).toContain(
|
|
1102
1153
|
"In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
|
|
1103
1154
|
);
|
|
1155
|
+
expect(contract.aiUsageRules).toContain(
|
|
1156
|
+
"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.",
|
|
1157
|
+
);
|
|
1158
|
+
expect(contract.aiUsageRules).toContain(
|
|
1159
|
+
"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.",
|
|
1160
|
+
);
|
|
1104
1161
|
expect(contract.aiUsageRules).toContain(
|
|
1105
1162
|
"In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
|
|
1106
1163
|
);
|
|
@@ -49,6 +49,11 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
49
49
|
'Small semantic integer domains such as rows, cols, gaps, jitter, counts, levels, bands, passes, points, tiles, and segments must use variant: "discrete".',
|
|
50
50
|
'Finite animation step domains such as flip depth, character count, glyph steps, and frame steps must use variant: "discrete" when the marker count stays within the Toolcraft visual budget.',
|
|
51
51
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
52
|
+
"Use slider unit only for measurement or scale suffixes such as %, px, °, x, s, ms, fps, rows/cols, or similar domain units.",
|
|
53
|
+
"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.",
|
|
54
|
+
"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.",
|
|
55
|
+
"Compact symbol/CSS units render tight, such as 70%, 24px, 1.2x, and 8s; word units render with a space, such as 5 cols, when they are truly needed.",
|
|
56
|
+
"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.",
|
|
52
57
|
"Schema sliders render stacked at full width; do not put sliders in two-column inline layout groups.",
|
|
53
58
|
"The fontPicker component is the only built-in exception with two internal footer sliders for letter spacing and line height.",
|
|
54
59
|
"For a small named option set, prefer Select or Segmented instead of forcing a discrete Slider.",
|
|
@@ -94,6 +99,9 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
94
99
|
'Small semantic integer domains such as rows, cols, gaps, jitter, counts, levels, bands, passes, points, tiles, and segments must use variant: "discrete".',
|
|
95
100
|
'Finite animation step domains such as flip depth, character count, glyph steps, and frame steps must use variant: "discrete" when the marker count stays within the Toolcraft visual budget.',
|
|
96
101
|
"Large or precision stepped ranges such as speed, FPS, rate, duration, density, size, and intensity stay visually continuous even when they declare step.",
|
|
102
|
+
"Use rangeSlider unit only for measurement or scale suffixes; do not use it for repeated entity nouns already named by the section or label.",
|
|
103
|
+
"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.",
|
|
104
|
+
"Compact symbol/CSS units render tight, such as 20% – 80% or 12px – 48px; word units render with a space when truly needed.",
|
|
97
105
|
"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.",
|
|
98
106
|
"RangeSlider defaultValue must start with different lower and upper values so the two-thumb control does not collapse into a single-value slider.",
|
|
99
107
|
"Manual range value editing accepts common separators such as slash, hyphen, spaces, and dashes; do not create custom parsers for RangeSlider labels.",
|
|
@@ -806,6 +814,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
806
814
|
],
|
|
807
815
|
layoutConstraints: [
|
|
808
816
|
"FileDrop lives in the controls panel; single-layer apps use its preview and clear behavior.",
|
|
817
|
+
"When fileDrop has multiple: true and more than one image is present, the runtime renders a four-column thumbnail grid with the add-more tile last.",
|
|
809
818
|
],
|
|
810
819
|
requiredAcceptance: [
|
|
811
820
|
"Prove file import changes media state and product output; prove clear removes source material.",
|
|
@@ -814,6 +823,8 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
814
823
|
aiUsageRules: [
|
|
815
824
|
"Use fileDrop for source material uploads in the controls panel, not on the canvas.",
|
|
816
825
|
"In single-layer apps, the runtime shows the uploaded image as the fileDrop preview and provides the clear action.",
|
|
826
|
+
"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.",
|
|
827
|
+
"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.",
|
|
817
828
|
"In multi-layer apps, deletion and visibility belong to the Layers panel; fileDrop remains an upload target.",
|
|
818
829
|
],
|
|
819
830
|
commands: ["media.delete", "media.import"],
|
|
@@ -942,11 +953,18 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
942
953
|
"Choose canvas.sizing.mode from product context instead of copying a universal 1024px artboard.",
|
|
943
954
|
"Use intrinsic-media for single-layer upload/generation apps so imported media natural size becomes canvas.size.",
|
|
944
955
|
"Use editable-output by default for generated, exportable, shader, poster, badge, wall, banner, thumbnail, and product-output apps where users should see or edit width and height.",
|
|
956
|
+
"When no explicit product size is provided, the runtime default canvas is 16:9 at 1920x1080; do not reset a new product-output app to 1:1 unless the product meaning requires it.",
|
|
945
957
|
"A user-provided base/default size is not a reason to remove size controls; model it as canvas.size plus editable-output unless the prompt or reference explicitly locks output dimensions.",
|
|
946
958
|
"Use fixed-output only when the product output size must not be user-editable, and prove that lock with canvasSizingCoverage fixed-output-size acceptance.",
|
|
947
959
|
"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.",
|
|
948
960
|
"If canvas.size is provided without an explicit sizing mode, defineToolcraft treats it as editable-output and adds Canvas width and Canvas height controls.",
|
|
949
961
|
"The runtime Canvas width and Canvas height block uses the technical Setup section and renders without a visible section heading; do not add a separate Canvas section label above these fields.",
|
|
962
|
+
"When the user manually edits Canvas width or Canvas height, the runtime keeps the typed dimension, keeps the other dimension unchanged, switches Aspect ratio to Custom, and shows the reduced current ratio in the custom ratio inputs.",
|
|
963
|
+
"Aspect ratio presets are the only interaction that may resize both canvas dimensions from a preset; manual size inputs are exact output dimensions.",
|
|
964
|
+
"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 1x to 2x without changing visible canvas size, and adding/enabling it requires a full pnpm verify:perf checkpoint.",
|
|
965
|
+
"After enabling canvas.renderScale, verify that canvas preview stays responsive while dragging sliders and other high-frequency controls at the selected scale.",
|
|
966
|
+
"Performance fixes for canvas.renderScale must preserve the selected visual quality; do not silently downsample, stretch a lower-resolution backing canvas, blur output, or clamp canvas.renderScale below the user's chosen value to pass budgets.",
|
|
967
|
+
"Do not enable canvas.renderScale for DOM/SVG/vector-native previews; preserve vector fidelity through native vector rendering instead of raster supersampling.",
|
|
950
968
|
],
|
|
951
969
|
capabilities: ["drag", "zoom", "radar", "upload", "editable-size"],
|
|
952
970
|
commands: [
|
|
@@ -993,7 +1011,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
993
1011
|
"Do not hand-roll settings import/export through app routes, hidden file inputs, or panelActions.",
|
|
994
1012
|
"Settings transfer appears as the first technical Setup controls-panel section when enabled and renders without a visible section heading; it imports and exports control values, canvas size, and timeline state.",
|
|
995
1013
|
"A settings-transfer section with only Export Settings and Import Settings means canvas sizing is not editable-output or canvas size controls already exist elsewhere.",
|
|
996
|
-
"When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width,
|
|
1014
|
+
"When settings transfer and editable-output canvas sizing are both enabled, the first technical Setup runtime section contains Export Settings, Import Settings, Aspect ratio, Canvas width, Canvas height, and optional Resolution scale in that order and renders without a visible section heading.",
|
|
997
1015
|
"Keep sticky footer panelActions for product delivery actions only, such as Export PNG, Export Video, Copy, Generate, Apply, or Download.",
|
|
998
1016
|
],
|
|
999
1017
|
capabilities: ["settings-import-export"],
|
|
@@ -1040,7 +1058,9 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
1040
1058
|
"Expensive renderers must cache decoded media, source pixels, glyph atlases, gradients, and other reusable inputs by media id, canvas size, and stable control keys.",
|
|
1041
1059
|
"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.",
|
|
1042
1060
|
"Performance matrices must declare rendererWorkload as none, simple-composition, text-output, vector-output, or pixel-output.",
|
|
1043
|
-
"A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
|
|
1061
|
+
"A full performance checkpoint must run with pnpm verify:perf when the first working app version exists, renderer/canvas/animation/export/timeline/layers change, canvas.renderScale or the Resolution scale retina slider is added/enabled, a bug that previously broke functionality is fixed, any performance optimization lands, or the user requests performance, lag, jank, animation speed, or drag/zoom stabilization work.",
|
|
1062
|
+
"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.",
|
|
1063
|
+
"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.",
|
|
1044
1064
|
"Renderer specs must include a Renderer Technique Decision Matrix with sourceRepresentation, productRepresentation, previewRenderer, exportRenderer, rendererWorkload, rendererStrategy, whyNotAlternativeStrategies, fidelityRisks, and performanceRisks.",
|
|
1045
1065
|
"Custom renderer apps must mirror the Renderer Technique Decision Matrix in typed rendererTechnique config so validation can reject contradictory renderer choices.",
|
|
1046
1066
|
"Custom renderer specs must include a Renderer Layer Inventory and mirror it in typed rendererTechnique.layers so dense raster backgrounds cannot silently rasterize semantic foreground output.",
|
|
@@ -1126,6 +1146,7 @@ export const TOOLCRAFT_COMPONENT_CONTRACTS = {
|
|
|
1126
1146
|
"Use control.description for the short help tooltip shown beside visible labels. It must describe the product behavior or output affected by the control, not restate the label.",
|
|
1127
1147
|
"Do not write label-recap descriptions such as Adjusts Opacity, Controls Speed, or Sets Background.",
|
|
1128
1148
|
"If there is no useful product-specific explanation, omit control.description; the runtime should not show a help tooltip for that label.",
|
|
1149
|
+
"Do not add control.description to sequential colors such as Color 1, Color 2, or simple palette controls such as Spread when the section title already names the color or palette context.",
|
|
1129
1150
|
"For compound controls such as FontPicker, do not use control.description to enumerate the control's owned fields. FontPicker descriptions must not recap font family, weight, size, case, color, opacity, letter spacing, or line height; use description only for non-obvious product scope or omit it.",
|
|
1130
1151
|
"The runtime renders a filled Phosphor question icon beside each visible ControlFieldLabel; generated apps must not hand-build their own help icon beside built-in labels.",
|
|
1131
1152
|
"If a source label is unavoidably long, keep the visible label concise and rely on the native title tooltip for the full text.",
|
|
@@ -237,7 +237,7 @@ export const TOOLCRAFT_DECISION_CONTRACT = [
|
|
|
237
237
|
currentConstraint:
|
|
238
238
|
"Performance coverage currently asks every visible non-action control for a performance scenario.",
|
|
239
239
|
desiredBehavior:
|
|
240
|
-
"Heavy workload controls get min/default/max workload coverage; ordinary controls get lightweight responsiveness coverage so they cannot hang or break input. Animated previews suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions without changing user playback state. A full performance checkpoint is required when the first working version of an app exists, when renderer/canvas/animation/export/timeline/layers change, after fixing a bug that previously broke functionality, after any performance optimization, and whenever the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom. Browser performance tests read budgets from typed performance config and run sequentially for stable measurements.",
|
|
240
|
+
"Heavy workload controls get min/default/max workload coverage; ordinary controls get lightweight responsiveness coverage so they cannot hang or break input. Animated previews suspend or coalesce non-essential animation work during canvas drag, pan, pinch, zoom, and radar/center interactions without changing user playback state. A full performance checkpoint is required when the first working version of an app exists, when renderer/canvas/animation/export/timeline/layers change, when canvas.renderScale or the Resolution scale retina slider is added/enabled, after fixing a bug that previously broke functionality, after any performance optimization, and whenever the user asks to optimize performance, fix lag, remove jank, speed up animation, or stabilize drag/zoom. Performance fixes must preserve the selected render scale and must not pass budgets by silently downsampling, stretching a lower-resolution backing canvas, blurring output, or clamping canvas.renderScale below the user's chosen value. Browser performance tests read budgets from typed performance config and run sequentially for stable measurements.",
|
|
241
241
|
enforcement: ["performance-validator", "browser-helper", "starter-agents"],
|
|
242
242
|
id: "performance-coverage-levels",
|
|
243
243
|
level: "invariant",
|
|
@@ -226,7 +226,7 @@ describe("CanvasShell", () => {
|
|
|
226
226
|
await waitFor(() => {
|
|
227
227
|
expect(screen.getByTestId("canvas-offset").textContent).toBe("-8,12");
|
|
228
228
|
});
|
|
229
|
-
expect(screen.getByTestId("canvas-zoom").textContent).toBe("
|
|
229
|
+
expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
|
|
230
230
|
});
|
|
231
231
|
|
|
232
232
|
it("zooms the canvas world from a trackpad pinch around the pointer", async () => {
|
|
@@ -243,7 +243,7 @@ describe("CanvasShell", () => {
|
|
|
243
243
|
const world = canvas.querySelector("[data-toolcraft-canvas-world]") as HTMLElement;
|
|
244
244
|
mockCanvasRect(canvas);
|
|
245
245
|
|
|
246
|
-
expect(world.style.transform).toBe("translate(-50%, -50%) translate(0px, 0px) scale(
|
|
246
|
+
expect(world.style.transform).toBe("translate(-50%, -50%) translate(0px, 0px) scale(1)");
|
|
247
247
|
|
|
248
248
|
const wheelEvent = new WheelEvent("wheel", {
|
|
249
249
|
bubbles: true,
|
|
@@ -257,10 +257,10 @@ describe("CanvasShell", () => {
|
|
|
257
257
|
expect(canvas.dispatchEvent(wheelEvent)).toBe(false);
|
|
258
258
|
expect(wheelEvent.defaultPrevented).toBe(true);
|
|
259
259
|
await waitFor(() => {
|
|
260
|
-
expect(screen.getByTestId("canvas-zoom").textContent).toBe("
|
|
260
|
+
expect(screen.getByTestId("canvas-zoom").textContent).toBe("150");
|
|
261
261
|
});
|
|
262
|
-
expect(world.style.transform).toContain("translate(-
|
|
263
|
-
expect(world.style.transform).toContain("scale(1.
|
|
262
|
+
expect(world.style.transform).toContain("translate(-100px");
|
|
263
|
+
expect(world.style.transform).toContain("scale(1.5)");
|
|
264
264
|
});
|
|
265
265
|
|
|
266
266
|
it("prevents panel pinch gestures without moving the canvas viewport", async () => {
|
|
@@ -294,7 +294,7 @@ describe("CanvasShell", () => {
|
|
|
294
294
|
|
|
295
295
|
expect(overlay.dispatchEvent(wheelEvent)).toBe(false);
|
|
296
296
|
expect(wheelEvent.defaultPrevented).toBe(true);
|
|
297
|
-
expect(screen.getByTestId("canvas-zoom").textContent).toBe("
|
|
297
|
+
expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
|
|
298
298
|
expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
|
|
299
299
|
expect(overlay.closest("[data-toolcraft-canvas-world]")).toBeNull();
|
|
300
300
|
|
|
@@ -306,7 +306,7 @@ describe("CanvasShell", () => {
|
|
|
306
306
|
|
|
307
307
|
expect(overlay.dispatchEvent(scrollEvent)).toBe(true);
|
|
308
308
|
expect(scrollEvent.defaultPrevented).toBe(false);
|
|
309
|
-
expect(screen.getByTestId("canvas-zoom").textContent).toBe("
|
|
309
|
+
expect(screen.getByTestId("canvas-zoom").textContent).toBe("100");
|
|
310
310
|
expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0");
|
|
311
311
|
});
|
|
312
312
|
|
|
@@ -436,6 +436,101 @@ describe("ControlsPanel", () => {
|
|
|
436
436
|
);
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
+
it("suppresses obvious help icons in sequential color sections", () => {
|
|
440
|
+
const schema = defineToolcraft({
|
|
441
|
+
canvas: { enabled: false },
|
|
442
|
+
panels: {
|
|
443
|
+
controls: {
|
|
444
|
+
sections: [
|
|
445
|
+
{
|
|
446
|
+
controls: {
|
|
447
|
+
color1: {
|
|
448
|
+
defaultValue: { hex: "#DFFF1A" },
|
|
449
|
+
description: "Sets the first bead color.",
|
|
450
|
+
label: "Color 1",
|
|
451
|
+
target: "beads.color1",
|
|
452
|
+
type: "color",
|
|
453
|
+
},
|
|
454
|
+
color2: {
|
|
455
|
+
defaultValue: { hex: "#8CFF3A" },
|
|
456
|
+
description: "Sets the second bead color.",
|
|
457
|
+
label: "Color 2",
|
|
458
|
+
target: "beads.color2",
|
|
459
|
+
type: "color",
|
|
460
|
+
},
|
|
461
|
+
color3: {
|
|
462
|
+
defaultValue: { hex: "#F4FF5A" },
|
|
463
|
+
description: "Sets the third bead color.",
|
|
464
|
+
label: "Color 3",
|
|
465
|
+
target: "beads.color3",
|
|
466
|
+
type: "color",
|
|
467
|
+
},
|
|
468
|
+
color4: {
|
|
469
|
+
defaultValue: { hex: "#B8FF2E" },
|
|
470
|
+
description: "Sets the fourth bead color.",
|
|
471
|
+
label: "Color 4",
|
|
472
|
+
target: "beads.color4",
|
|
473
|
+
type: "color",
|
|
474
|
+
},
|
|
475
|
+
color5: {
|
|
476
|
+
defaultValue: { hex: "#ECFF68" },
|
|
477
|
+
description: "Sets the fifth bead color.",
|
|
478
|
+
label: "Color 5",
|
|
479
|
+
target: "beads.color5",
|
|
480
|
+
type: "color",
|
|
481
|
+
},
|
|
482
|
+
colorSpread: {
|
|
483
|
+
defaultValue: 34,
|
|
484
|
+
description:
|
|
485
|
+
"Controls how often beads use colors 2-5 instead of Color 1.",
|
|
486
|
+
label: "Spread",
|
|
487
|
+
max: 100,
|
|
488
|
+
min: 0,
|
|
489
|
+
target: "beads.colorSpread",
|
|
490
|
+
type: "slider",
|
|
491
|
+
unit: "%",
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
layoutGroups: [
|
|
495
|
+
{
|
|
496
|
+
columns: 2,
|
|
497
|
+
controls: ["color1", "color2"],
|
|
498
|
+
layout: "inline",
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
columns: 2,
|
|
502
|
+
controls: ["color3", "color4"],
|
|
503
|
+
layout: "inline",
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
title: "Bead Colors",
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
controls: {
|
|
510
|
+
includeBackground: {
|
|
511
|
+
defaultValue: true,
|
|
512
|
+
description:
|
|
513
|
+
"Controls PNG background transparency while preview and video keep the background.",
|
|
514
|
+
label: "Include",
|
|
515
|
+
target: "export.includeBackground",
|
|
516
|
+
type: "switch",
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
title: "Background",
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
title: "Controls",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
renderControlsPanelWithSchema(schema);
|
|
528
|
+
|
|
529
|
+
expect(screen.queryByRole("button", { name: "Color 5 help" })).toBeNull();
|
|
530
|
+
expect(screen.queryByRole("button", { name: "Spread help" })).toBeNull();
|
|
531
|
+
expect(screen.getByRole("button", { name: "Include help" })).toBeTruthy();
|
|
532
|
+
});
|
|
533
|
+
|
|
439
534
|
it("renders short visible toggle plus parameter rows as equal-width toggle-parameter rows", () => {
|
|
440
535
|
const schema = defineToolcraft({
|
|
441
536
|
canvas: { enabled: false },
|
|
@@ -1534,6 +1629,46 @@ describe("ControlsPanel", () => {
|
|
|
1534
1629
|
container.querySelector('[data-slot="slider"][data-variant="discrete"]'),
|
|
1535
1630
|
).toBeNull();
|
|
1536
1631
|
expect(container.querySelectorAll('[data-slot="slider-marker"]')).toHaveLength(0);
|
|
1632
|
+
expect(container.textContent).toContain("118 cols/s");
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
it("keeps textual slider value labels non-editable", () => {
|
|
1636
|
+
const schema = defineToolcraft({
|
|
1637
|
+
canvas: { enabled: true },
|
|
1638
|
+
panels: {
|
|
1639
|
+
controls: {
|
|
1640
|
+
sections: [
|
|
1641
|
+
{
|
|
1642
|
+
controls: {
|
|
1643
|
+
letterSpacing: {
|
|
1644
|
+
defaultValue: 2,
|
|
1645
|
+
label: "Letter spacing",
|
|
1646
|
+
max: 4,
|
|
1647
|
+
min: 0,
|
|
1648
|
+
step: 1,
|
|
1649
|
+
target: "text.letterSpacing",
|
|
1650
|
+
type: "slider",
|
|
1651
|
+
valueLabel: "Normal",
|
|
1652
|
+
},
|
|
1653
|
+
},
|
|
1654
|
+
title: "Text",
|
|
1655
|
+
},
|
|
1656
|
+
],
|
|
1657
|
+
title: "Generation Controls",
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
renderControlsPanelWithSchema(schema);
|
|
1663
|
+
|
|
1664
|
+
const valueLabel = screen
|
|
1665
|
+
.getAllByText("Normal")
|
|
1666
|
+
.find((node) => node.getAttribute("aria-hidden") !== "true");
|
|
1667
|
+
|
|
1668
|
+
expect(screen.queryByRole("button", { name: "Edit Letter spacing value" })).toBeNull();
|
|
1669
|
+
expect(valueLabel).toBeTruthy();
|
|
1670
|
+
expect(valueLabel?.className).toContain("cursor-default");
|
|
1671
|
+
expect(valueLabel?.className).not.toContain("cursor-text");
|
|
1537
1672
|
});
|
|
1538
1673
|
|
|
1539
1674
|
it("keeps schema sliders stacked even when a layout group asks for an inline row", () => {
|
|
@@ -1589,6 +1724,8 @@ describe("ControlsPanel", () => {
|
|
|
1589
1724
|
expect(container.querySelector('[data-control-layout="inline"]')).toBeNull();
|
|
1590
1725
|
expect(fpsSlider).toBeTruthy();
|
|
1591
1726
|
expect(container.querySelectorAll('[data-slot="slider-marker"]').length).toBeGreaterThan(0);
|
|
1727
|
+
expect(container.textContent).toContain("17 fps");
|
|
1728
|
+
expect(container.textContent).toContain("3.7x");
|
|
1592
1729
|
});
|
|
1593
1730
|
|
|
1594
1731
|
it("passes schema disabled state into slider controls", () => {
|
|
@@ -2283,6 +2420,76 @@ describe("ControlsPanel", () => {
|
|
|
2283
2420
|
expect(screen.getByTestId("media-count").textContent).toBe("0");
|
|
2284
2421
|
});
|
|
2285
2422
|
|
|
2423
|
+
it("renders multi-image file uploads as an add-last thumbnail grid", () => {
|
|
2424
|
+
const schema = defineToolcraft({
|
|
2425
|
+
canvas: { enabled: true, upload: true },
|
|
2426
|
+
panels: {
|
|
2427
|
+
controls: {
|
|
2428
|
+
sections: [
|
|
2429
|
+
{
|
|
2430
|
+
controls: {
|
|
2431
|
+
source: {
|
|
2432
|
+
accept: "PNG, SVG",
|
|
2433
|
+
label: "Images",
|
|
2434
|
+
multiple: true,
|
|
2435
|
+
target: "input.sources",
|
|
2436
|
+
type: "fileDrop",
|
|
2437
|
+
},
|
|
2438
|
+
},
|
|
2439
|
+
layout: "standalone",
|
|
2440
|
+
title: "Input",
|
|
2441
|
+
},
|
|
2442
|
+
],
|
|
2443
|
+
title: "Generation Controls",
|
|
2444
|
+
},
|
|
2445
|
+
},
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
const { container } = renderControlsPanelWithSchema(schema, undefined, {
|
|
2449
|
+
layers: [
|
|
2450
|
+
{ id: "layer-1", kind: "layer", name: "one", visible: true },
|
|
2451
|
+
{ id: "layer-2", kind: "layer", name: "two", visible: true },
|
|
2452
|
+
],
|
|
2453
|
+
mediaAssets: [
|
|
2454
|
+
{
|
|
2455
|
+
dataUrl:
|
|
2456
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23777'/%3E%3C/svg%3E",
|
|
2457
|
+
fileName: "one.svg",
|
|
2458
|
+
id: "media-1",
|
|
2459
|
+
layerId: "layer-1",
|
|
2460
|
+
mimeType: "image/svg+xml",
|
|
2461
|
+
position: { x: 0, y: 0 },
|
|
2462
|
+
size: { height: 96, unit: "px", width: 96 },
|
|
2463
|
+
},
|
|
2464
|
+
{
|
|
2465
|
+
dataUrl:
|
|
2466
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23999'/%3E%3C/svg%3E",
|
|
2467
|
+
fileName: "two.svg",
|
|
2468
|
+
id: "media-2",
|
|
2469
|
+
layerId: "layer-2",
|
|
2470
|
+
mimeType: "image/svg+xml",
|
|
2471
|
+
position: { x: 0, y: 0 },
|
|
2472
|
+
size: { height: 96, unit: "px", width: 96 },
|
|
2473
|
+
},
|
|
2474
|
+
],
|
|
2475
|
+
selectedLayerId: "layer-2",
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
expect(screen.getByRole("button", { name: "Add image files" })).toBeTruthy();
|
|
2479
|
+
const previewGrid = container.querySelector('[data-slot="file-upload-preview-grid"]');
|
|
2480
|
+
expect(previewGrid?.className).toContain("grid-cols-4");
|
|
2481
|
+
expect(previewGrid?.lastElementChild?.getAttribute("data-slot")).toBe(
|
|
2482
|
+
"file-upload-add-preview",
|
|
2483
|
+
);
|
|
2484
|
+
expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy();
|
|
2485
|
+
expect(screen.getByRole("img", { name: "two.svg" })).toBeTruthy();
|
|
2486
|
+
|
|
2487
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" }));
|
|
2488
|
+
|
|
2489
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
2490
|
+
expect(screen.queryByRole("img", { name: "two.svg" })).toBeNull();
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2286
2493
|
it("keeps file upload deletion in the layers panel for multi-layer apps", () => {
|
|
2287
2494
|
const schema = defineToolcraft({
|
|
2288
2495
|
canvas: { enabled: true, upload: true },
|
|
@@ -3126,6 +3333,68 @@ describe("ControlsPanel", () => {
|
|
|
3126
3333
|
expect(values["canvas.size.height"]).toBe(1080);
|
|
3127
3334
|
});
|
|
3128
3335
|
|
|
3336
|
+
it("switches editable-output aspect ratio to custom after manual size edits", () => {
|
|
3337
|
+
const schema = defineToolcraft({
|
|
3338
|
+
canvas: {
|
|
3339
|
+
enabled: true,
|
|
3340
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
3341
|
+
sizing: { mode: "editable-output" },
|
|
3342
|
+
},
|
|
3343
|
+
panels: {
|
|
3344
|
+
controls: {
|
|
3345
|
+
sections: [],
|
|
3346
|
+
title: "Controls",
|
|
3347
|
+
},
|
|
3348
|
+
},
|
|
3349
|
+
});
|
|
3350
|
+
|
|
3351
|
+
renderControlsPanelWithSchema(schema);
|
|
3352
|
+
|
|
3353
|
+
const widthInput = screen.getByDisplayValue("1920") as HTMLInputElement;
|
|
3354
|
+
|
|
3355
|
+
fireEvent.change(widthInput, { target: { value: "1600" } });
|
|
3356
|
+
fireEvent.blur(widthInput);
|
|
3357
|
+
|
|
3358
|
+
expect(screen.getByTestId("canvas-size").textContent).toBe("1600,1080");
|
|
3359
|
+
expect(screen.getAllByText("Custom...").length).toBeGreaterThan(0);
|
|
3360
|
+
expect(screen.getByDisplayValue("40")).toBeTruthy();
|
|
3361
|
+
expect(screen.getByDisplayValue("27")).toBeTruthy();
|
|
3362
|
+
expect(JSON.parse(screen.getByTestId("values-json").textContent ?? "{}")).toMatchObject({
|
|
3363
|
+
"canvas.aspectRatio": {
|
|
3364
|
+
height: 27,
|
|
3365
|
+
mode: "custom",
|
|
3366
|
+
value: "40:27",
|
|
3367
|
+
width: 40,
|
|
3368
|
+
},
|
|
3369
|
+
"canvas.size.height": 1080,
|
|
3370
|
+
"canvas.size.width": 1600,
|
|
3371
|
+
});
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
it("renders raster render scale as the last technical setup control", () => {
|
|
3375
|
+
const schema = defineToolcraft({
|
|
3376
|
+
canvas: {
|
|
3377
|
+
enabled: true,
|
|
3378
|
+
renderScale: true,
|
|
3379
|
+
size: { height: 1080, unit: "px", width: 1920 },
|
|
3380
|
+
sizing: { mode: "editable-output" },
|
|
3381
|
+
},
|
|
3382
|
+
panels: {
|
|
3383
|
+
controls: {
|
|
3384
|
+
sections: [],
|
|
3385
|
+
title: "Controls",
|
|
3386
|
+
},
|
|
3387
|
+
},
|
|
3388
|
+
});
|
|
3389
|
+
renderControlsPanelWithSchema(schema);
|
|
3390
|
+
|
|
3391
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3392
|
+
|
|
3393
|
+
expect(screen.getByText("Resolution scale")).toBeTruthy();
|
|
3394
|
+
expect(screen.getByText("2x")).toBeTruthy();
|
|
3395
|
+
expect(values["canvas.renderScale"]).toBe(2);
|
|
3396
|
+
});
|
|
3397
|
+
|
|
3129
3398
|
it("applies empty text input values while typing and reset restores defaults", () => {
|
|
3130
3399
|
renderControlsPanel();
|
|
3131
3400
|
|