@pixel-point/toolcraft 0.0.7 → 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/templates/runtime/contracts/component-contracts.test.ts +84 -12
- package/templates/runtime/contracts/component-contracts.ts +40 -17
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- 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 +404 -8
- package/templates/runtime/react/controls-panel.tsx +90 -4
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +4 -2
- package/templates/runtime/schema/types.ts +4 -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 +1282 -48
- package/templates/runtime/testing/performance.ts +676 -39
- package/templates/starter/AGENTS.md +6 -5
- package/templates/starter/docs/toolcraft/acceptance-testing.md +11 -5
- package/templates/starter/docs/toolcraft/assembly-workflow.md +4 -2
- package/templates/starter/docs/toolcraft/component-rules.md +16 -7
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +47 -5
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -6
- 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/package.json +3 -0
- 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 +66 -5
- package/templates/ui/components/controls/actions/actions-control.tsx +3 -5
- 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 +1 -0
- package/templates/ui/components/panel/panel-section.tsx +53 -1
|
@@ -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",
|
|
@@ -2501,6 +2543,22 @@ describe("ControlsPanel", () => {
|
|
|
2501
2543
|
);
|
|
2502
2544
|
expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy();
|
|
2503
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");
|
|
2504
2562
|
|
|
2505
2563
|
fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" }));
|
|
2506
2564
|
|
|
@@ -2508,6 +2566,118 @@ describe("ControlsPanel", () => {
|
|
|
2508
2566
|
expect(screen.queryByRole("img", { name: "two.svg" })).toBeNull();
|
|
2509
2567
|
});
|
|
2510
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
|
+
|
|
2511
2681
|
it("keeps file upload deletion in the layers panel for multi-layer apps", () => {
|
|
2512
2682
|
const schema = defineToolcraft({
|
|
2513
2683
|
canvas: { enabled: true, upload: true },
|
|
@@ -2574,6 +2744,65 @@ describe("ControlsPanel", () => {
|
|
|
2574
2744
|
expect(colorControlList?.children).toHaveLength(1);
|
|
2575
2745
|
});
|
|
2576
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
|
+
|
|
2577
2806
|
it("omits visible color field labels only inside color-only sections", () => {
|
|
2578
2807
|
const schema = defineToolcraft({
|
|
2579
2808
|
canvas: { enabled: true },
|
|
@@ -3341,6 +3570,54 @@ describe("ControlsPanel", () => {
|
|
|
3341
3570
|
expect(container.querySelector('[data-control-section-reset-button]')).toBeTruthy();
|
|
3342
3571
|
});
|
|
3343
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
|
+
|
|
3344
3621
|
it("renders collection actions as add/remove controls for runtime item arrays", () => {
|
|
3345
3622
|
const schema = defineToolcraft({
|
|
3346
3623
|
canvas: { enabled: false },
|
|
@@ -3449,6 +3726,101 @@ describe("ControlsPanel", () => {
|
|
|
3449
3726
|
expect(screen.getByDisplayValue("New label")).toBeTruthy();
|
|
3450
3727
|
});
|
|
3451
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
|
+
|
|
3452
3824
|
it("commits canvas size controls to runtime state on blur or Enter", () => {
|
|
3453
3825
|
const { container } = renderControlsPanel();
|
|
3454
3826
|
|
|
@@ -4017,6 +4389,30 @@ describe("ControlsPanel", () => {
|
|
|
4017
4389
|
expect(container.querySelector('[data-slot="control-section-header"]')).toBeTruthy();
|
|
4018
4390
|
});
|
|
4019
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
|
+
|
|
4020
4416
|
it("persists ordinary section collapse state as a UI preference across remounts", () => {
|
|
4021
4417
|
const schema = createSchema();
|
|
4022
4418
|
const { unmount } = renderControlsPanelWithSchema(schema);
|
|
@@ -70,7 +70,11 @@ import type {
|
|
|
70
70
|
ToolcraftPanelState,
|
|
71
71
|
ToolcraftState,
|
|
72
72
|
} from "../state/types";
|
|
73
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
isToolcraftImageFile,
|
|
75
|
+
readImportedFile,
|
|
76
|
+
readImportedImageFile,
|
|
77
|
+
} from "./media-file";
|
|
74
78
|
import { PanelContainer } from "./panel-host";
|
|
75
79
|
import type { PanelPlacement, PanelStateChange } from "./panel-host-types";
|
|
76
80
|
import type { ToolcraftControlRendererMap } from "./control-renderers";
|
|
@@ -585,6 +589,8 @@ function getCollectionItemDefaultValue(control: ToolcraftControlSchema): unknown
|
|
|
585
589
|
return { hex: "#C1FF00" };
|
|
586
590
|
case "colorOpacity":
|
|
587
591
|
return { hex: "#C1FF00", opacity: 100 };
|
|
592
|
+
case "fontPicker":
|
|
593
|
+
return asFontPickerValue(control.itemControl?.defaultValue);
|
|
588
594
|
case "checkbox":
|
|
589
595
|
case "switch":
|
|
590
596
|
return false;
|
|
@@ -1790,7 +1796,9 @@ export function ControlsPanel({
|
|
|
1790
1796
|
}
|
|
1791
1797
|
|
|
1792
1798
|
function isColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1793
|
-
return /\b(colou?rs?|palette|palettes)\b/i.test(
|
|
1799
|
+
return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
|
|
1800
|
+
sectionTitle ?? "",
|
|
1801
|
+
);
|
|
1794
1802
|
}
|
|
1795
1803
|
|
|
1796
1804
|
function isSequentialColorLabel(label: string): boolean {
|
|
@@ -2221,6 +2229,16 @@ export function ControlsPanel({
|
|
|
2221
2229
|
showLabel={itemControl?.label !== false}
|
|
2222
2230
|
/>
|
|
2223
2231
|
);
|
|
2232
|
+
case "fontPicker":
|
|
2233
|
+
return (
|
|
2234
|
+
<FontPicker
|
|
2235
|
+
defaultValue={asFontPickerValue(itemControl?.defaultValue)}
|
|
2236
|
+
key={key}
|
|
2237
|
+
name={itemName || "Font"}
|
|
2238
|
+
onValueChange={update}
|
|
2239
|
+
value={asFontPickerValue(item)}
|
|
2240
|
+
/>
|
|
2241
|
+
);
|
|
2224
2242
|
case "text":
|
|
2225
2243
|
return (
|
|
2226
2244
|
<TextInput
|
|
@@ -2590,15 +2608,51 @@ export function ControlsPanel({
|
|
|
2590
2608
|
}
|
|
2591
2609
|
|
|
2592
2610
|
case "fileDrop": {
|
|
2593
|
-
const
|
|
2611
|
+
const assetKind = control.assetKind === "file" ? "file" : "image";
|
|
2612
|
+
const previewMediaAssets = state.schema.panels.layers
|
|
2613
|
+
? []
|
|
2614
|
+
: state.mediaAssets.filter(
|
|
2615
|
+
(asset) =>
|
|
2616
|
+
asset.sourceTarget === undefined ||
|
|
2617
|
+
asset.sourceTarget === control.target,
|
|
2618
|
+
);
|
|
2594
2619
|
const previewMediaAsset = previewMediaAssets[0];
|
|
2595
2620
|
const previews = previewMediaAssets.map((asset): FileDropPreview => ({
|
|
2596
2621
|
alt: asset.fileName,
|
|
2622
|
+
assetKind: asset.assetKind ?? "image",
|
|
2623
|
+
fileName: asset.fileName,
|
|
2597
2624
|
id: asset.id,
|
|
2598
2625
|
size: asset.size,
|
|
2599
2626
|
src: asset.dataUrl,
|
|
2600
2627
|
}));
|
|
2628
|
+
const previewMediaIds = previewMediaAssets.map((asset) => asset.id);
|
|
2601
2629
|
const importFile = (file: File, replaceExisting: boolean): void => {
|
|
2630
|
+
if (assetKind === "file") {
|
|
2631
|
+
void readImportedFile(file).then((importedFile) => {
|
|
2632
|
+
if (!importedFile) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
dispatchCommand({
|
|
2637
|
+
asset: {
|
|
2638
|
+
assetKind: "file",
|
|
2639
|
+
dataUrl: importedFile.dataUrl,
|
|
2640
|
+
fileName: file.name,
|
|
2641
|
+
mimeType: file.type || "application/octet-stream",
|
|
2642
|
+
position: { x: 0, y: 0 },
|
|
2643
|
+
sourceTarget: control.target,
|
|
2644
|
+
},
|
|
2645
|
+
replaceExisting,
|
|
2646
|
+
type: "media.import",
|
|
2647
|
+
});
|
|
2648
|
+
});
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
if (!isToolcraftImageFile(file)) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2602
2656
|
void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
|
|
2603
2657
|
if (!importedImage) {
|
|
2604
2658
|
return;
|
|
@@ -2606,11 +2660,13 @@ export function ControlsPanel({
|
|
|
2606
2660
|
|
|
2607
2661
|
dispatchCommand({
|
|
2608
2662
|
asset: {
|
|
2663
|
+
assetKind: "image",
|
|
2609
2664
|
dataUrl: importedImage.dataUrl,
|
|
2610
2665
|
fileName: file.name,
|
|
2611
2666
|
mimeType: file.type || "image/*",
|
|
2612
2667
|
position: { x: 0, y: 0 },
|
|
2613
2668
|
size: importedImage.size,
|
|
2669
|
+
sourceTarget: control.target,
|
|
2614
2670
|
},
|
|
2615
2671
|
replaceExisting,
|
|
2616
2672
|
type: "media.import",
|
|
@@ -2620,7 +2676,11 @@ export function ControlsPanel({
|
|
|
2620
2676
|
|
|
2621
2677
|
return (
|
|
2622
2678
|
<FileDrop
|
|
2623
|
-
accept={
|
|
2679
|
+
accept={
|
|
2680
|
+
control.accept ??
|
|
2681
|
+
(assetKind === "file" ? "" : "PNG, JPEG, GIF, SVG, WebP")
|
|
2682
|
+
}
|
|
2683
|
+
assetKind={assetKind}
|
|
2624
2684
|
multiple={control.multiple}
|
|
2625
2685
|
key={id}
|
|
2626
2686
|
onClear={
|
|
@@ -2647,6 +2707,32 @@ export function ControlsPanel({
|
|
|
2647
2707
|
type: "media.delete",
|
|
2648
2708
|
});
|
|
2649
2709
|
}}
|
|
2710
|
+
onPreviewReorder={(orderedPreviews) => {
|
|
2711
|
+
const orderedPreviewIds = orderedPreviews.flatMap((item) =>
|
|
2712
|
+
item.id ? [item.id] : [],
|
|
2713
|
+
);
|
|
2714
|
+
|
|
2715
|
+
if (orderedPreviewIds.length !== previewMediaIds.length) {
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
const previewIdSet = new Set(previewMediaIds);
|
|
2720
|
+
let orderedPreviewIndex = 0;
|
|
2721
|
+
const mediaIds = state.mediaAssets.map((asset) => {
|
|
2722
|
+
if (!previewIdSet.has(asset.id)) {
|
|
2723
|
+
return asset.id;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
const nextId = orderedPreviewIds[orderedPreviewIndex];
|
|
2727
|
+
orderedPreviewIndex += 1;
|
|
2728
|
+
return nextId ?? asset.id;
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
dispatchCommand({
|
|
2732
|
+
mediaIds,
|
|
2733
|
+
type: "media.reorder",
|
|
2734
|
+
});
|
|
2735
|
+
}}
|
|
2650
2736
|
preview={
|
|
2651
2737
|
previewMediaAsset
|
|
2652
2738
|
? {
|