// screen_ledger.jsx — Vulnerability ledger (filter / sort / search / group)
const DATE_FILTERS = [["all", "All time"], ["7", "7 days"], ["30", "30 days"], ["90", "Quarter"]];
const GROUP_OPTS = [["none", "Flat list"], ["supplier", "Supplier"], ["brand", "OEM"], ["severity", "Severity"], ["status", "Status"]];
const SORTS = [["sev", "Severity"], ["cvss", "CVSS"], ["epss", "EPSS"], ["date", "Newest"]];
// Selectable triage states, in display order — driven by the backend config
// (window.STATUS_SELECTABLE, mutated in place at startup), fallback baked into
// components.jsx. Same array reference, so it stays in sync after config loads.
const STATUS_ORDER = STATUS_SELECTABLE;
const PAGE_SIZE = 9;   // vulnerabilities shown per ledger page

// Trigger text for a multi-select filter dropdown: "All", the picked labels, or a count.
function multiVal(sel, order, labelOf) {
  if (sel.size === 0) return "All";
  const picked = order.filter((k) => sel.has(k)).map(labelOf);
  return picked.length <= 2 ? picked.join(", ") : `${picked.length} selected`;
}

// Build the page-number strip with leading/trailing ellipses, e.g. 1 … 5 6 7 … 42.
function pageStrip(cur, total) {
  if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
  const out = [1];
  if (cur > 3) out.push("…");
  for (let i = Math.max(2, cur - 1); i <= Math.min(total - 1, cur + 1); i++) out.push(i);
  if (cur < total - 2) out.push("…");
  out.push(total);
  return out;
}

function StatusMenu({ status, onPick, onClose, anchorRef }) {
  const ref = useRef(null);
  useEffect(() => {
    function out(e) {if (ref.current && !ref.current.contains(e.target) && !(anchorRef && anchorRef.current && anchorRef.current.contains(e.target))) onClose();}
    document.addEventListener("mousedown", out);
    return () => document.removeEventListener("mousedown", out);
  }, []);
  const opts = STATUS_SELECTABLE;
  return (
    <div className="stmenu" ref={ref}>
      <div className="stmenu-head">Set status</div>
      {opts.map((s) =>
      <button key={s} className={"stmenu-item" + (s === status ? " is-on" : "")} onClick={() => {onPick(s);onClose();}}>
          <span className="statuspill-dot" style={{ background: STATUS_META[s].dot }} />
          {STATUS_META[s].label}
          {s === status && <Icon name="check" size={14} className="stmenu-check" />}
        </button>
      )}
    </div>);

}

