Paged lists: 25 rows, then a clearly marked next page
Marty (2026-09-19): once a membership gets large, My line and the other lists scroll forever. One helper, pageList(), pages what each pane already renders: rows past the current page are hidden and a solid-mint control bar (row range, Previous/Next 25, page count) goes under the list. Row markup and bindings are untouched, so it goes on every growing list at once: My line per level (the Activity drawer pages with its row, and a chip drill-down opens on the right page), the Overview roster, the Campaigns table and its credit ledger, each Pipeline column, and the Messages thread list. Asset tags bumped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -225,6 +225,43 @@
|
||||
}
|
||||
// ── overview v3 charts: hand-rolled SVG/CSS, real data only ──
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
// Paged lists (Marty, 2026-09-19): 25 rows, then a clearly marked next page, on every list
|
||||
// that grows with the membership. Works on what is already rendered: rows past the page are
|
||||
// hidden and a control bar goes under the list, so a row's buttons and bindings never change.
|
||||
// key remembers the page across re-renders of the same list
|
||||
// host the element the bar is appended to (a wrapper, never a <table>)
|
||||
// rowSel the rows, queried inside host
|
||||
// opts per (default 25); tail: a selector for a drawer that follows its row and pages
|
||||
// with it; focus: a row predicate, opens on whichever page holds that row
|
||||
const PAGE_SIZE = 25;
|
||||
const PAGE_AT = {};
|
||||
function pageList(key, host, rowSel, opts) {
|
||||
if (!host) return; opts = opts || {}; const per = opts.per || PAGE_SIZE;
|
||||
const rows = [...host.querySelectorAll(rowSel)];
|
||||
const old = host.querySelector(':scope > .pgr'); if (old) old.remove();
|
||||
if (rows.length <= per) { rows.forEach(r => { r.hidden = false; }); return; }
|
||||
const pages = Math.ceil(rows.length / per);
|
||||
let p = PAGE_AT[key] || 1;
|
||||
if (opts.focus) { const i = rows.findIndex(opts.focus); if (i >= 0) p = Math.floor(i / per) + 1; }
|
||||
p = Math.min(Math.max(1, p), pages); PAGE_AT[key] = p;
|
||||
const start = (p - 1) * per;
|
||||
rows.forEach((r, i) => {
|
||||
const on = i >= start && i < start + per; r.hidden = !on;
|
||||
if (opts.tail) { const t = r.nextElementSibling; if (t && t.matches(opts.tail) && !on && !t.hidden) { t.hidden = true; const b = r.querySelector('[aria-expanded]'); if (b) { b.setAttribute('aria-expanded', 'false'); b.textContent = 'Activity \u25BE'; } } }
|
||||
});
|
||||
const bar = document.createElement('div'); bar.className = 'pgr';
|
||||
bar.innerHTML = '<span class="pgr-n">Showing <b>' + (start + 1) + '\u2013' + Math.min(start + per, rows.length) + '</b> of ' + rows.length + '</span>'
|
||||
+ '<span class="pgr-b"><button type="button" class="pgr-btn" data-go="' + (p - 1) + '"' + (p <= 1 ? ' disabled' : '') + '>\u2190 Previous ' + per + '</button>'
|
||||
+ '<span class="pgr-pg">Page ' + p + ' of ' + pages + '</span>'
|
||||
+ '<button type="button" class="pgr-btn" data-go="' + (p + 1) + '"' + (p >= pages ? ' disabled' : '') + '>Next ' + per + ' \u2192</button></span>';
|
||||
bar.querySelectorAll('[data-go]').forEach(b => b.addEventListener('click', () => {
|
||||
PAGE_AT[key] = Number(b.dataset.go);
|
||||
pageList(key, host, rowSel, Object.assign({}, opts, { focus: null }));
|
||||
host.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}));
|
||||
host.appendChild(bar);
|
||||
}
|
||||
const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' };
|
||||
function bars(el, xel, data) { // data: [{v,label,tip,alt}]
|
||||
if (!el) return;
|
||||
@@ -564,6 +601,7 @@
|
||||
+ '<td>' + new Date(r.joined).toLocaleDateString() + '</td>'
|
||||
+ '<td><span class="badge' + (r.status === 'joined free' ? ' amber' : '') + '">' + r.status + '</span></td></tr>').join('');
|
||||
wrap.appendChild(t);
|
||||
pageList('roster', wrap, 'table.roster tr');
|
||||
}
|
||||
// overview recent-activity widget: chain events + line joins, newest first
|
||||
try {
|
||||
@@ -831,6 +869,7 @@
|
||||
+ r.items.map(i => '<tr><td class="muted small" style="white-space:nowrap">' + when(i.ts) + '</td><td class="num" style="white-space:nowrap"><b style="color:' + (i.delta > 0 ? '#3ecf7a' : 'inherit') + '">' + (i.delta > 0 ? '+' : '') + i.delta.toLocaleString() + '</b></td><td>' + esc(i.note) + '</td></tr>').join('')
|
||||
+ '</tbody></table></div>';
|
||||
el.appendChild(box);
|
||||
pageList('credits', box, 'tbody tr');
|
||||
} catch (e) {}
|
||||
}
|
||||
async function loadCampaigns() {
|
||||
@@ -883,6 +922,7 @@
|
||||
: ' <button class="btn small sec" data-topup="' + c.id + '">Buy more views</button>')
|
||||
+ '</td></tr>').join('') + '</tbody></table>';
|
||||
el.appendChild(tbl);
|
||||
pageList('camps', tbl, 'tbody tr');
|
||||
const note = document.createElement('p'); note.className = 'muted small';
|
||||
note.textContent = 'Credits you put into a campaign are set aside the moment you start it, so your Available number drops right away and the campaign spends from its own budget. Featured links and verified-visit packs are paid in full up front; a featured run is sold by the day, so use Extend run to keep it going. Whatever a campaign has not spent comes back to Available when it ends.';
|
||||
el.appendChild(note);
|
||||
@@ -1483,6 +1523,7 @@
|
||||
$('pipeDue').innerHTML = r.due.length ? '<div class="pipe-due">' + r.due.map(c => pipeCardHtml(c, true)).join('') + '</div>' : '<p class="muted small">Nothing due. Set a follow-up date on any card and it shows up here.</p>';
|
||||
$('pipeSummary').textContent = r.counts.total ? r.counts.total + ' people on your board' + (r.counts.stalled ? ', ' + r.counts.stalled + ' stalled' : '') + '. Cards move on their own when something happens on the ledger; open one to add a note, a follow-up date or a tag.' : 'Nobody on your board yet. Add prospects on My line, and everyone who joins through your link appears here on their own.';
|
||||
$('pipeBoard').innerHTML = r.columns.map(col => '<div class="pipe-col"><h4>' + esc(col.label) + '<span>' + col.cards.length + '</span></h4><p class="hint">' + esc(col.hint) + '</p>' + col.cards.map(c => pipeCardHtml(c, false)).join('') + '</div>').join('');
|
||||
$('pipeBoard').querySelectorAll('.pipe-col').forEach((col, i) => pageList('pipe-' + i, col, ':scope > [data-pk]'));
|
||||
$('pipeLive').querySelectorAll('[data-pk]').forEach(el => {
|
||||
el.addEventListener('click', () => openPipeCard(el.dataset.pk));
|
||||
el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPipeCard(el.dataset.pk); } });
|
||||
@@ -1599,6 +1640,8 @@
|
||||
el.querySelectorAll('[data-cemail]').forEach(b =>
|
||||
b.addEventListener('click', () => openConvo(b.dataset.cemail, b.dataset.cname)));
|
||||
el.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', () => toggleLineActivity(b)));
|
||||
const focusRef = LINE_FOCUS_REF ? LINE_FOCUS_REF.replace(/"/g, '') : null;
|
||||
el.querySelectorAll('.lin-lvl').forEach((lv, i) => pageList('line-' + i, lv, ':scope > .lin-row', { tail: '.lin-act', focus: focusRef ? row => !!row.querySelector('[data-act="' + focusRef + '"]') : null }));
|
||||
if (LINE_FOCUS_REF) {
|
||||
const ref = LINE_FOCUS_REF; LINE_FOCUS_REF = null;
|
||||
const b = el.querySelector('[data-act="' + ref.replace(/"/g, '') + '"]');
|
||||
@@ -2657,6 +2700,7 @@
|
||||
+ (t.unread ? '<span class="ct-un">' + t.unread + '</span>' : '') + '</div>').join('');
|
||||
$('chatThreads').querySelectorAll('.chat-thread').forEach(el =>
|
||||
el.addEventListener('click', () => openConvo(el.dataset.email, el.dataset.name)));
|
||||
pageList('chat', $('chatThreads'), '.chat-thread');
|
||||
} catch (e) { $('chatThreads').innerHTML = '<div class="chat-empty">Could not load messages.</div>'; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user