@hyperframes/studio 0.8.55 → 0.8.57

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 (145) hide show
  1. package/dist/assets/hyperframes-player-c23wnIfc.js +464 -0
  2. package/dist/assets/{index-DrHQAuGO.css → index-DKJpHYYd.css} +1 -1
  3. package/dist/assets/{index-BEIRytvV.js → index-DgkAzx3n.js} +1 -1
  4. package/dist/assets/{index-DaNpNFJC.js → index-IDNr3X1H.js} +1 -1
  5. package/dist/assets/{index-TcBZyzNA.js → index-gbOCDCuZ.js} +202 -202
  6. package/dist/index.d.ts +111 -17
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +21534 -20683
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles.css +1 -1
  11. package/package.json +7 -7
  12. package/src/App.tsx +18 -1
  13. package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
  14. package/src/components/EditorShell.selectionSync.test.tsx +57 -67
  15. package/src/components/EditorShell.tsx +62 -51
  16. package/src/components/EditorShellTestMocks.ts +46 -0
  17. package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
  18. package/src/components/ProjectUnreachableBanner.tsx +89 -0
  19. package/src/components/editor/DomEditOverlay.tsx +19 -55
  20. package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
  21. package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
  22. package/src/components/editor/GridOverlay.tsx +12 -23
  23. package/src/components/editor/MotionPathOverlay.test.tsx +66 -0
  24. package/src/components/editor/MotionPathOverlay.tsx +3 -1
  25. package/src/components/editor/PreviewGuides.test.tsx +6 -1
  26. package/src/components/editor/PreviewGuides.tsx +8 -10
  27. package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
  28. package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
  29. package/src/components/editor/PropertyPanel.deselect.test.tsx +71 -0
  30. package/src/components/editor/PropertyPanel.tsx +4 -3
  31. package/src/components/editor/SnapToolbar.test.tsx +26 -12
  32. package/src/components/editor/SnapToolbar.tsx +30 -59
  33. package/src/components/editor/domEditOverlayTestMocks.ts +16 -0
  34. package/src/components/editor/previewReadOnly.overlay.test.tsx +260 -0
  35. package/src/components/editor/previewReadOnlyContext.tsx +59 -0
  36. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +9 -0
  37. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
  38. package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
  39. package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -1
  40. package/src/components/editor/propertyPanelInputCoverage.test.tsx +3 -13
  41. package/src/components/editor/useDomEditNudge.ts +9 -0
  42. package/src/components/editor/useDomEditOverlayGestures.ts +4 -0
  43. package/src/components/editor/useOffCanvasIndicators.ts +50 -0
  44. package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
  45. package/src/components/editor/usePreviewCompositionRect.ts +12 -12
  46. package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
  47. package/src/components/editor/useVolumeAutomation.ts +57 -16
  48. package/src/components/nle/NLEContext.tsx +40 -4
  49. package/src/components/nle/NLEPreview.test.ts +66 -1
  50. package/src/components/nle/NLEPreview.tsx +88 -44
  51. package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
  52. package/src/components/nle/PreviewOverlays.tsx +19 -24
  53. package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
  54. package/src/components/nle/PreviewPane.tsx +17 -2
  55. package/src/components/nle/TimelinePane.tsx +5 -1
  56. package/src/components/nle/previewReadOnly.test.tsx +129 -0
  57. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
  58. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -6
  59. package/src/components/nle/usePreviewBlockDrop.ts +3 -1
  60. package/src/hooks/appHotkeysDispatch.ts +16 -1
  61. package/src/hooks/newTweenTargetHooks.test.tsx +1 -0
  62. package/src/hooks/timelineAudioGroupCreate.ts +3 -4
  63. package/src/hooks/timelineTrackVisibility.ts +7 -8
  64. package/src/hooks/useAppHotkeys.test.ts +71 -3
  65. package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
  66. package/src/hooks/useAppHotkeys.ts +4 -0
  67. package/src/hooks/useCompositionDimensions.test.tsx +38 -0
  68. package/src/hooks/useCompositionDimensions.ts +17 -1
  69. package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
  70. package/src/hooks/useDomEditAttributeCommits.ts +14 -3
  71. package/src/hooks/useDomEditCommits.test.tsx +1 -0
  72. package/src/hooks/useDomEditCommits.ts +3 -0
  73. package/src/hooks/useDomEditSession.test.tsx +1 -0
  74. package/src/hooks/useDomEditSession.ts +7 -4
  75. package/src/hooks/useDomEditWiring.ts +40 -40
  76. package/src/hooks/useDomGeometryCommits.test.tsx +85 -2
  77. package/src/hooks/useDomGeometryCommits.ts +26 -20
  78. package/src/hooks/useFileTree.test.tsx +56 -0
  79. package/src/hooks/useFileTree.ts +8 -1
  80. package/src/hooks/useGestureCommit.test.tsx +29 -0
  81. package/src/hooks/useGestureCommit.ts +23 -0
  82. package/src/hooks/useInlineTextEdit.test.tsx +23 -2
  83. package/src/hooks/useInlineTextEdit.ts +11 -2
  84. package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
  85. package/src/hooks/useSdkSession.ts +46 -10
  86. package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
  87. package/src/hooks/useServerConnection.ts +81 -8
  88. package/src/hooks/useTimelineAssetDropOps.test.tsx +3 -4
  89. package/src/hooks/useTimelineAssetDropOps.ts +1 -6
  90. package/src/hooks/useTimelineEditing.test.tsx +10 -7
  91. package/src/hooks/useTimelineEditing.ts +2 -7
  92. package/src/index.ts +12 -0
  93. package/src/player/components/Player.test.ts +141 -13
  94. package/src/player/components/Player.tsx +58 -2
  95. package/src/player/components/ShortcutsPanel.test.tsx +4 -10
  96. package/src/player/components/Timeline.tsx +23 -573
  97. package/src/player/components/TimelineCanvas.tsx +9 -30
  98. package/src/player/components/TimelineClip.test.tsx +52 -0
  99. package/src/player/components/TimelineClip.tsx +13 -2
  100. package/src/player/components/TimelineOverlays.test.ts +67 -32
  101. package/src/player/components/TimelineOverlays.tsx +46 -107
  102. package/src/player/components/TimelineProvider.tsx +191 -0
  103. package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
  104. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  105. package/src/player/components/testRootHarness.ts +18 -0
  106. package/src/player/components/timelineMotionStyles.test.ts +3 -3
  107. package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
  108. package/src/player/components/timelineProviderStateBuilders.ts +83 -0
  109. package/src/player/components/timelineTheme.test.ts +4 -4
  110. package/src/player/components/timelineTheme.ts +34 -30
  111. package/src/player/components/timelineTrackDisplay.ts +1 -4
  112. package/src/player/components/useTimelineClipContextMenu.ts +1 -1
  113. package/src/player/components/useTimelineOverlaysState.ts +100 -0
  114. package/src/player/components/useTimelineProviderState.tsx +583 -0
  115. package/src/player/components/useTimelineTrackDerivations.ts +2 -2
  116. package/src/player/hooks/playbackAdapterResolution.ts +98 -0
  117. package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
  118. package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
  119. package/src/player/hooks/timelineSyncHydration.ts +0 -23
  120. package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
  121. package/src/player/hooks/useShadowPreviewReload.ts +183 -0
  122. package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
  123. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
  124. package/src/player/hooks/useTimelinePlayer.ts +65 -99
  125. package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -91
  126. package/src/player/store/playerStore.ts +0 -4
  127. package/src/player/store/timelineElement.ts +2 -4
  128. package/src/player/store/timelineResetState.ts +0 -1
  129. package/src/styles/hex-baseline.json +3 -4
  130. package/src/styles/studio.css +7 -7
  131. package/src/styles/theme.css +29 -0
  132. package/src/telemetry/events.test.ts +18 -0
  133. package/src/telemetry/events.ts +10 -0
  134. package/src/telemetry/system.test.ts +49 -0
  135. package/src/telemetry/system.ts +6 -0
  136. package/src/telemetry/tabId.test.ts +43 -0
  137. package/src/telemetry/tabId.ts +44 -0
  138. package/src/utils/previewCompositionSize.ts +26 -0
  139. package/src/utils/reloadDebug.ts +2 -4
  140. package/src/utils/sourcePatcher.ts +1 -1
  141. package/src/utils/studioTelemetry.test.ts +14 -0
  142. package/src/utils/studioTelemetry.ts +4 -0
  143. package/dist/assets/hyperframes-player-BjAGc2ID.js +0 -464
  144. package/src/player/hooks/useTimelineRowElements.test.ts +0 -28
  145. package/src/player/hooks/useTimelineRowElements.ts +0 -17
