Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
69c603c
refactor(studio): keep legacy fixture state out of rendered rows
miguel-heygen Sep 20, 2026
044d91e
fix(studio): keep timeline keyboard actor mounted
miguel-heygen Sep 20, 2026
ff4f966
refactor(studio): remove expansion telemetry and prove stable nested …
miguel-heygen Sep 20, 2026
369b4f3
test(studio): type nested timeline fixture broadly
miguel-heygen Sep 20, 2026
5073869
test(studio): remove expansion lane assertions
miguel-heygen Sep 20, 2026
53a5a58
test(studio): assert nested row identity by key
miguel-heygen Sep 20, 2026
a4dea2e
fix(studio): satisfy removal and format guards
miguel-heygen Sep 20, 2026
ec57f1d
fix(studio): clear fallow findings in timeline cleanup
miguel-heygen Sep 20, 2026
079d50e
test(studio): update timeline removal expectations
miguel-heygen Sep 20, 2026
5f7263d
fix(studio): remove stale timeline test helpers
miguel-heygen Sep 20, 2026
e5576fa
fix(studio): keep automation lanes mounted without disclosure
miguel-heygen Sep 20, 2026
64d2410
refactor(studio): keep audio lanes while removing clip expansion
miguel-heygen Sep 20, 2026
d072ba1
fix(studio): restore audio disclosure state
miguel-heygen Sep 20, 2026
e05aea6
test(studio): remove obsolete expanded fixture assertions
miguel-heygen Sep 20, 2026
0757ed2
fix(studio): resolve provider rebase without clip auto expansion
miguel-heygen Sep 20, 2026
21f1433
fix(studio): remove inline keyframe expansion rows
miguel-heygen Sep 21, 2026
a4d7195
fix(studio): restore automation disclosure state
miguel-heygen Sep 21, 2026
d71663c
fix(studio): remove stale timeline model inputs
miguel-heygen Sep 21, 2026
2e82cd9
fix(studio): pass animation state to timeline layout
miguel-heygen Sep 21, 2026
cae707f
fix(studio): remove stale expansion tests and helpers
miguel-heygen Sep 21, 2026
e711b69
test(studio): cover shared audio lane labels
miguel-heygen Sep 21, 2026
fe99d5a
fix(studio): remove unused keyframe callback plumbing
miguel-heygen Sep 21, 2026
dd1bc96
fix(studio): clear rebase lint leftovers
miguel-heygen Sep 21, 2026
4ab6d73
fix(studio): remove unused timeline header export
miguel-heygen Sep 21, 2026
ceb5b4b
style(studio): format timeline cleanup
miguel-heygen Sep 21, 2026
d992255
fix(studio): restore composable timeline exports
miguel-heygen Sep 21, 2026
10fe462
fix(studio): keep audio lanes open across selection
miguel-heygen Sep 21, 2026
9fa0d88
fix(studio): align timeline lane test contract
miguel-heygen Sep 21, 2026
adf7d73
fix(studio): remove stale lane test import
miguel-heygen Sep 21, 2026
f4f9a56
fix(studio): align track header test callback
miguel-heygen Sep 21, 2026
efd2e2c
fix(studio): remove stale track header test prop
miguel-heygen Sep 21, 2026
5b67b0a
test(studio): preserve ruler half pixel alignment
miguel-heygen Sep 21, 2026
48246eb
test(studio): keep audio lane fixtures drawable
miguel-heygen Sep 21, 2026
066bb98
test(studio): assert shared lane owner label
miguel-heygen Sep 21, 2026
3d2dce2
fix(studio): align audio lane expansion with layout state
miguel-heygen Sep 21, 2026
214bced
test(studio): use the audio lane owner label
miguel-heygen Sep 21, 2026
738926f
fix(studio): unify audio lane expansion state
miguel-heygen Sep 21, 2026
47f9904
fix(studio): align automation lane labels
miguel-heygen Sep 21, 2026
af9b917
fix(studio): remove stale lane height import
miguel-heygen Sep 21, 2026
1a4882e
Merge origin/main into hfoss22/d834-pr2
miguel-heygen Sep 22, 2026
6980d4f
fix(studio): stride automation lane labels by AUTOMATION_LANE_H
miguel-heygen Sep 22, 2026
8c3da13
test(studio): assert automation labels against the real curve, not a …
miguel-heygen Sep 22, 2026
0ea52b6
revert(studio): restore the keyframe-row feature from main
miguel-heygen Sep 23, 2026
a977479
refactor(studio): remove dead expanded-child timeline code
miguel-heygen Sep 23, 2026
05fcf70
chore: merge origin/main into hfoss22/d834-pr2
miguel-heygen Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 0 additions & 63 deletions packages/studio/src/components/nle/TimelinePane.test.ts

