@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.
Files changed (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.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 +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. 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
- 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"
@@ -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
+ }