Season full-year option; metric select into mobile sheet; comfort inline on compare (#81)
- Season filter: add a "Full year" select-all row above the four seasons (checked at 12 months, clears to none, indeterminate when partial). One shared change in seasonFilterDropdown/applySeasonMonthChange/syncSeasonChecks covers both pages; the summary already reads "All year" at 12. - Metric selector rides the filter sheet on phones: initFilterSheet gains a sheetOnly option that relocates given controls into the sheet at the 640px breakpoint and restores them inline on desktop (matchMedia; reparenting keeps listeners). Compare passes its "Distribution by" control, calendar its "Color the calendar by" block. - Compare comfort/tolerance slider moved out of the params panel into a standalone full-width card above the results, so it's always visible instead of buried in the mobile pill sheet. The params sheet now holds basis + season + date range. Frontend-only; no backend or payload change.
This commit is contained in:
parent
f3ba056738
commit
ee9c9b3045
6 changed files with 85 additions and 30 deletions
|
|
@ -290,7 +290,9 @@ 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);
|
||||
const calSheet = initFilterSheet({ panel: filtersEl, label: "Filters" });
|
||||
// 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")] });
|
||||
calSheet.setActive(checkedMonths.size < 12);
|
||||
|
||||
// Close any open filter dropdown when tapping outside it (native <details> stays
|
||||
|
|
|
|||
|
|
@ -57,8 +57,9 @@
|
|||
<!-- The comfort target, what temperature to judge by, the tolerance band, and
|
||||
the date range. Comfort/band/basis recompute instantly (no refetch); only
|
||||
the date range or the location set trigger a data load. -->
|
||||
<div id="cmp-params" class="cmp-params" hidden>
|
||||
<div class="cmp-param cmp-comfort">
|
||||
<!-- Comfort/tolerance target — the primary knob, kept inline above the results
|
||||
(never inside the mobile filter sheet). Recomputes the ranking instantly. -->
|
||||
<div id="cmp-comfort" class="cmp-comfort-wrap" hidden>
|
||||
<div class="cmp-range-labels">
|
||||
<span class="cmp-rl cmp-rl-comfort">Comfort <b id="cmp-comfort-val"></b></span>
|
||||
<span class="cmp-rl cmp-rl-tol">Tolerance <b id="cmp-tol-val"></b></span>
|
||||
|
|
@ -82,6 +83,7 @@
|
|||
<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">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ const refreshBtnSheet = document.getElementById("cmp-refresh-sheet");
|
|||
const refreshBtns = [refreshBtn, refreshBtnSheet];
|
||||
const locList = document.getElementById("cmp-loc-list");
|
||||
const params = document.getElementById("cmp-params");
|
||||
const comfortWrap = document.getElementById("cmp-comfort"); // now inline, above the sheet
|
||||
const placeholder = document.getElementById("cmp-placeholder");
|
||||
const head = document.getElementById("cmp-head");
|
||||
const baseline = document.getElementById("cmp-baseline");
|
||||
|
|
@ -543,6 +544,7 @@ function renderDist() {
|
|||
function renderAll() {
|
||||
const has = locations.length > 0;
|
||||
params.hidden = !has;
|
||||
comfortWrap.hidden = !has;
|
||||
placeholder.hidden = has;
|
||||
addBtn.querySelector("span").textContent = has ? "Add another location" : "Add location";
|
||||
for (const b of refreshBtns) b.hidden = !isDirty();
|
||||
|
|
@ -680,7 +682,9 @@ clickOpensPicker(startInput, endInput); // whole-field tap opens the month pic
|
|||
document.getElementById("cmp-dist-count").checked = distCount;
|
||||
startInput.value = range.start; endInput.value = range.end;
|
||||
buildSeasonFilter();
|
||||
fabSheet = initFilterSheet({ panel: params, label: "Filters" });
|
||||
// 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.setActive(monthsActive());
|
||||
|
||||
// A shared link's locations win; else the last-used set; else seed from the spot
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@
|
|||
// 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 })
|
||||
// initFilterSheet({ panel, label, sheetOnly })
|
||||
// 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" }) {
|
||||
export function initFilterSheet({ panel, label = "Filters", sheetOnly = [] }) {
|
||||
if (!panel) return { setActive() {}, close() {} };
|
||||
|
||||
// The pill lives in <body> so it floats over the whole page regardless of scroll.
|
||||
|
|
@ -63,6 +65,23 @@ export function initFilterSheet({ panel, label = "Filters" }) {
|
|||
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).
|
||||
|
|
|
|||
|
|
@ -300,13 +300,17 @@ export function seasonFilterDropdown(checkedMonths, opts = {}) {
|
|||
<div class="season-months" hidden>${months}</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
// A "Full year" select-all sits above the four seasons: checked when all 12 months
|
||||
// are on, cleared to none when unticked (indeterminate state set by syncSeasonChecks).
|
||||
const allYear = `<label class="season-check season-allyear"><input type="checkbox" class="allyear-cb" data-allyear${checkedMonths.size === 12 ? " checked" : ""}>
|
||||
<span class="season-sw allyear-sw" aria-hidden="true"></span><span class="season-name">Full year</span></label>`;
|
||||
return `<details class="cal-dd cal-season-dd" data-dd="season">
|
||||
<summary>
|
||||
<span class="cal-filter-label">Seasons</span>
|
||||
<span class="cal-dd-sum">${seasonSummaryText(checkedMonths, opts)}</span>
|
||||
<span class="cal-dd-caret" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div class="cal-dd-panel cal-season-panel">${rows}</div>
|
||||
<div class="cal-dd-panel cal-season-panel">${allYear}${rows}</div>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
|
|
@ -320,11 +324,21 @@ export function syncSeasonChecks(root, checkedMonths) {
|
|||
cb.checked = on === ms.length;
|
||||
cb.indeterminate = on > 0 && on < ms.length;
|
||||
});
|
||||
const ay = root.querySelector(".allyear-cb");
|
||||
if (ay) {
|
||||
ay.checked = checkedMonths.size === 12;
|
||||
ay.indeterminate = checkedMonths.size > 0 && checkedMonths.size < 12;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a season/month checkbox change to the month Set. Returns true if the event
|
||||
// was a season/month toggle (so the caller knows to persist + re-render).
|
||||
export function applySeasonMonthChange(cb, checkedMonths) {
|
||||
if (cb.dataset.allyear != null) {
|
||||
if (cb.checked) allMonths().forEach((m) => checkedMonths.add(m));
|
||||
else checkedMonths.clear();
|
||||
return true;
|
||||
}
|
||||
if (cb.dataset.season) {
|
||||
const ms = monthsOfSeason(cb.dataset.season);
|
||||
if (cb.checked) ms.forEach((m) => checkedMonths.add(m));
|
||||
|
|
|
|||
|
|
@ -735,6 +735,13 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
|||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .25);
|
||||
}
|
||||
.season-name { font-weight: 600; }
|
||||
/* "Full year" select-all, above the four seasons — a divider separates it, and its
|
||||
swatch runs the four season colors so it reads as "everything". */
|
||||
.season-allyear { --season: var(--accent); border-bottom: 1px solid var(--border); border-radius: 0; margin-bottom: 2px; }
|
||||
.season-allyear .allyear-sw {
|
||||
background: linear-gradient(90deg, var(--season-winter), var(--season-spring), var(--season-summer), var(--season-fall));
|
||||
box-shadow: none;
|
||||
}
|
||||
.season-expand {
|
||||
flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
|
||||
background: transparent; border: 1px solid var(--border); border-radius: 7px;
|
||||
|
|
@ -995,6 +1002,13 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
|
|||
|
||||
/* Comfort vs tolerance shown as two separate labels, and a temperature marker above
|
||||
each of the four handles (the band borders). */
|
||||
/* Comfort/tolerance panel — pulled out of the params grid so it stays inline above
|
||||
the results (and out of the mobile filter sheet); a full-width bordered card. */
|
||||
.cmp-comfort-wrap {
|
||||
margin: 0 0 18px; padding: 14px 16px 8px;
|
||||
border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
|
||||
}
|
||||
.cmp-comfort-wrap[hidden] { display: none; }
|
||||
.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); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue