@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.
- package/dist/assets/hyperframes-player-BBrKzTGd.js +459 -0
- package/dist/assets/index-0P10SwC_.css +1 -0
- package/dist/assets/{index-BXYJbIax.js → index-312a3Ceu.js} +1 -1
- package/dist/assets/index-B9YvRJz1.js +396 -0
- package/dist/assets/{index-Ct-X2wlf.js → index-CQHiecE7.js} +1 -1
- package/dist/{chunk-JND3XUJL.js → chunk-RCLGSZ7C.js} +1 -1
- package/dist/chunk-RCLGSZ7C.js.map +1 -0
- package/dist/{domEditingLayers-UIQZJCOA.js → domEditingLayers-S6YOLVRG.js} +2 -2
- package/dist/index.d.ts +48 -10
- package/dist/index.html +2 -2
- package/dist/index.js +6045 -3507
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -24
- package/src/components/AskAgentModal.tsx +23 -6
- package/src/components/LintModal.tsx +40 -10
- package/src/components/MediaPreview.tsx +36 -1
- package/src/components/SaveQueuePausedBanner.tsx +7 -6
- package/src/components/StudioErrorBoundary.tsx +19 -31
- package/src/components/StudioFeedbackBar.tsx +11 -2
- package/src/components/StudioGlobalDragOverlay.tsx +1 -1
- package/src/components/StudioHeader.tsx +184 -106
- package/src/components/StudioLeftSidebar.tsx +24 -3
- package/src/components/StudioOverlays.tsx +34 -8
- package/src/components/StudioPreviewArea.tsx +8 -0
- package/src/components/StudioRightPanel.tsx +160 -9
- package/src/components/StudioSplash.tsx +6 -3
- package/src/components/StudioToast.tsx +12 -11
- package/src/components/TimelineToolbar.test.tsx +9 -3
- package/src/components/TimelineToolbar.tsx +63 -29
- package/src/components/editor/DomEditCropHandles.tsx +238 -0
- package/src/components/editor/DomEditOverlay.tsx +111 -108
- package/src/components/editor/DomEditRotateHandle.tsx +41 -0
- package/src/components/editor/PropertyPanel.test.ts +48 -0
- package/src/components/editor/PropertyPanel.tsx +27 -47
- package/src/components/editor/PropertyPanelEmptyState.tsx +33 -0
- package/src/components/editor/SnapToolbar.tsx +20 -1
- package/src/components/editor/clipPathHelpers.ts +113 -0
- package/src/components/editor/colorGradingScopePatch.test.ts +74 -0
- package/src/components/editor/colorGradingScopePatch.ts +57 -0
- package/src/components/editor/domEditOverlayCrop.test.ts +106 -0
- package/src/components/editor/domEditOverlayCrop.ts +115 -0
- package/src/components/editor/domEditOverlayGeometry.ts +14 -0
- package/src/components/editor/domEditOverlayShape.ts +39 -0
- package/src/components/editor/domEditing.test.ts +2 -0
- package/src/components/editor/domEditingTypes.ts +3 -0
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +0 -6
- package/src/components/editor/marqueeCommit.ts +3 -2
- package/src/components/editor/propertyPanelColorGradingControls.tsx +371 -329
- package/src/components/editor/propertyPanelColorGradingSection.tsx +274 -55
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +275 -0
- package/src/components/editor/propertyPanelHelpers.ts +32 -125
- package/src/components/editor/propertyPanelMediaSection.tsx +253 -99
- package/src/components/editor/propertyPanelStyleSections.tsx +75 -1
- package/src/components/editor/propertyPanelTypes.ts +113 -0
- package/src/components/editor/snapTargetCollection.ts +2 -3
- package/src/components/editor/useDomEditCompositionRect.ts +68 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +13 -6
- package/src/components/editor/useDomEditOverlayRects.ts +5 -3
- package/src/components/nle/CompositionBreadcrumb.tsx +3 -2
- package/src/components/nle/NLELayout.tsx +54 -51
- package/src/components/nle/NLEPreview.tsx +41 -5
- package/src/components/nle/TimelineResizeDivider.tsx +97 -0
- package/src/components/nle/usePreviewBlockDrop.ts +31 -7
- package/src/components/renders/RenderQueue.tsx +125 -24
- package/src/components/renders/RenderQueueItem.tsx +132 -64
- package/src/components/renders/useRenderQueue.ts +136 -24
- package/src/components/sidebar/AssetsTab.test.ts +87 -0
- package/src/components/sidebar/AssetsTab.tsx +113 -15
- package/src/components/sidebar/BlocksTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +91 -0
- package/src/components/storyboard/StoryboardFrameFocus.tsx +39 -16
- package/src/components/storyboard/StoryboardLoaded.tsx +19 -2
- package/src/components/storyboard/StoryboardView.tsx +6 -0
- package/src/components/studioColorGradingScope.ts +124 -0
- package/src/components/studioMediaJobs.ts +123 -0
- package/src/components/ui/Button.tsx +20 -11
- package/src/components/ui/HyperframesLoader.tsx +9 -2
- package/src/components/ui/SearchInput.tsx +53 -0
- package/src/components/ui/Tooltip.tsx +52 -6
- package/src/components/ui/VideoFrameThumbnail.tsx +26 -3
- package/src/components/ui/useDialogBehavior.ts +83 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +7 -0
- package/src/hooks/domSelectionTestHarness.ts +1 -0
- package/src/hooks/useCompositionContentLoader.ts +40 -0
- package/src/hooks/useCropMode.ts +91 -0
- package/src/hooks/useDomEditCommits.test.tsx +2 -0
- package/src/hooks/useEditorSave.ts +14 -0
- package/src/hooks/useFileManager.ts +39 -25
- package/src/hooks/useFrameCapture.ts +12 -1
- package/src/hooks/usePanelLayout.ts +1 -0
- package/src/hooks/usePreviewPersistence.ts +24 -15
- package/src/hooks/useStudioContextValue.ts +5 -0
- package/src/hooks/useToast.ts +66 -13
- package/src/player/components/ShortcutsPanel.tsx +9 -0
- package/src/player/store/playerStore.ts +14 -0
- package/src/styles/studio.css +61 -0
- package/src/utils/sdkResolverShadow.test.ts +53 -3
- package/src/utils/sdkResolverShadow.ts +41 -1
- package/src/utils/studioPendingEdits.test.ts +43 -0
- package/src/utils/studioPendingEdits.ts +45 -0
- package/src/utils/studioUiPreferences.ts +4 -0
- package/dist/assets/hyperframes-player-BGuumSiM.js +0 -459
- package/dist/assets/index-DmkOvZns.css +0 -1
- package/dist/assets/index-SYSbQyK4.js +0 -396
- package/dist/chunk-JND3XUJL.js.map +0 -1
- /package/dist/{domEditingLayers-UIQZJCOA.js.map → domEditingLayers-S6YOLVRG.js.map} +0 -0
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { Check, ClipboardList, Film, Music } from "../../icons/SystemIcons";
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Check, ClipboardList, Film, Music, Scissors } from "../../icons/SystemIcons";
|
|
3
3
|
import type { DomEditSelection } from "./domEditing";
|
|
4
4
|
import {
|
|
5
|
+
type BackgroundRemovalProgress,
|
|
6
|
+
type BackgroundRemovalResult,
|
|
5
7
|
formatNumericValue,
|
|
6
8
|
formatTimingValue,
|
|
7
9
|
LABEL,
|
|
8
10
|
parseNumericValue,
|
|
9
11
|
RESPONSIVE_GRID,
|
|
12
|
+
stripQueryAndHash,
|
|
10
13
|
} from "./propertyPanelHelpers";
|
|
11
14
|
import { Section, SegmentedControl, SelectField, SliderControl } from "./propertyPanelPrimitives";
|
|
12
15
|
|
|
16
|
+
// fallow-ignore-next-line complexity
|
|
13
17
|
export function MediaSection({
|
|
14
18
|
projectDir,
|
|
15
19
|
element,
|
|
@@ -17,6 +21,7 @@ export function MediaSection({
|
|
|
17
21
|
onSetStyle,
|
|
18
22
|
onSetAttribute,
|
|
19
23
|
onSetHtmlAttribute,
|
|
24
|
+
onRemoveBackground,
|
|
20
25
|
}: {
|
|
21
26
|
projectDir: string | null;
|
|
22
27
|
element: DomEditSelection;
|
|
@@ -24,8 +29,19 @@ export function MediaSection({
|
|
|
24
29
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
25
30
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
26
31
|
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
32
|
+
onRemoveBackground?: (
|
|
33
|
+
inputPath: string,
|
|
34
|
+
options: {
|
|
35
|
+
createBackgroundPlate?: boolean;
|
|
36
|
+
quality?: "fast" | "balanced" | "best";
|
|
37
|
+
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
38
|
+
},
|
|
39
|
+
) => Promise<BackgroundRemovalResult>;
|
|
27
40
|
}) {
|
|
28
41
|
const isVideo = element.tagName === "video";
|
|
42
|
+
const isAudio = element.tagName === "audio";
|
|
43
|
+
const isImage = element.tagName === "img";
|
|
44
|
+
const isVisualMedia = isVideo || isImage;
|
|
29
45
|
const el = element.element;
|
|
30
46
|
|
|
31
47
|
const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
|
@@ -53,15 +69,64 @@ export function MediaSection({
|
|
|
53
69
|
|
|
54
70
|
const srcAttr = el.getAttribute("src") ?? "";
|
|
55
71
|
const [copied, setCopied] = useState(false);
|
|
72
|
+
const [removeBusy, setRemoveBusy] = useState(false);
|
|
73
|
+
const [removeProgress, setRemoveProgress] = useState<BackgroundRemovalProgress | null>(null);
|
|
74
|
+
const [createPlate, setCreatePlate] = useState(false);
|
|
75
|
+
const [quality, setQuality] = useState<"fast" | "balanced" | "best">("balanced");
|
|
56
76
|
|
|
57
77
|
const absoluteSrc =
|
|
58
78
|
projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
|
|
79
|
+
const projectSrc =
|
|
80
|
+
srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr)
|
|
81
|
+
? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr)
|
|
82
|
+
: "";
|
|
83
|
+
const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
|
|
84
|
+
const panelTitle = isImage ? "Image" : isVideo ? "Video" : "Audio";
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
setRemoveProgress(null);
|
|
88
|
+
setCreatePlate(false);
|
|
89
|
+
}, [srcAttr]);
|
|
90
|
+
|
|
91
|
+
const applyCutoutResult = async (result: BackgroundRemovalResult) => {
|
|
92
|
+
await onSetHtmlAttribute("src", result.outputPath);
|
|
93
|
+
if (isVideo) {
|
|
94
|
+
await onSetAttribute("has-audio", "");
|
|
95
|
+
await onSetHtmlAttribute("muted", "true");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const runBackgroundRemoval = async () => {
|
|
100
|
+
if (!onRemoveBackground || !projectSrc || removeBusy) return;
|
|
101
|
+
setRemoveBusy(true);
|
|
102
|
+
setRemoveProgress({ status: "processing", progress: 0, stage: "Preparing" });
|
|
103
|
+
try {
|
|
104
|
+
const result = await onRemoveBackground(projectSrc, {
|
|
105
|
+
createBackgroundPlate: isVideo && createPlate,
|
|
106
|
+
quality,
|
|
107
|
+
onProgress: setRemoveProgress,
|
|
108
|
+
});
|
|
109
|
+
await applyCutoutResult(result);
|
|
110
|
+
setRemoveProgress({
|
|
111
|
+
status: "complete",
|
|
112
|
+
progress: 100,
|
|
113
|
+
stage: "Applied cutout",
|
|
114
|
+
...result,
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
setRemoveProgress({
|
|
118
|
+
status: "failed",
|
|
119
|
+
progress: 0,
|
|
120
|
+
stage: "Failed",
|
|
121
|
+
error: error instanceof Error ? error.message : String(error),
|
|
122
|
+
});
|
|
123
|
+
} finally {
|
|
124
|
+
setRemoveBusy(false);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
59
127
|
|
|
60
128
|
return (
|
|
61
|
-
<Section
|
|
62
|
-
title={isVideo ? "Video" : "Audio"}
|
|
63
|
-
icon={isVideo ? <Film size={15} /> : <Music size={15} />}
|
|
64
|
-
>
|
|
129
|
+
<Section title={panelTitle} icon={isAudio ? <Music size={15} /> : <Film size={15} />}>
|
|
65
130
|
<div className="space-y-4">
|
|
66
131
|
{srcAttr && (
|
|
67
132
|
<div className="min-w-0">
|
|
@@ -90,103 +155,192 @@ export function MediaSection({
|
|
|
90
155
|
</div>
|
|
91
156
|
)}
|
|
92
157
|
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
step={5}
|
|
115
|
-
displayValue={`${formatNumericValue(playbackRate)}x`}
|
|
116
|
-
formatDisplayValue={(next) => `${formatNumericValue(next / 100)}x`}
|
|
117
|
-
onCommit={(next) => {
|
|
118
|
-
void onSetAttribute("playback-rate", formatNumericValue(next / 100));
|
|
119
|
-
}}
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<div className="grid min-w-0 gap-1.5">
|
|
124
|
-
<span className={LABEL}>Media start</span>
|
|
125
|
-
<SliderControl
|
|
126
|
-
value={Math.round(mediaStart * 100)}
|
|
127
|
-
min={0}
|
|
128
|
-
max={mediaStartMax * 100}
|
|
129
|
-
step={10}
|
|
130
|
-
displayValue={formatTimingValue(mediaStart)}
|
|
131
|
-
formatDisplayValue={(next) => formatTimingValue(next / 100)}
|
|
132
|
-
onCommit={(next) => {
|
|
133
|
-
void onSetAttribute("media-start", (next / 100).toFixed(2));
|
|
134
|
-
}}
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<div className={RESPONSIVE_GRID}>
|
|
139
|
-
<div className="grid min-w-0 gap-1.5">
|
|
140
|
-
<span className={LABEL}>Loop</span>
|
|
141
|
-
<SegmentedControl
|
|
142
|
-
value={hasLoop ? "on" : "off"}
|
|
143
|
-
onChange={(next) => {
|
|
144
|
-
void onSetHtmlAttribute("loop", next === "on" ? "true" : null);
|
|
145
|
-
}}
|
|
146
|
-
options={[
|
|
147
|
-
{ label: "On", value: "on" },
|
|
148
|
-
{ label: "Off", value: "off" },
|
|
149
|
-
]}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
152
|
-
<div className="grid min-w-0 gap-1.5">
|
|
153
|
-
<span className={LABEL}>Muted</span>
|
|
154
|
-
<SegmentedControl
|
|
155
|
-
value={hasMuted ? "on" : "off"}
|
|
156
|
-
onChange={(next) => {
|
|
157
|
-
void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
|
|
158
|
-
}}
|
|
159
|
-
options={[
|
|
160
|
-
{ label: "On", value: "on" },
|
|
161
|
-
{ label: "Off", value: "off" },
|
|
162
|
-
]}
|
|
163
|
-
/>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
{isVideo && (
|
|
168
|
-
<div className="grid min-w-0 gap-1.5">
|
|
169
|
-
<span className={LABEL}>Has audio track</span>
|
|
170
|
-
<SegmentedControl
|
|
171
|
-
value={hasAudio ? "yes" : "no"}
|
|
172
|
-
onChange={(next) => {
|
|
173
|
-
if (next === "yes") {
|
|
174
|
-
void onSetAttribute("has-audio", "true");
|
|
175
|
-
void onSetHtmlAttribute("muted", null);
|
|
176
|
-
} else {
|
|
177
|
-
void onSetAttribute("has-audio", "");
|
|
178
|
-
void onSetHtmlAttribute("muted", "true");
|
|
158
|
+
{isVisualMedia && (
|
|
159
|
+
<div className="grid min-w-0 max-w-full gap-2 overflow-hidden rounded-md bg-panel-input/30 p-2">
|
|
160
|
+
<div className="flex min-w-0 items-center justify-between gap-2">
|
|
161
|
+
<div className="min-w-0">
|
|
162
|
+
<div className={LABEL}>Cutout</div>
|
|
163
|
+
<div className="mt-0.5 truncate text-[10px] text-panel-text-4">
|
|
164
|
+
Create transparent {isVideo ? "WebM video" : "PNG image"}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
disabled={!canRemoveBackground || removeBusy}
|
|
170
|
+
onClick={(event) => {
|
|
171
|
+
event.stopPropagation();
|
|
172
|
+
void runBackgroundRemoval();
|
|
173
|
+
}}
|
|
174
|
+
className="flex h-8 flex-shrink-0 items-center gap-1.5 rounded-md bg-panel-input px-2.5 text-[11px] font-medium text-panel-text-2 transition-colors hover:bg-panel-hover hover:text-panel-text-1 disabled:cursor-not-allowed disabled:opacity-50"
|
|
175
|
+
title={
|
|
176
|
+
canRemoveBackground
|
|
177
|
+
? "Remove background and save a transparent asset"
|
|
178
|
+
: "Select a project-local image or video asset"
|
|
179
179
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
>
|
|
181
|
+
<Scissors size={13} />
|
|
182
|
+
<span>{removeBusy ? "Working" : "Remove BG"}</span>
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="grid grid-cols-2 gap-2">
|
|
187
|
+
<SelectField
|
|
188
|
+
label="Quality"
|
|
189
|
+
value={quality}
|
|
190
|
+
onChange={(next) => setQuality(next as typeof quality)}
|
|
191
|
+
options={["fast", "balanced", "best"]}
|
|
192
|
+
/>
|
|
193
|
+
{isVideo ? (
|
|
194
|
+
<div className="grid min-w-0 gap-1.5">
|
|
195
|
+
<span className={LABEL}>BG plate</span>
|
|
196
|
+
<SegmentedControl
|
|
197
|
+
value={createPlate ? "on" : "off"}
|
|
198
|
+
onChange={(next) => setCreatePlate(next === "on")}
|
|
199
|
+
options={[
|
|
200
|
+
{ label: "On", value: "on" },
|
|
201
|
+
{ label: "Off", value: "off" },
|
|
202
|
+
]}
|
|
203
|
+
/>
|
|
204
|
+
<span className="text-[10px] leading-tight text-panel-text-4">
|
|
205
|
+
Optional hole-cut background copy.
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
) : (
|
|
209
|
+
<div />
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{removeProgress && (
|
|
214
|
+
<div className="space-y-1">
|
|
215
|
+
<div className="flex min-w-0 items-center justify-between gap-2 text-[10px] text-panel-text-4">
|
|
216
|
+
<span className="min-w-0 flex-1 truncate">
|
|
217
|
+
{removeProgress.error ?? removeProgress.stage ?? "Processing"}
|
|
218
|
+
</span>
|
|
219
|
+
<span>{Math.round(removeProgress.progress)}%</span>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="h-1 overflow-hidden rounded-full bg-panel-border">
|
|
222
|
+
<div
|
|
223
|
+
className={`h-full rounded-full ${
|
|
224
|
+
removeProgress.status === "failed" ? "bg-red-400" : "bg-studio-accent"
|
|
225
|
+
}`}
|
|
226
|
+
style={{ width: `${Math.max(0, Math.min(100, removeProgress.progress))}%` }}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{removeProgress?.status === "complete" && removeProgress.outputPath && (
|
|
233
|
+
<div
|
|
234
|
+
className="truncate text-[10px] font-medium text-panel-text-3"
|
|
235
|
+
title={removeProgress.outputPath}
|
|
236
|
+
>
|
|
237
|
+
Applied {removeProgress.outputPath}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
186
240
|
</div>
|
|
187
241
|
)}
|
|
188
242
|
|
|
189
|
-
{isVideo && (
|
|
243
|
+
{(isVideo || isAudio) && (
|
|
244
|
+
<>
|
|
245
|
+
<div className="grid min-w-0 gap-1.5">
|
|
246
|
+
<span className={LABEL}>Volume</span>
|
|
247
|
+
<SliderControl
|
|
248
|
+
value={volumePercent}
|
|
249
|
+
min={0}
|
|
250
|
+
max={100}
|
|
251
|
+
step={1}
|
|
252
|
+
displayValue={`${volumePercent}%`}
|
|
253
|
+
formatDisplayValue={(next) => `${Math.round(next)}%`}
|
|
254
|
+
onCommit={(next) => {
|
|
255
|
+
void onSetAttribute("volume", formatNumericValue(next / 100));
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="grid min-w-0 gap-1.5">
|
|
261
|
+
<span className={LABEL}>Playback rate</span>
|
|
262
|
+
<SliderControl
|
|
263
|
+
value={playbackRate * 100}
|
|
264
|
+
min={25}
|
|
265
|
+
max={300}
|
|
266
|
+
step={5}
|
|
267
|
+
displayValue={`${formatNumericValue(playbackRate)}x`}
|
|
268
|
+
formatDisplayValue={(next) => `${formatNumericValue(next / 100)}x`}
|
|
269
|
+
onCommit={(next) => {
|
|
270
|
+
void onSetAttribute("playback-rate", formatNumericValue(next / 100));
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className="grid min-w-0 gap-1.5">
|
|
276
|
+
<span className={LABEL}>Media start</span>
|
|
277
|
+
<SliderControl
|
|
278
|
+
value={Math.round(mediaStart * 100)}
|
|
279
|
+
min={0}
|
|
280
|
+
max={mediaStartMax * 100}
|
|
281
|
+
step={10}
|
|
282
|
+
displayValue={formatTimingValue(mediaStart)}
|
|
283
|
+
formatDisplayValue={(next) => formatTimingValue(next / 100)}
|
|
284
|
+
onCommit={(next) => {
|
|
285
|
+
void onSetAttribute("media-start", (next / 100).toFixed(2));
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div className={RESPONSIVE_GRID}>
|
|
291
|
+
<div className="grid min-w-0 gap-1.5">
|
|
292
|
+
<span className={LABEL}>Loop</span>
|
|
293
|
+
<SegmentedControl
|
|
294
|
+
value={hasLoop ? "on" : "off"}
|
|
295
|
+
onChange={(next) => {
|
|
296
|
+
void onSetHtmlAttribute("loop", next === "on" ? "true" : null);
|
|
297
|
+
}}
|
|
298
|
+
options={[
|
|
299
|
+
{ label: "On", value: "on" },
|
|
300
|
+
{ label: "Off", value: "off" },
|
|
301
|
+
]}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
<div className="grid min-w-0 gap-1.5">
|
|
305
|
+
<span className={LABEL}>Muted</span>
|
|
306
|
+
<SegmentedControl
|
|
307
|
+
value={hasMuted ? "on" : "off"}
|
|
308
|
+
onChange={(next) => {
|
|
309
|
+
void onSetHtmlAttribute("muted", next === "on" ? "true" : null);
|
|
310
|
+
}}
|
|
311
|
+
options={[
|
|
312
|
+
{ label: "On", value: "on" },
|
|
313
|
+
{ label: "Off", value: "off" },
|
|
314
|
+
]}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{isVideo && (
|
|
320
|
+
<div className="grid min-w-0 gap-1.5">
|
|
321
|
+
<span className={LABEL}>Has audio track</span>
|
|
322
|
+
<SegmentedControl
|
|
323
|
+
value={hasAudio ? "yes" : "no"}
|
|
324
|
+
onChange={(next) => {
|
|
325
|
+
if (next === "yes") {
|
|
326
|
+
void onSetAttribute("has-audio", "true");
|
|
327
|
+
void onSetHtmlAttribute("muted", null);
|
|
328
|
+
} else {
|
|
329
|
+
void onSetAttribute("has-audio", "");
|
|
330
|
+
void onSetHtmlAttribute("muted", "true");
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
options={[
|
|
334
|
+
{ label: "Yes", value: "yes" },
|
|
335
|
+
{ label: "No", value: "no" },
|
|
336
|
+
]}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{isVisualMedia && (
|
|
190
344
|
<>
|
|
191
345
|
<div className={RESPONSIVE_GRID}>
|
|
192
346
|
<SelectField
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import { Eye, Layers, Palette, Settings, Square, Zap } from "../../icons/SystemIcons";
|
|
2
|
+
import { Eye, Layers, Palette, Scissors, Settings, Square, Zap } from "../../icons/SystemIcons";
|
|
3
3
|
import { buildDefaultGradientModel, serializeGradient } from "./gradientValue";
|
|
4
4
|
import { isTextEditableSelection, type DomEditSelection } from "./domEditing";
|
|
5
5
|
import {
|
|
6
6
|
buildBoxShadowPresetValue,
|
|
7
7
|
buildClipPathValue,
|
|
8
|
+
buildInsetClipPathSides,
|
|
8
9
|
buildInsetClipPathValue,
|
|
9
10
|
buildStrokeStyleUpdates,
|
|
10
11
|
buildStrokeWidthStyleUpdates,
|
|
@@ -17,10 +18,12 @@ import {
|
|
|
17
18
|
inferClipPathPreset,
|
|
18
19
|
LABEL,
|
|
19
20
|
normalizePanelPxValue,
|
|
21
|
+
parseInsetClipPathSides,
|
|
20
22
|
parseNumericValue,
|
|
21
23
|
parsePxMetricValue,
|
|
22
24
|
RESPONSIVE_GRID,
|
|
23
25
|
setCssFilterFunctionPx,
|
|
26
|
+
type ClipPathInsetSides,
|
|
24
27
|
type BoxShadowPreset,
|
|
25
28
|
} from "./propertyPanelHelpers";
|
|
26
29
|
import {
|
|
@@ -35,6 +38,7 @@ import { ColorField } from "./propertyPanelColor";
|
|
|
35
38
|
import { GradientField, ImageFillField } from "./propertyPanelFill";
|
|
36
39
|
import { BorderRadiusEditor } from "./BorderRadiusEditor";
|
|
37
40
|
|
|
41
|
+
// fallow-ignore-next-line complexity
|
|
38
42
|
export function StyleSections({
|
|
39
43
|
projectId,
|
|
40
44
|
element,
|
|
@@ -43,6 +47,8 @@ export function StyleSections({
|
|
|
43
47
|
onSetStyle,
|
|
44
48
|
onImportAssets,
|
|
45
49
|
gsapBorderRadius,
|
|
50
|
+
cropMode = false,
|
|
51
|
+
onCropModeChange,
|
|
46
52
|
}: {
|
|
47
53
|
projectId: string;
|
|
48
54
|
element: DomEditSelection;
|
|
@@ -51,6 +57,8 @@ export function StyleSections({
|
|
|
51
57
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
52
58
|
onImportAssets?: (files: FileList) => Promise<string[]>;
|
|
53
59
|
gsapBorderRadius?: { tl: number; tr: number; br: number; bl: number } | null;
|
|
60
|
+
cropMode?: boolean;
|
|
61
|
+
onCropModeChange?: (active: boolean) => void;
|
|
54
62
|
}) {
|
|
55
63
|
const styleEditingDisabled = !element.capabilities.canEditStyles;
|
|
56
64
|
const isFlex = styles.display === "flex" || styles.display === "inline-flex";
|
|
@@ -86,7 +94,16 @@ export function StyleSections({
|
|
|
86
94
|
const backdropBlurValue = getCssFilterFunctionPx(styles["backdrop-filter"], "blur");
|
|
87
95
|
const clipPathValue = styles["clip-path"] || "none";
|
|
88
96
|
const clipPathPreset = inferClipPathPreset(clipPathValue);
|
|
97
|
+
const parsedClipInsets = parseInsetClipPathSides(clipPathValue);
|
|
89
98
|
const clipInsetValue = getClipPathInsetPx(clipPathValue);
|
|
99
|
+
const clipInsetSides = parsedClipInsets ?? {
|
|
100
|
+
top: clipInsetValue,
|
|
101
|
+
right: clipInsetValue,
|
|
102
|
+
bottom: clipInsetValue,
|
|
103
|
+
left: clipInsetValue,
|
|
104
|
+
radius: radiusValue,
|
|
105
|
+
};
|
|
106
|
+
const showClipInsetSides = clipPathPreset === "inset" || parsedClipInsets != null;
|
|
90
107
|
const backgroundImage = styles["background-image"] ?? "none";
|
|
91
108
|
const hasTextControls = isTextEditableSelection(element);
|
|
92
109
|
|
|
@@ -117,6 +134,19 @@ export function StyleSections({
|
|
|
117
134
|
}
|
|
118
135
|
};
|
|
119
136
|
|
|
137
|
+
const commitClipInsetSide = (side: keyof ClipPathInsetSides, nextValue: string) => {
|
|
138
|
+
const next = parsePxMetricValue(nextValue);
|
|
139
|
+
if (next == null) return;
|
|
140
|
+
const sides: ClipPathInsetSides = {
|
|
141
|
+
top: clipInsetSides.top,
|
|
142
|
+
right: clipInsetSides.right,
|
|
143
|
+
bottom: clipInsetSides.bottom,
|
|
144
|
+
left: clipInsetSides.left,
|
|
145
|
+
};
|
|
146
|
+
sides[side] = next;
|
|
147
|
+
onSetStyle("clip-path", buildInsetClipPathSides(sides, clipInsetSides.radius));
|
|
148
|
+
};
|
|
149
|
+
|
|
120
150
|
return (
|
|
121
151
|
<>
|
|
122
152
|
{isFlex && (
|
|
@@ -344,6 +374,50 @@ export function StyleSections({
|
|
|
344
374
|
}
|
|
345
375
|
/>
|
|
346
376
|
</div>
|
|
377
|
+
{showClipInsetSides && (
|
|
378
|
+
<div className="grid gap-2">
|
|
379
|
+
<div className="grid grid-cols-4 gap-2">
|
|
380
|
+
<MetricField
|
|
381
|
+
label="T"
|
|
382
|
+
value={formatPxMetricValue(clipInsetSides.top)}
|
|
383
|
+
disabled={styleEditingDisabled}
|
|
384
|
+
onCommit={(next) => commitClipInsetSide("top", next)}
|
|
385
|
+
/>
|
|
386
|
+
<MetricField
|
|
387
|
+
label="R"
|
|
388
|
+
value={formatPxMetricValue(clipInsetSides.right)}
|
|
389
|
+
disabled={styleEditingDisabled}
|
|
390
|
+
onCommit={(next) => commitClipInsetSide("right", next)}
|
|
391
|
+
/>
|
|
392
|
+
<MetricField
|
|
393
|
+
label="B"
|
|
394
|
+
value={formatPxMetricValue(clipInsetSides.bottom)}
|
|
395
|
+
disabled={styleEditingDisabled}
|
|
396
|
+
onCommit={(next) => commitClipInsetSide("bottom", next)}
|
|
397
|
+
/>
|
|
398
|
+
<MetricField
|
|
399
|
+
label="L"
|
|
400
|
+
value={formatPxMetricValue(clipInsetSides.left)}
|
|
401
|
+
disabled={styleEditingDisabled}
|
|
402
|
+
onCommit={(next) => commitClipInsetSide("left", next)}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
disabled={styleEditingDisabled || !onCropModeChange}
|
|
410
|
+
aria-pressed={cropMode}
|
|
411
|
+
onClick={() => onCropModeChange?.(!cropMode)}
|
|
412
|
+
className={`inline-flex h-8 w-fit items-center gap-1.5 rounded-md border px-2 text-[11px] font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
|
|
413
|
+
cropMode
|
|
414
|
+
? "border-studio-accent/60 bg-studio-accent/15 text-studio-accent"
|
|
415
|
+
: "border-panel-border bg-panel-input text-panel-text-2 hover:text-white"
|
|
416
|
+
}`}
|
|
417
|
+
>
|
|
418
|
+
<Scissors size={13} />
|
|
419
|
+
Crop
|
|
420
|
+
</button>
|
|
347
421
|
</div>
|
|
348
422
|
</Section>
|
|
349
423
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { ArcPathSegment, GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
3
|
+
import type { DomEditSelection } from "./domEditing";
|
|
4
|
+
import type { ImportedFontAsset } from "./fontAssets";
|
|
5
|
+
|
|
6
|
+
export interface BackgroundRemovalProgress {
|
|
7
|
+
status: "processing" | "complete" | "failed";
|
|
8
|
+
progress: number;
|
|
9
|
+
stage?: string;
|
|
10
|
+
outputPath?: string;
|
|
11
|
+
backgroundOutputPath?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
provider?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BackgroundRemovalResult {
|
|
17
|
+
outputPath: string;
|
|
18
|
+
backgroundOutputPath?: string;
|
|
19
|
+
provider?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PropertyPanelProps {
|
|
23
|
+
projectId: string;
|
|
24
|
+
projectDir: string | null;
|
|
25
|
+
assets: string[];
|
|
26
|
+
element: DomEditSelection | null;
|
|
27
|
+
multiSelectCount?: number;
|
|
28
|
+
copiedAgentPrompt: boolean;
|
|
29
|
+
onClearSelection: () => void;
|
|
30
|
+
onUngroup?: () => void;
|
|
31
|
+
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
32
|
+
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
33
|
+
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
|
|
34
|
+
onApplyColorGradingScope?: (
|
|
35
|
+
scope: "source-file" | "project",
|
|
36
|
+
value: string | null,
|
|
37
|
+
) => Promise<{ changedFiles: number; changedElements: number }>;
|
|
38
|
+
onSetHtmlAttribute: (attr: string, value: string | null) => void | Promise<void>;
|
|
39
|
+
onRemoveBackground?: (
|
|
40
|
+
inputPath: string,
|
|
41
|
+
options: {
|
|
42
|
+
createBackgroundPlate?: boolean;
|
|
43
|
+
quality?: "fast" | "balanced" | "best";
|
|
44
|
+
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
45
|
+
},
|
|
46
|
+
) => Promise<BackgroundRemovalResult>;
|
|
47
|
+
onSetManualOffset: (element: DomEditSelection, next: { x: number; y: number }) => void;
|
|
48
|
+
onSetManualSize: (element: DomEditSelection, next: { width: number; height: number }) => void;
|
|
49
|
+
onSetManualRotation: (element: DomEditSelection, next: { angle: number }) => void;
|
|
50
|
+
onSetText: (value: string, fieldKey?: string) => void;
|
|
51
|
+
onSetTextFieldStyle: (fieldKey: string, property: string, value: string) => void;
|
|
52
|
+
onAddTextField: (afterFieldKey?: string) => string | Promise<string | null> | null;
|
|
53
|
+
onRemoveTextField: (fieldKey: string) => void;
|
|
54
|
+
onAskAgent: () => void;
|
|
55
|
+
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
|
|
56
|
+
fontAssets?: ImportedFontAsset[];
|
|
57
|
+
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
58
|
+
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
|
|
59
|
+
gsapAnimations?: GsapAnimation[];
|
|
60
|
+
gsapMultipleTimelines?: boolean;
|
|
61
|
+
gsapUnsupportedTimelinePattern?: boolean;
|
|
62
|
+
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
|
63
|
+
onUpdateGsapMeta?: (
|
|
64
|
+
animId: string,
|
|
65
|
+
updates: { duration?: number; ease?: string; position?: number },
|
|
66
|
+
) => void;
|
|
67
|
+
onDeleteGsapAnimation?: (animId: string) => void;
|
|
68
|
+
onAddGsapProperty?: (animId: string, prop: string) => void;
|
|
69
|
+
onRemoveGsapProperty?: (animId: string, prop: string) => void;
|
|
70
|
+
onUpdateGsapFromProperty?: (animId: string, prop: string, value: number | string) => void;
|
|
71
|
+
onAddGsapFromProperty?: (animId: string, prop: string) => void;
|
|
72
|
+
onRemoveGsapFromProperty?: (animId: string, prop: string) => void;
|
|
73
|
+
onAddGsapAnimation?: (method: "to" | "from" | "set" | "fromTo") => void;
|
|
74
|
+
onSetArcPath?: (
|
|
75
|
+
animId: string,
|
|
76
|
+
config: {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
autoRotate?: boolean | number;
|
|
79
|
+
segments?: ArcPathSegment[];
|
|
80
|
+
},
|
|
81
|
+
) => void;
|
|
82
|
+
onUpdateArcSegment?: (
|
|
83
|
+
animId: string,
|
|
84
|
+
segmentIndex: number,
|
|
85
|
+
update: Partial<ArcPathSegment>,
|
|
86
|
+
) => void;
|
|
87
|
+
onUnroll?: (animationId: string) => void;
|
|
88
|
+
onAddKeyframe?: (
|
|
89
|
+
animationId: string,
|
|
90
|
+
percentage: number,
|
|
91
|
+
property: string,
|
|
92
|
+
value: number | string,
|
|
93
|
+
) => void;
|
|
94
|
+
onRemoveKeyframe?: (animationId: string, percentage: number) => void;
|
|
95
|
+
onUpdateKeyframeEase?: (animationId: string, percentage: number, ease: string) => void;
|
|
96
|
+
onSetAllKeyframeEases?: (animationId: string, ease: string) => void;
|
|
97
|
+
onConvertToKeyframes?: (animationId: string, duration?: number) => void;
|
|
98
|
+
onCommitAnimatedProperty?: (
|
|
99
|
+
selection: DomEditSelection,
|
|
100
|
+
property: string,
|
|
101
|
+
value: number | string,
|
|
102
|
+
) => Promise<void>;
|
|
103
|
+
onCommitAnimatedProperties?: (
|
|
104
|
+
selection: DomEditSelection,
|
|
105
|
+
props: Record<string, number | string>,
|
|
106
|
+
) => Promise<void>;
|
|
107
|
+
onSeekToTime?: (time: number) => void;
|
|
108
|
+
recordingState?: "idle" | "recording" | "preview";
|
|
109
|
+
recordingDuration?: number;
|
|
110
|
+
onToggleRecording?: () => void;
|
|
111
|
+
cropMode?: boolean;
|
|
112
|
+
onCropModeChange?: (active: boolean) => void;
|
|
113
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// fallow-ignore-file unused-file
|
|
2
1
|
// fallow-ignore-file code-duplication
|
|
3
2
|
import type { DomEditSelection } from "./domEditing";
|
|
4
3
|
import {
|
|
5
4
|
isElementVisibleForOverlay,
|
|
6
|
-
|
|
5
|
+
toVisibleOverlayRect,
|
|
7
6
|
type OverlayRect,
|
|
8
7
|
} from "./domEditOverlayGeometry";
|
|
9
8
|
import {
|
|
@@ -106,7 +105,7 @@ export function collectSnapContext(input: {
|
|
|
106
105
|
id: string;
|
|
107
106
|
}> = [];
|
|
108
107
|
for (let i = 0; i < elements.length; i++) {
|
|
109
|
-
const rect =
|
|
108
|
+
const rect = toVisibleOverlayRect(input.overlayEl, input.iframe, elements[i]);
|
|
110
109
|
if (rect) entries.push({ rect, id: `snap-target-${i}` });
|
|
111
110
|
}
|
|
112
111
|
|