@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,1442 @@
|
|
|
1
|
+
import { TOOLCRAFT_COMPONENT_CONTRACTS } from "../contracts/component-contracts";
|
|
2
|
+
import type {
|
|
3
|
+
ToolcraftAssemblyCapability,
|
|
4
|
+
ToolcraftAssemblyCommand,
|
|
5
|
+
ToolcraftAssemblyComponentId,
|
|
6
|
+
ToolcraftAssemblyContract,
|
|
7
|
+
ToolcraftAssemblyPanelContract,
|
|
8
|
+
ToolcraftAppSchema,
|
|
9
|
+
ToolcraftControlLayoutGroupSchema,
|
|
10
|
+
ToolcraftControlSectionSchema,
|
|
11
|
+
ToolcraftCanvasSize,
|
|
12
|
+
ToolcraftCanvasSizingSchema,
|
|
13
|
+
ToolcraftControlSchema,
|
|
14
|
+
ToolcraftControlsPanelSchema,
|
|
15
|
+
ToolcraftSettingsTransferSchema,
|
|
16
|
+
ToolcraftTimelinePanelSchema,
|
|
17
|
+
ToolcraftToolbarSchema,
|
|
18
|
+
ResolvedToolcraftPanelsSchema,
|
|
19
|
+
ResolvedToolcraftSettingsTransferSchema,
|
|
20
|
+
ResolvedToolcraftTimelinePanelSchema,
|
|
21
|
+
ResolvedToolcraftAppSchema,
|
|
22
|
+
} from "./types";
|
|
23
|
+
|
|
24
|
+
const defaultCanvasSize = {
|
|
25
|
+
height: 1024,
|
|
26
|
+
unit: "px",
|
|
27
|
+
width: 1024,
|
|
28
|
+
} satisfies ToolcraftCanvasSize;
|
|
29
|
+
|
|
30
|
+
type ResolvedCanvas = ResolvedToolcraftAppSchema["canvas"];
|
|
31
|
+
type ResolvedExport = ResolvedToolcraftAppSchema["export"];
|
|
32
|
+
type ResolvedToolbar = Required<ToolcraftToolbarSchema>;
|
|
33
|
+
type PanelContract = {
|
|
34
|
+
capabilities?: readonly string[];
|
|
35
|
+
defaultPlacement: ToolcraftAssemblyPanelContract["defaultPlacement"];
|
|
36
|
+
snapEdges: ToolcraftAssemblyPanelContract["snapEdges"];
|
|
37
|
+
visualComponent: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const canvasSizeControlTargets = {
|
|
41
|
+
height: "canvas.size.height",
|
|
42
|
+
width: "canvas.size.width",
|
|
43
|
+
} as const;
|
|
44
|
+
const maxAutoInlineControlLabelLength = 18;
|
|
45
|
+
const settingsTransferTarget = "runtime.settingsTransfer";
|
|
46
|
+
const runtimeSetupSectionTitle = "Setup";
|
|
47
|
+
const settingsTransferHeavyControlTypes = new Set([
|
|
48
|
+
"channelMixer",
|
|
49
|
+
"code",
|
|
50
|
+
"colorOpacity",
|
|
51
|
+
"curves",
|
|
52
|
+
"fileDrop",
|
|
53
|
+
"fontPicker",
|
|
54
|
+
"gradient",
|
|
55
|
+
"imagePicker",
|
|
56
|
+
"palette",
|
|
57
|
+
"rangeSlider",
|
|
58
|
+
"vector",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export type ToolcraftSettingsTransferEligibilityReason =
|
|
62
|
+
| "control-count"
|
|
63
|
+
| "score"
|
|
64
|
+
| "section-count";
|
|
65
|
+
|
|
66
|
+
export type ToolcraftSettingsTransferEligibility = {
|
|
67
|
+
controlCount: number;
|
|
68
|
+
eligible: boolean;
|
|
69
|
+
reasons: ToolcraftSettingsTransferEligibilityReason[];
|
|
70
|
+
score: number;
|
|
71
|
+
sectionCount: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type ToolcraftControlActionSchema = NonNullable<
|
|
75
|
+
ToolcraftControlSchema["actions"]
|
|
76
|
+
>[number];
|
|
77
|
+
|
|
78
|
+
function unique<const Value extends string>(values: readonly Value[]): Value[] {
|
|
79
|
+
return Array.from(new Set(values));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertNever(value: never): never {
|
|
83
|
+
throw new Error(`Unsupported Toolcraft template persistence storage: ${String(value)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolvePersistence(
|
|
87
|
+
persistence: ToolcraftAppSchema["persistence"],
|
|
88
|
+
): ResolvedToolcraftAppSchema["persistence"] {
|
|
89
|
+
switch (persistence?.storage) {
|
|
90
|
+
case undefined:
|
|
91
|
+
case "none":
|
|
92
|
+
return { storage: "none" };
|
|
93
|
+
case "localStorage":
|
|
94
|
+
return persistence;
|
|
95
|
+
default:
|
|
96
|
+
return assertNever(persistence);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function slugifySettingsTransferAppId(value: string | undefined): string {
|
|
101
|
+
const slug = value
|
|
102
|
+
?.trim()
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
105
|
+
.replace(/^-+|-+$/g, "");
|
|
106
|
+
|
|
107
|
+
return slug || "toolcraft-app";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getSettingsTransferMode(
|
|
111
|
+
settingsTransfer: ToolcraftSettingsTransferSchema | undefined,
|
|
112
|
+
): "auto" | boolean {
|
|
113
|
+
if (typeof settingsTransfer === "object" && settingsTransfer !== null) {
|
|
114
|
+
return settingsTransfer.enabled ?? "auto";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return settingsTransfer ?? "auto";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getSettingsTransferObject(
|
|
121
|
+
settingsTransfer: ToolcraftSettingsTransferSchema | undefined,
|
|
122
|
+
): Extract<ToolcraftSettingsTransferSchema, object> | undefined {
|
|
123
|
+
return typeof settingsTransfer === "object" && settingsTransfer !== null
|
|
124
|
+
? settingsTransfer
|
|
125
|
+
: undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getSettingsTransferAppId({
|
|
129
|
+
controls,
|
|
130
|
+
persistence,
|
|
131
|
+
settingsTransfer,
|
|
132
|
+
}: {
|
|
133
|
+
controls: ToolcraftControlsPanelSchema | undefined;
|
|
134
|
+
persistence: ResolvedToolcraftAppSchema["persistence"];
|
|
135
|
+
settingsTransfer: ToolcraftSettingsTransferSchema | undefined;
|
|
136
|
+
}): string {
|
|
137
|
+
const objectSchema = getSettingsTransferObject(settingsTransfer);
|
|
138
|
+
|
|
139
|
+
if (objectSchema?.appId) {
|
|
140
|
+
return slugifySettingsTransferAppId(objectSchema.appId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (persistence.storage === "localStorage") {
|
|
144
|
+
const match = /^toolcraft:(.+):state:v\d+$/u.exec(persistence.key);
|
|
145
|
+
|
|
146
|
+
if (match?.[1]) {
|
|
147
|
+
return slugifySettingsTransferAppId(match[1]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return slugifySettingsTransferAppId(controls?.title);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getSettingsTransferFileName({
|
|
155
|
+
appId,
|
|
156
|
+
settingsTransfer,
|
|
157
|
+
}: {
|
|
158
|
+
appId: string;
|
|
159
|
+
settingsTransfer: ToolcraftSettingsTransferSchema | undefined;
|
|
160
|
+
}): string {
|
|
161
|
+
const explicitFileName = getSettingsTransferObject(settingsTransfer)?.fileName?.trim();
|
|
162
|
+
|
|
163
|
+
if (explicitFileName) {
|
|
164
|
+
return explicitFileName.endsWith(".json") ? explicitFileName : `${explicitFileName}.json`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return `${appId}-settings.json`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveCanvasSizing(
|
|
171
|
+
canvas: ToolcraftAppSchema["canvas"],
|
|
172
|
+
): ToolcraftCanvasSizingSchema {
|
|
173
|
+
if (canvas.sizing) {
|
|
174
|
+
return canvas.sizing;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (canvas.size) {
|
|
178
|
+
return { mode: "editable-output" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (canvas.upload) {
|
|
182
|
+
return { mode: "intrinsic-media" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { mode: "intrinsic-media" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveExport(
|
|
189
|
+
exportSchema: ToolcraftAppSchema["export"],
|
|
190
|
+
): ResolvedExport {
|
|
191
|
+
return {
|
|
192
|
+
png: {
|
|
193
|
+
background: exportSchema?.png?.background ?? "include",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getPanelDragMode(
|
|
199
|
+
contract: { capabilities?: readonly string[] },
|
|
200
|
+
): ToolcraftAssemblyPanelContract["dragMode"] {
|
|
201
|
+
return contract.capabilities?.includes("dragMode:handle") ? "handle" : "panel";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createPanelAssemblyContract({
|
|
205
|
+
capabilities = [],
|
|
206
|
+
commands = [],
|
|
207
|
+
contract,
|
|
208
|
+
enabled,
|
|
209
|
+
}: {
|
|
210
|
+
capabilities?: readonly ToolcraftAssemblyCapability[];
|
|
211
|
+
commands?: readonly ToolcraftAssemblyCommand[];
|
|
212
|
+
contract: PanelContract;
|
|
213
|
+
enabled: boolean;
|
|
214
|
+
}): ToolcraftAssemblyPanelContract {
|
|
215
|
+
const panelCapabilities = enabled
|
|
216
|
+
? unique<ToolcraftAssemblyCapability>([
|
|
217
|
+
"panels.draggable",
|
|
218
|
+
"panels.snap",
|
|
219
|
+
"panels.doubleClickReset",
|
|
220
|
+
...capabilities,
|
|
221
|
+
])
|
|
222
|
+
: [];
|
|
223
|
+
const panelCommands = enabled
|
|
224
|
+
? unique<ToolcraftAssemblyCommand>(["panels.setOffset", "panels.resetOffset", ...commands])
|
|
225
|
+
: [];
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
capabilities: panelCapabilities,
|
|
229
|
+
commands: panelCommands,
|
|
230
|
+
defaultPlacement: contract.defaultPlacement,
|
|
231
|
+
dragMode: getPanelDragMode(contract),
|
|
232
|
+
enabled,
|
|
233
|
+
requiredWrapper: "PanelHost",
|
|
234
|
+
snapEdges: contract.snapEdges,
|
|
235
|
+
visualComponent: contract.visualComponent,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function createToolcraftAssembly({
|
|
240
|
+
canvas,
|
|
241
|
+
panels,
|
|
242
|
+
toolbar,
|
|
243
|
+
}: {
|
|
244
|
+
canvas: ResolvedCanvas;
|
|
245
|
+
panels: ResolvedToolcraftPanelsSchema;
|
|
246
|
+
toolbar: ResolvedToolbar;
|
|
247
|
+
}): ToolcraftAssemblyContract {
|
|
248
|
+
const components: ToolcraftAssemblyComponentId[] = [];
|
|
249
|
+
const capabilities: ToolcraftAssemblyCapability[] = [];
|
|
250
|
+
const commands: ToolcraftAssemblyCommand[] = [];
|
|
251
|
+
const toolbarEnabled = toolbar.history || toolbar.radar || toolbar.theme || toolbar.zoom;
|
|
252
|
+
const canvasEditableSize = canvas.sizing.mode === "editable-output";
|
|
253
|
+
|
|
254
|
+
if (canvas.enabled) {
|
|
255
|
+
components.push("canvas");
|
|
256
|
+
|
|
257
|
+
if (canvasEditableSize) {
|
|
258
|
+
capabilities.push("canvas.editableSize");
|
|
259
|
+
commands.push("canvas.setSize");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (canvas.draggable) {
|
|
263
|
+
capabilities.push("canvas.draggable");
|
|
264
|
+
commands.push("canvas.panBy", "canvas.setOffset", "canvas.setViewport");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (canvas.upload) {
|
|
268
|
+
capabilities.push("canvas.upload");
|
|
269
|
+
commands.push("media.delete", "media.import");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const controlsPanel = panels.controls
|
|
274
|
+
? createPanelAssemblyContract({
|
|
275
|
+
capabilities: ["controls.panel", "controls.defaults"],
|
|
276
|
+
commands: ["controls.apply", "controls.reset", "controls.setValue"],
|
|
277
|
+
contract: TOOLCRAFT_COMPONENT_CONTRACTS.controlsPanel,
|
|
278
|
+
enabled: true,
|
|
279
|
+
})
|
|
280
|
+
: undefined;
|
|
281
|
+
|
|
282
|
+
if (controlsPanel) {
|
|
283
|
+
components.push("controlsPanel");
|
|
284
|
+
capabilities.push(...controlsPanel.capabilities);
|
|
285
|
+
commands.push(...controlsPanel.commands);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const layersPanel = panels.layers
|
|
289
|
+
? createPanelAssemblyContract({
|
|
290
|
+
capabilities: [
|
|
291
|
+
"layers.groups",
|
|
292
|
+
"layers.panel",
|
|
293
|
+
"layers.selection",
|
|
294
|
+
"layers.visibility",
|
|
295
|
+
],
|
|
296
|
+
commands: [
|
|
297
|
+
"layers.add",
|
|
298
|
+
"layers.delete",
|
|
299
|
+
"layers.moveToGroup",
|
|
300
|
+
"layers.rename",
|
|
301
|
+
"layers.reorder",
|
|
302
|
+
"layers.select",
|
|
303
|
+
"layers.toggleCollapsed",
|
|
304
|
+
"layers.toggleVisibility",
|
|
305
|
+
],
|
|
306
|
+
contract: TOOLCRAFT_COMPONENT_CONTRACTS.layersPanel,
|
|
307
|
+
enabled: true,
|
|
308
|
+
})
|
|
309
|
+
: undefined;
|
|
310
|
+
|
|
311
|
+
if (layersPanel) {
|
|
312
|
+
components.push("layersPanel");
|
|
313
|
+
capabilities.push(...layersPanel.capabilities);
|
|
314
|
+
commands.push(...layersPanel.commands);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const timelineKeyframesEnabled = panels.timeline?.mode === "keyframes";
|
|
318
|
+
const timelinePanel = panels.timeline?.enabled
|
|
319
|
+
? createPanelAssemblyContract({
|
|
320
|
+
capabilities: [
|
|
321
|
+
"timeline.duration",
|
|
322
|
+
"timeline.panel",
|
|
323
|
+
"timeline.playback",
|
|
324
|
+
...(timelineKeyframesEnabled ? (["timeline.keyframes"] as const) : []),
|
|
325
|
+
],
|
|
326
|
+
commands: [
|
|
327
|
+
"timeline.setCurrentTime",
|
|
328
|
+
"timeline.setDuration",
|
|
329
|
+
"timeline.setPlaying",
|
|
330
|
+
"timeline.toggleLoop",
|
|
331
|
+
"timeline.togglePlayback",
|
|
332
|
+
...(timelineKeyframesEnabled
|
|
333
|
+
? ([
|
|
334
|
+
"timeline.changeKeyframeEasing",
|
|
335
|
+
"timeline.deleteControlKeyframes",
|
|
336
|
+
"timeline.deleteKeyframe",
|
|
337
|
+
"timeline.moveKeyframe",
|
|
338
|
+
"timeline.selectKeyframe",
|
|
339
|
+
"timeline.setExpanded",
|
|
340
|
+
"timeline.toggleControlKeyframes",
|
|
341
|
+
"timeline.toggleExpanded",
|
|
342
|
+
] as const)
|
|
343
|
+
: []),
|
|
344
|
+
],
|
|
345
|
+
contract: TOOLCRAFT_COMPONENT_CONTRACTS.timelinePanel,
|
|
346
|
+
enabled: true,
|
|
347
|
+
})
|
|
348
|
+
: undefined;
|
|
349
|
+
|
|
350
|
+
if (timelinePanel) {
|
|
351
|
+
components.push("timelinePanel");
|
|
352
|
+
capabilities.push(...timelinePanel.capabilities);
|
|
353
|
+
commands.push(...timelinePanel.commands);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const toolbarCommands: ToolcraftAssemblyCommand[] = [];
|
|
357
|
+
const toolbarCapabilities: ToolcraftAssemblyCapability[] = [];
|
|
358
|
+
|
|
359
|
+
if (toolbar.history) {
|
|
360
|
+
toolbarCapabilities.push("history.undoRedo", "toolbar.history");
|
|
361
|
+
toolbarCommands.push("history.redo", "history.undo");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (toolbar.radar) {
|
|
365
|
+
toolbarCapabilities.push("toolbar.radar");
|
|
366
|
+
toolbarCommands.push("canvas.center");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (toolbar.theme) {
|
|
370
|
+
toolbarCapabilities.push("toolbar.theme");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (toolbar.zoom) {
|
|
374
|
+
toolbarCapabilities.push("toolbar.zoom");
|
|
375
|
+
toolbarCommands.push("canvas.zoomIn", "canvas.zoomOut", "canvas.zoomReset");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const toolbarPanel = createPanelAssemblyContract({
|
|
379
|
+
capabilities: toolbarCapabilities,
|
|
380
|
+
commands: toolbarCommands,
|
|
381
|
+
contract: TOOLCRAFT_COMPONENT_CONTRACTS.toolbar,
|
|
382
|
+
enabled: toolbarEnabled,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (toolbarEnabled) {
|
|
386
|
+
components.push("toolbar");
|
|
387
|
+
capabilities.push(...toolbarPanel.capabilities);
|
|
388
|
+
commands.push(...toolbarPanel.commands);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
capabilities: unique(capabilities),
|
|
393
|
+
commands: unique(commands),
|
|
394
|
+
components: unique(components),
|
|
395
|
+
surfaces: {
|
|
396
|
+
canvas: {
|
|
397
|
+
capabilities: canvas.enabled
|
|
398
|
+
? unique<ToolcraftAssemblyCapability>([
|
|
399
|
+
...(canvasEditableSize ? (["canvas.editableSize"] as const) : []),
|
|
400
|
+
...(canvas.draggable ? (["canvas.draggable"] as const) : []),
|
|
401
|
+
...(canvas.upload ? (["canvas.upload"] as const) : []),
|
|
402
|
+
])
|
|
403
|
+
: [],
|
|
404
|
+
commands: canvas.enabled
|
|
405
|
+
? unique<ToolcraftAssemblyCommand>([
|
|
406
|
+
...(canvasEditableSize ? (["canvas.setSize"] as const) : []),
|
|
407
|
+
...(canvas.draggable
|
|
408
|
+
? (["canvas.panBy", "canvas.setOffset", "canvas.setViewport"] as const)
|
|
409
|
+
: []),
|
|
410
|
+
...(canvas.upload ? (["media.delete", "media.import"] as const) : []),
|
|
411
|
+
])
|
|
412
|
+
: [],
|
|
413
|
+
enabled: canvas.enabled,
|
|
414
|
+
visualComponent: "CanvasShell",
|
|
415
|
+
},
|
|
416
|
+
panels: {
|
|
417
|
+
controls: controlsPanel,
|
|
418
|
+
layers: layersPanel,
|
|
419
|
+
timeline: timelinePanel,
|
|
420
|
+
toolbar: toolbarPanel,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function hasControlTarget(
|
|
427
|
+
panels: ToolcraftAppSchema["panels"],
|
|
428
|
+
target: string,
|
|
429
|
+
): boolean {
|
|
430
|
+
return (panels.controls?.sections ?? []).some((section) =>
|
|
431
|
+
Object.values(section.controls).some((control) => control.target === target),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function getControlDefaultSectionLayout(
|
|
436
|
+
control: ToolcraftControlSchema,
|
|
437
|
+
): "grouped" | "standalone" {
|
|
438
|
+
const contract = (
|
|
439
|
+
TOOLCRAFT_COMPONENT_CONTRACTS as Record<
|
|
440
|
+
string,
|
|
441
|
+
{ defaultSectionLayout?: "grouped" | "standalone"; kind?: string } | undefined
|
|
442
|
+
>
|
|
443
|
+
)[control.type];
|
|
444
|
+
|
|
445
|
+
return contract?.kind === "control" && contract.defaultSectionLayout
|
|
446
|
+
? contract.defaultSectionLayout
|
|
447
|
+
: "grouped";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function getControlSectionLayout(
|
|
451
|
+
control: ToolcraftControlSchema,
|
|
452
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
453
|
+
): "grouped" | "standalone" {
|
|
454
|
+
if (
|
|
455
|
+
(control.type === "color" || control.type === "colorOpacity") &&
|
|
456
|
+
entries.some(
|
|
457
|
+
([, entryControl]) =>
|
|
458
|
+
entryControl.type !== "color" &&
|
|
459
|
+
entryControl.type !== "colorOpacity" &&
|
|
460
|
+
getControlDefaultSectionLayout(entryControl) === "grouped",
|
|
461
|
+
)
|
|
462
|
+
) {
|
|
463
|
+
return "grouped";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return getControlDefaultSectionLayout(control);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function createControlsRecord(
|
|
470
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
471
|
+
): Record<string, ToolcraftControlSchema> {
|
|
472
|
+
return Object.fromEntries(
|
|
473
|
+
entries.map(([id, control]) => [id, normalizeControlSchema(control)]),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function isSliderLikeControl(control: ToolcraftControlSchema): boolean {
|
|
478
|
+
return control.type === "slider" || control.type === "rangeSlider";
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getStepMarkerCount(control: ToolcraftControlSchema): number | undefined {
|
|
482
|
+
if (
|
|
483
|
+
typeof control.step !== "number" ||
|
|
484
|
+
typeof control.min !== "number" ||
|
|
485
|
+
typeof control.max !== "number" ||
|
|
486
|
+
!Number.isFinite(control.step) ||
|
|
487
|
+
!Number.isFinite(control.min) ||
|
|
488
|
+
!Number.isFinite(control.max) ||
|
|
489
|
+
control.step <= 0 ||
|
|
490
|
+
control.max <= control.min
|
|
491
|
+
) {
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const rawStepCount = (control.max - control.min) / control.step;
|
|
496
|
+
const roundedStepCount = Math.round(rawStepCount);
|
|
497
|
+
const stepCount =
|
|
498
|
+
Math.abs(rawStepCount - roundedStepCount) < Number.EPSILON * 100
|
|
499
|
+
? roundedStepCount
|
|
500
|
+
: Math.floor(rawStepCount) + 1;
|
|
501
|
+
|
|
502
|
+
return Math.max(2, stepCount + 1);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function normalizeControlSchema(
|
|
506
|
+
control: ToolcraftControlSchema,
|
|
507
|
+
): ToolcraftControlSchema {
|
|
508
|
+
if (
|
|
509
|
+
!isSliderLikeControl(control) ||
|
|
510
|
+
typeof control.step !== "number" ||
|
|
511
|
+
control.variant !== "discrete"
|
|
512
|
+
) {
|
|
513
|
+
return control;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
...control,
|
|
518
|
+
markerCount: getStepMarkerCount(control) ?? control.markerCount,
|
|
519
|
+
variant: "discrete",
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function filterLayoutGroupsForControlIds(
|
|
524
|
+
layoutGroups: readonly ToolcraftControlLayoutGroupSchema[] | undefined,
|
|
525
|
+
controlIds: ReadonlySet<string>,
|
|
526
|
+
): ToolcraftControlLayoutGroupSchema[] {
|
|
527
|
+
return (layoutGroups ?? [])
|
|
528
|
+
.map((layoutGroup) => ({
|
|
529
|
+
...layoutGroup,
|
|
530
|
+
controls: layoutGroup.controls.filter((controlId) => controlIds.has(controlId)),
|
|
531
|
+
}))
|
|
532
|
+
.filter((layoutGroup) => layoutGroup.controls.length > 1);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function hasControlEntries(
|
|
536
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
537
|
+
): boolean {
|
|
538
|
+
return entries.length > 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isPanelActionsControl(control: ToolcraftControlSchema): boolean {
|
|
542
|
+
return control.type === "panelActions";
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isSettingsTransferControl(control: ToolcraftControlSchema): boolean {
|
|
546
|
+
return control.type === "settingsTransfer";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isRuntimeOnlyActionControl(control: ToolcraftControlSchema): boolean {
|
|
550
|
+
return isPanelActionsControl(control) || isSettingsTransferControl(control);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isSettingsTransferEligibilityControl(control: ToolcraftControlSchema): boolean {
|
|
554
|
+
return (
|
|
555
|
+
!isRuntimeOnlyActionControl(control) &&
|
|
556
|
+
control.target !== canvasSizeControlTargets.width &&
|
|
557
|
+
control.target !== canvasSizeControlTargets.height
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function getSettingsTransferComplexityScore({
|
|
562
|
+
panels,
|
|
563
|
+
}: {
|
|
564
|
+
panels: ToolcraftAppSchema["panels"];
|
|
565
|
+
}): {
|
|
566
|
+
controlCount: number;
|
|
567
|
+
score: number;
|
|
568
|
+
sectionCount: number;
|
|
569
|
+
} {
|
|
570
|
+
const sections = panels.controls?.sections ?? [];
|
|
571
|
+
let controlCount = 0;
|
|
572
|
+
let score = 0;
|
|
573
|
+
|
|
574
|
+
for (const section of sections) {
|
|
575
|
+
for (const control of Object.values(section.controls)) {
|
|
576
|
+
if (!isSettingsTransferEligibilityControl(control)) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
controlCount += 1;
|
|
581
|
+
score += settingsTransferHeavyControlTypes.has(control.type) ? 3 : 1;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const sectionCount = sections.filter((section) =>
|
|
586
|
+
Object.values(section.controls).some((control) =>
|
|
587
|
+
isSettingsTransferEligibilityControl(control),
|
|
588
|
+
),
|
|
589
|
+
).length;
|
|
590
|
+
|
|
591
|
+
score += Math.max(0, sectionCount - 4);
|
|
592
|
+
|
|
593
|
+
if (
|
|
594
|
+
panels.timeline === true ||
|
|
595
|
+
(typeof panels.timeline === "object" && panels.timeline.enabled !== false)
|
|
596
|
+
) {
|
|
597
|
+
score += 2;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (panels.layers) {
|
|
601
|
+
score += 2;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return { controlCount, score, sectionCount };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function getToolcraftSettingsTransferEligibility({
|
|
608
|
+
panels,
|
|
609
|
+
}: {
|
|
610
|
+
panels: ToolcraftAppSchema["panels"];
|
|
611
|
+
}): ToolcraftSettingsTransferEligibility {
|
|
612
|
+
const { controlCount, score, sectionCount } = getSettingsTransferComplexityScore({
|
|
613
|
+
panels,
|
|
614
|
+
});
|
|
615
|
+
const reasons: ToolcraftSettingsTransferEligibilityReason[] = [];
|
|
616
|
+
|
|
617
|
+
if (controlCount >= 12) {
|
|
618
|
+
reasons.push("control-count");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (sectionCount >= 5) {
|
|
622
|
+
reasons.push("section-count");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (score >= 18) {
|
|
626
|
+
reasons.push("score");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
controlCount,
|
|
631
|
+
eligible: reasons.length > 0,
|
|
632
|
+
reasons,
|
|
633
|
+
score,
|
|
634
|
+
sectionCount,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function shouldAutoEnableSettingsTransfer({
|
|
639
|
+
panels,
|
|
640
|
+
}: {
|
|
641
|
+
panels: ToolcraftAppSchema["panels"];
|
|
642
|
+
}): boolean {
|
|
643
|
+
return getToolcraftSettingsTransferEligibility({ panels }).eligible;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function resolveSettingsTransfer({
|
|
647
|
+
controls,
|
|
648
|
+
panels,
|
|
649
|
+
persistence,
|
|
650
|
+
settingsTransfer,
|
|
651
|
+
}: {
|
|
652
|
+
controls: ToolcraftControlsPanelSchema | undefined;
|
|
653
|
+
panels: ToolcraftAppSchema["panels"];
|
|
654
|
+
persistence: ResolvedToolcraftAppSchema["persistence"];
|
|
655
|
+
settingsTransfer: ToolcraftSettingsTransferSchema | undefined;
|
|
656
|
+
}): ResolvedToolcraftSettingsTransferSchema {
|
|
657
|
+
const mode = getSettingsTransferMode(settingsTransfer);
|
|
658
|
+
const appId = getSettingsTransferAppId({
|
|
659
|
+
controls,
|
|
660
|
+
persistence,
|
|
661
|
+
settingsTransfer,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
appId,
|
|
666
|
+
enabled:
|
|
667
|
+
Boolean(controls) &&
|
|
668
|
+
(mode === "auto" ? shouldAutoEnableSettingsTransfer({ panels }) : mode),
|
|
669
|
+
fileName: getSettingsTransferFileName({ appId, settingsTransfer }),
|
|
670
|
+
mode,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function createSettingsTransferSection(
|
|
675
|
+
settingsTransfer: ResolvedToolcraftSettingsTransferSchema,
|
|
676
|
+
): ToolcraftControlSectionSchema | null {
|
|
677
|
+
if (!settingsTransfer.enabled) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
controls: {
|
|
683
|
+
settingsTransfer: {
|
|
684
|
+
label: false,
|
|
685
|
+
target: settingsTransferTarget,
|
|
686
|
+
type: "settingsTransfer",
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
layout: "standalone",
|
|
690
|
+
title: runtimeSetupSectionTitle,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getCanvasSizeLayoutGroups(
|
|
695
|
+
sizeControlIds: readonly string[],
|
|
696
|
+
): ToolcraftControlSectionSchema["layoutGroups"] {
|
|
697
|
+
return sizeControlIds.length > 1
|
|
698
|
+
? [{ columns: 2, controls: [...sizeControlIds], layout: "inline" }]
|
|
699
|
+
: undefined;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function createCanvasSizeSection({
|
|
703
|
+
sizeControlIds,
|
|
704
|
+
sizeControls,
|
|
705
|
+
}: {
|
|
706
|
+
sizeControlIds: readonly string[];
|
|
707
|
+
sizeControls: ToolcraftControlSectionSchema["controls"];
|
|
708
|
+
}): ToolcraftControlSectionSchema {
|
|
709
|
+
return {
|
|
710
|
+
controls: sizeControls,
|
|
711
|
+
layoutGroups: getCanvasSizeLayoutGroups(sizeControlIds),
|
|
712
|
+
title: runtimeSetupSectionTitle,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
717
|
+
settingsTransferSection,
|
|
718
|
+
sizeControlIds,
|
|
719
|
+
sizeControls,
|
|
720
|
+
}: {
|
|
721
|
+
settingsTransferSection: ToolcraftControlSectionSchema;
|
|
722
|
+
sizeControlIds: readonly string[];
|
|
723
|
+
sizeControls: ToolcraftControlSectionSchema["controls"];
|
|
724
|
+
}): ToolcraftControlSectionSchema {
|
|
725
|
+
const canvasSizeLayoutGroups = getCanvasSizeLayoutGroups(sizeControlIds) ?? [];
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
...settingsTransferSection,
|
|
729
|
+
controls: {
|
|
730
|
+
...settingsTransferSection.controls,
|
|
731
|
+
...sizeControls,
|
|
732
|
+
},
|
|
733
|
+
layoutGroups:
|
|
734
|
+
canvasSizeLayoutGroups.length > 0 || settingsTransferSection.layoutGroups?.length
|
|
735
|
+
? [
|
|
736
|
+
...(settingsTransferSection.layoutGroups ?? []),
|
|
737
|
+
...canvasSizeLayoutGroups,
|
|
738
|
+
]
|
|
739
|
+
: undefined,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function isPrimaryPanelAction(action: ToolcraftControlActionSchema): boolean {
|
|
744
|
+
return typeof action !== "string" && action.variant !== "outline";
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function orderPanelActions(
|
|
748
|
+
actions: readonly ToolcraftControlActionSchema[],
|
|
749
|
+
): ToolcraftControlActionSchema[] {
|
|
750
|
+
if (actions.length !== 2) {
|
|
751
|
+
return [...actions];
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return [...actions].sort(
|
|
755
|
+
(left, right) => Number(isPrimaryPanelAction(left)) - Number(isPrimaryPanelAction(right)),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function createMergedPanelActionsControl(
|
|
760
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
761
|
+
): ToolcraftControlSchema | null {
|
|
762
|
+
const firstControl = entries[0]?.[1];
|
|
763
|
+
|
|
764
|
+
if (!firstControl) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const actions = entries.flatMap(([, control]) => [...(control.actions ?? [])]);
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
...firstControl,
|
|
772
|
+
actions: orderPanelActions(actions),
|
|
773
|
+
target: firstControl.target || "panel.actions",
|
|
774
|
+
type: "panelActions",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function splitControlsPanelActionSections(
|
|
779
|
+
sections: readonly ToolcraftControlSectionSchema[],
|
|
780
|
+
): {
|
|
781
|
+
bodySections: ToolcraftControlSectionSchema[];
|
|
782
|
+
stickyFooterSections: ToolcraftControlSectionSchema[];
|
|
783
|
+
} {
|
|
784
|
+
const bodySections: ToolcraftControlSectionSchema[] = [];
|
|
785
|
+
const stickyFooterSections: ToolcraftControlSectionSchema[] = [];
|
|
786
|
+
const stickyFooterActionEntries: [string, ToolcraftControlSchema][] = [];
|
|
787
|
+
|
|
788
|
+
for (const section of sections) {
|
|
789
|
+
if (section.actionGroup) {
|
|
790
|
+
const entries = Object.entries(section.controls);
|
|
791
|
+
const actionEntries = entries.filter(([, control]) => isPanelActionsControl(control));
|
|
792
|
+
const passthroughEntries = entries.filter(([, control]) => !isPanelActionsControl(control));
|
|
793
|
+
|
|
794
|
+
stickyFooterActionEntries.push(...actionEntries);
|
|
795
|
+
|
|
796
|
+
if (hasControlEntries(passthroughEntries)) {
|
|
797
|
+
stickyFooterSections.push({
|
|
798
|
+
...section,
|
|
799
|
+
controls: createControlsRecord(passthroughEntries),
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const bodyEntries: [string, ToolcraftControlSchema][] = [];
|
|
807
|
+
const actionEntries: [string, ToolcraftControlSchema][] = [];
|
|
808
|
+
|
|
809
|
+
for (const entry of Object.entries(section.controls)) {
|
|
810
|
+
const [, control] = entry;
|
|
811
|
+
|
|
812
|
+
if (isPanelActionsControl(control)) {
|
|
813
|
+
actionEntries.push(entry);
|
|
814
|
+
} else {
|
|
815
|
+
bodyEntries.push(entry);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (hasControlEntries(bodyEntries)) {
|
|
820
|
+
const controlIds = new Set(bodyEntries.map(([id]) => id));
|
|
821
|
+
const layoutGroups = filterLayoutGroupsForControlIds(section.layoutGroups, controlIds);
|
|
822
|
+
const title = getBodySectionTitleAfterActionSplit(section.title);
|
|
823
|
+
|
|
824
|
+
bodySections.push({
|
|
825
|
+
...section,
|
|
826
|
+
controls: createControlsRecord(bodyEntries),
|
|
827
|
+
layoutGroups: layoutGroups.length > 0 ? layoutGroups : undefined,
|
|
828
|
+
title,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (hasControlEntries(actionEntries)) {
|
|
833
|
+
stickyFooterActionEntries.push(...actionEntries);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const mergedActionsControl = createMergedPanelActionsControl(stickyFooterActionEntries);
|
|
838
|
+
|
|
839
|
+
if (mergedActionsControl) {
|
|
840
|
+
stickyFooterSections.unshift({
|
|
841
|
+
actionGroup: "secondary",
|
|
842
|
+
controls: { footer: mergedActionsControl },
|
|
843
|
+
layout: "standalone",
|
|
844
|
+
title: "Export",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return { bodySections, stickyFooterSections };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function getBodySectionTitleAfterActionSplit(
|
|
852
|
+
title: ToolcraftControlSectionSchema["title"],
|
|
853
|
+
): ToolcraftControlSectionSchema["title"] {
|
|
854
|
+
if (!title) {
|
|
855
|
+
return title;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return isActionOrExportSectionTitle(title) ? undefined : title;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function isActionOrExportSectionTitle(title: string): boolean {
|
|
862
|
+
const normalizedTitle = title.trim().toLowerCase();
|
|
863
|
+
|
|
864
|
+
return normalizedTitle === "action" ||
|
|
865
|
+
normalizedTitle === "actions" ||
|
|
866
|
+
normalizedTitle === "export" ||
|
|
867
|
+
normalizedTitle === "exports";
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function isShortControlLabel(id: string, control: ToolcraftControlSchema): boolean {
|
|
871
|
+
const label = typeof control.label === "string" ? control.label : id;
|
|
872
|
+
|
|
873
|
+
return label.length <= maxAutoInlineControlLabelLength;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function isNumericTextControl(control: ToolcraftControlSchema): boolean {
|
|
877
|
+
if (control.type !== "text") {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (typeof control.defaultValue === "number") {
|
|
882
|
+
return Number.isFinite(control.defaultValue);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return (
|
|
886
|
+
typeof control.defaultValue === "string" &&
|
|
887
|
+
/^-?\d+(?:\.\d+)?(?:px|%|s)?$/u.test(control.defaultValue.trim())
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function isColorValueControl(control: ToolcraftControlSchema): boolean {
|
|
892
|
+
return control.type === "color" || control.type === "colorOpacity";
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function hasVisibleControlLabel(control: ToolcraftControlSchema): boolean {
|
|
896
|
+
return typeof control.label === "string" && control.label.trim().length > 0;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function shouldAutoInlineMixedFieldControls(
|
|
900
|
+
first: [string, ToolcraftControlSchema],
|
|
901
|
+
second: [string, ToolcraftControlSchema],
|
|
902
|
+
): boolean {
|
|
903
|
+
const [firstId, firstControl] = first;
|
|
904
|
+
const [secondId, secondControl] = second;
|
|
905
|
+
const isNumericColorPair =
|
|
906
|
+
(isNumericTextControl(firstControl) && isColorValueControl(secondControl)) ||
|
|
907
|
+
(isColorValueControl(firstControl) && isNumericTextControl(secondControl));
|
|
908
|
+
|
|
909
|
+
return (
|
|
910
|
+
isNumericColorPair &&
|
|
911
|
+
hasVisibleControlLabel(firstControl) &&
|
|
912
|
+
hasVisibleControlLabel(secondControl) &&
|
|
913
|
+
isShortControlLabel(firstId, firstControl) &&
|
|
914
|
+
isShortControlLabel(secondId, secondControl)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function shouldAutoInlineControls(
|
|
919
|
+
first: [string, ToolcraftControlSchema],
|
|
920
|
+
second: [string, ToolcraftControlSchema],
|
|
921
|
+
): boolean {
|
|
922
|
+
const [firstId, firstControl] = first;
|
|
923
|
+
const [secondId, secondControl] = second;
|
|
924
|
+
|
|
925
|
+
if (
|
|
926
|
+
isNumericTextControl(firstControl) &&
|
|
927
|
+
isNumericTextControl(secondControl) &&
|
|
928
|
+
isShortControlLabel(firstId, firstControl) &&
|
|
929
|
+
isShortControlLabel(secondId, secondControl)
|
|
930
|
+
) {
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return shouldAutoInlineMixedFieldControls(first, second);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function addAutoLayoutGroupsToSection(
|
|
938
|
+
section: ToolcraftControlSectionSchema,
|
|
939
|
+
): ToolcraftControlSectionSchema {
|
|
940
|
+
if (section.layout === "standalone" || section.actionGroup) {
|
|
941
|
+
return section;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const entries = Object.entries(section.controls);
|
|
945
|
+
const explicitLayoutGroups = section.layoutGroups ?? [];
|
|
946
|
+
const groupedControlIds = new Set<string>();
|
|
947
|
+
|
|
948
|
+
for (const layoutGroup of explicitLayoutGroups) {
|
|
949
|
+
for (const controlId of layoutGroup.controls) {
|
|
950
|
+
groupedControlIds.add(controlId);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const autoLayoutGroups: ToolcraftControlLayoutGroupSchema[] = [];
|
|
955
|
+
|
|
956
|
+
for (let index = 0; index < entries.length - 1; index += 1) {
|
|
957
|
+
const firstEntry = entries[index];
|
|
958
|
+
const secondEntry = entries[index + 1];
|
|
959
|
+
|
|
960
|
+
if (!firstEntry || !secondEntry) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const [firstId] = firstEntry;
|
|
965
|
+
const [secondId] = secondEntry;
|
|
966
|
+
|
|
967
|
+
if (groupedControlIds.has(firstId) || groupedControlIds.has(secondId)) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (!shouldAutoInlineControls(firstEntry, secondEntry)) {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
autoLayoutGroups.push({
|
|
976
|
+
columns: 2,
|
|
977
|
+
controls: [firstId, secondId],
|
|
978
|
+
layout: "inline",
|
|
979
|
+
});
|
|
980
|
+
groupedControlIds.add(firstId);
|
|
981
|
+
groupedControlIds.add(secondId);
|
|
982
|
+
index += 1;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const layoutGroups = [...explicitLayoutGroups, ...autoLayoutGroups];
|
|
986
|
+
|
|
987
|
+
return layoutGroups.length > 0
|
|
988
|
+
? {
|
|
989
|
+
...section,
|
|
990
|
+
layoutGroups,
|
|
991
|
+
}
|
|
992
|
+
: section;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function getImplicitStandaloneSectionTitle(
|
|
996
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
997
|
+
): string {
|
|
998
|
+
const names = entries
|
|
999
|
+
.map(([id, control]) => getSectionTitlePart(id, control, entries))
|
|
1000
|
+
.filter((name): name is string => Boolean(name));
|
|
1001
|
+
|
|
1002
|
+
if (names.length === 1) {
|
|
1003
|
+
return names[0];
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (names.length === 2) {
|
|
1007
|
+
return `${names[0]} & ${names[1]}`;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return getCommonTargetSectionTitle(entries) ?? "Appearance";
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function getSectionTitlePart(
|
|
1014
|
+
id: string,
|
|
1015
|
+
control: ToolcraftControlSchema,
|
|
1016
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
1017
|
+
): string | undefined {
|
|
1018
|
+
if (typeof control.label === "string" && control.label.trim()) {
|
|
1019
|
+
const label = control.label.trim();
|
|
1020
|
+
|
|
1021
|
+
if (!isGenericSectionTitle(label)) {
|
|
1022
|
+
return label;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const title = titleizeControlId(id);
|
|
1027
|
+
|
|
1028
|
+
if (title && !isGenericSectionTitle(title)) {
|
|
1029
|
+
return title;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (entries.length === 1) {
|
|
1033
|
+
return getControlTypeSectionTitle(control);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return undefined;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function getControlTypeSectionTitle(
|
|
1040
|
+
control: ToolcraftControlSchema,
|
|
1041
|
+
): string | undefined {
|
|
1042
|
+
switch (control.type) {
|
|
1043
|
+
case "channelMixer":
|
|
1044
|
+
return "Channels";
|
|
1045
|
+
case "curves":
|
|
1046
|
+
return "Curves";
|
|
1047
|
+
case "fontPicker":
|
|
1048
|
+
return "Typography";
|
|
1049
|
+
case "gradient":
|
|
1050
|
+
return "Gradient";
|
|
1051
|
+
case "palette":
|
|
1052
|
+
return "Palette";
|
|
1053
|
+
case "settingsTransfer":
|
|
1054
|
+
return "Settings";
|
|
1055
|
+
default:
|
|
1056
|
+
return undefined;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function getCommonTargetSectionTitle(
|
|
1061
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
1062
|
+
): string | undefined {
|
|
1063
|
+
const targetPrefixes = new Set(
|
|
1064
|
+
entries
|
|
1065
|
+
.map(([, control]) => control.target.split(".")[0]?.trim())
|
|
1066
|
+
.filter((prefix): prefix is string => Boolean(prefix)),
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
if (targetPrefixes.size !== 1) {
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const [targetPrefix] = targetPrefixes;
|
|
1074
|
+
const title = titlePrefixToSectionTitle(targetPrefix);
|
|
1075
|
+
|
|
1076
|
+
return title && !isGenericSectionTitle(title) ? title : undefined;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function titlePrefixToSectionTitle(prefix: string | undefined): string | undefined {
|
|
1080
|
+
if (!prefix) {
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
switch (prefix) {
|
|
1085
|
+
case "canvas":
|
|
1086
|
+
return runtimeSetupSectionTitle;
|
|
1087
|
+
case "runtime":
|
|
1088
|
+
return "Settings";
|
|
1089
|
+
case "style":
|
|
1090
|
+
return "Appearance";
|
|
1091
|
+
default:
|
|
1092
|
+
return titleizeControlId(prefix);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function isGenericColorSectionTitle(title: string): boolean {
|
|
1097
|
+
const normalizedTitle = title.trim().toLowerCase();
|
|
1098
|
+
|
|
1099
|
+
return normalizedTitle === "color" || normalizedTitle === "colors";
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function isGenericSectionTitle(title: string): boolean {
|
|
1103
|
+
const normalizedTitle = title.trim().toLowerCase();
|
|
1104
|
+
|
|
1105
|
+
return (
|
|
1106
|
+
normalizedTitle === "control" ||
|
|
1107
|
+
normalizedTitle === "controls" ||
|
|
1108
|
+
normalizedTitle === "setting" ||
|
|
1109
|
+
normalizedTitle === "settings" ||
|
|
1110
|
+
isGenericColorSectionTitle(title)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function isColorOnlySectionEntries(
|
|
1115
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
1116
|
+
): boolean {
|
|
1117
|
+
return entries.length > 0 && entries.every(([, control]) => control.type === "color");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function titleizeControlId(id: string): string | undefined {
|
|
1121
|
+
const title = id
|
|
1122
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
1123
|
+
.replace(/[_-]+/g, " ")
|
|
1124
|
+
.trim()
|
|
1125
|
+
.replace(/\s+/g, " ");
|
|
1126
|
+
|
|
1127
|
+
if (!title) {
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return title.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function withImplicitStandaloneSectionTitle(
|
|
1135
|
+
section: ToolcraftControlSectionSchema,
|
|
1136
|
+
entries: readonly [string, ToolcraftControlSchema][],
|
|
1137
|
+
): ToolcraftControlSectionSchema {
|
|
1138
|
+
if (section.title) {
|
|
1139
|
+
if (isColorOnlySectionEntries(entries) && isGenericColorSectionTitle(section.title)) {
|
|
1140
|
+
return { ...section, title: "Appearance" };
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return section;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const title = getImplicitStandaloneSectionTitle(entries);
|
|
1147
|
+
|
|
1148
|
+
return { ...section, title };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function normalizeMixedSectionLayout(
|
|
1152
|
+
section: ToolcraftControlSectionSchema,
|
|
1153
|
+
): ToolcraftControlSectionSchema[] {
|
|
1154
|
+
const entries = Object.entries(section.controls);
|
|
1155
|
+
|
|
1156
|
+
if (section.layout === "standalone") {
|
|
1157
|
+
return [
|
|
1158
|
+
addAutoLayoutGroupsToSection(
|
|
1159
|
+
withImplicitStandaloneSectionTitle(section, entries),
|
|
1160
|
+
),
|
|
1161
|
+
];
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (entries.length <= 1) {
|
|
1165
|
+
return [addAutoLayoutGroupsToSection(withImplicitStandaloneSectionTitle(section, entries))];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const layouts = entries.map(([, control]) => getControlSectionLayout(control, entries));
|
|
1169
|
+
const uniqueLayouts = new Set(layouts);
|
|
1170
|
+
|
|
1171
|
+
if (uniqueLayouts.size <= 1) {
|
|
1172
|
+
return [addAutoLayoutGroupsToSection(withImplicitStandaloneSectionTitle(section, entries))];
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const normalizedSections: ToolcraftControlSectionSchema[] = [];
|
|
1176
|
+
let currentLayout = layouts[0];
|
|
1177
|
+
let currentEntries: [string, ToolcraftControlSchema][] = [];
|
|
1178
|
+
|
|
1179
|
+
const pushCurrentSection = (): void => {
|
|
1180
|
+
if (!currentLayout || currentEntries.length === 0) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const controlIds = new Set(currentEntries.map(([id]) => id));
|
|
1185
|
+
|
|
1186
|
+
if (currentLayout === "standalone") {
|
|
1187
|
+
normalizedSections.push(
|
|
1188
|
+
withImplicitStandaloneSectionTitle(
|
|
1189
|
+
{
|
|
1190
|
+
controls: createControlsRecord(currentEntries),
|
|
1191
|
+
layout: "standalone",
|
|
1192
|
+
},
|
|
1193
|
+
currentEntries,
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
} else {
|
|
1197
|
+
normalizedSections.push(
|
|
1198
|
+
addAutoLayoutGroupsToSection({
|
|
1199
|
+
...section,
|
|
1200
|
+
controls: createControlsRecord(currentEntries),
|
|
1201
|
+
layoutGroups: filterLayoutGroupsForControlIds(section.layoutGroups, controlIds),
|
|
1202
|
+
}),
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
for (const [index, entry] of entries.entries()) {
|
|
1208
|
+
const layout = layouts[index] ?? "grouped";
|
|
1209
|
+
|
|
1210
|
+
if (layout !== currentLayout) {
|
|
1211
|
+
pushCurrentSection();
|
|
1212
|
+
currentLayout = layout;
|
|
1213
|
+
currentEntries = [];
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
currentEntries.push(entry);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
pushCurrentSection();
|
|
1220
|
+
|
|
1221
|
+
return normalizedSections;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function normalizeControlsPanelLayout(
|
|
1225
|
+
controls: ToolcraftControlsPanelSchema,
|
|
1226
|
+
): ToolcraftControlsPanelSchema {
|
|
1227
|
+
const { bodySections, stickyFooterSections } = splitControlsPanelActionSections(
|
|
1228
|
+
controls.sections,
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
return {
|
|
1232
|
+
...controls,
|
|
1233
|
+
sections: [
|
|
1234
|
+
...bodySections.flatMap(normalizeMixedSectionLayout),
|
|
1235
|
+
...stickyFooterSections.flatMap(normalizeMixedSectionLayout),
|
|
1236
|
+
],
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function normalizePanels({
|
|
1241
|
+
canvas,
|
|
1242
|
+
panels,
|
|
1243
|
+
settingsTransfer,
|
|
1244
|
+
}: {
|
|
1245
|
+
canvas: ResolvedCanvas;
|
|
1246
|
+
panels: ToolcraftAppSchema["panels"];
|
|
1247
|
+
settingsTransfer: ResolvedToolcraftSettingsTransferSchema;
|
|
1248
|
+
}): ResolvedToolcraftPanelsSchema {
|
|
1249
|
+
const normalizedTimeline = resolveTimelinePanel(panels.timeline);
|
|
1250
|
+
const normalizedPanels: ResolvedToolcraftPanelsSchema = {
|
|
1251
|
+
...(panels.controls ? { controls: panels.controls } : {}),
|
|
1252
|
+
...(panels.layers ? { layers: panels.layers } : {}),
|
|
1253
|
+
...(normalizedTimeline ? { timeline: normalizedTimeline } : {}),
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
if (!panels.controls) {
|
|
1257
|
+
return normalizedPanels;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const controls = { ...panels.controls };
|
|
1261
|
+
const settingsTransferSection = createSettingsTransferSection(settingsTransfer);
|
|
1262
|
+
|
|
1263
|
+
if (!canvas.enabled || canvas.sizing.mode !== "editable-output") {
|
|
1264
|
+
return {
|
|
1265
|
+
...normalizedPanels,
|
|
1266
|
+
controls: normalizeControlsPanelLayout({
|
|
1267
|
+
...controls,
|
|
1268
|
+
sections: [
|
|
1269
|
+
...(settingsTransferSection ? [settingsTransferSection] : []),
|
|
1270
|
+
...controls.sections,
|
|
1271
|
+
],
|
|
1272
|
+
}),
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const sizeControls: ToolcraftControlSectionSchema["controls"] = {};
|
|
1277
|
+
const sizeControlIds: string[] = [];
|
|
1278
|
+
|
|
1279
|
+
if (!hasControlTarget(panels, canvasSizeControlTargets.width)) {
|
|
1280
|
+
sizeControls.canvasWidth = {
|
|
1281
|
+
defaultValue: canvas.size.width,
|
|
1282
|
+
label: "Canvas width",
|
|
1283
|
+
performanceReason: "Canvas width changes output dimensions and renderer workload.",
|
|
1284
|
+
performanceRole: "workload",
|
|
1285
|
+
target: canvasSizeControlTargets.width,
|
|
1286
|
+
type: "text",
|
|
1287
|
+
};
|
|
1288
|
+
sizeControlIds.push("canvasWidth");
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (!hasControlTarget(panels, canvasSizeControlTargets.height)) {
|
|
1292
|
+
sizeControls.canvasHeight = {
|
|
1293
|
+
defaultValue: canvas.size.height,
|
|
1294
|
+
label: "Canvas height",
|
|
1295
|
+
performanceReason: "Canvas height changes output dimensions and renderer workload.",
|
|
1296
|
+
performanceRole: "workload",
|
|
1297
|
+
target: canvasSizeControlTargets.height,
|
|
1298
|
+
type: "text",
|
|
1299
|
+
};
|
|
1300
|
+
sizeControlIds.push("canvasHeight");
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (Object.keys(sizeControls).length === 0) {
|
|
1304
|
+
return {
|
|
1305
|
+
...normalizedPanels,
|
|
1306
|
+
controls: normalizeControlsPanelLayout({
|
|
1307
|
+
...controls,
|
|
1308
|
+
sections: [
|
|
1309
|
+
...(settingsTransferSection ? [settingsTransferSection] : []),
|
|
1310
|
+
...controls.sections,
|
|
1311
|
+
],
|
|
1312
|
+
}),
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const runtimeSettingsSection = settingsTransferSection
|
|
1317
|
+
? mergeCanvasSizeControlsIntoSettingsTransferSection({
|
|
1318
|
+
settingsTransferSection,
|
|
1319
|
+
sizeControlIds,
|
|
1320
|
+
sizeControls,
|
|
1321
|
+
})
|
|
1322
|
+
: createCanvasSizeSection({ sizeControlIds, sizeControls });
|
|
1323
|
+
|
|
1324
|
+
return {
|
|
1325
|
+
...normalizedPanels,
|
|
1326
|
+
controls: {
|
|
1327
|
+
...normalizeControlsPanelLayout({
|
|
1328
|
+
...controls,
|
|
1329
|
+
sections: [
|
|
1330
|
+
runtimeSettingsSection,
|
|
1331
|
+
...controls.sections,
|
|
1332
|
+
],
|
|
1333
|
+
}),
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function resolveTimelinePanel(
|
|
1339
|
+
timeline: ToolcraftTimelinePanelSchema | undefined,
|
|
1340
|
+
): ResolvedToolcraftTimelinePanelSchema | undefined {
|
|
1341
|
+
if (timeline === true) {
|
|
1342
|
+
return { enabled: true, mode: "keyframes" };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (!timeline || timeline.enabled === false) {
|
|
1346
|
+
return undefined;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return {
|
|
1350
|
+
enabled: true,
|
|
1351
|
+
mode: timeline.mode ?? "keyframes",
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function hasVisibleRuntimePanel({
|
|
1356
|
+
panels,
|
|
1357
|
+
toolbar,
|
|
1358
|
+
}: {
|
|
1359
|
+
panels: ResolvedToolcraftPanelsSchema;
|
|
1360
|
+
toolbar: ResolvedToolbar;
|
|
1361
|
+
}): boolean {
|
|
1362
|
+
return Boolean(
|
|
1363
|
+
panels.controls ||
|
|
1364
|
+
panels.layers ||
|
|
1365
|
+
panels.timeline ||
|
|
1366
|
+
toolbar.history ||
|
|
1367
|
+
toolbar.radar ||
|
|
1368
|
+
toolbar.theme ||
|
|
1369
|
+
toolbar.zoom,
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function assertPanelPersistenceContract({
|
|
1374
|
+
panels,
|
|
1375
|
+
persistence,
|
|
1376
|
+
toolbar,
|
|
1377
|
+
}: {
|
|
1378
|
+
panels: ResolvedToolcraftPanelsSchema;
|
|
1379
|
+
persistence: ResolvedToolcraftAppSchema["persistence"];
|
|
1380
|
+
toolbar: ResolvedToolbar;
|
|
1381
|
+
}): void {
|
|
1382
|
+
if (persistence.storage !== "localStorage" || !hasVisibleRuntimePanel({ panels, toolbar })) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (persistence.include.includes("panels")) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
'Toolcraft apps with visible runtime panels and localStorage persistence must include "panels" so dragged panel positions survive reload.',
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
export function defineToolcraft(schema: ToolcraftAppSchema): ResolvedToolcraftAppSchema {
|
|
1396
|
+
const canvasEnabled = schema.canvas.enabled;
|
|
1397
|
+
const canvasSize = schema.canvas.size;
|
|
1398
|
+
const canvasSizing = resolveCanvasSizing(schema.canvas);
|
|
1399
|
+
const persistence = resolvePersistence(schema.persistence);
|
|
1400
|
+
const settingsTransfer = resolveSettingsTransfer({
|
|
1401
|
+
controls: schema.panels.controls,
|
|
1402
|
+
panels: schema.panels,
|
|
1403
|
+
persistence,
|
|
1404
|
+
settingsTransfer: schema.settingsTransfer,
|
|
1405
|
+
});
|
|
1406
|
+
const canvas = {
|
|
1407
|
+
...schema.canvas,
|
|
1408
|
+
draggable: canvasEnabled ? (schema.canvas.draggable ?? true) : false,
|
|
1409
|
+
size: canvasSize ?? defaultCanvasSize,
|
|
1410
|
+
sizeSource: canvasSize ? ("app" as const) : ("runtime-default" as const),
|
|
1411
|
+
sizing: canvasSizing,
|
|
1412
|
+
upload: schema.canvas.upload ?? false,
|
|
1413
|
+
};
|
|
1414
|
+
const panels = normalizePanels({
|
|
1415
|
+
canvas,
|
|
1416
|
+
panels: schema.panels,
|
|
1417
|
+
settingsTransfer,
|
|
1418
|
+
});
|
|
1419
|
+
const toolbar = {
|
|
1420
|
+
history: schema.toolbar?.history ?? canvasEnabled,
|
|
1421
|
+
radar: schema.toolbar?.radar ?? canvasEnabled,
|
|
1422
|
+
theme: schema.toolbar?.theme ?? true,
|
|
1423
|
+
zoom: schema.toolbar?.zoom ?? canvasEnabled,
|
|
1424
|
+
};
|
|
1425
|
+
const exportSchema = resolveExport(schema.export);
|
|
1426
|
+
|
|
1427
|
+
assertPanelPersistenceContract({ panels, persistence, toolbar });
|
|
1428
|
+
|
|
1429
|
+
return {
|
|
1430
|
+
assembly: createToolcraftAssembly({
|
|
1431
|
+
canvas,
|
|
1432
|
+
panels,
|
|
1433
|
+
toolbar,
|
|
1434
|
+
}),
|
|
1435
|
+
canvas,
|
|
1436
|
+
export: exportSchema,
|
|
1437
|
+
panels,
|
|
1438
|
+
persistence,
|
|
1439
|
+
settingsTransfer,
|
|
1440
|
+
toolbar,
|
|
1441
|
+
};
|
|
1442
|
+
}
|