// 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;