@hyperframes/studio 0.7.37 → 0.7.38

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 (75) hide show
  1. package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
  2. package/dist/assets/index-BLRTwY5l.js +396 -0
  3. package/dist/assets/{index-BXYJbIax.js → index-ClUipc8i.js} +1 -1
  4. package/dist/assets/index-DJaiR8T2.css +1 -0
  5. package/dist/assets/{index-Ct-X2wlf.js → index-Ykq7ihge.js} +1 -1
  6. package/dist/index.d.ts +35 -10
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +5146 -3321
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +19 -18
  12. package/src/components/AskAgentModal.tsx +23 -6
  13. package/src/components/LintModal.tsx +40 -10
  14. package/src/components/MediaPreview.tsx +36 -1
  15. package/src/components/SaveQueuePausedBanner.tsx +7 -6
  16. package/src/components/StudioErrorBoundary.tsx +19 -31
  17. package/src/components/StudioFeedbackBar.tsx +11 -2
  18. package/src/components/StudioGlobalDragOverlay.tsx +1 -1
  19. package/src/components/StudioHeader.tsx +184 -106
  20. package/src/components/StudioLeftSidebar.tsx +24 -3
  21. package/src/components/StudioOverlays.tsx +34 -8
  22. package/src/components/StudioRightPanel.tsx +154 -9
  23. package/src/components/StudioSplash.tsx +6 -3
  24. package/src/components/StudioToast.tsx +12 -11
  25. package/src/components/TimelineToolbar.test.tsx +9 -3
  26. package/src/components/TimelineToolbar.tsx +63 -29
  27. package/src/components/editor/PropertyPanel.tsx +18 -17
  28. package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
  29. package/src/components/editor/colorGradingScopePatch.ts +57 -0
  30. package/src/components/editor/manualEditingAvailability.test.ts +0 -12
  31. package/src/components/editor/manualEditingAvailability.ts +0 -6
  32. package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
  33. package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
  34. package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
  35. package/src/components/editor/propertyPanelHelpers.ts +22 -91
  36. package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
  37. package/src/components/editor/propertyPanelTypes.ts +111 -0
  38. package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
  39. package/src/components/nle/NLELayout.tsx +54 -51
  40. package/src/components/nle/NLEPreview.tsx +41 -5
  41. package/src/components/nle/TimelineResizeDivider.tsx +97 -0
  42. package/src/components/nle/usePreviewBlockDrop.ts +31 -7
  43. package/src/components/renders/RenderQueue.tsx +125 -24
  44. package/src/components/renders/RenderQueueItem.tsx +132 -64
  45. package/src/components/renders/useRenderQueue.ts +136 -24
  46. package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
  47. package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
  48. package/src/components/storyboard/StoryboardView.tsx +6 -0
  49. package/src/components/studioColorGradingScope.ts +124 -0
  50. package/src/components/studioMediaJobs.ts +123 -0
  51. package/src/components/ui/Button.tsx +20 -11
  52. package/src/components/ui/HyperframesLoader.tsx +9 -2
  53. package/src/components/ui/SearchInput.tsx +53 -0
  54. package/src/components/ui/Tooltip.tsx +52 -6
  55. package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
  56. package/src/components/ui/useDialogBehavior.ts +83 -0
  57. package/src/contexts/PanelLayoutContext.tsx +3 -0
  58. package/src/contexts/StudioContext.tsx +7 -0
  59. package/src/hooks/useCompositionContentLoader.ts +40 -0
  60. package/src/hooks/useEditorSave.ts +14 -0
  61. package/src/hooks/useFileManager.ts +39 -25
  62. package/src/hooks/useFrameCapture.ts +12 -1
  63. package/src/hooks/usePanelLayout.ts +1 -0
  64. package/src/hooks/usePreviewPersistence.ts +24 -15
  65. package/src/hooks/useStudioContextValue.ts +5 -0
  66. package/src/hooks/useToast.ts +66 -13
  67. package/src/styles/studio.css +61 -0
  68. package/src/utils/sdkResolverShadow.test.ts +53 -3
  69. package/src/utils/sdkResolverShadow.ts +41 -1
  70. package/src/utils/studioPendingEdits.test.ts +43 -0
  71. package/src/utils/studioPendingEdits.ts +45 -0
  72. package/src/utils/studioUiPreferences.ts +4 -0
  73. package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
  74. package/dist/assets/index-DmkOvZns.css +0 -1
  75. package/dist/assets/index-SYSbQyK4.js +0 -396