This file was deleted.

193 changes: 8 additions & 185 deletions packages/studio/src/components/nle/TimelinePane.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,10 @@
import { useCallback, type ReactNode } from "react";
import type { ReactNode } from "react";
import { Timeline } from "../../player";
import type { TimelineElement, TimelineTimeRange } from "../../player";
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
import { AudioMeterStrip } from "./AudioMeterStrip";
import { useTimelineEditContext } from "../../contexts/TimelineEditContext";
import { trackStudioExpandedClipEdit } from "../../telemetry/events";
import { useNLEContext } from "./NLEContext";
import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";

type TimelineMoveEdit = {
element: TimelineElement;
updates: Pick<TimelineElement, "start" | "track">;
};

export function forwardRebasedTimelineMoveElements(
edits: TimelineMoveEdit[],
coalesceKey: string | undefined,
operation: TimelineMoveOperation | undefined,
onMoveElements: (
edits: TimelineMoveEdit[],
coalesceKey?: string,
operation?: TimelineMoveOperation,
coalesceMs?: number,
) => Promise<void> | void,
coalesceMs?: number,
) {
return onMoveElements(
edits.map(({ element, updates }) => {
const basis = element.expandedParentStart;
if (basis === undefined) return { element, updates };
return {
element: { ...element, id: element.domId ?? element.id, start: element.start - basis },
updates: { ...updates, start: Math.max(0, updates.start - basis) },
};
}),
coalesceKey,
operation,
coalesceMs,
);
}

type TimelineResizeChange = {
element: TimelineElement;
start: number;
duration: number;
playbackStart?: number;
};

export function forwardRebasedTimelineResizeElements(
changes: TimelineResizeChange[],
options: { coalesceKey?: string } | undefined,
onResizeElements: (
changes: TimelineResizeChange[],
options?: { coalesceKey?: string },
) => Promise<void> | void,
) {
return onResizeElements(
changes.map((change) => {
const basis = change.element.expandedParentStart;
if (basis === undefined) return change;
return {
...change,
element: {
...change.element,
id: change.element.domId ?? change.element.id,
start: change.element.start - basis,
},
start: Math.max(0, change.start - basis),
};
}),
options,
);
}

