openvibers/openvibe.report Commits

The trending topics are a rail on a phone, every topic shown (#84)

OP OpenVibers committed 11 days ago · 77c785447e · 4 files, +28 −54

The owner's rule of 10-31: never hide content to shorten a page. On a phone the home showed one topic at a time
behind a row of radio chips, the rest display:none (5 hidden on 11-03). The topics are now a rail you swipe, every
topic in it; a wider screen shows them as before.

Changes

ddiff --git a/public/css/app.css b/public/css/app.css
iindex 641759e..3cb9c9e 100644
---- a/public/css/app.css
++++ b/public/css/app.css
@@@ -198,27 +198,14 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.rp-lead a::after { content: ''; position: absolute; left: 0; right: 0; top: -11px; bottom: -11px; }
}
--/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page shows one block at a time, picked with a
-- row of chips. Radio inputs and :has(), no script: the chips and the one-block rule live under 640 px and only
-- where :has() is understood, so a wider screen, or an older browser, shows every block and no chips. */
--.pick-head { display: none; }
--.pick-row { display: none; }
--@supports selector(:has(*)) { @media (max-width: 640px) {
-- .pick-bar { margin-top: 0; }
-- .pick-head { display: block; }
-- .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px,2.4vw,28px)/1.2 var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif); letter-spacing: -.02em; }
-- .pick-row { display: flex; gap: 8px; overflow-x: auto; margin: 10px -16px 0; padding: 2px 16px 4px; scrollbar-width: none; overscroll-behavior-x: contain; }
-- .pick-row::-webkit-scrollbar { display: none; }
-- .pick-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
-- .pick-row label { display: inline-flex; align-items: center; flex: none; min-height: 44px; padding: 0 15px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; cursor: pointer; }
-- .pick-row input:checked + label { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
-- .pick-row input:focus-visible + label { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
-- .pick .pick-pane { display: none; }
-- .pick .pick-list > li { display: none; }
-- /* The chip names the block, so the block's own heading gives way to it; the content stays in the page for the
-- crawlers and for the reader on a wide screen. */
-- .pick .sc-sec > h2, .pick section > h2, .pick > h2 { display: none; }
--} }
++/* The front page's lists (the owner, 2026-10-31: never hide content to shorten a page): on a phone a list is a rail
++ you swipe, every item in it, the next one peeking in; a wider screen shows it as before. The topics used to be one
++ at a time behind a row of radio chips, the rest display:none. */
++@media (max-width: 640px) {
++ .pick--rail .pick-list { display: flex; gap: 12px; overflow-x: auto; scroll-snap-type: x mandatory; margin: 0 -16px; padding: 2px 16px 10px; list-style: none; scrollbar-width: none; overscroll-behavior-x: contain; }
++ .pick--rail .pick-list::-webkit-scrollbar { display: none; }
++ .pick--rail .pick-list > li { flex: 0 0 84%; min-width: 0; scroll-snap-align: start; }
++}
/* The fold (the owner's short-pages rule): closed on a phone, and where there is room it looks like the section it
replaces: a heading and its content, with no card around it. */
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 4f2cf1b..164ddac 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -14,7 +14,7 @@ const appIcon = require('openvibe-shared/app-icon');
const kit = require('openvibe-shared/kit');
const seo = require('openvibe-shared/seo');
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
--const { pickList, pickListRules, foldAround } = require('./short');
++const { pickList, foldAround } = require('./short');
// A short name for a chip: the title, cut where it stops saying anything.
const clip = (s, n = 40) => { const x = String(s == null ? '' : s); return x.length > n ? `${x.slice(0, n - 1).trimEnd()}…` : x; };
const sc = require('openvibe-shared/showcase');
@@@ -273,7 +273,6 @@ function home({ stats, rundown, topics } = {}) {
title: null, body: out, active: '/', path: '/', agent: true,
jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Report', url: 'https://openvibe.report/', description: "The network's numbers open to everyone, what streamers talked about in The Rundown, and every stream's after-show report." }), seo.jsonLd.organization()],
// The rule that shows the picked topic of the pick list: one per topic, written for this page.
-- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickListRules('topics', trending.length)}}}`],
});
}
ddiff --git a/server/views/short.js b/server/views/short.js
iindex e431d31..6ae9192 100644
---- a/server/views/short.js
++++ b/server/views/short.js
@@@ -41,27 +41,22 @@ function pickRules(key, ids) {
}
/**
-- * A pick list: a list of things of the same kind, one at a time on a phone, picked with a row of chips. The list
-- * keeps its own markup and every item stays in the page; the caller marks each item with the index its chip picks.
++ * A list on the front page (the owner, 2026-10-31: never hide content to shorten a page). On a phone it is a rail you
++ * swipe, every item in it; a wider screen shows the list as before. It used to be one item at a time, picked with a
++ * row of radio chips, the rest display:none. `labels` is kept for the callers and no longer drawn: the list's own
++ * heading names it.
*
-- * pickList({ key, label, head, labels, html }) labels: the chip labels, in the order of the items
++ * pickList({ key, label, head, labels, html })
*/
--function pickList({ key, label, head = '', labels, html }) {
++function pickList({ key, labels, html }) {
labels = labels || [];
if (!labels.length) return '';
-- const chips = labels.map((l, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + i + '" value="' + i + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + i + '">' + esc(l) + '</label>').join('');
-- return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><div class="pick-row" role="radiogroup" aria-label="' + esc(label) + '">' + chips + '</div></div>' + html + '</div>';
++ return '<div class="pick pick--rail" id="pick-' + esc(key) + '">' + html + '</div>';
}
--/** The rule that shows the picked item of a pick list: one per item, so the CSS is written for the page. */
--function pickListRule(key, i) {
-- return '.pick#pick-' + key + ':has(#pk-' + key + '-' + i + ':checked) .pick-list>li[data-i="' + i + '"]{display:block}';
--}
--
--/** The rules for every item of a pick list. */
--function pickListRules(key, n) {
-- return Array.from({ length: n }, (_, i) => pickListRule(key, i)).join('');
--}
++/** No longer needed (every item shows): kept so a caller that still asks for the rules gets none. */
++function pickListRule() { return ''; }
++function pickListRules() { return ''; }
/**
* What a fold holds, counted, for its title row (the owner, 2026-10-31: a fold that shortens a page shows its count):
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 0f86ac6..3fb6dd9 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -4,20 +4,16 @@ const { boot, check, done } = require('./helpers/boot');
(async () => {
const t = await boot();
try {
-- await check('the topics are one pick block: a chip per topic, the first picked, every topic in the page', async () => {
++ await check('the topics are a rail on a phone: every topic shown, no chips, nothing hidden (the owner, 2026-10-31)', async () => {
const r = await t.get('/');
assert.strictEqual(r.status, 200, r.text);
const page = r.text;
-- const start = page.indexOf('<div class="pick" id="pick-topics">');
-- assert.ok(start > 0, 'the pick block is on the home');
++ const start = page.indexOf('<div class="pick pick--rail" id="pick-topics">');
++ assert.ok(start > 0, 'the topics rail is on the home');
const block = page.slice(start, start + 8000);
-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="What streamers talked about">/);
-- const chips = [...block.matchAll(/<input type="radio" name="pick-topics" id="pk-topics-(\d+)" value="\1"( checked)?><label for="pk-topics-\d+">([^<]+)<\/label>/g)];
-- assert.ok(chips.length >= 1, 'a chip per topic (' + chips.length + ')');
-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), [chips[0][1]], 'the first topic is picked');
++ assert.ok(!/type="radio"|pick-row/.test(block), 'no radio chips');
const topics = [...block.matchAll(/<li data-i="(\d+)"><a class="rp-topic"/g)];
-- assert.strictEqual(topics.length, chips.length, 'every topic is in the page');
-- assert.deepStrictEqual(topics.map((m) => m[1]), chips.map((m) => m[1]), 'each topic knows its chip');
++ assert.ok(topics.length >= 1, 'every topic is in the page (' + topics.length + ')');
});
await check('the hero draws an after-show report in miniature, not sc.room\'s default motif (MOTION-1)', async () => {
const r = await t.get('/');
@@@ -25,15 +21,12 @@ const { boot, check, done } = require('./helpers/boot');
assert.match(r.text, /After the show · sample[\s\S]*?peak 41[\s\S]*?top moment/);
assert.ok(!/class="sc-room-motif"/.test(r.text), 'no default motif');
});
-- await check('the CSS guard: the chips are hidden and the topics all show until 640 px and where :has() is understood', async () => {
++ await check('the CSS: on a phone a list is a rail you swipe, and no rule hides an item', async () => {
const r = await t.get('/');
-- const page = r.text;
-- assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick#pick-topics:has\(#pk-topics-0:checked\) \.pick-list>li\[data-i="0"\]\{display:block\}/, 'the rule that shows the picked topic');
++ assert.ok(!/\.pick#pick-topics:has\(/.test(r.text), 'no per-item rule in the page');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'app.css'), 'utf8');
-- assert.match(css, /\.pick-row \{ display: none; \}/, 'the chips are hidden by default');
-- assert.match(css, /@supports selector\(:has\(\*\)\) \{ @media \(max-width: 640px\) \{/, 'the phone rules are under :has() and 640 px');
-- assert.match(css, /\.pick-row label \{[^}]*min-height: 44px/, 'a chip is a 44 px target');
-- assert.match(css, /\.pick \.pick-list > li \{ display: none; \}/, 'the topics are hidden on a phone');
++ assert.match(css, /\.pick--rail \.pick-list \{ display: flex;[^}]*overflow-x: auto;[^}]*scroll-snap-type: x mandatory/, 'the list is a rail');
++ assert.ok(!/pick-list > li \{ display: none|pick-pane \{ display: none/.test(css), 'nothing is hidden');
});
await check('the parts a reader rarely needs are folds, open in the page and closed on a phone', async () => {
const r = await t.get('/');