diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index 2aa0a0794..8a9e3446e 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -19,16 +19,10 @@ import { elementFxChain, groupAutomationLanes, isCarveLane } from "./automationL import { AUTOMATION_LANE_H } from "./automationLaneHeight"; import { clipTimingStart } from "../../hooks/gsapShared"; import { LaneToggleButton, LayerDisclosureRow } from "./LayerDisclosureRow"; -import { LABEL_COL_W, LANE_H, TRACK_H, getTimelineLaneTop } from "./timelineLayout"; +import { LABEL_COL_W, TRACK_H, getTimelineLaneTop } from "./timelineLayout"; import type { TimelineTheme } from "./timelineTheme"; -import { - resolveLaneHeaderState, - type KeyframeNavigationState, - type TimelinePropertyLane, -} from "./trackHeaderLaneState"; -import { valueReadout } from "./trackHeaderLaneValues"; import { trackDisplaySuffix } from "./timelineTrackDisplay"; -import { timelineLogicalRowCellId, timelinePropertyRowId } from "./timelineNavigationIdentity"; +import { AutomationLaneHeaderRow, PropertyGroupHeaderRow } from "./trackHeaderLabelRows"; /** Accent rail + inset marking a row as a group MEMBER, matching the level-2 * nesting its `aria-level` already reports. */ @@ -89,313 +83,6 @@ interface TimelineTrackHeaderProps { onSeek?: (time: number) => void; } -// Figma layout: prev-keyframe ‹, the add/remove toggle (children), next ›. -function PropertyGroupNavigation({ - navigation, - label, - expandedElement, - onSeek, - children, -}: { - navigation: KeyframeNavigationState; - label: string; - expandedElement: TimelineElement; - onSeek?: (time: number) => void; - children: React.ReactNode; -}) { - // The 12x20px glyph is all the lane row has room for, so the WCAG 24x24 - // target is met with a centered transparent ::before overlay instead of a - // bigger box; focus-visible matches every other control in this header. - const CHEVRON_BUTTON_CLASS = - "relative h-5 w-3 border-0 bg-transparent p-0 text-white/55 hover:text-white disabled:text-white/15 " + - "focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " + - "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " + - "before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']"; - const seekTo = (keyframe: { percentage: number } | null) => { - if (keyframe) { - onSeek?.(expandedElement.start + (keyframe.percentage / 100) * expandedElement.duration); - } - }; - return ( - - - {children} - - - ); -} - -function PropertyGroupHeaderRow({ - lanesId, - lane, - laneIndex, - isLastLane, - expandedElement, - currentTime, - clipPercentage, - gutterBackground, - columnWidth, - onTogglePropertyGroupKeyframe, - onSeek, - rovingTargetId = null, -}: { - lanesId: string; - lane: TimelinePropertyLane; - laneIndex: number; - isLastLane: boolean; - expandedElement: TimelineElement; - currentTime: number; - clipPercentage: number; - gutterBackground: string; - columnWidth: number; - onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"]; - onSeek?: (time: number) => void; - rovingTargetId: string | null; -}) { - const elementId = expandedElement.key ?? expandedElement.id; - const { navigation, values, label, toggleTarget } = resolveLaneHeaderState( - lane, - currentTime, - clipPercentage, - ); - - return ( -
- {/* Tree connector: vertical spine (top-half on the last lane) + branch tick. */} -
- ); -} - -/** - * One envelope's row in the label column. - * - * Named here rather than inside the lane, on the same tree connector the - * keyframe rows use: an automation lane is a child of its clip exactly as a - * property group is, and drawing its name over the envelope put the label on top - * of the curve it describes and scrolled it away from its own row. - */ -function AutomationLaneHeaderRow({ - target, - label, - name, - param, - alsoAutomatedBy, - top, - isLastLane, - gutterBackground, - columnWidth, - onRemove, - isCarve, - onReveal, -}: { - /** The lane the ACTIVE clip draws in this row, or null when it draws none — - * the row belongs to the property, and a clip may be absent from it. */ - target: string | null; - /** The whole thing on one line: the row's identity, its tooltip, and the - * remove button's name. */ - label: string; - /** What the effect is — "Peaking EQ 1.6 kHz". */ - name: string; - /** Which knob the envelope drives. Empty when there is no second line to draw. */ - param: string; - /** Set when this clip's group automates the SAME parameter. Gain stages - * multiply — 0.42 on the group under 0.80 here plays at 0.34 — so an author - * who drew one curve and then another hears something quieter than either - * with nothing on screen to say why (groups doc §5). Not a warning; an - * explanation. */ - alsoAutomatedBy?: string; - top: number; - isLastLane: boolean; - gutterBackground: string; - columnWidth: number; - onRemove?: (target: string) => void; - /** The carve owns this envelope and rewrites it on every re-run, so it is - * shown but not the author's to edit or delete. */ - isCarve?: boolean; - /** Reveal this parameter in the rack. Absent, the name is inert rather than a - * button that looks live and does nothing. */ - onReveal?: () => void; -}) { - return ( -
- {/* Tree connector, as the keyframe rows draw it: spine down the row, branch - tick at the name's own height. */} -
- ); -} - // fallow-ignore-next-line complexity export function TimelineTrackHeader({ trackNumber, diff --git a/packages/studio/src/player/components/trackHeaderLabelRows.tsx b/packages/studio/src/player/components/trackHeaderLabelRows.tsx new file mode 100644 index 000000000..55e606a62 --- /dev/null +++ b/packages/studio/src/player/components/trackHeaderLabelRows.tsx @@ -0,0 +1,328 @@ +/** + * The rows a track header draws BELOW its own line: one per disclosed keyframe + * property group, one per automation envelope. + * + * Split out of `TimelineTrackHeader.tsx`, which owned all of this and stood at + * 763 lines against the studio's 600-line cap. Nothing else moved: these three + * components never read the header's state, only their own props, which is why + * they are the seam. + */ + +import type { TimelineElement } from "../store/playerStore"; +import type { TimelineEditCallbacks } from "./timelineCallbacks"; +import { AUTOMATION_LANE_H } from "./automationLaneHeight"; +import { LANE_H, getTimelineLaneTop } from "./timelineLayout"; +import { + resolveLaneHeaderState, + type KeyframeNavigationState, + type TimelinePropertyLane, +} from "./trackHeaderLaneState"; +import { valueReadout } from "./trackHeaderLaneValues"; +import { timelineLogicalRowCellId, timelinePropertyRowId } from "./timelineNavigationIdentity"; + +// Figma layout: prev-keyframe ‹, the add/remove toggle (children), next ›. +function PropertyGroupNavigation({ + navigation, + label, + expandedElement, + onSeek, + children, +}: { + navigation: KeyframeNavigationState; + label: string; + expandedElement: TimelineElement; + onSeek?: (time: number) => void; + children: React.ReactNode; +}) { + // The 12x20px glyph is all the lane row has room for, so the WCAG 24x24 + // target is met with a centered transparent ::before overlay instead of a + // bigger box; focus-visible matches every other control in this header. + const CHEVRON_BUTTON_CLASS = + "relative h-5 w-3 border-0 bg-transparent p-0 text-white/55 hover:text-white disabled:text-white/15 " + + "focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " + + "before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " + + "before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']"; + const seekTo = (keyframe: { percentage: number } | null) => { + if (keyframe) { + onSeek?.(expandedElement.start + (keyframe.percentage / 100) * expandedElement.duration); + } + }; + return ( + + + {children} + + + ); +} + +export function PropertyGroupHeaderRow({ + lanesId, + lane, + laneIndex, + isLastLane, + expandedElement, + currentTime, + clipPercentage, + gutterBackground, + columnWidth, + onTogglePropertyGroupKeyframe, + onSeek, + rovingTargetId = null, +}: { + lanesId: string; + lane: TimelinePropertyLane; + laneIndex: number; + isLastLane: boolean; + expandedElement: TimelineElement; + currentTime: number; + clipPercentage: number; + gutterBackground: string; + columnWidth: number; + onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"]; + onSeek?: (time: number) => void; + rovingTargetId: string | null; +}) { + const elementId = expandedElement.key ?? expandedElement.id; + const { navigation, values, label, toggleTarget } = resolveLaneHeaderState( + lane, + currentTime, + clipPercentage, + ); + + return ( +
+ {/* Tree connector: vertical spine (top-half on the last lane) + branch tick. */} +
+ ); +} + +/** + * One envelope's row in the label column. + * + * Named here rather than inside the lane, on the same tree connector the + * keyframe rows use: an automation lane is a child of its clip exactly as a + * property group is, and drawing its name over the envelope put the label on top + * of the curve it describes and scrolled it away from its own row. + */ +export function AutomationLaneHeaderRow({ + target, + label, + name, + param, + alsoAutomatedBy, + top, + isLastLane, + gutterBackground, + columnWidth, + onRemove, + isCarve, + onReveal, +}: { + /** The lane the ACTIVE clip draws in this row, or null when it draws none — + * the row belongs to the property, and a clip may be absent from it. */ + target: string | null; + /** The whole thing on one line: the row's identity, its tooltip, and the + * remove button's name. */ + label: string; + /** What the effect is — "Peaking EQ 1.6 kHz". */ + name: string; + /** Which knob the envelope drives. Empty when there is no second line to draw. */ + param: string; + /** Set when this clip's group automates the SAME parameter. Gain stages + * multiply — 0.42 on the group under 0.80 here plays at 0.34 — so an author + * who drew one curve and then another hears something quieter than either + * with nothing on screen to say why (groups doc §5). Not a warning; an + * explanation. */ + alsoAutomatedBy?: string; + top: number; + isLastLane: boolean; + gutterBackground: string; + columnWidth: number; + onRemove?: (target: string) => void; + /** The carve owns this envelope and rewrites it on every re-run, so it is + * shown but not the author's to edit or delete. */ + isCarve?: boolean; + /** Reveal this parameter in the rack. Absent, the name is inert rather than a + * button that looks live and does nothing. */ + onReveal?: () => void; +}) { + return ( +
+ {/* Tree connector, as the keyframe rows draw it: spine down the row, branch + tick at the name's own height. */} +
+ ); +}