@@ -0,0 +1,275 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { Minus, Plus, RotateCcw, Settings } from "../../icons/SystemIcons";
3
+ import { LABEL } from "./propertyPanelHelpers";
4
+
5
+ const SLIDER_THUMB_SIZE = 10;
6
+ const SLIDER_THUMB_RADIUS = SLIDER_THUMB_SIZE / 2;
7
+
8
+ function clampNumber(value: number, min: number, max: number): number {
9
+ if (!Number.isFinite(value)) return min;
10
+ return Math.min(max, Math.max(min, value));
11
+ }
12
+
13
+ function formatNumericInput(value: number, scale: number): string {
14
+ const scaled = value / scale;
15
+ return scale === 100 ? scaled.toFixed(2) : String(Math.round(scaled));
16
+ }
17
+
18
+ function parseNumericInput(value: string, scale: number): number | null {
19
+ const parsed = Number(value);
20
+ if (!Number.isFinite(parsed)) return null;
21
+ return parsed * scale;
22
+ }
23
+
24
+ function tickPercent(value: number, min: number, max: number): number {
25
+ if (max <= min) return 0;
26
+ return ((value - min) / (max - min)) * 100;
27
+ }
28
+
29
+ export function ColorGradingSliderControl({
30
+ label,
31
+ value,
32
+ min,
33
+ max,
34
+ step,
35
+ neutral = min,
36
+ scale = 1,
37
+ suffix = "",
38
+ displayValue,
39
+ disabled,
40
+ onCommit,
41
+ onReset,
42
+ settings,
43
+ }: {
44
+ label: string;
45
+ value: number;
46
+ min: number;
47
+ max: number;
48
+ step: number;
49
+ neutral?: number;
50
+ scale?: number;
51
+ suffix?: string;
52
+ displayValue: string;
53
+ disabled?: boolean;
54
+ onCommit: (nextValue: number) => void;
55
+ onReset?: () => void;
56
+ settings?: {
57
+ active?: boolean;
58
+ label: string;
59
+ onClick: () => void;
60
+ };
61
+ }) {
62
+ const [draftState, setDraftState] = useState<{ value: number; source: number } | null>(null);
63
+ const [inputDraft, setInputDraft] = useState<{ value: string; source: number } | null>(null);
64
+ const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
65
+ const valueRef = useRef(value);
66
+ valueRef.current = value;
67
+
68
+ useEffect(
69
+ () => () => {
70
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
71
+ },
72
+ [],
73
+ );
74
+
75
+ const clampDraft = useCallback(
76
+ (nextValue: number) => clampNumber(nextValue, min, max),
77
+ [max, min],
78
+ );
79
+
80
+ const setLocalDraft = useCallback(
81
+ (nextValue: number) => {
82
+ const clamped = clampDraft(nextValue);
83
+ const source = valueRef.current;
84
+ setDraftState({ value: clamped, source });
85
+ setInputDraft({ value: formatNumericInput(clamped, scale), source });
86
+ return clamped;
87
+ },
88
+ [clampDraft, scale],
89
+ );
90
+
91
+ const commitDraft = useCallback(
92
+ (nextValue: number) => {
93
+ const clamped = setLocalDraft(nextValue);
94
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
95
+ if (clamped !== valueRef.current) onCommit(clamped);
96
+ },
97
+ [onCommit, setLocalDraft],
98
+ );
99
+
100
+ const scheduleCommit = useCallback(
101
+ (nextValue: number) => {
102
+ const clamped = setLocalDraft(nextValue);
103
+ if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
104
+ commitTimerRef.current = setTimeout(() => {
105
+ if (clamped !== valueRef.current) onCommit(clamped);
106
+ }, 40);
107
+ },
108
+ [onCommit, setLocalDraft],
109
+ );
110
+
111
+ const draft = draftState?.source === value ? draftState.value : value;
112
+ const inputValue =
113
+ inputDraft?.source === value ? inputDraft.value : formatNumericInput(draft, scale);
114
+
115
+ const commitInputDraft = useCallback(() => {
116
+ const parsed = parseNumericInput(inputValue, scale);
117
+ if (parsed === null) {
118
+ setInputDraft(null);
119
+ return;
120
+ }
121
+ commitDraft(parsed);
122
+ }, [commitDraft, inputValue, scale]);
123
+
124
+ const nudge = useCallback(
125
+ (direction: -1 | 1) => {
126
+ commitDraft(draft + step * direction);
127
+ },
128
+ [commitDraft, draft, step],
129
+ );
130
+
131
+ const range = max - min;
132
+ const valuePercent = range === 0 ? 0 : ((draft - min) / range) * 100;
133
+ const neutralPercent = range === 0 ? 0 : ((neutral - min) / range) * 100;
134
+ const fillLeft = Math.min(valuePercent, neutralPercent);
135
+ const fillWidth = Math.abs(valuePercent - neutralPercent);
136
+ const ticks = Array.from(new Set([min, neutral, max])).sort((a, b) => a - b);
137
+
138
+ return (
139
+ <div className="grid min-w-0 gap-0.5 rounded-md bg-panel-input/30 px-1.5 py-1">
140
+ <div className="flex min-w-0 items-center gap-1">
141
+ <span className={`${LABEL} min-w-0 flex-1 truncate`}>{label}</span>
142
+ {settings && (
143
+ <button
144
+ type="button"
145
+ disabled={disabled}
146
+ aria-label={settings.label}
147
+ onClick={(event) => {
148
+ event.stopPropagation();
149
+ settings.onClick();
150
+ }}
151
+ className={`relative flex h-5 w-5 flex-shrink-0 items-center justify-center rounded transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40 ${
152
+ settings.active ? "text-studio-accent" : "text-panel-text-5"
153
+ }`}
154
+ title={settings.label}
155
+ >
156
+ <Settings size={11} />
157
+ {settings.active && (
158
+ <span className="absolute right-0.5 top-0.5 h-1 w-1 rounded-full bg-studio-accent" />
159
+ )}
160
+ </button>
161
+ )}
162
+ {onReset && (
163
+ <button
164
+ type="button"
165
+ disabled={disabled}
166
+ aria-label={`Reset ${label}`}
167
+ onClick={(event) => {
168
+ event.stopPropagation();
169
+ onReset();
170
+ }}
171
+ className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded text-panel-text-5 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
172
+ title={`Reset ${label}`}
173
+ >
174
+ <RotateCcw size={11} />
175
+ </button>
176
+ )}
177
+ </div>
178
+
179
+ <div className="relative h-5 min-w-0">
180
+ <div
181
+ data-color-grading-slider-track="true"
182
+ className="pointer-events-none absolute inset-y-0 z-0"
183
+ style={{ left: SLIDER_THUMB_RADIUS, right: SLIDER_THUMB_RADIUS }}
184
+ >
185
+ {ticks.map((tick) => (
186
+ <div
187
+ key={tick}
188
+ data-color-grading-slider-tick="true"
189
+ className="absolute top-1/2 h-3 w-px -translate-y-1/2 bg-panel-text-3"
190
+ style={{ left: `${tickPercent(tick, min, max)}%` }}
191
+ title={String(tick / scale)}
192
+ />
193
+ ))}
194
+ <div className="absolute left-0 right-0 top-1/2 z-10 h-0.5 -translate-y-1/2 rounded-full bg-panel-border" />
195
+ <div
196
+ className="absolute top-1/2 z-20 h-0.5 -translate-y-1/2 rounded-full bg-studio-accent"
197
+ style={{ left: `${fillLeft}%`, width: `${fillWidth}%` }}
198
+ />
199
+ </div>
200
+ <input
201
+ type="range"
202
+ min={min}
203
+ max={max}
204
+ step={step}
205
+ value={draft}
206
+ disabled={disabled}
207
+ aria-label={label}
208
+ onChange={(event) => scheduleCommit(Number(event.currentTarget.value))}
209
+ onMouseUp={() => commitDraft(draft)}
210
+ onTouchEnd={() => commitDraft(draft)}
211
+ onBlur={() => commitDraft(draft)}
212
+ className="hf-color-grading-range absolute left-0 right-0 top-1/2 z-30 min-w-0 w-full -translate-y-1/2"
213
+ title={displayValue}
214
+ />
215
+ </div>
216
+
217
+ <div className="flex min-w-0 items-center justify-end gap-1">
218
+ <div className="flex flex-shrink-0 items-center rounded-md bg-panel-input px-1.5 py-px">
219
+ <input
220
+ type="number"
221
+ value={inputValue}
222
+ min={min / scale}
223
+ max={max / scale}
224
+ step={step / scale}
225
+ disabled={disabled}
226
+ onChange={(event) =>
227
+ setInputDraft({ value: event.currentTarget.value, source: valueRef.current })
228
+ }
229
+ onBlur={commitInputDraft}
230
+ onKeyDown={(event) => {
231
+ if (event.key === "Enter") {
232
+ event.currentTarget.blur();
233
+ return;
234
+ }
235
+ if (event.key === "ArrowUp") {
236
+ event.preventDefault();
237
+ nudge(1);
238
+ return;
239
+ }
240
+ if (event.key === "ArrowDown") {
241
+ event.preventDefault();
242
+ nudge(-1);
243
+ }
244
+ }}
245
+ className="hf-color-grading-number h-4 w-[36px] bg-transparent text-right text-[10px] font-medium tabular-nums text-panel-text-1 outline-none disabled:cursor-not-allowed"
246
+ title={displayValue}
247
+ />
248
+ {suffix && <span className="ml-0.5 text-[10px] text-panel-text-5">{suffix}</span>}
249
+ </div>
250
+ <div className="flex flex-shrink-0 overflow-hidden rounded-md bg-panel-input">
251
+ <button
252
+ type="button"
253
+ disabled={disabled}
254
+ aria-label={`Decrease ${label}`}
255
+ onClick={() => nudge(-1)}
256
+ className="flex h-5 w-5 items-center justify-center text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
257
+ title={`Decrease ${label}`}
258
+ >
259
+ <Minus size={11} />
260
+ </button>
261
+ <button
262
+ type="button"
263
+ disabled={disabled}
264
+ aria-label={`Increase ${label}`}
265
+ onClick={() => nudge(1)}
266
+ className="flex h-5 w-5 items-center justify-center border-l border-panel-border text-panel-text-4 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-40"
267
+ title={`Increase ${label}`}
268
+ >
269
+ <Plus size={11} />
270
+ </button>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ );
275
+ }
@@ -1,91 +1,21 @@
1
1
  import { parseCssColor, type ParsedColor } from "./colorValue";
