@pixel-point/toolcraft 0.0.6 → 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/src/generate.test.mjs +1 -1
- package/templates/runtime/contracts/component-contracts.test.ts +39 -3
- package/templates/runtime/contracts/component-contracts.ts +54 -3
- package/templates/runtime/contracts/decision-contracts.test.ts +3 -2
- package/templates/runtime/contracts/decision-contracts.ts +1 -1
- package/templates/runtime/react/controls-panel.test.tsx +186 -1
- package/templates/runtime/react/controls-panel.tsx +382 -6
- package/templates/runtime/schema/define-toolcraft.test.ts +1 -0
- package/templates/runtime/schema/define-toolcraft.ts +7 -1
- package/templates/runtime/schema/types.ts +23 -0
- package/templates/runtime/state/reducer.test.ts +53 -0
- package/templates/runtime/state/reducer.ts +47 -0
- package/templates/runtime/state/types.ts +2 -0
- package/templates/starter/AGENTS.md +7 -7
- 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 +5 -5
- package/templates/starter/docs/toolcraft/component-rules.md +5 -1
- package/templates/starter/docs/toolcraft/custom-controls.md +2 -2
- package/templates/starter/docs/toolcraft/performance.md +4 -8
- package/templates/starter/docs/toolcraft/schema-reference.md +6 -1
- package/templates/starter/docs/toolcraft/workflow.md +5 -8
- package/templates/starter/package.json +1 -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/font-picker/font-picker-control.tsx +1 -6
- package/templates/ui/components/controls/index.ts +8 -0
- 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,
|
|
@@ -136,6 +137,7 @@ const canvasAspectRatioOptions = [
|
|
|
136
137
|
|
|
137
138
|
const sectionedCompoundControlTypes = new Set([
|
|
138
139
|
"channelMixer",
|
|
140
|
+
"collectionActions",
|
|
139
141
|
"fontPicker",
|
|
140
142
|
"gradient",
|
|
141
143
|
"palette",
|
|
@@ -440,6 +442,8 @@ function formatControlValueLabel(
|
|
|
440
442
|
|
|
441
443
|
return `${colorOpacityValue.hex} ${colorOpacityValue.opacity}%`;
|
|
442
444
|
}
|
|
445
|
+
case "collectionActions":
|
|
446
|
+
return `${asCollectionItems(value, control.defaultValue).length} items`;
|
|
443
447
|
case "fontPicker":
|
|
444
448
|
return asFontPickerValue(value).fontId;
|
|
445
449
|
case "gradient":
|
|
@@ -494,6 +498,10 @@ function formatControlValueLabel(
|
|
|
494
498
|
}
|
|
495
499
|
|
|
496
500
|
function asColorValue(value: unknown): { hex: string } {
|
|
501
|
+
if (typeof value === "string") {
|
|
502
|
+
return { hex: value };
|
|
503
|
+
}
|
|
504
|
+
|
|
497
505
|
if (isRecord(value)) {
|
|
498
506
|
return { hex: asString(value.hex, "#C1FF00") };
|
|
499
507
|
}
|
|
@@ -502,6 +510,10 @@ function asColorValue(value: unknown): { hex: string } {
|
|
|
502
510
|
}
|
|
503
511
|
|
|
504
512
|
function asColorOpacityValue(value: unknown): ColorOpacityValue {
|
|
513
|
+
if (typeof value === "string") {
|
|
514
|
+
return { hex: value, opacity: 100 };
|
|
515
|
+
}
|
|
516
|
+
|
|
505
517
|
if (isRecord(value)) {
|
|
506
518
|
return {
|
|
507
519
|
hex: asString(value.hex, "#C1FF00"),
|
|
@@ -512,6 +524,84 @@ function asColorOpacityValue(value: unknown): ColorOpacityValue {
|
|
|
512
524
|
return { hex: "#C1FF00", opacity: 100 };
|
|
513
525
|
}
|
|
514
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
|
+
|
|
515
605
|
function asGradientType(value: unknown): GradientType {
|
|
516
606
|
return value === "linear" ||
|
|
517
607
|
value === "radial" ||
|
|
@@ -1844,6 +1934,42 @@ export function ControlsPanel({
|
|
|
1844
1934
|
return getKeyframeLabelAction(control, name, getControlValue(control));
|
|
1845
1935
|
}
|
|
1846
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
|
+
|
|
1847
1973
|
function renderColorGroup(
|
|
1848
1974
|
entries: readonly ControlEntry[],
|
|
1849
1975
|
headerKeyframeTarget: string | null,
|
|
@@ -1917,6 +2043,240 @@ export function ControlsPanel({
|
|
|
1917
2043
|
);
|
|
1918
2044
|
}
|
|
1919
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
|
+
|
|
1920
2280
|
const visibleSections = resolvedControlsPanel.sections
|
|
1921
2281
|
.map((section) => ({
|
|
1922
2282
|
entries: getVisibleSectionEntries(section),
|
|
@@ -1963,6 +2323,23 @@ export function ControlsPanel({
|
|
|
1963
2323
|
isSectionCollapsible && collapsedSectionByKey[sectionCollapseKey] === true;
|
|
1964
2324
|
const headerKeyframeEntry = getSectionHeaderKeyframeEntry(entries, section.title);
|
|
1965
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;
|
|
1966
2343
|
const inlineLayoutGroupByControlId = getInlineLayoutGroupByControlId({
|
|
1967
2344
|
controlsById: visibleControls,
|
|
1968
2345
|
layoutGroups: section.layoutGroups,
|
|
@@ -1970,11 +2347,7 @@ export function ControlsPanel({
|
|
|
1970
2347
|
|
|
1971
2348
|
return (
|
|
1972
2349
|
<PanelSection
|
|
1973
|
-
action={
|
|
1974
|
-
headerKeyframeEntry
|
|
1975
|
-
? getSectionHeaderKeyframeAction(headerKeyframeEntry)
|
|
1976
|
-
: undefined
|
|
1977
|
-
}
|
|
2350
|
+
action={sectionHeaderAction}
|
|
1978
2351
|
actionGroup={section.actionGroup}
|
|
1979
2352
|
allowCompoundDividers={entries.length > 1}
|
|
1980
2353
|
collapsed={isSectionCollapsed}
|
|
@@ -2190,6 +2563,9 @@ export function ControlsPanel({
|
|
|
2190
2563
|
});
|
|
2191
2564
|
}
|
|
2192
2565
|
|
|
2566
|
+
case "collectionActions":
|
|
2567
|
+
return renderCollectionActionsControl({ control, name, value });
|
|
2568
|
+
|
|
2193
2569
|
case "curves": {
|
|
2194
2570
|
const curvesName = control.label === false ? "Curves" : name;
|
|
2195
2571
|
|
|
@@ -60,6 +60,7 @@ const runtimeSetupSectionTitle = "Setup";
|
|
|
60
60
|
const settingsTransferHeavyControlTypes = new Set([
|
|
61
61
|
"channelMixer",
|
|
62
62
|
"code",
|
|
63
|
+
"collectionActions",
|
|
63
64
|
"colorOpacity",
|
|
64
65
|
"curves",
|
|
65
66
|
"fileDrop",
|
|
@@ -338,7 +339,12 @@ function createToolcraftAssembly({
|
|
|
338
339
|
const controlsPanel = panels.controls
|
|
339
340
|
? createPanelAssemblyContract({
|
|
340
341
|
capabilities: ["controls.panel", "controls.defaults"],
|
|
341
|
-
commands: [
|
|
342
|
+
commands: [
|
|
343
|
+
"controls.apply",
|
|
344
|
+
"controls.reset",
|
|
345
|
+
"controls.resetTargets",
|
|
346
|
+
"controls.setValue",
|
|
347
|
+
],
|
|
342
348
|
contract: TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel,
|
|
343
349
|
enabled: true,
|
|
344
350
|
})
|
|
@@ -95,6 +95,7 @@ export type ToolcraftAssemblyCommand =
|
|
|
95
95
|
| "canvas.zoomReset"
|
|
96
96
|
| "controls.apply"
|
|
97
97
|
| "controls.reset"
|
|
98
|
+
| "controls.resetTargets"
|
|
98
99
|
| "controls.setValue"
|
|
99
100
|
| "history.redo"
|
|
100
101
|
| "history.undo"
|
|
@@ -292,6 +293,20 @@ export type ToolcraftColorOpacityValueSchema = {
|
|
|
292
293
|
opacity?: number;
|
|
293
294
|
};
|
|
294
295
|
|
|
296
|
+
export type ToolcraftCollectionItemControlSchema = {
|
|
297
|
+
commitMode?: "content" | "setting";
|
|
298
|
+
defaultValue?: unknown;
|
|
299
|
+
label?: boolean | string;
|
|
300
|
+
markerCount?: number;
|
|
301
|
+
max?: number;
|
|
302
|
+
min?: number;
|
|
303
|
+
options?: readonly { label: string; value: string }[];
|
|
304
|
+
step?: number;
|
|
305
|
+
type: string;
|
|
306
|
+
unit?: string;
|
|
307
|
+
variant?: string;
|
|
308
|
+
};
|
|
309
|
+
|
|
295
310
|
export type ToolcraftFontPickerValueSchema = {
|
|
296
311
|
color?: string;
|
|
297
312
|
fontId: string;
|
|
@@ -308,23 +323,31 @@ export type ToolcraftCurveInterpolation = "monotone" | "smooth";
|
|
|
308
323
|
export type ToolcraftControlSchema = {
|
|
309
324
|
accept?: string;
|
|
310
325
|
actions?: readonly (ToolcraftActionSchema | string)[];
|
|
326
|
+
addLabel?: string;
|
|
311
327
|
commitMode?: "content" | "setting";
|
|
312
328
|
defaultValue?: unknown;
|
|
313
329
|
description?: string;
|
|
314
330
|
disabled?: boolean;
|
|
315
331
|
disabledWhen?: ToolcraftControlDisabledConditionSchema;
|
|
332
|
+
hardMaxItems?: number;
|
|
316
333
|
interpolation?: ToolcraftCurveInterpolation;
|
|
317
334
|
items?: readonly ToolcraftImagePickerItemSchema[];
|
|
335
|
+
itemControl?: ToolcraftCollectionItemControlSchema;
|
|
336
|
+
itemDefaultValue?: unknown;
|
|
337
|
+
itemLabel?: string;
|
|
318
338
|
keyframeable?: boolean;
|
|
319
339
|
label?: boolean | string;
|
|
320
340
|
markerCount?: number;
|
|
321
341
|
max?: number;
|
|
322
342
|
min?: number;
|
|
343
|
+
minItems?: number;
|
|
323
344
|
multiple?: boolean;
|
|
324
345
|
orderRole?: ToolcraftControlOrderRole;
|
|
325
346
|
performanceReason?: string;
|
|
326
347
|
performanceRole?: ToolcraftControlPerformanceRole;
|
|
327
348
|
options?: readonly { label: string; value: string }[];
|
|
349
|
+
recommendedMaxItems?: number;
|
|
350
|
+
removeLabel?: string;
|
|
328
351
|
step?: number;
|
|
329
352
|
target: string;
|
|
330
353
|
type: string;
|
|
@@ -45,6 +45,59 @@ describe("toolcraftReducer", () => {
|
|
|
45
45
|
expect(state.history.undo.at(-1)?.label).toBe("Reset controls");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it("resets selected control targets to defaults and records one history patch", () => {
|
|
49
|
+
const app = defineToolcraft({
|
|
50
|
+
canvas: { enabled: false },
|
|
51
|
+
panels: {
|
|
52
|
+
controls: {
|
|
53
|
+
sections: [
|
|
54
|
+
{
|
|
55
|
+
controls: {
|
|
56
|
+
contrast: {
|
|
57
|
+
defaultValue: 22,
|
|
58
|
+
target: "style.contrast",
|
|
59
|
+
type: "slider",
|
|
60
|
+
},
|
|
61
|
+
opacity: {
|
|
62
|
+
defaultValue: 75,
|
|
63
|
+
target: "selectedLayer.opacity",
|
|
64
|
+
type: "slider",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
title: "Tone",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
title: "Controls",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const changedOpacity = toolcraftReducer(createToolcraftState(app), {
|
|
75
|
+
target: "selectedLayer.opacity",
|
|
76
|
+
type: "controls.setValue",
|
|
77
|
+
value: 12,
|
|
78
|
+
});
|
|
79
|
+
const changedBoth = toolcraftReducer(changedOpacity, {
|
|
80
|
+
target: "style.contrast",
|
|
81
|
+
type: "controls.setValue",
|
|
82
|
+
value: 9,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const state = toolcraftReducer(changedBoth, {
|
|
86
|
+
label: "Reset Tone section",
|
|
87
|
+
targets: ["selectedLayer.opacity"],
|
|
88
|
+
type: "controls.resetTargets",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(state.values["selectedLayer.opacity"]).toBe(75);
|
|
92
|
+
expect(state.values["style.contrast"]).toBe(9);
|
|
93
|
+
expect(state.history.undo).toHaveLength(3);
|
|
94
|
+
expect(state.history.undo.at(-1)).toMatchObject({
|
|
95
|
+
after: { "selectedLayer.opacity": 75 },
|
|
96
|
+
before: { "selectedLayer.opacity": 12 },
|
|
97
|
+
label: "Reset Tone section",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
48
101
|
it("updates canvas size and records history", () => {
|
|
49
102
|
const size = { width: 1200, height: 900, unit: "px" } as const;
|
|
50
103
|
|
|
@@ -212,6 +212,17 @@ function getResetCanvasSize(
|
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
function canvasSizesEqual(
|
|
216
|
+
first: ToolcraftState["canvas"]["size"],
|
|
217
|
+
second: ToolcraftState["canvas"]["size"],
|
|
218
|
+
): boolean {
|
|
219
|
+
return (
|
|
220
|
+
first.height === second.height &&
|
|
221
|
+
first.unit === second.unit &&
|
|
222
|
+
first.width === second.width
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
215
226
|
function clampTimelineDuration(value: number): number {
|
|
216
227
|
if (!Number.isFinite(value)) {
|
|
217
228
|
return minTimelineDurationSeconds;
|
|
@@ -712,6 +723,42 @@ export function toolcraftReducer(
|
|
|
712
723
|
);
|
|
713
724
|
}
|
|
714
725
|
|
|
726
|
+
case "controls.resetTargets": {
|
|
727
|
+
const targetSet = new Set(command.targets);
|
|
728
|
+
const before: Record<string, unknown> = {};
|
|
729
|
+
const after: Record<string, unknown> = {};
|
|
730
|
+
|
|
731
|
+
for (const target of targetSet) {
|
|
732
|
+
if (!(target in state.defaults) || Object.is(state.values[target], state.defaults[target])) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
before[target] = state.values[target];
|
|
737
|
+
after[target] = state.defaults[target];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const resetCanvasSize = getResetCanvasSize(state);
|
|
741
|
+
const shouldResetCanvasSize =
|
|
742
|
+
resetCanvasSize !== null &&
|
|
743
|
+
(targetSet.has(canvasSizeWidthTarget) || targetSet.has(canvasSizeHeightTarget)) &&
|
|
744
|
+
!canvasSizesEqual(state.canvas.size, resetCanvasSize);
|
|
745
|
+
|
|
746
|
+
if (shouldResetCanvasSize) {
|
|
747
|
+
before["canvas.size"] = state.canvas.size;
|
|
748
|
+
after["canvas.size"] = resetCanvasSize;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (Object.keys(after).length === 0) {
|
|
752
|
+
return state;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return commitStatePatch(state, {
|
|
756
|
+
after,
|
|
757
|
+
before,
|
|
758
|
+
label: command.label ?? "Reset section",
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
715
762
|
case "layers.add": {
|
|
716
763
|
const layer = createLayer(state, command.layer);
|
|
717
764
|
const insertIndex = clampInsertIndex(state.layers.length, command.insertIndex);
|
|
@@ -11,6 +11,7 @@ export type ToolcraftCommand =
|
|
|
11
11
|
}
|
|
12
12
|
| { type: "controls.apply" }
|
|
13
13
|
| { type: "controls.reset" }
|
|
14
|
+
| { label?: string; targets: string[]; type: "controls.resetTargets" }
|
|
14
15
|
| { insertIndex?: number; layer?: ToolcraftLayerDraft; type: "layers.add" }
|
|
15
16
|
| { layerId: string; type: "layers.delete" }
|
|
16
17
|
| { layerIds: string[]; parentGroupId: string | null; type: "layers.moveToGroup" }
|
|
@@ -82,6 +83,7 @@ export const toolcraftRuntimeCommandTypes = [
|
|
|
82
83
|
"controls.setValue",
|
|
83
84
|
"controls.apply",
|
|
84
85
|
"controls.reset",
|
|
86
|
+
"controls.resetTargets",
|
|
85
87
|
"layers.add",
|
|
86
88
|
"layers.delete",
|
|
87
89
|
"layers.moveToGroup",
|