@hyperframes/studio 0.6.0-alpha.12 → 0.6.0-alpha.14

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.
Files changed (61) hide show
  1. package/dist/assets/hyperframes-player-BI1oj9hu.js +418 -0
  2. package/dist/assets/index-CBj2NLRG.js +117 -0
  3. package/dist/assets/index-D1JDq7Gg.css +1 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +427 -4487
  8. package/src/components/AskAgentModal.tsx +120 -0
  9. package/src/components/StudioHeader.tsx +133 -0
  10. package/src/components/StudioLeftSidebar.tsx +125 -0
  11. package/src/components/StudioPreviewArea.tsx +163 -0
  12. package/src/components/StudioRightPanel.tsx +198 -0
  13. package/src/components/TimelineToolbar.tsx +89 -0
  14. package/src/components/editor/DomEditOverlay.tsx +15 -1
  15. package/src/components/editor/PropertyPanel.tsx +129 -2681
  16. package/src/components/editor/domEditing.ts +38 -5
  17. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  18. package/src/components/editor/manualEditingAvailability.ts +1 -1
  19. package/src/components/editor/manualEdits.ts +32 -0
  20. package/src/components/editor/propertyPanelColor.tsx +371 -0
  21. package/src/components/editor/propertyPanelFill.tsx +421 -0
  22. package/src/components/editor/propertyPanelFont.tsx +455 -0
  23. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  24. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  25. package/src/components/editor/propertyPanelSections.tsx +453 -0
  26. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  27. package/src/components/nle/NLELayout.tsx +0 -10
  28. package/src/contexts/DomEditContext.tsx +137 -0
  29. package/src/contexts/FileManagerContext.tsx +110 -0
  30. package/src/contexts/PanelLayoutContext.tsx +68 -0
  31. package/src/contexts/StudioContext.tsx +135 -0
  32. package/src/hooks/useAppHotkeys.ts +326 -0
  33. package/src/hooks/useAskAgentModal.ts +162 -0
  34. package/src/hooks/useCaptionDetection.ts +132 -0
  35. package/src/hooks/useCompositionDimensions.ts +25 -0
  36. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  37. package/src/hooks/useDomEditCommits.ts +437 -0
  38. package/src/hooks/useDomEditSession.ts +342 -0
  39. package/src/hooks/useDomEditTextCommits.ts +330 -0
  40. package/src/hooks/useDomSelection.ts +398 -0
  41. package/src/hooks/useFileManager.ts +431 -0
  42. package/src/hooks/useFrameCapture.ts +77 -0
  43. package/src/hooks/useLintModal.ts +35 -0
  44. package/src/hooks/useManifestPersistence.ts +492 -0
  45. package/src/hooks/usePanelLayout.ts +68 -0
  46. package/src/hooks/usePreviewInteraction.ts +153 -0
  47. package/src/hooks/useRenderClipContent.ts +124 -0
  48. package/src/hooks/useTimelineEditing.ts +472 -0
  49. package/src/player/components/Player.tsx +19 -1
  50. package/src/player/components/Timeline.test.ts +0 -8
  51. package/src/player/components/Timeline.tsx +2 -83
  52. package/src/player/components/TimelineClip.tsx +9 -244
  53. package/src/player/hooks/useTimelinePlayer.ts +140 -103
  54. package/src/utils/domEditHelpers.ts +50 -0
  55. package/src/utils/studioFontHelpers.ts +83 -0
  56. package/src/utils/studioHelpers.ts +214 -0
  57. package/src/utils/studioPreviewHelpers.ts +185 -0
  58. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  59. package/dist/assets/index-FWg79aJz.css +0 -1
  60. package/dist/assets/index-xdyn_qRZ.js +0 -110
  61. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,411 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Eye, Layers, Palette, Settings, Square, Zap } from "../../icons/SystemIcons";
