@pixel-point/toolcraft 0.0.2
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/LICENSE.md +98 -0
- package/README.md +41 -0
- package/bin/create-toolcraft-app.mjs +8 -0
- package/bin/toolcraft.mjs +8 -0
- package/package.json +24 -0
- package/scripts/prepare-pack.mjs +29 -0
- package/src/cli.mjs +392 -0
- package/src/cli.test.mjs +284 -0
- package/src/copy-recursive.mjs +86 -0
- package/src/generate.mjs +212 -0
- package/src/generate.test.mjs +322 -0
- package/src/import-map.mjs +14 -0
- package/src/package-json.mjs +80 -0
- package/src/package-json.test.mjs +67 -0
- package/src/rewrite-imports.mjs +85 -0
- package/src/rewrite-imports.test.mjs +58 -0
- package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
- package/templates/runtime/contracts/component-contracts.ts +1340 -0
- package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
- package/templates/runtime/contracts/decision-contracts.ts +283 -0
- package/templates/runtime/contracts/index.test.ts +14 -0
- package/templates/runtime/contracts/index.ts +3 -0
- package/templates/runtime/contracts/types.ts +56 -0
- package/templates/runtime/export/export.test.ts +203 -0
- package/templates/runtime/export/export.ts +132 -0
- package/templates/runtime/export/index.ts +1 -0
- package/templates/runtime/index.ts +14 -0
- package/templates/runtime/react/canvas-shell.test.tsx +424 -0
- package/templates/runtime/react/canvas-shell.tsx +408 -0
- package/templates/runtime/react/control-renderers.ts +31 -0
- package/templates/runtime/react/controls-panel.test.tsx +3736 -0
- package/templates/runtime/react/controls-panel.tsx +2327 -0
- package/templates/runtime/react/curve-geometry.test.ts +70 -0
- package/templates/runtime/react/index.ts +15 -0
- package/templates/runtime/react/layer-tree.ts +96 -0
- package/templates/runtime/react/layers-panel.test.tsx +487 -0
- package/templates/runtime/react/layers-panel.tsx +1348 -0
- package/templates/runtime/react/media-file.ts +82 -0
- package/templates/runtime/react/panel-host-config.ts +80 -0
- package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
- package/templates/runtime/react/panel-host-geometry.ts +109 -0
- package/templates/runtime/react/panel-host-types.ts +74 -0
- package/templates/runtime/react/panel-host.test.tsx +102 -0
- package/templates/runtime/react/panel-host.tsx +353 -0
- package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
- package/templates/runtime/react/settings-transfer.test.ts +150 -0
- package/templates/runtime/react/settings-transfer.ts +279 -0
- package/templates/runtime/react/storage-key-migration.ts +48 -0
- package/templates/runtime/react/theme-runtime.tsx +177 -0
- package/templates/runtime/react/timeline-panel.test.tsx +668 -0
- package/templates/runtime/react/timeline-panel.tsx +2953 -0
- package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
- package/templates/runtime/react/toolbar-panel.tsx +205 -0
- package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
- package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
- package/templates/runtime/react/toolcraft-app.tsx +81 -0
- package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
- package/templates/runtime/react/toolcraft-root.tsx +203 -0
- package/templates/runtime/react/use-toolcraft.ts +41 -0
- package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
- package/templates/runtime/schema/define-toolcraft.ts +1442 -0
- package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
- package/templates/runtime/schema/keyframe-capability.ts +51 -0
- package/templates/runtime/schema/runtime-targets.ts +40 -0
- package/templates/runtime/schema/types.ts +370 -0
- package/templates/runtime/state/canvas-zoom.ts +8 -0
- package/templates/runtime/state/create-template-state.test.ts +242 -0
- package/templates/runtime/state/create-template-state.ts +95 -0
- package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
- package/templates/runtime/state/keyframe-evaluation.ts +203 -0
- package/templates/runtime/state/persistence.test.ts +217 -0
- package/templates/runtime/state/persistence.ts +511 -0
- package/templates/runtime/state/reducer.test.ts +937 -0
- package/templates/runtime/state/reducer.ts +1212 -0
- package/templates/runtime/state/timeline-readiness.ts +43 -0
- package/templates/runtime/state/types.ts +242 -0
- package/templates/runtime/styles.css +125 -0
- package/templates/runtime/testing/performance.test.ts +1058 -0
- package/templates/runtime/testing/performance.ts +1078 -0
- package/templates/starter/AGENTS.md +186 -0
- package/templates/starter/LICENSE.md +98 -0
- package/templates/starter/NOTICE.md +8 -0
- package/templates/starter/docs/toolcraft/README.md +41 -0
- package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
- package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
- package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
- package/templates/starter/docs/toolcraft/component-rules.md +299 -0
- package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
- package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
- package/templates/starter/docs/toolcraft/performance.md +112 -0
- package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
- package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
- package/templates/starter/docs/toolcraft/workflow.md +87 -0
- package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
- package/templates/starter/e2e/app-controls.spec.ts +41 -0
- package/templates/starter/e2e/app-performance.spec.ts +326 -0
- package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
- package/templates/starter/e2e/performance-helpers.ts +612 -0
- package/templates/starter/e2e/product-observable-helpers.ts +170 -0
- package/templates/starter/index.html +12 -0
- package/templates/starter/package.json +52 -0
- package/templates/starter/playwright.config.ts +43 -0
- package/templates/starter/scripts/check-ai-skills.mjs +95 -0
- package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
- package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
- package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
- package/templates/starter/scripts/toolcraft-port.mjs +54 -0
- package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
- package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
- package/templates/starter/src/app/starter-acceptance.ts +2646 -0
- package/templates/starter/src/app/starter-performance.test.ts +1390 -0
- package/templates/starter/src/app/starter-performance.ts +12 -0
- package/templates/starter/src/app/starter-schema.test.ts +70 -0
- package/templates/starter/src/app/starter-schema.ts +15 -0
- package/templates/starter/src/main.tsx +18 -0
- package/templates/starter/src/router.tsx +16 -0
- package/templates/starter/src/routes/index.tsx +7 -0
- package/templates/starter/src/routes/root.tsx +19 -0
- package/templates/starter/src/styles.css +120 -0
- package/templates/starter/tsconfig.json +11 -0
- package/templates/starter/vite.config.ts +13 -0
- package/templates/ui/components/composites/accordion.tsx +73 -0
- package/templates/ui/components/composites/alert-dialog.tsx +190 -0
- package/templates/ui/components/composites/alert.tsx +74 -0
- package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
- package/templates/ui/components/composites/avatar.tsx +98 -0
- package/templates/ui/components/composites/badge.tsx +69 -0
- package/templates/ui/components/composites/breadcrumb.tsx +106 -0
- package/templates/ui/components/composites/card.tsx +91 -0
- package/templates/ui/components/composites/combobox.tsx +486 -0
- package/templates/ui/components/composites/command.tsx +296 -0
- package/templates/ui/components/composites/context-menu.tsx +247 -0
- package/templates/ui/components/composites/dialog.tsx +282 -0
- package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
- package/templates/ui/components/composites/empty.tsx +110 -0
- package/templates/ui/components/composites/hover-card.tsx +44 -0
- package/templates/ui/components/composites/index.ts +30 -0
- package/templates/ui/components/composites/menubar.tsx +214 -0
- package/templates/ui/components/composites/navigation-menu.tsx +167 -0
- package/templates/ui/components/composites/pagination.tsx +131 -0
- package/templates/ui/components/composites/progress.tsx +72 -0
- package/templates/ui/components/composites/radio-group.tsx +84 -0
- package/templates/ui/components/composites/resizable.tsx +42 -0
- package/templates/ui/components/composites/sheet.tsx +153 -0
- package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
- package/templates/ui/components/composites/sidebar.tsx +431 -0
- package/templates/ui/components/composites/sonner.tsx +35 -0
- package/templates/ui/components/composites/spinner.tsx +43 -0
- package/templates/ui/components/composites/table.tsx +108 -0
- package/templates/ui/components/composites/tabs.tsx +83 -0
- package/templates/ui/components/control-layout/index.tsx +437 -0
- package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
- package/templates/ui/components/controls/actions/index.ts +9 -0
- package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
- package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
- package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
- package/templates/ui/components/controls/boolean/index.ts +4 -0
- package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
- package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
- package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
- package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
- package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
- package/templates/ui/components/controls/code-textarea/index.ts +4 -0
- package/templates/ui/components/controls/color/color-control.tsx +571 -0
- package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
- package/templates/ui/components/controls/color/index.ts +41 -0
- package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
- package/templates/ui/components/controls/color/palette-control.tsx +535 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
- package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
- package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
- package/templates/ui/components/controls/control-types.ts +43 -0
- package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
- package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
- package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
- package/templates/ui/components/controls/curves/index.ts +6 -0
- package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
- package/templates/ui/components/controls/file-drop/index.ts +5 -0
- package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
- package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
- package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
- package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
- package/templates/ui/components/controls/font-picker/index.ts +24 -0
- package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
- package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
- package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
- package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
- package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
- package/templates/ui/components/controls/gradient/index.ts +4 -0
- package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
- package/templates/ui/components/controls/image-picker/index.ts +7 -0
- package/templates/ui/components/controls/index.ts +192 -0
- package/templates/ui/components/controls/range-input/index.ts +4 -0
- package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
- package/templates/ui/components/controls/range-slider/index.ts +4 -0
- package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
- package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
- package/templates/ui/components/controls/segmented/index.ts +8 -0
- package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
- package/templates/ui/components/controls/select/index.ts +4 -0
- package/templates/ui/components/controls/select/select-control.tsx +223 -0
- package/templates/ui/components/controls/slider/index.ts +4 -0
- package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
- package/templates/ui/components/controls/slider/slider-value.ts +56 -0
- package/templates/ui/components/controls/text-input/index.ts +4 -0
- package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
- package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
- package/templates/ui/components/controls/vector/index.ts +8 -0
- package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
- package/templates/ui/components/panel/index.ts +19 -0
- package/templates/ui/components/panel/panel-actions.tsx +165 -0
- package/templates/ui/components/panel/panel-header.tsx +61 -0
- package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
- package/templates/ui/components/panel/panel-section.tsx +168 -0
- package/templates/ui/components/panel/panel-surface.tsx +206 -0
- package/templates/ui/components/panel/panel.tsx +210 -0
- package/templates/ui/components/primitives/animated-loader.tsx +61 -0
- package/templates/ui/components/primitives/button-group.tsx +134 -0
- package/templates/ui/components/primitives/button.tsx +429 -0
- package/templates/ui/components/primitives/checkbox.tsx +62 -0
- package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
- package/templates/ui/components/primitives/field.tsx +225 -0
- package/templates/ui/components/primitives/index.ts +82 -0
- package/templates/ui/components/primitives/input-group.tsx +298 -0
- package/templates/ui/components/primitives/input.tsx +61 -0
- package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
- package/templates/ui/components/primitives/label.tsx +16 -0
- package/templates/ui/components/primitives/popover.tsx +126 -0
- package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
- package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
- package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
- package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
- package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
- package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
- package/templates/ui/components/primitives/select.tsx +408 -0
- package/templates/ui/components/primitives/selection-state.ts +31 -0
- package/templates/ui/components/primitives/separator.tsx +21 -0
- package/templates/ui/components/primitives/slider/index.ts +4 -0
- package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
- package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
- package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
- package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
- package/templates/ui/components/primitives/slider/slider.tsx +511 -0
- package/templates/ui/components/primitives/switch.tsx +35 -0
- package/templates/ui/components/primitives/textarea.tsx +49 -0
- package/templates/ui/components/primitives/toggle-group.tsx +114 -0
- package/templates/ui/components/primitives/toggle.tsx +46 -0
- package/templates/ui/components/primitives/tooltip.tsx +100 -0
- package/templates/ui/hooks/use-mobile.ts +21 -0
- package/templates/ui/index.ts +31 -0
- package/templates/ui/lib/control-outline.ts +3 -0
- package/templates/ui/lib/input-control-style.ts +131 -0
- package/templates/ui/lib/style-guide-color-utils.ts +111 -0
- package/templates/ui/lib/utils.ts +6 -0
- package/templates/ui/styles.css +291 -0
|
@@ -0,0 +1,2327 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { DiamondIcon } from "@phosphor-icons/react";
|
|
5
|
+
import {
|
|
6
|
+
Actions,
|
|
7
|
+
AnchorGrid,
|
|
8
|
+
Button,
|
|
9
|
+
ChannelMixer,
|
|
10
|
+
Checkbox,
|
|
11
|
+
CodeTextarea,
|
|
12
|
+
Color,
|
|
13
|
+
ColorOpacity,
|
|
14
|
+
ControlInlineGroup,
|
|
15
|
+
ControlFieldLabelActionProvider,
|
|
16
|
+
ControlFieldLabelHelpProvider,
|
|
17
|
+
Curves,
|
|
18
|
+
FileDrop,
|
|
19
|
+
FontPicker,
|
|
20
|
+
Gradient,
|
|
21
|
+
ImagePicker,
|
|
22
|
+
Palette,
|
|
23
|
+
Panel,
|
|
24
|
+
PanelActions,
|
|
25
|
+
PanelSection,
|
|
26
|
+
type PanelActionObjectOption,
|
|
27
|
+
RangeInput,
|
|
28
|
+
RangeSlider,
|
|
29
|
+
Segmented,
|
|
30
|
+
Select,
|
|
31
|
+
Slider,
|
|
32
|
+
Switch,
|
|
33
|
+
TextInput,
|
|
34
|
+
Tooltip,
|
|
35
|
+
TooltipContent,
|
|
36
|
+
TooltipTrigger,
|
|
37
|
+
Vector,
|
|
38
|
+
type ChannelMixerValues,
|
|
39
|
+
type ControlChangeMeta,
|
|
40
|
+
type ColorControlInput,
|
|
41
|
+
type ColorControlInputPair,
|
|
42
|
+
type ColorOpacityValue,
|
|
43
|
+
type CurveInterpolation,
|
|
44
|
+
type FontPickerValue,
|
|
45
|
+
type GradientStop,
|
|
46
|
+
type GradientType,
|
|
47
|
+
type ImagePickerItem,
|
|
48
|
+
type VectorPadVariant,
|
|
49
|
+
} from "@repo/ui";
|
|
50
|
+
|
|
51
|
+
import type {
|
|
52
|
+
ToolcraftActionCommand,
|
|
53
|
+
ToolcraftActionSchema,
|
|
54
|
+
ToolcraftControlConditionSchema,
|
|
55
|
+
ToolcraftControlLayoutGroupSchema,
|
|
56
|
+
ToolcraftControlSectionSchema,
|
|
57
|
+
ToolcraftControlSchema,
|
|
58
|
+
ResolvedToolcraftAppSchema,
|
|
59
|
+
} from "../schema/types";
|
|
60
|
+
import { getToolcraftControlKeyframeCapability } from "../schema/keyframe-capability";
|
|
61
|
+
import { getToolcraftCanvasSizeTargetDimension } from "../schema/runtime-targets";
|
|
62
|
+
import type {
|
|
63
|
+
ToolcraftCommand,
|
|
64
|
+
ToolcraftPanelState,
|
|
65
|
+
ToolcraftState,
|
|
66
|
+
} from "../state/types";
|
|
67
|
+
import { readImportedImageFile } from "./media-file";
|
|
68
|
+
import { PanelContainer } from "./panel-host";
|
|
69
|
+
import type { PanelPlacement, PanelStateChange } from "./panel-host-types";
|
|
70
|
+
import type { ToolcraftControlRendererMap } from "./control-renderers";
|
|
71
|
+
import {
|
|
72
|
+
downloadToolcraftSettings,
|
|
73
|
+
importToolcraftSettings,
|
|
74
|
+
} from "./settings-transfer";
|
|
75
|
+
import {
|
|
76
|
+
readToolcraftLocalStorageValue,
|
|
77
|
+
removeToolcraftLocalStorageValue,
|
|
78
|
+
} from "./storage-key-migration";
|
|
79
|
+
import { useToolcraft } from "./use-toolcraft";
|
|
80
|
+
|
|
81
|
+
export type ControlsPanelProps = {
|
|
82
|
+
className?: string;
|
|
83
|
+
controlRenderers?: ToolcraftControlRendererMap;
|
|
84
|
+
framed?: boolean;
|
|
85
|
+
onPanelAction?: ToolcraftPanelActionHandler;
|
|
86
|
+
onPanelStateChange?: PanelStateChange;
|
|
87
|
+
panelPlacement?: PanelPlacement;
|
|
88
|
+
panelState?: ToolcraftPanelState;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type ToolcraftPanelActionContext = {
|
|
92
|
+
action: ToolcraftActionSchema;
|
|
93
|
+
dispatch: React.Dispatch<ToolcraftCommand>;
|
|
94
|
+
reportProgress: (progress: number) => void;
|
|
95
|
+
state: ToolcraftState;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type ToolcraftPanelActionHandler = (
|
|
99
|
+
context: ToolcraftPanelActionContext,
|
|
100
|
+
) => PromiseLike<unknown> | void;
|
|
101
|
+
|
|
102
|
+
type AnyRecord = Record<string, unknown>;
|
|
103
|
+
type ControlEntry = [string, ToolcraftControlSchema];
|
|
104
|
+
type ControlRenderGroup =
|
|
105
|
+
| { entries: readonly ControlEntry[]; kind: "colorGroup" }
|
|
106
|
+
| { entry: ControlEntry; kind: "control" };
|
|
107
|
+
type RenderedControlRenderGroup = {
|
|
108
|
+
ids: readonly string[];
|
|
109
|
+
node: React.ReactNode;
|
|
110
|
+
};
|
|
111
|
+
type FooterActionProgressEntry = {
|
|
112
|
+
id: number;
|
|
113
|
+
progress: number | null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const hiddenDiscreteMarkerCount = 2;
|
|
117
|
+
const controlsPanelSectionCollapseStorageVersion = 1;
|
|
118
|
+
|
|
119
|
+
const sectionedCompoundControlTypes = new Set([
|
|
120
|
+
"channelMixer",
|
|
121
|
+
"fontPicker",
|
|
122
|
+
"gradient",
|
|
123
|
+
"palette",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const defaultGradientStops = [
|
|
127
|
+
{ color: "#FFFFFF", position: "0%" },
|
|
128
|
+
{ color: "#7CFF3A", position: "46%" },
|
|
129
|
+
{ color: "#111111", position: "100%" },
|
|
130
|
+
] as const satisfies readonly GradientStop[];
|
|
131
|
+
|
|
132
|
+
const defaultChannelMixerValues = {
|
|
133
|
+
B: { B: 100, G: 0, R: 0 },
|
|
134
|
+
G: { B: 0, G: 100, R: 0 },
|
|
135
|
+
R: { B: 0, G: 0, R: 100 },
|
|
136
|
+
} satisfies ChannelMixerValues;
|
|
137
|
+
|
|
138
|
+
function cn(...classNames: Array<string | false | null | undefined>): string {
|
|
139
|
+
return classNames.filter(Boolean).join(" ");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
143
|
+
return (
|
|
144
|
+
typeof value === "object" &&
|
|
145
|
+
value !== null &&
|
|
146
|
+
"then" in value &&
|
|
147
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function noopReportProgress(): void {}
|
|
152
|
+
|
|
153
|
+
function clampFooterActionProgress(progress: number): number {
|
|
154
|
+
if (!Number.isFinite(progress)) {
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Math.max(0, Math.min(1, progress));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function canCreateControlKeyframe(control: ToolcraftControlSchema): boolean {
|
|
162
|
+
return getToolcraftControlKeyframeCapability(control).capable;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function shouldRenderCompoundControlSectionDivider(
|
|
166
|
+
control: ToolcraftControlSchema,
|
|
167
|
+
): boolean {
|
|
168
|
+
if (control.type === "curves") {
|
|
169
|
+
return control.variant !== "single";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return sectionedCompoundControlTypes.has(control.type);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function withCompoundControlSectionDivider({
|
|
176
|
+
children,
|
|
177
|
+
control,
|
|
178
|
+
}: {
|
|
179
|
+
children: React.ReactNode;
|
|
180
|
+
control: ToolcraftControlSchema;
|
|
181
|
+
}): React.ReactNode {
|
|
182
|
+
if (!shouldRenderCompoundControlSectionDivider(control)) {
|
|
183
|
+
return children;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="contents" data-control-section-divider="compound">
|
|
188
|
+
{children}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isRecord(value: unknown): value is AnyRecord {
|
|
194
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function valuesEqual(first: unknown, second: unknown): boolean {
|
|
198
|
+
if (Object.is(first, second)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
(typeof first !== "object" || first === null) &&
|
|
204
|
+
(typeof second !== "object" || second === null)
|
|
205
|
+
) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
return JSON.stringify(first) === JSON.stringify(second);
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function valuesInclude(value: unknown, options: readonly unknown[]): boolean {
|
|
217
|
+
return options.some((option) => valuesEqual(value, option));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readComparableNumber(value: unknown): number | null {
|
|
221
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof value === "string") {
|
|
226
|
+
const numberValue = Number(value.trim());
|
|
227
|
+
|
|
228
|
+
return Number.isFinite(numberValue) ? numberValue : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function compareConditionNumbers(
|
|
235
|
+
value: unknown,
|
|
236
|
+
expected: number | undefined,
|
|
237
|
+
comparator: (value: number, expected: number) => boolean,
|
|
238
|
+
): boolean | null {
|
|
239
|
+
if (typeof expected !== "number" || !Number.isFinite(expected)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const numberValue = readComparableNumber(value);
|
|
244
|
+
|
|
245
|
+
return numberValue === null ? false : comparator(numberValue, expected);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function asNumber(value: unknown, fallback: number): number {
|
|
249
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function asString(value: unknown, fallback = ""): string {
|
|
253
|
+
if (typeof value === "string") {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function asBoolean(value: unknown, fallback = false): boolean {
|
|
261
|
+
return typeof value === "boolean" ? value : fallback;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getPanelActionButtonVariant(
|
|
265
|
+
variant: ToolcraftActionSchema["variant"],
|
|
266
|
+
): PanelActionObjectOption["variant"] {
|
|
267
|
+
switch (variant) {
|
|
268
|
+
case "destructive":
|
|
269
|
+
case "ghost":
|
|
270
|
+
case "link":
|
|
271
|
+
case "outline":
|
|
272
|
+
case "secondary":
|
|
273
|
+
return variant;
|
|
274
|
+
default:
|
|
275
|
+
return "default";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function asNumberArray(value: unknown, fallback: readonly number[]): readonly number[] {
|
|
280
|
+
return Array.isArray(value) && value.every((item) => typeof item === "number")
|
|
281
|
+
? value
|
|
282
|
+
: fallback;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function asRangeInputValue(value: unknown): { end: string; start: string } {
|
|
286
|
+
if (isRecord(value)) {
|
|
287
|
+
return {
|
|
288
|
+
end: asString(value.end, "100%"),
|
|
289
|
+
start: asString(value.start, "0%"),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { end: "100%", start: "0%" };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function asVectorValue(value: unknown): { x: string; y: string } {
|
|
297
|
+
if (isRecord(value)) {
|
|
298
|
+
return {
|
|
299
|
+
x: asString(value.x, "0.00"),
|
|
300
|
+
y: asString(value.y, "0.00"),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { x: "0.00", y: "0.00" };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function asVectorPadVariant(value: string | undefined): VectorPadVariant {
|
|
308
|
+
if (
|
|
309
|
+
value === "whiteBalance" ||
|
|
310
|
+
value === "colorBalance" ||
|
|
311
|
+
value === "chromaOffset" ||
|
|
312
|
+
value === "toneBias"
|
|
313
|
+
) {
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return "default";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function asCurveInterpolation(value: string | undefined): CurveInterpolation | undefined {
|
|
321
|
+
return value === "monotone" || value === "smooth" ? value : undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function formatControlValueLabel(
|
|
325
|
+
control: ToolcraftControlSchema,
|
|
326
|
+
value: unknown,
|
|
327
|
+
): string {
|
|
328
|
+
if (typeof control.valueLabel === "string") {
|
|
329
|
+
return control.valueLabel;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
switch (control.type) {
|
|
333
|
+
case "checkbox":
|
|
334
|
+
case "switch":
|
|
335
|
+
return asBoolean(value) ? "On" : "Off";
|
|
336
|
+
case "color":
|
|
337
|
+
return asColorValue(value).hex;
|
|
338
|
+
case "colorOpacity": {
|
|
339
|
+
const colorOpacityValue = asColorOpacityValue(value);
|
|
340
|
+
|
|
341
|
+
return `${colorOpacityValue.hex} ${colorOpacityValue.opacity}%`;
|
|
342
|
+
}
|
|
343
|
+
case "fontPicker":
|
|
344
|
+
return asFontPickerValue(value).fontId;
|
|
345
|
+
case "gradient":
|
|
346
|
+
return `${asGradientValue(value).stops.length} stops`;
|
|
347
|
+
case "imagePicker":
|
|
348
|
+
return (
|
|
349
|
+
control.items?.find((item) => item.value === value)?.alt ??
|
|
350
|
+
asString(value)
|
|
351
|
+
);
|
|
352
|
+
case "palette": {
|
|
353
|
+
if (isRecord(value)) {
|
|
354
|
+
const family = asString(value.family);
|
|
355
|
+
const shade = asString(value.shade);
|
|
356
|
+
|
|
357
|
+
return [family, shade].filter(Boolean).join(" ") || "Palette";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return "Palette";
|
|
361
|
+
}
|
|
362
|
+
case "rangeInput": {
|
|
363
|
+
const rangeValue = asRangeInputValue(value);
|
|
364
|
+
|
|
365
|
+
return `${rangeValue.start} – ${rangeValue.end}`;
|
|
366
|
+
}
|
|
367
|
+
case "rangeSlider": {
|
|
368
|
+
const rangeValue = asNumberArray(value, []);
|
|
369
|
+
|
|
370
|
+
return rangeValue.length > 0
|
|
371
|
+
? rangeValue.map((item) => `${item}${control.unit ?? ""}`).join(" – ")
|
|
372
|
+
: "Range";
|
|
373
|
+
}
|
|
374
|
+
case "select":
|
|
375
|
+
case "segmented":
|
|
376
|
+
return (
|
|
377
|
+
control.options?.find((option) => option.value === value)?.label ??
|
|
378
|
+
asString(value)
|
|
379
|
+
);
|
|
380
|
+
case "slider":
|
|
381
|
+
return `${asNumber(value, asNumber(control.defaultValue, control.min ?? 0))}${
|
|
382
|
+
control.unit ?? ""
|
|
383
|
+
}`;
|
|
384
|
+
case "vector": {
|
|
385
|
+
const vectorValue = asVectorValue(value);
|
|
386
|
+
|
|
387
|
+
return `${vectorValue.x}, ${vectorValue.y}`;
|
|
388
|
+
}
|
|
389
|
+
default:
|
|
390
|
+
return typeof value === "string" || typeof value === "number"
|
|
391
|
+
? String(value)
|
|
392
|
+
: control.type;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function asColorValue(value: unknown): { hex: string } {
|
|
397
|
+
if (isRecord(value)) {
|
|
398
|
+
return { hex: asString(value.hex, "#C1FF00") };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { hex: "#C1FF00" };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function asColorOpacityValue(value: unknown): ColorOpacityValue {
|
|
405
|
+
if (isRecord(value)) {
|
|
406
|
+
return {
|
|
407
|
+
hex: asString(value.hex, "#C1FF00"),
|
|
408
|
+
opacity: Math.min(100, Math.max(0, Math.round(asNumber(value.opacity, 100)))),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { hex: "#C1FF00", opacity: 100 };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function asGradientType(value: unknown): GradientType {
|
|
416
|
+
return value === "linear" ||
|
|
417
|
+
value === "radial" ||
|
|
418
|
+
value === "angular" ||
|
|
419
|
+
value === "diamond"
|
|
420
|
+
? value
|
|
421
|
+
: "linear";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function asGradientValue(value: unknown): {
|
|
425
|
+
angle: number;
|
|
426
|
+
gradientType: GradientType;
|
|
427
|
+
stops: readonly GradientStop[];
|
|
428
|
+
} {
|
|
429
|
+
if (isRecord(value)) {
|
|
430
|
+
return {
|
|
431
|
+
angle: asNumber(value.angle, 90),
|
|
432
|
+
gradientType: asGradientType(value.gradientType),
|
|
433
|
+
stops: Array.isArray(value.stops)
|
|
434
|
+
? (value.stops as readonly GradientStop[])
|
|
435
|
+
: defaultGradientStops,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
angle: 90,
|
|
441
|
+
gradientType: "linear",
|
|
442
|
+
stops: defaultGradientStops,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function asFontPickerValue(value: unknown): FontPickerValue {
|
|
447
|
+
if (typeof value === "string") {
|
|
448
|
+
return {
|
|
449
|
+
color: "#FFFFFF",
|
|
450
|
+
fontId: value,
|
|
451
|
+
fontSize: 16,
|
|
452
|
+
fontWeight: "400",
|
|
453
|
+
letterSpacing: "normal",
|
|
454
|
+
lineHeight: "normal",
|
|
455
|
+
opacity: 100,
|
|
456
|
+
textCase: "original",
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (isRecord(value)) {
|
|
461
|
+
return {
|
|
462
|
+
color: asString(value.color, "#FFFFFF"),
|
|
463
|
+
fontId: asString(value.fontId, "inter"),
|
|
464
|
+
fontSize: asNumber(value.fontSize, 16),
|
|
465
|
+
fontWeight: asString(value.fontWeight, "400"),
|
|
466
|
+
letterSpacing:
|
|
467
|
+
value.letterSpacing === "tighter" ||
|
|
468
|
+
value.letterSpacing === "tight" ||
|
|
469
|
+
value.letterSpacing === "normal" ||
|
|
470
|
+
value.letterSpacing === "wide" ||
|
|
471
|
+
value.letterSpacing === "wider" ||
|
|
472
|
+
value.letterSpacing === "widest"
|
|
473
|
+
? value.letterSpacing
|
|
474
|
+
: "normal",
|
|
475
|
+
lineHeight:
|
|
476
|
+
value.lineHeight === "none" ||
|
|
477
|
+
value.lineHeight === "tight" ||
|
|
478
|
+
value.lineHeight === "snug" ||
|
|
479
|
+
value.lineHeight === "normal" ||
|
|
480
|
+
value.lineHeight === "relaxed" ||
|
|
481
|
+
value.lineHeight === "loose"
|
|
482
|
+
? value.lineHeight
|
|
483
|
+
: "normal",
|
|
484
|
+
opacity: Math.min(100, Math.max(0, Math.round(asNumber(value.opacity, 100)))),
|
|
485
|
+
textCase:
|
|
486
|
+
value.textCase === "original" ||
|
|
487
|
+
value.textCase === "uppercase" ||
|
|
488
|
+
value.textCase === "lowercase" ||
|
|
489
|
+
value.textCase === "capitalize" ||
|
|
490
|
+
value.textCase === "titleCase"
|
|
491
|
+
? value.textCase
|
|
492
|
+
: "original",
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
color: "#FFFFFF",
|
|
498
|
+
fontId: "inter",
|
|
499
|
+
fontSize: 16,
|
|
500
|
+
fontWeight: "400",
|
|
501
|
+
letterSpacing: "normal",
|
|
502
|
+
lineHeight: "normal",
|
|
503
|
+
opacity: 100,
|
|
504
|
+
textCase: "original",
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function asActionSchemas(
|
|
509
|
+
actions: readonly (ToolcraftActionSchema | string)[] | undefined,
|
|
510
|
+
): readonly ToolcraftActionSchema[] {
|
|
511
|
+
return (actions ?? []).map((action) =>
|
|
512
|
+
typeof action === "string"
|
|
513
|
+
? {
|
|
514
|
+
label: action,
|
|
515
|
+
value: action,
|
|
516
|
+
}
|
|
517
|
+
: action,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function getActionCommand(action: ToolcraftActionSchema): ToolcraftActionCommand | null {
|
|
522
|
+
if (action.command) {
|
|
523
|
+
return action.command;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
switch (action.value.toLowerCase()) {
|
|
527
|
+
case "apply":
|
|
528
|
+
return "controls.apply";
|
|
529
|
+
case "reset":
|
|
530
|
+
return "controls.reset";
|
|
531
|
+
default:
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getActionLabel(action: ToolcraftActionSchema): string {
|
|
537
|
+
return action.label ?? action.value;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getControlName(id: string, label: boolean | string | undefined): string {
|
|
541
|
+
if (typeof label === "string") {
|
|
542
|
+
return label;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return id;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function ControlKeyframeButton({
|
|
549
|
+
active,
|
|
550
|
+
name,
|
|
551
|
+
onClick,
|
|
552
|
+
}: {
|
|
553
|
+
active: boolean;
|
|
554
|
+
name: string;
|
|
555
|
+
onClick: () => void;
|
|
556
|
+
}): React.JSX.Element {
|
|
557
|
+
const label = active ? `Disable ${name} keyframes` : `Add ${name} keyframe`;
|
|
558
|
+
|
|
559
|
+
return (
|
|
560
|
+
<Tooltip>
|
|
561
|
+
<TooltipTrigger
|
|
562
|
+
render={
|
|
563
|
+
<Button
|
|
564
|
+
aria-label={label}
|
|
565
|
+
aria-pressed={active}
|
|
566
|
+
className={cn(
|
|
567
|
+
"size-4 opacity-100 transition-opacity duration-150 ease-out hover:!bg-transparent active:!bg-transparent aria-pressed:!bg-transparent data-popup-open:!bg-transparent [&_svg:not([class*='size-'])]:!size-2.5 [&_svg:not([class*='size-'])]:!opacity-70 data-[icon-active=true]:[&_svg:not([class*='size-'])]:!opacity-100",
|
|
568
|
+
active &&
|
|
569
|
+
"!text-[color:var(--link)] aria-pressed:!text-[color:var(--link)] data-popup-open:!text-[color:var(--link)] [&_svg]:!text-[color:var(--link)] [&_svg]:!fill-[color:var(--link)]",
|
|
570
|
+
)}
|
|
571
|
+
data-icon-active={active}
|
|
572
|
+
onClick={(event) => {
|
|
573
|
+
event.stopPropagation();
|
|
574
|
+
onClick();
|
|
575
|
+
|
|
576
|
+
if (typeof event.currentTarget.blur === "function") {
|
|
577
|
+
event.currentTarget.blur();
|
|
578
|
+
}
|
|
579
|
+
}}
|
|
580
|
+
size="icon-sm"
|
|
581
|
+
style={active ? { color: "var(--link)" } : undefined}
|
|
582
|
+
type="button"
|
|
583
|
+
variant="ghost-static"
|
|
584
|
+
/>
|
|
585
|
+
}
|
|
586
|
+
>
|
|
587
|
+
<DiamondIcon weight={active ? "fill" : "regular"} />
|
|
588
|
+
</TooltipTrigger>
|
|
589
|
+
<TooltipContent side="top">{label}</TooltipContent>
|
|
590
|
+
</Tooltip>
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function getControlRenderGroups(entries: readonly ControlEntry[]): ControlRenderGroup[] {
|
|
595
|
+
const groups: ControlRenderGroup[] = [];
|
|
596
|
+
let index = 0;
|
|
597
|
+
|
|
598
|
+
while (index < entries.length) {
|
|
599
|
+
const entry = entries[index];
|
|
600
|
+
|
|
601
|
+
if (!entry) {
|
|
602
|
+
index += 1;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (entry[1].type !== "color") {
|
|
607
|
+
groups.push({ entry, kind: "control" });
|
|
608
|
+
index += 1;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const colorEntries: ControlEntry[] = [];
|
|
613
|
+
|
|
614
|
+
while (entries[index]?.[1].type === "color") {
|
|
615
|
+
const colorEntry = entries[index];
|
|
616
|
+
|
|
617
|
+
if (colorEntry) {
|
|
618
|
+
colorEntries.push(colorEntry);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
index += 1;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
for (let colorIndex = 0; colorIndex < colorEntries.length; colorIndex += 2) {
|
|
625
|
+
const firstEntry = colorEntries[colorIndex];
|
|
626
|
+
const secondEntry = colorEntries[colorIndex + 1];
|
|
627
|
+
|
|
628
|
+
if (firstEntry && secondEntry) {
|
|
629
|
+
groups.push({ entries: [firstEntry, secondEntry], kind: "colorGroup" });
|
|
630
|
+
} else if (firstEntry) {
|
|
631
|
+
groups.push({ entry: firstEntry, kind: "control" });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return groups;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function getControlRenderGroupIds(group: ControlRenderGroup): readonly string[] {
|
|
640
|
+
return group.kind === "colorGroup"
|
|
641
|
+
? group.entries.map(([id]) => id)
|
|
642
|
+
: [group.entry[0]];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function countControlsByType(
|
|
646
|
+
sections: readonly ToolcraftControlSectionSchema[],
|
|
647
|
+
type: string,
|
|
648
|
+
): number {
|
|
649
|
+
return sections.reduce(
|
|
650
|
+
(count, section) =>
|
|
651
|
+
count +
|
|
652
|
+
Object.values(section.controls).filter((control) => control.type === type)
|
|
653
|
+
.length,
|
|
654
|
+
0,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function shouldCommitTextControlOnBlur(control: ToolcraftControlSchema): boolean {
|
|
659
|
+
return (
|
|
660
|
+
control.commitMode === "setting" ||
|
|
661
|
+
Boolean(getToolcraftCanvasSizeTargetDimension(control.target))
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isColorFieldControl(control: ToolcraftControlSchema | undefined): boolean {
|
|
666
|
+
return control?.type === "color" || control?.type === "colorOpacity";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function isColorOnlySection(entries: readonly ControlEntry[]): boolean {
|
|
670
|
+
return entries.length > 0 && entries.every(([, control]) => isColorFieldControl(control));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function getRenderedControlsSectionTitle(
|
|
674
|
+
section: ToolcraftControlSectionSchema,
|
|
675
|
+
): ToolcraftControlSectionSchema["title"] {
|
|
676
|
+
return shouldHideRuntimeControlsSectionTitle(section) ? undefined : section.title;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function shouldHideRuntimeControlsSectionTitle(
|
|
680
|
+
section: ToolcraftControlSectionSchema,
|
|
681
|
+
): boolean {
|
|
682
|
+
return isRuntimeSetupSection(section) || isStickyFooterActionSection(section);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function isRuntimeSetupSection(section: ToolcraftControlSectionSchema): boolean {
|
|
686
|
+
if (section.title !== "Setup") {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return Object.values(section.controls).some(
|
|
691
|
+
(control) =>
|
|
692
|
+
control.type === "settingsTransfer" ||
|
|
693
|
+
control.target === "canvas.size.width" ||
|
|
694
|
+
control.target === "canvas.size.height",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function isStickyFooterActionSection(section: ToolcraftControlSectionSchema): boolean {
|
|
699
|
+
return (
|
|
700
|
+
section.actionGroup !== undefined &&
|
|
701
|
+
Object.values(section.controls).some((control) => control.type === "panelActions")
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function getControlsPanelSectionCollapseKey({
|
|
706
|
+
entries,
|
|
707
|
+
section,
|
|
708
|
+
sectionIndex,
|
|
709
|
+
}: {
|
|
710
|
+
entries: readonly ControlEntry[];
|
|
711
|
+
section: ToolcraftControlSectionSchema;
|
|
712
|
+
sectionIndex: number;
|
|
713
|
+
}): string {
|
|
714
|
+
const targets = entries.map(([id, control]) => `${id}:${control.target}`).join("|");
|
|
715
|
+
|
|
716
|
+
return `${section.title ?? "section"}:${sectionIndex}:${targets}`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function hashStorageKeyPart(value: string): string {
|
|
720
|
+
let hash = 2166136261;
|
|
721
|
+
|
|
722
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
723
|
+
hash ^= value.charCodeAt(index);
|
|
724
|
+
hash = Math.imul(hash, 16777619);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return (hash >>> 0).toString(36);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function getControlsPanelSectionCollapseStorageKey(
|
|
731
|
+
schema: ResolvedToolcraftAppSchema,
|
|
732
|
+
): string | null {
|
|
733
|
+
const controlsPanel = schema.panels.controls;
|
|
734
|
+
|
|
735
|
+
if (!controlsPanel) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const appIdentity =
|
|
740
|
+
schema.persistence.storage === "localStorage"
|
|
741
|
+
? `persistence:${schema.persistence.key}:v${schema.persistence.version}`
|
|
742
|
+
: JSON.stringify({
|
|
743
|
+
sections: controlsPanel.sections.map((section) => ({
|
|
744
|
+
controls: Object.entries(section.controls).map(([id, control]) => ({
|
|
745
|
+
id,
|
|
746
|
+
label: control.label,
|
|
747
|
+
target: control.target,
|
|
748
|
+
type: control.type,
|
|
749
|
+
})),
|
|
750
|
+
title: section.title,
|
|
751
|
+
})),
|
|
752
|
+
title: controlsPanel.title,
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
return `toolcraft:ui:controls-panel-sections:${hashStorageKeyPart(appIdentity)}:v${controlsPanelSectionCollapseStorageVersion}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function readControlsPanelCollapsedSections(
|
|
759
|
+
storageKey: string | null,
|
|
760
|
+
): Record<string, boolean> {
|
|
761
|
+
if (!storageKey || typeof window === "undefined") {
|
|
762
|
+
return {};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
const rawValue = readToolcraftLocalStorageValue(storageKey);
|
|
767
|
+
|
|
768
|
+
if (!rawValue) {
|
|
769
|
+
return {};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const payload: unknown = JSON.parse(rawValue);
|
|
773
|
+
|
|
774
|
+
if (
|
|
775
|
+
!payload ||
|
|
776
|
+
typeof payload !== "object" ||
|
|
777
|
+
!("version" in payload) ||
|
|
778
|
+
payload.version !== controlsPanelSectionCollapseStorageVersion ||
|
|
779
|
+
!("collapsed" in payload) ||
|
|
780
|
+
!Array.isArray(payload.collapsed)
|
|
781
|
+
) {
|
|
782
|
+
return {};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return Object.fromEntries(
|
|
786
|
+
payload.collapsed
|
|
787
|
+
.filter((item): item is string => typeof item === "string" && item.length > 0)
|
|
788
|
+
.map((item) => [item, true]),
|
|
789
|
+
);
|
|
790
|
+
} catch {
|
|
791
|
+
return {};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function writeControlsPanelCollapsedSections(
|
|
796
|
+
storageKey: string | null,
|
|
797
|
+
collapsedSectionByKey: Record<string, boolean>,
|
|
798
|
+
): void {
|
|
799
|
+
if (!storageKey || typeof window === "undefined") {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const collapsed = Object.entries(collapsedSectionByKey)
|
|
804
|
+
.filter(([, collapsedValue]) => collapsedValue)
|
|
805
|
+
.map(([key]) => key);
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
if (collapsed.length === 0) {
|
|
809
|
+
removeToolcraftLocalStorageValue(storageKey);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
window.localStorage.setItem(
|
|
814
|
+
storageKey,
|
|
815
|
+
JSON.stringify({
|
|
816
|
+
collapsed,
|
|
817
|
+
version: controlsPanelSectionCollapseStorageVersion,
|
|
818
|
+
}),
|
|
819
|
+
);
|
|
820
|
+
} catch {
|
|
821
|
+
// UI preferences are best-effort; panel interaction stays authoritative.
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function shouldShowColorFieldLabel({
|
|
826
|
+
control,
|
|
827
|
+
sectionHasOnlyColorFields,
|
|
828
|
+
}: {
|
|
829
|
+
control: ToolcraftControlSchema;
|
|
830
|
+
sectionHasOnlyColorFields: boolean;
|
|
831
|
+
}): boolean {
|
|
832
|
+
return control.label !== false && !sectionHasOnlyColorFields;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function hasColorOpacityControl(
|
|
836
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
837
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
838
|
+
): boolean {
|
|
839
|
+
return layoutGroup.controls.some(
|
|
840
|
+
(controlId) => controlsById[controlId]?.type === "colorOpacity",
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function hasSliderControl(
|
|
845
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
846
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
847
|
+
): boolean {
|
|
848
|
+
return layoutGroup.controls.some(
|
|
849
|
+
(controlId) =>
|
|
850
|
+
controlsById[controlId]?.type === "slider" ||
|
|
851
|
+
controlsById[controlId]?.type === "rangeSlider",
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function hasSectionedCompoundControl(
|
|
856
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
857
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
858
|
+
): boolean {
|
|
859
|
+
return layoutGroup.controls.some((controlId) => {
|
|
860
|
+
const control = controlsById[controlId];
|
|
861
|
+
|
|
862
|
+
return control ? shouldRenderCompoundControlSectionDivider(control) : false;
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const maxInlineSwitchLabelLength = 16;
|
|
867
|
+
const maxInlineSwitchLabelWordCount = 2;
|
|
868
|
+
|
|
869
|
+
function isInlineSwitchLabelSafe(
|
|
870
|
+
controlId: string,
|
|
871
|
+
control: ToolcraftControlSchema,
|
|
872
|
+
): boolean {
|
|
873
|
+
const label = getControlName(controlId, control.label).trim();
|
|
874
|
+
const wordCount = label.split(/\s+/u).filter(Boolean).length;
|
|
875
|
+
|
|
876
|
+
return label.length <= maxInlineSwitchLabelLength && wordCount <= maxInlineSwitchLabelWordCount;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function hasUnsafeInlineSwitchLabels(
|
|
880
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
881
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
882
|
+
): boolean {
|
|
883
|
+
const switchEntries = layoutGroup.controls
|
|
884
|
+
.map((controlId) => [controlId, controlsById[controlId]] as const)
|
|
885
|
+
.filter(
|
|
886
|
+
(entry): entry is readonly [string, ToolcraftControlSchema] =>
|
|
887
|
+
Boolean(entry[1]) && entry[1].type === "switch",
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
return (
|
|
891
|
+
switchEntries.length > 1 &&
|
|
892
|
+
switchEntries.some(([controlId, control]) => !isInlineSwitchLabelSafe(controlId, control))
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function isBooleanControl(control: ToolcraftControlSchema | undefined): boolean {
|
|
897
|
+
return control?.type === "checkbox" || control?.type === "switch";
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function isToggleParameterLayoutGroup(
|
|
901
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
902
|
+
layoutGroup: ToolcraftControlLayoutGroupSchema,
|
|
903
|
+
): boolean {
|
|
904
|
+
if (layoutGroup.controls.length !== 2) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const controls = layoutGroup.controls.map((controlId) => controlsById[controlId]);
|
|
909
|
+
const booleanControlCount = controls.filter((control) => isBooleanControl(control)).length;
|
|
910
|
+
const parameterControlCount = controls.filter((control) => !isBooleanControl(control)).length;
|
|
911
|
+
|
|
912
|
+
return booleanControlCount === 1 && parameterControlCount === 1;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function getControlTargetEntityKey(control: ToolcraftControlSchema): string | null {
|
|
916
|
+
const parts = control.target.split(".");
|
|
917
|
+
|
|
918
|
+
if (parts.length < 2) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return parts.slice(0, -1).join(".");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function shouldAutoInlineBooleanPair(
|
|
926
|
+
controlsById: Record<string, ToolcraftControlSchema>,
|
|
927
|
+
firstId: string,
|
|
928
|
+
secondId: string,
|
|
929
|
+
): boolean {
|
|
930
|
+
const firstControl = controlsById[firstId];
|
|
931
|
+
const secondControl = controlsById[secondId];
|
|
932
|
+
|
|
933
|
+
if (!isBooleanControl(firstControl) || !isBooleanControl(secondControl)) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (
|
|
938
|
+
!isInlineSwitchLabelSafe(firstId, firstControl) ||
|
|
939
|
+
!isInlineSwitchLabelSafe(secondId, secondControl)
|
|
940
|
+
) {
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const firstEntityKey = getControlTargetEntityKey(firstControl);
|
|
945
|
+
const secondEntityKey = getControlTargetEntityKey(secondControl);
|
|
946
|
+
|
|
947
|
+
return firstEntityKey !== null && firstEntityKey === secondEntityKey;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function getAutoInlineBooleanLayoutGroups({
|
|
951
|
+
controlsById,
|
|
952
|
+
renderedGroups,
|
|
953
|
+
reservedControlIds,
|
|
954
|
+
}: {
|
|
955
|
+
controlsById: Record<string, ToolcraftControlSchema>;
|
|
956
|
+
renderedGroups: readonly RenderedControlRenderGroup[];
|
|
957
|
+
reservedControlIds: ReadonlySet<string>;
|
|
958
|
+
}): ToolcraftControlLayoutGroupSchema[] {
|
|
959
|
+
const layoutGroups: ToolcraftControlLayoutGroupSchema[] = [];
|
|
960
|
+
let index = 0;
|
|
961
|
+
|
|
962
|
+
while (index < renderedGroups.length) {
|
|
963
|
+
const firstIds = renderedGroups[index]?.ids;
|
|
964
|
+
const secondIds = renderedGroups[index + 1]?.ids;
|
|
965
|
+
const firstId = firstIds?.length === 1 ? firstIds[0] : undefined;
|
|
966
|
+
const secondId = secondIds?.length === 1 ? secondIds[0] : undefined;
|
|
967
|
+
|
|
968
|
+
if (
|
|
969
|
+
firstId &&
|
|
970
|
+
secondId &&
|
|
971
|
+
!reservedControlIds.has(firstId) &&
|
|
972
|
+
!reservedControlIds.has(secondId) &&
|
|
973
|
+
shouldAutoInlineBooleanPair(controlsById, firstId, secondId)
|
|
974
|
+
) {
|
|
975
|
+
layoutGroups.push({
|
|
976
|
+
columns: 2,
|
|
977
|
+
controls: [firstId, secondId],
|
|
978
|
+
layout: "inline",
|
|
979
|
+
});
|
|
980
|
+
index += 2;
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
index += 1;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return layoutGroups;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function getControlMarkerCount(
|
|
991
|
+
control: ToolcraftControlSchema,
|
|
992
|
+
markerLimit?: number,
|
|
993
|
+
): number | undefined {
|
|
994
|
+
const markerCount = control.markerCount;
|
|
995
|
+
|
|
996
|
+
if (
|
|
997
|
+
markerLimit &&
|
|
998
|
+
control.variant === "discrete" &&
|
|
999
|
+
typeof markerCount === "number" &&
|
|
1000
|
+
markerCount > markerLimit
|
|
1001
|
+
) {
|
|
1002
|
+
return hiddenDiscreteMarkerCount;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return markerCount;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function renderControlLayoutGroups({
|
|
1009
|
+
controlsById,
|
|
1010
|
+
layoutGroups,
|
|
1011
|
+
renderedGroups,
|
|
1012
|
+
}: {
|
|
1013
|
+
controlsById: Record<string, ToolcraftControlSchema>;
|
|
1014
|
+
layoutGroups?: readonly ToolcraftControlLayoutGroupSchema[];
|
|
1015
|
+
renderedGroups: readonly RenderedControlRenderGroup[];
|
|
1016
|
+
}): React.ReactNode[] {
|
|
1017
|
+
if (!layoutGroups?.length) {
|
|
1018
|
+
const autoLayoutGroups = getAutoInlineBooleanLayoutGroups({
|
|
1019
|
+
controlsById,
|
|
1020
|
+
renderedGroups,
|
|
1021
|
+
reservedControlIds: new Set(),
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
if (autoLayoutGroups.length === 0) {
|
|
1025
|
+
return renderedGroups.map((group) => group.node);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return renderControlLayoutGroups({
|
|
1029
|
+
controlsById,
|
|
1030
|
+
layoutGroups: autoLayoutGroups,
|
|
1031
|
+
renderedGroups,
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const reservedControlIds = new Set(layoutGroups.flatMap((group) => group.controls));
|
|
1036
|
+
const resolvedLayoutGroups = [
|
|
1037
|
+
...layoutGroups,
|
|
1038
|
+
...getAutoInlineBooleanLayoutGroups({
|
|
1039
|
+
controlsById,
|
|
1040
|
+
renderedGroups,
|
|
1041
|
+
reservedControlIds,
|
|
1042
|
+
}),
|
|
1043
|
+
];
|
|
1044
|
+
const layoutGroupByControlId = getInlineLayoutGroupByControlId({
|
|
1045
|
+
controlsById,
|
|
1046
|
+
layoutGroups: resolvedLayoutGroups,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const nodes: React.ReactNode[] = [];
|
|
1050
|
+
|
|
1051
|
+
for (const renderedGroup of renderedGroups) {
|
|
1052
|
+
const layoutGroup = renderedGroup.ids
|
|
1053
|
+
.map((id) => layoutGroupByControlId.get(id))
|
|
1054
|
+
.find((group): group is ToolcraftControlLayoutGroupSchema => Boolean(group));
|
|
1055
|
+
|
|
1056
|
+
if (!layoutGroup) {
|
|
1057
|
+
nodes.push(renderedGroup.node);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const groupedRenderedControls = renderedGroups.filter((candidate) =>
|
|
1062
|
+
candidate.ids.some((id) => layoutGroup.controls.includes(id)),
|
|
1063
|
+
);
|
|
1064
|
+
const firstGroupedControl = groupedRenderedControls[0];
|
|
1065
|
+
|
|
1066
|
+
if (firstGroupedControl !== renderedGroup) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (groupedRenderedControls.length < 2) {
|
|
1071
|
+
nodes.push(renderedGroup.node);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
nodes.push(
|
|
1076
|
+
<ControlInlineGroup
|
|
1077
|
+
columns={layoutGroup.columns ?? 2}
|
|
1078
|
+
kind={
|
|
1079
|
+
isToggleParameterLayoutGroup(controlsById, layoutGroup)
|
|
1080
|
+
? "toggleParameter"
|
|
1081
|
+
: "default"
|
|
1082
|
+
}
|
|
1083
|
+
key={`layout-group-${layoutGroup.controls.join("-")}`}
|
|
1084
|
+
>
|
|
1085
|
+
{groupedRenderedControls.map((group) => group.node)}
|
|
1086
|
+
</ControlInlineGroup>,
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return nodes;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function getInlineLayoutGroupByControlId({
|
|
1094
|
+
controlsById,
|
|
1095
|
+
layoutGroups,
|
|
1096
|
+
}: {
|
|
1097
|
+
controlsById: Record<string, ToolcraftControlSchema>;
|
|
1098
|
+
layoutGroups?: readonly ToolcraftControlLayoutGroupSchema[];
|
|
1099
|
+
}): Map<string, ToolcraftControlLayoutGroupSchema> {
|
|
1100
|
+
const layoutGroupByControlId = new Map<string, ToolcraftControlLayoutGroupSchema>();
|
|
1101
|
+
|
|
1102
|
+
for (const layoutGroup of layoutGroups ?? []) {
|
|
1103
|
+
if (layoutGroup.layout !== "inline") {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (
|
|
1108
|
+
hasColorOpacityControl(controlsById, layoutGroup) ||
|
|
1109
|
+
hasSliderControl(controlsById, layoutGroup) ||
|
|
1110
|
+
hasSectionedCompoundControl(controlsById, layoutGroup) ||
|
|
1111
|
+
hasUnsafeInlineSwitchLabels(controlsById, layoutGroup)
|
|
1112
|
+
) {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
for (const controlId of layoutGroup.controls) {
|
|
1117
|
+
layoutGroupByControlId.set(controlId, layoutGroup);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return layoutGroupByControlId;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export function ControlsPanel({
|
|
1125
|
+
className,
|
|
1126
|
+
controlRenderers,
|
|
1127
|
+
framed = true,
|
|
1128
|
+
onPanelAction,
|
|
1129
|
+
onPanelStateChange,
|
|
1130
|
+
panelPlacement,
|
|
1131
|
+
panelState,
|
|
1132
|
+
}: ControlsPanelProps): React.JSX.Element | null {
|
|
1133
|
+
const { dispatch, state } = useToolcraft();
|
|
1134
|
+
const nextFooterActionIdRef = React.useRef(0);
|
|
1135
|
+
const [footerActionProgressEntries, setFooterActionProgressEntries] = React.useState<
|
|
1136
|
+
readonly FooterActionProgressEntry[]
|
|
1137
|
+
>([]);
|
|
1138
|
+
const sectionCollapseStorageKey = React.useMemo(
|
|
1139
|
+
() => getControlsPanelSectionCollapseStorageKey(state.schema),
|
|
1140
|
+
[state.schema],
|
|
1141
|
+
);
|
|
1142
|
+
const [collapsedSectionByKey, setCollapsedSectionByKey] = React.useState<
|
|
1143
|
+
Record<string, boolean>
|
|
1144
|
+
>(() => readControlsPanelCollapsedSections(sectionCollapseStorageKey));
|
|
1145
|
+
const stickyFooterProgress = React.useMemo(() => {
|
|
1146
|
+
for (let index = footerActionProgressEntries.length - 1; index >= 0; index -= 1) {
|
|
1147
|
+
const progress = footerActionProgressEntries[index]?.progress;
|
|
1148
|
+
|
|
1149
|
+
if (typeof progress === "number") {
|
|
1150
|
+
return progress;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return null;
|
|
1155
|
+
}, [footerActionProgressEntries]);
|
|
1156
|
+
const controlsPanel = state.schema.panels.controls;
|
|
1157
|
+
const keyframedControlIds = React.useMemo(
|
|
1158
|
+
() => new Set(state.timeline.keyframeGroups.map((group) => group.controlId)),
|
|
1159
|
+
[state.timeline.keyframeGroups],
|
|
1160
|
+
);
|
|
1161
|
+
const keyframeControlsEnabled = Boolean(
|
|
1162
|
+
state.schema.assembly.capabilities.includes("timeline.keyframes") &&
|
|
1163
|
+
state.timeline.expanded,
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
React.useEffect(() => {
|
|
1167
|
+
setCollapsedSectionByKey(readControlsPanelCollapsedSections(sectionCollapseStorageKey));
|
|
1168
|
+
}, [sectionCollapseStorageKey]);
|
|
1169
|
+
|
|
1170
|
+
if (!controlsPanel) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const resolvedControlsPanel = controlsPanel;
|
|
1175
|
+
const placement = panelPlacement ?? (framed ? "frame" : "surface");
|
|
1176
|
+
const lastHistoryPatch = state.history.undo.at(-1);
|
|
1177
|
+
const controlsResetKey =
|
|
1178
|
+
lastHistoryPatch?.label === "Reset controls" ? state.history.undo.length : 0;
|
|
1179
|
+
|
|
1180
|
+
function dispatchCommand(command: ToolcraftCommand): void {
|
|
1181
|
+
dispatch(command);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function setControlValue(
|
|
1185
|
+
target: string,
|
|
1186
|
+
value: unknown,
|
|
1187
|
+
label?: string,
|
|
1188
|
+
meta?: ControlChangeMeta,
|
|
1189
|
+
): void {
|
|
1190
|
+
dispatchCommand({
|
|
1191
|
+
history: meta?.history,
|
|
1192
|
+
historyGroup: meta?.historyGroup,
|
|
1193
|
+
label,
|
|
1194
|
+
target,
|
|
1195
|
+
type: "controls.setValue",
|
|
1196
|
+
value,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function createFooterActionProgressTracker(): {
|
|
1201
|
+
reportProgress: (progress: number) => void;
|
|
1202
|
+
trackResult: (result: PromiseLike<unknown> | void) => void;
|
|
1203
|
+
} {
|
|
1204
|
+
const id = nextFooterActionIdRef.current;
|
|
1205
|
+
nextFooterActionIdRef.current += 1;
|
|
1206
|
+
|
|
1207
|
+
let latestProgress: number | null = null;
|
|
1208
|
+
let isTracked = false;
|
|
1209
|
+
|
|
1210
|
+
function reportProgress(progress: number): void {
|
|
1211
|
+
latestProgress = clampFooterActionProgress(progress);
|
|
1212
|
+
|
|
1213
|
+
if (!isTracked) {
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
setFooterActionProgressEntries((entries) =>
|
|
1218
|
+
entries.map((entry) =>
|
|
1219
|
+
entry.id === id ? { ...entry, progress: latestProgress } : entry,
|
|
1220
|
+
),
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function trackResult(result: PromiseLike<unknown> | void): void {
|
|
1225
|
+
if (!isPromiseLike(result)) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
isTracked = true;
|
|
1230
|
+
setFooterActionProgressEntries((entries) => [
|
|
1231
|
+
...entries,
|
|
1232
|
+
{ id, progress: latestProgress },
|
|
1233
|
+
]);
|
|
1234
|
+
|
|
1235
|
+
void Promise.resolve(result)
|
|
1236
|
+
.catch((error: unknown) => {
|
|
1237
|
+
console.error("Toolcraft panel action failed.", error);
|
|
1238
|
+
})
|
|
1239
|
+
.finally(() => {
|
|
1240
|
+
setFooterActionProgressEntries((entries) =>
|
|
1241
|
+
entries.filter((entry) => entry.id !== id),
|
|
1242
|
+
);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return { reportProgress, trackResult };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function runAction(
|
|
1250
|
+
action: ToolcraftActionSchema,
|
|
1251
|
+
options: { trackFooterPending?: boolean } = {},
|
|
1252
|
+
): void {
|
|
1253
|
+
const command = action.command ?? (onPanelAction ? null : getActionCommand(action));
|
|
1254
|
+
|
|
1255
|
+
if (command) {
|
|
1256
|
+
dispatchCommand({ type: command });
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const footerActionProgressTracker = options.trackFooterPending
|
|
1261
|
+
? createFooterActionProgressTracker()
|
|
1262
|
+
: null;
|
|
1263
|
+
const result = onPanelAction?.({
|
|
1264
|
+
action,
|
|
1265
|
+
dispatch,
|
|
1266
|
+
reportProgress:
|
|
1267
|
+
footerActionProgressTracker?.reportProgress ?? noopReportProgress,
|
|
1268
|
+
state,
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
footerActionProgressTracker?.trackResult(result);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function getControlValue(control: ToolcraftControlSchema): unknown {
|
|
1275
|
+
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(control.target);
|
|
1276
|
+
|
|
1277
|
+
return canvasSizeDimension
|
|
1278
|
+
? state.canvas.size[canvasSizeDimension]
|
|
1279
|
+
: (state.values[control.target] ?? control.defaultValue);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function getControlDefaultValueByTarget(target: string): unknown {
|
|
1283
|
+
for (const section of resolvedControlsPanel.sections) {
|
|
1284
|
+
for (const control of Object.values(section.controls)) {
|
|
1285
|
+
if (control.target === target) {
|
|
1286
|
+
return control.defaultValue;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return undefined;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function getTargetValue(target: string): unknown {
|
|
1295
|
+
const canvasSizeDimension = getToolcraftCanvasSizeTargetDimension(target);
|
|
1296
|
+
|
|
1297
|
+
return canvasSizeDimension
|
|
1298
|
+
? state.canvas.size[canvasSizeDimension]
|
|
1299
|
+
: (state.values[target] ?? getControlDefaultValueByTarget(target));
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function conditionMatches(condition: ToolcraftControlConditionSchema): boolean {
|
|
1303
|
+
const value = getTargetValue(condition.target);
|
|
1304
|
+
const matches: boolean[] = [];
|
|
1305
|
+
|
|
1306
|
+
if ("equals" in condition) {
|
|
1307
|
+
matches.push(valuesEqual(value, condition.equals));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if ("notEquals" in condition) {
|
|
1311
|
+
matches.push(!valuesEqual(value, condition.notEquals));
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (Array.isArray(condition.oneOf)) {
|
|
1315
|
+
matches.push(valuesInclude(value, condition.oneOf));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
if (Array.isArray(condition.notOneOf)) {
|
|
1319
|
+
matches.push(!valuesInclude(value, condition.notOneOf));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const greaterThan = compareConditionNumbers(
|
|
1323
|
+
value,
|
|
1324
|
+
condition.greaterThan,
|
|
1325
|
+
(numberValue, expected) => numberValue > expected,
|
|
1326
|
+
);
|
|
1327
|
+
const greaterThanOrEqual = compareConditionNumbers(
|
|
1328
|
+
value,
|
|
1329
|
+
condition.greaterThanOrEqual,
|
|
1330
|
+
(numberValue, expected) => numberValue >= expected,
|
|
1331
|
+
);
|
|
1332
|
+
const lessThan = compareConditionNumbers(
|
|
1333
|
+
value,
|
|
1334
|
+
condition.lessThan,
|
|
1335
|
+
(numberValue, expected) => numberValue < expected,
|
|
1336
|
+
);
|
|
1337
|
+
const lessThanOrEqual = compareConditionNumbers(
|
|
1338
|
+
value,
|
|
1339
|
+
condition.lessThanOrEqual,
|
|
1340
|
+
(numberValue, expected) => numberValue <= expected,
|
|
1341
|
+
);
|
|
1342
|
+
|
|
1343
|
+
for (const match of [
|
|
1344
|
+
greaterThan,
|
|
1345
|
+
greaterThanOrEqual,
|
|
1346
|
+
lessThan,
|
|
1347
|
+
lessThanOrEqual,
|
|
1348
|
+
]) {
|
|
1349
|
+
if (match !== null) {
|
|
1350
|
+
matches.push(match);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return matches.length > 0 && matches.every(Boolean);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function isControlDisabled(control: ToolcraftControlSchema): boolean {
|
|
1358
|
+
if (control.disabled) {
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return control.disabledWhen ? conditionMatches(control.disabledWhen) : false;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function isControlVisible(control: ToolcraftControlSchema): boolean {
|
|
1366
|
+
return control.visibleWhen ? conditionMatches(control.visibleWhen) : true;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function isSectionVisible(section: ToolcraftControlSectionSchema): boolean {
|
|
1370
|
+
return section.visibleWhen ? conditionMatches(section.visibleWhen) : true;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function getVisibleSectionEntries(
|
|
1374
|
+
section: ToolcraftControlSectionSchema,
|
|
1375
|
+
): ControlEntry[] {
|
|
1376
|
+
return Object.entries(section.controls).filter(([, control]) =>
|
|
1377
|
+
isControlVisible(control),
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function getControlsRecord(
|
|
1382
|
+
entries: readonly ControlEntry[],
|
|
1383
|
+
): Record<string, ToolcraftControlSchema> {
|
|
1384
|
+
return Object.fromEntries(entries);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function getSelectedControlKeyframeTime(controlId: string): number | undefined {
|
|
1388
|
+
const selectedKeyframeId = state.timeline.selectedKeyframeId;
|
|
1389
|
+
|
|
1390
|
+
if (!selectedKeyframeId) {
|
|
1391
|
+
return undefined;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const selectedKeyframe = state.timeline.keyframeGroups
|
|
1395
|
+
.find((group) => group.controlId === controlId)
|
|
1396
|
+
?.keyframes.find((keyframe) => keyframe.id === selectedKeyframeId);
|
|
1397
|
+
|
|
1398
|
+
return selectedKeyframe?.timeSeconds;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function maybeUpsertControlKeyframe(
|
|
1402
|
+
control: ToolcraftControlSchema,
|
|
1403
|
+
name: string,
|
|
1404
|
+
value: unknown,
|
|
1405
|
+
): void {
|
|
1406
|
+
if (
|
|
1407
|
+
!keyframeControlsEnabled ||
|
|
1408
|
+
!keyframedControlIds.has(control.target) ||
|
|
1409
|
+
!canCreateControlKeyframe(control)
|
|
1410
|
+
) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
dispatchCommand({
|
|
1415
|
+
controlId: control.target,
|
|
1416
|
+
controlLabel: name,
|
|
1417
|
+
timeSeconds: getSelectedControlKeyframeTime(control.target),
|
|
1418
|
+
type: "timeline.upsertControlKeyframe",
|
|
1419
|
+
value,
|
|
1420
|
+
valueLabel: formatControlValueLabel(control, value),
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function getKeyframeLabelAction(
|
|
1425
|
+
control: ToolcraftControlSchema,
|
|
1426
|
+
name: string,
|
|
1427
|
+
value: unknown,
|
|
1428
|
+
): React.ReactNode {
|
|
1429
|
+
if (!keyframeControlsEnabled || !canCreateControlKeyframe(control)) {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
return (
|
|
1434
|
+
<ControlKeyframeButton
|
|
1435
|
+
active={keyframedControlIds.has(control.target)}
|
|
1436
|
+
name={name}
|
|
1437
|
+
onClick={() => {
|
|
1438
|
+
dispatchCommand({
|
|
1439
|
+
controlId: control.target,
|
|
1440
|
+
controlLabel: name,
|
|
1441
|
+
type: "timeline.toggleControlKeyframes",
|
|
1442
|
+
value,
|
|
1443
|
+
valueLabel: formatControlValueLabel(control, value),
|
|
1444
|
+
});
|
|
1445
|
+
}}
|
|
1446
|
+
/>
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function withKeyframeLabelAction({
|
|
1451
|
+
children,
|
|
1452
|
+
control,
|
|
1453
|
+
disableAction = false,
|
|
1454
|
+
labelActionName,
|
|
1455
|
+
name,
|
|
1456
|
+
providerKey,
|
|
1457
|
+
value,
|
|
1458
|
+
}: {
|
|
1459
|
+
children: React.ReactNode;
|
|
1460
|
+
control: ToolcraftControlSchema;
|
|
1461
|
+
disableAction?: boolean;
|
|
1462
|
+
labelActionName?: string;
|
|
1463
|
+
name: string;
|
|
1464
|
+
providerKey: string;
|
|
1465
|
+
value: unknown;
|
|
1466
|
+
}): React.ReactNode {
|
|
1467
|
+
if (disableAction) {
|
|
1468
|
+
return children;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const actionName = labelActionName ?? name;
|
|
1472
|
+
const action = getKeyframeLabelAction(control, actionName, value);
|
|
1473
|
+
|
|
1474
|
+
if (!action) {
|
|
1475
|
+
return children;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
return (
|
|
1479
|
+
<ControlFieldLabelActionProvider
|
|
1480
|
+
action={action}
|
|
1481
|
+
key={providerKey}
|
|
1482
|
+
label={actionName}
|
|
1483
|
+
>
|
|
1484
|
+
{children}
|
|
1485
|
+
</ControlFieldLabelActionProvider>
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function getControlHelpText(
|
|
1490
|
+
control: ToolcraftControlSchema,
|
|
1491
|
+
): string | null {
|
|
1492
|
+
const description = control.description?.trim();
|
|
1493
|
+
|
|
1494
|
+
return description || null;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function withControlLabelHelp({
|
|
1498
|
+
children,
|
|
1499
|
+
control,
|
|
1500
|
+
label,
|
|
1501
|
+
providerKey,
|
|
1502
|
+
}: {
|
|
1503
|
+
children: React.ReactNode;
|
|
1504
|
+
control: ToolcraftControlSchema;
|
|
1505
|
+
label: string;
|
|
1506
|
+
providerKey: string;
|
|
1507
|
+
}): React.ReactNode {
|
|
1508
|
+
const help = getControlHelpText(control);
|
|
1509
|
+
|
|
1510
|
+
if (!help) {
|
|
1511
|
+
return children;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
return (
|
|
1515
|
+
<ControlFieldLabelHelpProvider
|
|
1516
|
+
help={help}
|
|
1517
|
+
key={`${providerKey}-help`}
|
|
1518
|
+
label={label}
|
|
1519
|
+
>
|
|
1520
|
+
{children}
|
|
1521
|
+
</ControlFieldLabelHelpProvider>
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function getSectionHeaderKeyframeEntry(
|
|
1526
|
+
entries: readonly ControlEntry[],
|
|
1527
|
+
title: React.ReactNode,
|
|
1528
|
+
): ControlEntry | null {
|
|
1529
|
+
if (typeof title !== "string") {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const matchingTitleEntry = entries.find(([id, control]) => {
|
|
1534
|
+
if (control.type === "channelMixer" || control.type === "curves") {
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const name = getControlName(id, control.label);
|
|
1539
|
+
|
|
1540
|
+
return name === title && canCreateControlKeyframe(control);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
if (matchingTitleEntry) {
|
|
1544
|
+
return matchingTitleEntry;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function getSectionHeaderKeyframeAction(entry: ControlEntry): React.ReactNode {
|
|
1551
|
+
const [id, control] = entry;
|
|
1552
|
+
const name = getControlName(id, control.label);
|
|
1553
|
+
|
|
1554
|
+
return getKeyframeLabelAction(control, name, getControlValue(control));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function renderColorGroup(
|
|
1558
|
+
entries: readonly ControlEntry[],
|
|
1559
|
+
headerKeyframeTarget: string | null,
|
|
1560
|
+
sectionHasOnlyColorFields: boolean,
|
|
1561
|
+
): React.JSX.Element | null {
|
|
1562
|
+
const colorInputs = entries.map(([id, control]) => {
|
|
1563
|
+
const name = getControlName(id, control.label);
|
|
1564
|
+
const value = getControlValue(control);
|
|
1565
|
+
const colorValue = asColorValue(value);
|
|
1566
|
+
|
|
1567
|
+
return {
|
|
1568
|
+
hex: colorValue.hex,
|
|
1569
|
+
name,
|
|
1570
|
+
onValueChange: (nextValue, meta) => {
|
|
1571
|
+
setControlValue(control.target, nextValue, name, meta);
|
|
1572
|
+
maybeUpsertControlKeyframe(control, name, nextValue);
|
|
1573
|
+
},
|
|
1574
|
+
showLabel: shouldShowColorFieldLabel({
|
|
1575
|
+
control,
|
|
1576
|
+
sectionHasOnlyColorFields,
|
|
1577
|
+
}),
|
|
1578
|
+
} satisfies ColorControlInput;
|
|
1579
|
+
});
|
|
1580
|
+
const [firstInput, secondInput] = colorInputs;
|
|
1581
|
+
|
|
1582
|
+
if (!firstInput) {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (!secondInput) {
|
|
1587
|
+
const firstEntry = entries[0];
|
|
1588
|
+
const firstControl = firstEntry?.[1];
|
|
1589
|
+
const firstValue = firstControl ? getControlValue(firstControl) : undefined;
|
|
1590
|
+
|
|
1591
|
+
return firstControl ? (
|
|
1592
|
+
withKeyframeLabelAction({
|
|
1593
|
+
children: (
|
|
1594
|
+
<Color
|
|
1595
|
+
hex={firstInput.hex}
|
|
1596
|
+
key={firstInput.name}
|
|
1597
|
+
name={firstInput.name}
|
|
1598
|
+
onValueChange={firstInput.onValueChange}
|
|
1599
|
+
showLabel={shouldShowColorFieldLabel({
|
|
1600
|
+
control: firstControl,
|
|
1601
|
+
sectionHasOnlyColorFields,
|
|
1602
|
+
})}
|
|
1603
|
+
/>
|
|
1604
|
+
),
|
|
1605
|
+
control: firstControl,
|
|
1606
|
+
disableAction: firstControl.target === headerKeyframeTarget,
|
|
1607
|
+
name: firstInput.name,
|
|
1608
|
+
providerKey: firstInput.name,
|
|
1609
|
+
value: firstValue,
|
|
1610
|
+
}) as React.JSX.Element
|
|
1611
|
+
) : (
|
|
1612
|
+
<Color
|
|
1613
|
+
hex={firstInput.hex}
|
|
1614
|
+
key={firstInput.name}
|
|
1615
|
+
name={firstInput.name}
|
|
1616
|
+
onValueChange={firstInput.onValueChange}
|
|
1617
|
+
showLabel={firstInput.showLabel}
|
|
1618
|
+
/>
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
return (
|
|
1623
|
+
<Color
|
|
1624
|
+
inputs={[firstInput, secondInput] as ColorControlInputPair}
|
|
1625
|
+
key={`${firstInput.name}-${secondInput.name}`}
|
|
1626
|
+
/>
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const visibleSections = resolvedControlsPanel.sections
|
|
1631
|
+
.map((section) => ({
|
|
1632
|
+
entries: getVisibleSectionEntries(section),
|
|
1633
|
+
section,
|
|
1634
|
+
}))
|
|
1635
|
+
.filter(({ entries, section }) => isSectionVisible(section) && entries.length > 0);
|
|
1636
|
+
const visibleControlsPanelSections = visibleSections.map(({ entries, section }) => ({
|
|
1637
|
+
...section,
|
|
1638
|
+
controls: getControlsRecord(entries),
|
|
1639
|
+
}));
|
|
1640
|
+
const vectorControlCount = countControlsByType(visibleControlsPanelSections, "vector");
|
|
1641
|
+
const vectorPadShape = vectorControlCount === 1 ? "square" : "compact";
|
|
1642
|
+
|
|
1643
|
+
const panel = (
|
|
1644
|
+
<Panel
|
|
1645
|
+
className={cn(
|
|
1646
|
+
"shrink-0",
|
|
1647
|
+
placement === "frame" && "max-h-none",
|
|
1648
|
+
className,
|
|
1649
|
+
)}
|
|
1650
|
+
collapsed={panelState?.collapsed}
|
|
1651
|
+
contentTransitionSuppressionKey={
|
|
1652
|
+
keyframeControlsEnabled ? "keyframes" : "plain"
|
|
1653
|
+
}
|
|
1654
|
+
key={controlsResetKey}
|
|
1655
|
+
onCollapsedChange={(collapsed) => onPanelStateChange?.({ collapsed })}
|
|
1656
|
+
onResetControls={() => dispatchCommand({ type: "controls.reset" })}
|
|
1657
|
+
stickyFooterActive={footerActionProgressEntries.length > 0}
|
|
1658
|
+
stickyFooterProgress={stickyFooterProgress}
|
|
1659
|
+
title={resolvedControlsPanel.title}
|
|
1660
|
+
>
|
|
1661
|
+
{visibleSections.map(({ entries, section }, sectionIndex) => {
|
|
1662
|
+
const visibleControls = getControlsRecord(entries);
|
|
1663
|
+
const sectionHasOnlyColorFields = isColorOnlySection(entries);
|
|
1664
|
+
const renderedSectionTitle = getRenderedControlsSectionTitle(section);
|
|
1665
|
+
const sectionSpacing = isRuntimeSetupSection(section) ? "technical" : "default";
|
|
1666
|
+
const sectionCollapseKey = getControlsPanelSectionCollapseKey({
|
|
1667
|
+
entries,
|
|
1668
|
+
section,
|
|
1669
|
+
sectionIndex,
|
|
1670
|
+
});
|
|
1671
|
+
const isSectionCollapsible = renderedSectionTitle !== undefined;
|
|
1672
|
+
const isSectionCollapsed =
|
|
1673
|
+
isSectionCollapsible && collapsedSectionByKey[sectionCollapseKey] === true;
|
|
1674
|
+
const headerKeyframeEntry = getSectionHeaderKeyframeEntry(entries, section.title);
|
|
1675
|
+
const headerKeyframeTarget = headerKeyframeEntry?.[1].target ?? null;
|
|
1676
|
+
const inlineLayoutGroupByControlId = getInlineLayoutGroupByControlId({
|
|
1677
|
+
controlsById: visibleControls,
|
|
1678
|
+
layoutGroups: section.layoutGroups,
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
return (
|
|
1682
|
+
<PanelSection
|
|
1683
|
+
action={
|
|
1684
|
+
headerKeyframeEntry
|
|
1685
|
+
? getSectionHeaderKeyframeAction(headerKeyframeEntry)
|
|
1686
|
+
: undefined
|
|
1687
|
+
}
|
|
1688
|
+
actionGroup={section.actionGroup}
|
|
1689
|
+
allowCompoundDividers={entries.length > 1}
|
|
1690
|
+
collapsed={isSectionCollapsed}
|
|
1691
|
+
collapsible={isSectionCollapsible}
|
|
1692
|
+
key={`${section.title ?? "section"}-${sectionIndex}`}
|
|
1693
|
+
onCollapsedChange={(collapsed) => {
|
|
1694
|
+
setCollapsedSectionByKey((current) => {
|
|
1695
|
+
const next = {
|
|
1696
|
+
...current,
|
|
1697
|
+
[sectionCollapseKey]: collapsed,
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
writeControlsPanelCollapsedSections(sectionCollapseStorageKey, next);
|
|
1701
|
+
|
|
1702
|
+
return next;
|
|
1703
|
+
});
|
|
1704
|
+
}}
|
|
1705
|
+
spacing={sectionSpacing}
|
|
1706
|
+
title={renderedSectionTitle}
|
|
1707
|
+
>
|
|
1708
|
+
{renderControlLayoutGroups({
|
|
1709
|
+
controlsById: visibleControls,
|
|
1710
|
+
layoutGroups: section.layoutGroups,
|
|
1711
|
+
renderedGroups: getControlRenderGroups(entries).map((group) => {
|
|
1712
|
+
const ids = getControlRenderGroupIds(group);
|
|
1713
|
+
|
|
1714
|
+
if (group.kind === "colorGroup") {
|
|
1715
|
+
return {
|
|
1716
|
+
ids,
|
|
1717
|
+
node: renderColorGroup(
|
|
1718
|
+
group.entries,
|
|
1719
|
+
headerKeyframeTarget,
|
|
1720
|
+
sectionHasOnlyColorFields,
|
|
1721
|
+
),
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const [id, rawControl] = group.entry;
|
|
1726
|
+
const isInInlineLayoutGroup = inlineLayoutGroupByControlId.has(id);
|
|
1727
|
+
const disabled = isControlDisabled(rawControl);
|
|
1728
|
+
const control =
|
|
1729
|
+
disabled === Boolean(rawControl.disabled)
|
|
1730
|
+
? rawControl
|
|
1731
|
+
: { ...rawControl, disabled };
|
|
1732
|
+
const name = getControlName(id, control.label);
|
|
1733
|
+
const value = getControlValue(control);
|
|
1734
|
+
const usesHeaderKeyframeAction = control.target === headerKeyframeTarget;
|
|
1735
|
+
const commitWithLabel =
|
|
1736
|
+
(label: string) =>
|
|
1737
|
+
(nextValue: unknown, meta?: ControlChangeMeta): void => {
|
|
1738
|
+
setControlValue(control.target, nextValue, label, meta);
|
|
1739
|
+
maybeUpsertControlKeyframe(control, label, nextValue);
|
|
1740
|
+
};
|
|
1741
|
+
const commit = commitWithLabel(name);
|
|
1742
|
+
const node = (() => {
|
|
1743
|
+
switch (control.type) {
|
|
1744
|
+
case "actions": {
|
|
1745
|
+
const actions = asActionSchemas(control.actions);
|
|
1746
|
+
|
|
1747
|
+
return (
|
|
1748
|
+
<Actions
|
|
1749
|
+
actions={actions.map((action) => ({
|
|
1750
|
+
icon: action.icon,
|
|
1751
|
+
label: action.label,
|
|
1752
|
+
value: action.value,
|
|
1753
|
+
}))}
|
|
1754
|
+
key={id}
|
|
1755
|
+
name={name}
|
|
1756
|
+
onAction={(actionValue) => {
|
|
1757
|
+
const action = actions.find((item) => item.value === actionValue);
|
|
1758
|
+
|
|
1759
|
+
if (action) {
|
|
1760
|
+
runAction(action);
|
|
1761
|
+
}
|
|
1762
|
+
}}
|
|
1763
|
+
showLabel={control.label !== false}
|
|
1764
|
+
/>
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
case "anchorGrid":
|
|
1769
|
+
return withKeyframeLabelAction({
|
|
1770
|
+
children: (
|
|
1771
|
+
<AnchorGrid
|
|
1772
|
+
key={id}
|
|
1773
|
+
name={name}
|
|
1774
|
+
onValueChange={commit}
|
|
1775
|
+
value={
|
|
1776
|
+
asString(value, "center") as React.ComponentProps<
|
|
1777
|
+
typeof AnchorGrid
|
|
1778
|
+
>["value"]
|
|
1779
|
+
}
|
|
1780
|
+
/>
|
|
1781
|
+
),
|
|
1782
|
+
control,
|
|
1783
|
+
disableAction: usesHeaderKeyframeAction,
|
|
1784
|
+
name,
|
|
1785
|
+
providerKey: id,
|
|
1786
|
+
value,
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
case "channelMixer": {
|
|
1790
|
+
const channelMixerName = name;
|
|
1791
|
+
|
|
1792
|
+
return withKeyframeLabelAction({
|
|
1793
|
+
children: (
|
|
1794
|
+
<ChannelMixer
|
|
1795
|
+
key={id}
|
|
1796
|
+
name={channelMixerName}
|
|
1797
|
+
onValueChange={(nextValue) =>
|
|
1798
|
+
commitWithLabel(channelMixerName)(nextValue.values)
|
|
1799
|
+
}
|
|
1800
|
+
values={
|
|
1801
|
+
isRecord(value) ? (value as ChannelMixerValues) : defaultChannelMixerValues
|
|
1802
|
+
}
|
|
1803
|
+
/>
|
|
1804
|
+
),
|
|
1805
|
+
control,
|
|
1806
|
+
disableAction: false,
|
|
1807
|
+
labelActionName: channelMixerName,
|
|
1808
|
+
name,
|
|
1809
|
+
providerKey: id,
|
|
1810
|
+
value,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
case "checkbox":
|
|
1815
|
+
return (
|
|
1816
|
+
<Checkbox
|
|
1817
|
+
checked={asBoolean(value)}
|
|
1818
|
+
key={id}
|
|
1819
|
+
name={name}
|
|
1820
|
+
onCheckedChange={commit}
|
|
1821
|
+
showLabel={control.label !== false}
|
|
1822
|
+
/>
|
|
1823
|
+
);
|
|
1824
|
+
|
|
1825
|
+
case "code":
|
|
1826
|
+
return withKeyframeLabelAction({
|
|
1827
|
+
children: (
|
|
1828
|
+
<CodeTextarea
|
|
1829
|
+
defaultValue={asString(control.defaultValue, asString(value))}
|
|
1830
|
+
key={id}
|
|
1831
|
+
name={name}
|
|
1832
|
+
onValueChange={commit}
|
|
1833
|
+
value={asString(value)}
|
|
1834
|
+
/>
|
|
1835
|
+
),
|
|
1836
|
+
control,
|
|
1837
|
+
disableAction: usesHeaderKeyframeAction,
|
|
1838
|
+
name,
|
|
1839
|
+
providerKey: id,
|
|
1840
|
+
value,
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
case "color": {
|
|
1844
|
+
const colorValue = asColorValue(value);
|
|
1845
|
+
|
|
1846
|
+
return withKeyframeLabelAction({
|
|
1847
|
+
children: (
|
|
1848
|
+
<Color
|
|
1849
|
+
hex={colorValue.hex}
|
|
1850
|
+
key={id}
|
|
1851
|
+
name={name}
|
|
1852
|
+
onValueChange={commit}
|
|
1853
|
+
showLabel={shouldShowColorFieldLabel({
|
|
1854
|
+
control,
|
|
1855
|
+
sectionHasOnlyColorFields,
|
|
1856
|
+
})}
|
|
1857
|
+
/>
|
|
1858
|
+
),
|
|
1859
|
+
control,
|
|
1860
|
+
disableAction: usesHeaderKeyframeAction,
|
|
1861
|
+
name,
|
|
1862
|
+
providerKey: id,
|
|
1863
|
+
value,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
case "colorOpacity": {
|
|
1868
|
+
const colorOpacityValue = asColorOpacityValue(value);
|
|
1869
|
+
|
|
1870
|
+
return withKeyframeLabelAction({
|
|
1871
|
+
children: (
|
|
1872
|
+
<ColorOpacity
|
|
1873
|
+
hex={colorOpacityValue.hex}
|
|
1874
|
+
key={id}
|
|
1875
|
+
name={name}
|
|
1876
|
+
onValueChange={commit}
|
|
1877
|
+
opacity={colorOpacityValue.opacity}
|
|
1878
|
+
showLabel={shouldShowColorFieldLabel({
|
|
1879
|
+
control,
|
|
1880
|
+
sectionHasOnlyColorFields,
|
|
1881
|
+
})}
|
|
1882
|
+
/>
|
|
1883
|
+
),
|
|
1884
|
+
control,
|
|
1885
|
+
disableAction: usesHeaderKeyframeAction,
|
|
1886
|
+
name,
|
|
1887
|
+
providerKey: id,
|
|
1888
|
+
value: colorOpacityValue,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
case "curves": {
|
|
1893
|
+
const curvesName = control.label === false ? "Curves" : name;
|
|
1894
|
+
|
|
1895
|
+
return withKeyframeLabelAction({
|
|
1896
|
+
children: (
|
|
1897
|
+
<Curves
|
|
1898
|
+
interpolation={asCurveInterpolation(control.interpolation)}
|
|
1899
|
+
key={id}
|
|
1900
|
+
name={curvesName}
|
|
1901
|
+
onValueChange={commitWithLabel(curvesName)}
|
|
1902
|
+
variant={control.variant === "single" ? "single" : "rgb"}
|
|
1903
|
+
{...(isRecord(value) ? value : {})}
|
|
1904
|
+
/>
|
|
1905
|
+
),
|
|
1906
|
+
control,
|
|
1907
|
+
disableAction: false,
|
|
1908
|
+
labelActionName: curvesName,
|
|
1909
|
+
name: curvesName,
|
|
1910
|
+
providerKey: id,
|
|
1911
|
+
value,
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
case "fileDrop": {
|
|
1916
|
+
const previewMediaAsset = state.schema.panels.layers
|
|
1917
|
+
? undefined
|
|
1918
|
+
: state.mediaAssets[0];
|
|
1919
|
+
|
|
1920
|
+
return (
|
|
1921
|
+
<FileDrop
|
|
1922
|
+
accept={control.accept ?? "PNG, JPEG, GIF, SVG, WebP"}
|
|
1923
|
+
key={id}
|
|
1924
|
+
onClear={
|
|
1925
|
+
previewMediaAsset
|
|
1926
|
+
? () => {
|
|
1927
|
+
dispatchCommand({
|
|
1928
|
+
mediaId: previewMediaAsset.id,
|
|
1929
|
+
type: "media.delete",
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
: undefined
|
|
1933
|
+
}
|
|
1934
|
+
onFileSelect={(file) => {
|
|
1935
|
+
void readImportedImageFile(file, state.canvas.size).then((importedImage) => {
|
|
1936
|
+
if (!importedImage) {
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
dispatchCommand({
|
|
1941
|
+
asset: {
|
|
1942
|
+
dataUrl: importedImage.dataUrl,
|
|
1943
|
+
fileName: file.name,
|
|
1944
|
+
mimeType: file.type || "image/*",
|
|
1945
|
+
position: { x: 0, y: 0 },
|
|
1946
|
+
size: importedImage.size,
|
|
1947
|
+
},
|
|
1948
|
+
type: "media.import",
|
|
1949
|
+
});
|
|
1950
|
+
});
|
|
1951
|
+
}}
|
|
1952
|
+
preview={
|
|
1953
|
+
previewMediaAsset
|
|
1954
|
+
? {
|
|
1955
|
+
alt: previewMediaAsset.fileName,
|
|
1956
|
+
size: previewMediaAsset.size,
|
|
1957
|
+
src: previewMediaAsset.dataUrl,
|
|
1958
|
+
}
|
|
1959
|
+
: undefined
|
|
1960
|
+
}
|
|
1961
|
+
/>
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
case "gradient": {
|
|
1966
|
+
const gradientValue = asGradientValue(value);
|
|
1967
|
+
|
|
1968
|
+
return withKeyframeLabelAction({
|
|
1969
|
+
children: (
|
|
1970
|
+
<Gradient
|
|
1971
|
+
angle={gradientValue.angle}
|
|
1972
|
+
gradientType={gradientValue.gradientType}
|
|
1973
|
+
key={id}
|
|
1974
|
+
onValueChange={commit}
|
|
1975
|
+
stops={gradientValue.stops}
|
|
1976
|
+
/>
|
|
1977
|
+
),
|
|
1978
|
+
control,
|
|
1979
|
+
disableAction: usesHeaderKeyframeAction,
|
|
1980
|
+
name,
|
|
1981
|
+
providerKey: id,
|
|
1982
|
+
value,
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
case "fontPicker": {
|
|
1987
|
+
const fontPickerValue = asFontPickerValue(value);
|
|
1988
|
+
|
|
1989
|
+
return withKeyframeLabelAction({
|
|
1990
|
+
children: (
|
|
1991
|
+
<FontPicker
|
|
1992
|
+
defaultValue={asFontPickerValue(control.defaultValue)}
|
|
1993
|
+
disabled={control.disabled}
|
|
1994
|
+
key={id}
|
|
1995
|
+
name={name}
|
|
1996
|
+
onValueChange={commit}
|
|
1997
|
+
value={fontPickerValue}
|
|
1998
|
+
/>
|
|
1999
|
+
),
|
|
2000
|
+
control,
|
|
2001
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2002
|
+
name,
|
|
2003
|
+
providerKey: id,
|
|
2004
|
+
value: fontPickerValue,
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
case "imagePicker":
|
|
2009
|
+
return (
|
|
2010
|
+
<ImagePicker
|
|
2011
|
+
items={control.items as readonly ImagePickerItem[] | undefined}
|
|
2012
|
+
key={id}
|
|
2013
|
+
name={name}
|
|
2014
|
+
onValueChange={commit}
|
|
2015
|
+
value={asString(value, control.items?.[0]?.value ?? "")}
|
|
2016
|
+
/>
|
|
2017
|
+
);
|
|
2018
|
+
|
|
2019
|
+
case "palette":
|
|
2020
|
+
return withKeyframeLabelAction({
|
|
2021
|
+
children: (
|
|
2022
|
+
<Palette
|
|
2023
|
+
defaultValue={
|
|
2024
|
+
isRecord(control.defaultValue)
|
|
2025
|
+
? (control.defaultValue as React.ComponentProps<
|
|
2026
|
+
typeof Palette
|
|
2027
|
+
>["defaultValue"])
|
|
2028
|
+
: undefined
|
|
2029
|
+
}
|
|
2030
|
+
key={id}
|
|
2031
|
+
onCommit={(nextValue) => commit(nextValue)}
|
|
2032
|
+
value={
|
|
2033
|
+
isRecord(value)
|
|
2034
|
+
? (value as React.ComponentProps<typeof Palette>["value"])
|
|
2035
|
+
: undefined
|
|
2036
|
+
}
|
|
2037
|
+
/>
|
|
2038
|
+
),
|
|
2039
|
+
control,
|
|
2040
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2041
|
+
name,
|
|
2042
|
+
providerKey: id,
|
|
2043
|
+
value,
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
case "panelActions": {
|
|
2047
|
+
const actions = asActionSchemas(control.actions);
|
|
2048
|
+
|
|
2049
|
+
return (
|
|
2050
|
+
<PanelActions
|
|
2051
|
+
actions={actions.map((action) => ({
|
|
2052
|
+
icon: action.icon,
|
|
2053
|
+
name: getActionLabel(action),
|
|
2054
|
+
value: action.value,
|
|
2055
|
+
variant: getPanelActionButtonVariant(action.variant),
|
|
2056
|
+
}))}
|
|
2057
|
+
key={id}
|
|
2058
|
+
onAction={(actionValue) => {
|
|
2059
|
+
const action = actions.find((item) => item.value === actionValue);
|
|
2060
|
+
|
|
2061
|
+
if (action) {
|
|
2062
|
+
runAction(action, { trackFooterPending: true });
|
|
2063
|
+
}
|
|
2064
|
+
}}
|
|
2065
|
+
/>
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
case "rangeInput": {
|
|
2070
|
+
const rangeValue = asRangeInputValue(value);
|
|
2071
|
+
|
|
2072
|
+
return withKeyframeLabelAction({
|
|
2073
|
+
children: (
|
|
2074
|
+
<RangeInput
|
|
2075
|
+
defaultValue={asRangeInputValue(control.defaultValue)}
|
|
2076
|
+
end={rangeValue.end}
|
|
2077
|
+
key={id}
|
|
2078
|
+
name={name}
|
|
2079
|
+
onValueChange={commit}
|
|
2080
|
+
start={rangeValue.start}
|
|
2081
|
+
/>
|
|
2082
|
+
),
|
|
2083
|
+
control,
|
|
2084
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2085
|
+
name,
|
|
2086
|
+
providerKey: id,
|
|
2087
|
+
value,
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
case "rangeSlider":
|
|
2092
|
+
return withKeyframeLabelAction({
|
|
2093
|
+
children: (
|
|
2094
|
+
<RangeSlider
|
|
2095
|
+
baseValue={asNumberArray(control.defaultValue, [])}
|
|
2096
|
+
disabled={control.disabled}
|
|
2097
|
+
markerCount={getControlMarkerCount(control)}
|
|
2098
|
+
max={control.max ?? 100}
|
|
2099
|
+
min={control.min ?? 0}
|
|
2100
|
+
key={id}
|
|
2101
|
+
name={name}
|
|
2102
|
+
onValueChange={commit}
|
|
2103
|
+
step={control.step ?? 0.1}
|
|
2104
|
+
unit={control.unit}
|
|
2105
|
+
value={asNumberArray(value, [control.min ?? 0, control.max ?? 100])}
|
|
2106
|
+
valueLabel={control.valueLabel}
|
|
2107
|
+
variant={
|
|
2108
|
+
control.variant === "discrete" ? "discrete" : "continuous"
|
|
2109
|
+
}
|
|
2110
|
+
/>
|
|
2111
|
+
),
|
|
2112
|
+
control,
|
|
2113
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2114
|
+
name,
|
|
2115
|
+
providerKey: id,
|
|
2116
|
+
value,
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
case "segmented":
|
|
2120
|
+
return withKeyframeLabelAction({
|
|
2121
|
+
children: (
|
|
2122
|
+
<Segmented
|
|
2123
|
+
key={id}
|
|
2124
|
+
name={name}
|
|
2125
|
+
onValueChange={commit}
|
|
2126
|
+
options={control.options ?? []}
|
|
2127
|
+
value={asString(value, control.options?.[0]?.value ?? "")}
|
|
2128
|
+
variant={control.variant === "dots" ? "dots" : "default"}
|
|
2129
|
+
/>
|
|
2130
|
+
),
|
|
2131
|
+
control,
|
|
2132
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2133
|
+
name,
|
|
2134
|
+
providerKey: id,
|
|
2135
|
+
value,
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
case "select":
|
|
2139
|
+
return (
|
|
2140
|
+
<Select
|
|
2141
|
+
key={id}
|
|
2142
|
+
layout={isInInlineLayoutGroup ? "stacked" : undefined}
|
|
2143
|
+
name={name}
|
|
2144
|
+
onValueChange={commit}
|
|
2145
|
+
options={control.options ?? []}
|
|
2146
|
+
value={asString(value, control.options?.[0]?.value ?? "")}
|
|
2147
|
+
/>
|
|
2148
|
+
);
|
|
2149
|
+
|
|
2150
|
+
case "settingsTransfer":
|
|
2151
|
+
return (
|
|
2152
|
+
<PanelActions
|
|
2153
|
+
actions={[
|
|
2154
|
+
{
|
|
2155
|
+
icon: "upload-simple",
|
|
2156
|
+
name: "Export Settings",
|
|
2157
|
+
onClick: () => downloadToolcraftSettings(state),
|
|
2158
|
+
variant: "outline",
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
icon: "download-simple",
|
|
2162
|
+
name: "Import Settings",
|
|
2163
|
+
onClick: () => {
|
|
2164
|
+
void importToolcraftSettings({ dispatch, state });
|
|
2165
|
+
},
|
|
2166
|
+
variant: "outline",
|
|
2167
|
+
},
|
|
2168
|
+
]}
|
|
2169
|
+
key={id}
|
|
2170
|
+
columns={2}
|
|
2171
|
+
/>
|
|
2172
|
+
);
|
|
2173
|
+
|
|
2174
|
+
case "slider":
|
|
2175
|
+
return withKeyframeLabelAction({
|
|
2176
|
+
children: (
|
|
2177
|
+
<Slider
|
|
2178
|
+
baseValue={asNumber(control.defaultValue, control.min ?? 0)}
|
|
2179
|
+
disabled={control.disabled}
|
|
2180
|
+
key={id}
|
|
2181
|
+
markerCount={getControlMarkerCount(control)}
|
|
2182
|
+
max={control.max ?? 100}
|
|
2183
|
+
min={control.min ?? 0}
|
|
2184
|
+
name={name}
|
|
2185
|
+
onValueChange={commit}
|
|
2186
|
+
step={control.step ?? 1}
|
|
2187
|
+
unit={control.unit}
|
|
2188
|
+
value={asNumber(
|
|
2189
|
+
value,
|
|
2190
|
+
asNumber(control.defaultValue, control.min ?? 0),
|
|
2191
|
+
)}
|
|
2192
|
+
valueLabel={control.valueLabel}
|
|
2193
|
+
variant={
|
|
2194
|
+
control.variant === "discrete" ? "discrete" : "continuous"
|
|
2195
|
+
}
|
|
2196
|
+
/>
|
|
2197
|
+
),
|
|
2198
|
+
control,
|
|
2199
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2200
|
+
name,
|
|
2201
|
+
providerKey: id,
|
|
2202
|
+
value,
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
case "switch":
|
|
2206
|
+
return (
|
|
2207
|
+
<Switch
|
|
2208
|
+
checked={asBoolean(value)}
|
|
2209
|
+
key={id}
|
|
2210
|
+
name={name}
|
|
2211
|
+
onCheckedChange={commit}
|
|
2212
|
+
showLabel={control.label !== false}
|
|
2213
|
+
/>
|
|
2214
|
+
);
|
|
2215
|
+
|
|
2216
|
+
case "text":
|
|
2217
|
+
return withKeyframeLabelAction({
|
|
2218
|
+
children: (
|
|
2219
|
+
<TextInput
|
|
2220
|
+
commitOnBlur={shouldCommitTextControlOnBlur(control)}
|
|
2221
|
+
defaultValue={asString(control.defaultValue, asString(value))}
|
|
2222
|
+
key={id}
|
|
2223
|
+
name={name}
|
|
2224
|
+
onValueChange={commit}
|
|
2225
|
+
value={asString(value)}
|
|
2226
|
+
/>
|
|
2227
|
+
),
|
|
2228
|
+
control,
|
|
2229
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2230
|
+
name,
|
|
2231
|
+
providerKey: id,
|
|
2232
|
+
value,
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
case "vector": {
|
|
2236
|
+
const vectorValue = asVectorValue(value);
|
|
2237
|
+
|
|
2238
|
+
return withKeyframeLabelAction({
|
|
2239
|
+
children: (
|
|
2240
|
+
<Vector
|
|
2241
|
+
defaultValue={asVectorValue(control.defaultValue)}
|
|
2242
|
+
key={id}
|
|
2243
|
+
name={name}
|
|
2244
|
+
onValueChange={commit}
|
|
2245
|
+
padShape={vectorPadShape}
|
|
2246
|
+
padVariant={asVectorPadVariant(control.variant)}
|
|
2247
|
+
x={vectorValue.x}
|
|
2248
|
+
xLabel={control.xLabel}
|
|
2249
|
+
y={vectorValue.y}
|
|
2250
|
+
yLabel={control.yLabel}
|
|
2251
|
+
/>
|
|
2252
|
+
),
|
|
2253
|
+
control,
|
|
2254
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2255
|
+
name,
|
|
2256
|
+
providerKey: id,
|
|
2257
|
+
value,
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
default: {
|
|
2262
|
+
const CustomControl = controlRenderers?.[control.type];
|
|
2263
|
+
|
|
2264
|
+
if (!CustomControl) {
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
return withKeyframeLabelAction({
|
|
2269
|
+
children: (
|
|
2270
|
+
<React.Fragment key={id}>
|
|
2271
|
+
{CustomControl({
|
|
2272
|
+
control,
|
|
2273
|
+
controlId: id,
|
|
2274
|
+
dispatch,
|
|
2275
|
+
keyframeAction: getKeyframeLabelAction(control, name, value),
|
|
2276
|
+
name,
|
|
2277
|
+
setValue: commit,
|
|
2278
|
+
state,
|
|
2279
|
+
value,
|
|
2280
|
+
})}
|
|
2281
|
+
</React.Fragment>
|
|
2282
|
+
),
|
|
2283
|
+
control,
|
|
2284
|
+
disableAction: usesHeaderKeyframeAction,
|
|
2285
|
+
name,
|
|
2286
|
+
providerKey: id,
|
|
2287
|
+
value,
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
})();
|
|
2292
|
+
|
|
2293
|
+
return {
|
|
2294
|
+
ids,
|
|
2295
|
+
node: withCompoundControlSectionDivider({
|
|
2296
|
+
children: withControlLabelHelp({
|
|
2297
|
+
children: node,
|
|
2298
|
+
control,
|
|
2299
|
+
label: name,
|
|
2300
|
+
providerKey: id,
|
|
2301
|
+
}),
|
|
2302
|
+
control,
|
|
2303
|
+
}),
|
|
2304
|
+
};
|
|
2305
|
+
}),
|
|
2306
|
+
})}
|
|
2307
|
+
</PanelSection>
|
|
2308
|
+
);
|
|
2309
|
+
})}
|
|
2310
|
+
</Panel>
|
|
2311
|
+
);
|
|
2312
|
+
|
|
2313
|
+
if (placement === "surface") {
|
|
2314
|
+
return panel;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
return (
|
|
2318
|
+
<PanelContainer
|
|
2319
|
+
onPanelStateChange={onPanelStateChange}
|
|
2320
|
+
panelState={panelState}
|
|
2321
|
+
panelType="controls"
|
|
2322
|
+
placement={placement}
|
|
2323
|
+
>
|
|
2324
|
+
{panel}
|
|
2325
|
+
</PanelContainer>
|
|
2326
|
+
);
|
|
2327
|
+
}
|