@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
|
@@ -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
|
|
|
@@ -70,7 +71,11 @@ import type {
|
|
|
70
71
|
ToolcraftPanelState,
|
|
71
72
|
ToolcraftState,
|
|
72
73
|
} from "../state/types";
|
|
73
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
isToolcraftImageFile,
|
|
76
|
+
readImportedFile,
|
|
77
|
+
readImportedImageFile,
|
|
78
|
+
} from "./media-file";
|
|
74
79
|
import { PanelContainer } from "./panel-host";
|
|
75
80
|
import type { PanelPlacement, PanelStateChange } from "./panel-host-types";
|
|
76
81
|
import type { ToolcraftControlRendererMap } from "./control-renderers";
|
|
@@ -417,6 +422,17 @@ function asVectorPadVariant(value: string | undefined): VectorPadVariant {
|
|
|
417
422
|
return "default";
|
|
418
423
|
}
|
|
419
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
|
+
|
|
420
436
|
function asCurveInterpolation(value: string | undefined): CurveInterpolation | undefined {
|
|
421
437
|
return value === "monotone" || value === "smooth" ? value : undefined;
|
|
422
438
|
}
|
|
@@ -585,6 +601,8 @@ function getCollectionItemDefaultValue(control: ToolcraftControlSchema): unknown
|
|
|
585
601
|
return { hex: "#C1FF00" };
|
|
586
602
|
case "colorOpacity":
|
|
587
603
|
return { hex: "#C1FF00", opacity: 100 };
|
|
604
|
+
case "fontPicker":
|
|
605
|
+
return asFontPickerValue(control.itemControl?.defaultValue);
|
|
588
606
|
case "checkbox":
|
|
589
607
|
case "switch":
|
|
590
608
|
return false;
|
|
@@ -1148,6 +1166,15 @@ function hasSliderControl(
|
|
|
1148
1166
|
);
|
|
1149
1167
|
}
|
|
1150
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
|
+
|
|
1151
1178
|
function hasSectionedCompoundControl(
|
|
1152
1179
|
controlsById: Record<string, ToolcraftControlSchema>,
|
|
1153
1180
|
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
@@ -1208,6 +1235,22 @@ function isToggleParameterLayoutGroup(
|
|
|
1208
1235
|
return booleanControlCount === 1 && parameterControlCount === 1;
|
|
1209
1236
|
}
|
|
1210
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
|
+
|
|
1211
1254
|
function getControlTargetEntityKey(control: ToolcraftControlSchema): string | null {
|
|
1212
1255
|
const parts = control.target.split(".");
|
|
1213
1256
|
|
|
@@ -1403,6 +1446,7 @@ function getInlineLayoutGroupByControlId({
|
|
|
1403
1446
|
if (
|
|
1404
1447
|
hasColorOpacityControl(controlsById, layoutGroup) ||
|
|
1405
1448
|
hasSliderControl(controlsById, layoutGroup) ||
|
|
1449
|
+
hasSegmentedControl(controlsById, layoutGroup) ||
|
|
1406
1450
|
hasSectionedCompoundControl(controlsById, layoutGroup) ||
|
|
1407
1451
|
hasUnsafeInlineSwitchLabels(controlsById, layoutGroup)
|
|
1408
1452
|
) {
|
|
@@ -1790,7 +1834,9 @@ export function ControlsPanel({
|
|
|
1790
1834
|
}
|
|
1791
1835
|
|
|
1792
1836
|
function isColorSectionTitle(sectionTitle: string | undefined): boolean {
|
|
1793
|
-
return /\b(colou?rs?|palette|palettes)\b/i.test(
|
|
1837
|
+
return /\b(colou?rs?|palette|palettes|shades?|accents?)\b/i.test(
|
|
1838
|
+
sectionTitle ?? "",
|
|
1839
|
+
);
|
|
1794
1840
|
}
|
|
1795
1841
|
|
|
1796
1842
|
function isSequentialColorLabel(label: string): boolean {
|
|
@@ -1885,6 +1931,10 @@ export function ControlsPanel({
|
|
|
1885
1931
|
providerKey: string;
|
|
1886
1932
|
sectionTitle: string | undefined;
|
|
1887
1933
|
}): React.ReactNode {
|
|
1934
|
+
if (control.label === false) {
|
|
1935
|
+
return children;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1888
1938
|
const help = getControlHelpText({ control, label, sectionTitle });
|
|
1889
1939
|
|
|
1890
1940
|
if (!help) {
|
|
@@ -2221,6 +2271,16 @@ export function ControlsPanel({
|
|
|
2221
2271
|
showLabel={itemControl?.label !== false}
|
|
2222
2272
|
/>
|
|
2223
2273
|
);
|
|
2274
|
+
case "fontPicker":
|
|
2275
|
+
return (
|
|
2276
|
+
<FontPicker
|
|
2277
|
+
defaultValue={asFontPickerValue(itemControl?.defaultValue)}
|
|
2278
|
+
key={key}
|
|
2279
|
+
name={itemName || "Font"}
|
|
2280
|
+
onValueChange={update}
|
|
2281
|
+
value={asFontPickerValue(item)}
|
|
2282
|
+
/>
|
|
2283
|
+
);
|
|
2224
2284
|
case "text":
|
|
2225
2285
|
return (
|
|
2226
2286
|
<TextInput
|
|
@@ -2386,13 +2446,23 @@ export function ControlsPanel({
|
|
|
2386
2446
|
}
|
|
2387
2447
|
|
|
2388
2448
|
const [id, rawControl] = group.entry;
|
|
2389
|
-
const
|
|
2449
|
+
const inlineLayoutGroup = inlineLayoutGroupByControlId.get(id);
|
|
2450
|
+
const isInInlineLayoutGroup = Boolean(inlineLayoutGroup);
|
|
2390
2451
|
const disabled = isControlDisabled(rawControl);
|
|
2391
|
-
const
|
|
2452
|
+
const resolvedControl =
|
|
2392
2453
|
disabled === Boolean(rawControl.disabled)
|
|
2393
2454
|
? rawControl
|
|
2394
2455
|
: { ...rawControl, disabled };
|
|
2395
|
-
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);
|
|
2396
2466
|
const value = getControlValue(control);
|
|
2397
2467
|
const usesHeaderKeyframeAction = control.target === headerKeyframeTarget;
|
|
2398
2468
|
const commitWithLabel =
|
|
@@ -2504,6 +2574,7 @@ export function ControlsPanel({
|
|
|
2504
2574
|
key={id}
|
|
2505
2575
|
name={name}
|
|
2506
2576
|
onValueChange={commit}
|
|
2577
|
+
showLabel={control.label !== false}
|
|
2507
2578
|
value={asString(value)}
|
|
2508
2579
|
/>
|
|
2509
2580
|
),
|
|
@@ -2590,15 +2661,51 @@ export function ControlsPanel({
|
|
|
2590
2661
|
}
|
|
2591
2662
|
|
|
2592
2663
|
case "fileDrop": {
|
|
2593
|
-
const
|
|
2664
|
+
const assetKind = control.assetKind === "file" ? "file" : "image";
|
|
2665
|
+
const previewMediaAssets = state.schema.panels.layers
|
|
2666
|
+
? []
|
|
2667
|
+
: state.mediaAssets.filter(
|
|
2668
|
+
(asset) =>
|
|
2669
|
+
asset.sourceTarget === undefined ||
|
|
2670
|
+
asset.sourceTarget === control.target,
|
|
2671
|
+
);
|
|
2594
2672
|
const previewMediaAsset = previewMediaAssets[0];
|
|
2595
2673
|
const previews = previewMediaAssets.map((asset): FileDropPreview => ({
|
|
2596
2674
|
alt: asset.fileName,
|
|
2675
|
+
assetKind: asset.assetKind ?? "image",
|
|
2676
|
+
fileName: asset.fileName,
|
|
2597
2677
|
id: asset.id,
|
|
2598
2678
|
size: asset.size,
|
|
2599
2679
|
src: asset.dataUrl,
|
|
2600
2680
|
}));
|
|
2681
|
+
const previewMediaIds = previewMediaAssets.map((asset) => asset.id);
|
|
2601
2682
|
const importFile = (file: File, replaceExisting: boolean): void => {
|
|
2683
|
+
if (assetKind === "file") {
|
|
2684
|
+
void readImportedFile(file).then((importedFile) => {
|
|
2685
|
+
if (!importedFile) {
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
dispatchCommand({
|
|
2690
|
+
asset: {
|
|
2691
|
+
assetKind: "file",
|
|
2692
|
+
dataUrl: importedFile.dataUrl,
|
|
2693
|
+
fileName: file.name,
|
|
2694
|
+
mimeType: file.type || "application/octet-stream",
|
|
2695
|
+
position: { x: 0, y: 0 },
|
|
2696
|
+
sourceTarget: control.target,
|
|
2697
|
+
},
|
|
2698
|
+
replaceExisting,
|
|
2699
|
+
type: "media.import",
|
|
2700
|
+
});
|
|
2701
|
+
});
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
if (!isToolcraftImageFile(file)) {
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2602
2709
|
void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
|
|
2603
2710
|
if (!importedImage) {
|
|
2604
2711
|
return;
|
|
@@ -2606,11 +2713,13 @@ export function ControlsPanel({
|
|
|
2606
2713
|
|
|
2607
2714
|
dispatchCommand({
|
|
2608
2715
|
asset: {
|
|
2716
|
+
assetKind: "image",
|
|
2609
2717
|
dataUrl: importedImage.dataUrl,
|
|
2610
2718
|
fileName: file.name,
|
|
2611
2719
|
mimeType: file.type || "image/*",
|
|
2612
2720
|
position: { x: 0, y: 0 },
|
|
2613
2721
|
size: importedImage.size,
|
|
2722
|
+
sourceTarget: control.target,
|
|
2614
2723
|
},
|
|
2615
2724
|
replaceExisting,
|
|
2616
2725
|
type: "media.import",
|
|
@@ -2620,7 +2729,11 @@ export function ControlsPanel({
|
|
|
2620
2729
|
|
|
2621
2730
|
return (
|
|
2622
2731
|
<FileDrop
|
|
2623
|
-
accept={
|
|
2732
|
+
accept={
|
|
2733
|
+
control.accept ??
|
|
2734
|
+
(assetKind === "file" ? "" : "PNG, JPEG, GIF, SVG, WebP")
|
|
2735
|
+
}
|
|
2736
|
+
assetKind={assetKind}
|
|
2624
2737
|
multiple={control.multiple}
|
|
2625
2738
|
key={id}
|
|
2626
2739
|
onClear={
|
|
@@ -2647,6 +2760,32 @@ export function ControlsPanel({
|
|
|
2647
2760
|
type: "media.delete",
|
|
2648
2761
|
});
|
|
2649
2762
|
}}
|
|
2763
|
+
onPreviewReorder={(orderedPreviews) => {
|
|
2764
|
+
const orderedPreviewIds = orderedPreviews.flatMap((item) =>
|
|
2765
|
+
item.id ? [item.id] : [],
|
|
2766
|
+
);
|
|
2767
|
+
|
|
2768
|
+
if (orderedPreviewIds.length !== previewMediaIds.length) {
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const previewIdSet = new Set(previewMediaIds);
|
|
2773
|
+
let orderedPreviewIndex = 0;
|
|
2774
|
+
const mediaIds = state.mediaAssets.map((asset) => {
|
|
2775
|
+
if (!previewIdSet.has(asset.id)) {
|
|
2776
|
+
return asset.id;
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
const nextId = orderedPreviewIds[orderedPreviewIndex];
|
|
2780
|
+
orderedPreviewIndex += 1;
|
|
2781
|
+
return nextId ?? asset.id;
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
dispatchCommand({
|
|
2785
|
+
mediaIds,
|
|
2786
|
+
type: "media.reorder",
|
|
2787
|
+
});
|
|
2788
|
+
}}
|
|
2650
2789
|
preview={
|
|
2651
2790
|
previewMediaAsset
|
|
2652
2791
|
? {
|
|
@@ -2777,6 +2916,7 @@ export function ControlsPanel({
|
|
|
2777
2916
|
key={id}
|
|
2778
2917
|
name={name}
|
|
2779
2918
|
onValueChange={commit}
|
|
2919
|
+
showLabel={control.label !== false}
|
|
2780
2920
|
start={rangeValue.start}
|
|
2781
2921
|
/>
|
|
2782
2922
|
),
|
|
@@ -2843,6 +2983,7 @@ export function ControlsPanel({
|
|
|
2843
2983
|
name={name}
|
|
2844
2984
|
onValueChange={commit}
|
|
2845
2985
|
options={control.options ?? []}
|
|
2986
|
+
showLabel={control.label !== false}
|
|
2846
2987
|
value={asString(value, control.options?.[0]?.value ?? "")}
|
|
2847
2988
|
/>
|
|
2848
2989
|
);
|
|
@@ -2922,6 +3063,7 @@ export function ControlsPanel({
|
|
|
2922
3063
|
key={id}
|
|
2923
3064
|
name={name}
|
|
2924
3065
|
onValueChange={commit}
|
|
3066
|
+
showLabel={control.label !== false}
|
|
2925
3067
|
value={asString(value)}
|
|
2926
3068
|
/>
|
|
2927
3069
|
),
|
|
@@ -2934,6 +3076,7 @@ export function ControlsPanel({
|
|
|
2934
3076
|
|
|
2935
3077
|
case "vector": {
|
|
2936
3078
|
const vectorValue = asVectorValue(value);
|
|
3079
|
+
const padVariant = asVectorPadVariant(control.variant);
|
|
2937
3080
|
|
|
2938
3081
|
return withKeyframeLabelAction({
|
|
2939
3082
|
children: (
|
|
@@ -2942,8 +3085,12 @@ export function ControlsPanel({
|
|
|
2942
3085
|
key={id}
|
|
2943
3086
|
name={name}
|
|
2944
3087
|
onValueChange={commit}
|
|
3088
|
+
padCoordinateMode={asVectorPadCoordinateMode(
|
|
3089
|
+
control.coordinateMode,
|
|
3090
|
+
padVariant,
|
|
3091
|
+
)}
|
|
2945
3092
|
padShape={vectorPadShape}
|
|
2946
|
-
padVariant={
|
|
3093
|
+
padVariant={padVariant}
|
|
2947
3094
|
x={vectorValue.x}
|
|
2948
3095
|
xLabel={control.xLabel}
|
|
2949
3096
|
y={vectorValue.y}
|
|
@@ -5,6 +5,10 @@ export type ToolcraftImportedImageFile = {
|
|
|
5
5
|
size: ToolcraftCanvasSize;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
export type ToolcraftImportedFile = {
|
|
9
|
+
dataUrl: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
8
12
|
function readFileDataUrl(file: File): Promise<string | null> {
|
|
9
13
|
return new Promise((resolve) => {
|
|
10
14
|
const reader = new FileReader();
|
|
@@ -65,6 +69,21 @@ function readImageDataUrlSize(dataUrl: string): Promise<ToolcraftCanvasSize | nu
|
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
export function isToolcraftImageFile(file: File): boolean {
|
|
73
|
+
return (
|
|
74
|
+
file.type.startsWith("image/") ||
|
|
75
|
+
/\.(avif|gif|heic|heif|jpe?g|png|svg|tiff?|webp)$/i.test(file.name)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function readImportedFile(
|
|
80
|
+
file: File,
|
|
81
|
+
): Promise<ToolcraftImportedFile | null> {
|
|
82
|
+
const dataUrl = await readFileDataUrl(file);
|
|
83
|
+
|
|
84
|
+
return dataUrl ? { dataUrl } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export async function readImportedImageFile(
|
|
69
88
|
file: File,
|
|
70
89
|
fallbackSize: ToolcraftCanvasSize,
|
|
@@ -539,6 +539,7 @@ describe("defineToolcraft", () => {
|
|
|
539
539
|
"history.undo",
|
|
540
540
|
"media.delete",
|
|
541
541
|
"media.import",
|
|
542
|
+
"media.reorder",
|
|
542
543
|
"layers.reorder",
|
|
543
544
|
"timeline.setCurrentTime",
|
|
544
545
|
"canvas.center",
|
|
@@ -836,7 +837,6 @@ describe("defineToolcraft", () => {
|
|
|
836
837
|
step: 0.25,
|
|
837
838
|
target: "canvas.renderScale",
|
|
838
839
|
type: "slider",
|
|
839
|
-
unit: "x",
|
|
840
840
|
variant: "discrete",
|
|
841
841
|
});
|
|
842
842
|
});
|
|
@@ -1396,6 +1396,51 @@ describe("defineToolcraft", () => {
|
|
|
1396
1396
|
]);
|
|
1397
1397
|
});
|
|
1398
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
|
+
|
|
1399
1444
|
it("preserves controls panel action sections and render metadata", () => {
|
|
1400
1445
|
const app = defineToolcraft({
|
|
1401
1446
|
canvas: { enabled: true },
|
|
@@ -332,7 +332,7 @@ function createToolcraftAssembly({
|
|
|
332
332
|
|
|
333
333
|
if (canvas.upload) {
|
|
334
334
|
capabilities.push("canvas.upload");
|
|
335
|
-
commands.push("media.delete", "media.import");
|
|
335
|
+
commands.push("media.delete", "media.import", "media.reorder");
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
@@ -478,7 +478,9 @@ function createToolcraftAssembly({
|
|
|
478
478
|
...(canvas.draggable
|
|
479
479
|
? (["canvas.panBy", "canvas.setOffset", "canvas.setViewport"] as const)
|
|
480
480
|
: []),
|
|
481
|
-
...(canvas.upload
|
|
481
|
+
...(canvas.upload
|
|
482
|
+
? (["media.delete", "media.import", "media.reorder"] as const)
|
|
483
|
+
: []),
|
|
482
484
|
])
|
|
483
485
|
: [],
|
|
484
486
|
enabled: canvas.enabled,
|
|
@@ -522,6 +524,10 @@ function getControlSectionLayout(
|
|
|
522
524
|
control: ToolcraftControlSchema,
|
|
523
525
|
entries: readonly [string, ToolcraftControlSchema][],
|
|
524
526
|
): "grouped" | "standalone" {
|
|
527
|
+
if (isControlGatedBySameSectionControl(control, entries)) {
|
|
528
|
+
return "grouped";
|
|
529
|
+
}
|
|
530
|
+
|
|
525
531
|
if (
|
|
526
532
|
(control.type === "color" || control.type === "colorOpacity") &&
|
|
527
533
|
entries.some(
|
|
@@ -537,6 +543,24 @@ function getControlSectionLayout(
|
|
|
537
543
|
return getControlDefaultSectionLayout(control);
|
|
538
544
|
}
|
|
539
545
|
|
|
546
|
+
function isControlGatedBySameSectionControl(
|
|
547
|
+
control: ToolcraftControlSchema,
|
|
548
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
549
|
+
): boolean {
|
|
550
|
+
const gateTargets = [control.visibleWhen?.target, control.disabledWhen?.target].filter(
|
|
551
|
+
(target): target is string => Boolean(target),
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
if (gateTargets.length === 0) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return entries.some(([, entryControl]) =>
|
|
559
|
+
gateTargets.includes(entryControl.target) &&
|
|
560
|
+
getControlDefaultSectionLayout(entryControl) === "grouped",
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
540
564
|
function createControlsRecord(
|
|
541
565
|
entries: readonly [string, ToolcraftControlSchema][],
|
|
542
566
|
): Record<string, ToolcraftControlSchema> {
|
|
@@ -1426,7 +1450,6 @@ function normalizePanels({
|
|
|
1426
1450
|
step: canvas.renderScale.step,
|
|
1427
1451
|
target: canvasRenderScaleTarget,
|
|
1428
1452
|
type: "slider",
|
|
1429
|
-
unit: "x",
|
|
1430
1453
|
variant: "discrete",
|
|
1431
1454
|
}
|
|
1432
1455
|
: undefined;
|
|
@@ -1458,6 +1481,7 @@ function normalizePanels({
|
|
|
1458
1481
|
const aspectRatioControl: ToolcraftControlSchema = {
|
|
1459
1482
|
defaultValue: getCanvasAspectRatioDefaultValue(canvas.size),
|
|
1460
1483
|
label: "Aspect ratio",
|
|
1484
|
+
orderRole: "input",
|
|
1461
1485
|
performanceReason: "Aspect ratio changes output dimensions and renderer workload.",
|
|
1462
1486
|
performanceRole: "workload",
|
|
1463
1487
|
target: canvasAspectRatioTarget,
|
|
@@ -1468,6 +1492,7 @@ function normalizePanels({
|
|
|
1468
1492
|
sizeControls.canvasWidth = {
|
|
1469
1493
|
defaultValue: canvas.size.width,
|
|
1470
1494
|
label: "Canvas width",
|
|
1495
|
+
orderRole: "input",
|
|
1471
1496
|
performanceReason: "Canvas width changes output dimensions and renderer workload.",
|
|
1472
1497
|
performanceRole: "workload",
|
|
1473
1498
|
target: canvasSizeControlTargets.width,
|
|
@@ -1480,6 +1505,7 @@ function normalizePanels({
|
|
|
1480
1505
|
sizeControls.canvasHeight = {
|
|
1481
1506
|
defaultValue: canvas.size.height,
|
|
1482
1507
|
label: "Canvas height",
|
|
1508
|
+
orderRole: "input",
|
|
1483
1509
|
performanceReason: "Canvas height changes output dimensions and renderer workload.",
|
|
1484
1510
|
performanceRole: "workload",
|
|
1485
1511
|
target: canvasSizeControlTargets.height,
|
|
@@ -109,6 +109,7 @@ export type ToolcraftAssemblyCommand =
|
|
|
109
109
|
| "layers.toggleVisibility"
|
|
110
110
|
| "media.delete"
|
|
111
111
|
| "media.import"
|
|
112
|
+
| "media.reorder"
|
|
112
113
|
| "panels.resetOffset"
|
|
113
114
|
| "panels.setOffset"
|
|
114
115
|
| "timeline.changeKeyframeEasing"
|
|
@@ -273,6 +274,8 @@ export type ToolcraftControlPerformanceRole =
|
|
|
273
274
|
| "responsiveness"
|
|
274
275
|
| "workload";
|
|
275
276
|
|
|
277
|
+
export type ToolcraftFileDropAssetKind = "file" | "image";
|
|
278
|
+
|
|
276
279
|
export type ToolcraftControlConditionSchema = {
|
|
277
280
|
equals?: unknown;
|
|
278
281
|
greaterThan?: number;
|
|
@@ -320,11 +323,15 @@ export type ToolcraftFontPickerValueSchema = {
|
|
|
320
323
|
|
|
321
324
|
export type ToolcraftCurveInterpolation = "monotone" | "smooth";
|
|
322
325
|
|
|
326
|
+
export type ToolcraftVectorCoordinateMode = "cartesian" | "screen";
|
|
327
|
+
|
|
323
328
|
export type ToolcraftControlSchema = {
|
|
324
329
|
accept?: string;
|
|
325
330
|
actions?: readonly (ToolcraftActionSchema | string)[];
|
|
331
|
+
assetKind?: ToolcraftFileDropAssetKind;
|
|
326
332
|
addLabel?: string;
|
|
327
333
|
commitMode?: "content" | "setting";
|
|
334
|
+
coordinateMode?: ToolcraftVectorCoordinateMode;
|
|
328
335
|
defaultValue?: unknown;
|
|
329
336
|
description?: string;
|
|
330
337
|
disabled?: boolean;
|