@hyperframes/studio 0.7.37 → 0.7.39

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 (109) hide show
  1. package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
  2. package/dist/assets/index-0P10SwC_.css +1 -0
  3. package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
  4. package/dist/assets/index-B9YvRJz1.js +396 -0
  5. package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
  6. package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
  7. package/dist/chunk-RCLGSZ7C.js.map +1 -0
  8. package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
  9. package/dist/index.d.ts +48 -10
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +6045 -3507
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +26 -24
  15. package/src/components/AskAgentModal.tsx +23 -6
  16. package/src/components/LintModal.tsx +40 -10
  17. package/src/components/MediaPreview.tsx +36 -1
  18. package/src/components/SaveQueuePausedBanner.tsx +7 -6
  19. package/src/components/StudioErrorBoundary.tsx +19 -31
  20. package/src/components/StudioFeedbackBar.tsx +11 -2
  21. package/src/components/StudioGlobalDragOverlay.tsx +1 -1
  22. package/src/components/StudioHeader.tsx +184 -106
  23. package/src/components/StudioLeftSidebar.tsx +24 -3
  24. package/src/components/StudioOverlays.tsx +34 -8
  25. package/src/components/StudioPreviewArea.tsx +8 -0
  26. package/src/components/StudioRightPanel.tsx +160 -9
  27. package/src/components/StudioSplash.tsx +6 -3
  28. package/src/components/StudioToast.tsx +12 -11
  29. package/src/components/TimelineToolbar.test.tsx +9 -3
  30. package/src/components/TimelineToolbar.tsx +63 -29
  31. package/src/components/editor/DomEditCropHandles.tsx +238 -0
  32. package/src/components/editor/DomEditOverlay.tsx +111 -108
  33. package/src/components/editor/DomEditRotateHandle.tsx +41 -0
  34. package/src/components/editor/PropertyPanel.test.ts +48 -0
  35. package/src/components/editor/PropertyPanel.tsx +27 -47
  36. package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
  37. package/src/components/editor/SnapToolbar.tsx +20 -1
  38. package/src/components/editor/clipPathHelpers.ts +113 -0
  39. package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
  40. package/src/components/editor/colorGradingScopePatch.ts +57 -0
  41. package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
  42. package/src/components/editor/domEditOverlayCrop.ts +115 -0
  43. package/src/components/editor/domEditOverlayGeometry.ts +14 -0
  44. package/src/components/editor/domEditOverlayShape.ts +39 -0
  45. package/src/components/editor/domEditing.test.ts +2 -0
  46. package/src/components/editor/domEditingTypes.ts +3 -0
  47. package/src/components/editor/manualEditingAvailability.test.ts +0 -12
  48. package/src/components/editor/manualEditingAvailability.ts +0 -6
  49. package/src/components/editor/marqueeCommit.ts +3 -2
  50. package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
  51. package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
  52. package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
  53. package/src/components/editor/propertyPanelHelpers.ts +32 -125
  54. package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
  55. package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
  56. package/src/components/editor/propertyPanelTypes.ts +113 -0
  57. package/src/components/editor/snapTargetCollection.ts +2 -3
  58. package/src/components/editor/useDomEditCompositionRect.ts +68 -0
  59. package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
  60. package/src/components/editor/useDomEditOverlayRects.ts +5 -3
  61. package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
  62. package/src/components/nle/NLELayout.tsx +54 -51
  63. package/src/components/nle/NLEPreview.tsx +41 -5
  64. package/src/components/nle/TimelineResizeDivider.tsx +97 -0
  65. package/src/components/nle/usePreviewBlockDrop.ts +31 -7
  66. package/src/components/renders/RenderQueue.tsx +125 -24
  67. package/src/components/renders/RenderQueueItem.tsx +132 -64
  68. package/src/components/renders/useRenderQueue.ts +136 -24
  69. package/src/components/sidebar/AssetsTab.test.ts +87 -0
  70. package/src/components/sidebar/AssetsTab.tsx +113 -15
  71. package/src/components/sidebar/BlocksTab.tsx +2 -1
  72. package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
  73. package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
  74. package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
  75. package/src/components/storyboard/StoryboardView.tsx +6 -0
  76. package/src/components/studioColorGradingScope.ts +124 -0
  77. package/src/components/studioMediaJobs.ts +123 -0
  78. package/src/components/ui/Button.tsx +20 -11
  79. package/src/components/ui/HyperframesLoader.tsx +9 -2
  80. package/src/components/ui/SearchInput.tsx +53 -0
  81. package/src/components/ui/Tooltip.tsx +52 -6
  82. package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
  83. package/src/components/ui/useDialogBehavior.ts +83 -0
  84. package/src/contexts/PanelLayoutContext.tsx +3 -0
  85. package/src/contexts/StudioContext.tsx +7 -0
  86. package/src/hooks/domSelectionTestHarness.ts +1 -0
  87. package/src/hooks/useCompositionContentLoader.ts +40 -0
  88. package/src/hooks/useCropMode.ts +91 -0
  89. package/src/hooks/useDomEditCommits.test.tsx +2 -0
  90. package/src/hooks/useEditorSave.ts +14 -0
  91. package/src/hooks/useFileManager.ts +39 -25
  92. package/src/hooks/useFrameCapture.ts +12 -1
  93. package/src/hooks/usePanelLayout.ts +1 -0
  94. package/src/hooks/usePreviewPersistence.ts +24 -15
  95. package/src/hooks/useStudioContextValue.ts +5 -0
  96. package/src/hooks/useToast.ts +66 -13
  97. package/src/player/components/ShortcutsPanel.tsx +9 -0
  98. package/src/player/store/playerStore.ts +14 -0
  99. package/src/styles/studio.css +61 -0
  100. package/src/utils/sdkResolverShadow.test.ts +53 -3
  101. package/src/utils/sdkResolverShadow.ts +41 -1
  102. package/src/utils/studioPendingEdits.test.ts +43 -0
  103. package/src/utils/studioPendingEdits.ts +45 -0
  104. package/src/utils/studioUiPreferences.ts +4 -0
  105. package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
  106. package/dist/assets/index-DmkOvZns.css +0 -1
  107. package/dist/assets/index-SYSbQyK4.js +0 -396
  108. package/dist/chunk-JND3XUJL.js.map +0 -1
  109. /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
