openvibers/openvibe.live Commits

Rundown topics on a phone: 26,000 px to 3,600, by a counted fold (#504)

OP OpenVibers committed 12 days ago · 5da1ede794 · 2 files, +24 −7

The phone sweep's longest page (161 pages over ten sites, none overflowing). The first dozen topics show; the other
48 fold under "48 more topics" (the owner, 10-31: counted folds, never hiding). A card's summary is a four-line
teaser: the whole of it is the topic's own page, one tap away.

Changes

ddiff --git a/public/css/features/rundown.css b/public/css/features/rundown.css
iindex 8cec509c..ae33f863 100644
---- a/public/css/features/rundown.css
++++ b/public/css/features/rundown.css
@@@ -228,3 +228,14 @@ a.rd-use:hover b { color: var(--accent); }
.rd-onair-item { min-width: 200px; }
.rd-streamer-head { align-items: flex-start; }
}
++
++/* Topics (/rundown/topics): a card's summary is a four-line teaser, and the topics after the first dozen fold under
++ their count. */
++.rd-what-clip { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; line-clamp: 4; overflow: hidden; }
++.rd-more { margin-top: 14px; border-radius: 14px; border: 1px solid var(--border, rgba(255,255,255,.1)); }
++.rd-more > summary { padding: 14px 16px; cursor: pointer; font-weight: 700; list-style: none; display: flex; align-items: center; gap: 10px; }
++.rd-more > summary::-webkit-details-marker { display: none; }
++.rd-more > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(-45deg); transition: transform .18s ease; }
++.rd-more[open] > summary::before { transform: translateY(-4px) rotate(45deg); }
++.rd-more > .rd-topic-grid { padding: 4px 12px 14px; }
++@media (prefers-reduced-motion: reduce) { .rd-more > summary::before { transition: none; } }
ddiff --git a/public/js/rundown.js b/public/js/rundown.js
iindex 706eef5a..975b7dc4 100644
---- a/public/js/rundown.js
++++ b/public/js/rundown.js
@@@ -247,6 +247,18 @@ async function _rdMore(btn) {
}
// ── Topics ───────────────────────────────────────────────────
++// The first dozen show; the rest fold under their count (the owner, 10-31: short pages by counted folds, never by
++// hiding). A card's summary is a teaser of four lines: the whole of it is the topic's own page.
++const RD_FIRST = 12;
++function _rdTopicCard(t) {
++ return `<article class="rd-card rd-topic-card">
++ ${_rdTopicChip(t)}
++ ${t.headline ? `<h3 class="rd-h3">${_rdA(`/rundown/t/${encodeURIComponent(t.slug)}`, _rdEsc(t.headline))}</h3>` : ''}
++ ${t.summary ? `<p class="rd-what rd-what-clip">${_rdEsc(t.summary)}</p>` : ''}
++ <p class="rd-trend-meta"><span class="rd-stack" aria-hidden="true">${(t.people || []).map((p) => `<i style="--rd-c:${_rdEsc(p.profile_color || '#64748b')}">${p.avatar_url ? `<img src="${_rdEsc(p.avatar_url)}" alt="" width="20" height="20" loading="lazy">` : _rdEsc(_rdName(p).charAt(0).toUpperCase())}</i>`).join('')}</span>
++ ${t.streamers} streamer${t.streamers === 1 ? '' : 's'} · ${t.takes} take${t.takes === 1 ? '' : 's'} · ${_rdEsc(_rdAgo(t.last_seen_at))}</p>
++ </article>`;
++}
async function _rdTopics(root, query) {
const sort = query.get('sort') === 'most' ? 'most' : 'recent';
const kind = RD_KIND[query.get('kind')] ? query.get('kind') : '';
@@@ -257,13 +269,7 @@ async function _rdTopics(root, query) {
<div class="rd-seg" role="group" aria-label="Sort">${_rdA(href({ sort: '' }), 'Most recent', `rd-seg-btn${sort === 'recent' ? ' is-on' : ''}`)}${_rdA(href({ sort: 'most' }), 'Most talked about', `rd-seg-btn${sort === 'most' ? ' is-on' : ''}`)}</div>
<div class="rd-chips" role="group" aria-label="Kind">${_rdA(href({ kind: '' }), 'All', `rd-chip${kind ? '' : ' is-on'}`)}${Object.entries(RD_KIND).map(([k, [label, color]]) => _rdA(href({ kind: k }), label, `rd-chip${kind === k ? ' is-on' : ''}`).replace('class="rd-chip', `style="--rd-k:${color}" class="rd-chip`)).join('')}</div>
</div>
-- ${d.topics.length ? `<div class="rd-topic-grid">${d.topics.map((t) => `<article class="rd-card rd-topic-card">
-- ${_rdTopicChip(t)}
-- ${t.headline ? `<h3 class="rd-h3">${_rdA(`/rundown/t/${encodeURIComponent(t.slug)}`, _rdEsc(t.headline))}</h3>` : ''}
-- ${t.summary ? `<p class="rd-what">${_rdEsc(t.summary)}</p>` : ''}
-- <p class="rd-trend-meta"><span class="rd-stack" aria-hidden="true">${(t.people || []).map((p) => `<i style="--rd-c:${_rdEsc(p.profile_color || '#64748b')}">${p.avatar_url ? `<img src="${_rdEsc(p.avatar_url)}" alt="" width="20" height="20" loading="lazy">` : _rdEsc(_rdName(p).charAt(0).toUpperCase())}</i>`).join('')}</span>
-- ${t.streamers} streamer${t.streamers === 1 ? '' : 's'} · ${t.takes} take${t.takes === 1 ? '' : 's'} · ${_rdEsc(_rdAgo(t.last_seen_at))}</p>
-- </article>`).join('')}</div>` : _rdEmpty('fa-comments', kind ? 'No topics of that kind yet.' : 'No topics yet: the desk is reading this week\'s streams.')}`;
++ ${d.topics.length ? `<div class="rd-topic-grid">${d.topics.slice(0, RD_FIRST).map(_rdTopicCard).join('')}</div>${d.topics.length > RD_FIRST ? `<details class="rd-more"><summary>${d.topics.length - RD_FIRST} more topic${d.topics.length - RD_FIRST === 1 ? '' : 's'}</summary><div class="rd-topic-grid">${d.topics.slice(RD_FIRST).map(_rdTopicCard).join('')}</div></details>` : ''}` : _rdEmpty('fa-comments', kind ? 'No topics of that kind yet.' : 'No topics yet: the desk is reading this week\'s streams.')}`;
document.title = 'Topics · The Rundown · OpenVibe.Live';
}
async function _rdTopic(root, slug) {