2
2
  import { COMMON_LOCAL_FONT_FAMILIES } from "./fontCatalog";
3
3
  import type { DomEditSelection } from "./domEditing";
4
- import type { ImportedFontAsset } from "./fontAssets";
5
4
  import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
6
5
  import { roundToCenti } from "../../utils/rounding";
7
6
 
8
- export interface PropertyPanelProps {
9
- projectId: string;
10
- projectDir: string | null;
11
- assets: string[];
12
- element: DomEditSelection | null;
13
- multiSelectCount?: number;
14
- copiedAgentPrompt: boolean;
15
- onClearSelection: () => void;
16
- /** Dissolve the selected data-hf-group wrapper (shown only for group selections). */
17
- onUngroup?: () => void;
18
- onSetStyle: (prop: string, value: string) => void | Promise<void>;
19
- onSetAttribute: (attr: string, value: string) => void | Promise<void>;
20
- onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
21
- onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
22
- onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
23
- onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
24
- onSetManualRotation: (element: DomEditSelection, next: { angle: number }) => void;
25
- onSetText: (value: string, fieldKey?: string) => void;
26
- onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
27
- onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
28
- onRemoveTextField: (fieldKey: string) => void;
29
- onAskAgent: () => void;
30
- onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
31
- fontAssets?: ImportedFontAsset[];
32
- onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
33
- previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
34
- gsapAnimations?: import("@hyperframes/parsers/gsap-parser").GsapAnimation[];
35
- gsapMultipleTimelines?: boolean;
36
- gsapUnsupportedTimelinePattern?: boolean;
37
- onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
38
- onUpdateGsapMeta?: (
39
- animId: string,
40
- updates: { duration?: number; ease?: string; position?: number },
41
- ) => void;
42
- onDeleteGsapAnimation?: (animId: string) => void;
43
- onAddGsapProperty?: (animId: string, prop: string) => void;
44
- onRemoveGsapProperty?: (animId: string, prop: string) => void;
45
- onUpdateGsapFromProperty?: (animId: string, prop: string, value: number | string) => void;
46
- onAddGsapFromProperty?: (animId: string, prop: string) => void;
47
- onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
48
- onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
49
- onSetArcPath?: (
50
- animId: string,
51
- config: {
52
- enabled: boolean;
53
- autoRotate?: boolean | number;
54
- segments?: import("@hyperframes/parsers/gsap-parser").ArcPathSegment[];
55
- },
56
- ) => void;
57
- onUpdateArcSegment?: (
58
- animId: string,
59
- segmentIndex: number,
60
- update: Partial<import("@hyperframes/parsers/gsap-parser").ArcPathSegment>,
61
- ) => void;
62
- /** Unroll computed (helper/loop) tweens into literal tweens for direct editing. */
63
- onUnroll?: (animationId: string) => void;
64
- onAddKeyframe?: (
65
- animationId: string,
66
- percentage: number,
67
- property: string,
68
- value: number | string,
69
- ) => void;
70
- onRemoveKeyframe?: (animationId: string, percentage: number) => void;
71
- onUpdateKeyframeEase?: (animationId: string, percentage: number, ease: string) => void;
72
- onSetAllKeyframeEases?: (animationId: string, ease: string) => void;
73
- onConvertToKeyframes?: (animationId: string, duration?: number) => void;
74
- onCommitAnimatedProperty?: (
75
- selection: DomEditSelection,
76
- property: string,
77
- value: number | string,
78
- ) => Promise<void>;
79
- /** Batched variant: commit several props into ONE keyframe (e.g. the 3D cube's
80
- * rotationX/Y/Z) so multi-axis edits don't race into adjacent duplicates. */
81
- onCommitAnimatedProperties?: (
82
- selection: DomEditSelection,
83
- props: Record<string, number | string>,
84
- ) => Promise<void>;
85
- onSeekToTime?: (time: number) => void;
86
- recordingState?: "idle" | "recording" | "preview";
87
- recordingDuration?: number;
88
- onToggleRecording?: () => void;
7
+ export type {
8
+ BackgroundRemovalProgress,
9
+ BackgroundRemovalResult,
10
+ PropertyPanelProps,
11
+ } from "./propertyPanelTypes";
12
+
13
+ export function stripQueryAndHash(value: string): string {
14
+ const queryIndex = value.indexOf("?");
15
+ const hashIndex = value.indexOf("#");
16
+ if (queryIndex < 0) return hashIndex < 0 ? value : value.slice(0, hashIndex);
17
+ if (hashIndex < 0) return value.slice(0, queryIndex);
18
+ return value.slice(0, Math.min(queryIndex, hashIndex));
89
19
  }
90
20
 
91
21
  /* ------------------------------------------------------------------ */
@@ -322,23 +252,24 @@ export function normalizeTextMetricValue(
322
252
  }
323
253
 
324
254
  function splitCssFunctions(value: string): string[] {
255
+ const source = value.trim();
325
256
  const functions: string[] = [];
326
- let current = "";
327
- // fallow-ignore-next-line code-duplication -- pre-existing; surfaced in this file's diff by an unrelated line shift
328
257
  let depth = 0;
258
+ let start = 0;
329
259
 
330
- for (const char of value.trim()) {
260
+ for (let index = 0; index < source.length; index += 1) {
261
+ const char = source[index];
331
262
  if (char === "(") depth += 1;
332
263
  if (char === ")") depth = Math.max(0, depth - 1);
333
264
  if (/\s/.test(char) && depth === 0) {
334
- if (current.trim()) functions.push(current.trim());
335
- current = "";
336
- continue;
265
+ const part = source.slice(start, index).trim();
266
+ if (part) functions.push(part);
267
+ start = index + 1;
337
268
  }
338
- current += char;
339
269
  }
340
270
 
341
- if (current.trim()) functions.push(current.trim());
271
+ const lastPart = source.slice(start).trim();
272
+ if (lastPart) functions.push(lastPart);
342
273
  return functions;
343
274
  }
344
275
 
@@ -490,11 +421,11 @@ export function extractBackgroundImageUrl(value: string | undefined): string {
490
421
 
491
422
  // ── GSAP runtime value readers (used by PropertyPanel) ────────────────────
492
423
 
493
- // fallow-ignore-next-line complexity -- pre-existing; surfaced in this file's diff by an unrelated line shift
494
424
  // Core transform channels the panel ALWAYS reads live — even before a just-set
495
425
  // value (e.g. rotationX) has re-parsed into `gsapAnimations`. Without this the
496
426
  // cube + fields drop the prop and flicker to 0 on every commit; gsap.getProperty
497
427
  // reflects the in-place instant patch, so it's the true current value.
428
+ // fallow-ignore-next-line complexity
498
429
  const ALWAYS_READ_CHANNELS = [
499
430
  "x",
500
431
  "y",