@hyperframes/studio 0.7.46 → 0.7.47
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/dist/assets/index-Dq7FEg0K.css +1 -0
- package/dist/assets/{index-BGVoOXdg.js → index-DsckwbdW.js} +1 -1
- package/dist/assets/index-VsAbY3rl.js +423 -0
- package/dist/assets/{index-DO2MAFSG.js → index-uvB30_yI.js} +1 -1
- package/dist/index.d.ts +96 -29
- package/dist/index.html +2 -2
- package/dist/index.js +8443 -4305
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -32
- package/src/components/DesignPanelPromoteProvider.tsx +45 -0
- package/src/components/PanelTabButton.tsx +31 -0
- package/src/components/StudioPreviewArea.tsx +75 -3
- package/src/components/StudioRightPanel.tsx +111 -113
- package/src/components/editor/LayersPanel.tsx +2 -22
- package/src/components/editor/PromotableControl.tsx +105 -0
- package/src/components/editor/propertyPanelSections.tsx +42 -21
- package/src/components/panels/VariablesBindElement.tsx +266 -0
- package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
- package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
- package/src/components/panels/VariablesPanel.tsx +554 -0
- package/src/components/panels/VariablesRowAction.tsx +25 -0
- package/src/components/panels/VariablesValueControls.tsx +215 -0
- package/src/components/renders/useRenderQueue.ts +12 -0
- package/src/contexts/TimelineEditContext.tsx +7 -2
- package/src/contexts/VariablePromoteContext.tsx +138 -0
- package/src/contexts/variablePromoteHelpers.test.ts +124 -0
- package/src/contexts/variablePromoteHelpers.ts +53 -0
- package/src/contexts/variablePromoteIntegration.test.ts +80 -0
- package/src/hooks/previewVariablesStore.ts +34 -0
- package/src/hooks/timelineEditingHelpers.test.ts +104 -0
- package/src/hooks/timelineEditingHelpers.ts +386 -10
- package/src/hooks/useDomEditWiring.ts +9 -7
- package/src/hooks/useDomSelection.test.ts +36 -2
- package/src/hooks/useDomSelection.ts +56 -75
- package/src/hooks/useElementLifecycleOps.ts +57 -17
- package/src/hooks/usePreviewDocumentVersion.ts +27 -0
- package/src/hooks/useProjectCompositionVariables.ts +131 -0
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useStudioSdkSessions.ts +37 -0
- package/src/hooks/useTimelineEditing.test.tsx +963 -0
- package/src/hooks/useTimelineEditing.ts +122 -100
- package/src/hooks/useTimelineEditingTypes.ts +15 -0
- package/src/hooks/useTimelineGroupEditing.ts +370 -0
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
- package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
- package/src/hooks/useVariablesPersist.ts +61 -0
- package/src/player/components/BeatStrip.tsx +19 -3
- package/src/player/components/Player.tsx +7 -1
- package/src/player/components/Timeline.test.ts +38 -0
- package/src/player/components/Timeline.tsx +102 -80
- package/src/player/components/TimelineCanvas.tsx +340 -296
- package/src/player/components/TimelineClip.test.tsx +11 -0
- package/src/player/components/TimelineClipDiamonds.tsx +40 -9
- package/src/player/components/TimelineDragGhost.tsx +59 -0
- package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
- package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
- package/src/player/components/TimelineLayerGutter.tsx +61 -0
- package/src/player/components/TimelineRuler.tsx +4 -2
- package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
- package/src/player/components/timelineCallbacks.ts +20 -2
- package/src/player/components/timelineClipDragPreview.ts +52 -0
- package/src/player/components/timelineDragDrop.ts +19 -3
- package/src/player/components/timelineDropIndicator.test.ts +48 -0
- package/src/player/components/timelineDropIndicator.ts +33 -0
- package/src/player/components/timelineEditing.test.ts +251 -1
- package/src/player/components/timelineEditing.ts +178 -19
- package/src/player/components/timelineGroupEditing.ts +159 -0
- package/src/player/components/timelineLayerDrag.test.ts +200 -0
- package/src/player/components/timelineLayerDrag.ts +383 -0
- package/src/player/components/timelineLayout.test.ts +34 -0
- package/src/player/components/timelineLayout.ts +47 -0
- package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
- package/src/player/components/timelineSnapTargets.test.ts +144 -0
- package/src/player/components/timelineSnapTargets.ts +164 -0
- package/src/player/components/timelineStacking.ts +62 -0
- package/src/player/components/timelineTrackOrder.test.ts +144 -0
- package/src/player/components/timelineTrackOrder.ts +173 -0
- package/src/player/components/useTimelineActiveClips.test.ts +20 -0
- package/src/player/components/useTimelineActiveClips.ts +15 -4
- package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
- package/src/player/components/useTimelineClipDrag.ts +172 -126
- package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
- package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
- package/src/player/hooks/useTimelinePlayer.ts +7 -0
- package/src/player/lib/layerOrdering.test.ts +77 -0
- package/src/player/lib/layerOrdering.ts +98 -0
- package/src/player/lib/playbackTypes.ts +3 -0
- package/src/player/lib/timelineDOM.test.ts +103 -0
- package/src/player/lib/timelineDOM.ts +80 -0
- package/src/player/lib/timelineElementHelpers.ts +1 -1
- package/src/player/store/playerStore.test.ts +92 -0
- package/src/player/store/playerStore.ts +69 -9
- package/src/utils/blockInstaller.ts +2 -14
- package/src/utils/editHistory.test.ts +61 -0
- package/src/utils/editHistory.ts +7 -1
- package/src/utils/rootDuration.test.ts +34 -0
- package/src/utils/rootDuration.ts +17 -0
- package/src/utils/sdkCutover.ts +45 -0
- package/src/utils/studioHelpers.test.ts +31 -0
- package/src/utils/studioHelpers.ts +29 -1
- package/src/utils/studioUrlState.test.ts +29 -0
- package/src/utils/studioUrlState.ts +16 -1
- package/dist/assets/index-BsLQ2qGn.js +0 -420
- package/dist/assets/index-DoVLXke0.css +0 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add/edit form for a variable declaration. Builds a typed
|
|
3
|
+
* CompositionVariable from free-text drafts; structural validation beyond
|
|
4
|
+
* the field-level checks here is the SDK's job (can() on dispatch).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import type { CompositionVariable, CompositionVariableType } from "@hyperframes/sdk";
|
|
9
|
+
import { VARIABLES_INPUT_CLASS } from "./VariablesValueControls";
|
|
10
|
+
|
|
11
|
+
const VARIABLE_TYPES: CompositionVariableType[] = [
|
|
12
|
+
"string",
|
|
13
|
+
"number",
|
|
14
|
+
"color",
|
|
15
|
+
"boolean",
|
|
16
|
+
"enum",
|
|
17
|
+
"font",
|
|
18
|
+
"image",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export interface DeclarationDraft {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
type: CompositionVariableType;
|
|
25
|
+
defaultRaw: string;
|
|
26
|
+
description: string;
|
|
27
|
+
min: string;
|
|
28
|
+
max: string;
|
|
29
|
+
step: string;
|
|
30
|
+
optionsRaw: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const EMPTY_DRAFT: DeclarationDraft = {
|
|
34
|
+
id: "",
|
|
35
|
+
label: "",
|
|
36
|
+
type: "string",
|
|
37
|
+
defaultRaw: "",
|
|
38
|
+
description: "",
|
|
39
|
+
min: "",
|
|
40
|
+
max: "",
|
|
41
|
+
step: "",
|
|
42
|
+
optionsRaw: "",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Per-type field mapping — one ternary per optional field.
|
|
46
|
+
// fallow-ignore-next-line complexity
|
|
47
|
+
export function draftFromDeclaration(decl: CompositionVariable): DeclarationDraft {
|
|
48
|
+
const numeric = decl.type === "number" ? decl : null;
|
|
49
|
+
return {
|
|
50
|
+
...EMPTY_DRAFT,
|
|
51
|
+
id: decl.id,
|
|
52
|
+
label: decl.label,
|
|
53
|
+
type: decl.type,
|
|
54
|
+
defaultRaw: String(decl.default),
|
|
55
|
+
description: decl.description ?? "",
|
|
56
|
+
min: numeric?.min !== undefined ? String(numeric.min) : "",
|
|
57
|
+
max: numeric?.max !== undefined ? String(numeric.max) : "",
|
|
58
|
+
step: numeric?.step !== undefined ? String(numeric.step) : "",
|
|
59
|
+
optionsRaw:
|
|
60
|
+
decl.type === "enum" ? decl.options.map((o) => `${o.value}:${o.label}`).join("\n") : "",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function numberDeclFromDraft(
|
|
65
|
+
base: { id: string; label: string; description?: string },
|
|
66
|
+
draft: DeclarationDraft,
|
|
67
|
+
): CompositionVariable | string {
|
|
68
|
+
const value = Number(draft.defaultRaw);
|
|
69
|
+
if (!Number.isFinite(value)) return "Default must be a number.";
|
|
70
|
+
const constraint = (key: "min" | "max" | "step") => {
|
|
71
|
+
const raw = draft[key].trim();
|
|
72
|
+
if (!raw) return {};
|
|
73
|
+
const parsed = Number(raw);
|
|
74
|
+
return Number.isFinite(parsed) ? { [key]: parsed } : {};
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
type: "number",
|
|
79
|
+
default: value,
|
|
80
|
+
...constraint("min"),
|
|
81
|
+
...constraint("max"),
|
|
82
|
+
...constraint("step"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// fallow-ignore-next-line complexity
|
|
87
|
+
function enumDeclFromDraft(
|
|
88
|
+
base: { id: string; label: string; description?: string },
|
|
89
|
+
draft: DeclarationDraft,
|
|
90
|
+
): CompositionVariable | string {
|
|
91
|
+
const options = draft.optionsRaw
|
|
92
|
+
.split("\n")
|
|
93
|
+
.map((line) => line.trim())
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((line) => {
|
|
96
|
+
const [value, ...rest] = line.split(":");
|
|
97
|
+
const v = (value ?? "").trim();
|
|
98
|
+
return { value: v, label: rest.join(":").trim() || v };
|
|
99
|
+
})
|
|
100
|
+
.filter((o) => o.value.length > 0);
|
|
101
|
+
if (options.length === 0) return "Enum needs at least one option (one per line, value:Label).";
|
|
102
|
+
const value = draft.defaultRaw.trim() || (options[0]?.value ?? "");
|
|
103
|
+
if (!options.some((o) => o.value === value)) return "Default must be one of the options.";
|
|
104
|
+
return { ...base, type: "enum", default: value, options };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fields the form actually models. On an unchanged-type edit, every OTHER key
|
|
109
|
+
* of the original declaration (font source/default_name/default_source,
|
|
110
|
+
* brandRole, placeholder, maxLength, unit, …) must ride through untouched —
|
|
111
|
+
* updateVariableDeclaration replaces wholesale, so dropping them here would
|
|
112
|
+
* silently strip schema metadata on every Edit + Save.
|
|
113
|
+
*/
|
|
114
|
+
const FORM_OWNED_KEYS = new Set([
|
|
115
|
+
"id",
|
|
116
|
+
"label",
|
|
117
|
+
"type",
|
|
118
|
+
"default",
|
|
119
|
+
"description",
|
|
120
|
+
"min",
|
|
121
|
+
"max",
|
|
122
|
+
"step",
|
|
123
|
+
"options",
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
export function mergeDeclarationEdit(
|
|
127
|
+
original: CompositionVariable,
|
|
128
|
+
edited: CompositionVariable,
|
|
129
|
+
): CompositionVariable {
|
|
130
|
+
// Type changed → old type-specific metadata no longer applies.
|
|
131
|
+
if (original.type !== edited.type) return edited;
|
|
132
|
+
const passthrough: Record<string, unknown> = {};
|
|
133
|
+
for (const [key, value] of Object.entries(original)) {
|
|
134
|
+
if (!FORM_OWNED_KEYS.has(key)) passthrough[key] = value;
|
|
135
|
+
}
|
|
136
|
+
// Both sides are same-type declarations and edited owns every form key, so
|
|
137
|
+
// the merge preserves the declared shape.
|
|
138
|
+
return { ...passthrough, ...edited };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Build a typed declaration from the form draft; string on validation error. */
|
|
142
|
+
// fallow-ignore-next-line complexity
|
|
143
|
+
export function declarationFromDraft(draft: DeclarationDraft): CompositionVariable | string {
|
|
144
|
+
const id = draft.id.trim();
|
|
145
|
+
if (!id) return "Variable id is required.";
|
|
146
|
+
const label = draft.label.trim() || id;
|
|
147
|
+
const description = draft.description.trim() || undefined;
|
|
148
|
+
const base = { id, label, ...(description ? { description } : {}) };
|
|
149
|
+
switch (draft.type) {
|
|
150
|
+
case "number":
|
|
151
|
+
return numberDeclFromDraft(base, draft);
|
|
152
|
+
case "boolean":
|
|
153
|
+
return { ...base, type: "boolean", default: draft.defaultRaw.trim() === "true" };
|
|
154
|
+
case "enum":
|
|
155
|
+
return enumDeclFromDraft(base, draft);
|
|
156
|
+
default:
|
|
157
|
+
// string / color / font / image — string default, verbatim.
|
|
158
|
+
return { ...base, type: draft.type, default: draft.defaultRaw };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
|
163
|
+
return (
|
|
164
|
+
<div className="space-y-1">
|
|
165
|
+
<label className="text-[9px] font-medium text-neutral-500">{label}</label>
|
|
166
|
+
{children}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function DefaultField({
|
|
172
|
+
draft,
|
|
173
|
+
onChange,
|
|
174
|
+
}: {
|
|
175
|
+
draft: DeclarationDraft;
|
|
176
|
+
onChange: (defaultRaw: string) => void;
|
|
177
|
+
}) {
|
|
178
|
+
if (draft.type === "boolean") {
|
|
179
|
+
return (
|
|
180
|
+
<select
|
|
181
|
+
value={draft.defaultRaw === "true" ? "true" : "false"}
|
|
182
|
+
onChange={(e) => onChange(e.target.value)}
|
|
183
|
+
className={VARIABLES_INPUT_CLASS}
|
|
184
|
+
>
|
|
185
|
+
<option value="false">false</option>
|
|
186
|
+
<option value="true">true</option>
|
|
187
|
+
</select>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
<input
|
|
192
|
+
type="text"
|
|
193
|
+
value={draft.defaultRaw}
|
|
194
|
+
onChange={(e) => onChange(e.target.value)}
|
|
195
|
+
className={VARIABLES_INPUT_CLASS}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function DeclarationForm({
|
|
201
|
+
initial,
|
|
202
|
+
submitLabel,
|
|
203
|
+
onSubmit,
|
|
204
|
+
onCancel,
|
|
205
|
+
}: {
|
|
206
|
+
initial: DeclarationDraft;
|
|
207
|
+
submitLabel: string;
|
|
208
|
+
onSubmit: (decl: CompositionVariable) => void;
|
|
209
|
+
onCancel: () => void;
|
|
210
|
+
}) {
|
|
211
|
+
const [draft, setDraft] = useState<DeclarationDraft>(initial);
|
|
212
|
+
const [error, setError] = useState<string | null>(null);
|
|
213
|
+
const editingExisting = initial.id.length > 0;
|
|
214
|
+
const set = (patch: Partial<DeclarationDraft>) => setDraft((d) => ({ ...d, ...patch }));
|
|
215
|
+
|
|
216
|
+
const submit = () => {
|
|
217
|
+
const result = declarationFromDraft(draft);
|
|
218
|
+
if (typeof result === "string") {
|
|
219
|
+
setError(result);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
setError(null);
|
|
223
|
+
onSubmit(result);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div className="space-y-2 rounded-lg border border-neutral-800 bg-neutral-900/60 p-2">
|
|
228
|
+
<div className="grid grid-cols-2 gap-2">
|
|
229
|
+
<Field label="ID">
|
|
230
|
+
<input
|
|
231
|
+
type="text"
|
|
232
|
+
value={draft.id}
|
|
233
|
+
disabled={editingExisting}
|
|
234
|
+
onChange={(e) => set({ id: e.target.value })}
|
|
235
|
+
placeholder="title"
|
|
236
|
+
className={`${VARIABLES_INPUT_CLASS} font-mono disabled:opacity-50`}
|
|
237
|
+
/>
|
|
238
|
+
</Field>
|
|
239
|
+
<Field label="Label">
|
|
240
|
+
<input
|
|
241
|
+
type="text"
|
|
242
|
+
value={draft.label}
|
|
243
|
+
onChange={(e) => set({ label: e.target.value })}
|
|
244
|
+
placeholder="Title"
|
|
245
|
+
className={VARIABLES_INPUT_CLASS}
|
|
246
|
+
/>
|
|
247
|
+
</Field>
|
|
248
|
+
</div>
|
|
249
|
+
<div className="grid grid-cols-2 gap-2">
|
|
250
|
+
<Field label="Type">
|
|
251
|
+
<select
|
|
252
|
+
value={draft.type}
|
|
253
|
+
onChange={(e) => {
|
|
254
|
+
const type = VARIABLE_TYPES.find((t) => t === e.target.value);
|
|
255
|
+
if (type) set({ type });
|
|
256
|
+
}}
|
|
257
|
+
className={VARIABLES_INPUT_CLASS}
|
|
258
|
+
>
|
|
259
|
+
{VARIABLE_TYPES.map((t) => (
|
|
260
|
+
<option key={t} value={t}>
|
|
261
|
+
{t}
|
|
262
|
+
</option>
|
|
263
|
+
))}
|
|
264
|
+
</select>
|
|
265
|
+
</Field>
|
|
266
|
+
<Field label="Default">
|
|
267
|
+
<DefaultField draft={draft} onChange={(defaultRaw) => set({ defaultRaw })} />
|
|
268
|
+
</Field>
|
|
269
|
+
</div>
|
|
270
|
+
{draft.type === "number" && (
|
|
271
|
+
<div className="grid grid-cols-3 gap-2">
|
|
272
|
+
{(["min", "max", "step"] as const).map((key) => (
|
|
273
|
+
<Field key={key} label={key}>
|
|
274
|
+
<input
|
|
275
|
+
type="text"
|
|
276
|
+
value={draft[key]}
|
|
277
|
+
onChange={(e) => set({ [key]: e.target.value })}
|
|
278
|
+
className={`${VARIABLES_INPUT_CLASS} tabular-nums`}
|
|
279
|
+
/>
|
|
280
|
+
</Field>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
{draft.type === "enum" && (
|
|
285
|
+
<Field label="Options (one per line, value:Label)">
|
|
286
|
+
<textarea
|
|
287
|
+
value={draft.optionsRaw}
|
|
288
|
+
onChange={(e) => set({ optionsRaw: e.target.value })}
|
|
289
|
+
rows={3}
|
|
290
|
+
className={`${VARIABLES_INPUT_CLASS} resize-y font-mono`}
|
|
291
|
+
/>
|
|
292
|
+
</Field>
|
|
293
|
+
)}
|
|
294
|
+
<Field label="Description (optional)">
|
|
295
|
+
<input
|
|
296
|
+
type="text"
|
|
297
|
+
value={draft.description}
|
|
298
|
+
onChange={(e) => set({ description: e.target.value })}
|
|
299
|
+
className={VARIABLES_INPUT_CLASS}
|
|
300
|
+
/>
|
|
301
|
+
</Field>
|
|
302
|
+
{error && <p className="text-[10px] text-red-400">{error}</p>}
|
|
303
|
+
<div className="flex items-center justify-end gap-2 pt-1">
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={onCancel}
|
|
307
|
+
className="h-6 rounded px-2 text-[10px] text-neutral-500 hover:text-neutral-300"
|
|
308
|
+
>
|
|
309
|
+
Cancel
|
|
310
|
+
</button>
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
onClick={submit}
|
|
314
|
+
className="h-6 rounded bg-neutral-800 px-2 text-[10px] font-medium text-neutral-200 hover:bg-neutral-700"
|
|
315
|
+
>
|
|
316
|
+
{submitLabel}
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variables tab section for compositions OTHER than the active one. A variable
|
|
3
|
+
* promoted into a sub-comp lives in that frame's file, not the active session —
|
|
4
|
+
* this surfaces every such file's declarations grouped by path, with per-file
|
|
5
|
+
* management (edit declaration / remove). Live-preview override for these is a
|
|
6
|
+
* follow-up (values are per-composition-scope), so no preview control is shown.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useState, type MutableRefObject } from "react";
|
|
10
|
+
import type { Composition, CompositionVariable } from "@hyperframes/sdk";
|
|
11
|
+
import {
|
|
12
|
+
useEditVariablesInFile,
|
|
13
|
+
useProjectCompositionVariables,
|
|
14
|
+
type CompositionVariableGroup,
|
|
15
|
+
type RecordEditFn,
|
|
16
|
+
} from "../../hooks/useProjectCompositionVariables";
|
|
17
|
+
import {
|
|
18
|
+
DeclarationForm,
|
|
19
|
+
draftFromDeclaration,
|
|
20
|
+
mergeDeclarationEdit,
|
|
21
|
+
} from "./VariablesDeclarationForm";
|
|
22
|
+
import { RowAction } from "./VariablesRowAction";
|
|
23
|
+
|
|
24
|
+
function CompositionSection({
|
|
25
|
+
group,
|
|
26
|
+
editingKey,
|
|
27
|
+
onToggleEdit,
|
|
28
|
+
onSave,
|
|
29
|
+
onRemove,
|
|
30
|
+
}: {
|
|
31
|
+
group: CompositionVariableGroup;
|
|
32
|
+
editingKey: string | null;
|
|
33
|
+
onToggleEdit: (key: string | null) => void;
|
|
34
|
+
onSave: (path: string, decl: CompositionVariable) => void;
|
|
35
|
+
onRemove: (path: string, id: string) => void;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-1.5">
|
|
39
|
+
<p
|
|
40
|
+
className="truncate text-[9px] font-medium uppercase tracking-wider text-neutral-500"
|
|
41
|
+
title={group.path}
|
|
42
|
+
>
|
|
43
|
+
{group.path}
|
|
44
|
+
</p>
|
|
45
|
+
{group.variables.map((decl) => {
|
|
46
|
+
const key = `${group.path}::${decl.id}`;
|
|
47
|
+
const editing = editingKey === key;
|
|
48
|
+
return (
|
|
49
|
+
<div key={key} className="space-y-1.5 rounded-lg border border-neutral-800/70 p-2">
|
|
50
|
+
<div className="flex items-center gap-1.5">
|
|
51
|
+
<span className="truncate text-[10px] font-medium text-neutral-300">
|
|
52
|
+
{decl.label}
|
|
53
|
+
</span>
|
|
54
|
+
<span className="rounded bg-neutral-800 px-1 py-px font-mono text-[8px] text-neutral-500">
|
|
55
|
+
{decl.type}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="ml-auto flex items-center gap-1">
|
|
58
|
+
<RowAction
|
|
59
|
+
label="Edit"
|
|
60
|
+
title="Edit declaration"
|
|
61
|
+
onClick={() => onToggleEdit(editing ? null : key)}
|
|
62
|
+
/>
|
|
63
|
+
<RowAction
|
|
64
|
+
label="✕"
|
|
65
|
+
title="Remove declaration"
|
|
66
|
+
danger
|
|
67
|
+
onClick={() => onRemove(group.path, decl.id)}
|
|
68
|
+
/>
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
{decl.description && <p className="text-[9px] text-neutral-500">{decl.description}</p>}
|
|
72
|
+
{editing && (
|
|
73
|
+
<DeclarationForm
|
|
74
|
+
initial={draftFromDeclaration(decl)}
|
|
75
|
+
submitLabel="Save"
|
|
76
|
+
onSubmit={(edited) => onSave(group.path, mergeDeclarationEdit(decl, edited))}
|
|
77
|
+
onCancel={() => onToggleEdit(null)}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function VariablesOtherCompositions({
|
|
88
|
+
fileTree,
|
|
89
|
+
excludePath,
|
|
90
|
+
refreshKey,
|
|
91
|
+
readProjectFile,
|
|
92
|
+
writeProjectFile,
|
|
93
|
+
recordEdit,
|
|
94
|
+
reloadPreview,
|
|
95
|
+
domEditSaveTimestampRef,
|
|
96
|
+
}: {
|
|
97
|
+
fileTree: string[];
|
|
98
|
+
excludePath: string;
|
|
99
|
+
refreshKey: unknown;
|
|
100
|
+
readProjectFile: (path: string) => Promise<string>;
|
|
101
|
+
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
102
|
+
recordEdit: RecordEditFn;
|
|
103
|
+
reloadPreview: () => void;
|
|
104
|
+
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
105
|
+
}) {
|
|
106
|
+
const [selfRefresh, setSelfRefresh] = useState(0);
|
|
107
|
+
const groups = useProjectCompositionVariables(
|
|
108
|
+
fileTree,
|
|
109
|
+
excludePath,
|
|
110
|
+
readProjectFile,
|
|
111
|
+
`${refreshKey}:${selfRefresh}`,
|
|
112
|
+
);
|
|
113
|
+
const editInFile = useEditVariablesInFile({
|
|
114
|
+
readProjectFile,
|
|
115
|
+
writeProjectFile,
|
|
116
|
+
recordEdit,
|
|
117
|
+
reloadPreview,
|
|
118
|
+
domEditSaveTimestampRef,
|
|
119
|
+
});
|
|
120
|
+
const [editingKey, setEditingKey] = useState<string | null>(null);
|
|
121
|
+
|
|
122
|
+
const onSave = useCallback(
|
|
123
|
+
(path: string, decl: CompositionVariable) => {
|
|
124
|
+
setEditingKey(null);
|
|
125
|
+
void editInFile(path, `Update variable "${decl.id}"`, (s: Composition) =>
|
|
126
|
+
s.updateVariableDeclaration(decl.id, decl),
|
|
127
|
+
).then(() => setSelfRefresh((r) => r + 1));
|
|
128
|
+
},
|
|
129
|
+
[editInFile],
|
|
130
|
+
);
|
|
131
|
+
const onRemove = useCallback(
|
|
132
|
+
(path: string, id: string) => {
|
|
133
|
+
void editInFile(path, `Remove variable "${id}"`, (s: Composition) =>
|
|
134
|
+
s.removeVariableDeclaration(id),
|
|
135
|
+
).then(() => setSelfRefresh((r) => r + 1));
|
|
136
|
+
},
|
|
137
|
+
[editInFile],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (groups.length === 0) return null;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="space-y-3 border-t border-neutral-800 pt-3">
|
|
144
|
+
<p className="text-[9px] font-medium uppercase tracking-wider text-neutral-600">
|
|
145
|
+
Other compositions
|
|
146
|
+
</p>
|
|
147
|
+
{groups.map((group) => (
|
|
148
|
+
<CompositionSection
|
|
149
|
+
key={group.path}
|
|
150
|
+
group={group}
|
|
151
|
+
editingKey={editingKey}
|
|
152
|
+
onToggleEdit={setEditingKey}
|
|
153
|
+
onSave={onSave}
|
|
154
|
+
onRemove={onRemove}
|
|
155
|
+
/>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|