openvibers/openvibe.report Commits
The trending topics are a rail on a phone, every topic shown (#84)
Changes
@@@ -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 itreplaces: a heading and its content, with no card around it. */@@@ -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)}}}`],});}@@@ -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):@@@ -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('/');
@@ -198,27 +198,14 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -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; }
.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
/* 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. */
replaces: a heading and its content, with no card around it. */
@@ -14,7 +14,7 @@ const appIcon = require('openvibe-shared/app-icon');
@@ -14,7 +14,7 @@ const appIcon = require('openvibe-shared/app-icon');
const kit = require('openvibe-shared/kit');
const kit = require('openvibe-shared/kit');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
// 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.
// 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 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');
const sc = require('openvibe-shared/showcase');
@@ -273,7 +273,6 @@ function home({ stats, rundown, topics } = {}) {
@@ -273,7 +273,6 @@ function home({ stats, rundown, topics } = {}) {
title: null, body: out, active: '/', path: '/', agent: true,
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()],
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.
// 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)}}}`],
});
});
}
}
@@ -41,27 +41,22 @@ function pickRules(key, ids) {
@@ -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 || [];
labels = labels || [];
if (!labels.length) return '';
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):
* What a fold holds, counted, for its title row (the owner, 2026-10-31: a fold that shortens a page shows its count):
@@ -4,20 +4,16 @@ const { boot, check, done } = require('./helpers/boot');
@@ -4,20 +4,16 @@ const { boot, check, done } = require('./helpers/boot');
(async () => {
(async () => {
const t = await boot();
const t = await boot();
try {
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('/');
const r = await t.get('/');
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.status, 200, r.text);
const page = 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);
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)];
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 () => {
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('/');
const r = await t.get('/');
@@ -25,15 +21,12 @@ const { boot, check, done } = require('./helpers/boot');
@@ -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.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');
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 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');
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 () => {
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('/');
const r = await t.get('/');