@hyperframes/studio 0.6.0-alpha.8 → 0.6.0
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-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
- package/dist/assets/index-D1JDq7Gg.css +1 -0
- package/dist/assets/index-DUqUmaoH.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +428 -4299
- 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 +163 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +15 -1
- package/src/components/editor/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- package/src/components/editor/domEditing.ts +38 -5
- package/src/components/editor/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +32 -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/nle/NLELayout.tsx +8 -11
- package/src/components/nle/NLEPreview.tsx +3 -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/player/components/Player.tsx +35 -4
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +10 -103
- package/src/player/components/TimelineClip.tsx +9 -244
- package/src/player/hooks/useTimelinePlayer.ts +140 -103
- 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/index-14zH9lqh.css +0 -1
- package/dist/assets/index-ClYcrksa.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -28,11 +28,6 @@ import {
|
|
|
28
28
|
} from "./timelineTheme";
|
|
29
29
|
import { getPinchTimelineZoomPercent, getTimelinePixelsPerSecond } from "./timelineZoom";
|
|
30
30
|
import { TIMELINE_ASSET_MIME } from "../../utils/timelineAssetDrop";
|
|
31
|
-
import {
|
|
32
|
-
canInspectTimelineElement,
|
|
33
|
-
getTimelineElementKey,
|
|
34
|
-
isAudioTimelineElement,
|
|
35
|
-
} from "../../utils/timelineInspector";
|
|
36
31
|
|
|
37
32
|
/* ── Layout ─────────────────────────────────────────────────────── */
|
|
38
33
|
const GUTTER = 32;
|
|
@@ -335,12 +330,6 @@ interface TimelineProps {
|
|
|
335
330
|
intent: BlockedTimelineEditIntent,
|
|
336
331
|
) => void;
|
|
337
332
|
onSelectElement?: (element: import("../store/playerStore").TimelineElement | null) => void;
|
|
338
|
-
onInspectElement?: (element: import("../store/playerStore").TimelineElement) => void;
|
|
339
|
-
inspectedElementId?: string | null;
|
|
340
|
-
layerChildCounts?: ReadonlyMap<string, number>;
|
|
341
|
-
thumbnailedElementIds?: ReadonlySet<string>;
|
|
342
|
-
onToggleElementThumbnail?: (element: import("../store/playerStore").TimelineElement) => void;
|
|
343
|
-
disabled?: boolean;
|
|
344
333
|
theme?: Partial<TimelineTheme>;
|
|
345
334
|
}
|
|
346
335
|
|
|
@@ -389,12 +378,6 @@ export const Timeline = memo(function Timeline({
|
|
|
389
378
|
onResizeElement,
|
|
390
379
|
onBlockedEditAttempt,
|
|
391
380
|
onSelectElement,
|
|
392
|
-
onInspectElement,
|
|
393
|
-
inspectedElementId,
|
|
394
|
-
layerChildCounts,
|
|
395
|
-
thumbnailedElementIds,
|
|
396
|
-
onToggleElementThumbnail,
|
|
397
|
-
disabled = false,
|
|
398
381
|
theme: themeOverrides,
|
|
399
382
|
}: TimelineProps = {}) {
|
|
400
383
|
const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
|
|
@@ -414,8 +397,6 @@ export const Timeline = memo(function Timeline({
|
|
|
414
397
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
415
398
|
const [hoveredClip, setHoveredClip] = useState<string | null>(null);
|
|
416
399
|
const isDragging = useRef(false);
|
|
417
|
-
const disabledRef = useRef(disabled);
|
|
418
|
-
disabledRef.current = disabled;
|
|
419
400
|
const shiftClickClipRef = useRef<{
|
|
420
401
|
element: TimelineElement;
|
|
421
402
|
anchorX: number;
|
|
@@ -446,7 +427,6 @@ export const Timeline = memo(function Timeline({
|
|
|
446
427
|
const resizingClipRef = useRef<ResizingClipState | null>(null);
|
|
447
428
|
resizingClipRef.current = resizingClip;
|
|
448
429
|
const blockedClipRef = useRef<BlockedClipState | null>(null);
|
|
449
|
-
const deleteInFlightRef = useRef(false);
|
|
450
430
|
const onMoveElementRef = useRef(onMoveElement);
|
|
451
431
|
onMoveElementRef.current = onMoveElement;
|
|
452
432
|
const onResizeElementRef = useRef(onResizeElement);
|
|
@@ -502,19 +482,6 @@ export const Timeline = memo(function Timeline({
|
|
|
502
482
|
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
503
483
|
});
|
|
504
484
|
|
|
505
|
-
useEffect(() => {
|
|
506
|
-
if (!disabled) return;
|
|
507
|
-
stopClipDragAutoScrollRef.current();
|
|
508
|
-
isDragging.current = false;
|
|
509
|
-
isRangeSelecting.current = false;
|
|
510
|
-
blockedClipRef.current = null;
|
|
511
|
-
setDraggedClip(null);
|
|
512
|
-
setResizingClip(null);
|
|
513
|
-
setRangeSelection(null);
|
|
514
|
-
setShowPopover(false);
|
|
515
|
-
setIsDragOver(false);
|
|
516
|
-
}, [disabled]);
|
|
517
|
-
|
|
518
485
|
// Effective duration: max of store duration and the furthest element end.
|
|
519
486
|
// processTimelineMessage updates elements but not duration, so elements can
|
|
520
487
|
// extend beyond the store's duration — this ensures fit mode shows everything.
|
|
@@ -741,7 +708,6 @@ export const Timeline = memo(function Timeline({
|
|
|
741
708
|
|
|
742
709
|
const seekFromX = useCallback(
|
|
743
710
|
(clientX: number) => {
|
|
744
|
-
if (disabledRef.current) return;
|
|
745
711
|
const el = scrollRef.current;
|
|
746
712
|
if (!el || effectiveDuration <= 0) return;
|
|
747
713
|
const rect = el.getBoundingClientRect();
|
|
@@ -799,7 +765,6 @@ export const Timeline = memo(function Timeline({
|
|
|
799
765
|
};
|
|
800
766
|
|
|
801
767
|
const handleWindowPointerMove = (e: PointerEvent) => {
|
|
802
|
-
if (disabledRef.current) return;
|
|
803
768
|
const drag = draggedClipRef.current;
|
|
804
769
|
const resize = resizingClipRef.current;
|
|
805
770
|
const blocked = blockedClipRef.current;
|
|
@@ -884,7 +849,6 @@ export const Timeline = memo(function Timeline({
|
|
|
884
849
|
|
|
885
850
|
const handleWindowPointerUp = () => {
|
|
886
851
|
stopClipDragAutoScrollRef.current();
|
|
887
|
-
if (disabledRef.current) return;
|
|
888
852
|
const resize = resizingClipRef.current;
|
|
889
853
|
if (resize) {
|
|
890
854
|
resizingClipRef.current = null;
|
|
@@ -976,35 +940,20 @@ export const Timeline = memo(function Timeline({
|
|
|
976
940
|
};
|
|
977
941
|
});
|
|
978
942
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
event.preventDefault();
|
|
987
|
-
deleteInFlightRef.current = true;
|
|
988
|
-
suppressClickRef.current = true;
|
|
943
|
+
const prevSelectedRef = useRef(selectedElementRef.current);
|
|
944
|
+
// eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
|
|
945
|
+
useEffect(() => {
|
|
946
|
+
const prev = prevSelectedRef.current;
|
|
947
|
+
const curr = selectedElementRef.current;
|
|
948
|
+
prevSelectedRef.current = curr;
|
|
949
|
+
if (prev && !curr) {
|
|
989
950
|
setShowPopover(false);
|
|
990
951
|
setRangeSelection(null);
|
|
991
|
-
|
|
992
|
-
deleteInFlightRef.current = false;
|
|
993
|
-
requestAnimationFrame(() => {
|
|
994
|
-
suppressClickRef.current = false;
|
|
995
|
-
});
|
|
996
|
-
});
|
|
997
|
-
};
|
|
998
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
999
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
952
|
+
}
|
|
1000
953
|
});
|
|
1001
954
|
|
|
1002
955
|
const handlePointerDown = useCallback(
|
|
1003
956
|
(e: React.PointerEvent) => {
|
|
1004
|
-
if (disabledRef.current) {
|
|
1005
|
-
e.preventDefault();
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
957
|
if (e.button !== 0) return;
|
|
1009
958
|
|
|
1010
959
|
// Shift+click starts range selection — even on clips
|
|
@@ -1036,7 +985,6 @@ export const Timeline = memo(function Timeline({
|
|
|
1036
985
|
);
|
|
1037
986
|
const handlePointerMove = useCallback(
|
|
1038
987
|
(e: React.PointerEvent) => {
|
|
1039
|
-
if (disabledRef.current) return;
|
|
1040
988
|
if (isRangeSelecting.current) {
|
|
1041
989
|
const rect = scrollRef.current?.getBoundingClientRect();
|
|
1042
990
|
if (rect) {
|
|
@@ -1107,7 +1055,6 @@ export const Timeline = memo(function Timeline({
|
|
|
1107
1055
|
|
|
1108
1056
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
1109
1057
|
const handleAssetDragOver = useCallback((e: React.DragEvent) => {
|
|
1110
|
-
if (disabledRef.current) return;
|
|
1111
1058
|
const hasFiles = e.dataTransfer.files.length > 0;
|
|
1112
1059
|
const hasAsset = Array.from(e.dataTransfer.types).includes(TIMELINE_ASSET_MIME);
|
|
1113
1060
|
if (!hasFiles && !hasAsset) return;
|
|
@@ -1122,7 +1069,6 @@ export const Timeline = memo(function Timeline({
|
|
|
1122
1069
|
(e: React.DragEvent) => {
|
|
1123
1070
|
e.preventDefault();
|
|
1124
1071
|
setIsDragOver(false);
|
|
1125
|
-
if (disabledRef.current) return;
|
|
1126
1072
|
if (onFileDrop && e.dataTransfer.files.length > 0) {
|
|
1127
1073
|
const scroll = scrollRef.current;
|
|
1128
1074
|
const rect = scroll?.getBoundingClientRect();
|
|
@@ -1179,7 +1125,6 @@ export const Timeline = memo(function Timeline({
|
|
|
1179
1125
|
|
|
1180
1126
|
const handlePinchWheel = useCallback(
|
|
1181
1127
|
(e: WheelEvent) => {
|
|
1182
|
-
if (disabledRef.current) return;
|
|
1183
1128
|
if (!e.ctrlKey) return;
|
|
1184
1129
|
const scroll = scrollRef.current;
|
|
1185
1130
|
if (!scroll || durationRef.current <= 0 || fitPpsRef.current <= 0 || ppsRef.current <= 0) {
|
|
@@ -1235,7 +1180,6 @@ export const Timeline = memo(function Timeline({
|
|
|
1235
1180
|
className={`h-full border-t bg-[#0a0a0b] flex flex-col select-none transition-colors duration-150 ${
|
|
1236
1181
|
isDragOver ? "border-studio-accent/50 bg-studio-accent/[0.03]" : "border-neutral-800/50"
|
|
1237
1182
|
}`}
|
|
1238
|
-
aria-disabled={disabled || undefined}
|
|
1239
1183
|
onDragOver={handleAssetDragOver}
|
|
1240
1184
|
onDragLeave={() => setIsDragOver(false)}
|
|
1241
1185
|
onDrop={handleAssetDrop}
|
|
@@ -1391,13 +1335,8 @@ export const Timeline = memo(function Timeline({
|
|
|
1391
1335
|
<div
|
|
1392
1336
|
ref={setContainerRef}
|
|
1393
1337
|
aria-label="Timeline"
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
disabled
|
|
1397
|
-
? "cursor-not-allowed opacity-45"
|
|
1398
|
-
: shiftHeld
|
|
1399
|
-
? "cursor-crosshair"
|
|
1400
|
-
: "cursor-default"
|
|
1338
|
+
className={`relative border-t select-none h-full overflow-hidden ${
|
|
1339
|
+
shiftHeld ? "cursor-crosshair" : "cursor-default"
|
|
1401
1340
|
}`}
|
|
1402
1341
|
style={{
|
|
1403
1342
|
touchAction: "pan-x pan-y",
|
|
@@ -1536,14 +1475,6 @@ export const Timeline = memo(function Timeline({
|
|
|
1536
1475
|
const elementKey = el.key ?? el.id;
|
|
1537
1476
|
const capabilities = getTimelineEditCapabilities(el);
|
|
1538
1477
|
const isSelected = selectedElementId === elementKey;
|
|
1539
|
-
const canInspectClip = canInspectTimelineElement(el);
|
|
1540
|
-
const isInspectorActive =
|
|
1541
|
-
canInspectClip && inspectedElementId === getTimelineElementKey(el);
|
|
1542
|
-
const childCount = canInspectClip
|
|
1543
|
-
? (layerChildCounts?.get(elementKey) ?? 0)
|
|
1544
|
-
: 0;
|
|
1545
|
-
const isThumbnailActive = thumbnailedElementIds?.has(elementKey) ?? false;
|
|
1546
|
-
const thumbnailLabel = isAudioTimelineElement(el) ? "waveform" : "thumbnail";
|
|
1547
1478
|
const isComposition = !!el.compositionSrc;
|
|
1548
1479
|
const clipKey = `${elementKey}-${i}`;
|
|
1549
1480
|
const isHovered = hoveredClip === clipKey;
|
|
@@ -1567,32 +1498,8 @@ export const Timeline = memo(function Timeline({
|
|
|
1567
1498
|
theme={theme}
|
|
1568
1499
|
trackStyle={clipStyle}
|
|
1569
1500
|
isComposition={isComposition}
|
|
1570
|
-
isInspectorActive={isInspectorActive}
|
|
1571
|
-
isThumbnailActive={isThumbnailActive}
|
|
1572
|
-
thumbnailLabel={thumbnailLabel}
|
|
1573
|
-
childCount={childCount}
|
|
1574
1501
|
onHoverStart={() => setHoveredClip(clipKey)}
|
|
1575
1502
|
onHoverEnd={() => setHoveredClip(null)}
|
|
1576
|
-
onInspectorClick={
|
|
1577
|
-
canInspectClip && onInspectElement
|
|
1578
|
-
? (e) => {
|
|
1579
|
-
e.stopPropagation();
|
|
1580
|
-
if (suppressClickRef.current) return;
|
|
1581
|
-
setSelectedElementId(elementKey);
|
|
1582
|
-
onSelectElement?.(el);
|
|
1583
|
-
onInspectElement(el);
|
|
1584
|
-
}
|
|
1585
|
-
: undefined
|
|
1586
|
-
}
|
|
1587
|
-
onThumbnailClick={
|
|
1588
|
-
onToggleElementThumbnail && canInspectClip
|
|
1589
|
-
? (e) => {
|
|
1590
|
-
e.stopPropagation();
|
|
1591
|
-
if (suppressClickRef.current) return;
|
|
1592
|
-
onToggleElementThumbnail(el);
|
|
1593
|
-
}
|
|
1594
|
-
: undefined
|
|
1595
|
-
}
|
|
1596
1503
|
onResizeStart={(edge, e) => {
|
|
1597
1504
|
if (e.button !== 0 || e.shiftKey || !onResizeElement) return;
|
|
1598
1505
|
if (edge === "start" && !capabilities.canTrimStart) return;
|
|
@@ -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"
|