@pixel-point/toolcraft 0.0.4 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/prepare-pack.mjs +5 -0
- package/src/generate.mjs +13 -0
- package/src/generate.test.mjs +7 -1
- package/templates/runtime/contracts/component-contracts.test.ts +96 -3
- package/templates/runtime/contracts/component-contracts.ts +75 -3
- package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/react/canvas-shell.test.tsx +7 -7
- package/templates/runtime/react/controls-panel.test.tsx +455 -1
- package/templates/runtime/react/controls-panel.tsx +515 -30
- package/templates/runtime/react/settings-transfer.test.ts +3 -3
- package/templates/runtime/react/timeline-panel.test.tsx +69 -0
- package/templates/runtime/react/timeline-panel.tsx +98 -10
- package/templates/runtime/react/toolbar-panel.test.tsx +6 -6
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +2 -2
- package/templates/runtime/schema/define-toolcraft.test.ts +78 -1
- package/templates/runtime/schema/define-toolcraft.ts +145 -6
- package/templates/runtime/schema/runtime-targets.ts +1 -0
- package/templates/runtime/schema/types.ts +46 -1
- package/templates/runtime/state/canvas-zoom.ts +1 -1
- package/templates/runtime/state/create-template-state.test.ts +6 -6
- package/templates/runtime/state/reducer.test.ts +139 -8
- package/templates/runtime/state/reducer.ts +88 -22
- package/templates/runtime/state/types.ts +3 -0
- package/templates/starter/AGENTS.md +8 -8
- package/templates/starter/docs/toolcraft/README.md +4 -3
- package/templates/starter/docs/toolcraft/acceptance-testing.md +6 -2
- package/templates/starter/docs/toolcraft/assembly-workflow.md +8 -6
- package/templates/starter/docs/toolcraft/component-rules.md +17 -1
- package/templates/starter/docs/toolcraft/custom-controls.md +2 -2
- package/templates/starter/docs/toolcraft/performance.md +8 -7
- package/templates/starter/docs/toolcraft/renderer-technique.md +1 -1
- package/templates/starter/docs/toolcraft/schema-reference.md +15 -4
- package/templates/starter/docs/toolcraft/workflow.md +5 -8
- package/templates/starter/gitignore +36 -0
- package/templates/starter/package.json +1 -1
- package/templates/starter/src/app/starter-acceptance.test.ts +55 -0
- package/templates/starter/src/app/starter-acceptance.ts +67 -1
- package/templates/starter/src/app/starter-performance.test.ts +1 -1
- package/templates/ui/components/controls/collection-actions/collection-actions-control.tsx +60 -0
- package/templates/ui/components/controls/collection-actions/index.ts +4 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +101 -18
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1 -6
- package/templates/ui/components/controls/index.ts +8 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +4 -1
- package/templates/ui/components/controls/slider/slider-value.ts +48 -5
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +6 -1
- package/templates/ui/index.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { DiamondIcon } from "@phosphor-icons/react";
|
|
4
|
+
import { ArrowCounterClockwiseIcon, DiamondIcon } from "@phosphor-icons/react";
|
|
5
5
|
import {
|
|
6
6
|
Actions,
|
|
7
7
|
AnchorGrid,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ChannelMixer,
|
|
10
10
|
Checkbox,
|
|
11
11
|
CodeTextarea,
|
|
12
|
+
CollectionActions,
|
|
12
13
|
Color,
|
|
13
14
|
ColorOpacity,
|
|
14
15
|
ControlInlineGroup,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
type ColorOpacityValue,
|
|
43
44
|
type CurveInterpolation,
|
|
44
45
|
type FontPickerValue,
|
|
46
|
+
type FileDropPreview,
|
|
45
47
|
type GradientStop,
|
|
46
48
|
type GradientType,
|
|
47
49
|
type ImagePickerItem,
|
|
@@ -135,6 +137,7 @@ const canvasAspectRatioOptions = [
|
|
|
135
137
|
|
|
136
138
|
const sectionedCompoundControlTypes = new Set([
|
|
137
139
|
"channelMixer",
|
|
140
|
+
"collectionActions",
|
|
138
141
|
"fontPicker",
|
|
139
142
|
"gradient",
|
|
140
143
|
"palette",
|
|
@@ -439,6 +442,8 @@ function formatControlValueLabel(
|
|
|
439
442
|
|
|
440
443
|
return `${colorOpacityValue.hex} ${colorOpacityValue.opacity}%`;
|
|
441
444
|
}
|
|
445
|
+
case "collectionActions":
|
|
446
|
+
return `${asCollectionItems(value, control.defaultValue).length} items`;
|
|
442
447
|
case "fontPicker":
|
|
443
448
|
return asFontPickerValue(value).fontId;
|
|
444
449
|
case "gradient":
|
|
@@ -493,6 +498,10 @@ function formatControlValueLabel(
|
|
|
493
498
|
}
|
|
494
499
|
|
|
495
500
|
function asColorValue(value: unknown): { hex: string } {
|
|
501
|
+
if (typeof value === "string") {
|
|
502
|
+
return { hex: value };
|
|
503
|
+
}
|
|
504
|
+
|
|
496
505
|
if (isRecord(value)) {
|
|
497
506
|
return { hex: asString(value.hex, "#C1FF00") };
|
|
498
507
|
}
|
|
@@ -501,6 +510,10 @@ function asColorValue(value: unknown): { hex: string } {
|
|
|
501
510
|
}
|
|
502
511
|
|
|
503
512
|
function asColorOpacityValue(value: unknown): ColorOpacityValue {
|
|
513
|
+
if (typeof value === "string") {
|
|
514
|
+
return { hex: value, opacity: 100 };
|
|
515
|
+
}
|
|
516
|
+
|
|
504
517
|
if (isRecord(value)) {
|
|
505
518
|
return {
|
|
506
519
|
hex: asString(value.hex, "#C1FF00"),
|
|
@@ -511,6 +524,84 @@ function asColorOpacityValue(value: unknown): ColorOpacityValue {
|
|
|
511
524
|
return { hex: "#C1FF00", opacity: 100 };
|
|
512
525
|
}
|
|
513
526
|
|
|
527
|
+
function asCollectionItems(value: unknown, fallback: unknown): unknown[] {
|
|
528
|
+
if (Array.isArray(value)) {
|
|
529
|
+
return [...value];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function getCollectionMinItems(control: ToolcraftControlSchema): number {
|
|
536
|
+
return Math.max(0, Math.floor(asNumber(control.minItems, 0)));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function getCollectionHardMaxItems(control: ToolcraftControlSchema): number | null {
|
|
540
|
+
if (typeof control.hardMaxItems !== "number" || !Number.isFinite(control.hardMaxItems)) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return Math.max(0, Math.floor(control.hardMaxItems));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getCollectionItemType(control: ToolcraftControlSchema): string {
|
|
548
|
+
return control.itemControl?.type ?? "color";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function getCollectionItemBaseLabel(control: ToolcraftControlSchema): string {
|
|
552
|
+
if (control.itemLabel) {
|
|
553
|
+
return control.itemLabel;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const label = control.itemControl?.label;
|
|
557
|
+
|
|
558
|
+
return typeof label === "string" ? label : "Item";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function getCollectionItemName(
|
|
562
|
+
control: ToolcraftControlSchema,
|
|
563
|
+
index: number,
|
|
564
|
+
): string {
|
|
565
|
+
const label = control.itemControl?.label;
|
|
566
|
+
|
|
567
|
+
if (label === false) {
|
|
568
|
+
return "";
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return `${getCollectionItemBaseLabel(control)} ${index + 1}`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function getCollectionItemDefaultValue(control: ToolcraftControlSchema): unknown {
|
|
575
|
+
if ("itemDefaultValue" in control) {
|
|
576
|
+
return control.itemDefaultValue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (typeof control.itemControl?.defaultValue !== "undefined") {
|
|
580
|
+
return control.itemControl.defaultValue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
switch (getCollectionItemType(control)) {
|
|
584
|
+
case "color":
|
|
585
|
+
return { hex: "#C1FF00" };
|
|
586
|
+
case "colorOpacity":
|
|
587
|
+
return { hex: "#C1FF00", opacity: 100 };
|
|
588
|
+
case "checkbox":
|
|
589
|
+
case "switch":
|
|
590
|
+
return false;
|
|
591
|
+
case "rangeInput":
|
|
592
|
+
return { end: "100%", start: "0%" };
|
|
593
|
+
case "select":
|
|
594
|
+
case "segmented":
|
|
595
|
+
return control.itemControl?.options?.[0]?.value ?? "";
|
|
596
|
+
case "slider":
|
|
597
|
+
return control.itemControl?.min ?? 0;
|
|
598
|
+
case "text":
|
|
599
|
+
return "";
|
|
600
|
+
default:
|
|
601
|
+
return "";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
514
605
|
function asGradientType(value: unknown): GradientType {
|
|
515
606
|
return value === "linear" ||
|
|
516
607
|
value === "radial" ||
|
|
@@ -1691,12 +1782,94 @@ export function ControlsPanel({
|
|
|
1691
1782
|
);
|
|
1692
1783
|
}
|
|
1693
1784
|
|
|
1694
|
-
function
|
|
1695
|
-
|
|
1696
|
-
|
|
1785
|
+
function normalizeControlHelpContext(value: string): string {
|
|
1786
|
+
return value
|
|
1787
|
+
.toLowerCase()
|
|
1788
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
1789
|
+
.trim();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function isColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1793
|
+
return /\b(colou?rs?|palette|palettes)\b/i.test(sectionTitle ?? "");
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function isSequentialColorLabel(label: string): boolean {
|
|
1797
|
+
return /^colou?r\s+\d+$/i.test(label.trim());
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function isSimplePaletteDistributionLabel(label: string): boolean {
|
|
1801
|
+
return /^(spread|mix|distribution)$/i.test(label.trim());
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function isGenericControlHelpDescription(description: string): boolean {
|
|
1805
|
+
return /^(adjusts?|changes?|chooses?|controls?|defines?|selects?|sets?|updates?)\b/i.test(
|
|
1806
|
+
description.trim(),
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function shouldSuppressObviousControlHelp({
|
|
1811
|
+
control,
|
|
1812
|
+
description,
|
|
1813
|
+
label,
|
|
1814
|
+
sectionTitle,
|
|
1815
|
+
}: {
|
|
1816
|
+
control: ToolcraftControlSchema;
|
|
1817
|
+
description: string;
|
|
1818
|
+
label: string;
|
|
1819
|
+
sectionTitle: string | undefined;
|
|
1820
|
+
}): boolean {
|
|
1821
|
+
const isColorControl = control.type === "color" || control.type === "colorOpacity";
|
|
1822
|
+
|
|
1823
|
+
if (isColorSectionTitle(sectionTitle)) {
|
|
1824
|
+
if (isColorControl && isSequentialColorLabel(label)) {
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (
|
|
1829
|
+
isSimplePaletteDistributionLabel(label) &&
|
|
1830
|
+
isGenericControlHelpDescription(description)
|
|
1831
|
+
) {
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
const normalizedDescription = normalizeControlHelpContext(description);
|
|
1837
|
+
const normalizedLabel = normalizeControlHelpContext(label);
|
|
1838
|
+
|
|
1839
|
+
return (
|
|
1840
|
+
Boolean(normalizedLabel) &&
|
|
1841
|
+
isGenericControlHelpDescription(description) &&
|
|
1842
|
+
normalizedDescription === normalizedLabel
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function getControlHelpText({
|
|
1847
|
+
control,
|
|
1848
|
+
label,
|
|
1849
|
+
sectionTitle,
|
|
1850
|
+
}: {
|
|
1851
|
+
control: ToolcraftControlSchema;
|
|
1852
|
+
label: string;
|
|
1853
|
+
sectionTitle: string | undefined;
|
|
1854
|
+
}): string | null {
|
|
1697
1855
|
const description = control.description?.trim();
|
|
1698
1856
|
|
|
1699
|
-
|
|
1857
|
+
if (!description) {
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (
|
|
1862
|
+
shouldSuppressObviousControlHelp({
|
|
1863
|
+
control,
|
|
1864
|
+
description,
|
|
1865
|
+
label,
|
|
1866
|
+
sectionTitle,
|
|
1867
|
+
})
|
|
1868
|
+
) {
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
return description;
|
|
1700
1873
|
}
|
|
1701
1874
|
|
|
1702
1875
|
function withControlLabelHelp({
|
|
@@ -1704,13 +1877,15 @@ export function ControlsPanel({
|
|
|
1704
1877
|
control,
|
|
1705
1878
|
label,
|
|
1706
1879
|
providerKey,
|
|
1880
|
+
sectionTitle,
|
|
1707
1881
|
}: {
|
|
1708
1882
|
children: React.ReactNode;
|
|
1709
1883
|
control: ToolcraftControlSchema;
|
|
1710
1884
|
label: string;
|
|
1711
1885
|
providerKey: string;
|
|
1886
|
+
sectionTitle: string | undefined;
|
|
1712
1887
|
}): React.ReactNode {
|
|
1713
|
-
const help = getControlHelpText(control);
|
|
1888
|
+
const help = getControlHelpText({ control, label, sectionTitle });
|
|
1714
1889
|
|
|
1715
1890
|
if (!help) {
|
|
1716
1891
|
return children;
|
|
@@ -1759,6 +1934,42 @@ export function ControlsPanel({
|
|
|
1759
1934
|
return getKeyframeLabelAction(control, name, getControlValue(control));
|
|
1760
1935
|
}
|
|
1761
1936
|
|
|
1937
|
+
function getSectionResetAction({
|
|
1938
|
+
sectionTitle,
|
|
1939
|
+
targets,
|
|
1940
|
+
}: {
|
|
1941
|
+
sectionTitle: string;
|
|
1942
|
+
targets: readonly string[];
|
|
1943
|
+
}): React.ReactNode {
|
|
1944
|
+
const label = `Reset ${sectionTitle} section`;
|
|
1945
|
+
|
|
1946
|
+
return (
|
|
1947
|
+
<Tooltip>
|
|
1948
|
+
<TooltipTrigger
|
|
1949
|
+
render={
|
|
1950
|
+
<Button
|
|
1951
|
+
aria-label={label}
|
|
1952
|
+
data-control-section-reset-button=""
|
|
1953
|
+
onClick={() => {
|
|
1954
|
+
dispatchCommand({
|
|
1955
|
+
label,
|
|
1956
|
+
targets: Array.from(new Set(targets)),
|
|
1957
|
+
type: "controls.resetTargets",
|
|
1958
|
+
});
|
|
1959
|
+
}}
|
|
1960
|
+
size="icon-sm"
|
|
1961
|
+
type="button"
|
|
1962
|
+
variant="ghost"
|
|
1963
|
+
/>
|
|
1964
|
+
}
|
|
1965
|
+
>
|
|
1966
|
+
<ArrowCounterClockwiseIcon />
|
|
1967
|
+
</TooltipTrigger>
|
|
1968
|
+
<TooltipContent side="top">{label}</TooltipContent>
|
|
1969
|
+
</Tooltip>
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1762
1973
|
function renderColorGroup(
|
|
1763
1974
|
entries: readonly ControlEntry[],
|
|
1764
1975
|
headerKeyframeTarget: string | null,
|
|
@@ -1832,6 +2043,240 @@ export function ControlsPanel({
|
|
|
1832
2043
|
);
|
|
1833
2044
|
}
|
|
1834
2045
|
|
|
2046
|
+
function renderCollectionActionsControl({
|
|
2047
|
+
control,
|
|
2048
|
+
name,
|
|
2049
|
+
value,
|
|
2050
|
+
}: {
|
|
2051
|
+
control: ToolcraftControlSchema;
|
|
2052
|
+
name: string;
|
|
2053
|
+
value: unknown;
|
|
2054
|
+
}): React.JSX.Element {
|
|
2055
|
+
const items = asCollectionItems(value, control.defaultValue);
|
|
2056
|
+
const minItems = getCollectionMinItems(control);
|
|
2057
|
+
const hardMaxItems = getCollectionHardMaxItems(control);
|
|
2058
|
+
const canAdd = hardMaxItems === null || items.length < hardMaxItems;
|
|
2059
|
+
const canRemove = items.length > minItems;
|
|
2060
|
+
const itemType = getCollectionItemType(control);
|
|
2061
|
+
|
|
2062
|
+
function setItems(nextItems: unknown[], label: string): void {
|
|
2063
|
+
setControlValue(control.target, nextItems, label);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function addItem(): void {
|
|
2067
|
+
if (!canAdd) {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
setItems([...items, getCollectionItemDefaultValue(control)], control.addLabel ?? "Add item");
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function removeItem(): void {
|
|
2075
|
+
if (!canRemove) {
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
setItems(items.slice(0, -1), control.removeLabel ?? "Remove item");
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function updateItem(index: number, nextValue: unknown, meta?: ControlChangeMeta): void {
|
|
2083
|
+
const nextItems = items.map((item, itemIndex) =>
|
|
2084
|
+
itemIndex === index ? nextValue : item,
|
|
2085
|
+
);
|
|
2086
|
+
const itemName = getCollectionItemName(control, index) || name;
|
|
2087
|
+
|
|
2088
|
+
setControlValue(control.target, nextItems, itemName, meta);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function renderColorItems(): React.ReactNode {
|
|
2092
|
+
return (
|
|
2093
|
+
<div
|
|
2094
|
+
className="grid min-w-0 grid-cols-2 gap-x-2 gap-y-4"
|
|
2095
|
+
data-slot="collection-actions-items-grid"
|
|
2096
|
+
>
|
|
2097
|
+
{items.map((item, index) => {
|
|
2098
|
+
const itemName = getCollectionItemName(control, index);
|
|
2099
|
+
|
|
2100
|
+
return (
|
|
2101
|
+
<Color
|
|
2102
|
+
hex={asColorValue(item).hex}
|
|
2103
|
+
key={itemName || index}
|
|
2104
|
+
name={itemName}
|
|
2105
|
+
onValueChange={(nextValue: { hex: string }, meta?: ControlChangeMeta) =>
|
|
2106
|
+
updateItem(index, nextValue, meta)
|
|
2107
|
+
}
|
|
2108
|
+
showLabel={false}
|
|
2109
|
+
/>
|
|
2110
|
+
);
|
|
2111
|
+
})}
|
|
2112
|
+
</div>
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function renderColorOpacityItems(): React.ReactNode {
|
|
2117
|
+
return items.map((item, index) => {
|
|
2118
|
+
const colorOpacityValue = asColorOpacityValue(item);
|
|
2119
|
+
const itemName = getCollectionItemName(control, index);
|
|
2120
|
+
|
|
2121
|
+
return (
|
|
2122
|
+
<ColorOpacity
|
|
2123
|
+
hex={colorOpacityValue.hex}
|
|
2124
|
+
key={itemName || index}
|
|
2125
|
+
name={itemName}
|
|
2126
|
+
onValueChange={(nextValue, meta) => updateItem(index, nextValue, meta)}
|
|
2127
|
+
opacity={colorOpacityValue.opacity}
|
|
2128
|
+
showLabel={false}
|
|
2129
|
+
/>
|
|
2130
|
+
);
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function renderStackedItemControl(item: unknown, index: number): React.ReactNode {
|
|
2135
|
+
const itemControl = control.itemControl;
|
|
2136
|
+
const itemName = getCollectionItemName(control, index);
|
|
2137
|
+
const key = `${itemName || "item"}-${index}`;
|
|
2138
|
+
const update = (nextValue: unknown, meta?: ControlChangeMeta) => {
|
|
2139
|
+
updateItem(index, nextValue, meta);
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
switch (itemType) {
|
|
2143
|
+
case "checkbox":
|
|
2144
|
+
return (
|
|
2145
|
+
<Checkbox
|
|
2146
|
+
checked={asBoolean(item)}
|
|
2147
|
+
key={key}
|
|
2148
|
+
name={itemName}
|
|
2149
|
+
onCheckedChange={update}
|
|
2150
|
+
showLabel={itemControl?.label !== false}
|
|
2151
|
+
/>
|
|
2152
|
+
);
|
|
2153
|
+
case "rangeInput": {
|
|
2154
|
+
const rangeValue = asRangeInputValue(item);
|
|
2155
|
+
|
|
2156
|
+
return (
|
|
2157
|
+
<RangeInput
|
|
2158
|
+
defaultValue={asRangeInputValue(itemControl?.defaultValue)}
|
|
2159
|
+
end={rangeValue.end}
|
|
2160
|
+
key={key}
|
|
2161
|
+
name={itemName}
|
|
2162
|
+
onValueChange={update}
|
|
2163
|
+
start={rangeValue.start}
|
|
2164
|
+
/>
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
case "segmented":
|
|
2168
|
+
return (
|
|
2169
|
+
<Segmented
|
|
2170
|
+
key={key}
|
|
2171
|
+
name={itemName}
|
|
2172
|
+
onValueChange={update}
|
|
2173
|
+
options={itemControl?.options ?? []}
|
|
2174
|
+
value={asString(item, itemControl?.options?.[0]?.value ?? "")}
|
|
2175
|
+
variant={itemControl?.variant === "dots" ? "dots" : "default"}
|
|
2176
|
+
/>
|
|
2177
|
+
);
|
|
2178
|
+
case "select":
|
|
2179
|
+
return (
|
|
2180
|
+
<Select
|
|
2181
|
+
key={key}
|
|
2182
|
+
name={itemName}
|
|
2183
|
+
onValueChange={update}
|
|
2184
|
+
options={itemControl?.options ?? []}
|
|
2185
|
+
value={asString(item, itemControl?.options?.[0]?.value ?? "")}
|
|
2186
|
+
/>
|
|
2187
|
+
);
|
|
2188
|
+
case "slider":
|
|
2189
|
+
return (
|
|
2190
|
+
<Slider
|
|
2191
|
+
baseValue={asNumber(
|
|
2192
|
+
itemControl?.defaultValue,
|
|
2193
|
+
itemControl?.min ?? 0,
|
|
2194
|
+
)}
|
|
2195
|
+
key={key}
|
|
2196
|
+
markerCount={
|
|
2197
|
+
typeof itemControl?.markerCount === "number"
|
|
2198
|
+
? itemControl.markerCount
|
|
2199
|
+
: undefined
|
|
2200
|
+
}
|
|
2201
|
+
max={itemControl?.max ?? 100}
|
|
2202
|
+
min={itemControl?.min ?? 0}
|
|
2203
|
+
name={itemName}
|
|
2204
|
+
onValueChange={update}
|
|
2205
|
+
step={itemControl?.step ?? 1}
|
|
2206
|
+
unit={itemControl?.unit}
|
|
2207
|
+
value={asNumber(
|
|
2208
|
+
item,
|
|
2209
|
+
asNumber(itemControl?.defaultValue, itemControl?.min ?? 0),
|
|
2210
|
+
)}
|
|
2211
|
+
variant={itemControl?.variant === "discrete" ? "discrete" : "continuous"}
|
|
2212
|
+
/>
|
|
2213
|
+
);
|
|
2214
|
+
case "switch":
|
|
2215
|
+
return (
|
|
2216
|
+
<Switch
|
|
2217
|
+
checked={asBoolean(item)}
|
|
2218
|
+
key={key}
|
|
2219
|
+
name={itemName}
|
|
2220
|
+
onCheckedChange={update}
|
|
2221
|
+
showLabel={itemControl?.label !== false}
|
|
2222
|
+
/>
|
|
2223
|
+
);
|
|
2224
|
+
case "text":
|
|
2225
|
+
return (
|
|
2226
|
+
<TextInput
|
|
2227
|
+
commitOnBlur={itemControl?.commitMode === "setting"}
|
|
2228
|
+
defaultValue={asString(itemControl?.defaultValue, asString(item))}
|
|
2229
|
+
key={key}
|
|
2230
|
+
name={itemName}
|
|
2231
|
+
onValueChange={update}
|
|
2232
|
+
value={asString(item)}
|
|
2233
|
+
/>
|
|
2234
|
+
);
|
|
2235
|
+
default:
|
|
2236
|
+
return (
|
|
2237
|
+
<TextInput
|
|
2238
|
+
defaultValue={asString(itemControl?.defaultValue, asString(item))}
|
|
2239
|
+
key={key}
|
|
2240
|
+
name={itemName}
|
|
2241
|
+
onValueChange={update}
|
|
2242
|
+
value={asString(item)}
|
|
2243
|
+
/>
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function renderCollectionItems(): React.ReactNode {
|
|
2249
|
+
if (itemType === "color") {
|
|
2250
|
+
return renderColorItems();
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
if (itemType === "colorOpacity") {
|
|
2254
|
+
return renderColorOpacityItems();
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
return items.map(renderStackedItemControl);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
return (
|
|
2261
|
+
<div className="min-w-0 space-y-3" data-slot="collection-actions-control">
|
|
2262
|
+
<CollectionActions
|
|
2263
|
+
addLabel={control.addLabel ?? `Add ${getCollectionItemBaseLabel(control)}`}
|
|
2264
|
+
canAdd={canAdd}
|
|
2265
|
+
canRemove={canRemove}
|
|
2266
|
+
name={name}
|
|
2267
|
+
onAdd={addItem}
|
|
2268
|
+
onRemove={removeItem}
|
|
2269
|
+
removeLabel={
|
|
2270
|
+
control.removeLabel ?? `Remove ${getCollectionItemBaseLabel(control)}`
|
|
2271
|
+
}
|
|
2272
|
+
/>
|
|
2273
|
+
<div className="min-w-0 space-y-4" data-slot="collection-actions-items">
|
|
2274
|
+
{renderCollectionItems()}
|
|
2275
|
+
</div>
|
|
2276
|
+
</div>
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
1835
2280
|
const visibleSections = resolvedControlsPanel.sections
|
|
1836
2281
|
.map((section) => ({
|
|
1837
2282
|
entries: getVisibleSectionEntries(section),
|
|
@@ -1878,6 +2323,23 @@ export function ControlsPanel({
|
|
|
1878
2323
|
isSectionCollapsible && collapsedSectionByKey[sectionCollapseKey] === true;
|
|
1879
2324
|
const headerKeyframeEntry = getSectionHeaderKeyframeEntry(entries, section.title);
|
|
1880
2325
|
const headerKeyframeTarget = headerKeyframeEntry?.[1].target ?? null;
|
|
2326
|
+
const headerKeyframeAction = headerKeyframeEntry
|
|
2327
|
+
? getSectionHeaderKeyframeAction(headerKeyframeEntry)
|
|
2328
|
+
: null;
|
|
2329
|
+
const sectionResetAction = isSectionCollapsible
|
|
2330
|
+
? getSectionResetAction({
|
|
2331
|
+
sectionTitle:
|
|
2332
|
+
typeof renderedSectionTitle === "string" ? renderedSectionTitle : "section",
|
|
2333
|
+
targets: entries.map(([, control]) => control.target),
|
|
2334
|
+
})
|
|
2335
|
+
: null;
|
|
2336
|
+
const sectionHeaderAction =
|
|
2337
|
+
headerKeyframeAction || sectionResetAction ? (
|
|
2338
|
+
<>
|
|
2339
|
+
{headerKeyframeAction}
|
|
2340
|
+
{sectionResetAction}
|
|
2341
|
+
</>
|
|
2342
|
+
) : undefined;
|
|
1881
2343
|
const inlineLayoutGroupByControlId = getInlineLayoutGroupByControlId({
|
|
1882
2344
|
controlsById: visibleControls,
|
|
1883
2345
|
layoutGroups: section.layoutGroups,
|
|
@@ -1885,11 +2347,7 @@ export function ControlsPanel({
|
|
|
1885
2347
|
|
|
1886
2348
|
return (
|
|
1887
2349
|
<PanelSection
|
|
1888
|
-
action={
|
|
1889
|
-
headerKeyframeEntry
|
|
1890
|
-
? getSectionHeaderKeyframeAction(headerKeyframeEntry)
|
|
1891
|
-
: undefined
|
|
1892
|
-
}
|
|
2350
|
+
action={sectionHeaderAction}
|
|
1893
2351
|
actionGroup={section.actionGroup}
|
|
1894
2352
|
allowCompoundDividers={entries.length > 1}
|
|
1895
2353
|
collapsed={isSectionCollapsed}
|
|
@@ -2105,6 +2563,9 @@ export function ControlsPanel({
|
|
|
2105
2563
|
});
|
|
2106
2564
|
}
|
|
2107
2565
|
|
|
2566
|
+
case "collectionActions":
|
|
2567
|
+
return renderCollectionActionsControl({ control, name, value });
|
|
2568
|
+
|
|
2108
2569
|
case "curves": {
|
|
2109
2570
|
const curvesName = control.label === false ? "Curves" : name;
|
|
2110
2571
|
|
|
@@ -2129,13 +2590,38 @@ export function ControlsPanel({
|
|
|
2129
2590
|
}
|
|
2130
2591
|
|
|
2131
2592
|
case "fileDrop": {
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2593
|
+
const previewMediaAssets = state.schema.panels.layers ? [] : state.mediaAssets;
|
|
2594
|
+
const previewMediaAsset = previewMediaAssets[0];
|
|
2595
|
+
const previews = previewMediaAssets.map((asset): FileDropPreview => ({
|
|
2596
|
+
alt: asset.fileName,
|
|
2597
|
+
id: asset.id,
|
|
2598
|
+
size: asset.size,
|
|
2599
|
+
src: asset.dataUrl,
|
|
2600
|
+
}));
|
|
2601
|
+
const importFile = (file: File, replaceExisting: boolean): void => {
|
|
2602
|
+
void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
|
|
2603
|
+
if (!importedImage) {
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
dispatchCommand({
|
|
2608
|
+
asset: {
|
|
2609
|
+
dataUrl: importedImage.dataUrl,
|
|
2610
|
+
fileName: file.name,
|
|
2611
|
+
mimeType: file.type || "image/*",
|
|
2612
|
+
position: { x: 0, y: 0 },
|
|
2613
|
+
size: importedImage.size,
|
|
2614
|
+
},
|
|
2615
|
+
replaceExisting,
|
|
2616
|
+
type: "media.import",
|
|
2617
|
+
});
|
|
2618
|
+
});
|
|
2619
|
+
};
|
|
2135
2620
|
|
|
2136
2621
|
return (
|
|
2137
2622
|
<FileDrop
|
|
2138
2623
|
accept={control.accept ?? "PNG, JPEG, GIF, SVG, WebP"}
|
|
2624
|
+
multiple={control.multiple}
|
|
2139
2625
|
key={id}
|
|
2140
2626
|
onClear={
|
|
2141
2627
|
previewMediaAsset
|
|
@@ -2147,33 +2633,31 @@ export function ControlsPanel({
|
|
|
2147
2633
|
}
|
|
2148
2634
|
: undefined
|
|
2149
2635
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
size: importedImage.size,
|
|
2163
|
-
},
|
|
2164
|
-
type: "media.import",
|
|
2165
|
-
});
|
|
2636
|
+
onFilesSelect={(files) => {
|
|
2637
|
+
files.forEach((file) => importFile(file, false));
|
|
2638
|
+
}}
|
|
2639
|
+
onFileSelect={(file) => importFile(file, true)}
|
|
2640
|
+
onPreviewRemove={(item) => {
|
|
2641
|
+
if (!item.id) {
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
dispatchCommand({
|
|
2646
|
+
mediaId: item.id,
|
|
2647
|
+
type: "media.delete",
|
|
2166
2648
|
});
|
|
2167
2649
|
}}
|
|
2168
2650
|
preview={
|
|
2169
2651
|
previewMediaAsset
|
|
2170
2652
|
? {
|
|
2653
|
+
id: previewMediaAsset.id,
|
|
2171
2654
|
alt: previewMediaAsset.fileName,
|
|
2172
2655
|
size: previewMediaAsset.size,
|
|
2173
2656
|
src: previewMediaAsset.dataUrl,
|
|
2174
2657
|
}
|
|
2175
2658
|
: undefined
|
|
2176
2659
|
}
|
|
2660
|
+
previews={previews}
|
|
2177
2661
|
/>
|
|
2178
2662
|
);
|
|
2179
2663
|
}
|
|
@@ -2514,6 +2998,7 @@ export function ControlsPanel({
|
|
|
2514
2998
|
control,
|
|
2515
2999
|
label: name,
|
|
2516
3000
|
providerKey: id,
|
|
3001
|
+
sectionTitle: section.title,
|
|
2517
3002
|
}),
|
|
2518
3003
|
control,
|
|
2519
3004
|
}),
|
|
@@ -131,10 +131,10 @@ describe("settings transfer", () => {
|
|
|
131
131
|
expect(state.values["generation.prompt"]).toBe("Imported prompt");
|
|
132
132
|
expect(state.values["unknown.target"]).toBeUndefined();
|
|
133
133
|
expect(state.values["canvas.aspectRatio"]).toEqual({
|
|
134
|
-
height:
|
|
134
|
+
height: 9,
|
|
135
135
|
mode: "custom",
|
|
136
|
-
value: "
|
|
137
|
-
width:
|
|
136
|
+
value: "16:9",
|
|
137
|
+
width: 16,
|
|
138
138
|
});
|
|
139
139
|
expect(state.canvas.size).toEqual({ height: 900, unit: "px", width: 1600 });
|
|
140
140
|
expect(state.timeline).toMatchObject({
|