UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

86 lines (85 loc) 5.2 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useMemo, useState } from 'react'; import { cn } from '../lib/utils.js'; import { LaneRow } from './lane.js'; import { defaultLaneTimelineLabels, } from './lane-timeline-types.js'; import { TimeAxis } from './time-axis.js'; const LABEL_GUTTER_PX = 140; const RANGE_MS = { '1h': 60 * 60_000, '6h': 6 * 60 * 60_000, '24h': 24 * 60 * 60_000, '7d': 7 * 24 * 60 * 60_000, }; export function LaneTimeline({ lanes, now, defaultRange = '6h', range: controlledRange, onRangeChange, selectedDate: controlledDate, onDateChange, title, groupChip, activeCount, legend, selectedBarId, onSelectBar, labels, className, }) { const resolvedLabels = { ...defaultLaneTimelineLabels, ...(labels ?? {}) }; const [internalRange, setInternalRange] = useState(defaultRange); const [internalDate, setInternalDate] = useState(() => now ?? new Date()); const range = controlledRange ?? internalRange; const selectedDate = controlledDate ?? internalDate; const referenceNow = now ?? new Date(); const { windowStart, windowEnd } = useMemo(() => { const rangeMs = RANGE_MS[range]; const futurePadMs = Math.max(rangeMs / 6, 5 * 60_000); const isToday = selectedDate.toDateString() === referenceNow.toDateString(); const end = isToday ? new Date(referenceNow.getTime() + futurePadMs) : endOfDay(selectedDate); // futurePadMs is only relevant for the "today" path (gives the now-line breathing room). // Historical days should render exactly rangeMs of window — no asymmetric pad. const start = new Date(end.getTime() - rangeMs - (isToday ? futurePadMs : 0)); return { windowStart: start, windowEnd: end }; }, [range, selectedDate, referenceNow]); const ticks = useMemo(() => buildTicks(windowStart, windowEnd, range), [windowStart, windowEnd, range]); const handleRange = (r) => { if (controlledRange === undefined) setInternalRange(r); onRangeChange?.(r); }; const handleDate = (d) => { if (controlledDate === undefined) setInternalDate(d); onDateChange?.(d); }; const showNowLine = referenceNow.getTime() >= windowStart.getTime() && referenceNow.getTime() <= windowEnd.getTime(); const nowPositionPct = showNowLine ? ((referenceNow.getTime() - windowStart.getTime()) / (windowEnd.getTime() - windowStart.getTime())) * 100 : 0; return (_jsxs("section", { className: cn('flex flex-col rounded-lg border border-border bg-card p-4', className), children: [_jsx(TimeAxis, { range: range, onRangeChange: handleRange, selectedDate: selectedDate, onDateChange: handleDate, now: referenceNow, title: title, groupChip: groupChip, activeCount: activeCount, labels: resolvedLabels, ticks: ticks, labelGutterPx: LABEL_GUTTER_PX }), _jsxs("div", { className: "relative", children: [lanes.map((lane) => (_jsx(LaneRow, { lane: lane, windowStart: windowStart, windowEnd: windowEnd, labelGutterPx: LABEL_GUTTER_PX, selectedBarId: selectedBarId, onSelectBar: onSelectBar }, lane.id))), showNowLine && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute top-0 bottom-0 w-px bg-primary", style: { left: `calc(${LABEL_GUTTER_PX}px + (100% - ${LABEL_GUTTER_PX}px) * ${nowPositionPct / 100})`, }, children: _jsx("div", { className: "-translate-x-1/2 -top-1 absolute size-2 rounded-full bg-primary" }) }))] }), legend && legend.length > 0 && (_jsxs("div", { className: "mt-3 flex items-center gap-4 border-border/40 border-t pt-3 text-muted-foreground text-xs", children: [_jsx("span", { className: "uppercase tracking-wider", children: "Legend" }), legend.map((item) => (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: cn('inline-block size-2 rounded-sm', item.swatchClass) }), item.label] }, item.label))), showNowLine && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("span", { className: "inline-block size-2 rounded-full bg-primary" }), resolvedLabels.now, " \u00B7 ", formatTime(referenceNow)] }))] }))] })); } function endOfDay(d) { const e = new Date(d); e.setHours(23, 59, 59, 999); return e; } function formatTime(d) { return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', hour12: false, }).format(d); } function buildTicks(start, end, range) { const span = end.getTime() - start.getTime(); const tickCount = range === '24h' ? 8 : 7; const out = []; for (let i = 0; i <= tickCount; i++) { const t = new Date(start.getTime() + (span * i) / tickCount); out.push({ position: (i / tickCount) * 100, label: formatTickLabel(t, range) }); } return out; } function formatTickLabel(d, range) { if (range === '7d') { return new Intl.DateTimeFormat(undefined, { weekday: 'short' }).format(d); } return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', hour12: false, }).format(d); }