@@ -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
  /* ------------------------------------------------------------------ */
@@ -230,6 +160,16 @@ const BOX_SHADOW_PRESETS = {
230
160
 
231
161
  export type BoxShadowPreset = keyof typeof BOX_SHADOW_PRESETS | "custom";
232
162
 
163
+ export {
164
+ buildClipPathValue,
165
+ buildInsetClipPathSides,
166
+ buildInsetClipPathValue,
167
+ getClipPathInsetPx,
168
+ inferClipPathPreset,
169
+ parseInsetClipPathSides,
170
+ type ClipPathInsetSides,
171
+ } from "./clipPathHelpers";
172
+
233
173
  /* ------------------------------------------------------------------ */
234
174
  /* Shared types */
235
175
  /* ------------------------------------------------------------------ */
@@ -322,23 +262,24 @@ export function normalizeTextMetricValue(
322
262
  }
323
263
 
324
264
  function splitCssFunctions(value: string): string[] {
265
+ const source = value.trim();
325
266
  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
267
  let depth = 0;
268
+ let start = 0;
329
269
 
330
- for (const char of value.trim()) {
270
+ for (let index = 0; index < source.length; index += 1) {
271
+ const char = source[index];
331
272
  if (char === "(") depth += 1;
332
273
  if (char === ")") depth = Math.max(0, depth - 1);
333
274
  if (/\s/.test(char) && depth === 0) {
334
- if (current.trim()) functions.push(current.trim());
335
- current = "";
336
- continue;
275
+ const part = source.slice(start, index).trim();
276
+ if (part) functions.push(part);
277
+ start = index + 1;
337
278
  }
338
- current += char;
339
279
  }
340
280
 
341
- if (current.trim()) functions.push(current.trim());
281
+ const lastPart = source.slice(start).trim();
282
+ if (lastPart) functions.push(lastPart);
342
283
  return functions;
343
284
  }
344
285
 
@@ -383,23 +324,6 @@ export function buildBoxShadowPresetValue(
383
324
  return BOX_SHADOW_PRESETS[preset];
384
325
  }
385
326
 
386
- export function inferClipPathPreset(
387
- value: string | undefined,
388
- ): "none" | "inset" | "circle" | "custom" {
389
- const normalized = value?.trim();
390
- if (!normalized || normalized === "none") return "none";
391
- if (/^inset\(/i.test(normalized)) return "inset";
392
- if (/^circle\(/i.test(normalized)) return "circle";
393
- return "custom";
394
- }
395
-
396
- export function getClipPathInsetPx(value: string | undefined): number {
397
- const match = /^inset\(\s*(-?\d+(?:\.\d+)?)px\b/i.exec(value?.trim() ?? "");
398
- if (!match) return 0;
399
- const parsed = Number.parseFloat(match[1]);
400
- return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
401
- }
402
-
403
327
  export function buildStrokeWidthStyleUpdates(
404
328
  nextWidth: string,
405
329
  currentBorderStyle: string | undefined,
@@ -428,23 +352,6 @@ export function buildStrokeStyleUpdates(
428
352
  return updates;
429
353
  }
430
354
 
431
- export function buildClipPathValue(
432
- preset: "none" | "inset" | "circle" | "custom",
433
- radiusValue: number,
434
- fallback: string | undefined,
435
- ) {
436
- if (preset === "custom") return fallback?.trim() || "none";
437
- if (preset === "circle") return "circle(50% at 50% 50%)";
438
- if (preset === "inset") {
439
- return `inset(0 round ${formatNumericValue(Math.max(0, radiusValue))}px)`;
440
- }
441
- return "none";
442
- }
443
-
444
- export function buildInsetClipPathValue(insetPx: number, radiusValue: number): string {
445
- return `inset(${formatNumericValue(Math.max(0, insetPx))}px round ${formatNumericValue(Math.max(0, radiusValue))}px)`;
446
- }
447
-
448
355
  export function adjustNumericToken(
449
356
  value: string,
450
357
  direction: 1 | -1,
@@ -490,11 +397,11 @@ export function extractBackgroundImageUrl(value: string | undefined): string {
490
397
 
491
398
  // ── GSAP runtime value readers (used by PropertyPanel) ────────────────────
492
399
 
493
- // fallow-ignore-next-line complexity -- pre-existing; surfaced in this file's diff by an unrelated line shift
494
400
  // Core transform channels the panel ALWAYS reads live — even before a just-set
495
401
  // value (e.g. rotationX) has re-parsed into `gsapAnimations`. Without this the
496
402
  // cube + fields drop the prop and flicker to 0 on every commit; gsap.getProperty
497
403
  // reflects the in-place instant patch, so it's the true current value.
404
+ // fallow-ignore-next-line complexity
498
405
  const ALWAYS_READ_CHANNELS = [
499
406
  "x",
500
407
  "y",