3
+ import { buildDefaultGradientModel, serializeGradient } from "./gradientValue";
4
+ import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
5
+ import {
6
+ buildBoxShadowPresetValue,
7
+ buildClipPathValue,
8
+ buildInsetClipPathValue,
9
+ buildStrokeStyleUpdates,
10
+ buildStrokeWidthStyleUpdates,
11
+ extractBackgroundImageUrl,
12
+ formatNumericValue,
13
+ formatPxMetricValue,
14
+ getCssFilterFunctionPx,
15
+ getClipPathInsetPx,
16
+ inferBoxShadowPreset,
17
+ inferClipPathPreset,
18
+ LABEL,
19
+ normalizePanelPxValue,
20
+ parseNumericValue,
21
+ parsePxMetricValue,
22
+ RESPONSIVE_GRID,
23
+ setCssFilterFunctionPx,
24
+ type BoxShadowPreset,
25
+ } from "./propertyPanelHelpers";
26
+ import {
27
+ DetailField,
28
+ MetricField,
29
+ Section,
30
+ SegmentedControl,
31
+ SelectField,
32
+ SliderControl,
33
+ } from "./propertyPanelPrimitives";
34
+ import { ColorField } from "./propertyPanelColor";
35
+ import { GradientField, ImageFillField } from "./propertyPanelFill";
36
+
37
+ export function StyleSections({
38
+ projectId,
39
+ element,
40
+ styles,
41
+ assets,
42
+ onSetStyle,
43
+ onImportAssets,
44
+ }: {
45
+ projectId: string;
46
+ element: DomEditSelection;
47
+ styles: Record<string, string>;
48
+ assets: string[];
49
+ onSetStyle: (prop: string, value: string) => void | Promise<void>;
50
+ onImportAssets?: (files: FileList) => Promise<string[]>;
51
+ }) {
52
+ const styleEditingDisabled = !element.capabilities.canEditStyles;
53
+ const isFlex = styles.display === "flex" || styles.display === "inline-flex";
54
+ const radiusValue = parseNumericValue(styles["border-radius"]) ?? 0;
55
+ const opacityValue = Math.round((parseNumericValue(styles.opacity) ?? 1) * 100);
56
+ const borderWidthValue =
57
+ parsePxMetricValue(styles["border-width"] ?? "") ??
58
+ parsePxMetricValue(styles["border-top-width"] ?? "") ??
59
+ 0;
60
+ const hasVisualBackground =
61
+ (styles.background != null && styles.background !== "none" && styles.background !== "") ||
62
+ (styles["background-color"] != null &&
63
+ styles["background-color"] !== "transparent" &&
64
+ styles["background-color"] !== "rgba(0, 0, 0, 0)" &&
65
+ styles["background-color"] !== "") ||
66
+ (styles["background-image"] != null &&
67
+ styles["background-image"] !== "none" &&
68
+ styles["background-image"] !== "") ||
69
+ borderWidthValue > 0;
70
+ const borderStyleValue = styles["border-style"] || styles["border-top-style"] || "none";
71
+ const borderColorValue =
72
+ styles["border-color"] || styles["border-top-color"] || "rgba(255, 255, 255, 0.18)";
73
+ const boxShadowPreset = inferBoxShadowPreset(styles["box-shadow"]);
74
+ const filterBlurValue = getCssFilterFunctionPx(styles.filter, "blur");
75
+ const backdropBlurValue = getCssFilterFunctionPx(styles["backdrop-filter"], "blur");
76
+ const clipPathValue = styles["clip-path"] || "none";
77
+ const clipPathPreset = inferClipPathPreset(clipPathValue);
78
+ const clipInsetValue = getClipPathInsetPx(clipPathValue);
79
+ const backgroundImage = styles["background-image"] ?? "none";
80
+ const hasTextControls = isTextEditableSelection(element);
81
+
82
+ const fillMode =
83
+ backgroundImage && backgroundImage !== "none"
84
+ ? backgroundImage.includes("gradient")
85
+ ? "Gradient"
86
+ : "Image"
87
+ : "Solid";
88
+ const [preferredFillMode, setPreferredFillMode] = useState(fillMode);
89
+ const imageUrl = extractBackgroundImageUrl(backgroundImage);
90
+
91
+ useEffect(() => {
92
+ setPreferredFillMode(fillMode);
93
+ }, [fillMode, element.id, element.selector, backgroundImage]);
94
+
95
+ const handleFillModeChange = (nextMode: string) => {
96
+ setPreferredFillMode(nextMode);
97
+ if (nextMode === "Solid") {
98
+ onSetStyle("background-image", "none");
99
+ return;
100
+ }
101
+ if (nextMode === "Gradient" && !backgroundImage.includes("gradient")) {
102
+ onSetStyle(
103
+ "background-image",
104
+ serializeGradient(buildDefaultGradientModel(styles["background-color"])),
105
+ );
106
+ }
107
+ };
108
+
109
+ return (
110
+ <>
111
+ {isFlex && (
112
+ <Section title="Flex" icon={<Layers size={15} />}>
113
+ <div className="space-y-4">
114
+ <SegmentedControl
115
+ disabled={styleEditingDisabled}
116
+ value={styles["flex-direction"] || "row"}
117
+ onChange={(next) => onSetStyle("flex-direction", next)}
118
+ options={[
119
+ { label: "→ Row", value: "row" },
120
+ { label: "↓ Column", value: "column" },
121
+ ]}
122
+ />
123
+ <div className={RESPONSIVE_GRID}>
124
+ <SelectField
125
+ label="Justify"
126
+ value={styles["justify-content"] || "flex-start"}
127
+ disabled={styleEditingDisabled}
128
+ onChange={(next) => onSetStyle("justify-content", next)}
129
+ options={[
130
+ "flex-start",
131
+ "center",
132
+ "space-between",
133
+ "space-around",
134
+ "space-evenly",
135
+ "flex-end",
136
+ ]}
137
+ />
138
+ <SelectField
139
+ label="Align"
140
+ value={styles["align-items"] || "stretch"}
141
+ disabled={styleEditingDisabled}
142
+ onChange={(next) => onSetStyle("align-items", next)}
143
+ options={["stretch", "flex-start", "center", "flex-end", "baseline"]}
144
+ />
145
+ </div>
146
+ <DetailField
147
+ label="Gap"
148
+ value={styles.gap ?? "0px"}
149
+ disabled={styleEditingDisabled}
150
+ onCommit={(next) => onSetStyle("gap", next.endsWith("px") ? next : `${next}px`)}
151
+ />
152
+ </div>
153
+ </Section>
154
+ )}
155
+
156
+ {hasVisualBackground && (
157
+ <Section title="Radius" icon={<Settings size={15} />}>
158
+ <SliderControl
159
+ value={radiusValue}
160
+ min={0}
161
+ max={Math.max(240, Math.ceil(radiusValue))}
162
+ step={1}
163
+ disabled={styleEditingDisabled}
164
+ displayValue={`${formatNumericValue(radiusValue)}px`}
165
+ formatDisplayValue={(next) => `${formatNumericValue(next)}px`}
166
+ onCommit={(next) => onSetStyle("border-radius", `${formatNumericValue(next)}px`)}
167
+ />
168
+ </Section>
169
+ )}
170
+
171
+ <Section title="Stroke" icon={<Square size={15} />}>
172
+ <div className="space-y-4">
173
+ <div className={RESPONSIVE_GRID}>
174
+ <MetricField
175
+ label="Width"
176
+ value={formatPxMetricValue(borderWidthValue)}
177
+ disabled={styleEditingDisabled}
178
+ liveCommit
179
+ onCommit={async (next) => {
180
+ const normalized = normalizePanelPxValue(next, {
181
+ min: 0,
182
+ max: 200,
183
+ fallback: borderWidthValue,
184
+ });
185
+ if (!normalized) return;
186
+ for (const [property, value] of buildStrokeWidthStyleUpdates(
187
+ normalized,
188
+ borderStyleValue,
189
+ )) {
190
+ await onSetStyle(property, value);
191
+ }
192
+ }}
193
+ />
194
+ <SelectField
195
+ label="Style"
196
+ value={borderStyleValue}
197
+ disabled={styleEditingDisabled}
198
+ onChange={async (next) => {
199
+ for (const [property, value] of buildStrokeStyleUpdates(
200
+ next,
201
+ formatPxMetricValue(borderWidthValue),
202
+ )) {
203
+ await onSetStyle(property, value);
204
+ }
205
+ }}
206
+ options={[
207
+ "none",
208
+ "solid",
209
+ "dashed",
210
+ "dotted",
211
+ "double",
212
+ "hidden",
213
+ "groove",
214
+ "ridge",
215
+ "inset",
216
+ "outset",
217
+ ]}
218
+ />
219
+ </div>
220
+ <ColorField
221
+ label="Stroke color"
222
+ value={borderColorValue}
223
+ disabled={styleEditingDisabled}
224
+ onCommit={(next) => onSetStyle("border-color", next)}
225
+ />
226
+ </div>
227
+ </Section>
228
+
229
+ <Section title="Effects" icon={<Zap size={15} />}>
230
+ <div className="space-y-4">
231
+ <SelectField
232
+ label="Shadow"
233
+ value={boxShadowPreset}
234
+ disabled={styleEditingDisabled}
235
+ onChange={(next) => {
236
+ if (next === "custom") return;
237
+ onSetStyle(
238
+ "box-shadow",
239
+ buildBoxShadowPresetValue(next as BoxShadowPreset, styles["box-shadow"]),
240
+ );
241
+ }}
242
+ options={["custom", "none", "soft", "lift", "glow"]}
243
+ />
244
+ <div className={RESPONSIVE_GRID}>
245
+ <div className="grid min-w-0 gap-1.5">
246
+ <span className={LABEL}>Layer blur</span>
247
+ <SliderControl
248
+ value={filterBlurValue}
249
+ min={0}
250
+ max={Math.max(40, Math.ceil(filterBlurValue))}
251
+ step={1}
252
+ disabled={styleEditingDisabled}
253
+ displayValue={`${formatNumericValue(filterBlurValue)}px`}
254
+ formatDisplayValue={(next) => `${formatNumericValue(next)}px`}
255
+ onCommit={(next) =>
256
+ onSetStyle("filter", setCssFilterFunctionPx(styles.filter, "blur", next))
257
+ }
258
+ />
259
+ </div>
260
+ <div className="grid min-w-0 gap-1.5">
261
+ <span className={LABEL}>Backdrop</span>
262
+ <SliderControl
263
+ value={backdropBlurValue}
264
+ min={0}
265
+ max={Math.max(60, Math.ceil(backdropBlurValue))}
266
+ step={1}
267
+ disabled={styleEditingDisabled}
268
+ displayValue={`${formatNumericValue(backdropBlurValue)}px`}
269
+ formatDisplayValue={(next) => `${formatNumericValue(next)}px`}
270
+ onCommit={(next) =>
271
+ onSetStyle(
272
+ "backdrop-filter",
273
+ setCssFilterFunctionPx(styles["backdrop-filter"], "blur", next),
274
+ )
275
+ }
276
+ />
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </Section>
281
+
282
+ <Section title="Clip" icon={<Layers size={15} />}>
283
+ <div className="space-y-4">
284
+ <div className={RESPONSIVE_GRID}>
285
+ <SelectField
286
+ label="Overflow"
287
+ value={styles.overflow || "visible"}
288
+ disabled={styleEditingDisabled}
289
+ onChange={(next) => onSetStyle("overflow", next)}
290
+ options={["visible", "hidden", "clip", "auto", "scroll"]}
291
+ />
292
+ <SelectField
293
+ label="Mask"
294
+ value={clipPathPreset}
295
+ disabled={styleEditingDisabled}
296
+ onChange={(next) => {
297
+ if (next === "custom") return;
298
+ onSetStyle(
299
+ "clip-path",
300
+ buildClipPathValue(
301
+ next as "none" | "inset" | "circle",
302
+ radiusValue,
303
+ clipPathValue,
304
+ ),
305
+ );
306
+ }}
307
+ options={["custom", "none", "inset", "circle"]}
308
+ />
309
+ </div>
310
+ <div className="grid min-w-0 gap-1.5">
311
+ <span className={LABEL}>Mask inset</span>
312
+ <SliderControl
313
+ value={clipInsetValue}
314
+ min={0}
315
+ max={Math.max(120, Math.ceil(clipInsetValue))}
316
+ step={1}
317
+ disabled={styleEditingDisabled}
318
+ displayValue={`${formatNumericValue(clipInsetValue)}px`}
319
+ formatDisplayValue={(next) => `${formatNumericValue(next)}px`}
320
+ onCommit={(next) =>
321
+ onSetStyle("clip-path", buildInsetClipPathValue(next, radiusValue))
322
+ }
323
+ />
324
+ </div>
325
+ </div>
326
+ </Section>
327
+
328
+ <Section title="Transparency" icon={<Eye size={15} />}>
329
+ <div className="space-y-4">
330
+ <SliderControl
331
+ value={opacityValue}
332
+ min={0}
333
+ max={100}
334
+ step={1}
335
+ disabled={styleEditingDisabled}
336
+ displayValue={`${opacityValue}%`}
337
+ formatDisplayValue={(next) => `${Math.round(next)}%`}
338
+ onCommit={(next) => onSetStyle("opacity", formatNumericValue(next / 100))}
339
+ />
340
+ <SelectField
341
+ label="Mode"
342
+ value={styles["mix-blend-mode"] || "normal"}
343
+ disabled={styleEditingDisabled}
344
+ onChange={(next) => onSetStyle("mix-blend-mode", next)}
345
+ options={["normal", "multiply", "screen", "overlay", "darken", "lighten"]}
346
+ />
347
+ </div>
348
+ </Section>
349
+
350
+ <Section
351
+ title="Fill"
352
+ icon={<Palette size={15} />}
353
+ accessory={
354
+ <div className="rounded-full border border-neutral-700 bg-neutral-900 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-400">
355
+ {preferredFillMode}
356
+ </div>
357
+ }
358
+ >
359
+ <div className="space-y-4">
360
+ <SegmentedControl
361
+ disabled={styleEditingDisabled}
362
+ value={preferredFillMode}
363
+ onChange={handleFillModeChange}
364
+ options={[
365
+ { label: "Solid", value: "Solid" },
366
+ { label: "Gradient", value: "Gradient" },
367
+ { label: "Image", value: "Image" },
368
+ ]}
369
+ />
370
+ {preferredFillMode === "Solid" ? (
371
+ <ColorField
372
+ label="Fill color"
373
+ value={styles["background-color"] ?? "transparent"}
374
+ disabled={styleEditingDisabled}
375
+ onCommit={(next) => onSetStyle("background-color", next)}
376
+ />
377
+ ) : preferredFillMode === "Gradient" ? (
378
+ <GradientField
379
+ value={
380
+ backgroundImage !== "none"
381
+ ? backgroundImage
382
+ : serializeGradient(buildDefaultGradientModel(styles["background-color"]))
383
+ }
384
+ fallbackColor={styles["background-color"]}
385
+ disabled={styleEditingDisabled}
386
+ onCommit={(next) => onSetStyle("background-image", next)}
387
+ />
388
+ ) : (
389
+ <ImageFillField
390
+ projectId={projectId}
391
+ sourceFile={element.sourceFile}
392
+ value={imageUrl}
393
+ assets={assets}
394
+ disabled={styleEditingDisabled}
395
+ onCommit={(next) => onSetStyle("background-image", next)}
396
+ onImportAssets={onImportAssets}
397
+ />
398
+ )}
399
+ {!hasTextControls && (
400
+ <ColorField
401
+ label="Text color"
402
+ value={styles.color ?? "rgb(0, 0, 0)"}
403
+ disabled={styleEditingDisabled}
404
+ onCommit={(next) => onSetStyle("color", next)}
405
+ />
406
+ )}
407
+ </div>
408
+ </Section>
409
+ </>
410
+ );
411
+ }
@@ -52,9 +52,6 @@ interface NLELayoutProps {
52
52
  ) => Promise<void> | void;
