@hyperframes/studio 0.7.58 → 0.7.60
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-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
|
|
3
|
+
import { Check, ClipboardList } from "../../icons/SystemIcons";
|
|
4
|
+
import type { DomEditSelection } from "./domEditing";
|
|
5
|
+
import {
|
|
6
|
+
type BackgroundRemovalProgress,
|
|
7
|
+
type BackgroundRemovalResult,
|
|
8
|
+
formatNumericValue,
|
|
9
|
+
formatTimingValue,
|
|
10
|
+
parseNumericValue,
|
|
11
|
+
stripQueryAndHash,
|
|
12
|
+
} from "./propertyPanelHelpers";
|
|
13
|
+
import { FlatSelectRow, FlatSlider } from "./propertyPanelFlatPrimitives";
|
|
14
|
+
import { FlatToggle } from "./propertyPanelFlatToggle";
|
|
15
|
+
|
|
16
|
+
// fallow-ignore-next-line complexity
|
|
17
|
+
export function FlatMediaSection({
|
|
18
|
+
projectDir,
|
|
19
|
+
element,
|
|
20
|
+
styles,
|
|
21
|
+
onSetStyle,
|
|
22
|
+
onSetAttribute,
|
|
23
|
+
onSetHtmlAttribute,
|
|
24
|
+
onRemoveBackground,
|
|
25
|
+
}: {
|
|
26
|
+
projectDir: string | null;
|
|
27
|
+
element: DomEditSelection;
|
|
28
|
+
styles: Record<string, string>;
|
|
29
|
+
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
30
|
+
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
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>;
|
|
40
|
+
}) {
|
|
41
|
+
const track = useTrackDesignInput();
|
|
42
|
+
const isVideo = element.tagName === "video";
|
|
43
|
+
const isAudio = element.tagName === "audio";
|
|
44
|
+
const isImage = element.tagName === "img";
|
|
45
|
+
const isVisualMedia = isVideo || isImage;
|
|
46
|
+
const el = element.element;
|
|
47
|
+
|
|
48
|
+
const volume = parseNumericValue(element.dataAttributes.volume ?? "") ?? 1;
|
|
49
|
+
const volumePercent = Math.round(volume * 100);
|
|
50
|
+
const mediaStart =
|
|
51
|
+
Number.parseFloat(
|
|
52
|
+
element.dataAttributes["media-start"] ?? element.dataAttributes["playback-start"] ?? "0",
|
|
53
|
+
) || 0;
|
|
54
|
+
const playbackRate = Number.parseFloat(element.dataAttributes["playback-rate"] ?? "1") || 1;
|
|
55
|
+
const sourceDuration =
|
|
56
|
+
Number.parseFloat(element.dataAttributes["source-duration"] ?? "") ||
|
|
57
|
+
(el as HTMLMediaElement).duration ||
|
|
58
|
+
0;
|
|
59
|
+
const mediaStartMax = Math.max(30, Math.ceil(sourceDuration || mediaStart + 10));
|
|
60
|
+
const hasLoop = el.hasAttribute("loop");
|
|
61
|
+
const hasMuted = el.hasAttribute("muted");
|
|
62
|
+
const hasAudio = element.dataAttributes["has-audio"] === "true";
|
|
63
|
+
const objectFit = styles["object-fit"] || "contain";
|
|
64
|
+
const objectPosition = styles["object-position"] || "center";
|
|
65
|
+
|
|
66
|
+
const srcAttr = el.getAttribute("src") ?? "";
|
|
67
|
+
const [copied, setCopied] = useState(false);
|
|
68
|
+
const [removeBusy, setRemoveBusy] = useState(false);
|
|
69
|
+
const [removeProgress, setRemoveProgress] = useState<BackgroundRemovalProgress | null>(null);
|
|
70
|
+
const [createPlate, setCreatePlate] = useState(false);
|
|
71
|
+
const [quality, setQuality] = useState<"fast" | "balanced" | "best">("balanced");
|
|
72
|
+
|
|
73
|
+
const absoluteSrc =
|
|
74
|
+
projectDir && srcAttr && !srcAttr.startsWith("http") ? `${projectDir}/${srcAttr}` : srcAttr;
|
|
75
|
+
const projectSrc =
|
|
76
|
+
srcAttr && !/^(?:https?:|data:|blob:)/i.test(srcAttr)
|
|
77
|
+
? stripQueryAndHash(srcAttr.startsWith("./") ? srcAttr.slice(2) : srcAttr)
|
|
78
|
+
: "";
|
|
79
|
+
const canRemoveBackground = Boolean(onRemoveBackground && isVisualMedia && projectSrc);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setRemoveProgress(null);
|
|
83
|
+
setCreatePlate(false);
|
|
84
|
+
}, [srcAttr]);
|
|
85
|
+
|
|
86
|
+
const applyCutoutResult = async (result: BackgroundRemovalResult) => {
|
|
87
|
+
await onSetHtmlAttribute("src", result.outputPath);
|
|
88
|
+
if (isVideo) {
|
|
89
|
+
await onSetAttribute("has-audio", "");
|
|
90
|
+
await onSetHtmlAttribute("muted", "true");
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const runBackgroundRemoval = async () => {
|
|
95
|
+
if (!onRemoveBackground || !projectSrc || removeBusy) return;
|
|
96
|
+
track("button", "Remove background");
|
|
97
|
+
setRemoveBusy(true);
|
|
98
|
+
setRemoveProgress({ status: "processing", progress: 0, stage: "Preparing" });
|
|
99
|
+
try {
|
|
100
|
+
const result = await onRemoveBackground(projectSrc, {
|
|
101
|
+
createBackgroundPlate: isVideo && createPlate,
|
|
102
|
+
quality,
|
|
103
|
+
onProgress: setRemoveProgress,
|
|
104
|
+
});
|
|
105
|
+
await applyCutoutResult(result);
|
|
106
|
+
setRemoveProgress({ status: "complete", progress: 100, stage: "Applied cutout", ...result });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
setRemoveProgress({
|
|
109
|
+
status: "failed",
|
|
110
|
+
progress: 0,
|
|
111
|
+
stage: "Failed",
|
|
112
|
+
error: error instanceof Error ? error.message : String(error),
|
|
113
|
+
});
|
|
114
|
+
} finally {
|
|
115
|
+
setRemoveBusy(false);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="space-y-1.5">
|
|
121
|
+
<div className="flex min-h-8 items-center justify-between gap-2">
|
|
122
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
123
|
+
<span className="h-5 w-8 flex-shrink-0 rounded-[3px] bg-panel-surface" />
|
|
124
|
+
<span className="min-w-0 truncate font-mono text-[11px] text-panel-text-0">
|
|
125
|
+
{srcAttr}
|
|
126
|
+
</span>
|
|
127
|
+
</span>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
data-flat-media-copy="true"
|
|
131
|
+
onClick={() => {
|
|
132
|
+
track("button", "Copy media path");
|
|
133
|
+
void navigator.clipboard.writeText(absoluteSrc).then(() => {
|
|
134
|
+
setCopied(true);
|
|
135
|
+
setTimeout(() => setCopied(false), 1500);
|
|
136
|
+
});
|
|
137
|
+
}}
|
|
138
|
+
className="flex flex-shrink-0 items-center gap-1 text-[10px] text-panel-text-3 hover:text-panel-text-1"
|
|
139
|
+
>
|
|
140
|
+
{copied ? <Check size={11} /> : <ClipboardList size={11} />}
|
|
141
|
+
{copied ? "Copied" : "Copy"}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
{isVisualMedia && (
|
|
145
|
+
<div className="ml-[1px] border-l-2 border-panel-border-input py-1 pl-[10px]">
|
|
146
|
+
<div className="flex min-h-6 items-center justify-between">
|
|
147
|
+
<span className="flex items-baseline gap-[7px]">
|
|
148
|
+
<span className="text-[11px] font-semibold text-panel-text-1">Cutout</span>
|
|
149
|
+
<span className="font-mono text-[9px] text-panel-text-4">
|
|
150
|
+
transparent {isVideo ? "WebM" : "PNG"}
|
|
151
|
+
</span>
|
|
152
|
+
</span>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
data-flat-media-remove-bg="true"
|
|
156
|
+
disabled={!canRemoveBackground || removeBusy}
|
|
157
|
+
onClick={() => void runBackgroundRemoval()}
|
|
158
|
+
className="flex items-center gap-1 text-[10px] font-medium text-panel-accent disabled:cursor-not-allowed disabled:opacity-50"
|
|
159
|
+
title={
|
|
160
|
+
canRemoveBackground
|
|
161
|
+
? "Remove background and save a transparent asset"
|
|
162
|
+
: "Select a project-local image or video asset"
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
{removeBusy ? "Working" : "Remove BG"}
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
<FlatSelectRow
|
|
169
|
+
label="Quality"
|
|
170
|
+
value={quality}
|
|
171
|
+
options={["fast", "balanced", "best"]}
|
|
172
|
+
tier="explicitDefault"
|
|
173
|
+
onChange={(next) => setQuality(next as typeof quality)}
|
|
174
|
+
/>
|
|
175
|
+
{isVideo && (
|
|
176
|
+
<FlatToggle label="BG plate" checked={createPlate} onChange={setCreatePlate} />
|
|
177
|
+
)}
|
|
178
|
+
{removeProgress && (
|
|
179
|
+
<div className="mt-1 space-y-1">
|
|
180
|
+
<div className="flex items-center justify-between text-[10px] text-panel-text-4">
|
|
181
|
+
<span className="min-w-0 flex-1 truncate">
|
|
182
|
+
{removeProgress.error ?? removeProgress.stage ?? "Processing"}
|
|
183
|
+
</span>
|
|
184
|
+
<span>{Math.round(removeProgress.progress)}%</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="h-1 overflow-hidden rounded-full bg-panel-hover">
|
|
187
|
+
<div
|
|
188
|
+
className={`h-full rounded-full ${
|
|
189
|
+
removeProgress.status === "failed" ? "bg-red-400" : "bg-panel-accent"
|
|
190
|
+
}`}
|
|
191
|
+
style={{ width: `${Math.max(0, Math.min(100, removeProgress.progress))}%` }}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
{(isVideo || isAudio) && (
|
|
199
|
+
<>
|
|
200
|
+
<FlatSlider
|
|
201
|
+
label="Volume"
|
|
202
|
+
value={volumePercent}
|
|
203
|
+
min={0}
|
|
204
|
+
max={100}
|
|
205
|
+
tier={volumePercent === 100 ? "default" : "explicitCustom"}
|
|
206
|
+
displayValue={`${volumePercent}%`}
|
|
207
|
+
onCommit={(next) => void onSetAttribute("volume", formatNumericValue(next / 100))}
|
|
208
|
+
/>
|
|
209
|
+
<FlatSlider
|
|
210
|
+
label="Rate"
|
|
211
|
+
value={playbackRate * 100}
|
|
212
|
+
min={25}
|
|
213
|
+
max={300}
|
|
214
|
+
tier={playbackRate === 1 ? "default" : "explicitCustom"}
|
|
215
|
+
displayValue={`${formatNumericValue(playbackRate)}x`}
|
|
216
|
+
onCommit={(next) =>
|
|
217
|
+
void onSetAttribute("playback-rate", formatNumericValue(next / 100))
|
|
218
|
+
}
|
|
219
|
+
/>
|
|
220
|
+
<FlatSlider
|
|
221
|
+
label="Media start"
|
|
222
|
+
value={Math.round(mediaStart * 100)}
|
|
223
|
+
min={0}
|
|
224
|
+
max={mediaStartMax * 100}
|
|
225
|
+
tier={mediaStart === 0 ? "default" : "explicitCustom"}
|
|
226
|
+
displayValue={formatTimingValue(mediaStart)}
|
|
227
|
+
onCommit={(next) => void onSetAttribute("media-start", (next / 100).toFixed(2))}
|
|
228
|
+
/>
|
|
229
|
+
<FlatToggle
|
|
230
|
+
label="Loop"
|
|
231
|
+
checked={hasLoop}
|
|
232
|
+
onChange={(next) => void onSetHtmlAttribute("loop", next ? "true" : null)}
|
|
233
|
+
/>
|
|
234
|
+
<FlatToggle
|
|
235
|
+
label="Muted"
|
|
236
|
+
checked={hasMuted}
|
|
237
|
+
onChange={(next) => void onSetHtmlAttribute("muted", next ? "true" : null)}
|
|
238
|
+
/>
|
|
239
|
+
{isVideo && (
|
|
240
|
+
<FlatToggle
|
|
241
|
+
label="Has audio track"
|
|
242
|
+
checked={hasAudio}
|
|
243
|
+
onChange={(next) => {
|
|
244
|
+
if (next) {
|
|
245
|
+
void onSetAttribute("has-audio", "true");
|
|
246
|
+
void onSetHtmlAttribute("muted", null);
|
|
247
|
+
} else {
|
|
248
|
+
void onSetAttribute("has-audio", "");
|
|
249
|
+
void onSetHtmlAttribute("muted", "true");
|
|
250
|
+
}
|
|
251
|
+
}}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
</>
|
|
255
|
+
)}
|
|
256
|
+
{isVisualMedia && (
|
|
257
|
+
<>
|
|
258
|
+
<FlatSelectRow
|
|
259
|
+
label="Fit"
|
|
260
|
+
value={objectFit}
|
|
261
|
+
options={["contain", "cover", "fill", "none", "scale-down"]}
|
|
262
|
+
tier={objectFit === "contain" ? "default" : "explicitCustom"}
|
|
263
|
+
onChange={(next) => void onSetStyle("object-fit", next)}
|
|
264
|
+
/>
|
|
265
|
+
<FlatSelectRow
|
|
266
|
+
label="Position"
|
|
267
|
+
value={objectPosition}
|
|
268
|
+
options={[
|
|
269
|
+
"center",
|
|
270
|
+
"top",
|
|
271
|
+
"bottom",
|
|
272
|
+
"left",
|
|
273
|
+
"right",
|
|
274
|
+
"left top",
|
|
275
|
+
"right top",
|
|
276
|
+
"left bottom",
|
|
277
|
+
"right bottom",
|
|
278
|
+
]}
|
|
279
|
+
tier={objectPosition === "center" ? "default" : "explicitCustom"}
|
|
280
|
+
onChange={(next) => void onSetStyle("object-position", next)}
|
|
281
|
+
/>
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { FlatMotionSection, FlatTimingRow } from "./propertyPanelFlatMotionSection";
|
|
7
|
+
import type { DomEditSelection } from "./domEditing";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.body.innerHTML = "";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function baseElement(overrides: Partial<DomEditSelection> = {}): DomEditSelection {
|
|
16
|
+
return {
|
|
17
|
+
element: document.createElement("div"),
|
|
18
|
+
id: "hero",
|
|
19
|
+
selector: "#hero",
|
|
20
|
+
label: "Hero",
|
|
21
|
+
tagName: "div",
|
|
22
|
+
sourceFile: "index.html",
|
|
23
|
+
compositionPath: "index.html",
|
|
24
|
+
isCompositionHost: false,
|
|
25
|
+
isInsideLockedComposition: false,
|
|
26
|
+
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
|
27
|
+
textContent: "",
|
|
28
|
+
dataAttributes: { start: "8", duration: "4" },
|
|
29
|
+
inlineStyles: {},
|
|
30
|
+
computedStyles: {},
|
|
31
|
+
textFields: [],
|
|
32
|
+
capabilities: {
|
|
33
|
+
canSelect: true,
|
|
34
|
+
canEditStyles: true,
|
|
35
|
+
canCrop: true,
|
|
36
|
+
canMove: true,
|
|
37
|
+
canResize: true,
|
|
38
|
+
canApplyManualOffset: true,
|
|
39
|
+
canApplyManualSize: true,
|
|
40
|
+
canApplyManualRotation: true,
|
|
41
|
+
},
|
|
42
|
+
...overrides,
|
|
43
|
+
} as DomEditSelection;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderInto(node: React.ReactElement) {
|
|
47
|
+
const host = document.createElement("div");
|
|
48
|
+
document.body.append(host);
|
|
49
|
+
const root = createRoot(host);
|
|
50
|
+
act(() => {
|
|
51
|
+
root.render(node);
|
|
52
|
+
});
|
|
53
|
+
return { host, root };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("FlatTimingRow", () => {
|
|
57
|
+
it("renders Start, End, and Duration from the element's data attributes", () => {
|
|
58
|
+
const { host, root } = renderInto(
|
|
59
|
+
<FlatTimingRow element={baseElement()} onSetAttribute={vi.fn()} />,
|
|
60
|
+
);
|
|
61
|
+
expect(host.textContent).toContain("Start");
|
|
62
|
+
expect(host.textContent).toContain("End");
|
|
63
|
+
expect(host.textContent).toContain("Duration");
|
|
64
|
+
// Values render inside <input>s (CommitField), not as text nodes, so they
|
|
65
|
+
// don't show up in textContent — assert on the rendered input values,
|
|
66
|
+
// in the same Start/End/Duration order the row is built in.
|
|
67
|
+
const inputs = host.querySelectorAll<HTMLInputElement>("input");
|
|
68
|
+
expect(inputs[0]?.value).toBe("8.00s");
|
|
69
|
+
expect(inputs[1]?.value).toBe("12.00s");
|
|
70
|
+
expect(inputs[2]?.value).toBe("4.00s");
|
|
71
|
+
act(() => root.unmount());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("shows the inferred note when duration is derived from animations, not authored", () => {
|
|
75
|
+
const onSetAttribute = vi.fn();
|
|
76
|
+
const element = baseElement({ dataAttributes: { start: "0", duration: "0" } });
|
|
77
|
+
const { host, root } = renderInto(
|
|
78
|
+
<FlatTimingRow
|
|
79
|
+
element={element}
|
|
80
|
+
animations={[{ position: 2, duration: 3 } as never]}
|
|
81
|
+
onSetAttribute={onSetAttribute}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
expect(host.textContent).toContain("Inferred");
|
|
85
|
+
act(() => root.unmount());
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("commits a Start edit through onSetAttribute", () => {
|
|
89
|
+
const onSetAttribute = vi.fn();
|
|
90
|
+
const { host, root } = renderInto(
|
|
91
|
+
<FlatTimingRow element={baseElement()} onSetAttribute={onSetAttribute} />,
|
|
92
|
+
);
|
|
93
|
+
const startInput = host.querySelectorAll("input")[0];
|
|
94
|
+
if (!startInput) throw new Error("expected a Start input");
|
|
95
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
|
|
96
|
+
act(() => {
|
|
97
|
+
setter.call(startInput, "10s");
|
|
98
|
+
startInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
99
|
+
startInput.dispatchEvent(new Event("focusout", { bubbles: true }));
|
|
100
|
+
});
|
|
101
|
+
expect(onSetAttribute).toHaveBeenCalledWith("start", "10.00");
|
|
102
|
+
act(() => root.unmount());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("pins an inferred range through ONE atomic onSetAttributes call when provided, instead of two sequential onSetAttribute calls", async () => {
|
|
106
|
+
const onSetAttribute = vi.fn();
|
|
107
|
+
const onSetAttributes = vi.fn().mockResolvedValue(undefined);
|
|
108
|
+
const element = baseElement({ dataAttributes: { start: "0", duration: "0" } });
|
|
109
|
+
const { host, root } = renderInto(
|
|
110
|
+
<FlatTimingRow
|
|
111
|
+
element={element}
|
|
112
|
+
animations={[{ position: 2, duration: 3 } as never]}
|
|
113
|
+
onSetAttribute={onSetAttribute}
|
|
114
|
+
onSetAttributes={onSetAttributes}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
// Range is inferred (start=2, duration=3) — editing Start alone must pin
|
|
118
|
+
// the WHOLE range (both attrs), not just data-start.
|
|
119
|
+
const startInput = host.querySelectorAll("input")[0];
|
|
120
|
+
if (!startInput) throw new Error("expected a Start input");
|
|
121
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
|
|
122
|
+
await act(async () => {
|
|
123
|
+
setter.call(startInput, "5s");
|
|
124
|
+
startInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
125
|
+
startInput.dispatchEvent(new Event("focusout", { bubbles: true }));
|
|
126
|
+
await Promise.resolve();
|
|
127
|
+
});
|
|
128
|
+
expect(onSetAttributes).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(onSetAttributes).toHaveBeenCalledWith(element, { start: "5.00", duration: "3.00" });
|
|
130
|
+
expect(onSetAttribute).not.toHaveBeenCalled();
|
|
131
|
+
act(() => root.unmount());
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("falls back to two sequential onSetAttribute calls to pin an inferred range when onSetAttributes is not provided", async () => {
|
|
135
|
+
const onSetAttribute = vi.fn().mockResolvedValue(undefined);
|
|
136
|
+
const element = baseElement({ dataAttributes: { start: "0", duration: "0" } });
|
|
137
|
+
const { host, root } = renderInto(
|
|
138
|
+
<FlatTimingRow
|
|
139
|
+
element={element}
|
|
140
|
+
animations={[{ position: 2, duration: 3 } as never]}
|
|
141
|
+
onSetAttribute={onSetAttribute}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
const startInput = host.querySelectorAll("input")[0];
|
|
145
|
+
if (!startInput) throw new Error("expected a Start input");
|
|
146
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
|
|
147
|
+
await act(async () => {
|
|
148
|
+
setter.call(startInput, "5s");
|
|
149
|
+
startInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
150
|
+
startInput.dispatchEvent(new Event("focusout", { bubbles: true }));
|
|
151
|
+
await Promise.resolve();
|
|
152
|
+
});
|
|
153
|
+
expect(onSetAttribute).toHaveBeenNthCalledWith(1, "start", "5.00");
|
|
154
|
+
expect(onSetAttribute).toHaveBeenNthCalledWith(2, "duration", "3.00");
|
|
155
|
+
act(() => root.unmount());
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("FlatMotionSection", () => {
|
|
160
|
+
it("renders Timing when showTiming is true and the effect list when showEffects is true", () => {
|
|
161
|
+
const { host, root } = renderInto(
|
|
162
|
+
<FlatMotionSection
|
|
163
|
+
element={baseElement()}
|
|
164
|
+
animations={[
|
|
165
|
+
{
|
|
166
|
+
id: "a1",
|
|
167
|
+
method: "to",
|
|
168
|
+
position: 0.8,
|
|
169
|
+
duration: 1.2,
|
|
170
|
+
ease: "power2.out",
|
|
171
|
+
properties: { opacity: 1 },
|
|
172
|
+
} as never,
|
|
173
|
+
]}
|
|
174
|
+
showTiming
|
|
175
|
+
showEffects
|
|
176
|
+
onSetAttribute={vi.fn()}
|
|
177
|
+
onAddAnimation={vi.fn()}
|
|
178
|
+
onUpdateProperty={vi.fn()}
|
|
179
|
+
onUpdateMeta={vi.fn()}
|
|
180
|
+
onDeleteAnimation={vi.fn()}
|
|
181
|
+
onAddProperty={vi.fn()}
|
|
182
|
+
onRemoveProperty={vi.fn()}
|
|
183
|
+
/>,
|
|
184
|
+
);
|
|
185
|
+
expect(host.textContent).toContain("Start");
|
|
186
|
+
expect(host.textContent).toContain("power2.out");
|
|
187
|
+
act(() => root.unmount());
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("omits Timing entirely when showTiming is false", () => {
|
|
191
|
+
const { host, root } = renderInto(
|
|
192
|
+
<FlatMotionSection
|
|
193
|
+
element={baseElement()}
|
|
194
|
+
animations={[]}
|
|
195
|
+
showTiming={false}
|
|
196
|
+
showEffects
|
|
197
|
+
onSetAttribute={vi.fn()}
|
|
198
|
+
onAddAnimation={vi.fn()}
|
|
199
|
+
onUpdateProperty={vi.fn()}
|
|
200
|
+
onUpdateMeta={vi.fn()}
|
|
201
|
+
onDeleteAnimation={vi.fn()}
|
|
202
|
+
onAddProperty={vi.fn()}
|
|
203
|
+
onRemoveProperty={vi.fn()}
|
|
204
|
+
/>,
|
|
205
|
+
);
|
|
206
|
+
expect(host.textContent).not.toContain("Start");
|
|
207
|
+
act(() => root.unmount());
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("omits the effect list entirely when showEffects is false", () => {
|
|
211
|
+
const { host, root } = renderInto(
|
|
212
|
+
<FlatMotionSection
|
|
213
|
+
element={baseElement()}
|
|
214
|
+
animations={[
|
|
215
|
+
{
|
|
216
|
+
id: "a1",
|
|
217
|
+
method: "to",
|
|
218
|
+
position: 0.8,
|
|
219
|
+
duration: 1.2,
|
|
220
|
+
ease: "power2.out",
|
|
221
|
+
properties: { opacity: 1 },
|
|
222
|
+
} as never,
|
|
223
|
+
]}
|
|
224
|
+
showTiming
|
|
225
|
+
showEffects={false}
|
|
226
|
+
onSetAttribute={vi.fn()}
|
|
227
|
+
onAddAnimation={vi.fn()}
|
|
228
|
+
onUpdateProperty={vi.fn()}
|
|
229
|
+
onUpdateMeta={vi.fn()}
|
|
230
|
+
onDeleteAnimation={vi.fn()}
|
|
231
|
+
onAddProperty={vi.fn()}
|
|
232
|
+
onRemoveProperty={vi.fn()}
|
|
233
|
+
/>,
|
|
234
|
+
);
|
|
235
|
+
expect(host.textContent).not.toContain("power2.out");
|
|
236
|
+
act(() => root.unmount());
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("opens the add-method menu on '+ Add effect' and calls onAddAnimation with the chosen method", () => {
|
|
240
|
+
const onAddAnimation = vi.fn();
|
|
241
|
+
const { host, root } = renderInto(
|
|
242
|
+
<FlatMotionSection
|
|
243
|
+
element={baseElement()}
|
|
244
|
+
animations={[]}
|
|
245
|
+
showTiming
|
|
246
|
+
showEffects
|
|
247
|
+
onSetAttribute={vi.fn()}
|
|
248
|
+
onAddAnimation={onAddAnimation}
|
|
249
|
+
onUpdateProperty={vi.fn()}
|
|
250
|
+
onUpdateMeta={vi.fn()}
|
|
251
|
+
onDeleteAnimation={vi.fn()}
|
|
252
|
+
onAddProperty={vi.fn()}
|
|
253
|
+
onRemoveProperty={vi.fn()}
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
const buttons = () => Array.from(host.querySelectorAll("button"));
|
|
257
|
+
const addTrigger = buttons().find((b) => b.textContent === "+ Add effect");
|
|
258
|
+
if (!addTrigger) throw new Error("expected an '+ Add effect' trigger button");
|
|
259
|
+
act(() => {
|
|
260
|
+
addTrigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
261
|
+
});
|
|
262
|
+
const animateButton = buttons().find((b) => b.textContent === "Animate");
|
|
263
|
+
if (!animateButton) throw new Error("expected an 'Animate' method button");
|
|
264
|
+
act(() => {
|
|
265
|
+
animateButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
266
|
+
});
|
|
267
|
+
expect(onAddAnimation).toHaveBeenCalledWith("to");
|
|
268
|
+
// The menu closes back to the trigger after a selection.
|
|
269
|
+
expect(buttons().some((b) => b.textContent === "+ Add effect")).toBe(true);
|
|
270
|
+
act(() => root.unmount());
|
|
271
|
+
});
|
|
272
|
+
});
|