function Ledger({ statusOf, setStatus, onOpen, initialFilter }) {
  // The ledger page owns its own endpoint (/api/ledger via the shared catalog cache).
  const { data, loading, error, refetch } = useApi(() => window.VC.ensureCatalog());
  const vulns = data || [];
  // Anchor the date filters on the most recent publication in the data (the feed
  // is historical), so "7/30/90 days" windows are meaningful rather than empty.
  const NOW = useMemo(() => {
    let m = 0;
    vulns.forEach(v => { const t = v.published ? Date.parse(v.published) : NaN; if (!isNaN(t) && t > m) m = t; });
    return m ? new Date(m) : new Date();
  }, [vulns]);
  const [q, setQ] = useState("");
  const [sevSel, setSevSel] = useState(new Set());
  const [affSel, setAffSel] = useState(new Set());
  const [supSel, setSupSel] = useState(new Set());
  const [brandSel, setBrandSel] = useState(new Set());
  const [statSel, setStatSel] = useState(new Set());
  const [dateF, setDateF] = useState("all");
  const [group, setGroup] = useState("none");
  const [sort, setSort] = useState("sev");
  const [openMenu, setOpenMenu] = useState(null);
  const [page, setPage] = useState(1);

  // Apply a filter handed in by another screen (e.g. Browse-by-company). It is
  // either the string "critical" or { type: "supplier" | "brand", value }.
  useEffect(() => {
    if (!initialFilter) return;
    if (initialFilter === "critical") { setSevSel(new Set(["Critical"])); return; }
    if (initialFilter.type === "supplier") setSupSel(new Set([initialFilter.value]));
    else if (initialFilter.type === "brand") setBrandSel(new Set([initialFilter.value]));
  }, [initialFilter]);

  function toggle(set, val, setter) {
    const n = new Set(set);n.has(val) ? n.delete(val) : n.add(val);setter(n);
  }

  const affOf = (v) => v.kind === "automotive" ? v.ecu : v.product_name;
  const affectsOpts = useMemo(() => [...new Set(vulns.map(affOf))].filter(Boolean).sort(), [vulns]);
  const supplierOpts = useMemo(() => [...new Set(vulns.map((v) => v.supplier))].filter(Boolean).sort(), [vulns]);

  const filtered = useMemo(() => {
    let rows = vulns.filter((v) => {
      if (sevSel.size && !sevSel.has(v.severity)) return false;
      if (affSel.size && !affSel.has(affOf(v))) return false;
      if (supSel.size && !supSel.has(v.supplier)) return false;
      if (brandSel.size && !(v.brands || []).some((b) => brandSel.has(b))) return false;
      if (statSel.size && !statSel.has(statusOf(v))) return false;
      if (dateF !== "all") {
        // Empty/unparseable published dates must be EXCLUDED from a finite window —
        // not silently kept. new Date("") is Invalid Date, so guard before comparing.
        const t = v.published ? Date.parse(v.published) : NaN;
        if (isNaN(t)) return false;
        const days = (NOW - t) / 8.64e7;
        if (days < 0 || days > +dateF) return false;
      }
      if (q.trim()) {
        const s = q.toLowerCase();
        const hay = [v.id, v.description, v.ecu, v.product_name, v.supplier, v.interface, v.protocol, (v.brands || []).join(" ")].join(" ").toLowerCase();
        if (!hay.includes(s)) return false;
      }
      return true;
    });
    rows.sort((a, b) => {
      if (sort === "sev") return window.VC.severityRank(a.severity) - window.VC.severityRank(b.severity) || b.cvss - a.cvss;
      if (sort === "cvss") return b.cvss - a.cvss;
      // Unscored rows (epss_score null) sink below every real score — including
      // genuine near-zero scores, which still rank above "no data".
      if (sort === "epss") return (b.epss_score != null ? b.epss_score : -1) - (a.epss_score != null ? a.epss_score : -1);
      return new Date(b.published) - new Date(a.published);
    });
    return rows;
  }, [vulns, q, sevSel, affSel, supSel, brandSel, statSel, dateF, sort, statusOf]);

  // ---- Pagination (PAGE_SIZE vulnerabilities per page) ----------------------
  // Any change to the filter/sort inputs resets the view back to page 1 so the
  // user isn't stranded on a page that no longer exists.
  useEffect(() => { setPage(1); }, [q, sevSel, affSel, supSel, brandSel, statSel, dateF, group, sort]);
  const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  // Clamp if the result set shrinks (e.g. data finishes loading) below the page.
  useEffect(() => { if (page > totalPages) setPage(totalPages); }, [totalPages, page]);
  const pageRows = useMemo(
    () => filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
    [filtered, page]);

  // Grouping only ever organises the rows visible on the current page, so each
  // page always holds exactly PAGE_SIZE vulnerabilities (the last page fewer).
  const grouped = useMemo(() => {
    if (group === "none") return [["", pageRows]];
    const map = new Map();
    pageRows.forEach((v) => {
      let keys = group === "brand" ? v.brands || ["—"] :
      group === "supplier" ? [v.supplier || "—"] :
      group === "severity" ? [v.severity] : [STATUS_META[statusOf(v)].label];
      keys.forEach((k) => {if (!map.has(k)) map.set(k, []);map.get(k).push(v);});
    });
    let entries = [...map.entries()];
    if (group === "severity") entries.sort((a, b) => window.VC.severityRank(a[0]) - window.VC.severityRank(b[0]));else
    entries.sort((a, b) => b[1].length - a[1].length);
    return entries;
  }, [pageRows, group, statusOf]);

  const activeFilters = sevSel.size + statSel.size + affSel.size + supSel.size + brandSel.size + (dateF !== "all" ? 1 : 0) + (q.trim() ? 1 : 0);

  function clearAll() {setQ("");setSevSel(new Set());setAffSel(new Set());setSupSel(new Set());setBrandSel(new Set());setStatSel(new Set());setDateF("all");}

  if (loading) return <PageLoading label="Loading the vulnerability ledger…" />;
  if (error) return <PageError message={error} onRetry={refetch} />;

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <span className="eyebrow">Ledger</span>
          <h1 className="page-title">Vulnerability ledger</h1>
          <p className="page-desc">Every AI-classified, enriched vulnerability across your monitored programs.</p>
        </div>
        <div className="page-head-aside">
          <button className="btn btn-ghost" onClick={() => window.VCExport.report(filtered, { target: "Vulnerability ledger" + (activeFilters ? " (filtered)" : ""), statusOf })}><Icon name="download" size={15} />Report</button>
          <button className="btn btn-ghost" onClick={() => window.VCExport.vulns(filtered, "vulnerabilities")}><Icon name="download" size={15} />Export CSV</button>
        </div>
      </div>

      {/* toolbar */}
      <div className="toolbar">
        <div className="search">
          <Icon name="search" size={16} className="search-ic" />
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search CVE, ECU, supplier, interface…" spellCheck="false" />
          {q && <button className="search-clear" onClick={() => setQ("")}><Icon name="close" size={14} /></button>}
        </div>
        <div className="tool-spacer" />
        <Dropdown label="Group" value={GROUP_OPTS.find((o) => o[0] === group)[1]} icon="layers">
          {(close) => GROUP_OPTS.map(([k, l]) =>
          <button key={k} className={"dd-item" + (group === k ? " is-on" : "")} onClick={() => {setGroup(k);close();}}>{l}{group === k && <Icon name="check" size={14} />}</button>
          )}
        </Dropdown>
        <Dropdown label="Sort" value={SORTS.find((o) => o[0] === sort)[1]} icon="sortDown">
          {(close) => SORTS.map(([k, l]) =>
          <button key={k} className={"dd-item" + (sort === k ? " is-on" : "")} onClick={() => {setSort(k);close();}}>{l}{sort === k && <Icon name="check" size={14} />}</button>
          )}
        </Dropdown>
      </div>

      {/* filter chips */}
      <div className="filterbar">
        <div className="filterbar-group">
          <span className="filterbar-lab"><Icon name="filter" size={14} />Severity</span>
          <Dropdown value={multiVal(sevSel, SEV_ORDER, s => s)} small portal>
            {(close) => (
              <>
                {SEV_ORDER.map((s) =>
                  <button key={s} className={"dd-item" + (sevSel.has(s) ? " is-on" : "")} onClick={() => toggle(sevSel, s, setSevSel)}>
                    <span className="dd-item-l"><span className={"chip-dot " + sevClass(s)} />{s}</span>{sevSel.has(s) && <Icon name="check" size={14} />}
                  </button>
                )}
                {sevSel.size > 0 && <button className="dd-item dd-item-clear" onClick={() => { setSevSel(new Set()); close(); }}>Clear</button>}
              </>
            )}
          </Dropdown>
        </div>
        <span className="filterbar-div" />
        <div className="filterbar-group">
          <span className="filterbar-lab">Status</span>
          <Dropdown value={multiVal(statSel, STATUS_ORDER, s => STATUS_META[s].label)} small portal>
            {(close) => (
              <>
                {STATUS_ORDER.map((s) =>
                  <button key={s} className={"dd-item" + (statSel.has(s) ? " is-on" : "")} onClick={() => toggle(statSel, s, setStatSel)}>
                    <span className="dd-item-l"><span className="chip-dot" style={{ background: STATUS_META[s].dot }} />{STATUS_META[s].label}</span>{statSel.has(s) && <Icon name="check" size={14} />}
                  </button>
                )}
                {statSel.size > 0 && <button className="dd-item dd-item-clear" onClick={() => { setStatSel(new Set()); close(); }}>Clear</button>}
              </>
            )}
          </Dropdown>
        </div>
        <span className="filterbar-div" />
        <div className="filterbar-group">
          <span className="filterbar-lab"><Icon name="clock" size={14} />Date</span>
          <Dropdown value={DATE_FILTERS.find(o => o[0] === dateF)[1]} small portal>
            {(close) => DATE_FILTERS.map(([k, l]) => (
              <button key={k} className={"dd-item" + (dateF === k ? " is-on" : "")} onClick={() => { setDateF(k); close(); }}>{l}{dateF === k && <Icon name="check" size={14} />}</button>
            ))}
          </Dropdown>
        </div>
        {(supSel.size > 0 || brandSel.size > 0) && (
          <>
            <span className="filterbar-div" />
            <div className="filterbar-group">
              <span className="filterbar-lab"><Icon name="layers" size={14} />Company</span>
              {[...supSel].map((s) => (
                <button key={"sup-" + s} className="chip chip-status is-on" onClick={() => toggle(supSel, s, setSupSel)} title="Remove supplier filter">
                  <span className="chip-dot" />{s}<Icon name="close" size={12} />
                </button>
              ))}
              {[...brandSel].map((b) => (
                <button key={"brd-" + b} className="chip chip-status is-on" onClick={() => toggle(brandSel, b, setBrandSel)} title="Remove OEM filter">
                  <span className="chip-dot" />{b}<Icon name="close" size={12} />
                </button>
              ))}
            </div>
          </>
        )}
        {activeFilters > 0 && <button className="filterbar-clear" onClick={clearAll}>Clear all ({activeFilters})</button>}
        <span className="filterbar-count">{filtered.length} of {vulns.length}{totalPages > 1 ? ` · page ${page}/${totalPages}` : ""}</span>
      </div>

      {/* table */}
      <div className="card table-card">
        <div className="lrow lrow-head">
          <ColHead className="lc-sev" label="Severity" count={sevSel.size}>
            {(close) =>
            <>
                <div className="colfilt-menu-head"><span>Severity</span>{sevSel.size > 0 && <button className="colfilt-clear" onClick={() => setSevSel(new Set())}>Clear</button>}</div>
                {SEV_ORDER.map((s) =>
              <button key={s} className={"dd-item" + (sevSel.has(s) ? " is-on" : "")} onClick={() => toggle(sevSel, s, setSevSel)}>
                    <span className="dd-item-l"><span className={"chip-dot " + sevClass(s)} />{s}</span>{sevSel.has(s) && <Icon name="check" size={14} />}
                  </button>
              )}
              </>
            }
          </ColHead>
          <span className="lc-id">CVE</span>
          <span className="lc-desc">Vulnerability</span>
          <ColHead className="lc-ecu" label="Affects" count={affSel.size}>
            {(close) =>
            <>
                <div className="colfilt-menu-head"><span>Affects</span>{affSel.size > 0 && <button className="colfilt-clear" onClick={() => setAffSel(new Set())}>Clear</button>}</div>
                {affectsOpts.map((a) =>
              <button key={a} className={"dd-item" + (affSel.has(a) ? " is-on" : "")} onClick={() => toggle(affSel, a, setAffSel)}>
                    <span className="dd-item-l">{a}</span>{affSel.has(a) && <Icon name="check" size={14} />}
                  </button>
              )}
              </>
            }
          </ColHead>
          <ColHead className="lc-sup" label="Supplier" count={supSel.size}>
            {(close) =>
            <>
                <div className="colfilt-menu-head"><span>Supplier</span>{supSel.size > 0 && <button className="colfilt-clear" onClick={() => setSupSel(new Set())}>Clear</button>}</div>
                {supplierOpts.map((s) =>
              <button key={s} className={"dd-item" + (supSel.has(s) ? " is-on" : "")} onClick={() => toggle(supSel, s, setSupSel)}>
                    <span className="dd-item-l">{s}</span>{supSel.has(s) && <Icon name="check" size={14} />}
                  </button>
              )}
              </>
            }
          </ColHead>
          <ColHead className="lc-date" label="Published" count={dateF !== "all" ? 1 : 0} align="right">
            {(close) =>
            <>
                <div className="colfilt-menu-head"><span>Published</span></div>
                {DATE_FILTERS.map(([k, l]) =>
              <button key={k} className={"dd-item" + (dateF === k ? " is-on" : "")} onClick={() => {setDateF(k);close();}}>
                    <span className="dd-item-l">{l}</span>{dateF === k && <Icon name="check" size={14} />}
                  </button>
              )}
              </>
            }
          </ColHead>
          <ColHead className="lc-stat" label="Status" count={statSel.size} align="right">
            {(close) =>
            <>
                <div className="colfilt-menu-head"><span>Status</span>{statSel.size > 0 && <button className="colfilt-clear" onClick={() => setStatSel(new Set())}>Clear</button>}</div>
                {["new", "triaged", "in-progress", "remediated", "accepted"].map((s) =>
              <button key={s} className={"dd-item" + (statSel.has(s) ? " is-on" : "")} onClick={() => toggle(statSel, s, setStatSel)}>
                    <span className="dd-item-l"><span className="chip-dot" style={{ background: STATUS_META[s].dot }} />{STATUS_META[s].label}</span>{statSel.has(s) && <Icon name="check" size={14} />}
                  </button>
              )}
              </>
            }
          </ColHead>
        </div>
        {filtered.length === 0 &&
        <div className="empty"><Icon name="search" size={22} /><p>No vulnerabilities match these filters.</p><button className="btn btn-ghost btn-sm" onClick={clearAll}>Clear filters</button></div>
        }
        {grouped.map(([gk, rows]) =>
        <div className="lgroup" key={gk || "flat"}>
            {gk &&
          <div className="lgroup-head">
                <span className="lgroup-title">{gk}</span>
                <span className="lgroup-count">{rows.length}</span>
                <span className="lgroup-line" />
              </div>
          }
            {rows.map((v) => {
            const st = statusOf(v);
            return (
              <div className="lrow lrow-data" key={(gk || "") + v.id} onClick={() => onOpen(v)}>
                  <span className="lc-sev"><SeverityTag s={v.severity} cvss={v.cvss} size="sm" /></span>
                  <span className="lc-id mono">
                    {v.id}
                    {v.epss_score != null && <em className="lc-epss">EPSS {v.epss_score}</em>}
                  </span>
                  <span className="lc-desc">
                    <span className="lc-desc-tx">{v.description}</span>
                  </span>
                  <span className="lc-ecu">{v.kind === "automotive" ? v.ecu : v.product_name}<em className="lc-ecu-if">{v.interface || v.protocol}</em></span>
                  <span className="lc-sup">{v.supplier}</span>
                  <span className="lc-date mono">{v.published ? v.published.slice(5) : "—"}</span>
                  <span className="lc-stat" onClick={(e) => e.stopPropagation()} style={{ position: "relative" }}>
                    <StatusPill status={st} interactive onClick={() => setOpenMenu(openMenu === v.id ? null : v.id)} />
                    {openMenu === v.id &&
                  <StatusMenu status={st} onPick={(s) => setStatus(v.id, s)} onClose={() => setOpenMenu(null)} />
                  }
                  </span>
                </div>);

          })}
          </div>
        )}
      </div>

      {/* pager — only shown once the result set spills past a single page */}
      {filtered.length > PAGE_SIZE &&
        <div className="pager">
          <span className="pager-info">
            Showing {(page - 1) * PAGE_SIZE + 1}–{Math.min(page * PAGE_SIZE, filtered.length)} of {filtered.length}
          </span>
          <div className="pager-ctrls">
            <button className="pager-btn" disabled={page <= 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>
              <Icon name="arrowL" size={14} />Prev
            </button>
            {pageStrip(page, totalPages).map((p, i) =>
              p === "…"
                ? <span key={"e" + i} className="pager-ellip">…</span>
                : <button key={p} className={"pager-num" + (p === page ? " is-on" : "")} onClick={() => setPage(p)}>{p}</button>
            )}
            <button className="pager-btn" disabled={page >= totalPages} onClick={() => setPage((p) => Math.min(totalPages, p + 1))}>
              Next<Icon name="arrowR" size={14} />
            </button>
          </div>
        </div>
      }
    </div>);

}