@@ -5,7 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
5
5
  import type { GsapAnimation, PropertyGroupName } from "@hyperframes/core/gsap-parser";
6
6
  import { afterEach, describe, expect, it, vi } from "vitest";
7
7
  import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
8
- import { TimelineTrackHeader } from "./TimelineTrackHeader";
8
+ import { TimelineTrackHeader, gutterFill } from "./TimelineTrackHeader";
9
9
  import { defaultTimelineTheme } from "./timelineTheme";
10
10
  import { type TimelineElement } from "../store/playerStore";
11
11
  import type { TimelineEditCallbacks } from "./timelineCallbacks";
@@ -794,10 +794,6 @@ describe("TimelineTrackHeader", () => {
794
794
  });
795
795
  expect(header()?.style.paddingLeft).toBe("14px");
796
796
  expect(header()?.style.borderLeft).toContain("2px");
797
- // And a lighter gutter, so the row reads as sitting INSIDE its group
798
- // rather than beside it. Overlaid on the theme's own fill rather than a
799
- // hard-coded colour, so it follows whatever the gutter is.
800
- expect(header()?.style.background).toContain("linear-gradient");
801
797
  act(() => view.root.unmount());
802
798
  });
803
799
 
@@ -912,3 +908,28 @@ describe("TimelineTrackHeader", () => {
912
908
  });
