import { useEffect, useMemo, useRef, useState } from "react"; import { formatDate, formatDateTime, formatTime, kindColor, kindLabel, niceTicks } from "./editorPresentation"; import type { EditorContextLane, EditorTimelineWindow, EditorVisibilityTrack, EditorWork } from "./model/editorTypes"; import { VisibilityTracks } from "./VisibilityTracks"; type EditorTimelineProps = { activeLabel: string; works: EditorWork[]; selectedWorkId?: string; window: EditorTimelineWindow; planStartMs?: number; planEndMs?: number; conflicts: Record; contextLanes: EditorContextLane[]; visibilityTracks: EditorVisibilityTrack[]; onSelectWork: (workId: string) => void; onClearSelection: () => void; }; const LABEL_WIDTH = 168; const AXIS_HEIGHT = 40; /** Минимальная высота активной дорожки (два ряда: съёмка + сброс). */ const MIN_ACTIVE_HEIGHT = 84; const VISIBILITY_HEIGHT = 24; const OTHER_HEIGHT = 30; export function EditorTimeline({ activeLabel, works, selectedWorkId, window: timelineWindow, planStartMs, planEndMs, conflicts, contextLanes, visibilityTracks, onSelectWork, onClearSelection }: EditorTimelineProps) { const wrapperRef = useRef(null); const [bodyWidth, setBodyWidth] = useState(900); const [bodyHeight, setBodyHeight] = useState(260); const span = Math.max(1, timelineWindow.toMs - timelineWindow.fromMs); const pxPerMs = bodyWidth / span; const toX = (timeMs: number) => (timeMs - timelineWindow.fromMs) * pxPerMs; const ticks = useMemo(() => niceTicks(timelineWindow.fromMs, timelineWindow.toMs, pxPerMs, 72), [pxPerMs, timelineWindow.fromMs, timelineWindow.toMs]); const days = useMemo(() => buildDays(timelineWindow), [timelineWindow]); // Фиксированная высота частей ниже активной дорожки (треки видимости + контекст КА). const belowActiveHeight = visibilityTracks.length * VISIBILITY_HEIGHT + (contextLanes.length > 0 ? 22 : 0) + contextLanes.length * OTHER_HEIGHT + 8; // Активная дорожка резиновая: занимает всё свободное место панели, поэтому при // подъёме панели вверх ряды съёмки/сброса расширяются, а низ не пустует. const activeHeight = Math.max(MIN_ACTIVE_HEIGHT, bodyHeight - AXIS_HEIGHT - belowActiveHeight); const visibilityTop = AXIS_HEIGHT + activeHeight; const dividerTop = visibilityTop + visibilityTracks.length * VISIBILITY_HEIGHT; const contextTop = dividerTop + (contextLanes.length > 0 ? 22 : 0); const contentHeight = AXIS_HEIGHT + activeHeight + belowActiveHeight; const surfaceHeight = Math.max(bodyHeight, contentHeight); useEffect(() => { const element = wrapperRef.current; if (!element) return; const measure = () => { setBodyWidth(Math.max(560, element.clientWidth - LABEL_WIDTH)); setBodyHeight(element.clientHeight); }; const observer = new ResizeObserver(measure); observer.observe(element); measure(); return () => observer.disconnect(); }, []); return (
Время
{days.map((day) => (
{formatDate(day).split(" ")[0]} {formatDate(day).split(" ")[1]}
))} {ticks.map((tick) => (
{formatTime(tick)}
))}
{ticks.map((tick) => (
))}
{planStartMs != null && planStartMs >= timelineWindow.fromMs && planStartMs <= timelineWindow.toMs && (
Начало плана
)} {planEndMs != null && planEndMs >= timelineWindow.fromMs && planEndMs <= timelineWindow.toMs && (
Конец плана
)}
{activeLabel} редактируется Съёмка Сброс
{works.map((work) => { const left = toX(work.startMs); const width = Math.max(6, toX(work.endMs) - toX(work.startMs)); const color = kindColor(work.kind); const selected = selectedWorkId === work.id; const hasConflict = Boolean(conflicts[work.id]?.length); return ( ); })}
{contextLanes.length > 0 && (
Другие КА · контекст (read-only)
)} {contextLanes.map((lane, index) => (
{lane.label}
{lane.works.map((work) => { if (work.endMs < timelineWindow.fromMs || work.startMs > timelineWindow.toMs) return null; const left = toX(work.startMs); const width = Math.max(3, toX(work.endMs) - toX(work.startMs)); return (
); })}
))}
); } function buildDays(window: EditorTimelineWindow): number[] { const result: number[] = []; const start = new Date(window.fromMs); let dayMs = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime(); while (dayMs <= window.toMs) { result.push(dayMs); dayMs += 24 * 60 * 60 * 1000; } return result; }