From 0ea99a714b2f3bc5a812c48998098601ceca1aed Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Sun, 23 Aug 2026 03:25:13 -0500 Subject: [PATCH] stats: pin the view switch in place; chips always on their own row; picker views start with every line on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the chip row sat beside the title in the source view and below it in the other two, so switching views threw the switch from one side of the card to the other. now: title left, switch right, chips on a full-width row underneath — identical geometry in every view, real spacing between chips and rows. endpoint/status views select everything by default with a 'clear' chip (and 'select all' to come back), so the breakdown is visible before any click. url: bare = all on, sel=- = none, sel=a,b = subset. Co-Authored-By: Claude Fable 5 --- src/pages/stats.ts | 54 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/pages/stats.ts b/src/pages/stats.ts index 69252db..5006473 100644 --- a/src/pages/stats.ts +++ b/src/pages/stats.ts @@ -370,9 +370,13 @@ export function statsPage(d: StatsData): string { content — that wrap was the chart "jumping" when a chip appeared. It still reserves one row height at all times, which covers the donut's hover preview and any single selection. */ - #tseries-chips { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; - min-height: 34px; flex: 1 1 0; min-width: 0; margin-left: auto; - justify-content: flex-end; } + /* always its own full-width row under the title, in EVERY view — the row + used to sit beside the title in one view and below it in the others, so + switching views threw the view switch from one side of the card to the + other. Same place, same shape, whatever is selected. */ + #tseries-chips { display: flex; flex-wrap: wrap; gap: 0.5rem 0.45rem; align-items: center; + min-height: 34px; flex-basis: 100%; min-width: 0; + justify-content: flex-start; margin: 0.15rem 0 0.35rem; } .ts-chip { display: inline-flex; align-items: center; gap: 0.4rem; background: var(--panel-2); border: 1px solid var(--line-2); color: var(--text); padding: 0.35rem 0.5rem 0.35rem 0.6rem; font-size: 0.72rem; cursor: pointer; @@ -390,11 +394,7 @@ export function statsPage(d: StatsData): string { .ts-chip.pick:hover { color: var(--text); border-color: var(--line-2); } .ts-chip.pick .ldot { opacity: 0.55; } .dim-btns button { padding: 0.25rem 0.5rem; font-size: 0.66rem; min-height: 28px; } - #tseries-dim { margin-left: 0.35rem; } - /* the picker views list every line as a chip: that row gets its own line, - left-aligned under the title, instead of squeezing beside it */ - .traffic-series[data-dim="endpoint"] #tseries-chips, - .traffic-series[data-dim="status"] #tseries-chips { flex-basis: 100%; justify-content: flex-start; } + #tseries-dim { margin-left: auto; } .empty-card { color: var(--dim); font-size: 0.8rem; padding: 2rem 0; text-align: center; } .pie-cta { margin-top: 0.6rem; font-size: 0.7rem; color: var(--dim); text-align: center; } .pie-cta a { color: var(--blue); text-decoration: none; } @@ -475,8 +475,6 @@ export function statsPage(d: StatsData): string { @media (max-width: 640px) { /* narrow: the heading alone nearly fills the line, so the chips get their own row rather than fighting it for space */ - #tseries-chips { flex-basis: 100%; } - #tseries-dim { margin-left: auto; } body { padding-left: 0.75rem; padding-right: 0.75rem; } #trend { height: 170px; } .legend { gap: 0.8rem; font-size: 0.65rem; flex-wrap: wrap; } @@ -1073,12 +1071,26 @@ export function statsPage(d: StatsData): string { if (pinned.length && showBaseline) { tseriesChips.appendChild(chip(DIMS[tsDim].all, null, true, () => { showBaseline = false; showSeries(); syncUrl(); })); } - for (const k of dimKeys()) { + const keys = dimKeys(); + for (const k of keys) { const sel = pinned.includes(k); const c = chip(k, keyColor(k), false, () => applySelection(k)); if (!sel) { c.classList.add('pick'); c.title = 'add to chart'; c.querySelector('.x').textContent = '+'; } tseriesChips.appendChild(c); } + if (pinned.length >= 2) { + const clear = document.createElement('button'); + clear.className = 'ts-chip clear'; + clear.textContent = 'clear'; + clear.addEventListener('click', () => applySelection(null)); + tseriesChips.appendChild(clear); + } else if (pinned.length === 0 && keys.length >= 2) { + const all = document.createElement('button'); + all.className = 'ts-chip clear'; + all.textContent = 'select all'; + all.addEventListener('click', () => { pinned = keys.slice(); showBaseline = true; showSeries(); syncUrl(); }); + tseriesChips.appendChild(all); + } return; } if (pinned.length === 0) { @@ -1316,7 +1328,16 @@ export function statsPage(d: StatsData): string { // pinned keys live under 'apps' for sources (shared links predate the other // views) and 'sel' otherwise, so a link never restores an endpoint as an app p.delete('apps'); p.delete('sel'); - if (pinned.length) p.set(tsDim === 'source' ? 'apps' : 'sel', pinned.join(',')); + if (tsDim === 'source') { + if (pinned.length) p.set('apps', pinned.join(',')); + } else { + // default (everything on) is the bare URL; '-' is the one state that + // needs a marker: nothing pinned, just the total + const keys = dimKeys(); + const allOn = pinned.length === keys.length && keys.every(k => pinned.includes(k)); + if (pinned.length === 0) p.set('sel', '-'); + else if (!allOn) p.set('sel', pinned.join(',')); + } if (tsRange === '7d') p.delete('range'); else p.set('range', tsRange); if (pinned.length && !showBaseline) p.set('total', '0'); else p.delete('total'); const q = p.toString(); @@ -1333,7 +1354,10 @@ export function statsPage(d: StatsData): string { tsDim = view; setActiveDimBtn(); const keys = dimKeys(); - pinned = [...new Set((p.get('sel') || '').split(',').map(s => s.trim()).filter(s => keys.includes(s)))]; + const sel = p.get('sel'); + pinned = sel == null ? keys.slice() + : sel === '-' ? [] + : [...new Set(sel.split(',').map(s => s.trim()).filter(s => keys.includes(s)))]; showBaseline = p.get('total') !== '0'; showSeries(); syncUrl(); @@ -1372,7 +1396,9 @@ export function statsPage(d: StatsData): string { function setDim(dim) { if (!DIMS[dim] || dim === tsDim) return; tsDim = dim; - pinned = []; hovered = null; showBaseline = true; + // the picker views start with every line ON — the first thing you see is + // the whole breakdown, and 'clear' is one tap if you want to pick instead + pinned = dim === 'source' ? [] : dimKeys(); hovered = null; showBaseline = true; document.querySelectorAll('.pie-legend span[data-domain], .ts-table tbody tr[data-domain]').forEach(el => el.classList.remove('sel')); setActiveDimBtn(); showSeries(); -- 2.54.0