@hyperframes/studio 0.6.0-alpha.9 → 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.
Files changed (66) hide show
  1. package/dist/assets/{hyperframes-player-DjsVzYFP.js → hyperframes-player-DOFETgjy.js} +1 -1
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-DUqUmaoH.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +428 -4299
  8. package/src/components/AskAgentModal.tsx +120 -0
  9. package/src/components/StudioHeader.tsx +133 -0
  10. package/src/components/StudioLeftSidebar.tsx +125 -0
  11. package/src/components/StudioPreviewArea.tsx +163 -0
  12. package/src/components/StudioRightPanel.tsx +198 -0
  13. package/src/components/TimelineToolbar.tsx +89 -0
  14. package/src/components/editor/DomEditOverlay.tsx +15 -1
  15. package/src/components/editor/PropertyPanel.test.ts +0 -49
  16. package/src/components/editor/PropertyPanel.tsx +132 -2763
  17. package/src/components/editor/domEditing.ts +38 -5
  18. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  19. package/src/components/editor/manualEditingAvailability.ts +1 -1
  20. package/src/components/editor/manualEdits.ts +32 -0
  21. package/src/components/editor/propertyPanelColor.tsx +371 -0
  22. package/src/components/editor/propertyPanelFill.tsx +421 -0
  23. package/src/components/editor/propertyPanelFont.tsx +455 -0
  24. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  25. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  26. package/src/components/editor/propertyPanelSections.tsx +453 -0
  27. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  28. package/src/components/nle/NLELayout.tsx +8 -11
  29. package/src/components/nle/NLEPreview.tsx +3 -0
  30. package/src/components/renders/RenderQueue.tsx +102 -31
  31. package/src/components/renders/useRenderQueue.ts +8 -2
  32. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  33. package/src/contexts/DomEditContext.tsx +137 -0
  34. package/src/contexts/FileManagerContext.tsx +110 -0
  35. package/src/contexts/PanelLayoutContext.tsx +68 -0
  36. package/src/contexts/StudioContext.tsx +135 -0
  37. package/src/hooks/useAppHotkeys.ts +326 -0
  38. package/src/hooks/useAskAgentModal.ts +162 -0
  39. package/src/hooks/useCaptionDetection.ts +132 -0
  40. package/src/hooks/useCompositionDimensions.ts +25 -0
  41. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  42. package/src/hooks/useDomEditCommits.ts +437 -0
  43. package/src/hooks/useDomEditSession.ts +342 -0
  44. package/src/hooks/useDomEditTextCommits.ts +330 -0
  45. package/src/hooks/useDomSelection.ts +398 -0
  46. package/src/hooks/useFileManager.ts +431 -0
  47. package/src/hooks/useFrameCapture.ts +77 -0
  48. package/src/hooks/useLintModal.ts +35 -0
  49. package/src/hooks/useManifestPersistence.ts +492 -0
  50. package/src/hooks/usePanelLayout.ts +68 -0
  51. package/src/hooks/usePreviewInteraction.ts +153 -0
  52. package/src/hooks/useRenderClipContent.ts +124 -0
  53. package/src/hooks/useTimelineEditing.ts +472 -0
  54. package/src/player/components/Player.tsx +33 -2
  55. package/src/player/components/Timeline.test.ts +0 -8
  56. package/src/player/components/Timeline.tsx +10 -103
  57. package/src/player/components/TimelineClip.tsx +9 -244
  58. package/src/player/hooks/useTimelinePlayer.ts +140 -103
  59. package/src/utils/domEditHelpers.ts +50 -0
  60. package/src/utils/studioFontHelpers.ts +83 -0
  61. package/src/utils/studioHelpers.ts +214 -0
  62. package/src/utils/studioPreviewHelpers.ts +185 -0
  63. package/src/utils/timelineDiscovery.ts +1 -1
  64. package/dist/assets/index-14zH9lqh.css +0 -1
  65. package/dist/assets/index-DYCiFGWQ.js +0 -108
  66. 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
- useMountEffect(() => {
980
- const handleKeyDown = (event: KeyboardEvent) => {
981
- if (disabledRef.current) return;
982
- if (!shouldHandleTimelineDeleteKey(event)) return;
983
- const selected = selectedElementRef.current;
984
- const onDelete = onDeleteElementRef.current;
985
- if (!selected || !onDelete || deleteInFlightRef.current) return;
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
- Promise.resolve(onDelete(selected)).finally(() => {
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
- aria-disabled={disabled || undefined}
1395
- className={`relative border-t select-none h-full overflow-hidden transition-opacity ${
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
- backgroundImage: clipBackgroundImage,
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} \u2022 Double-click to open`
180
- : `${displayLabel} \u2022 ${el.start.toFixed(1)}s \u2013 ${(el.start + el.duration).toFixed(1)}s`
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"