/* generic dropdown. Pass `portal` when the trigger sits inside an overflow:auto/hidden
   container (e.g. the .filterbar): the menu then renders to document.body with fixed
   positioning so it isn't clipped by the scroll container. */
function Dropdown({ label, value, icon, children, small, portal }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const ref = useRef(null);       // wraps the trigger button
  const menuRef = useRef(null);   // the (possibly portaled) menu
  useEffect(() => {
    function out(e) {
      const inBtn = ref.current && ref.current.contains(e.target);
      const inMenu = menuRef.current && menuRef.current.contains(e.target);
      if (!inBtn && !inMenu) setOpen(false);
    }
    document.addEventListener("mousedown", out);
    return () => document.removeEventListener("mousedown", out);
  }, []);
  // Anchor the portaled menu under the trigger, clamped to the viewport.
  useEffect(() => {
    if (!open || !portal || !ref.current) return;
    const r = ref.current.getBoundingClientRect();
    const menuWidth = 190;
    const left = Math.max(8, Math.min(r.right - menuWidth, window.innerWidth - menuWidth - 8));
    setPos({ top: r.bottom + 6, left });
  }, [open, portal]);

  const menuBody = open ? children(() => setOpen(false)) : null;
  return (
    <div className={"dd" + (small ? " dd-small" : "")} ref={ref}>
      <button className={"dd-btn" + (open ? " is-open" : "")} onClick={() => setOpen((o) => !o)}>
        {icon && <Icon name={icon} size={14} />}
        {label && <span className="dd-lab">{label}:</span>}<span className="dd-val">{value}</span>
        <Icon name="chevD" size={13} className="dd-caret" />
      </button>
      {open && (portal
        ? ReactDOM.createPortal(
            <div ref={menuRef} className="dd-menu dd-menu-portal"
              style={{ position: "fixed", right: "auto", top: pos ? pos.top : -9999, left: pos ? pos.left : -9999, zIndex: 999 }}>
              {menuBody}
            </div>,
            // Portal into the themed wrapper (not document.body) so the menu inherits
            // the --bg-panel/--text/etc. CSS variables defined on `.app`. position:fixed
            // still lets it escape the .filterbar overflow clip.
            (ref.current && ref.current.closest(".app")) || document.body)
        : <div ref={menuRef} className="dd-menu">{menuBody}</div>)}
    </div>);

}

