// Toolbar — top strip with date nav, search, score filter, label filter, // and the activity sparkline. const { useState, useEffect, useRef, useMemo } = React; function Toolbar(props) { const { endDate, onShiftDays, onJumpToDate, scoreThreshold, onScoreThreshold, enabledLabels, onToggleLabel, rows, totalMeows, visibleMeows, zoom, onResetZoom, player, onClosePlayer, } = props; const startDate = new Date(endDate); startDate.setDate(startDate.getDate() - 59); const [dateInput, setDateInput] = useState(MeowUtil.fmtDateInput(endDate)); useEffect(() => { setDateInput(MeowUtil.fmtDateInput(endDate)); }, [endDate]); function handleJump(e) { e.preventDefault(); const d = MeowUtil.parseDateInput(dateInput); if (d) onJumpToDate(d); } return (
Meow Detector
60-day audio timeline
{MeowUtil.fmtDayLabel(startDate)} {MeowUtil.fmtDayLabel(endDate)}
setDateInput(e.target.value)} onBlur={handleJump} aria-label="Jump to date" />
min score onScoreThreshold(parseFloat(e.target.value))} aria-label="Minimum score" /> {scoreThreshold.toFixed(2)}
zoom {zoom.toFixed(1)}× {zoom > 1.01 ? ( ) : null}
{visibleMeows.toLocaleString()} meows · 60 days
); } function Sparkline({ rows }) { // rows are most-recent-first; reverse for left-to-right oldest-to-newest const ordered = useMemo(() => rows.slice().reverse(), [rows]); const data = useMemo(() => ordered.map((r) => r.meows.length), [ordered]); const W = 320, H = 36; const max = Math.max(1, ...data); const bw = W / data.length; const [hover, setHover] = useState(null); // { i, x, y } return (
activity setHover(null)} > {data.map((v, i) => { const h = (v / max) * (H - 4); const isHover = hover && hover.i === i; return ( setHover({ i, x: e.clientX, y: e.clientY })} onMouseMove={(e) => setHover({ i, x: e.clientX, y: e.clientY })} /> ); })} {rows.length ? MeowUtil.fmtDayLabel(rows[rows.length - 1].startDate) : ''} · {rows.length ? MeowUtil.fmtDayLabel(rows[0].startDate) : ''} {hover && ordered[hover.i] ? ( ) : null}
); } function SparklineTooltip({ date, count, x, y }) { const W = 180; const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; const cx = x / z; const cy = y / z; const vw = window.innerWidth / z; const ref = useRef(null); const [h, setH] = useState(0); useEffect(() => { if (ref.current) setH(ref.current.offsetHeight); }, [date, count]); let left = cx - W / 2; if (left < 8) left = 8; if (left + W > vw - 8) left = vw - 8 - W; const top = cy - h - 12; return (
date {MeowUtil.fmtDayLabel(date)}
meows {count.toLocaleString()}
); } window.Toolbar = Toolbar; // Permanent mini-player rendered in the toolbar. function ToolbarPlayer({ player, onClose }) { const [progress, setProgress] = useState(0); const rafRef = useRef(0); useEffect(() => { if (!player) { setProgress(0); return; } function tick() { const elapsed = (performance.now() - player.startedAt) / 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]); if (!player) { return (
click any meow to play
); } const meow = player.meow; const startDate = MeowUtil.fracToDate(meow.start, player.row.startDate); const color = MeowUtil.scoreColor(meow.score); return (
{meow.label} {MeowUtil.fmtDayLabel(startDate)} · {MeowUtil.fmtClock(startDate)} {meow.durSec.toFixed(1)}s {meow.score.toFixed(2)}
); }