@pixel-point/toolcraft 0.0.6 → 0.0.8
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.test.mjs +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +123 -15
- package/templates/runtime/contracts/component-contracts.ts +93 -19
- package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.ts +2 -2
- 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 +590 -9
- package/templates/runtime/react/controls-panel.tsx +472 -10
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +2 -0
- package/templates/runtime/schema/define-toolcraft.ts +11 -3
- package/templates/runtime/schema/types.ts +27 -0
- package/templates/runtime/state/reducer.test.ts +357 -0
- package/templates/runtime/state/reducer.ts +195 -9
- package/templates/runtime/state/types.ts +12 -2
- package/templates/runtime/testing/performance.test.ts +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +13 -12
- package/templates/starter/docs/toolcraft/README.md +4 -3
- package/templates/starter/docs/toolcraft/acceptance-testing.md +17 -7
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -7
- package/templates/starter/docs/toolcraft/component-rules.md +20 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +9 -5
- package/templates/starter/docs/toolcraft/performance.md +51 -13
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +11 -6
- package/templates/starter/docs/toolcraft/workflow.md +7 -10
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/package.json +4 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +529 -19
- package/templates/starter/src/app/starter-acceptance.ts +269 -12
- package/templates/starter/src/app/starter-performance.test.ts +67 -6
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
- package/templates/ui/components/controls/collection-actions/index.ts +4 -0
- 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/font-picker/font-picker-control.tsx +1 -6
- package/templates/ui/components/controls/index.ts +9 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
- package/templates/ui/index.ts +1 -0
|
@@ -248,6 +248,9 @@ function StateProbe() {
|
|
|
248
248
|
{state.canvas.size.width},{state.canvas.size.height}
|
|
249
249
|
</span>
|
|
250
250
|
<span data-testid="media-count">{state.mediaAssets.length}</span>
|
|
251
|
+
<span data-testid="media-ids">
|
|
252
|
+
{state.mediaAssets.map((asset) => asset.id).join(",")}
|
|
253
|
+
</span>
|
|
251
254
|
<span data-testid="timeline-expanded">{String(state.timeline.expanded)}</span>
|
|
252
255
|
<span data-testid="timeline-keyframes">
|
|
253
256
|
{JSON.stringify(state.timeline.keyframeGroups)}
|
|
@@ -384,6 +387,46 @@ describe("ControlsPanel", () => {
|
|
|
384
387
|
expect(inlineGroup?.textContent).toContain("Snap Y");
|
|
385
388
|
});
|
|
386
389
|
|
|
390
|
+
it("keeps wrapped local action buttons left aligned below their label", () => {
|
|
391
|
+
const schema = defineToolcraft({
|
|
392
|
+
canvas: { enabled: false },
|
|
393
|
+
panels: {
|
|
394
|
+
controls: {
|
|
395
|
+
sections: [
|
|
396
|
+
{
|
|
397
|
+
controls: {
|
|
398
|
+
masks: {
|
|
399
|
+
actions: [
|
|
400
|
+
{ label: "Rect", value: "mask.add.rectangle" },
|
|
401
|
+
{ label: "Circle", value: "mask.add.circle" },
|
|
402
|
+
{ label: "Tri", value: "mask.add.triangle" },
|
|
403
|
+
{ label: "Delete", value: "mask.delete" },
|
|
404
|
+
],
|
|
405
|
+
defaultValue: [],
|
|
406
|
+
label: "Masks",
|
|
407
|
+
target: "composition.masks",
|
|
408
|
+
type: "actions",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
title: "Composition",
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
title: "Controls",
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
419
|
+
const actionField = container.querySelector('[data-slot="actions-control"]');
|
|
420
|
+
const actionButtons = container.querySelector(
|
|
421
|
+
'[data-slot="actions-control-buttons"]',
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
expect(actionField?.className).toContain("justify-between");
|
|
425
|
+
expect(actionField?.className).toContain("flex-wrap");
|
|
426
|
+
expect(actionButtons?.className).toContain("justify-start");
|
|
427
|
+
expect(actionButtons?.className).not.toContain("ml-auto");
|
|
428
|
+
});
|
|
429
|
+
|
|
387
430
|
it("renders the managed background toggle plus unlabeled color row as equal-width inline columns", () => {
|
|
388
431
|
const schema = defineToolcraft({
|
|
389
432
|
canvas: { enabled: false },
|
|
@@ -448,35 +491,35 @@ describe("ControlsPanel", () => {
|
|
|
448
491
|
defaultValue: { hex: "#DFFF1A" },
|
|
449
492
|
description: "Sets the first bead color.",
|
|
450
493
|
label: "Color 1",
|
|
451
|
-
target: "
|
|
494
|
+
target: "palette.accent1",
|
|
452
495
|
type: "color",
|
|
453
496
|
},
|
|
454
497
|
color2: {
|
|
455
498
|
defaultValue: { hex: "#8CFF3A" },
|
|
456
499
|
description: "Sets the second bead color.",
|
|
457
500
|
label: "Color 2",
|
|
458
|
-
target: "
|
|
501
|
+
target: "palette.accent2",
|
|
459
502
|
type: "color",
|
|
460
503
|
},
|
|
461
504
|
color3: {
|
|
462
505
|
defaultValue: { hex: "#F4FF5A" },
|
|
463
506
|
description: "Sets the third bead color.",
|
|
464
507
|
label: "Color 3",
|
|
465
|
-
target: "
|
|
508
|
+
target: "palette.accent3",
|
|
466
509
|
type: "color",
|
|
467
510
|
},
|
|
468
511
|
color4: {
|
|
469
512
|
defaultValue: { hex: "#B8FF2E" },
|
|
470
513
|
description: "Sets the fourth bead color.",
|
|
471
514
|
label: "Color 4",
|
|
472
|
-
target: "
|
|
515
|
+
target: "palette.accent4",
|
|
473
516
|
type: "color",
|
|
474
517
|
},
|
|
475
518
|
color5: {
|
|
476
519
|
defaultValue: { hex: "#ECFF68" },
|
|
477
520
|
description: "Sets the fifth bead color.",
|
|
478
521
|
label: "Color 5",
|
|
479
|
-
target: "
|
|
522
|
+
target: "palette.accent5",
|
|
480
523
|
type: "color",
|
|
481
524
|
},
|
|
482
525
|
colorSpread: {
|
|
@@ -486,7 +529,7 @@ describe("ControlsPanel", () => {
|
|
|
486
529
|
label: "Spread",
|
|
487
530
|
max: 100,
|
|
488
531
|
min: 0,
|
|
489
|
-
target: "
|
|
532
|
+
target: "palette.spread",
|
|
490
533
|
type: "slider",
|
|
491
534
|
unit: "%",
|
|
492
535
|
},
|
|
@@ -503,7 +546,7 @@ describe("ControlsPanel", () => {
|
|
|
503
546
|
layout: "inline",
|
|
504
547
|
},
|
|
505
548
|
],
|
|
506
|
-
title: "
|
|
549
|
+
title: "Accent Shades",
|
|
507
550
|
},
|
|
508
551
|
{
|
|
509
552
|
controls: {
|
|
@@ -1102,7 +1145,6 @@ describe("ControlsPanel", () => {
|
|
|
1102
1145
|
controls: {
|
|
1103
1146
|
font: {
|
|
1104
1147
|
defaultValue: {
|
|
1105
|
-
color: "#FFFFFF",
|
|
1106
1148
|
fontId: "inter",
|
|
1107
1149
|
fontSize: 16,
|
|
1108
1150
|
fontWeight: "400",
|
|
@@ -1271,6 +1313,24 @@ describe("ControlsPanel", () => {
|
|
|
1271
1313
|
});
|
|
1272
1314
|
});
|
|
1273
1315
|
|
|
1316
|
+
fireEvent.change(fontSizeInput, {
|
|
1317
|
+
target: { value: "768" },
|
|
1318
|
+
});
|
|
1319
|
+
fireEvent.blur(fontSizeInput);
|
|
1320
|
+
|
|
1321
|
+
await waitFor(() => {
|
|
1322
|
+
expect(JSON.parse(screen.getByTestId("font-value").textContent ?? "{}")).toEqual({
|
|
1323
|
+
color: "#FFFFFF",
|
|
1324
|
+
fontId: "roboto",
|
|
1325
|
+
fontSize: 768,
|
|
1326
|
+
fontWeight: "400",
|
|
1327
|
+
letterSpacing: "wider",
|
|
1328
|
+
lineHeight: "relaxed",
|
|
1329
|
+
opacity: 100,
|
|
1330
|
+
textCase: "uppercase",
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1274
1334
|
const colorHexInput = screen.getByLabelText("Color hex") as HTMLInputElement;
|
|
1275
1335
|
fireEvent.change(colorHexInput, {
|
|
1276
1336
|
target: { value: "#C1FF00" },
|
|
@@ -1286,7 +1346,7 @@ describe("ControlsPanel", () => {
|
|
|
1286
1346
|
expect(JSON.parse(screen.getByTestId("font-value").textContent ?? "{}")).toEqual({
|
|
1287
1347
|
color: "#C1FF00",
|
|
1288
1348
|
fontId: "roboto",
|
|
1289
|
-
fontSize:
|
|
1349
|
+
fontSize: 768,
|
|
1290
1350
|
fontWeight: "400",
|
|
1291
1351
|
letterSpacing: "wider",
|
|
1292
1352
|
lineHeight: "relaxed",
|
|
@@ -2483,6 +2543,22 @@ describe("ControlsPanel", () => {
|
|
|
2483
2543
|
);
|
|
2484
2544
|
expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy();
|
|
2485
2545
|
expect(screen.getByRole("img", { name: "two.svg" })).toBeTruthy();
|
|
2546
|
+
expect(screen.getByTestId("media-ids").textContent).toBe("media-1,media-2");
|
|
2547
|
+
|
|
2548
|
+
const firstPreview = screen
|
|
2549
|
+
.getByRole("img", { name: "one.svg" })
|
|
2550
|
+
.closest<HTMLElement>('[data-slot="file-upload-preview-item"]');
|
|
2551
|
+
const secondPreview = screen
|
|
2552
|
+
.getByRole("img", { name: "two.svg" })
|
|
2553
|
+
.closest<HTMLElement>('[data-slot="file-upload-preview-item"]');
|
|
2554
|
+
|
|
2555
|
+
if (!firstPreview || !secondPreview) {
|
|
2556
|
+
throw new Error("Expected multi-image previews to render");
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
expect(firstPreview.getAttribute("role")).toBe("button");
|
|
2560
|
+
expect(firstPreview.getAttribute("tabindex")).toBe("0");
|
|
2561
|
+
expect(firstPreview.className).toContain("cursor-grab");
|
|
2486
2562
|
|
|
2487
2563
|
fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" }));
|
|
2488
2564
|
|
|
@@ -2490,6 +2566,118 @@ describe("ControlsPanel", () => {
|
|
|
2490
2566
|
expect(screen.queryByRole("img", { name: "two.svg" })).toBeNull();
|
|
2491
2567
|
});
|
|
2492
2568
|
|
|
2569
|
+
it("renders arbitrary file uploads as a sortable file list", () => {
|
|
2570
|
+
const schema = defineToolcraft({
|
|
2571
|
+
canvas: { enabled: true, upload: true },
|
|
2572
|
+
panels: {
|
|
2573
|
+
controls: {
|
|
2574
|
+
sections: [
|
|
2575
|
+
{
|
|
2576
|
+
controls: {
|
|
2577
|
+
source: {
|
|
2578
|
+
assetKind: "file",
|
|
2579
|
+
label: "Files",
|
|
2580
|
+
multiple: true,
|
|
2581
|
+
target: "input.files",
|
|
2582
|
+
type: "fileDrop",
|
|
2583
|
+
},
|
|
2584
|
+
},
|
|
2585
|
+
layout: "standalone",
|
|
2586
|
+
title: "Input",
|
|
2587
|
+
},
|
|
2588
|
+
],
|
|
2589
|
+
title: "Generation Controls",
|
|
2590
|
+
},
|
|
2591
|
+
},
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
const { container } = renderControlsPanelWithSchema(schema, undefined, {
|
|
2595
|
+
layers: [
|
|
2596
|
+
{ id: "layer-1", kind: "layer", name: "one", visible: true },
|
|
2597
|
+
{ id: "layer-2", kind: "layer", name: "two", visible: true },
|
|
2598
|
+
],
|
|
2599
|
+
mediaAssets: [
|
|
2600
|
+
{
|
|
2601
|
+
assetKind: "file",
|
|
2602
|
+
dataUrl: "data:text/plain;base64,b25l",
|
|
2603
|
+
fileName: "one.txt",
|
|
2604
|
+
id: "media-1",
|
|
2605
|
+
layerId: "layer-1",
|
|
2606
|
+
mimeType: "text/plain",
|
|
2607
|
+
position: { x: 0, y: 0 },
|
|
2608
|
+
sourceTarget: "input.files",
|
|
2609
|
+
},
|
|
2610
|
+
{
|
|
2611
|
+
assetKind: "file",
|
|
2612
|
+
dataUrl: "data:application/json;base64,e30=",
|
|
2613
|
+
fileName: "two.json",
|
|
2614
|
+
id: "media-2",
|
|
2615
|
+
layerId: "layer-2",
|
|
2616
|
+
mimeType: "application/json",
|
|
2617
|
+
position: { x: 0, y: 0 },
|
|
2618
|
+
sourceTarget: "input.files",
|
|
2619
|
+
},
|
|
2620
|
+
],
|
|
2621
|
+
selectedLayerId: "layer-2",
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
const fileList = container.querySelector('[data-slot="file-upload-file-list"]');
|
|
2625
|
+
const fileRows = container.querySelectorAll('[data-slot="file-upload-file-item"]');
|
|
2626
|
+
const fileDividers = container.querySelectorAll('[data-slot="file-upload-file-divider"]');
|
|
2627
|
+
|
|
2628
|
+
expect(fileList).toBeTruthy();
|
|
2629
|
+
expect(fileRows).toHaveLength(2);
|
|
2630
|
+
expect(fileDividers).toHaveLength(2);
|
|
2631
|
+
expect(screen.getByText("one.txt")).toBeTruthy();
|
|
2632
|
+
expect(screen.getByText("two.json")).toBeTruthy();
|
|
2633
|
+
expect(screen.getByText("one.txt").className).toContain("text-xs");
|
|
2634
|
+
expect(screen.getByText("one.txt").className).not.toContain("truncate");
|
|
2635
|
+
expect(screen.getByText("one.txt").className).toContain("mask-image");
|
|
2636
|
+
expect(screen.getByRole("button", { name: "Remove one.txt" })).toBeTruthy();
|
|
2637
|
+
expect(screen.getByRole("button", { name: "Add a new file" })).toBeTruthy();
|
|
2638
|
+
expect(fileRows[0]?.className).not.toContain("border-b");
|
|
2639
|
+
expect(fileRows[0]?.className).toContain("hover:bg");
|
|
2640
|
+
expect(fileDividers[0]?.className).toContain("var(--border)_5%");
|
|
2641
|
+
expect(fileRows[0]?.getAttribute("role")).toBe("button");
|
|
2642
|
+
expect(fileRows[0]?.getAttribute("tabindex")).toBe("0");
|
|
2643
|
+
|
|
2644
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove two.json" }));
|
|
2645
|
+
|
|
2646
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
2647
|
+
expect(screen.queryByText("two.json")).toBeNull();
|
|
2648
|
+
});
|
|
2649
|
+
|
|
2650
|
+
it("renders the arbitrary file uploader empty state", () => {
|
|
2651
|
+
const schema = defineToolcraft({
|
|
2652
|
+
canvas: { enabled: true, upload: true },
|
|
2653
|
+
panels: {
|
|
2654
|
+
controls: {
|
|
2655
|
+
sections: [
|
|
2656
|
+
{
|
|
2657
|
+
controls: {
|
|
2658
|
+
source: {
|
|
2659
|
+
assetKind: "file",
|
|
2660
|
+
label: "Files",
|
|
2661
|
+
target: "input.files",
|
|
2662
|
+
type: "fileDrop",
|
|
2663
|
+
},
|
|
2664
|
+
},
|
|
2665
|
+
layout: "standalone",
|
|
2666
|
+
title: "Input",
|
|
2667
|
+
},
|
|
2668
|
+
],
|
|
2669
|
+
title: "Generation Controls",
|
|
2670
|
+
},
|
|
2671
|
+
},
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
renderControlsPanelWithSchema(schema);
|
|
2675
|
+
|
|
2676
|
+
expect(screen.getByText("Click to upload a file")).toBeTruthy();
|
|
2677
|
+
expect(screen.getByText("or drag it onto the canvas")).toBeTruthy();
|
|
2678
|
+
expect(screen.queryByText("Click to upload an image")).toBeNull();
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2493
2681
|
it("keeps file upload deletion in the layers panel for multi-layer apps", () => {
|
|
2494
2682
|
const schema = defineToolcraft({
|
|
2495
2683
|
canvas: { enabled: true, upload: true },
|
|
@@ -2556,6 +2744,65 @@ describe("ControlsPanel", () => {
|
|
|
2556
2744
|
expect(colorControlList?.children).toHaveLength(1);
|
|
2557
2745
|
});
|
|
2558
2746
|
|
|
2747
|
+
it("keeps an odd trailing plain color half-width in a color bank", () => {
|
|
2748
|
+
const schema = defineToolcraft({
|
|
2749
|
+
canvas: { enabled: true },
|
|
2750
|
+
panels: {
|
|
2751
|
+
controls: {
|
|
2752
|
+
sections: [
|
|
2753
|
+
{
|
|
2754
|
+
controls: {
|
|
2755
|
+
accent1: {
|
|
2756
|
+
defaultValue: { hex: "#9CE6FF" },
|
|
2757
|
+
label: "Accent 1",
|
|
2758
|
+
target: "accent.shade1",
|
|
2759
|
+
type: "color",
|
|
2760
|
+
},
|
|
2761
|
+
accent2: {
|
|
2762
|
+
defaultValue: { hex: "#FF7A90" },
|
|
2763
|
+
label: "Accent 2",
|
|
2764
|
+
target: "accent.shade2",
|
|
2765
|
+
type: "color",
|
|
2766
|
+
},
|
|
2767
|
+
accent3: {
|
|
2768
|
+
defaultValue: { hex: "#FFD166" },
|
|
2769
|
+
label: "Accent 3",
|
|
2770
|
+
target: "accent.shade3",
|
|
2771
|
+
type: "color",
|
|
2772
|
+
},
|
|
2773
|
+
accent4: {
|
|
2774
|
+
defaultValue: { hex: "#8BF38B" },
|
|
2775
|
+
label: "Accent 4",
|
|
2776
|
+
target: "accent.shade4",
|
|
2777
|
+
type: "color",
|
|
2778
|
+
},
|
|
2779
|
+
accent5: {
|
|
2780
|
+
defaultValue: { hex: "#BDA8FF" },
|
|
2781
|
+
label: "Accent 5",
|
|
2782
|
+
target: "accent.shade5",
|
|
2783
|
+
type: "color",
|
|
2784
|
+
},
|
|
2785
|
+
},
|
|
2786
|
+
title: "Accent Shades",
|
|
2787
|
+
},
|
|
2788
|
+
],
|
|
2789
|
+
title: "Generation Controls",
|
|
2790
|
+
},
|
|
2791
|
+
},
|
|
2792
|
+
});
|
|
2793
|
+
|
|
2794
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
2795
|
+
const pairedColorRows = container.querySelectorAll('[data-slot="color-control-grid"]');
|
|
2796
|
+
const trailingColorButtonGroup = screen
|
|
2797
|
+
.getByLabelText("Accent 5 hex")
|
|
2798
|
+
.closest('[data-slot="button-group"]');
|
|
2799
|
+
const trailingColorWidthContainer = trailingColorButtonGroup?.parentElement;
|
|
2800
|
+
|
|
2801
|
+
expect(pairedColorRows).toHaveLength(2);
|
|
2802
|
+
expect(trailingColorWidthContainer?.className).toContain("w-1/2");
|
|
2803
|
+
expect(trailingColorWidthContainer?.className).not.toContain("w-full");
|
|
2804
|
+
});
|
|
2805
|
+
|
|
2559
2806
|
it("omits visible color field labels only inside color-only sections", () => {
|
|
2560
2807
|
const schema = defineToolcraft({
|
|
2561
2808
|
canvas: { enabled: true },
|
|
@@ -3264,6 +3511,316 @@ describe("ControlsPanel", () => {
|
|
|
3264
3511
|
expect(screen.getByTestId("enabled-value").textContent).toBe("true");
|
|
3265
3512
|
});
|
|
3266
3513
|
|
|
3514
|
+
it("resets only controls from the selected section header action", () => {
|
|
3515
|
+
const schema = defineToolcraft({
|
|
3516
|
+
canvas: { enabled: false },
|
|
3517
|
+
panels: {
|
|
3518
|
+
controls: {
|
|
3519
|
+
sections: [
|
|
3520
|
+
{
|
|
3521
|
+
controls: {
|
|
3522
|
+
toneName: {
|
|
3523
|
+
defaultValue: "Soft",
|
|
3524
|
+
label: "Tone name",
|
|
3525
|
+
target: "tone.name",
|
|
3526
|
+
type: "text",
|
|
3527
|
+
},
|
|
3528
|
+
},
|
|
3529
|
+
title: "Tone",
|
|
3530
|
+
},
|
|
3531
|
+
{
|
|
3532
|
+
controls: {
|
|
3533
|
+
prompt: {
|
|
3534
|
+
defaultValue: "Poster",
|
|
3535
|
+
label: "Prompt",
|
|
3536
|
+
target: "generation.prompt",
|
|
3537
|
+
type: "text",
|
|
3538
|
+
},
|
|
3539
|
+
},
|
|
3540
|
+
title: "Output",
|
|
3541
|
+
},
|
|
3542
|
+
],
|
|
3543
|
+
title: "Controls",
|
|
3544
|
+
},
|
|
3545
|
+
},
|
|
3546
|
+
});
|
|
3547
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
3548
|
+
|
|
3549
|
+
fireEvent.change(screen.getByDisplayValue("Soft"), {
|
|
3550
|
+
target: { value: "Sharp" },
|
|
3551
|
+
});
|
|
3552
|
+
fireEvent.change(screen.getByDisplayValue("Poster"), {
|
|
3553
|
+
target: { value: "Banner" },
|
|
3554
|
+
});
|
|
3555
|
+
|
|
3556
|
+
const toneHeader = screen
|
|
3557
|
+
.getByText("Tone")
|
|
3558
|
+
.closest<HTMLElement>('[data-slot="control-section-header"]');
|
|
3559
|
+
const headerButtons = toneHeader?.querySelectorAll("button");
|
|
3560
|
+
|
|
3561
|
+
expect(headerButtons?.[0]?.getAttribute("data-control-section-reset-button")).toBe("");
|
|
3562
|
+
expect(headerButtons?.[1]?.getAttribute("data-control-section-collapse-button")).toBe("");
|
|
3563
|
+
|
|
3564
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset Tone section" }));
|
|
3565
|
+
|
|
3566
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3567
|
+
|
|
3568
|
+
expect(values["tone.name"]).toBe("Soft");
|
|
3569
|
+
expect(values["generation.prompt"]).toBe("Banner");
|
|
3570
|
+
expect(container.querySelector('[data-control-section-reset-button]')).toBeTruthy();
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3573
|
+
it("clears file upload previews from the selected section reset action", () => {
|
|
3574
|
+
const schema = defineToolcraft({
|
|
3575
|
+
canvas: { enabled: true, upload: true },
|
|
3576
|
+
panels: {
|
|
3577
|
+
controls: {
|
|
3578
|
+
sections: [
|
|
3579
|
+
{
|
|
3580
|
+
controls: {
|
|
3581
|
+
source: {
|
|
3582
|
+
defaultValue: null,
|
|
3583
|
+
label: "Image",
|
|
3584
|
+
target: "media.source",
|
|
3585
|
+
type: "fileDrop",
|
|
3586
|
+
},
|
|
3587
|
+
},
|
|
3588
|
+
title: "Source Image",
|
|
3589
|
+
},
|
|
3590
|
+
],
|
|
3591
|
+
title: "Controls",
|
|
3592
|
+
},
|
|
3593
|
+
},
|
|
3594
|
+
});
|
|
3595
|
+
|
|
3596
|
+
renderControlsPanelWithSchema(schema, {}, {
|
|
3597
|
+
mediaAssets: [
|
|
3598
|
+
{
|
|
3599
|
+
dataUrl:
|
|
3600
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 3'/%3E",
|
|
3601
|
+
fileName: "source.svg",
|
|
3602
|
+
id: "media-1",
|
|
3603
|
+
layerId: "layer-1",
|
|
3604
|
+
mimeType: "image/svg+xml",
|
|
3605
|
+
position: { x: 0, y: 0 },
|
|
3606
|
+
size: { height: 300, unit: "px", width: 400 },
|
|
3607
|
+
sourceTarget: "media.source",
|
|
3608
|
+
},
|
|
3609
|
+
],
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3612
|
+
expect(screen.getByRole("img", { name: "source.svg" })).toBeTruthy();
|
|
3613
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3614
|
+
|
|
3615
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset Source Image section" }));
|
|
3616
|
+
|
|
3617
|
+
expect(screen.queryByRole("img", { name: "source.svg" })).toBeNull();
|
|
3618
|
+
expect(screen.getByTestId("media-count").textContent).toBe("0");
|
|
3619
|
+
});
|
|
3620
|
+
|
|
3621
|
+
it("renders collection actions as add/remove controls for runtime item arrays", () => {
|
|
3622
|
+
const schema = defineToolcraft({
|
|
3623
|
+
canvas: { enabled: false },
|
|
3624
|
+
panels: {
|
|
3625
|
+
controls: {
|
|
3626
|
+
sections: [
|
|
3627
|
+
{
|
|
3628
|
+
controls: {
|
|
3629
|
+
colors: {
|
|
3630
|
+
defaultValue: [{ hex: "#DFFF1A" }, { hex: "#8CFF3A" }],
|
|
3631
|
+
itemControl: {
|
|
3632
|
+
defaultValue: { hex: "#C1FF00" },
|
|
3633
|
+
label: "Color",
|
|
3634
|
+
type: "color",
|
|
3635
|
+
},
|
|
3636
|
+
label: "Colors",
|
|
3637
|
+
minItems: 1,
|
|
3638
|
+
recommendedMaxItems: 2,
|
|
3639
|
+
target: "bead.colors",
|
|
3640
|
+
type: "collectionActions",
|
|
3641
|
+
},
|
|
3642
|
+
},
|
|
3643
|
+
title: "Bead Colors",
|
|
3644
|
+
},
|
|
3645
|
+
],
|
|
3646
|
+
title: "Controls",
|
|
3647
|
+
},
|
|
3648
|
+
},
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3651
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
3652
|
+
|
|
3653
|
+
const addButton = screen.getByRole("button", { name: "Add Color" });
|
|
3654
|
+
const removeButton = screen.getByRole("button", { name: "Remove Color" });
|
|
3655
|
+
const collectionHeader = container.querySelector(
|
|
3656
|
+
'[data-slot="collection-actions-control-header"]',
|
|
3657
|
+
);
|
|
3658
|
+
const colorGrid = container.querySelector(
|
|
3659
|
+
'[data-slot="collection-actions-items-grid"]',
|
|
3660
|
+
);
|
|
3661
|
+
|
|
3662
|
+
expect(collectionHeader?.textContent).toContain("Colors");
|
|
3663
|
+
expect(collectionHeader?.lastElementChild?.contains(removeButton)).toBe(true);
|
|
3664
|
+
expect(collectionHeader?.lastElementChild?.contains(addButton)).toBe(true);
|
|
3665
|
+
expect(colorGrid).toBeTruthy();
|
|
3666
|
+
expect(colorGrid?.children).toHaveLength(2);
|
|
3667
|
+
expect(screen.queryByText("Color 1")).toBeNull();
|
|
3668
|
+
expect(screen.queryByText("Color 2")).toBeNull();
|
|
3669
|
+
|
|
3670
|
+
fireEvent.click(addButton);
|
|
3671
|
+
fireEvent.click(addButton);
|
|
3672
|
+
|
|
3673
|
+
let values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3674
|
+
|
|
3675
|
+
expect(values["bead.colors"]).toHaveLength(4);
|
|
3676
|
+
expect(screen.queryByText("Color 4")).toBeNull();
|
|
3677
|
+
expect(colorGrid?.children).toHaveLength(4);
|
|
3678
|
+
expect((addButton as HTMLButtonElement).disabled).toBe(false);
|
|
3679
|
+
|
|
3680
|
+
fireEvent.click(removeButton);
|
|
3681
|
+
fireEvent.click(removeButton);
|
|
3682
|
+
fireEvent.click(removeButton);
|
|
3683
|
+
|
|
3684
|
+
values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3685
|
+
|
|
3686
|
+
expect(values["bead.colors"]).toHaveLength(1);
|
|
3687
|
+
expect((removeButton as HTMLButtonElement).disabled).toBe(true);
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
it("renders collection item controls beyond color lists", () => {
|
|
3691
|
+
const schema = defineToolcraft({
|
|
3692
|
+
canvas: { enabled: false },
|
|
3693
|
+
panels: {
|
|
3694
|
+
controls: {
|
|
3695
|
+
sections: [
|
|
3696
|
+
{
|
|
3697
|
+
controls: {
|
|
3698
|
+
labels: {
|
|
3699
|
+
defaultValue: ["Alpha"],
|
|
3700
|
+
itemControl: {
|
|
3701
|
+
defaultValue: "New label",
|
|
3702
|
+
label: "Label",
|
|
3703
|
+
type: "text",
|
|
3704
|
+
},
|
|
3705
|
+
minItems: 1,
|
|
3706
|
+
target: "glyph.labels",
|
|
3707
|
+
type: "collectionActions",
|
|
3708
|
+
},
|
|
3709
|
+
},
|
|
3710
|
+
title: "Glyph Labels",
|
|
3711
|
+
},
|
|
3712
|
+
],
|
|
3713
|
+
title: "Controls",
|
|
3714
|
+
},
|
|
3715
|
+
},
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
renderControlsPanelWithSchema(schema);
|
|
3719
|
+
|
|
3720
|
+
fireEvent.click(screen.getByRole("button", { name: "Add Label" }));
|
|
3721
|
+
|
|
3722
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3723
|
+
|
|
3724
|
+
expect(values["glyph.labels"]).toEqual(["Alpha", "New label"]);
|
|
3725
|
+
expect(screen.getByDisplayValue("Alpha")).toBeTruthy();
|
|
3726
|
+
expect(screen.getByDisplayValue("New label")).toBeTruthy();
|
|
3727
|
+
});
|
|
3728
|
+
|
|
3729
|
+
it("keeps a collection font picker color popover open while editing its color surface", async () => {
|
|
3730
|
+
const schema = defineToolcraft({
|
|
3731
|
+
canvas: { enabled: false },
|
|
3732
|
+
panels: {
|
|
3733
|
+
controls: {
|
|
3734
|
+
sections: [
|
|
3735
|
+
{
|
|
3736
|
+
controls: {
|
|
3737
|
+
textStyles: {
|
|
3738
|
+
defaultValue: [
|
|
3739
|
+
{
|
|
3740
|
+
color: "#FFFFFF",
|
|
3741
|
+
fontId: "inter",
|
|
3742
|
+
fontSize: 16,
|
|
3743
|
+
fontWeight: "400",
|
|
3744
|
+
letterSpacing: "normal",
|
|
3745
|
+
lineHeight: "normal",
|
|
3746
|
+
opacity: 100,
|
|
3747
|
+
textCase: "original",
|
|
3748
|
+
},
|
|
3749
|
+
],
|
|
3750
|
+
itemControl: {
|
|
3751
|
+
defaultValue: {
|
|
3752
|
+
color: "#FFFFFF",
|
|
3753
|
+
fontId: "inter",
|
|
3754
|
+
fontSize: 16,
|
|
3755
|
+
fontWeight: "400",
|
|
3756
|
+
letterSpacing: "normal",
|
|
3757
|
+
lineHeight: "normal",
|
|
3758
|
+
opacity: 100,
|
|
3759
|
+
textCase: "original",
|
|
3760
|
+
},
|
|
3761
|
+
label: "Font",
|
|
3762
|
+
type: "fontPicker",
|
|
3763
|
+
},
|
|
3764
|
+
label: "Text styles",
|
|
3765
|
+
minItems: 1,
|
|
3766
|
+
target: "text.styles",
|
|
3767
|
+
type: "collectionActions",
|
|
3768
|
+
},
|
|
3769
|
+
},
|
|
3770
|
+
title: "Typography",
|
|
3771
|
+
},
|
|
3772
|
+
],
|
|
3773
|
+
title: "Controls",
|
|
3774
|
+
},
|
|
3775
|
+
},
|
|
3776
|
+
});
|
|
3777
|
+
|
|
3778
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
3779
|
+
|
|
3780
|
+
fireEvent.click(screen.getByRole("button", { name: "Pick Color" }));
|
|
3781
|
+
|
|
3782
|
+
const surface = await waitFor(() => {
|
|
3783
|
+
const nextSurface = container.ownerDocument.body.querySelector<HTMLElement>(
|
|
3784
|
+
'[data-slot="style-guide-color-surface"]',
|
|
3785
|
+
);
|
|
3786
|
+
if (!nextSurface) {
|
|
3787
|
+
throw new Error("Expected color surface to be open.");
|
|
3788
|
+
}
|
|
3789
|
+
return nextSurface;
|
|
3790
|
+
});
|
|
3791
|
+
|
|
3792
|
+
const surfaceRect = {
|
|
3793
|
+
bottom: 100,
|
|
3794
|
+
height: 100,
|
|
3795
|
+
left: 0,
|
|
3796
|
+
right: 100,
|
|
3797
|
+
toJSON: () => ({}),
|
|
3798
|
+
top: 0,
|
|
3799
|
+
width: 100,
|
|
3800
|
+
x: 0,
|
|
3801
|
+
y: 0,
|
|
3802
|
+
} as DOMRect;
|
|
3803
|
+
Object.defineProperty(surface, "getBoundingClientRect", {
|
|
3804
|
+
configurable: true,
|
|
3805
|
+
value: () => surfaceRect,
|
|
3806
|
+
});
|
|
3807
|
+
|
|
3808
|
+
fireEvent.pointerDown(surface, {
|
|
3809
|
+
buttons: 1,
|
|
3810
|
+
clientX: 72,
|
|
3811
|
+
clientY: 28,
|
|
3812
|
+
pointerId: 1,
|
|
3813
|
+
});
|
|
3814
|
+
|
|
3815
|
+
expect(
|
|
3816
|
+
container.ownerDocument.body.querySelector('[data-slot="style-guide-color-surface"]'),
|
|
3817
|
+
).toBeTruthy();
|
|
3818
|
+
|
|
3819
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3820
|
+
|
|
3821
|
+
expect(values["text.styles"][0].color).not.toBe("#FFFFFF");
|
|
3822
|
+
});
|
|
3823
|
+
|
|
3267
3824
|
it("commits canvas size controls to runtime state on blur or Enter", () => {
|
|
3268
3825
|
const { container } = renderControlsPanel();
|
|
3269
3826
|
|
|
@@ -3832,6 +4389,30 @@ describe("ControlsPanel", () => {
|
|
|
3832
4389
|
expect(container.querySelector('[data-slot="control-section-header"]')).toBeTruthy();
|
|
3833
4390
|
});
|
|
3834
4391
|
|
|
4392
|
+
it("suppresses child control transitions when a section re-expands", () => {
|
|
4393
|
+
const schema = createSchema();
|
|
4394
|
+
renderControlsPanelWithSchema(schema);
|
|
4395
|
+
const basicTitle = screen.getByText("Basic");
|
|
4396
|
+
const header = basicTitle.closest<HTMLElement>('[data-slot="control-section-header"]');
|
|
4397
|
+
const section = basicTitle.closest("section");
|
|
4398
|
+
const initialBody = section?.querySelector<HTMLElement>(
|
|
4399
|
+
'[data-slot="panel-section-collapsible-body"]',
|
|
4400
|
+
);
|
|
4401
|
+
|
|
4402
|
+
fireEvent.click(header!);
|
|
4403
|
+
fireEvent.transitionEnd(initialBody!);
|
|
4404
|
+
fireEvent.click(header!);
|
|
4405
|
+
|
|
4406
|
+
const expandedBody = section?.querySelector<HTMLElement>(
|
|
4407
|
+
'[data-slot="panel-section-collapsible-body"]',
|
|
4408
|
+
);
|
|
4409
|
+
const innerControls = expandedBody?.firstElementChild as HTMLElement | null;
|
|
4410
|
+
|
|
4411
|
+
expect(expandedBody?.className).toContain("transition-[grid-template-rows,opacity]");
|
|
4412
|
+
expect(expandedBody?.className).toContain("grid-rows-[1fr]");
|
|
4413
|
+
expect(innerControls?.getAttribute("data-toolcraft-controls-mounting")).toBe("true");
|
|
4414
|
+
});
|
|
4415
|
+
|
|
3835
4416
|
it("persists ordinary section collapse state as a UI preference across remounts", () => {
|
|
3836
4417
|
const schema = createSchema();
|
|
3837
4418
|
const { unmount } = renderControlsPanelWithSchema(schema);
|