// Shared app shell: primary sidebar + subrail + top bar. // Used by dashboard and map views. const Sidebar = ({ activeModule = 'tree' }) => ( ); const Subrail = ({ view, onView }) => { const items = [ { id: 'dashboard', Icon: IDashboard, label: 'Dashboard', href: '../dashboard/dashboard.html' }, { id: 'list', Icon: IList, label: 'List', href: '../assets/assets.html' }, { id: 'map', Icon: IMap, label: 'Map', href: '../map/map.html' }, { id: 'docs', Icon: IDocs, label: 'Documents', href: '#' }, ]; // Track filter-panel open state so the funnel icon gets the active style // (matching the map/dashboard icons) when the panel is visible. const [filtersOpen, setFiltersOpen] = React.useState(false); React.useEffect(() => { const onState = (e) => setFiltersOpen(!!(e.detail && e.detail.open)); window.addEventListener('spotlite:filters-state', onState); return () => window.removeEventListener('spotlite:filters-state', onState); }, []); return (
{items.map(it => ( { if (onView && it.id !== 'map' && it.id !== 'dashboard' && it.id !== 'list') { e.preventDefault(); onView(it.id); } }}> ))}
window.dispatchEvent(new CustomEvent('spotlite:toggle-filters'))} style={{ cursor: 'pointer' }}>
); }; const TopBar = ({ crumb, title, children }) => (
{crumb}

{title}

{children}
); // ── Span info popover ──────────────────────────────────────────── // Shared across all map views. Pops up on conductor click; shows asset // designation, tier pill, alarm/alert counts, length, and "View in asset // list" link. Click X or outside to close. // // Props: // data: { n, tier, alarms, alerts, lengthM } | null // x, y: click position relative to .map-stage (px) // onClose: () => void const SpanInfoPopover = ({ data, x, y, onClose }) => { const ref = React.useRef(null); React.useEffect(() => { if (!data) return; const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); }; // Defer one frame so the click that opened us doesn't immediately close it const t = setTimeout(() => document.addEventListener('mousedown', onDown), 0); return () => { clearTimeout(t); document.removeEventListener('mousedown', onDown); }; }, [data, onClose]); if (!data) return null; const { n, seq, tier, alarms = 0, alerts = 0, lengthM, voltage = '150 kV', infra = 'HV overhead line' } = data; const tierLabel = tier === 'alarm' ? 'Alarm' : tier === 'alert' ? 'Alert' : 'Stable'; const designation = data.designation || `Span ${seq != null ? seq : n}`; const popoverW = 280; // Place to the right of the click; flip left if it would overflow const left = x + 14; const top = Math.max(8, y - 24); return (
Span {seq != null ? seq : n} — T{n}→T{n+1}
{designation}
{tierLabel}
Infrastructure{infra}
Voltage{voltage}
{lengthM != null &&
Span length{Math.round(lengthM)} m
}
Alarm trees 0 ? 'alarm' : '')}>{alarms}
Alert trees 0 ? 'alert' : '')}>{alerts}
View in asset list →
); }; Object.assign(window, { Sidebar, Subrail, TopBar, SpanInfoPopover });