/* column-header filter — popover uses portal to escape overflow */
function ColHead({ className, label, count, children, align }) {
  const [open, setOpen] = useState(false);
  const btnRef = useRef(null), cellRef = useRef(null);
  
  useEffect(() => {
    if (!open) return;
    function out(e) {
      if (cellRef.current && !cellRef.current.contains(e.target)) setOpen(false);
    }
    // Close when the page scrolls (the menu is fixed-positioned and would detach
    // from its column), but NOT when the user scrolls the menu's own list.
    function onScroll(e) {
      if (e.target instanceof Element && e.target.closest(".colfilt-menu")) return;
      setOpen(false);
    }
    document.addEventListener("mousedown", out);
    window.addEventListener("scroll", onScroll, true);
    return () => {
      document.removeEventListener("mousedown", out);
      window.removeEventListener("scroll", onScroll, true);
    };
  }, [open]);

  function toggleOpen() {
    setOpen((o) => !o);
  }

  return (
    <span ref={cellRef} className={"lc-head-cell " + (className || "") + (count ? " is-filtered" : "")}>
      <button
        ref={btnRef}
        type="button"
        className={"colfilt" + (count ? " is-active" : "") + (open ? " is-open" : "")}
        onClick={toggleOpen}
        aria-label={"Filter " + label}
      >
        <Icon name="filter" size={11} />
        {count > 0 && <span className="colfilt-badge">{count}</span>}
      </button>
      <span className="lc-head-lab">{label}</span>
      {open &&
        ReactDOM.createPortal(
          <ColHeadMenu
            btnRef={btnRef}
            cellRef={cellRef}
            align={align}
            onClose={() => setOpen(false)}
          >
            {children(() => setOpen(false))}
          </ColHeadMenu>,
          // Portal into the themed .app wrapper (not document.body) so the menu
          // picks up data-theme and the CSS variables defined on it.
          (cellRef.current && cellRef.current.closest(".app")) || document.body
        )
      }
    </span>
  );
}

