@pixel-point/toolcraft 0.0.8 → 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 +104 -14
- package/templates/runtime/contracts/component-contracts.ts +54 -22
- package/templates/runtime/contracts/decision-contracts.test.ts +5 -0
- package/templates/runtime/contracts/decision-contracts.ts +3 -3
- package/templates/runtime/react/controls-panel.test.tsx +374 -13
- package/templates/runtime/react/controls-panel.tsx +65 -4
- package/templates/runtime/schema/define-toolcraft.test.ts +45 -1
- package/templates/runtime/schema/define-toolcraft.ts +25 -1
- package/templates/runtime/schema/types.ts +3 -0
- package/templates/runtime/testing/performance.test.ts +134 -0
- package/templates/runtime/testing/performance.ts +34 -4
- package/templates/starter/AGENTS.md +4 -4
- package/templates/starter/docs/toolcraft/README.md +1 -1
- package/templates/starter/docs/toolcraft/acceptance-testing.md +5 -3
- package/templates/starter/docs/toolcraft/assembly-workflow.md +9 -5
- package/templates/starter/docs/toolcraft/component-rules.md +32 -11
- package/templates/starter/docs/toolcraft/performance.md +7 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +43 -14
- package/templates/starter/gitignore +1 -0
- package/templates/starter/package.json +2 -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 +739 -66
- package/templates/starter/src/app/starter-acceptance.ts +471 -12
- package/templates/ui/components/control-layout/index.tsx +8 -3
- package/templates/ui/components/controls/actions/actions-control.tsx +11 -3
- 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/index.ts +2 -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 +29 -5
|
@@ -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";
|
|
@@ -387,7 +388,7 @@ describe("ControlsPanel", () => {
|
|
|
387
388
|
expect(inlineGroup?.textContent).toContain("Snap Y");
|
|
388
389
|
});
|
|
389
390
|
|
|
390
|
-
it("
|
|
391
|
+
it("renders local action buttons as a two-column grid below their label", () => {
|
|
391
392
|
const schema = defineToolcraft({
|
|
392
393
|
canvas: { enabled: false },
|
|
393
394
|
panels: {
|
|
@@ -421,10 +422,53 @@ describe("ControlsPanel", () => {
|
|
|
421
422
|
'[data-slot="actions-control-buttons"]',
|
|
422
423
|
);
|
|
423
424
|
|
|
424
|
-
expect(actionField?.
|
|
425
|
-
expect(actionField?.className).toContain("
|
|
426
|
-
expect(actionButtons?.className).toContain("
|
|
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");
|
|
427
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");
|
|
428
472
|
});
|
|
429
473
|
|
|
430
474
|
it("renders the managed background toggle plus unlabeled color row as equal-width inline columns", () => {
|
|
@@ -474,6 +518,7 @@ describe("ControlsPanel", () => {
|
|
|
474
518
|
expect(screen.getByText("Include")).toBeTruthy();
|
|
475
519
|
expect(screen.getByRole("switch")).toBeTruthy();
|
|
476
520
|
expect(toggleParameterGroup).toBeTruthy();
|
|
521
|
+
expect(toggleParameterGroup?.className).toContain("gap-x-2.5");
|
|
477
522
|
expect(toggleParameterGroup?.getAttribute("style")).toContain(
|
|
478
523
|
"repeat(2, minmax(0, 1fr))",
|
|
479
524
|
);
|
|
@@ -616,13 +661,62 @@ describe("ControlsPanel", () => {
|
|
|
616
661
|
);
|
|
617
662
|
|
|
618
663
|
expect(screen.getByText("Loop")).toBeTruthy();
|
|
619
|
-
expect(screen.
|
|
664
|
+
expect(screen.queryByText("Duration")).toBeNull();
|
|
620
665
|
expect(toggleParameterGroup).toBeTruthy();
|
|
666
|
+
expect(toggleParameterGroup?.className).toContain("gap-x-2.5");
|
|
621
667
|
expect(toggleParameterGroup?.getAttribute("style")).toContain(
|
|
622
668
|
"repeat(2, minmax(0, 1fr))",
|
|
623
669
|
);
|
|
624
670
|
});
|
|
625
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
|
+
|
|
626
720
|
it("renders compact select pairs inline with stacked full-width fields", () => {
|
|
627
721
|
const schema = defineToolcraft({
|
|
628
722
|
canvas: { enabled: false },
|
|
@@ -677,6 +771,7 @@ describe("ControlsPanel", () => {
|
|
|
677
771
|
);
|
|
678
772
|
|
|
679
773
|
expect(inlineGroup).toBeTruthy();
|
|
774
|
+
expect(inlineGroup?.className).toContain("gap-x-2.5");
|
|
680
775
|
expect(screen.getByText("Format")).toBeTruthy();
|
|
681
776
|
expect(screen.getByText("Resolution")).toBeTruthy();
|
|
682
777
|
expect(triggers).toHaveLength(2);
|
|
@@ -1529,6 +1624,246 @@ describe("ControlsPanel", () => {
|
|
|
1529
1624
|
"toneBias",
|
|
1530
1625
|
"default",
|
|
1531
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" });
|
|
1532
1867
|
});
|
|
1533
1868
|
|
|
1534
1869
|
it("keeps explanatory label text in the native title instead of the visible label", () => {
|
|
@@ -1758,7 +2093,6 @@ describe("ControlsPanel", () => {
|
|
|
1758
2093
|
step: 0.1,
|
|
1759
2094
|
target: "animation.speed",
|
|
1760
2095
|
type: "slider",
|
|
1761
|
-
unit: "x",
|
|
1762
2096
|
},
|
|
1763
2097
|
},
|
|
1764
2098
|
layoutGroups: [
|
|
@@ -1785,7 +2119,7 @@ describe("ControlsPanel", () => {
|
|
|
1785
2119
|
expect(fpsSlider).toBeTruthy();
|
|
1786
2120
|
expect(container.querySelectorAll('[data-slot="slider-marker"]').length).toBeGreaterThan(0);
|
|
1787
2121
|
expect(container.textContent).toContain("17 fps");
|
|
1788
|
-
expect(container.textContent).toContain("3.
|
|
2122
|
+
expect(container.textContent).toContain("3.7");
|
|
1789
2123
|
});
|
|
1790
2124
|
|
|
1791
2125
|
it("passes schema disabled state into slider controls", () => {
|
|
@@ -2944,6 +3278,35 @@ describe("ControlsPanel", () => {
|
|
|
2944
3278
|
});
|
|
2945
3279
|
});
|
|
2946
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
|
+
|
|
2947
3310
|
it("keeps two color opacity controls stacked even when an inline row is requested", () => {
|
|
2948
3311
|
const schema = defineToolcraft({
|
|
2949
3312
|
canvas: { enabled: true },
|
|
@@ -3416,19 +3779,17 @@ describe("ControlsPanel", () => {
|
|
|
3416
3779
|
expect(gradientControlItem?.className).toContain(
|
|
3417
3780
|
"has-data-[control-section-divider=compound]:after:inset-x-3",
|
|
3418
3781
|
);
|
|
3419
|
-
expect(fontControlItem?.dataset.controlItemCompoundDividerPlacement).toBe(
|
|
3420
|
-
"both",
|
|
3421
|
-
);
|
|
3782
|
+
expect(fontControlItem?.dataset.controlItemCompoundDividerPlacement).toBe("top");
|
|
3422
3783
|
expect(fontControlItem?.className).toContain(
|
|
3423
3784
|
"has-data-[control-section-divider=compound]:pt-[18px]",
|
|
3424
3785
|
);
|
|
3425
|
-
expect(fontControlItem?.className).toContain(
|
|
3786
|
+
expect(fontControlItem?.className).not.toContain(
|
|
3426
3787
|
"has-data-[control-section-divider=compound]:pb-[18px]",
|
|
3427
3788
|
);
|
|
3428
3789
|
expect(fontControlItem?.className).toContain(
|
|
3429
3790
|
"has-data-[control-section-divider=compound]:before:inset-x-3",
|
|
3430
3791
|
);
|
|
3431
|
-
expect(fontControlItem?.className).toContain(
|
|
3792
|
+
expect(fontControlItem?.className).not.toContain(
|
|
3432
3793
|
"has-data-[control-section-divider=compound]:after:inset-x-3",
|
|
3433
3794
|
);
|
|
3434
3795
|
});
|
|
@@ -3948,7 +4309,7 @@ describe("ControlsPanel", () => {
|
|
|
3948
4309
|
const values = JSON.parse(screen.getByTestId("values-json").textContent ?? "{}");
|
|
3949
4310
|
|
|
3950
4311
|
expect(screen.getByText("Resolution scale")).toBeTruthy();
|
|
3951
|
-
expect(screen.getByText("
|
|
4312
|
+
expect(screen.getByText("2")).toBeTruthy();
|
|
3952
4313
|
expect(values["canvas.renderScale"]).toBe(2);
|
|
3953
4314
|
});
|
|
3954
4315
|
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
type GradientStop,
|
|
48
48
|
type GradientType,
|
|
49
49
|
type ImagePickerItem,
|
|
50
|
+
type VectorPadCoordinateMode,
|
|
50
51
|
type VectorPadVariant,
|
|
51
52
|
} from "@repo/ui";
|
|
52
53
|
|
|
@@ -421,6 +422,17 @@ function asVectorPadVariant(value: string | undefined): VectorPadVariant {
|
|
|
421
422
|
return "default";
|
|
422
423
|
}
|
|
423
424
|
|
|
425
|
+
function asVectorPadCoordinateMode(
|
|
426
|
+
value: string | undefined,
|
|
427
|
+
padVariant: VectorPadVariant,
|
|
428
|
+
): VectorPadCoordinateMode {
|
|
429
|
+
if (value === "cartesian" || value === "screen") {
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return padVariant === "default" ? "screen" : "cartesian";
|
|
434
|
+
}
|
|
435
|
+
|
|
424
436
|
function asCurveInterpolation(value: string | undefined): CurveInterpolation | undefined {
|
|
425
437
|
return value === "monotone" || value === "smooth" ? value : undefined;
|
|
426
438
|
}
|
|
@@ -1154,6 +1166,15 @@ function hasSliderControl(
|
|
|
1154
1166
|
);
|
|
1155
1167
|
}
|
|
1156
1168
|
|
|
1169
|
+
function hasSegmentedControl(
|
|
1170
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
1171
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
1172
|
+
): boolean {
|
|
1173
|
+
return layoutGroup.controls.some(
|
|
1174
|
+
(controlId) => controlsById[controlId]?.type === "segmented",
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1157
1178
|
function hasSectionedCompoundControl(
|
|
1158
1179
|
controlsById: Record<string, ToolcraftControlSchema>,
|
|
1159
1180
|
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
@@ -1214,6 +1235,22 @@ function isToggleParameterLayoutGroup(
|
|
|
1214
1235
|
return booleanControlCount === 1 && parameterControlCount === 1;
|
|
1215
1236
|
}
|
|
1216
1237
|
|
|
1238
|
+
function shouldHideToggleParameterControlLabel({
|
|
1239
|
+
control,
|
|
1240
|
+
controlsById,
|
|
1241
|
+
layoutGroup,
|
|
1242
|
+
}: {
|
|
1243
|
+
control: ToolcraftControlSchema;
|
|
1244
|
+
controlsById: Record<string, ToolcraftControlSchema>;
|
|
1245
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema | undefined;
|
|
1246
|
+
}): boolean {
|
|
1247
|
+
return Boolean(
|
|
1248
|
+
layoutGroup &&
|
|
1249
|
+
isToggleParameterLayoutGroup(controlsById, layoutGroup) &&
|
|
1250
|
+
!isBooleanControl(control),
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1217
1254
|
function getControlTargetEntityKey(control: ToolcraftControlSchema): string | null {
|
|
1218
1255
|
const parts = control.target.split(".");
|
|
1219
1256
|
|
|
@@ -1409,6 +1446,7 @@ function getInlineLayoutGroupByControlId({
|
|
|
1409
1446
|
if (
|
|
1410
1447
|
hasColorOpacityControl(controlsById, layoutGroup) ||
|
|
1411
1448
|
hasSliderControl(controlsById, layoutGroup) ||
|
|
1449
|
+
hasSegmentedControl(controlsById, layoutGroup) ||
|
|
1412
1450
|
hasSectionedCompoundControl(controlsById, layoutGroup) ||
|
|
1413
1451
|
hasUnsafeInlineSwitchLabels(controlsById, layoutGroup)
|
|
1414
1452
|
) {
|
|
@@ -1893,6 +1931,10 @@ export function ControlsPanel({
|
|
|
1893
1931
|
providerKey: string;
|
|
1894
1932
|
sectionTitle: string | undefined;
|
|
1895
1933
|
}): React.ReactNode {
|
|
1934
|
+
if (control.label === false) {
|
|
1935
|
+
return children;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1896
1938
|
const help = getControlHelpText({ control, label, sectionTitle });
|
|
1897
1939
|
|
|
1898
1940
|
if (!help) {
|
|
@@ -2404,13 +2446,23 @@ export function ControlsPanel({
|
|
|
2404
2446
|
}
|
|
2405
2447
|
|
|
2406
2448
|
const [id, rawControl] = group.entry;
|
|
2407
|
-
const
|
|
2449
|
+
const inlineLayoutGroup = inlineLayoutGroupByControlId.get(id);
|
|
2450
|
+
const isInInlineLayoutGroup = Boolean(inlineLayoutGroup);
|
|
2408
2451
|
const disabled = isControlDisabled(rawControl);
|
|
2409
|
-
const
|
|
2452
|
+
const resolvedControl =
|
|
2410
2453
|
disabled === Boolean(rawControl.disabled)
|
|
2411
2454
|
? rawControl
|
|
2412
2455
|
: { ...rawControl, disabled };
|
|
2413
|
-
const
|
|
2456
|
+
const shouldHideLabel = shouldHideToggleParameterControlLabel({
|
|
2457
|
+
control: resolvedControl,
|
|
2458
|
+
controlsById: visibleControls,
|
|
2459
|
+
layoutGroup: inlineLayoutGroup,
|
|
2460
|
+
});
|
|
2461
|
+
const control =
|
|
2462
|
+
shouldHideLabel && resolvedControl.label !== false
|
|
2463
|
+
? { ...resolvedControl, label: false }
|
|
2464
|
+
: resolvedControl;
|
|
2465
|
+
const name = getControlName(id, resolvedControl.label);
|
|
2414
2466
|
const value = getControlValue(control);
|
|
2415
2467
|
const usesHeaderKeyframeAction = control.target === headerKeyframeTarget;
|
|
2416
2468
|
const commitWithLabel =
|
|
@@ -2522,6 +2574,7 @@ export function ControlsPanel({
|
|
|
2522
2574
|
key={id}
|
|
2523
2575
|
name={name}
|
|
2524
2576
|
onValueChange={commit}
|
|
2577
|
+
showLabel={control.label !== false}
|
|
2525
2578
|
value={asString(value)}
|
|
2526
2579
|
/>
|
|
2527
2580
|
),
|
|
@@ -2863,6 +2916,7 @@ export function ControlsPanel({
|
|
|
2863
2916
|
key={id}
|
|
2864
2917
|
name={name}
|
|
2865
2918
|
onValueChange={commit}
|
|
2919
|
+
showLabel={control.label !== false}
|
|
2866
2920
|
start={rangeValue.start}
|
|
2867
2921
|
/>
|
|
2868
2922
|
),
|
|
@@ -2929,6 +2983,7 @@ export function ControlsPanel({
|
|
|
2929
2983
|
name={name}
|
|
2930
2984
|
onValueChange={commit}
|
|
2931
2985
|
options={control.options ?? []}
|
|
2986
|
+
showLabel={control.label !== false}
|
|
2932
2987
|
value={asString(value, control.options?.[0]?.value ?? "")}
|
|
2933
2988
|
/>
|
|
2934
2989
|
);
|
|
@@ -3008,6 +3063,7 @@ export function ControlsPanel({
|
|
|
3008
3063
|
key={id}
|
|
3009
3064
|
name={name}
|
|
3010
3065
|
onValueChange={commit}
|
|
3066
|
+
showLabel={control.label !== false}
|
|
3011
3067
|
value={asString(value)}
|
|
3012
3068
|
/>
|
|
3013
3069
|
),
|
|
@@ -3020,6 +3076,7 @@ export function ControlsPanel({
|
|
|
3020
3076
|
|
|
3021
3077
|
case "vector": {
|
|
3022
3078
|
const vectorValue = asVectorValue(value);
|
|
3079
|
+
const padVariant = asVectorPadVariant(control.variant);
|
|
3023
3080
|
|
|
3024
3081
|
return withKeyframeLabelAction({
|
|
3025
3082
|
children: (
|
|
@@ -3028,8 +3085,12 @@ export function ControlsPanel({
|
|
|
3028
3085
|
key={id}
|
|
3029
3086
|
name={name}
|
|
3030
3087
|
onValueChange={commit}
|
|
3088
|
+
padCoordinateMode={asVectorPadCoordinateMode(
|
|
3089
|
+
control.coordinateMode,
|
|
3090
|
+
padVariant,
|
|
3091
|
+
)}
|
|
3031
3092
|
padShape={vectorPadShape}
|
|
3032
|
-
padVariant={
|
|
3093
|
+
padVariant={padVariant}
|
|
3033
3094
|
x={vectorValue.x}
|
|
3034
3095
|
xLabel={control.xLabel}
|
|
3035
3096
|
y={vectorValue.y}
|
|
@@ -837,7 +837,6 @@ describe("defineToolcraft", () => {
|
|
|
837
837
|
step: 0.25,
|
|
838
838
|
target: "canvas.renderScale",
|
|
839
839
|
type: "slider",
|
|
840
|
-
unit: "x",
|
|
841
840
|
variant: "discrete",
|
|
842
841
|
});
|
|
843
842
|
});
|
|
@@ -1397,6 +1396,51 @@ describe("defineToolcraft", () => {
|
|
|
1397
1396
|
]);
|
|
1398
1397
|
});
|
|
1399
1398
|
|
|
1399
|
+
it("keeps mode-gated standalone controls inside their semantic section", () => {
|
|
1400
|
+
const app = defineToolcraft({
|
|
1401
|
+
canvas: { enabled: true },
|
|
1402
|
+
panels: {
|
|
1403
|
+
controls: {
|
|
1404
|
+
sections: [
|
|
1405
|
+
{
|
|
1406
|
+
controls: {
|
|
1407
|
+
sourceMode: {
|
|
1408
|
+
defaultValue: "preset",
|
|
1409
|
+
label: "Source",
|
|
1410
|
+
options: [
|
|
1411
|
+
{ label: "Preset", value: "preset" },
|
|
1412
|
+
{ label: "Image", value: "image" },
|
|
1413
|
+
],
|
|
1414
|
+
target: "source.mode",
|
|
1415
|
+
type: "segmented",
|
|
1416
|
+
},
|
|
1417
|
+
sourceUpload: {
|
|
1418
|
+
accept: "image/*",
|
|
1419
|
+
assetKind: "image",
|
|
1420
|
+
defaultValue: null,
|
|
1421
|
+
label: "Image",
|
|
1422
|
+
target: "source.upload",
|
|
1423
|
+
type: "fileDrop",
|
|
1424
|
+
visibleWhen: { equals: "image", target: "source.mode" },
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
title: "Source",
|
|
1428
|
+
},
|
|
1429
|
+
],
|
|
1430
|
+
title: "Controls",
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
expect(app.panels.controls?.sections).toHaveLength(1);
|
|
1436
|
+
expect(app.panels.controls?.sections[0]?.title).toBe("Source");
|
|
1437
|
+
expect(app.panels.controls?.sections[0]?.layout).toBeUndefined();
|
|
1438
|
+
expect(Object.keys(app.panels.controls?.sections[0]?.controls ?? {})).toEqual([
|
|
1439
|
+
"sourceMode",
|
|
1440
|
+
"sourceUpload",
|
|
1441
|
+
]);
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1400
1444
|
it("preserves controls panel action sections and render metadata", () => {
|
|
1401
1445
|
const app = defineToolcraft({
|
|
1402
1446
|
canvas: { enabled: true },
|