// 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)}
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
{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 (
);
}
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)}
);
}