Compare basis inline; synced inline + sheet metric selector (both pages) (#82)
- Compare: move the "Judge each day by its" basis toggle out of the filter sheet into the inline comfort card above the results (a scoring control, like the comfort slider). Handler is id-based, so no JS change for the move. - Metric selector is duplicated, not relocated: revert the sheetOnly relocation in filtersheet.js; keep the inline selector visible on all viewports and add a synced copy inside each page's filter sheet (mobile-only via CSS). The sheet copy clones the inline buttons; a single setter (setDistMetric / setColorMetric) drives the shared state and reflects the active metric on both, so picking any of the 8 metrics in either selector syncs the other and re-renders. Applies to compare's "Distribution by" and calendar's "Color the calendar by". Frontend-only; no backend or payload change.
This commit is contained in:
parent
ee9c9b3045
commit
de02ebcf19
6 changed files with 69 additions and 51 deletions
|
|
@ -62,6 +62,12 @@
|
|||
<button type="button" id="range-refresh" class="range-refresh" hidden>Refresh dates ↻</button>
|
||||
</div>
|
||||
<div id="cal-seasonyear" class="cal-seasonyear"></div>
|
||||
<!-- A synced copy of the metric selector for the phone sheet; buttons are cloned
|
||||
from the inline #metric-toggle in JS. Hidden on desktop. -->
|
||||
<div class="cal-metric-sheet">
|
||||
<span class="cal-filter-label">Color the calendar by</span>
|
||||
<div class="metric-toggle" id="metric-toggle-sheet" role="group" aria-label="Color the calendar by (in filters)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metric selector sits below the time filters, right above the grid it colors. -->
|
||||
|
|
|
|||
|
|
@ -144,19 +144,27 @@ initFindButton(findBtn, "Find a location",
|
|||
() => selected, (lat, lon) => selectLocation(lat, lon));
|
||||
|
||||
// ---- metric toggle ----
|
||||
// Reflect the restored metric on the toggle (the HTML defaults to High).
|
||||
document.querySelectorAll("#metric-toggle button").forEach((b) =>
|
||||
b.classList.toggle("active", b.dataset.metric === metric));
|
||||
|
||||
document.getElementById("metric-toggle").addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-metric]");
|
||||
if (!btn) return;
|
||||
metric = btn.dataset.metric;
|
||||
// An inline selector (above the grid) plus a synced copy in the filter sheet; the sheet
|
||||
// copy mirrors the inline 8 buttons (cloned). Either drives the shared `metric`.
|
||||
document.getElementById("metric-toggle-sheet").innerHTML =
|
||||
document.getElementById("metric-toggle").innerHTML;
|
||||
const syncColorButtons = () =>
|
||||
document.querySelectorAll("#metric-toggle button, #metric-toggle-sheet button")
|
||||
.forEach((b) => b.classList.toggle("active", b.dataset.metric === metric));
|
||||
function setColorMetric(m) {
|
||||
metric = m;
|
||||
try { localStorage.setItem("thermograph:calMetric", metric); } catch (err) {}
|
||||
document.querySelectorAll("#metric-toggle button").forEach((b) => b.classList.toggle("active", b === btn));
|
||||
syncColorButtons();
|
||||
renderCalendar();
|
||||
renderKey();
|
||||
});
|
||||
}
|
||||
syncColorButtons(); // reflect the restored metric on both (the HTML defaults to High)
|
||||
for (const id of ["metric-toggle", "metric-toggle-sheet"]) {
|
||||
document.getElementById(id).addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-metric]");
|
||||
if (btn) setColorMetric(btn.dataset.metric);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- category totals: share vs. count toggle ----
|
||||
// The strip normally prints each category's share; ticking "Show count" swaps every
|
||||
|
|
@ -290,9 +298,7 @@ filtersEl.addEventListener("change", (e) => {
|
|||
// Wire the per-season "Months" expand toggles (delegated on the stable container),
|
||||
// and the mobile floating pill that slides the whole filter panel up as a sheet.
|
||||
initSeasonExpand(seasonYearEl);
|
||||
// On phones the "Color the calendar by" metric selector rides the filter sheet too.
|
||||
const calSheet = initFilterSheet({ panel: filtersEl, label: "Filters",
|
||||
sheetOnly: [document.getElementById("metric-block")] });
|
||||
const calSheet = initFilterSheet({ panel: filtersEl, label: "Filters" });
|
||||
calSheet.setActive(checkedMonths.size < 12);
|
||||
|
||||
// Close any open filter dropdown when tapping outside it (native <details> stays
|
||||
|
|
|
|||
|
|
@ -81,10 +81,9 @@
|
|||
<input id="cmp-thi" type="range" min="30" max="100" step="1" class="thumb-tol" aria-label="Tolerance range upper bound" />
|
||||
</div>
|
||||
<span class="hint">Inner handles set your comfort range; outer handles set the tolerance range. Days in tolerance still count toward the score, scaled by how far past comfort they fall.</span>
|
||||
</div>
|
||||
|
||||
<div id="cmp-params" class="cmp-params" hidden>
|
||||
<div class="cmp-param cmp-basis-block">
|
||||
<!-- Which temperature to judge each day by — a scoring control, so it sits with
|
||||
the comfort slider above the results, not in the filter sheet. -->
|
||||
<div class="cmp-basis-block">
|
||||
<span class="cal-filter-label">Judge each day by its</span>
|
||||
<div class="metric-toggle" id="cmp-basis" role="group" aria-label="Temperature to compare">
|
||||
<button type="button" data-basis="tmax" class="active">High</button>
|
||||
|
|
@ -93,7 +92,9 @@
|
|||
<button type="button" data-basis="feels">Feels</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cmp-params" class="cmp-params" hidden>
|
||||
<div class="cmp-param cmp-season" id="cmp-season">
|
||||
<span class="cal-filter-label">Time of year</span>
|
||||
<div class="cmp-season-dd" id="cmp-season-dd"></div>
|
||||
|
|
@ -110,6 +111,13 @@
|
|||
<span class="hint">Editing the range arms Load — tap it to reload.</span>
|
||||
<button type="button" id="cmp-refresh-sheet" class="cmp-refresh-btn" hidden>Load / Refresh ↻</button>
|
||||
</div>
|
||||
|
||||
<!-- A synced copy of the "Distribution by" selector (below), for the phone
|
||||
sheet; buttons are cloned from the inline one in JS. Hidden on desktop. -->
|
||||
<div class="cmp-param cmp-dist-metric-block">
|
||||
<span class="cal-filter-label">Distribution by</span>
|
||||
<div class="metric-toggle" id="cmp-dist-metric-sheet" role="group" aria-label="Distribution metric (in filters)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cmp-head" class="cal-head" hidden></div>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,9 @@ const startInput = document.getElementById("cmp-start");
|
|||
const endInput = document.getElementById("cmp-end");
|
||||
const distSection = document.getElementById("cmp-dist");
|
||||
const distToggle = document.getElementById("cmp-dist-metric");
|
||||
const distToggleSheet = document.getElementById("cmp-dist-metric-sheet");
|
||||
// The sheet copy mirrors the inline selector's 8 buttons (cloned) and stays in sync.
|
||||
distToggleSheet.innerHTML = distToggle.innerHTML;
|
||||
const distBody = document.getElementById("cmp-dist-body");
|
||||
|
||||
|
||||
|
|
@ -602,15 +605,23 @@ thiInput.addEventListener("input", () => { thi = Math.max(+thiInput.value, hi);
|
|||
// (its per-category averages + ranges are in the active unit).
|
||||
onUnitChange(() => { syncSlicer(); renderResults(); renderDist(); });
|
||||
|
||||
// Distribution: its own metric selector + share/count toggle, re-rendering in place.
|
||||
distToggle.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-metric]");
|
||||
if (!btn) return;
|
||||
distMetric = btn.dataset.metric;
|
||||
// Distribution metric: an inline selector (by the chart) plus a synced copy in the
|
||||
// filter sheet. Either one drives the shared distMetric; both reflect the active state.
|
||||
const syncDistButtons = () =>
|
||||
document.querySelectorAll("#cmp-dist-metric button, #cmp-dist-metric-sheet button")
|
||||
.forEach((b) => b.classList.toggle("active", b.dataset.metric === distMetric));
|
||||
function setDistMetric(m) {
|
||||
distMetric = m;
|
||||
lsSet(CMP.distMetric, distMetric);
|
||||
distToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b === btn));
|
||||
syncDistButtons();
|
||||
renderDist();
|
||||
});
|
||||
}
|
||||
for (const t of [distToggle, distToggleSheet]) {
|
||||
t.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-metric]");
|
||||
if (btn) setDistMetric(btn.dataset.metric);
|
||||
});
|
||||
}
|
||||
distSection.addEventListener("change", (e) => {
|
||||
if (!e.target.closest("#cmp-dist-count")) return;
|
||||
distCount = e.target.checked;
|
||||
|
|
@ -678,13 +689,11 @@ clickOpensPicker(startInput, endInput); // whole-field tap opens the month pic
|
|||
|
||||
syncSlicer();
|
||||
basisToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.basis === basis));
|
||||
distToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.metric === distMetric));
|
||||
syncDistButtons(); // active state on both the inline + sheet distribution selectors
|
||||
document.getElementById("cmp-dist-count").checked = distCount;
|
||||
startInput.value = range.start; endInput.value = range.end;
|
||||
buildSeasonFilter();
|
||||
// On phones the "Distribution by" metric selector rides the filter sheet too.
|
||||
fabSheet = initFilterSheet({ panel: params, label: "Filters",
|
||||
sheetOnly: [document.querySelector(".cmp-dist-controls")] });
|
||||
fabSheet = initFilterSheet({ panel: params, label: "Filters" });
|
||||
fabSheet.setActive(monthsActive());
|
||||
|
||||
// A shared link's locations win; else the last-used set; else seed from the spot
|
||||
|
|
|
|||
|
|
@ -4,16 +4,14 @@
|
|||
// 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, sheetOnly })
|
||||
// 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")
|
||||
// sheetOnly — elements that live inline on desktop but move INTO the sheet on
|
||||
// mobile (e.g. a metric selector); restored to their slot on desktop
|
||||
// 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", sheetOnly = [] }) {
|
||||
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.
|
||||
|
|
@ -65,23 +63,6 @@ export function initFilterSheet({ panel, label = "Filters", sheetOnly = [] }) {
|
|||
new MutationObserver(reflect).observe(panel, { attributes: true, attributeFilter: ["hidden"] });
|
||||
reflect();
|
||||
|
||||
// Relocate `sheetOnly` controls into the sheet on phones and back to their inline
|
||||
// slot on desktop, at the same 640px breakpoint the CSS uses. Each is remembered by
|
||||
// its original parent + next sibling; reparenting preserves their event listeners.
|
||||
const mq = window.matchMedia("(max-width: 640px)");
|
||||
const homes = sheetOnly.filter(Boolean).map((el) => ({ el, parent: el.parentNode, next: el.nextSibling }));
|
||||
function placeExtras() {
|
||||
for (const h of homes) {
|
||||
if (mq.matches) {
|
||||
if (h.el.parentNode !== panel) panel.append(h.el); // append after the grip + existing controls
|
||||
} else if (h.el.parentNode === panel) {
|
||||
h.parent.insertBefore(h.el, h.next); // restore (next === null → append)
|
||||
}
|
||||
}
|
||||
}
|
||||
mq.addEventListener("change", placeExtras);
|
||||
placeExtras();
|
||||
|
||||
return {
|
||||
// Show a dot on the pill when a filter subset is active (so the applied filter is
|
||||
// visible even with the sheet closed).
|
||||
|
|
|
|||
|
|
@ -1009,6 +1009,12 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
|||
border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
|
||||
}
|
||||
.cmp-comfort-wrap[hidden] { display: none; }
|
||||
/* The basis toggle now rides the comfort card (a scoring control, above the results). */
|
||||
.cmp-comfort-wrap .cmp-basis-block { margin-top: 12px; }
|
||||
/* Sheet-only duplicate metric selectors: hidden on desktop (the inline copy shows),
|
||||
revealed inside the filter sheet on phones (see the max-width:640px block). */
|
||||
.cmp-dist-metric-block, .cal-metric-sheet { display: none; }
|
||||
.cal-metric-sheet .cal-filter-label { display: block; margin: 0 0 8px; }
|
||||
.cmp-range-labels { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0 0 6px; font-size: 13px; color: var(--muted); }
|
||||
.cmp-rl b { color: var(--text); font-size: 15px; margin-left: 2px; }
|
||||
.cmp-rl-comfort b { color: var(--accent); }
|
||||
|
|
@ -1157,6 +1163,8 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
|||
|
||||
.cmp-params { grid-template-columns: 1fr; }
|
||||
.cmp-basis-block .metric-toggle button { flex: 1 0 22%; padding: 10px 4px; }
|
||||
/* Reveal the synced metric selectors inside the filter sheet on phones. */
|
||||
.cmp-dist-metric-block, .cal-metric-sheet { display: block; }
|
||||
|
||||
/* --- filters reachable from anywhere: a floating pill slides the page's filter
|
||||
panel up as a bottom sheet (compare #cmp-params, calendar #cal-filters) --- */
|
||||
|
|
|
|||
Loading…
Reference in a new issue