@pixel-point/toolcraft 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/generate.mjs +34 -5
- package/src/generate.test.mjs +12 -0
- package/src/package-json.mjs +15 -0
- package/src/package-json.test.mjs +14 -1
- package/templates/runtime/contracts/component-contracts.test.ts +185 -23
- package/templates/runtime/contracts/component-contracts.ts +91 -36
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +4 -4
- package/templates/runtime/export/export.test.ts +31 -0
- package/templates/runtime/export/export.ts +41 -0
- package/templates/runtime/react/canvas-shell.test.tsx +77 -1
- package/templates/runtime/react/canvas-shell.tsx +178 -23
- package/templates/runtime/react/control-conditions.ts +166 -0
- package/templates/runtime/react/controls-panel-filedrop-reorder.test.tsx +176 -0
- package/templates/runtime/react/controls-panel.test.tsx +774 -17
- package/templates/runtime/react/controls-panel.tsx +155 -8
- package/templates/runtime/react/media-file.ts +19 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +46 -1
- package/templates/runtime/schema/define-toolcraft.ts +29 -3
- package/templates/runtime/schema/types.ts +7 -0
- package/templates/runtime/state/reducer.test.ts +304 -0
- package/templates/runtime/state/reducer.ts +148 -9
- package/templates/runtime/state/types.ts +10 -2
- package/templates/runtime/testing/performance.test.ts +1424 -56
- package/templates/runtime/testing/performance.ts +710 -43
- package/templates/starter/AGENTS.md +10 -9
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +16 -8
- package/templates/starter/docs/toolcraft/assembly-workflow.md +13 -7
- package/templates/starter/docs/toolcraft/component-rules.md +46 -16
- package/templates/starter/docs/toolcraft/custom-controls.md +8 -4
- package/templates/starter/docs/toolcraft/performance.md +54 -6
- package/templates/starter/docs/toolcraft/renderer-technique.md +4 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +48 -19
- package/templates/starter/docs/toolcraft/workflow.md +2 -2
- package/templates/starter/e2e/app-performance.spec.ts +136 -3
- package/templates/starter/e2e/performance-helpers.ts +197 -0
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +5 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +39 -4
- package/templates/starter/scripts/toolcraft-port.mjs +102 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +60 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +1288 -105
- package/templates/starter/src/app/starter-acceptance.ts +740 -24
- package/templates/starter/src/app/starter-performance.test.ts +66 -5
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +10 -4
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
- package/templates/ui/components/controls/color/index.ts +4 -1
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +340 -44
- package/templates/ui/components/controls/file-drop/index.ts +1 -1
- package/templates/ui/components/controls/index.ts +3 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
- package/templates/ui/components/controls/select/select-control.tsx +9 -4
- package/templates/ui/components/controls/slider/slider-value.ts +0 -1
- package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
- package/templates/ui/components/controls/vector/index.ts +1 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +84 -8
- package/templates/ui/components/panel/panel-section.tsx +82 -6
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getColorSurfaceStyle,
|
|
8
8
|
getColorSurfaceThumbPosition,
|
|
9
9
|
getSurfaceHsvColor,
|
|
10
|
+
StyleGuideColorPicker,
|
|
10
11
|
} from "@repo/ui/controls";
|
|
11
12
|
import * as React from "react";
|
|
12
13
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
@@ -248,6 +249,9 @@ function StateProbe() {
|
|
|
248
249
|
{state.canvas.size.width},{state.canvas.size.height}
|
|
249
250
|
</span>
|
|
250
251
|
<span data-testid="media-count">{state.mediaAssets.length}</span>
|
|
252
|
+
<span data-testid="media-ids">
|
|
253
|
+
{state.mediaAssets.map((asset) => asset.id).join(",")}
|
|
254
|
+
</span>
|
|
251
255
|
<span data-testid="timeline-expanded">{String(state.timeline.expanded)}</span>
|
|
252
256
|
<span data-testid="timeline-keyframes">
|
|
253
257
|
{JSON.stringify(state.timeline.keyframeGroups)}
|
|
@@ -384,6 +388,89 @@ describe("ControlsPanel", () => {
|
|
|
384
388
|
expect(inlineGroup?.textContent).toContain("Snap Y");
|
|
385
389
|
});
|
|
386
390
|
|
|
391
|
+
it("renders local action buttons as a two-column grid below their label", () => {
|
|
392
|
+
const schema = defineToolcraft({
|
|
393
|
+
canvas: { enabled: false },
|
|
394
|
+
panels: {
|
|
395
|
+
controls: {
|
|
396
|
+
sections: [
|
|
397
|
+
{
|
|
398
|
+
controls: {
|
|
399
|
+
masks: {
|
|
400
|
+
actions: [
|
|
401
|
+
{ label: "Rect", value: "mask.add.rectangle" },
|
|
402
|
+
{ label: "Circle", value: "mask.add.circle" },
|
|
403
|
+
{ label: "Tri", value: "mask.add.triangle" },
|
|
404
|
+
{ label: "Delete", value: "mask.delete" },
|
|
405
|
+
],
|
|
406
|
+
defaultValue: [],
|
|
407
|
+
label: "Masks",
|
|
408
|
+
target: "composition.masks",
|
|
409
|
+
type: "actions",
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
title: "Composition",
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
title: "Controls",
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
420
|
+
const actionField = container.querySelector('[data-slot="actions-control"]');
|
|
421
|
+
const actionButtons = container.querySelector(
|
|
422
|
+
'[data-slot="actions-control-buttons"]',
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
expect(actionField?.getAttribute("data-orientation")).toBe("vertical");
|
|
426
|
+
expect(actionField?.className).not.toContain("justify-between");
|
|
427
|
+
expect(actionButtons?.className).toContain("grid");
|
|
428
|
+
expect(actionButtons?.className).toContain("w-full");
|
|
429
|
+
expect(actionButtons?.className).toContain("grid-cols-2");
|
|
430
|
+
expect(actionButtons?.className).not.toContain("justify-start");
|
|
431
|
+
expect(actionButtons?.className).not.toContain("ml-auto");
|
|
432
|
+
for (const label of ["Rect", "Circle", "Tri", "Delete"]) {
|
|
433
|
+
expect(screen.getByRole("button", { name: label }).className).toContain(
|
|
434
|
+
"w-full",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("keeps a single local action button at half row width", () => {
|
|
440
|
+
const schema = defineToolcraft({
|
|
441
|
+
canvas: { enabled: false },
|
|
442
|
+
panels: {
|
|
443
|
+
controls: {
|
|
444
|
+
sections: [
|
|
445
|
+
{
|
|
446
|
+
controls: {
|
|
447
|
+
paletteActions: {
|
|
448
|
+
actions: [{ label: "Randomize", value: "palette.randomize" }],
|
|
449
|
+
defaultValue: null,
|
|
450
|
+
label: "Actions",
|
|
451
|
+
target: "palette.actions",
|
|
452
|
+
type: "actions",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
title: "Palette",
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
title: "Controls",
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
463
|
+
const actionButtons = container.querySelector<HTMLElement>(
|
|
464
|
+
'[data-slot="actions-control-buttons"]',
|
|
465
|
+
);
|
|
466
|
+
const button = screen.getByRole("button", { name: "Randomize" });
|
|
467
|
+
|
|
468
|
+
expect(actionButtons?.getAttribute("data-actions-count")).toBe("1");
|
|
469
|
+
expect(actionButtons?.className).toContain("w-1/2");
|
|
470
|
+
expect(actionButtons?.className).toContain("grid-cols-1");
|
|
471
|
+
expect(button.className).toContain("w-full");
|
|
472
|
+
});
|
|
473
|
+
|
|
387
474
|
it("renders the managed background toggle plus unlabeled color row as equal-width inline columns", () => {
|
|
388
475
|
const schema = defineToolcraft({
|
|
389
476
|
canvas: { enabled: false },
|
|
@@ -431,6 +518,7 @@ describe("ControlsPanel", () => {
|
|
|
431
518
|
expect(screen.getByText("Include")).toBeTruthy();
|
|
432
519
|
expect(screen.getByRole("switch")).toBeTruthy();
|
|
433
520
|
expect(toggleParameterGroup).toBeTruthy();
|
|
521
|
+
expect(toggleParameterGroup?.className).toContain("gap-x-2.5");
|
|
434
522
|
expect(toggleParameterGroup?.getAttribute("style")).toContain(
|
|
435
523
|
"repeat(2, minmax(0, 1fr))",
|
|
436
524
|
);
|
|
@@ -448,35 +536,35 @@ describe("ControlsPanel", () => {
|
|
|
448
536
|
defaultValue: { hex: "#DFFF1A" },
|
|
449
537
|
description: "Sets the first bead color.",
|
|
450
538
|
label: "Color 1",
|
|
451
|
-
target: "
|
|
539
|
+
target: "palette.accent1",
|
|
452
540
|
type: "color",
|
|
453
541
|
},
|
|
454
542
|
color2: {
|
|
455
543
|
defaultValue: { hex: "#8CFF3A" },
|
|
456
544
|
description: "Sets the second bead color.",
|
|
457
545
|
label: "Color 2",
|
|
458
|
-
target: "
|
|
546
|
+
target: "palette.accent2",
|
|
459
547
|
type: "color",
|
|
460
548
|
},
|
|
461
549
|
color3: {
|
|
462
550
|
defaultValue: { hex: "#F4FF5A" },
|
|
463
551
|
description: "Sets the third bead color.",
|
|
464
552
|
label: "Color 3",
|
|
465
|
-
target: "
|
|
553
|
+
target: "palette.accent3",
|
|
466
554
|
type: "color",
|
|
467
555
|
},
|
|
468
556
|
color4: {
|
|
469
557
|
defaultValue: { hex: "#B8FF2E" },
|
|
470
558
|
description: "Sets the fourth bead color.",
|
|
471
559
|
label: "Color 4",
|
|
472
|
-
target: "
|
|
560
|
+
target: "palette.accent4",
|
|
473
561
|
type: "color",
|
|
474
562
|
},
|
|
475
563
|
color5: {
|
|
476
564
|
defaultValue: { hex: "#ECFF68" },
|
|
477
565
|
description: "Sets the fifth bead color.",
|
|
478
566
|
label: "Color 5",
|
|
479
|
-
target: "
|
|
567
|
+
target: "palette.accent5",
|
|
480
568
|
type: "color",
|
|
481
569
|
},
|
|
482
570
|
colorSpread: {
|
|
@@ -486,7 +574,7 @@ describe("ControlsPanel", () => {
|
|
|
486
574
|
label: "Spread",
|
|
487
575
|
max: 100,
|
|
488
576
|
min: 0,
|
|
489
|
-
target: "
|
|
577
|
+
target: "palette.spread",
|
|
490
578
|
type: "slider",
|
|
491
579
|
unit: "%",
|
|
492
580
|
},
|
|
@@ -503,7 +591,7 @@ describe("ControlsPanel", () => {
|
|
|
503
591
|
layout: "inline",
|
|
504
592
|
},
|
|
505
593
|
],
|
|
506
|
-
title: "
|
|
594
|
+
title: "Accent Shades",
|
|
507
595
|
},
|
|
508
596
|
{
|
|
509
597
|
controls: {
|
|
@@ -573,13 +661,62 @@ describe("ControlsPanel", () => {
|
|
|
573
661
|
);
|
|
574
662
|
|
|
575
663
|
expect(screen.getByText("Loop")).toBeTruthy();
|
|
576
|
-
expect(screen.
|
|
664
|
+
expect(screen.queryByText("Duration")).toBeNull();
|
|
577
665
|
expect(toggleParameterGroup).toBeTruthy();
|
|
666
|
+
expect(toggleParameterGroup?.className).toContain("gap-x-2.5");
|
|
578
667
|
expect(toggleParameterGroup?.getAttribute("style")).toContain(
|
|
579
668
|
"repeat(2, minmax(0, 1fr))",
|
|
580
669
|
);
|
|
581
670
|
});
|
|
582
671
|
|
|
672
|
+
it("keeps segmented controls full-width even when an inline row is requested", () => {
|
|
673
|
+
const schema = defineToolcraft({
|
|
674
|
+
canvas: { enabled: false },
|
|
675
|
+
panels: {
|
|
676
|
+
controls: {
|
|
677
|
+
sections: [
|
|
678
|
+
{
|
|
679
|
+
controls: {
|
|
680
|
+
includeText: {
|
|
681
|
+
defaultValue: true,
|
|
682
|
+
label: "Include",
|
|
683
|
+
target: "text.enabled",
|
|
684
|
+
type: "switch",
|
|
685
|
+
},
|
|
686
|
+
dragTarget: {
|
|
687
|
+
defaultValue: "glass",
|
|
688
|
+
label: "Drag",
|
|
689
|
+
options: [
|
|
690
|
+
{ label: "Glass", value: "glass" },
|
|
691
|
+
{ label: "Text", value: "text" },
|
|
692
|
+
],
|
|
693
|
+
target: "text.dragTarget",
|
|
694
|
+
type: "segmented",
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
layoutGroups: [
|
|
698
|
+
{
|
|
699
|
+
columns: 2,
|
|
700
|
+
controls: ["includeText", "dragTarget"],
|
|
701
|
+
layout: "inline",
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
title: "Glass Text",
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
title: "Controls",
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
713
|
+
|
|
714
|
+
expect(screen.getByText("Include")).toBeTruthy();
|
|
715
|
+
expect(screen.getByText("Drag")).toBeTruthy();
|
|
716
|
+
expect(container.querySelector('[data-control-layout="inline"]')).toBeNull();
|
|
717
|
+
expect(container.querySelector('[data-slot="toggle-group"]')).toBeTruthy();
|
|
718
|
+
});
|
|
719
|
+
|
|
583
720
|
it("renders compact select pairs inline with stacked full-width fields", () => {
|
|
584
721
|
const schema = defineToolcraft({
|
|
585
722
|
canvas: { enabled: false },
|
|
@@ -634,6 +771,7 @@ describe("ControlsPanel", () => {
|
|
|
634
771
|
);
|
|
635
772
|
|
|
636
773
|
expect(inlineGroup).toBeTruthy();
|
|
774
|
+
expect(inlineGroup?.className).toContain("gap-x-2.5");
|
|
637
775
|
expect(screen.getByText("Format")).toBeTruthy();
|
|
638
776
|
expect(screen.getByText("Resolution")).toBeTruthy();
|
|
639
777
|
expect(triggers).toHaveLength(2);
|
|
@@ -1102,7 +1240,6 @@ describe("ControlsPanel", () => {
|
|
|
1102
1240
|
controls: {
|
|
1103
1241
|
font: {
|
|
1104
1242
|
defaultValue: {
|
|
1105
|
-
color: "#FFFFFF",
|
|
1106
1243
|
fontId: "inter",
|
|
1107
1244
|
fontSize: 16,
|
|
1108
1245
|
fontWeight: "400",
|
|
@@ -1487,6 +1624,246 @@ describe("ControlsPanel", () => {
|
|
|
1487
1624
|
"toneBias",
|
|
1488
1625
|
"default",
|
|
1489
1626
|
]);
|
|
1627
|
+
expect(
|
|
1628
|
+
[...container.querySelectorAll("[data-vector-pad-coordinate-mode]")].map((pad) =>
|
|
1629
|
+
pad.getAttribute("data-vector-pad-coordinate-mode"),
|
|
1630
|
+
),
|
|
1631
|
+
).toEqual([
|
|
1632
|
+
"cartesian",
|
|
1633
|
+
"cartesian",
|
|
1634
|
+
"cartesian",
|
|
1635
|
+
"cartesian",
|
|
1636
|
+
"screen",
|
|
1637
|
+
]);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it("maps default spatial vector pad movement to screen coordinates", () => {
|
|
1641
|
+
const schema = defineToolcraft({
|
|
1642
|
+
canvas: { enabled: true },
|
|
1643
|
+
panels: {
|
|
1644
|
+
controls: {
|
|
1645
|
+
sections: [
|
|
1646
|
+
{
|
|
1647
|
+
controls: {
|
|
1648
|
+
position: {
|
|
1649
|
+
defaultValue: { x: "0.00", y: "0.00" },
|
|
1650
|
+
label: "Position",
|
|
1651
|
+
target: "geometry.position",
|
|
1652
|
+
type: "vector",
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1655
|
+
title: "Geometry",
|
|
1656
|
+
},
|
|
1657
|
+
],
|
|
1658
|
+
title: "Controls",
|
|
1659
|
+
},
|
|
1660
|
+
},
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
renderControlsPanelWithSchema(schema);
|
|
1664
|
+
const pad = screen.getByRole("button", { name: "Position X/Y pad" });
|
|
1665
|
+
|
|
1666
|
+
expect(pad.getAttribute("data-vector-pad-coordinate-mode")).toBe("screen");
|
|
1667
|
+
vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({
|
|
1668
|
+
bottom: 100,
|
|
1669
|
+
height: 100,
|
|
1670
|
+
left: 0,
|
|
1671
|
+
right: 100,
|
|
1672
|
+
toJSON: () => undefined,
|
|
1673
|
+
top: 0,
|
|
1674
|
+
width: 100,
|
|
1675
|
+
x: 0,
|
|
1676
|
+
y: 0,
|
|
1677
|
+
} as DOMRect);
|
|
1678
|
+
Object.defineProperty(pad, "setPointerCapture", {
|
|
1679
|
+
configurable: true,
|
|
1680
|
+
value: vi.fn(),
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
fireEvent.pointerDown(pad, {
|
|
1684
|
+
buttons: 1,
|
|
1685
|
+
clientX: 0,
|
|
1686
|
+
clientY: 0,
|
|
1687
|
+
pointerId: 1,
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
1691
|
+
expect(values["geometry.position"]).toEqual({ x: "-1.00", y: "-1.00" });
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
it("locks vector pad movement to the dominant axis while shift is held", () => {
|
|
1695
|
+
const schema = defineToolcraft({
|
|
1696
|
+
canvas: { enabled: true },
|
|
1697
|
+
panels: {
|
|
1698
|
+
controls: {
|
|
1699
|
+
sections: [
|
|
1700
|
+
{
|
|
1701
|
+
controls: {
|
|
1702
|
+
position: {
|
|
1703
|
+
defaultValue: { x: "0.00", y: "0.00" },
|
|
1704
|
+
label: "Position",
|
|
1705
|
+
target: "geometry.position",
|
|
1706
|
+
type: "vector",
|
|
1707
|
+
},
|
|
1708
|
+
},
|
|
1709
|
+
title: "Geometry",
|
|
1710
|
+
},
|
|
1711
|
+
],
|
|
1712
|
+
title: "Controls",
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
renderControlsPanelWithSchema(schema);
|
|
1718
|
+
const pad = screen.getByRole("button", { name: "Position X/Y pad" });
|
|
1719
|
+
|
|
1720
|
+
vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({
|
|
1721
|
+
bottom: 100,
|
|
1722
|
+
height: 100,
|
|
1723
|
+
left: 0,
|
|
1724
|
+
right: 100,
|
|
1725
|
+
toJSON: () => undefined,
|
|
1726
|
+
top: 0,
|
|
1727
|
+
width: 100,
|
|
1728
|
+
x: 0,
|
|
1729
|
+
y: 0,
|
|
1730
|
+
} as DOMRect);
|
|
1731
|
+
Object.defineProperty(pad, "setPointerCapture", {
|
|
1732
|
+
configurable: true,
|
|
1733
|
+
value: vi.fn(),
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
fireEvent.pointerDown(pad, {
|
|
1737
|
+
buttons: 1,
|
|
1738
|
+
clientX: 50,
|
|
1739
|
+
clientY: 50,
|
|
1740
|
+
pointerId: 1,
|
|
1741
|
+
});
|
|
1742
|
+
fireEvent.pointerMove(pad, {
|
|
1743
|
+
buttons: 1,
|
|
1744
|
+
clientX: 90,
|
|
1745
|
+
clientY: 70,
|
|
1746
|
+
pointerId: 1,
|
|
1747
|
+
shiftKey: true,
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
1751
|
+
expect(values["geometry.position"]).toEqual({ x: "0.80", y: "0.00" });
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
it("locks vector pad movement vertically when shift drag is y-dominant", () => {
|
|
1755
|
+
const schema = defineToolcraft({
|
|
1756
|
+
canvas: { enabled: true },
|
|
1757
|
+
panels: {
|
|
1758
|
+
controls: {
|
|
1759
|
+
sections: [
|
|
1760
|
+
{
|
|
1761
|
+
controls: {
|
|
1762
|
+
position: {
|
|
1763
|
+
defaultValue: { x: "0.00", y: "0.00" },
|
|
1764
|
+
label: "Position",
|
|
1765
|
+
target: "geometry.position",
|
|
1766
|
+
type: "vector",
|
|
1767
|
+
},
|
|
1768
|
+
},
|
|
1769
|
+
title: "Geometry",
|
|
1770
|
+
},
|
|
1771
|
+
],
|
|
1772
|
+
title: "Controls",
|
|
1773
|
+
},
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
renderControlsPanelWithSchema(schema);
|
|
1778
|
+
const pad = screen.getByRole("button", { name: "Position X/Y pad" });
|
|
1779
|
+
|
|
1780
|
+
vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({
|
|
1781
|
+
bottom: 100,
|
|
1782
|
+
height: 100,
|
|
1783
|
+
left: 0,
|
|
1784
|
+
right: 100,
|
|
1785
|
+
toJSON: () => undefined,
|
|
1786
|
+
top: 0,
|
|
1787
|
+
width: 100,
|
|
1788
|
+
x: 0,
|
|
1789
|
+
y: 0,
|
|
1790
|
+
} as DOMRect);
|
|
1791
|
+
Object.defineProperty(pad, "setPointerCapture", {
|
|
1792
|
+
configurable: true,
|
|
1793
|
+
value: vi.fn(),
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
fireEvent.pointerDown(pad, {
|
|
1797
|
+
buttons: 1,
|
|
1798
|
+
clientX: 50,
|
|
1799
|
+
clientY: 50,
|
|
1800
|
+
pointerId: 1,
|
|
1801
|
+
});
|
|
1802
|
+
fireEvent.pointerMove(pad, {
|
|
1803
|
+
buttons: 1,
|
|
1804
|
+
clientX: 70,
|
|
1805
|
+
clientY: 90,
|
|
1806
|
+
pointerId: 1,
|
|
1807
|
+
shiftKey: true,
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
1811
|
+
expect(values["geometry.position"]).toEqual({ x: "0.00", y: "0.80" });
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
it("allows cartesian vector pads when a product intentionally uses y-up math", () => {
|
|
1815
|
+
const schema = defineToolcraft({
|
|
1816
|
+
canvas: { enabled: true },
|
|
1817
|
+
panels: {
|
|
1818
|
+
controls: {
|
|
1819
|
+
sections: [
|
|
1820
|
+
{
|
|
1821
|
+
controls: {
|
|
1822
|
+
vector: {
|
|
1823
|
+
coordinateMode: "cartesian",
|
|
1824
|
+
defaultValue: { x: "0.00", y: "0.00" },
|
|
1825
|
+
label: "Vector",
|
|
1826
|
+
target: "math.vector",
|
|
1827
|
+
type: "vector",
|
|
1828
|
+
},
|
|
1829
|
+
},
|
|
1830
|
+
title: "Math",
|
|
1831
|
+
},
|
|
1832
|
+
],
|
|
1833
|
+
title: "Controls",
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
renderControlsPanelWithSchema(schema);
|
|
1839
|
+
const pad = screen.getByRole("button", { name: "Vector X/Y pad" });
|
|
1840
|
+
|
|
1841
|
+
expect(pad.getAttribute("data-vector-pad-coordinate-mode")).toBe("cartesian");
|
|
1842
|
+
vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({
|
|
1843
|
+
bottom: 100,
|
|
1844
|
+
height: 100,
|
|
1845
|
+
left: 0,
|
|
1846
|
+
right: 100,
|
|
1847
|
+
toJSON: () => undefined,
|
|
1848
|
+
top: 0,
|
|
1849
|
+
width: 100,
|
|
1850
|
+
x: 0,
|
|
1851
|
+
y: 0,
|
|
1852
|
+
} as DOMRect);
|
|
1853
|
+
Object.defineProperty(pad, "setPointerCapture", {
|
|
1854
|
+
configurable: true,
|
|
1855
|
+
value: vi.fn(),
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
fireEvent.pointerDown(pad, {
|
|
1859
|
+
buttons: 1,
|
|
1860
|
+
clientX: 0,
|
|
1861
|
+
clientY: 0,
|
|
1862
|
+
pointerId: 1,
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
1866
|
+
expect(values["math.vector"]).toEqual({ x: "-1.00", y: "1.00" });
|
|
1490
1867
|
});
|
|
1491
1868
|
|
|
1492
1869
|
it("keeps explanatory label text in the native title instead of the visible label", () => {
|
|
@@ -1716,7 +2093,6 @@ describe("ControlsPanel", () => {
|
|
|
1716
2093
|
step: 0.1,
|
|
1717
2094
|
target: "animation.speed",
|
|
1718
2095
|
type: "slider",
|
|
1719
|
-
unit: "x",
|
|
1720
2096
|
},
|
|
1721
2097
|
},
|
|
1722
2098
|
layoutGroups: [
|
|
@@ -1743,7 +2119,7 @@ describe("ControlsPanel", () => {
|
|
|
1743
2119
|
expect(fpsSlider).toBeTruthy();
|
|
1744
2120
|
expect(container.querySelectorAll('[data-slot="slider-marker"]').length).toBeGreaterThan(0);
|
|
1745
2121
|
expect(container.textContent).toContain("17 fps");
|
|
1746
|
-
expect(container.textContent).toContain("3.
|
|
2122
|
+
expect(container.textContent).toContain("3.7");
|
|
1747
2123
|
});
|
|
1748
2124
|
|
|
1749
2125
|
it("passes schema disabled state into slider controls", () => {
|
|
@@ -2501,6 +2877,22 @@ describe("ControlsPanel", () => {
|
|
|
2501
2877
|
);
|
|
2502
2878
|
expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy();
|
|
2503
2879
|
expect(screen.getByRole("img", { name: "two.svg" })).toBeTruthy();
|
|
2880
|
+
expect(screen.getByTestId("media-ids").textContent).toBe("media-1,media-2");
|
|
2881
|
+
|
|
2882
|
+
const firstPreview = screen
|
|
2883
|
+
.getByRole("img", { name: "one.svg" })
|
|
2884
|
+
.closest<HTMLElement>('[data-slot="file-upload-preview-item"]');
|
|
2885
|
+
const secondPreview = screen
|
|
2886
|
+
.getByRole("img", { name: "two.svg" })
|
|
2887
|
+
.closest<HTMLElement>('[data-slot="file-upload-preview-item"]');
|
|
2888
|
+
|
|
2889
|
+
if (!firstPreview || !secondPreview) {
|
|
2890
|
+
throw new Error("Expected multi-image previews to render");
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
expect(firstPreview.getAttribute("role")).toBe("button");
|
|
2894
|
+
expect(firstPreview.getAttribute("tabindex")).toBe("0");
|
|
2895
|
+
expect(firstPreview.className).toContain("cursor-grab");
|
|
2504
2896
|
|
|
2505
2897
|
fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" }));
|
|
2506
2898
|
|
|
@@ -2508,6 +2900,118 @@ describe("ControlsPanel", () => {
|
|
|
2508
2900
|
expect(screen.queryByRole("img", { name: "two.svg" })).toBeNull();
|
|
2509
2901
|
});
|
|
2510
2902
|
|
|
2903
|
+
it("renders arbitrary file uploads as a sortable file list", () => {
|
|
2904
|
+
const schema = defineToolcraft({
|
|
2905
|
+
canvas: { enabled: true, upload: true },
|
|
2906
|
+
panels: {
|
|
2907
|
+
controls: {
|
|
2908
|
+
sections: [
|
|
2909
|
+
{
|
|
2910
|
+
controls: {
|
|
2911
|
+
source: {
|
|
2912
|
+
assetKind: "file",
|
|
2913
|
+
label: "Files",
|
|
2914
|
+
multiple: true,
|
|
2915
|
+
target: "input.files",
|
|
2916
|
+
type: "fileDrop",
|
|
2917
|
+
},
|
|
2918
|
+
},
|
|
2919
|
+
layout: "standalone",
|
|
2920
|
+
title: "Input",
|
|
2921
|
+
},
|
|
2922
|
+
],
|
|
2923
|
+
title: "Generation Controls",
|
|
2924
|
+
},
|
|
2925
|
+
},
|
|
2926
|
+
});
|
|
2927
|
+
|
|
2928
|
+
const { container } = renderControlsPanelWithSchema(schema, undefined, {
|
|
2929
|
+
layers: [
|
|
2930
|
+
{ id: "layer-1", kind: "layer", name: "one", visible: true },
|
|
2931
|
+
{ id: "layer-2", kind: "layer", name: "two", visible: true },
|
|
2932
|
+
],
|
|
2933
|
+
mediaAssets: [
|
|
2934
|
+
{
|
|
2935
|
+
assetKind: "file",
|
|
2936
|
+
dataUrl: "data:text/plain;base64,b25l",
|
|
2937
|
+
fileName: "one.txt",
|
|
2938
|
+
id: "media-1",
|
|
2939
|
+
layerId: "layer-1",
|
|
2940
|
+
mimeType: "text/plain",
|
|
2941
|
+
position: { x: 0, y: 0 },
|
|
2942
|
+
sourceTarget: "input.files",
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
assetKind: "file",
|
|
2946
|
+
dataUrl: "data:application/json;base64,e30=",
|
|
2947
|
+
fileName: "two.json",
|
|
2948
|
+
id: "media-2",
|
|
2949
|
+
layerId: "layer-2",
|
|
2950
|
+
mimeType: "application/json",
|
|
2951
|
+
position: { x: 0, y: 0 },
|
|
2952
|
+
sourceTarget: "input.files",
|
|
2953
|
+
},
|
|
2954
|
+
],
|
|
2955
|
+
selectedLayerId: "layer-2",
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
const fileList = container.querySelector('[data-slot="file-upload-file-list"]');
|
|
2959
|
+
const fileRows = container.querySelectorAll('[data-slot="file-upload-file-item"]');
|
|
2960
|
+
const fileDividers = container.querySelectorAll('[data-slot="file-upload-file-divider"]');
|
|
2961
|
+
|
|
2962
|
+
expect(fileList).toBeTruthy();
|
|
2963
|
+
expect(fileRows).toHaveLength(2);
|
|
2964
|
+
expect(fileDividers).toHaveLength(2);
|
|
2965
|
+
expect(screen.getByText("one.txt")).toBeTruthy();
|
|
2966
|
+
expect(screen.getByText("two.json")).toBeTruthy();
|
|
2967
|
+
expect(screen.getByText("one.txt").className).toContain("text-xs");
|
|
2968
|
+
expect(screen.getByText("one.txt").className).not.toContain("truncate");
|
|
2969
|
+
expect(screen.getByText("one.txt").className).toContain("mask-image");
|
|
2970
|
+
expect(screen.getByRole("button", { name: "Remove one.txt" })).toBeTruthy();
|
|
2971
|
+
expect(screen.getByRole("button", { name: "Add a new file" })).toBeTruthy();
|
|
2972
|
+
expect(fileRows[0]?.className).not.toContain("border-b");
|
|
2973
|
+
expect(fileRows[0]?.className).toContain("hover:bg");
|
|
2974
|
+
expect(fileDividers[0]?.className).toContain("var(--border)_5%");
|
|
2975
|
+
expect(fileRows[0]?.getAttribute("role")).toBe("button");
|
|
2976
|
+
expect(fileRows[0]?.getAttribute("tabindex")).toBe("0");
|
|
2977
|
+
|
|
2978
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove two.json" }));
|
|
2979
|
+
|
|
2980
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
2981
|
+
expect(screen.queryByText("two.json")).toBeNull();
|
|
2982
|
+
});
|
|
2983
|
+
|
|
2984
|
+
it("renders the arbitrary file uploader empty state", () => {
|
|
2985
|
+
const schema = defineToolcraft({
|
|
2986
|
+
canvas: { enabled: true, upload: true },
|
|
2987
|
+
panels: {
|
|
2988
|
+
controls: {
|
|
2989
|
+
sections: [
|
|
2990
|
+
{
|
|
2991
|
+
controls: {
|
|
2992
|
+
source: {
|
|
2993
|
+
assetKind: "file",
|
|
2994
|
+
label: "Files",
|
|
2995
|
+
target: "input.files",
|
|
2996
|
+
type: "fileDrop",
|
|
2997
|
+
},
|
|
2998
|
+
},
|
|
2999
|
+
layout: "standalone",
|
|
3000
|
+
title: "Input",
|
|
3001
|
+
},
|
|
3002
|
+
],
|
|
3003
|
+
title: "Generation Controls",
|
|
3004
|
+
},
|
|
3005
|
+
},
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
renderControlsPanelWithSchema(schema);
|
|
3009
|
+
|
|
3010
|
+
expect(screen.getByText("Click to upload a file")).toBeTruthy();
|
|
3011
|
+
expect(screen.getByText("or drag it onto the canvas")).toBeTruthy();
|
|
3012
|
+
expect(screen.queryByText("Click to upload an image")).toBeNull();
|
|
3013
|
+
});
|
|
3014
|
+
|
|
2511
3015
|
it("keeps file upload deletion in the layers panel for multi-layer apps", () => {
|
|
2512
3016
|
const schema = defineToolcraft({
|
|
2513
3017
|
canvas: { enabled: true, upload: true },
|
|
@@ -2574,6 +3078,65 @@ describe("ControlsPanel", () => {
|
|
|
2574
3078
|
expect(colorControlList?.children).toHaveLength(1);
|
|
2575
3079
|
});
|
|
2576
3080
|
|
|
3081
|
+
it("keeps an odd trailing plain color half-width in a color bank", () => {
|
|
3082
|
+
const schema = defineToolcraft({
|
|
3083
|
+
canvas: { enabled: true },
|
|
3084
|
+
panels: {
|
|
3085
|
+
controls: {
|
|
3086
|
+
sections: [
|
|
3087
|
+
{
|
|
3088
|
+
controls: {
|
|
3089
|
+
accent1: {
|
|
3090
|
+
defaultValue: { hex: "#9CE6FF" },
|
|
3091
|
+
label: "Accent 1",
|
|
3092
|
+
target: "accent.shade1",
|
|
3093
|
+
type: "color",
|
|
3094
|
+
},
|
|
3095
|
+
accent2: {
|
|
3096
|
+
defaultValue: { hex: "#FF7A90" },
|
|
3097
|
+
label: "Accent 2",
|
|
3098
|
+
target: "accent.shade2",
|
|
3099
|
+
type: "color",
|
|
3100
|
+
},
|
|
3101
|
+
accent3: {
|
|
3102
|
+
defaultValue: { hex: "#FFD166" },
|
|
3103
|
+
label: "Accent 3",
|
|
3104
|
+
target: "accent.shade3",
|
|
3105
|
+
type: "color",
|
|
3106
|
+
},
|
|
3107
|
+
accent4: {
|
|
3108
|
+
defaultValue: { hex: "#8BF38B" },
|
|
3109
|
+
label: "Accent 4",
|
|
3110
|
+
target: "accent.shade4",
|
|
3111
|
+
type: "color",
|
|
3112
|
+
},
|
|
3113
|
+
accent5: {
|
|
3114
|
+
defaultValue: { hex: "#BDA8FF" },
|
|
3115
|
+
label: "Accent 5",
|
|
3116
|
+
target: "accent.shade5",
|
|
3117
|
+
type: "color",
|
|
3118
|
+
},
|
|
3119
|
+
},
|
|
3120
|
+
title: "Accent Shades",
|
|
3121
|
+
},
|
|
3122
|
+
],
|
|
3123
|
+
title: "Generation Controls",
|
|
3124
|
+
},
|
|
3125
|
+
},
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
3129
|
+
const pairedColorRows = container.querySelectorAll('[data-slot="color-control-grid"]');
|
|
3130
|
+
const trailingColorButtonGroup = screen
|
|
3131
|
+
.getByLabelText("Accent 5 hex")
|
|
3132
|
+
.closest('[data-slot="button-group"]');
|
|
3133
|
+
const trailingColorWidthContainer = trailingColorButtonGroup?.parentElement;
|
|
3134
|
+
|
|
3135
|
+
expect(pairedColorRows).toHaveLength(2);
|
|
3136
|
+
expect(trailingColorWidthContainer?.className).toContain("w-1/2");
|
|
3137
|
+
expect(trailingColorWidthContainer?.className).not.toContain("w-full");
|
|
3138
|
+
});
|
|
3139
|
+
|
|
2577
3140
|
it("omits visible color field labels only inside color-only sections", () => {
|
|
2578
3141
|
const schema = defineToolcraft({
|
|
2579
3142
|
canvas: { enabled: true },
|
|
@@ -2715,6 +3278,35 @@ describe("ControlsPanel", () => {
|
|
|
2715
3278
|
});
|
|
2716
3279
|
});
|
|
2717
3280
|
|
|
3281
|
+
it("applies color picker slider changes while dragging", async () => {
|
|
3282
|
+
const onChange = vi.fn();
|
|
3283
|
+
|
|
3284
|
+
render(<StyleGuideColorPicker value="#C1FF00" onChange={onChange} />);
|
|
3285
|
+
|
|
3286
|
+
const hueSlider = screen.getByRole("slider", { name: "Color hue" });
|
|
3287
|
+
Object.defineProperty(hueSlider, "getBoundingClientRect", {
|
|
3288
|
+
configurable: true,
|
|
3289
|
+
value: () => ({
|
|
3290
|
+
bottom: 18,
|
|
3291
|
+
height: 18,
|
|
3292
|
+
left: 0,
|
|
3293
|
+
right: 360,
|
|
3294
|
+
top: 0,
|
|
3295
|
+
width: 360,
|
|
3296
|
+
x: 0,
|
|
3297
|
+
y: 0,
|
|
3298
|
+
toJSON: () => ({}),
|
|
3299
|
+
}),
|
|
3300
|
+
});
|
|
3301
|
+
|
|
3302
|
+
fireEvent.pointerDown(hueSlider, { clientX: 0 });
|
|
3303
|
+
fireEvent.pointerMove(window, { clientX: 180 });
|
|
3304
|
+
|
|
3305
|
+
await waitFor(() => {
|
|
3306
|
+
expect(onChange).toHaveBeenLastCalledWith("#00ffff");
|
|
3307
|
+
});
|
|
3308
|
+
});
|
|
3309
|
+
|
|
2718
3310
|
it("keeps two color opacity controls stacked even when an inline row is requested", () => {
|
|
2719
3311
|
const schema = defineToolcraft({
|
|
2720
3312
|
canvas: { enabled: true },
|
|
@@ -3187,19 +3779,17 @@ describe("ControlsPanel", () => {
|
|
|
3187
3779
|
expect(gradientControlItem?.className).toContain(
|
|
3188
3780
|
"has-data-[control-section-divider=compound]:after:inset-x-3",
|
|
3189
3781
|
);
|
|
3190
|
-
expect(fontControlItem?.dataset.controlItemCompoundDividerPlacement).toBe(
|
|
3191
|
-
"both",
|
|
3192
|
-
);
|
|
3782
|
+
expect(fontControlItem?.dataset.controlItemCompoundDividerPlacement).toBe("top");
|
|
3193
3783
|
expect(fontControlItem?.className).toContain(
|
|
3194
3784
|
"has-data-[control-section-divider=compound]:pt-[18px]",
|
|
3195
3785
|
);
|
|
3196
|
-
expect(fontControlItem?.className).toContain(
|
|
3786
|
+
expect(fontControlItem?.className).not.toContain(
|
|
3197
3787
|
"has-data-[control-section-divider=compound]:pb-[18px]",
|
|
3198
3788
|
);
|
|
3199
3789
|
expect(fontControlItem?.className).toContain(
|
|
3200
3790
|
"has-data-[control-section-divider=compound]:before:inset-x-3",
|
|
3201
3791
|
);
|
|
3202
|
-
expect(fontControlItem?.className).toContain(
|
|
3792
|
+
expect(fontControlItem?.className).not.toContain(
|
|
3203
3793
|
"has-data-[control-section-divider=compound]:after:inset-x-3",
|
|
3204
3794
|
);
|
|
3205
3795
|
});
|
|
@@ -3341,6 +3931,54 @@ describe("ControlsPanel", () => {
|
|
|
3341
3931
|
expect(container.querySelector('[data-control-section-reset-button]')).toBeTruthy();
|
|
3342
3932
|
});
|
|
3343
3933
|
|
|
3934
|
+
it("clears file upload previews from the selected section reset action", () => {
|
|
3935
|
+
const schema = defineToolcraft({
|
|
3936
|
+
canvas: { enabled: true, upload: true },
|
|
3937
|
+
panels: {
|
|
3938
|
+
controls: {
|
|
3939
|
+
sections: [
|
|
3940
|
+
{
|
|
3941
|
+
controls: {
|
|
3942
|
+
source: {
|
|
3943
|
+
defaultValue: null,
|
|
3944
|
+
label: "Image",
|
|
3945
|
+
target: "media.source",
|
|
3946
|
+
type: "fileDrop",
|
|
3947
|
+
},
|
|
3948
|
+
},
|
|
3949
|
+
title: "Source Image",
|
|
3950
|
+
},
|
|
3951
|
+
],
|
|
3952
|
+
title: "Controls",
|
|
3953
|
+
},
|
|
3954
|
+
},
|
|
3955
|
+
});
|
|
3956
|
+
|
|
3957
|
+
renderControlsPanelWithSchema(schema, {}, {
|
|
3958
|
+
mediaAssets: [
|
|
3959
|
+
{
|
|
3960
|
+
dataUrl:
|
|
3961
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 3'/%3E",
|
|
3962
|
+
fileName: "source.svg",
|
|
3963
|
+
id: "media-1",
|
|
3964
|
+
layerId: "layer-1",
|
|
3965
|
+
mimeType: "image/svg+xml",
|
|
3966
|
+
position: { x: 0, y: 0 },
|
|
3967
|
+
size: { height: 300, unit: "px", width: 400 },
|
|
3968
|
+
sourceTarget: "media.source",
|
|
3969
|
+
},
|
|
3970
|
+
],
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
expect(screen.getByRole("img", { name: "source.svg" })).toBeTruthy();
|
|
3974
|
+
expect(screen.getByTestId("media-count").textContent).toBe("1");
|
|
3975
|
+
|
|
3976
|
+
fireEvent.click(screen.getByRole("button", { name: "Reset Source Image section" }));
|
|
3977
|
+
|
|
3978
|
+
expect(screen.queryByRole("img", { name: "source.svg" })).toBeNull();
|
|
3979
|
+
expect(screen.getByTestId("media-count").textContent).toBe("0");
|
|
3980
|
+
});
|
|
3981
|
+
|
|
3344
3982
|
it("renders collection actions as add/remove controls for runtime item arrays", () => {
|
|
3345
3983
|
const schema = defineToolcraft({
|
|
3346
3984
|
canvas: { enabled: false },
|
|
@@ -3449,6 +4087,101 @@ describe("ControlsPanel", () => {
|
|
|
3449
4087
|
expect(screen.getByDisplayValue("New label")).toBeTruthy();
|
|
3450
4088
|
});
|
|
3451
4089
|
|
|
4090
|
+
it("keeps a collection font picker color popover open while editing its color surface", async () => {
|
|
4091
|
+
const schema = defineToolcraft({
|
|
4092
|
+
canvas: { enabled: false },
|
|
4093
|
+
panels: {
|
|
4094
|
+
controls: {
|
|
4095
|
+
sections: [
|
|
4096
|
+
{
|
|
4097
|
+
controls: {
|
|
4098
|
+
textStyles: {
|
|
4099
|
+
defaultValue: [
|
|
4100
|
+
{
|
|
4101
|
+
color: "#FFFFFF",
|
|
4102
|
+
fontId: "inter",
|
|
4103
|
+
fontSize: 16,
|
|
4104
|
+
fontWeight: "400",
|
|
4105
|
+
letterSpacing: "normal",
|
|
4106
|
+
lineHeight: "normal",
|
|
4107
|
+
opacity: 100,
|
|
4108
|
+
textCase: "original",
|
|
4109
|
+
},
|
|
4110
|
+
],
|
|
4111
|
+
itemControl: {
|
|
4112
|
+
defaultValue: {
|
|
4113
|
+
color: "#FFFFFF",
|
|
4114
|
+
fontId: "inter",
|
|
4115
|
+
fontSize: 16,
|
|
4116
|
+
fontWeight: "400",
|
|
4117
|
+
letterSpacing: "normal",
|
|
4118
|
+
lineHeight: "normal",
|
|
4119
|
+
opacity: 100,
|
|
4120
|
+
textCase: "original",
|
|
4121
|
+
},
|
|
4122
|
+
label: "Font",
|
|
4123
|
+
type: "fontPicker",
|
|
4124
|
+
},
|
|
4125
|
+
label: "Text styles",
|
|
4126
|
+
minItems: 1,
|
|
4127
|
+
target: "text.styles",
|
|
4128
|
+
type: "collectionActions",
|
|
4129
|
+
},
|
|
4130
|
+
},
|
|
4131
|
+
title: "Typography",
|
|
4132
|
+
},
|
|
4133
|
+
],
|
|
4134
|
+
title: "Controls",
|
|
4135
|
+
},
|
|
4136
|
+
},
|
|
4137
|
+
});
|
|
4138
|
+
|
|
4139
|
+
const { container } = renderControlsPanelWithSchema(schema);
|
|
4140
|
+
|
|
4141
|
+
fireEvent.click(screen.getByRole("button", { name: "Pick Color" }));
|
|
4142
|
+
|
|
4143
|
+
const surface = await waitFor(() => {
|
|
4144
|
+
const nextSurface = container.ownerDocument.body.querySelector<HTMLElement>(
|
|
4145
|
+
'[data-slot="style-guide-color-surface"]',
|
|
4146
|
+
);
|
|
4147
|
+
if (!nextSurface) {
|
|
4148
|
+
throw new Error("Expected color surface to be open.");
|
|
4149
|
+
}
|
|
4150
|
+
return nextSurface;
|
|
4151
|
+
});
|
|
4152
|
+
|
|
4153
|
+
const surfaceRect = {
|
|
4154
|
+
bottom: 100,
|
|
4155
|
+
height: 100,
|
|
4156
|
+
left: 0,
|
|
4157
|
+
right: 100,
|
|
4158
|
+
toJSON: () => ({}),
|
|
4159
|
+
top: 0,
|
|
4160
|
+
width: 100,
|
|
4161
|
+
x: 0,
|
|
4162
|
+
y: 0,
|
|
4163
|
+
} as DOMRect;
|
|
4164
|
+
Object.defineProperty(surface, "getBoundingClientRect", {
|
|
4165
|
+
configurable: true,
|
|
4166
|
+
value: () => surfaceRect,
|
|
4167
|
+
});
|
|
4168
|
+
|
|
4169
|
+
fireEvent.pointerDown(surface, {
|
|
4170
|
+
buttons: 1,
|
|
4171
|
+
clientX: 72,
|
|
4172
|
+
clientY: 28,
|
|
4173
|
+
pointerId: 1,
|
|
4174
|
+
});
|
|
4175
|
+
|
|
4176
|
+
expect(
|
|
4177
|
+
container.ownerDocument.body.querySelector('[data-slot="style-guide-color-surface"]'),
|
|
4178
|
+
).toBeTruthy();
|
|
4179
|
+
|
|
4180
|
+
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
4181
|
+
|
|
4182
|
+
expect(values["text.styles"][0].color).not.toBe("#FFFFFF");
|
|
4183
|
+
});
|
|
4184
|
+
|
|
3452
4185
|
it("commits canvas size controls to runtime state on blur or Enter", () => {
|
|
3453
4186
|
const { container } = renderControlsPanel();
|
|
3454
4187
|
|
|
@@ -3576,7 +4309,7 @@ describe("ControlsPanel", () => {
|
|
|
3576
4309
|
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3577
4310
|
|
|
3578
4311
|
expect(screen.getByText("Resolution scale")).toBeTruthy();
|
|
3579
|
-
expect(screen.getByText("
|
|
4312
|
+
expect(screen.getByText("2")).toBeTruthy();
|
|
3580
4313
|
expect(values["canvas.renderScale"]).toBe(2);
|
|
3581
4314
|
});
|
|
3582
4315
|
|
|
@@ -4017,6 +4750,30 @@ describe("ControlsPanel", () => {
|
|
|
4017
4750
|
expect(container.querySelector('[data-slot="control-section-header"]')).toBeTruthy();
|
|
4018
4751
|
});
|
|
4019
4752
|
|
|
4753
|
+
it("suppresses child control transitions when a section re-expands", () => {
|
|
4754
|
+
const schema = createSchema();
|
|
4755
|
+
renderControlsPanelWithSchema(schema);
|
|
4756
|
+
const basicTitle = screen.getByText("Basic");
|
|
4757
|
+
const header = basicTitle.closest<HTMLElement>('[data-slot="control-section-header"]');
|
|
4758
|
+
const section = basicTitle.closest("section");
|
|
4759
|
+
const initialBody = section?.querySelector<HTMLElement>(
|
|
4760
|
+
'[data-slot="panel-section-collapsible-body"]',
|
|
4761
|
+
);
|
|
4762
|
+
|
|
4763
|
+
fireEvent.click(header!);
|
|
4764
|
+
fireEvent.transitionEnd(initialBody!);
|
|
4765
|
+
fireEvent.click(header!);
|
|
4766
|
+
|
|
4767
|
+
const expandedBody = section?.querySelector<HTMLElement>(
|
|
4768
|
+
'[data-slot="panel-section-collapsible-body"]',
|
|
4769
|
+
);
|
|
4770
|
+
const innerControls = expandedBody?.firstElementChild as HTMLElement | null;
|
|
4771
|
+
|
|
4772
|
+
expect(expandedBody?.className).toContain("transition-[grid-template-rows,opacity]");
|
|
4773
|
+
expect(expandedBody?.className).toContain("grid-rows-[1fr]");
|
|
4774
|
+
expect(innerControls?.getAttribute("data-toolcraft-controls-mounting")).toBe("true");
|
|
4775
|
+
});
|
|
4776
|
+
|
|
4020
4777
|
it("persists ordinary section collapse state as a UI preference across remounts", () => {
|
|
4021
4778
|
const schema = createSchema();
|
|
4022
4779
|
const { unmount } = renderControlsPanelWithSchema(schema);
|