{ setQ(e.target.value); setHi(0); setSearchOpen(true); }}
onFocus={() => setSearchOpen(true)}
onBlur={() => setTimeout(() => setSearchOpen(false), 150)}
onKeyDown={onSearchKey} />
⌘K
{searchOpen && s && (
{hits.length === 0 && (
{lang === 'no' ? 'Ingen automater matcher' : 'No machines match'}
)}
{hits.map((m, i) => (
{ e.preventDefault(); go(m); }}
onMouseEnter={() => setHi(i)}
style={{ padding: '7px 10px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8,
background: i === hi ? 'var(--accent-soft)' : 'transparent' }}>
{m.location}
{m.id}
))}
)}
{bellOpen && (
<>
setBellOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 39 }} />
{bellAlarms.length === 0 && (
{lang === 'no' ? 'Ingen aktive varsler' : 'No active alerts'}
)}
{bellAlarms.map(a => (
{ setBellOpen(false); onNavigate && onNavigate({ page: 'machines', machineId: a.machineId }); }}
style={{ padding: '8px 10px', borderBottom: '1px solid var(--border)', cursor: 'pointer', fontSize: 12 }}>
{a.machineName}
{a.mins < 60 ? `${a.mins} min` : a.mins < 1440 ? `${Math.floor(a.mins / 60)} t` : `${Math.floor(a.mins / 1440)} d`}
{lang === 'no' ? a.msg_no : a.msg_en}
))}
>
)}
);
};
/* --- Tiny widgets --- */
const Sparkline = ({ data, width = 80, height = 28, color = 'var(--accent)', filled = true }) => {
if (!data || !data.length) return null;
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const step = width / (data.length - 1);
const pts = data.map((v, i) => [i * step, height - ((v - min) / range) * (height - 4) - 2]);
const d = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
const dFill = d + ` L${width},${height} L0,${height} Z`;
return (
);
};
const Donut = ({ size = 130, stroke = 18, segments }) => {
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const total = segments.reduce((s, x) => s + x.value, 0) || 1;
let acc = 0;
// viewBox: CSS kan skalere donuten (mobil-laget setter .donut 110px) uten
// at tegningen klippes — før ble den avskåret i ramma på smal skjerm.
return (
);
};
const StatusDot = ({ status }) =>
;
const Pill = ({ tone = 'empty', children, dot }) => (
{dot && }
{children}
);
Object.assign(window, { Sidebar, Topbar, Sparkline, Donut, StatusDot, Pill });