thermograph/frontend/static/filtersheet.js
Emi Griffith d6df04eab2 Subtree-merge thermograph-frontend (origin/main) into frontend/
git-subtree-dir: frontend
git-subtree-mainline: a4be7066e5
git-subtree-split: 3a98146da4
2026-07-22 22:01:11 -07:00

72 lines
3.2 KiB
JavaScript

// Mobile filter access: a small floating "Filters" pill (fixed, reachable from
// anywhere on the page) that slides the page's existing filter panel up as a bottom
// sheet. Desktop is untouched — the pill, scrim and grip are display:none above the
// phone breakpoint, so the panel stays inline exactly as before (all styling lives in
// the @media(max-width:640px) block in style.css). Shared by compare + calendar.
//
// initFilterSheet({ panel, label })
// panel — the filter panel element (becomes the sheet on mobile)
// label — text on the pill and the sheet header (e.g. "Filters")
// returns { setActive(bool), close() }
const FUNNEL = `<svg class="ff-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18M6 12h12M10 19h4"/></svg>`;
export function initFilterSheet({ panel, label = "Filters" }) {
if (!panel) return { setActive() {}, close() {} };
// The pill lives in <body> so it floats over the whole page regardless of scroll.
const fab = document.createElement("button");
fab.type = "button";
fab.className = "filter-fab";
fab.setAttribute("aria-expanded", "false");
fab.innerHTML = `${FUNNEL}<span class="ff-label">${label}</span><span class="ff-dot" hidden aria-hidden="true"></span>`;
const scrim = document.createElement("div");
scrim.className = "filter-scrim";
scrim.hidden = true;
// A grab handle + title + close, prepended to the panel; only shown in sheet mode.
const grip = document.createElement("div");
grip.className = "filter-sheet-grip";
grip.innerHTML = `<span class="fsg-bar" aria-hidden="true"></span>` +
`<span class="fsg-title">${label}</span>` +
`<button type="button" class="fsg-close" aria-label="Close filters">✕</button>`;
panel.prepend(grip);
document.body.append(scrim, fab);
document.body.classList.add("has-filter-sheet"); // scopes the extra bottom padding
const isOpen = () => panel.classList.contains("open");
function open() {
panel.classList.add("open");
scrim.hidden = false;
fab.setAttribute("aria-expanded", "true");
}
function close() {
panel.classList.remove("open");
scrim.hidden = true;
fab.setAttribute("aria-expanded", "false");
}
fab.addEventListener("click", () => (isOpen() ? close() : open()));
scrim.addEventListener("click", close);
grip.querySelector(".fsg-close").addEventListener("click", close);
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && isOpen()) close(); });
// The pill only makes sense once the page has data to filter — the panel starts
// hidden and the page reveals it. Mirror that onto the pill, and drop the sheet if
// the panel goes away.
const reflect = () => {
fab.classList.toggle("ready", !panel.hidden);
if (panel.hidden && isOpen()) close();
};
new MutationObserver(reflect).observe(panel, { attributes: true, attributeFilter: ["hidden"] });
reflect();
return {
// Show a dot on the pill when a filter subset is active (so the applied filter is
// visible even with the sheet closed).
setActive(active) { fab.querySelector(".ff-dot").hidden = !active; },
close,
};
}