@hyperframes/studio 0.6.0-alpha.9 → 0.6.1
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-CzwFysqv.js +418 -0
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-hYc4aP7M.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +421 -4303
- package/src/captions/components/CaptionOverlay.tsx +13 -246
- package/src/captions/components/CaptionOverlayUtils.ts +221 -0
- package/src/components/AskAgentModal.tsx +120 -0
- package/src/components/StudioHeader.tsx +133 -0
- package/src/components/StudioLeftSidebar.tsx +125 -0
- package/src/components/StudioPreviewArea.tsx +167 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +88 -993
- package/src/components/editor/EaseCurveEditor.tsx +221 -0
- package/src/components/editor/FileTree.tsx +13 -621
- package/src/components/editor/FileTreeIcons.tsx +128 -0
- package/src/components/editor/FileTreeNodes.tsx +496 -0
- package/src/components/editor/MotionPanel.tsx +16 -390
- package/src/components/editor/MotionPanelFields.tsx +185 -0
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- package/src/components/editor/domEditOverlayGeometry.ts +211 -0
- package/src/components/editor/domEditOverlayGestures.ts +138 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
- package/src/components/editor/domEditing.ts +44 -1117
- package/src/components/editor/domEditingAgentPrompt.ts +97 -0
- package/src/components/editor/domEditingDom.ts +266 -0
- package/src/components/editor/domEditingElement.ts +329 -0
- package/src/components/editor/domEditingLayers.ts +460 -0
- package/src/components/editor/domEditingTypes.ts +125 -0
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +84 -1049
- package/src/components/editor/manualEditsDom.ts +436 -0
- package/src/components/editor/manualEditsParsing.ts +280 -0
- package/src/components/editor/manualEditsSnapshot.ts +333 -0
- package/src/components/editor/manualEditsTypes.ts +141 -0
- package/src/components/editor/propertyPanelColor.tsx +371 -0
- package/src/components/editor/propertyPanelFill.tsx +421 -0
- package/src/components/editor/propertyPanelFont.tsx +455 -0
- package/src/components/editor/propertyPanelHelpers.ts +401 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
- package/src/components/editor/propertyPanelSections.tsx +453 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
- package/src/components/editor/studioMotion.ts +47 -434
- package/src/components/editor/studioMotionOps.ts +299 -0
- package/src/components/editor/studioMotionTypes.ts +168 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
- package/src/components/editor/useDomEditOverlayRects.ts +207 -0
- package/src/components/nle/NLELayout.tsx +68 -155
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/nle/useCompositionStack.ts +126 -0
- package/src/components/renders/RenderQueue.tsx +102 -31
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/contexts/DomEditContext.tsx +137 -0
- package/src/contexts/FileManagerContext.tsx +110 -0
- package/src/contexts/PanelLayoutContext.tsx +68 -0
- package/src/contexts/StudioContext.tsx +135 -0
- package/src/hooks/useAppHotkeys.ts +326 -0
- package/src/hooks/useAskAgentModal.ts +162 -0
- package/src/hooks/useCaptionDetection.ts +132 -0
- package/src/hooks/useCompositionDimensions.ts +25 -0
- package/src/hooks/useConsoleErrorCapture.ts +60 -0
- package/src/hooks/useDomEditCommits.ts +437 -0
- package/src/hooks/useDomEditSession.ts +342 -0
- package/src/hooks/useDomEditTextCommits.ts +330 -0
- package/src/hooks/useDomSelection.ts +398 -0
- package/src/hooks/useFileManager.ts +431 -0
- package/src/hooks/useFrameCapture.ts +77 -0
- package/src/hooks/useLintModal.ts +35 -0
- package/src/hooks/useManifestPersistence.ts +492 -0
- package/src/hooks/usePanelLayout.ts +68 -0
- package/src/hooks/usePreviewInteraction.ts +153 -0
- package/src/hooks/useRenderClipContent.ts +124 -0
- package/src/hooks/useTimelineEditing.ts +472 -0
- package/src/hooks/useToast.ts +20 -0
- package/src/player/components/Player.tsx +33 -2
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +196 -1518
- package/src/player/components/TimelineCanvas.tsx +434 -0
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/components/TimelineEmptyState.tsx +102 -0
- package/src/player/components/TimelineRuler.tsx +90 -0
- package/src/player/components/timelineIcons.tsx +49 -0
- package/src/player/components/timelineLayout.ts +215 -0
- package/src/player/components/timelineUtils.ts +211 -0
- package/src/player/components/useTimelineClipDrag.ts +388 -0
- package/src/player/components/useTimelinePlayhead.ts +200 -0
- package/src/player/components/useTimelineRangeSelection.ts +135 -0
- package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
- package/src/player/hooks/useTimelinePlayer.ts +105 -1371
- package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
- package/src/player/lib/playbackAdapter.ts +145 -0
- package/src/player/lib/playbackShortcuts.ts +68 -0
- package/src/player/lib/playbackTypes.ts +60 -0
- package/src/player/lib/timelineDOM.ts +373 -0
- package/src/player/lib/timelineElementHelpers.ts +303 -0
- package/src/player/lib/timelineIframeHelpers.ts +269 -0
- package/src/utils/domEditHelpers.ts +50 -0
- package/src/utils/studioFontHelpers.ts +83 -0
- package/src/utils/studioHelpers.ts +214 -0
- package/src/utils/studioPreviewHelpers.ts +185 -0
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-DYCiFGWQ.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { TimelineTrackStyle } from "./timelineTheme";
|
|
2
|
-
// TimelineClip — Visual clip component for the NLE timeline.
|
|
3
2
|
|
|
4
3
|
import { memo, type ReactNode } from "react";
|
|
5
4
|
import type { TimelineElement } from "../store/playerStore";
|
|
@@ -17,67 +16,15 @@ interface TimelineClipProps {
|
|
|
17
16
|
theme?: TimelineTheme;
|
|
18
17
|
trackStyle: TimelineTrackStyle;
|
|
19
18
|
isComposition: boolean;
|
|
20
|
-
isInspectorActive?: boolean;
|
|
21
|
-
isThumbnailActive?: boolean;
|
|
22
|
-
thumbnailLabel?: string;
|
|
23
|
-
childCount?: number;
|
|
24
19
|
onHoverStart: () => void;
|
|
25
20
|
onHoverEnd: () => void;
|
|
26
21
|
onPointerDown?: (e: React.PointerEvent) => void;
|
|
27
22
|
onResizeStart?: (edge: "start" | "end", e: React.PointerEvent) => void;
|
|
28
|
-
onInspectorClick?: (e: React.MouseEvent) => void;
|
|
29
|
-
onThumbnailClick?: (e: React.MouseEvent) => void;
|
|
30
23
|
onClick: (e: React.MouseEvent) => void;
|
|
31
24
|
onDoubleClick: (e: React.MouseEvent) => void;
|
|
32
25
|
children?: ReactNode;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
export const TIMELINE_CLIP_CONTROL_Z_INDEX = 20;
|
|
36
|
-
|
|
37
|
-
const COMPACT_CLIP_CONTROL_WIDTH = 112;
|
|
38
|
-
|
|
39
|
-
interface TimelineClipControlPresentationInput {
|
|
40
|
-
widthPx: number;
|
|
41
|
-
isSelected: boolean;
|
|
42
|
-
isHovered: boolean;
|
|
43
|
-
isInspectorActive: boolean;
|
|
44
|
-
isThumbnailActive: boolean;
|
|
45
|
-
isDragging: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface TimelineClipControlPresentation {
|
|
49
|
-
compact: boolean;
|
|
50
|
-
showControls: boolean;
|
|
51
|
-
containerClassName: string;
|
|
52
|
-
buttonClassName: string;
|
|
53
|
-
iconSize: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getTimelineClipControlPresentation({
|
|
57
|
-
widthPx,
|
|
58
|
-
isSelected,
|
|
59
|
-
isHovered,
|
|
60
|
-
isInspectorActive,
|
|
61
|
-
isThumbnailActive,
|
|
62
|
-
isDragging,
|
|
63
|
-
}: TimelineClipControlPresentationInput): TimelineClipControlPresentation {
|
|
64
|
-
const compact = widthPx < COMPACT_CLIP_CONTROL_WIDTH;
|
|
65
|
-
const isInteractive = isHovered || isSelected || isInspectorActive || isThumbnailActive;
|
|
66
|
-
const showControls = !isDragging && (!compact || isInteractive);
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
compact,
|
|
70
|
-
showControls,
|
|
71
|
-
containerClassName: compact
|
|
72
|
-
? "absolute right-1 top-1 flex items-center gap-1"
|
|
73
|
-
: "absolute right-2 top-2 flex items-center gap-1",
|
|
74
|
-
buttonClassName: compact
|
|
75
|
-
? "flex h-5 w-5 items-center justify-center rounded-[7px]"
|
|
76
|
-
: "flex h-6 w-6 items-center justify-center rounded-md",
|
|
77
|
-
iconSize: compact ? 12 : 14,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
28
|
export const TimelineClip = memo(function TimelineClip({
|
|
82
29
|
el,
|
|
83
30
|
pps,
|
|
@@ -89,16 +36,10 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
89
36
|
theme = defaultTimelineTheme,
|
|
90
37
|
trackStyle,
|
|
91
38
|
isComposition,
|
|
92
|
-
isInspectorActive = false,
|
|
93
|
-
isThumbnailActive = false,
|
|
94
|
-
thumbnailLabel = "thumbnail",
|
|
95
|
-
childCount = 0,
|
|
96
39
|
onHoverStart,
|
|
97
40
|
onHoverEnd,
|
|
98
41
|
onPointerDown,
|
|
99
42
|
onResizeStart,
|
|
100
|
-
onInspectorClick,
|
|
101
|
-
onThumbnailClick,
|
|
102
43
|
onClick,
|
|
103
44
|
onDoubleClick,
|
|
104
45
|
children,
|
|
@@ -120,38 +61,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
120
61
|
: theme.clipShadow;
|
|
121
62
|
const capabilities = getTimelineEditCapabilities(el);
|
|
122
63
|
const displayLabel = el.label || el.id || el.tag;
|
|
123
|
-
const inspectorLabel =
|
|
124
|
-
childCount > 0
|
|
125
|
-
? `${childCount} nested selectable layer${childCount === 1 ? "" : "s"}`
|
|
126
|
-
: "Inspect clip layer";
|
|
127
64
|
const showHandles = handleOpacity > 0.01;
|
|
128
|
-
const baseBackgroundImage = isSelected ? theme.clipBackgroundActive : theme.clipBackground;
|
|
129
|
-
const controlPresentation = getTimelineClipControlPresentation({
|
|
130
|
-
widthPx,
|
|
131
|
-
isSelected,
|
|
132
|
-
isHovered,
|
|
133
|
-
isInspectorActive,
|
|
134
|
-
isThumbnailActive,
|
|
135
|
-
isDragging,
|
|
136
|
-
});
|
|
137
|
-
const glossBackgroundImage = isSelected
|
|
138
|
-
? "linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0))"
|
|
139
|
-
: "linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0))";
|
|
140
|
-
const accentBackgroundImage = `linear-gradient(120deg, ${trackStyle.accent}${
|
|
141
|
-
isSelected ? "22" : "1e"
|
|
142
|
-
}, transparent 28%)`;
|
|
143
|
-
const compositionStripeBackgroundImage =
|
|
144
|
-
isComposition && !hasCustomContent
|
|
145
|
-
? "repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.05) 3px, rgba(255,255,255,0.05) 6px)"
|
|
146
|
-
: undefined;
|
|
147
|
-
const clipBackgroundImage = [
|
|
148
|
-
compositionStripeBackgroundImage,
|
|
149
|
-
glossBackgroundImage,
|
|
150
|
-
accentBackgroundImage,
|
|
151
|
-
baseBackgroundImage,
|
|
152
|
-
]
|
|
153
|
-
.filter(Boolean)
|
|
154
|
-
.join(", ");
|
|
155
65
|
|
|
156
66
|
return (
|
|
157
67
|
<div
|
|
@@ -165,7 +75,13 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
165
75
|
top: clipY,
|
|
166
76
|
bottom: clipY,
|
|
167
77
|
borderRadius: theme.clipRadius,
|
|
168
|
-
|
|
78
|
+
background: isSelected
|
|
79
|
+
? `linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}22, transparent 28%), ${theme.clipBackgroundActive}`
|
|
80
|
+
: `linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)), linear-gradient(120deg, ${trackStyle.accent}1e, transparent 28%), ${theme.clipBackground}`,
|
|
81
|
+
backgroundImage:
|
|
82
|
+
isComposition && !hasCustomContent
|
|
83
|
+
? `repeating-linear-gradient(135deg, transparent, transparent 3px, rgba(255,255,255,0.05) 3px, rgba(255,255,255,0.05) 6px)`
|
|
84
|
+
: undefined,
|
|
169
85
|
border: `1px solid ${borderColor}`,
|
|
170
86
|
boxShadow,
|
|
171
87
|
transition:
|
|
@@ -176,8 +92,8 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
176
92
|
}}
|
|
177
93
|
title={
|
|
178
94
|
isComposition
|
|
179
|
-
? `${el.compositionSrc}
|
|
180
|
-
: `${displayLabel}
|
|
95
|
+
? `${el.compositionSrc} • Double-click to open`
|
|
96
|
+
: `${displayLabel} • ${el.start.toFixed(1)}s – ${(el.start + el.duration).toFixed(1)}s`
|
|
181
97
|
}
|
|
182
98
|
onPointerEnter={onHoverStart}
|
|
183
99
|
onPointerLeave={onHoverEnd}
|
|
@@ -185,157 +101,6 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
185
101
|
onClick={onClick}
|
|
186
102
|
onDoubleClick={onDoubleClick}
|
|
187
103
|
>
|
|
188
|
-
{childCount > 0 && controlPresentation.showControls && (
|
|
189
|
-
<button
|
|
190
|
-
type="button"
|
|
191
|
-
className={`absolute flex items-center gap-1 rounded-md border border-studio-accent/30 bg-neutral-950/75 text-[10px] font-semibold tabular-nums text-studio-accent shadow-lg shadow-black/25 backdrop-blur transition-colors hover:border-studio-accent/60 hover:bg-studio-accent/15 ${
|
|
192
|
-
controlPresentation.compact ? "left-1 top-1 h-5 px-1" : "left-2 top-2 h-6 px-1.5"
|
|
193
|
-
}`}
|
|
194
|
-
style={{ zIndex: TIMELINE_CLIP_CONTROL_Z_INDEX }}
|
|
195
|
-
title={inspectorLabel}
|
|
196
|
-
aria-label={inspectorLabel}
|
|
197
|
-
onPointerDown={(event) => {
|
|
198
|
-
event.stopPropagation();
|
|
199
|
-
}}
|
|
200
|
-
onClick={(event) => {
|
|
201
|
-
event.stopPropagation();
|
|
202
|
-
onInspectorClick?.(event);
|
|
203
|
-
}}
|
|
204
|
-
>
|
|
205
|
-
<svg
|
|
206
|
-
width={controlPresentation.compact ? "11" : "13"}
|
|
207
|
-
height={controlPresentation.compact ? "11" : "13"}
|
|
208
|
-
viewBox="0 0 24 24"
|
|
209
|
-
fill="none"
|
|
210
|
-
stroke="currentColor"
|
|
211
|
-
strokeWidth="1.8"
|
|
212
|
-
strokeLinecap="round"
|
|
213
|
-
strokeLinejoin="round"
|
|
214
|
-
aria-hidden="true"
|
|
215
|
-
>
|
|
216
|
-
<rect x="4" y="4" width="6" height="6" rx="1" />
|
|
217
|
-
<rect x="14" y="4" width="6" height="6" rx="1" />
|
|
218
|
-
<rect x="4" y="14" width="6" height="6" rx="1" />
|
|
219
|
-
<path d="M14 17h6" />
|
|
220
|
-
</svg>
|
|
221
|
-
{childCount}
|
|
222
|
-
</button>
|
|
223
|
-
)}
|
|
224
|
-
{onInspectorClick &&
|
|
225
|
-
controlPresentation.compact &&
|
|
226
|
-
!controlPresentation.showControls &&
|
|
227
|
-
!isDragging && (
|
|
228
|
-
<button
|
|
229
|
-
type="button"
|
|
230
|
-
className="group/clip-inspect absolute right-1 top-1/2 flex h-7 w-2 -translate-y-1/2 items-center justify-center rounded-full border border-white/15 bg-neutral-950/70 text-neutral-300 shadow-lg shadow-black/25 backdrop-blur transition-all hover:w-5 hover:border-white/30 hover:bg-neutral-950/90 focus:w-5 focus:border-studio-accent/60 focus:bg-studio-accent/15 focus:outline-none"
|
|
231
|
-
style={{ zIndex: TIMELINE_CLIP_CONTROL_Z_INDEX }}
|
|
232
|
-
title={inspectorLabel}
|
|
233
|
-
aria-label={inspectorLabel}
|
|
234
|
-
onPointerDown={(event) => {
|
|
235
|
-
event.stopPropagation();
|
|
236
|
-
}}
|
|
237
|
-
onClick={(event) => {
|
|
238
|
-
event.stopPropagation();
|
|
239
|
-
onInspectorClick(event);
|
|
240
|
-
}}
|
|
241
|
-
>
|
|
242
|
-
<svg
|
|
243
|
-
className="opacity-0 transition-opacity group-hover/clip-inspect:opacity-100 group-focus/clip-inspect:opacity-100"
|
|
244
|
-
width="12"
|
|
245
|
-
height="12"
|
|
246
|
-
viewBox="0 0 24 24"
|
|
247
|
-
fill="none"
|
|
248
|
-
stroke="currentColor"
|
|
249
|
-
strokeWidth="1.8"
|
|
250
|
-
strokeLinecap="round"
|
|
251
|
-
strokeLinejoin="round"
|
|
252
|
-
aria-hidden="true"
|
|
253
|
-
>
|
|
254
|
-
<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z" />
|
|
255
|
-
<circle cx="12" cy="12" r="3" />
|
|
256
|
-
</svg>
|
|
257
|
-
</button>
|
|
258
|
-
)}
|
|
259
|
-
{(onThumbnailClick || onInspectorClick) && controlPresentation.showControls && (
|
|
260
|
-
<div
|
|
261
|
-
className={controlPresentation.containerClassName}
|
|
262
|
-
style={{ zIndex: TIMELINE_CLIP_CONTROL_Z_INDEX }}
|
|
263
|
-
>
|
|
264
|
-
{onThumbnailClick && (
|
|
265
|
-
<button
|
|
266
|
-
type="button"
|
|
267
|
-
className={`${controlPresentation.buttonClassName} border shadow-lg shadow-black/25 backdrop-blur transition-colors ${
|
|
268
|
-
isThumbnailActive
|
|
269
|
-
? "border-studio-accent/60 bg-studio-accent/18 text-studio-accent"
|
|
270
|
-
: "border-white/12 bg-neutral-950/70 text-neutral-400 hover:border-white/24 hover:text-neutral-100"
|
|
271
|
-
}`}
|
|
272
|
-
title={
|
|
273
|
-
isThumbnailActive ? `Hide clip ${thumbnailLabel}` : `Show clip ${thumbnailLabel}`
|
|
274
|
-
}
|
|
275
|
-
aria-label={
|
|
276
|
-
isThumbnailActive ? `Hide clip ${thumbnailLabel}` : `Show clip ${thumbnailLabel}`
|
|
277
|
-
}
|
|
278
|
-
onPointerDown={(event) => {
|
|
279
|
-
event.stopPropagation();
|
|
280
|
-
}}
|
|
281
|
-
onClick={(event) => {
|
|
282
|
-
event.stopPropagation();
|
|
283
|
-
onThumbnailClick(event);
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
286
|
-
<svg
|
|
287
|
-
width={controlPresentation.iconSize}
|
|
288
|
-
height={controlPresentation.iconSize}
|
|
289
|
-
viewBox="0 0 24 24"
|
|
290
|
-
fill="none"
|
|
291
|
-
stroke="currentColor"
|
|
292
|
-
strokeWidth="1.8"
|
|
293
|
-
strokeLinecap="round"
|
|
294
|
-
strokeLinejoin="round"
|
|
295
|
-
aria-hidden="true"
|
|
296
|
-
>
|
|
297
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
298
|
-
<circle cx="8" cy="10" r="1.5" />
|
|
299
|
-
<path d="m4 17 5-5 4 4 2-2 5 5" />
|
|
300
|
-
</svg>
|
|
301
|
-
</button>
|
|
302
|
-
)}
|
|
303
|
-
{onInspectorClick && (
|
|
304
|
-
<button
|
|
305
|
-
type="button"
|
|
306
|
-
className={`${controlPresentation.buttonClassName} border shadow-lg shadow-black/25 backdrop-blur transition-colors ${
|
|
307
|
-
isInspectorActive
|
|
308
|
-
? "border-studio-accent/60 bg-studio-accent/18 text-studio-accent"
|
|
309
|
-
: "border-white/12 bg-neutral-950/70 text-neutral-400 hover:border-white/24 hover:text-neutral-100"
|
|
310
|
-
}`}
|
|
311
|
-
title={inspectorLabel}
|
|
312
|
-
aria-label={inspectorLabel}
|
|
313
|
-
onPointerDown={(event) => {
|
|
314
|
-
event.stopPropagation();
|
|
315
|
-
}}
|
|
316
|
-
onClick={(event) => {
|
|
317
|
-
event.stopPropagation();
|
|
318
|
-
onInspectorClick(event);
|
|
319
|
-
}}
|
|
320
|
-
>
|
|
321
|
-
<svg
|
|
322
|
-
width={controlPresentation.iconSize}
|
|
323
|
-
height={controlPresentation.iconSize}
|
|
324
|
-
viewBox="0 0 24 24"
|
|
325
|
-
fill="none"
|
|
326
|
-
stroke="currentColor"
|
|
327
|
-
strokeWidth="1.8"
|
|
328
|
-
strokeLinecap="round"
|
|
329
|
-
strokeLinejoin="round"
|
|
330
|
-
aria-hidden="true"
|
|
331
|
-
>
|
|
332
|
-
<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z" />
|
|
333
|
-
<circle cx="12" cy="12" r="3" />
|
|
334
|
-
</svg>
|
|
335
|
-
</button>
|
|
336
|
-
)}
|
|
337
|
-
</div>
|
|
338
|
-
)}
|
|
339
104
|
<div
|
|
340
105
|
aria-hidden="true"
|
|
341
106
|
role="presentation"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { DragEventHandler } from "react";
|
|
2
|
+
import { GUTTER, RULER_H } from "./timelineLayout";
|
|
3
|
+
|
|
4
|
+
interface TimelineEmptyStateProps {
|
|
5
|
+
isDragOver: boolean;
|
|
6
|
+
onFileDrop?: boolean;
|
|
7
|
+
onDragOver: DragEventHandler<HTMLDivElement>;
|
|
8
|
+
onDragLeave: DragEventHandler<HTMLDivElement>;
|
|
9
|
+
onDrop: DragEventHandler<HTMLDivElement>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TimelineEmptyState({
|
|
13
|
+
isDragOver,
|
|
14
|
+
onFileDrop,
|
|
15
|
+
onDragOver,
|
|
16
|
+
onDragLeave,
|
|
17
|
+
onDrop,
|
|
18
|
+
}: TimelineEmptyStateProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={`h-full border-t bg-[#0a0a0b] flex flex-col select-none transition-colors duration-150 ${
|
|
22
|
+
isDragOver ? "border-studio-accent/50 bg-studio-accent/[0.03]" : "border-neutral-800/50"
|
|
23
|
+
}`}
|
|
24
|
+
onDragOver={onDragOver}
|
|
25
|
+
onDragLeave={onDragLeave}
|
|
26
|
+
onDrop={onDrop}
|
|
27
|
+
>
|
|
28
|
+
{/* Ruler */}
|
|
29
|
+
<div
|
|
30
|
+
className="flex-shrink-0 border-b border-neutral-800/40 flex items-end relative"
|
|
31
|
+
style={{ height: RULER_H, paddingLeft: GUTTER }}
|
|
32
|
+
>
|
|
33
|
+
{[0, 10, 20, 30, 40, 50].map((s) => (
|
|
34
|
+
<div
|
|
35
|
+
key={s}
|
|
36
|
+
className="flex flex-col items-center"
|
|
37
|
+
style={{ position: "absolute", left: GUTTER + s * 14 }}
|
|
38
|
+
>
|
|
39
|
+
<span className="text-[9px] text-neutral-600 font-mono tabular-nums leading-none mb-0.5">
|
|
40
|
+
{`${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, "0")}`}
|
|
41
|
+
</span>
|
|
42
|
+
<div className="w-px h-[5px] bg-neutral-700/40" />
|
|
43
|
+
</div>
|
|
44
|
+
))}
|
|
45
|
+
</div>
|
|
46
|
+
{/* Empty drop zone */}
|
|
47
|
+
<div className="flex-1 flex items-center justify-center">
|
|
48
|
+
<div
|
|
49
|
+
className={`flex items-center gap-3 px-6 py-3 border border-dashed rounded-lg transition-colors duration-150 ${
|
|
50
|
+
isDragOver ? "border-studio-accent/60 bg-studio-accent/[0.06]" : "border-neutral-700/50"
|
|
51
|
+
}`}
|
|
52
|
+
>
|
|
53
|
+
{isDragOver ? (
|
|
54
|
+
<>
|
|
55
|
+
<svg
|
|
56
|
+
width="18"
|
|
57
|
+
height="18"
|
|
58
|
+
viewBox="0 0 24 24"
|
|
59
|
+
fill="none"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
strokeWidth="1.5"
|
|
62
|
+
strokeLinecap="round"
|
|
63
|
+
strokeLinejoin="round"
|
|
64
|
+
className="text-studio-accent flex-shrink-0"
|
|
65
|
+
>
|
|
66
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
67
|
+
<polyline points="7 10 12 15 17 10" />
|
|
68
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
69
|
+
</svg>
|
|
70
|
+
<span className="text-[13px] text-studio-accent">Drop media files to import</span>
|
|
71
|
+
</>
|
|
72
|
+
) : (
|
|
73
|
+
<>
|
|
74
|
+
<svg
|
|
75
|
+
width="18"
|
|
76
|
+
height="18"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
strokeWidth="1.5"
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
className="text-neutral-600 flex-shrink-0"
|
|
84
|
+
>
|
|
85
|
+
<rect x="2" y="2" width="20" height="20" rx="2" />
|
|
86
|
+
<path d="M7 2v20" />
|
|
87
|
+
<path d="M17 2v20" />
|
|
88
|
+
<path d="M2 7h20" />
|
|
89
|
+
<path d="M2 17h20" />
|
|
90
|
+
</svg>
|
|
91
|
+
<span className="text-[13px] text-neutral-500">
|
|
92
|
+
{onFileDrop
|
|
93
|
+
? "Drop media here or describe your video to start"
|
|
94
|
+
: "Describe your video to start creating"}
|
|
95
|
+
</span>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import type { TimelineTheme } from "./timelineTheme";
|
|
3
|
+
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
4
|
+
import { GUTTER, RULER_H, formatTimelineTickLabel } from "./timelineLayout";
|
|
5
|
+
|
|
6
|
+
interface TimelineRulerProps {
|
|
7
|
+
major: number[];
|
|
8
|
+
minor: number[];
|
|
9
|
+
pps: number;
|
|
10
|
+
trackContentWidth: number;
|
|
11
|
+
totalH: number;
|
|
12
|
+
effectiveDuration: number;
|
|
13
|
+
majorTickInterval: number;
|
|
14
|
+
shiftHeld: boolean;
|
|
15
|
+
rangeSelection: TimelineRangeSelection | null;
|
|
16
|
+
theme: TimelineTheme;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TimelineRuler = memo(function TimelineRuler({
|
|
20
|
+
major,
|
|
21
|
+
minor,
|
|
22
|
+
pps,
|
|
23
|
+
trackContentWidth,
|
|
24
|
+
totalH,
|
|
25
|
+
effectiveDuration,
|
|
26
|
+
majorTickInterval,
|
|
27
|
+
shiftHeld,
|
|
28
|
+
rangeSelection,
|
|
29
|
+
theme,
|
|
30
|
+
}: TimelineRulerProps) {
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{/* Grid lines */}
|
|
34
|
+
<svg
|
|
35
|
+
className="absolute pointer-events-none"
|
|
36
|
+
style={{ left: GUTTER, width: trackContentWidth }}
|
|
37
|
+
height={totalH}
|
|
38
|
+
>
|
|
39
|
+
{major.map((t) => {
|
|
40
|
+
const x = t * pps;
|
|
41
|
+
return (
|
|
42
|
+
<line
|
|
43
|
+
key={`g-${t}`}
|
|
44
|
+
x1={x}
|
|
45
|
+
y1={RULER_H}
|
|
46
|
+
x2={x}
|
|
47
|
+
y2={totalH}
|
|
48
|
+
stroke={theme.tickMinor}
|
|
49
|
+
strokeWidth="1"
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</svg>
|
|
54
|
+
|
|
55
|
+
{/* Ruler */}
|
|
56
|
+
<div
|
|
57
|
+
className="relative overflow-hidden"
|
|
58
|
+
style={{ height: RULER_H, marginLeft: GUTTER, width: trackContentWidth }}
|
|
59
|
+
>
|
|
60
|
+
{shiftHeld && !rangeSelection && (
|
|
61
|
+
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
|
|
62
|
+
<span className="text-[9px] font-medium" style={{ color: theme.textSecondary }}>
|
|
63
|
+
Drag or click a clip to edit range
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
{minor.map((t) => (
|
|
68
|
+
<div key={`m-${t}`} className="absolute bottom-0" style={{ left: t * pps }}>
|
|
69
|
+
<div className="w-px h-[3px]" style={{ background: theme.tickMinor }} />
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
{major.map((t) => (
|
|
73
|
+
<div
|
|
74
|
+
key={`M-${t}`}
|
|
75
|
+
className="absolute bottom-0 flex flex-col items-center"
|
|
76
|
+
style={{ left: t * pps }}
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
className="text-[9px] font-mono tabular-nums leading-none mb-0.5"
|
|
80
|
+
style={{ color: theme.tickText }}
|
|
81
|
+
>
|
|
82
|
+
{formatTimelineTickLabel(t, effectiveDuration, majorTickInterval)}
|
|
83
|
+
</span>
|
|
84
|
+
<div className="w-px h-[5px]" style={{ background: theme.tickMajor }} />
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { getTimelineTrackStyle, type TimelineTrackStyle } from "./timelineTheme";
|
|
3
|
+
|
|
4
|
+
export interface TrackVisualStyle extends TimelineTrackStyle {
|
|
5
|
+
icon: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ICON_BASE = "/icons/timeline";
|
|
9
|
+
function TimelineIcon({ src }: { src: string }) {
|
|
10
|
+
return (
|
|
11
|
+
<img
|
|
12
|
+
src={src}
|
|
13
|
+
alt=""
|
|
14
|
+
width={12}
|
|
15
|
+
height={12}
|
|
16
|
+
style={{ filter: "brightness(0) invert(1)" }}
|
|
17
|
+
draggable={false}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const IconCaptions = <TimelineIcon src={`${ICON_BASE}/captions.svg`} />;
|
|
23
|
+
const IconImage = <TimelineIcon src={`${ICON_BASE}/image.svg`} />;
|
|
24
|
+
const IconMusic = <TimelineIcon src={`${ICON_BASE}/music.svg`} />;
|
|
25
|
+
const IconText = <TimelineIcon src={`${ICON_BASE}/text.svg`} />;
|
|
26
|
+
const IconComposition = <TimelineIcon src={`${ICON_BASE}/composition.svg`} />;
|
|
27
|
+
const IconAudio = <TimelineIcon src={`${ICON_BASE}/audio.svg`} />;
|
|
28
|
+
|
|
29
|
+
const ICONS: Record<string, ReactNode> = {
|
|
30
|
+
video: IconImage,
|
|
31
|
+
audio: IconMusic,
|
|
32
|
+
img: IconImage,
|
|
33
|
+
div: IconComposition,
|
|
34
|
+
span: IconCaptions,
|
|
35
|
+
p: IconText,
|
|
36
|
+
h1: IconText,
|
|
37
|
+
section: IconComposition,
|
|
38
|
+
sfx: IconAudio,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function getTrackStyle(tag: string): TrackVisualStyle {
|
|
42
|
+
const trackStyle = getTimelineTrackStyle(tag);
|
|
43
|
+
const normalized = tag.toLowerCase();
|
|
44
|
+
const icon =
|
|
45
|
+
normalized.startsWith("h") && normalized.length === 2 && "123456".includes(normalized[1] ?? "")
|
|
46
|
+
? ICONS.h1
|
|
47
|
+
: (ICONS[normalized] ?? IconComposition);
|
|
48
|
+
return { ...trackStyle, icon };
|
|
49
|
+
}
|