53
53
  onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
54
54
  onSelectTimelineElement?: (element: TimelineElement | null) => void;
55
- onInspectTimelineElement?: (element: TimelineElement) => void;
56
- inspectedTimelineElementId?: string | null;
57
- timelineLayerChildCounts?: ReadonlyMap<string, number>;
58
55
  /** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
59
56
  onCompIdToSrcChange?: (map: Map<string, string>) => void;
60
57
  /** Whether the timeline panel is visible (default: true) */
@@ -91,9 +88,6 @@ export const NLELayout = memo(function NLELayout({
91
88
  onResizeElement,
92
89
  onBlockedEditAttempt,
93
90
  onSelectTimelineElement,
94
- onInspectTimelineElement,
95
- inspectedTimelineElementId,
96
- timelineLayerChildCounts,
97
91
  onCompIdToSrcChange,
98
92
  timelineVisible,
99
93
  onToggleTimeline,
@@ -460,10 +454,6 @@ export const NLELayout = memo(function NLELayout({
460
454
  onResizeElement={onResizeElement}
461
455
  onBlockedEditAttempt={onBlockedEditAttempt}
462
456
  onSelectElement={onSelectTimelineElement}
463
- onInspectElement={onInspectTimelineElement}
464
- inspectedElementId={inspectedTimelineElementId}
465
- layerChildCounts={timelineLayerChildCounts}
466
- disabled={timelineDisabled}
467
457
  />
468
458
  </div>
469
459
  {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
@@ -0,0 +1,137 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import type { useDomEditSession } from "../hooks/useDomEditSession";
3
+
4
+ type DomEditValue = ReturnType<typeof useDomEditSession>;
5
+
6
+ const DomEditContext = createContext<DomEditValue | null>(null);
7
+
8
+ export function useDomEditContext(): DomEditValue {
9
+ const ctx = useContext(DomEditContext);
10
+ if (!ctx) throw new Error("useDomEditContext must be used within DomEditProvider");
11
+ return ctx;
12
+ }
13
+
14
+ export function DomEditProvider({
15
+ value: {
16
+ domEditSelection,
17
+ domEditGroupSelections,
18
+ domEditHoverSelection,
19
+ agentModalOpen,
20
+ agentModalAnchorPoint,
21
+ copiedAgentPrompt,
22
+ agentPromptSelectionContext,
23
+ domEditSelectionRef,
24
+ handleTimelineElementSelect,
25
+ handlePreviewCanvasMouseDown,
26
+ handlePreviewCanvasPointerMove,
27
+ handlePreviewCanvasPointerLeave,
28
+ applyDomSelection,
29
+ clearDomSelection,
30
+ handleDomStyleCommit,
31
+ handleDomPathOffsetCommit,
32
+ handleDomGroupPathOffsetCommit,
33
+ handleDomBoxSizeCommit,
34
+ handleDomRotationCommit,
35
+ handleDomManualEditsReset,
36
+ handleDomMotionCommit,
37
+ handleDomMotionClear,
38
+ handleDomTextCommit,
39
+ handleDomTextFieldStyleCommit,
40
+ handleDomAddTextField,
41
+ handleDomRemoveTextField,
42
+ handleAskAgent,
43
+ handleAgentModalSubmit,
44
+ handleBlockedDomMove,
45
+ handleDomManualDragStart,
46
+ handleDomEditElementDelete,
47
+ buildDomSelectionForTimelineElement,
48
+ resolveImportedFontAsset,
49
+ setAgentModalOpen,
50
+ setAgentPromptSelectionContext,
51
+ setAgentModalAnchorPoint,
52
+ },
53
+ children,
54
+ }: {
55
+ value: DomEditValue;
56
+ children: ReactNode;
57
+ }) {
58
+ const stable = useMemo<DomEditValue>(
59
+ () => ({
60
+ domEditSelection,
61
+ domEditGroupSelections,
62
+ domEditHoverSelection,
63
+ agentModalOpen,
64
+ agentModalAnchorPoint,
65
+ copiedAgentPrompt,
66
+ agentPromptSelectionContext,
67
+ domEditSelectionRef,
68
+ handleTimelineElementSelect,
69
+ handlePreviewCanvasMouseDown,
70
+ handlePreviewCanvasPointerMove,
71
+ handlePreviewCanvasPointerLeave,
72
+ applyDomSelection,
73
+ clearDomSelection,
74
+ handleDomStyleCommit,
75
+ handleDomPathOffsetCommit,
76
+ handleDomGroupPathOffsetCommit,
77
+ handleDomBoxSizeCommit,
78
+ handleDomRotationCommit,
79
+ handleDomManualEditsReset,
80
+ handleDomMotionCommit,
81
+ handleDomMotionClear,
82
+ handleDomTextCommit,
83
+ handleDomTextFieldStyleCommit,
84
+ handleDomAddTextField,
85
+ handleDomRemoveTextField,
86
+ handleAskAgent,
87
+ handleAgentModalSubmit,
88
+ handleBlockedDomMove,
89
+ handleDomManualDragStart,
90
+ handleDomEditElementDelete,
91
+ buildDomSelectionForTimelineElement,
92
+ resolveImportedFontAsset,
93
+ setAgentModalOpen,
94
+ setAgentPromptSelectionContext,
95
+ setAgentModalAnchorPoint,
96
+ }),
97
+ [
98
+ domEditSelection,
99
+ domEditGroupSelections,
100
+ domEditHoverSelection,
101
+ agentModalOpen,
102
+ agentModalAnchorPoint,
103
+ copiedAgentPrompt,
104
+ agentPromptSelectionContext,
105
+ domEditSelectionRef,
106
+ handleTimelineElementSelect,
107
+ handlePreviewCanvasMouseDown,
108
+ handlePreviewCanvasPointerMove,
109
+ handlePreviewCanvasPointerLeave,
110
+ applyDomSelection,
111
+ clearDomSelection,
112
+ handleDomStyleCommit,
113
+ handleDomPathOffsetCommit,
114
+ handleDomGroupPathOffsetCommit,
115
+ handleDomBoxSizeCommit,
116
+ handleDomRotationCommit,
117
+ handleDomManualEditsReset,
118
+ handleDomMotionCommit,
119
+ handleDomMotionClear,
120
+ handleDomTextCommit,
121
+ handleDomTextFieldStyleCommit,
122
+ handleDomAddTextField,
123
+ handleDomRemoveTextField,
124
+ handleAskAgent,
125
+ handleAgentModalSubmit,
126
+ handleBlockedDomMove,
127
+ handleDomManualDragStart,
128
+ handleDomEditElementDelete,
129
+ buildDomSelectionForTimelineElement,
130
+ resolveImportedFontAsset,
131
+ setAgentModalOpen,
132
+ setAgentPromptSelectionContext,
133
+ setAgentModalAnchorPoint,
134
+ ],
135
+ );
136
+ return <DomEditContext value={stable}>{children}</DomEditContext>;
137
+ }
@@ -0,0 +1,110 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import type { useFileManager } from "../hooks/useFileManager";
3
+
4
+ type FileManagerValue = ReturnType<typeof useFileManager>;
5
+
6
+ const FileManagerContext = createContext<FileManagerValue | null>(null);
7
+
8
+ export function useFileManagerContext(): FileManagerValue {
9
+ const ctx = useContext(FileManagerContext);
10
+ if (!ctx) throw new Error("useFileManagerContext must be used within FileManagerProvider");
11
+ return ctx;
12
+ }
13
+
14
+ export function FileManagerProvider({
15
+ value: {
16
+ editingFile,
17
+ setEditingFile,
18
+ projectDir,
19
+ fileTree,
20
+ setFileTree,
21
+ editingPathRef,
22
+ projectIdRef,
23
+ saveTimerRef,
24
+ importedFontAssetsRef,
25
+ readProjectFile,
26
+ writeProjectFile,
27
+ readOptionalProjectFile,
28
+ handleFileSelect,
29
+ handleContentChange,
30
+ refreshFileTree,
31
+ uploadProjectFiles,
32
+ handleCreateFile,
33
+ handleCreateFolder,
34
+ handleDeleteFile,
35
+ handleRenameFile,
36
+ handleDuplicateFile,
37
+ handleMoveFile,
38
+ handleImportFiles,
39
+ handleImportFonts,
40
+ compositions,
41
+ assets,
42
+ fontAssets,
43
+ },
44
+ children,
45
+ }: {
46
+ value: FileManagerValue;
47
+ children: ReactNode;
48
+ }) {
49
+ const stable = useMemo<FileManagerValue>(
50
+ () => ({
51
+ editingFile,
52
+ setEditingFile,
53
+ projectDir,
54
+ fileTree,
55
+ setFileTree,
56
+ editingPathRef,
57
+ projectIdRef,
58
+ saveTimerRef,
59
+ importedFontAssetsRef,
60
+ readProjectFile,
61
+ writeProjectFile,
62
+ readOptionalProjectFile,
63
+ handleFileSelect,
64
+ handleContentChange,
65
+ refreshFileTree,
66
+ uploadProjectFiles,
67
+ handleCreateFile,
68
+ handleCreateFolder,
69
+ handleDeleteFile,
70
+ handleRenameFile,
71
+ handleDuplicateFile,
72
+ handleMoveFile,
73
+ handleImportFiles,
74
+ handleImportFonts,
75
+ compositions,
76
+ assets,
77
+ fontAssets,
78
+ }),
79
+ [
80
+ editingFile,
81
+ setEditingFile,
82
+ projectDir,
83
+ fileTree,
84
+ setFileTree,
85
+ editingPathRef,
86
+ projectIdRef,
87
+ saveTimerRef,
88
+ importedFontAssetsRef,
89
+ readProjectFile,
90
+ writeProjectFile,
91
+ readOptionalProjectFile,
92
+ handleFileSelect,
93
+ handleContentChange,
94
+ refreshFileTree,
95
+ uploadProjectFiles,
96
+ handleCreateFile,
97
+ handleCreateFolder,
98
+ handleDeleteFile,
99
+ handleRenameFile,
100
+ handleDuplicateFile,
101
+ handleMoveFile,
102
+ handleImportFiles,
103
+ handleImportFonts,
104
+ compositions,
105
+ assets,
106
+ fontAssets,
107
+ ],
108
+ );
109
+ return <FileManagerContext value={stable}>{children}</FileManagerContext>;
110
+ }