913
909
  });
914
910
  });
911
+
912
+ // A host themes the timeline by overriding --timeline-* on theme.css, so the
913
+ // rendered gutter must carry the CSS variable, not a baked-in colour.
914
+ describe("theming", () => {
915
+ it("reads the gutter background and border from timeline theme tokens", () => {
916
+ const view = renderHeader({});
917
+ const header = view.host.querySelector<HTMLElement>('[role="rowheader"]');
918
+ expect(header?.style.background).toBe(defaultTimelineTheme.gutterBackground);
919
+ expect(header?.style.background).toContain("var(--timeline-gutter-bg)");
920
+ expect(header?.style.borderRight).toContain(defaultTimelineTheme.gutterBorder);
921
+ act(() => view.root.unmount());
922
+ });
923
+
924
+ // happy-dom's `background` shorthand garbles a `var()` layer next to
925
+ // `linear-gradient(...)` (verified), so this checks the same value through
926
+ // the pure function instead of the broken CSSOM roundtrip.
927
+ it("overlays the group-member tint on the themed gutter, not a hard-coded colour", () => {
928
+ const filled = gutterFill(defaultTimelineTheme.gutterBackground, true);
929
+ expect(filled).toContain("linear-gradient");
930
+ expect(filled.endsWith(defaultTimelineTheme.gutterBackground)).toBe(true);
931
+ expect(gutterFill(defaultTimelineTheme.gutterBackground, false)).toBe(
932
+ defaultTimelineTheme.gutterBackground,
933
+ );
934
+ });
935
+ });
@@ -34,7 +34,7 @@ const GROUP_MEMBER_INDENT = 14;
34
34
  const GROUP_MEMBER_TINT = "rgba(255,255,255,0.035)";
35
35
 
36
36
  /** The gutter fill for a row, tinted when it belongs to a group. */