export interface TimelinePaneProps {
/** Slot rendered above the timeline tracks (toolbar with split, delete, zoom) */
Expand Down Expand Up @@ -114,7 +47,6 @@ export interface TimelinePaneProps {
canPasteClip?: () => boolean;
}

// fallow-ignore-next-line complexity
export function TimelinePane({
timelineToolbar,
timelineFooter,
Expand Down Expand Up @@ -142,119 +74,10 @@ export function TimelinePane({
timelineSessionEpoch,
} = useNLEContext();

// Move/resize/split come from the timeline edit context, not props — the
// wrappers below intercept expanded clips and must call the *real* handlers.
// (Delete is a direct prop; it stays that way.)
// Move/resize/split come from the timeline edit context, not props.
const { onMoveElement, onMoveElements, onResizeElement, onResizeElements, onSplitElement } =
useTimelineEditContext();

// An expanded sub-comp child reaches the normal edit handlers in its own
// local coordinates: addressed by its real DOM id, with timeline time rebased
// onto the sub-comp it lives in. The handlers then save + reloadPreview exactly
// as they do for top-level clips — no separate live-DOM path.
const toLocalElement = useCallback(
(element: TimelineElement, basis: number): TimelineElement => ({
...element,
id: element.domId ?? element.id,
start: element.start - basis,
}),
[],
);

const handleMoveElement = useCallback(
(element: TimelineElement, updates: Pick<TimelineElement, "start" | "track">) => {
const basis = element.expandedParentStart;
if (basis === undefined) return onMoveElement?.(element, updates);
trackStudioExpandedClipEdit({ action: "move" });
onMoveElement?.(toLocalElement(element, basis), {
...updates,
start: Math.max(0, updates.start - basis),
});
},
[onMoveElement, toLocalElement],
);

// Batched move (ripple / insert): rebase each expanded sub-comp child to its
// local coords, exactly as handleMoveElement does for a single clip.
const handleMoveElements = useCallback(
(
edits: Array<{ element: TimelineElement; updates: Pick<TimelineElement, "start" | "track"> }>,
coalesceKey?: string,
operation?: TimelineMoveOperation,
coalesceMs?: number,
) => {
// Match the sibling handlers: report the telemetry when the batch touches at
// least one expanded sub-comp child (the clips being rebased to local coords).
if (edits.some(({ element }) => element.expandedParentStart !== undefined)) {
trackStudioExpandedClipEdit({ action: "move" });
}
if (!onMoveElements) return;
return forwardRebasedTimelineMoveElements(
edits,
coalesceKey,
operation,
onMoveElements,
coalesceMs,
);
},
[onMoveElements],
);

const handleResizeElement = useCallback(
(
element: TimelineElement,
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
) => {
const basis = element.expandedParentStart;
if (basis === undefined) return onResizeElement?.(element, updates);
trackStudioExpandedClipEdit({ action: "resize" });
onResizeElement?.(toLocalElement(element, basis), {
...updates,
start: Math.max(0, updates.start - basis),
});
},
[onResizeElement, toLocalElement],
);

const handleResizeElements = useCallback(
(
changes: Array<{
element: TimelineElement;
start: number;
duration: number;
playbackStart?: number;
}>,
options?: { coalesceKey?: string },
) => {
if (!onResizeElements) return;
if (changes.some(({ element }) => element.expandedParentStart !== undefined)) {
trackStudioExpandedClipEdit({ action: "resize" });
}
return forwardRebasedTimelineResizeElements(changes, options, onResizeElements);
},
[onResizeElements],
);

const handleDeleteElement = useCallback(
(element: TimelineElement) => {
const basis = element.expandedParentStart;
if (basis === undefined) return onDeleteElement?.(element);
trackStudioExpandedClipEdit({ action: "delete" });
return onDeleteElement?.(toLocalElement(element, basis));
},
[onDeleteElement, toLocalElement],
);

const handleSplitElement = useCallback(
(element: TimelineElement, splitTime: number) => {
const basis = element.expandedParentStart;
if (basis === undefined) return onSplitElement?.(element, splitTime);
trackStudioExpandedClipEdit({ action: "split" });
return onSplitElement?.(toLocalElement(element, basis), Math.max(0, splitTime - basis));
},
[onSplitElement, toLocalElement],
);

return (
<div
className="relative flex h-full flex-col"
Expand All @@ -280,16 +103,16 @@ export function TimelinePane({
onDrillDown={handleDrillDown}
renderClipContent={renderClipContent}
onFileDrop={onFileDrop}
onDeleteElement={handleDeleteElement}
onDeleteElement={onDeleteElement}
onAssetDrop={onAssetDrop}
onBlockDrop={onBlockDrop}
onCompositionDrop={onCompositionDrop}
onMoveElement={handleMoveElement}
onMoveElements={handleMoveElements}
onResizeElement={handleResizeElement}
onResizeElements={handleResizeElements}
onMoveElement={onMoveElement}
onMoveElements={onMoveElements}
onResizeElement={onResizeElement}
onResizeElements={onResizeElements}
onBlockedEditAttempt={onBlockedEditAttempt}
onSplitElement={handleSplitElement}
onSplitElement={onSplitElement}
onSelectElement={onSelectTimelineElement}
onRangeSelect={onRangeSelect}
onCopyClip={onCopyClip}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,55 +321,6 @@ describe("useCanvasZOrderTimelineMirror", () => {
expect(history.moveCoalesceKeys).toEqual([]);
});

it("maps the crossed neighbor to its timeline key and rebases expanded sub-comp children", async () => {
// t is an expanded sub-comp child (expandedParentStart 5, absolute start 5):
// the mirror must forward its persist in LOCAL time (start 0), the same
// rebase a timeline lane drag applies (forwardRebasedTimelineMoveElements).
setStoreElements([
{
...storeEl("a", 0, 25, 5),
sourceFile: "sub.html",
key: "sub.html#a",
expandedParentStart: 5,
},
{
...storeEl("b", 1, 5, 10),
sourceFile: "sub.html",
key: "sub.html#b",
expandedParentStart: 5,
},
{
...storeEl("t", 2, 5, 10),
sourceFile: "sub.html",
key: "sub.html#t",
expandedParentStart: 5,
},
]);
const edits: Array<{ element: TimelineElement; updates: { start: number; track: number } }> =
[];
const onMoveElements: TimelineEditCallbacks["onMoveElements"] = (batch) => {
edits.push(...batch);
};
const { mirror } = mountMirrorOnlyHarness(onMoveElements);

const mirrored = await act(async () =>
mirror({
selectionKey: "sub.html#t",
action: "bring-forward",
// The crossed sibling maps to sub.html#b via its DOM id + sourceFile —
// the same derivation reorder entries use (deriveTimelineStoreKey).
crossed: domTarget("b"),
sourceFile: "sub.html",
coalesceKey: "z-reorder:bring-forward:t",
}),
);
expect(mirrored).toBe(true);
expect(edits).toHaveLength(1);
// Rebased to sub-comp local coords: absolute 5 − parent start 5 = 0.
expect(edits[0].element.start).toBe(0);
expect(edits[0].updates).toMatchObject({ start: 0, track: 0 });
});

it("maps a cross-file duplicate selector to the source-scoped crossed occurrence", async () => {
setStoreElements([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { commitZMirrorLaneMove } from "../../player/components/timelineClipDragC
import { deriveTimelineStoreKey } from "../../player/lib/timelineElementHelpers";
import { buildStableSelector, getSelectorIndex } from "../editor/domEditingDom";
import { useStudioShellContextOptional } from "../../contexts/StudioContext";
import { forwardRebasedTimelineMoveElements } from "./TimelinePane";

export interface MirrorZOrderInput {
/** Timeline store key of the element the menu acted on (entry.key), if any. */
Expand Down Expand Up @@ -44,10 +43,9 @@ export interface MirrorZOrderInput {
* renders and the resolver expects. No alternate row expansion is built here.
*
* The mirror persists through the SAME machinery as a timeline lane drag
* (commitZMirrorLaneMove → persistMoveEdits → onMoveElements, with expanded
* children rebased to local coords via forwardRebasedTimelineMoveElements) —
* optimistic store update + rollback included, so the timeline reflects the
* lane change without a reload. The deps below deliberately OMIT
* (commitZMirrorLaneMove → persistMoveEdits → onMoveElements) — optimistic
* store update + rollback included, so the timeline reflects the lane change
* without a reload. The deps below deliberately OMIT
* `readZIndex`/`onStackingPatches`: the lane→z stacking sync
* (syncStackingForEdit) must not fire and recompute the z values the user just
* set — commitZMirrorLaneMove never calls it, and without these deps it would
Expand Down Expand Up @@ -160,16 +158,7 @@ function useMirrorLaneMoveCommit(): (
elements: els,
trackOrder: displayTrackOrder(els),
updateElement: (key, updates) => usePlayerStore.getState().updateElement(key, updates),
onMoveElements: onMoveElements
? (edits, coalesceKey2, operation, coalesceMs) =>
forwardRebasedTimelineMoveElements(
edits,
coalesceKey2,
operation,
onMoveElements,
coalesceMs,
)
: undefined,
onMoveElements,
// NO readZIndex / onStackingPatches: see the hook doc — the lane→z
// stacking sync must not re-trigger and fight the just-set z values.
},
Expand Down
Loading
Loading