function ColHeadMenu({ btnRef, cellRef, align, onClose, children }) {
  const [pos, setPos] = useState(null);
  const menuRef = useRef(null);

  useEffect(() => {
    if (cellRef.current && btnRef.current) {
      const r = cellRef.current.getBoundingClientRect();
      const menuWidth = 190;
      let left = r.left;
      if (align === "right") {
        left = Math.max(8, r.right - menuWidth);
      } else {
        left = Math.min(r.left, window.innerWidth - menuWidth - 8);
      }
      // Cap the menu to the space left below the header so long lists
      // (e.g. the Affects filter) scroll instead of running off-screen.
      const maxH = Math.max(160, window.innerHeight - r.bottom - 24);
      setPos({ top: r.bottom + 8, left, maxH });
    }
  }, [cellRef, btnRef, align]);

  useEffect(() => {
    function out(e) {
      if (menuRef.current && !menuRef.current.contains(e.target) && cellRef.current && !cellRef.current.contains(e.target)) {
        onClose();
      }
    }
    document.addEventListener("mousedown", out);
    return () => document.removeEventListener("mousedown", out);
  }, [onClose, cellRef]);

  if (!pos) return null;

  return (
    <div
      ref={menuRef}
      className="colfilt-menu"
      style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 999, maxHeight: pos.maxH, overflowY: "auto" }}
    >
      {children}
    </div>
  );
}

window.Ledger = Ledger;
window.ColHead = ColHead;
window.ColHeadMenu = ColHeadMenu;
window.Dropdown = Dropdown;
window.StatusMenu = StatusMenu;

window.ColHead = ColHead;