37
- function gutterFill(base: string, isGroupMember: boolean): string {
37
+ export function gutterFill(base: string, isGroupMember: boolean): string {
38
38
  return isGroupMember
39
39
  ? `linear-gradient(${GROUP_MEMBER_TINT}, ${GROUP_MEMBER_TINT}), ${base}`
40
40
  : base;
@@ -0,0 +1,18 @@
1
+ import { act } from "react";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+ import { afterEach } from "vitest";
4
+
5
+ export function createHappyDomRootHarness() {
6
+ const roots: Root[] = [];
7
+ afterEach(() => {
8
+ for (const root of roots.splice(0)) act(() => root.unmount());
9
+ document.body.innerHTML = "";
10
+ });
11
+ return {
12
+ mount(host: HTMLElement) {
13
+ const root = createRoot(host);
14
+ roots.push(root);
15
+ return root;
16
+ },
17
+ };
18
+ }
@@ -75,11 +75,11 @@ describe("timeline motion styles", () => {
75
75
  const bloomOverlayRule = expectRule(studioCss, ".timeline-clip::before");
76
76
  const activeBloomOverlayRule = expectRule(studioCss, ".timeline-clip[data-active]::before");
77
77
 
78
- expect(baseTimelineClipRule).toContain("background-color: rgba(255, 255, 255, 0.055)");
78
+ expect(baseTimelineClipRule).toContain("background-color: var(--clip-bg)");
79
79
  expect(activeTimelineClipRule).not.toContain("background: linear-gradient");
80
- expect(activeTimelineClipRule).toContain("border-color: rgba(60, 230, 172, 0.55)");
80
+ expect(activeTimelineClipRule).toContain("border-color: var(--clip-border-active)");
81
81
  expect(activeTimelineClipRule).not.toContain("box-shadow");
82
- expect(bloomOverlayRule).toContain("background: rgba(60, 230, 172, 0.2)");
82
+ expect(bloomOverlayRule).toContain("background: var(--clip-bg-active)");
83
83
  expect(bloomOverlayRule).not.toContain("linear-gradient");
84
84
  expect(bloomOverlayRule).toContain("opacity: 0");
85
85
  expect(activeBloomOverlayRule).toContain("opacity: 1");
@@ -0,0 +1,109 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import {
5
+ buildTimelineMeta,
6
+ resolveRenderClipContent,
7
+ resolveResizingElementIds,
8
+ shouldIgnoreTimelinePointerDown,
9
+ type TimelineMetaBuilderInputs,
10
+ } from "./timelineProviderStateBuilders";
11
+
12
+ function inputs(overrides: Partial<TimelineMetaBuilderInputs> = {}): TimelineMetaBuilderInputs {
13
+ return {
14
+ emptyState: {} as TimelineMetaBuilderInputs["emptyState"],
15
+ container: {
16
+ ref: vi.fn(),
17
+ "aria-label": "Timeline track view",
18
+ "data-timeline-element-count": 2,
19
+ isDragOver: false,
20
+ activeTool: "select",
21
+ shiftHeld: false,
22
+ accentClass: "ring",
23
+ onMouseMove: vi.fn(),
24
+ onMouseLeave: vi.fn(),
25
+ style: {},
26
+ },
27
+ viewport: {
28
+ ref: vi.fn(),
29
+ tabIndex: -1,
30
+ labelMode: false,
31
+ zoomMode: "fit",
32
+ onScroll: vi.fn(),
33
+ onFocus: vi.fn(),
34
+ onBlur: vi.fn(),
35
+ onDragOver: vi.fn(),
36
+ onDragLeave: vi.fn(),
37
+ onDrop: vi.fn(),
38
+ onPointerDown: vi.fn(),
39
+ onPointerMove: vi.fn(),
40
+ onPointerUp: vi.fn(),
41
+ onPointerCancel: vi.fn(),
42
+ onLostPointerCapture: vi.fn(),
43
+ },
44
+ elementCount: 2,
45
+ labelColumnWidth: 120,
46
+ razorGuide: null,
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ describe("buildTimelineMeta", () => {
52
+ it("uses the razor branch for the container class", () => {
53
+ const meta = buildTimelineMeta(
54
+ inputs({
55
+ container: {
56
+ ...inputs().container,
57
+ activeTool: "razor",
58
+ },
59
+ }),
60
+ );
61
+ expect(meta.containerProps.className).toContain("cursor-crosshair");
62
+ });
63
+
64
+ it("uses the shift branch for the container class", () => {
65
+ const meta = buildTimelineMeta(
66
+ inputs({ container: { ...inputs().container, shiftHeld: true } }),
67
+ );
68
+ expect(meta.containerProps.className).toContain("cursor-crosshair");
69
+ });
70
+
71
+ it("uses the default container class without an accent", () => {
72
+ const meta = buildTimelineMeta(inputs());
73
+ expect(meta.containerProps.className).toContain("cursor-default");
74
+ expect(meta.containerProps.className).not.toContain("ring");
75
+ });
76
+
77
+ it("uses the label-column inset only in label mode", () => {
78
+ const fit = buildTimelineMeta(inputs());
79
+ const labels = buildTimelineMeta(
80
+ inputs({ viewport: { ...inputs().viewport, labelMode: true } }),
81
+ );
82
+ expect(fit.viewportProps["data-timeline-auto-scroll-left-inset"]).toBe(0);
83
+ expect(labels.viewportProps["data-timeline-auto-scroll-left-inset"]).toBe(120);
84
+ });
85
+ });
86
+
87
+ describe("timeline provider branch helpers", () => {
88
+ it("resolves virtualized clip content", () => {
89
+ const render = () => null;
90
+ expect(resolveRenderClipContent(true, true, render)).toBeUndefined();
91
+ expect(resolveRenderClipContent(false, true, render)).toBe(render);
92
+ });
93
+
94
+ it("resolves resizing element ids", () => {
95
+ expect(resolveResizingElementIds(null)).toBeUndefined();
96
+ expect(resolveResizingElementIds({ element: { id: "clip" } } as never)).toEqual(["clip"]);
97
+ expect(resolveResizingElementIds({ groupPreview: [{ key: "group::clip" }] } as never)).toEqual([
98
+ "group::clip",
99
+ ]);
100
+ });
101
+
102
+ it("ignores pointer downs on interactive descendants only", () => {
103
+ const button = document.createElement("button");
104
+ const div = document.createElement("div");
105
+ expect(shouldIgnoreTimelinePointerDown(button)).toBe(true);
106
+ expect(shouldIgnoreTimelinePointerDown(div)).toBe(false);
107
+ expect(shouldIgnoreTimelinePointerDown(null)).toBe(false);
108
+ });
109
+ });
@@ -0,0 +1,83 @@
1
+ import type {
2
+ TimelineMeta,
3
+ TimelineContainerProps,
4
+ TimelineViewportProps,
5
+ } from "./TimelineProvider";
6
+ import type { ResizingClipState } from "./useTimelineClipDrag";
7
+ import type { TimelineLaneBaseProps } from "./timelineLaneProps";
8
+
9
+ export function resolveResizingElementIds(
10
+ resizingClip: ResizingClipState | null,
11
+ ): readonly string[] | undefined {
12
+ if (resizingClip?.groupPreview) return resizingClip.groupPreview.map((change) => change.key);
13
+ if (resizingClip) return [resizingClip.element.key ?? resizingClip.element.id];
14
+ return undefined;
15
+ }
16
+
17
+ export function resolveRenderClipContent(
18
+ rowVirtualizationActive: boolean,
19
+ isScrolling: boolean,
20
+ renderClipContent: TimelineLaneBaseProps["renderClipContent"],
21
+ ): TimelineLaneBaseProps["renderClipContent"] {
22
+ return rowVirtualizationActive && isScrolling ? undefined : renderClipContent;
23
+ }
24
+
25
+ export function shouldIgnoreTimelinePointerDown(target: EventTarget | null): boolean {
26
+ return target instanceof Element && target.closest("button, input, select, a") !== null;
27
+ }
28
+
29
+ type ContainerInputs = Omit<TimelineContainerProps, "className"> & {
30
+ isDragOver: boolean;
31
+ activeTool: string;
32
+ shiftHeld: boolean;
33
+ accentClass: string;
34
+ };
35
+
36
+ type ViewportInputs = Omit<
37
+ TimelineViewportProps,
38
+ "className" | "data-timeline-scroll-viewport" | "data-timeline-auto-scroll-left-inset"
39
+ > & {
40
+ labelMode: boolean;
41
+ zoomMode: "fit" | "manual";
42
+ };
43
+
44
+ export interface TimelineMetaBuilderInputs {
45
+ emptyState: TimelineMeta["emptyState"];
46
+ container: ContainerInputs;
47
+ viewport: ViewportInputs;
48
+ elementCount: number;
49
+ labelColumnWidth: number;
50
+ razorGuide: TimelineMeta["razorGuide"];
51
+ }
52
+
53
+ export function buildTimelineMeta({
54
+ emptyState,
55
+ container,
56
+ viewport,
57
+ elementCount,
58
+ labelColumnWidth,
59
+ razorGuide,
60
+ }: TimelineMetaBuilderInputs): TimelineMeta {
61
+ const { labelMode, zoomMode, ...viewportProps } = viewport;
62
+ const containerClassName = `relative border-t select-none h-full overflow-hidden ${container.isDragOver ? container.accentClass : ""} ${container.activeTool === "razor" ? "cursor-crosshair" : container.shiftHeld ? "cursor-crosshair" : "cursor-default"}`;
63
+ const viewportClassName = `${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-hidden`;
64
+ return {
65
+ emptyState,
66
+ containerProps: {
67
+ ref: container.ref,
68
+ "aria-label": container["aria-label"],
69
+ "data-timeline-element-count": elementCount,
70
+ className: containerClassName,
71
+ onMouseMove: container.onMouseMove,
72
+ onMouseLeave: container.onMouseLeave,
73
+ style: container.style,
74
+ },
75
+ viewportProps: {
76
+ ...viewportProps,
77
+ "data-timeline-scroll-viewport": true,
78
+ "data-timeline-auto-scroll-left-inset": labelMode ? labelColumnWidth : 0,
79
+ className: viewportClassName,
80
+ },
81
+ razorGuide,
82
+ };
83
+ }
@@ -9,10 +9,10 @@ import { getTrackStyle } from "./timelineIcons";
9
9
  describe("getTimelineTrackStyle", () => {
10
10
  it("uses one neutral clip style for every timeline tag", () => {
11
11
  const expectedStyle = {
12
- clip: "rgba(255,255,255,0.055)",
13
- clipActive: "rgba(60,230,172,0.16)",
14
- accent: "#3CE6AC",
15
- label: "rgba(255,255,255,0.5)",
12
+ clip: "var(--timeline-track-clip-fill)",
13
+ clipActive: "var(--timeline-track-clip-active)",
14
+ accent: "var(--color-accent)",
15
+ label: "var(--timeline-track-label)",
16
16
  };
17
17
 
18
18
  expect(getTimelineTrackStyle("video")).toEqual(expectedStyle);
@@ -22,6 +22,8 @@ export interface TimelineTheme {
22
22
  tickMinor: string;
23
23
  clipBackground: string;
24
24
  clipBackgroundActive: string;
25
+ clipBackgroundHover: string;
26
+ clipBackgroundDragging: string;
25
27
  clipBorder: string;
26
28
  clipBorderHover: string;
27
29
  clipBorderActive: string;
@@ -36,43 +38,45 @@ export interface TimelineTheme {
36
38
  }
37
39
 
38
40
  const TRACK_STYLE: TimelineTrackStyle = {
39
- clip: "rgba(255,255,255,0.055)",
40
- clipActive: "rgba(60,230,172,0.16)",
41
- accent: "#3CE6AC",
42
- label: "rgba(255,255,255,0.5)",
41
+ clip: "var(--timeline-track-clip-fill)",
42
+ clipActive: "var(--timeline-track-clip-active)",
43
+ accent: "var(--color-accent)",
44
+ label: "var(--timeline-track-label)",
43
45
  };
44
46
 
47
+ // Every field reads a CSS custom property (declared in styles/theme.css)
48
+ // rather than a literal, so a host themes the timeline the same way it
49
+ // themes the rest of Studio: by overriding the token, not this object.
45
50
  export const defaultTimelineTheme: TimelineTheme = {
46
- // Near-black card surfaces: the panels sit dark while the shell canvas
47
- // between them is a step LIGHTER (#18181B), so the gaps read as visible
48
- // seams around dark cards (CapCut-style).
49
- shellBackground: "#0A0A0B",
50
- shellBorder: "rgba(255,255,255,0.05)",
51
- rulerBorder: "rgba(255,255,255,0.16)",
52
- // All track lanes use a single uniform color — one step lighter than the panel
53
- // surface (#0A0A0B) so lanes are visibly distinct from the ruler/shell.
54
- rowBackground: "#101014",
55
- rowBorder: "rgba(255,255,255,0.06)",
56
- gutterBackground: "#0E0F12",
57
- gutterBorder: "rgba(255,255,255,0.10)",
58
- textPrimary: "rgba(255,255,255,0.92)",
59
- textSecondary: "rgba(255,255,255,0.62)",
60
- tickText: "rgba(255,255,255,0.34)",
61
- tickMajor: "rgba(255,255,255,0.10)",
62
- tickMinor: "rgba(255,255,255,0.06)",
63
- clipBackground: "#141922",
64
- clipBackgroundActive: "#181e28",
65
- clipBorder: "rgba(255,255,255,0.10)",
66
- clipBorderHover: "rgba(255,255,255,0.18)",
67
- clipBorderActive: "rgba(255,255,255,0.24)",
51
+ shellBackground: "var(--timeline-shell-bg)",
52
+ shellBorder: "var(--timeline-shell-border)",
53
+ rulerBorder: "var(--timeline-ruler-border)",
54
+ rowBackground: "var(--timeline-row-bg)",
55
+ rowBorder: "var(--timeline-row-border)",
56
+ gutterBackground: "var(--timeline-gutter-bg)",
57
+ gutterBorder: "var(--timeline-gutter-border)",
58
+ textPrimary: "var(--timeline-text-primary)",
59
+ textSecondary: "var(--timeline-text-secondary)",
60
+ tickText: "var(--timeline-tick-text)",
61
+ tickMajor: "var(--timeline-tick-major)",
62
+ tickMinor: "var(--timeline-tick-minor)",
63
+ clipBackground: "var(--timeline-clip-bg)",
64
+ clipBackgroundActive: "var(--timeline-clip-bg-active)",
65
+ clipBackgroundHover: "var(--timeline-clip-bg-hover)",
66
+ clipBackgroundDragging: "var(--timeline-clip-bg-dragging)",
67
+ clipBorder: "var(--timeline-clip-border)",
68
+ clipBorderHover: "var(--timeline-clip-border-hover)",
69
+ clipBorderActive: "var(--timeline-clip-border-active)",
70
+ // Shadows stay literal: a light host still wants a dark contact shadow, so
71
+ // these are not part of the surface/hairline/text/clip-colour theming ask.
68
72
  clipShadow: "none",
69
73
  clipShadowHover: "0 2px 8px rgba(0,0,0,0.2)",
70
74
  clipShadowActive: "0 2px 8px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.04)",
71
75
  clipShadowDragging: "0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)",
72
- handleColor: "rgba(255,255,255,0.2)",
73
- panelResizeSeam: "rgba(255,255,255,0.12)",
74
- panelResizeActive: "rgba(255,255,255,0.24)",
75
- clipRadius: "8px",
76
+ handleColor: "var(--timeline-handle)",
77
+ panelResizeSeam: "var(--timeline-resize-seam)",
78
+ panelResizeActive: "var(--timeline-resize-active)",
79
+ clipRadius: "var(--radius-lg)",
76
80
  };
77
81
 
78
82
  export function getTimelineTrackStyle(_tag: string): TimelineTrackStyle {
@@ -1,10 +1,7 @@
1
1
  /**
2
2
  * The one owner of "what track number does the user see".
3
3
  *
4
- * `TimelineElement.track` is a z-order SORT key, not a row number: an expanded
5
- * sub-composition child gets a synthesized fractional key (`host.track + n /
6
- * (siblings + 2)`, see `useTimelineRowElements`), so putting it in a string
7
- * announces "Hide track 0.16666666666666666". Every user-visible track number,
4
+ * `TimelineElement.track` is a z-order SORT key, not a row number. Every
8
5
  * whether it is rendered by a component or baked into an undo-history label,
9
6
  * routes through here; the raw key stays in callbacks and lookups only.
10
7
  */
@@ -1,6 +1,6 @@
1
1
  import { useCallback } from "react";
2
2
  import { usePlayerStore, type TimelineElement } from "../store/playerStore";
3
- import type { ClipContextMenuState } from "./TimelineOverlays";
3
+ import type { ClipContextMenuState } from "./TimelineProvider";
4
4
 
5
5
  /** A clicked member of an active multi-selection keeps the whole group
6
6
  * selected, so the context menu it opens acts on all of it. */
@@ -0,0 +1,100 @@
1
+ import type { TimelineElement } from "../store/playerStore";
2
+ import type { TimelineOverlaysState } from "./TimelineProvider";
3
+ import type { TimelineTimeRange } from "../store/rangeSelectionSlice";
4
+ import { useClipContextMenu } from "./useTimelineClipContextMenu";
5
+ import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
6
+ import { usePublishRangeSelection } from "./usePublishRangeSelection";
7
+ import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle";
8
+ import { useTrackGapMenu, type TrackGapHighlight } from "./useTrackGapMenu";
9
+
10
+ type OverlayStateInputs = Omit<
11
+ TimelineOverlaysState,
12
+ | "elements"
13
+ | "elementsRef"
14
+ | "gapContextMenu"
15
+ | "onDismissGapContextMenu"
16
+ | "onCloseTrackGap"
17
+ | "onCloseAllTrackGaps"
18
+ | "onHoverGapAction"
19
+ | "rangeSelection"
20
+ | "setRangeSelection"
21
+ > & {
22
+ elements: TimelineElement[];
23
+ elementsRef: { current: TimelineElement[] };
24
+ rangeSelection: Parameters<typeof useTimelineRangeSelection>[0] & {
25
+ onRangeSelect?: (range: TimelineTimeRange | null) => void;
26
+ };
27
+ gapMenu: Parameters<typeof useTrackGapMenu>[0];
28
+ onSelectElement: ((element: TimelineElement | null) => void) | undefined;
29
+ selectedElementId: string | null;
30
+ setRangeSelectionRef: { current: ((selection: null) => void) | null };
31
+ };
32
+
33
+ export type TimelineOverlaysStateResult = {
34
+ overlays: TimelineOverlaysState;
35
+ shiftClickClipRef: ReturnType<typeof useTimelineRangeSelection>["shiftClickClipRef"];
36
+ marqueeRect: ReturnType<typeof useTimelineRangeSelection>["marqueeRect"];
37
+ isScrubbing: ReturnType<typeof useTimelineRangeSelection>["isScrubbing"];
38
+ handlePointerDown: ReturnType<typeof useTimelineRangeSelection>["handlePointerDown"];
39
+ handlePointerMove: ReturnType<typeof useTimelineRangeSelection>["handlePointerMove"];
40
+ handlePointerUp: ReturnType<typeof useTimelineRangeSelection>["handlePointerUp"];
41
+ handlePointerCancel: ReturnType<typeof useTimelineRangeSelection>["handlePointerCancel"];
42
+ gapHighlight: TrackGapHighlight | null;
43
+ openGapMenu: ReturnType<typeof useTrackGapMenu>["openGapMenu"];
44
+ onContextMenuClip: ReturnType<typeof useClipContextMenu>;
45
+ };
46
+
47
+ export function useTimelineOverlaysState({
48
+ gapMenu,
49
+ rangeSelection: rangeSelectionInputs,
50
+ onSelectElement,
51
+ selectedElementId,
52
+ setRangeSelectionRef,
53
+ ...state
54
+ }: OverlayStateInputs): TimelineOverlaysStateResult {
55
+ const {
56
+ rangeSelection,
57
+ setRangeSelection,
58
+ shiftClickClipRef,
59
+ marqueeRect,
60
+ isScrubbing,
61
+ handlePointerDown,
62
+ handlePointerMove,
63
+ handlePointerUp,
64
+ handlePointerCancel,
65
+ } = useTimelineRangeSelection(rangeSelectionInputs);
66
+ usePublishRangeSelection(rangeSelection, rangeSelectionInputs.onRangeSelect);
67
+ setRangeSelectionRef.current = () => setRangeSelection(null); // oxlint-disable-line react/refs -- stable ref consumed by useTimelineClipDrag
68
+ useTimelineSelectionLifecycle(state.elements, selectedElementId, state.setShowPopover, () =>
69
+ setRangeSelection(null),
70
+ );
71
+ const gap = useTrackGapMenu(gapMenu);
72
+ const onContextMenuClip = useClipContextMenu(
73
+ onSelectElement,
74
+ gap.dismissGapMenu,
75
+ state.setClipContextMenu,
76
+ );
77
+
78
+ return {
79
+ overlays: {
80
+ ...state,
81
+ rangeSelection,
82
+ setRangeSelection,
83
+ gapContextMenu: gap.gapMenuModel,
84
+ onDismissGapContextMenu: gap.dismissGapMenu,
85
+ onCloseTrackGap: gap.closeTrackGap,
86
+ onCloseAllTrackGaps: gap.closeAllTrackGaps,
87
+ onHoverGapAction: gap.setHoveredGapAction,
88
+ },
89
+ shiftClickClipRef,
90
+ marqueeRect,
91
+ isScrubbing,
92
+ handlePointerDown,
93
+ handlePointerMove,
94
+ handlePointerUp,
95
+ handlePointerCancel,
96
+ gapHighlight: gap.gapHighlight,
97
+ openGapMenu: gap.openGapMenu,
98
+ onContextMenuClip,
99
+ };
100
+ }