// Timeline row + tooltip + inline mini-player. const { useState: useStateT, useEffect: useEffectT, useRef: useRefT, useMemo: useMemoT, useCallback: useCallbackT } = React; // ----- Tooltip (single, portal-style fixed positioning) ----- function Tooltip({ data }) { if (!data) return null; const { meow, row, x, y } = data; const startDate = MeowUtil.fracToDate(meow.start, row.startDate); const endDate = MeowUtil.fracToDate(meow.end, row.startDate); const W = 232; // html has CSS `zoom`, which scales fixed-positioned descendants. clientX/Y // come back in unscaled viewport pixels, so divide to match the local // coordinate space the browser uses when laying out left/top. const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; const cx = x / z; const cy = y / z; const vw = window.innerWidth / z; const vh = window.innerHeight / z; let left = cx + 14; let top = cy + 14; if (left + W > vw - 8) left = cx - W - 14; if (top + 160 > vh - 8) top = cy - 160; return (
{meow.label} {meow.score.toFixed(2)}
start {MeowUtil.fmtClock(startDate)}
end {MeowUtil.fmtClock(endDate)}
duration {meow.durSec.toFixed(1)}s
date {MeowUtil.fmtDayLabel(startDate)}
click to play
); } // ----- Inline mini-player ----- function MiniPlayer({ player, onClose, rowRect }) { const [progress, setProgress] = useStateT(0); const startedRef = useRefT(0); const rafRef = useRefT(0); useEffectT(() => { startedRef.current = performance.now(); function tick() { const elapsed = (performance.now() - startedRef.current) / 1000; const p = Math.min(1, elapsed / player.dur); setProgress(p); if (p < 1) rafRef.current = requestAnimationFrame(tick); } rafRef.current = requestAnimationFrame(tick); return () => cancelAnimationFrame(rafRef.current); }, [player.dur]); const meow = player.meow; return (
{meow.label} {MeowUtil.fmtClock(MeowUtil.fracToDate(meow.start, player.row.startDate))} {meow.score.toFixed(2)}
); } // ----- Single row ----- function TimelineRow(props) { const { row, zoom, pan, scoreThreshold, enabledLabels, isToday, onBlockHover, onBlockLeave, onBlockClick, activeMeowId, } = props; const filtered = useMemoT(() => { return row.meows.filter((m) => m.score >= scoreThreshold && enabledLabels.has(m.label)); }, [row.meows, scoreThreshold, enabledLabels]); // Min visible width so 2-second meows are still clickable. const MIN_PX = 3; return (
{MeowUtil.fmtDayRangeLabel(row.startDate).split(' ')[0]} {MeowUtil.fmtDayRangeLabel(row.startDate).split(' ').slice(1).join(' ')}
{filtered.length}
{/* No-recording overlay — darkens any portion outside recording periods */}
{(() => { const periods = (row.periods || []).slice().sort((a, b) => a.start - b.start); const gaps = []; let cursor = 0; for (const p of periods) { if (p.start > cursor) gaps.push({ start: cursor, end: p.start }); cursor = Math.max(cursor, p.end); } if (cursor < 1) gaps.push({ start: cursor, end: 1 }); return gaps.map((g, i) => (
)); })()}
{/* Hour ticks */}
{MeowUtil.HOUR_TICKS.map((t, i) => (
))}
{/* Meow blocks */}
{filtered.map((m) => { const leftPct = m.start * 100; const widthPct = Math.max(0, (m.end - m.start) * 100); // We boost min width via min-width css to keep blocks clickable const isActive = activeMeowId === m.id; return (
onBlockHover(m, row, e)} onMouseMove={(e) => onBlockHover(m, row, e)} onMouseLeave={() => onBlockLeave()} onClick={(e) => { e.stopPropagation(); onBlockClick(m, row, e); }} /> ); })}
); } window.TimelineRow = TimelineRow; window.Tooltip = Tooltip; window.MiniPlayer = MiniPlayer;