);
};
// DatePopover — custom calendar so we can enforce strict "commit only when a
// real date is clicked." Native fires `change` on month/
// year nudges and wheel-spins, which the rep complained about. Here, month/
// year navigation only mutates the popover's internal `view` state; the only
// path that calls onChange is the day-cell click.
const DatePopover = ({ value, onChange, ariaLabel }) => {
const [open, setOpen] = React.useState(false);
const [view, setView] = React.useState(() => {
const d = value ? new Date(value) : new Date();
return { y: d.getFullYear(), m: d.getMonth() };
});
const ref = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const onDocDown = (e) => {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
};
const onEsc = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDocDown);
document.addEventListener('keydown', onEsc);
return () => {
document.removeEventListener('mousedown', onDocDown);
document.removeEventListener('keydown', onEsc);
};
}, [open]);
const openPopover = () => {
// Reset the visible month to whatever the committed value points at, so
// re-opening after browsing months doesn't strand the user on a stale view.
const d = value ? new Date(value) : new Date();
setView({ y: d.getFullYear(), m: d.getMonth() });
setOpen(true);
};
const pad = (n) => String(n).padStart(2, '0');
const fmtIso = (y, m, d) => `${y}-${pad(m + 1)}-${pad(d)}`;
const display = value
? new Date(value).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
: 'Pick date';
const firstDow = new Date(view.y, view.m, 1).getDay();
const daysInMonth = new Date(view.y, view.m + 1, 0).getDate();
const cells = [];
for (let i = 0; i < firstDow; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
while (cells.length % 7 !== 0) cells.push(null);
const prev = () => setView(v => v.m === 0 ? { y: v.y - 1, m: 11 } : { y: v.y, m: v.m - 1 });
const next = () => setView(v => v.m === 11 ? { y: v.y + 1, m: 0 } : { y: v.y, m: v.m + 1 });
// The ONLY path that commits upward.
const selectDay = (d) => {
onChange(fmtIso(view.y, view.m, d));
setOpen(false);
};
const baseYear = new Date().getFullYear();
const yearOptions = [];
for (let y = baseYear - 10; y <= baseYear + 5; y++) yearOptions.push(y);
if (!yearOptions.includes(view.y)) {
yearOptions.push(view.y);
yearOptions.sort((a, b) => a - b);
}
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const dowNames = ['S','M','T','W','T','F','S'];
const selectedDay = value && (() => {
const d = new Date(value);
return (d.getFullYear() === view.y && d.getMonth() === view.m) ? d.getDate() : null;
})();
return (
{open && (
{dowNames.map((n, i) => (
{n}
))}
{cells.map((d, i) => {
if (d == null) return ;
const isSelected = d === selectedDay;
return (
);
})}
)}
);
};
// Date range — two custom popovers, one per bound. The popover commits only
// when a day cell is clicked; outside-click / Esc / month-year nav never fire.
const DateRange = ({ start, end, onChange }) => {
return (
onChange({ start: v, end })}
/>
→ onChange({ start, end: v })}
/>
);
};
// Confidence-band controls. Schema-driven: receives the list of upper/lower
// levels actually present in the `results` table (discovered server-side via
// INFORMATION_SCHEMA) and renders one checkbox per level on each side.
//
// `selection` is an array of keys like ["upper_80", "lower_60"]. The chart
// pairs same-level upper+lower into a single band with one color; otherwise
// each selection gets its own color (see chart.jsx).
const ConfidenceControls = ({ bandLevels, selection, onChange, disabled }) => {
const upperLevels = (bandLevels && bandLevels.upper) || [];
const lowerLevels = (bandLevels && bandLevels.lower) || [];
const empty = upperLevels.length === 0 && lowerLevels.length === 0;
const sel = new Set(selection || []);
const toggle = (key, on) => {
const next = new Set(sel);
if (on) next.add(key); else next.delete(key);
onChange([...next]);
};
return (