openvibers/openvibe.news Commits
Front page lists are rails on a phone, every item shown (#199)
Changes
@@@ -127,11 +127,7 @@ body.news-home .page { max-width: 1120px; }.nh-fold > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; }.nh-fold > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; border-radius: 14px; }.nh-fold .sc-sec { margin: 0; max-width: none; padding: 0 16px 16px; }--@media (max-width: 599px) {-- .nh-cards > li:nth-child(n+4), .nh-outlets > li:nth-child(n+4) { display: none; }-- .nh-wire .wire-list > li:nth-child(n+5) { display: none; }-- .nh-topics > li:nth-child(n+7) { display: none; }--}++/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */@media (pointer: coarse) {@@@ -151,27 +147,14 @@ body.news-home .page { max-width: 1120px; }section.wire > a::after { content: ''; position: absolute; left: 0; right: 0; top: -12px; bottom: -12px; }}--/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page's lists are one item at a time, picked-- with a row of chips. Radio inputs and :has(), no script: the chips and the one-item rule live under 640 px and only-- where :has() is understood, so a wider screen, or an older browser, shows every item 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-list > li { display: none; }-- /* The chip names the block, so the block's own heading gives way to it; the note under the wire says how it is-- grouped and stays for a reader who wants it (it is in the page for the crawlers either way). */-- .pick .sc-sec > h2, .pick .wire > h2 { display: none; }-- .pick .wire > p.meta { display: none; }--} }++/* The front page's lists (the owner, 2026-10-31: never hide content to shorten a page): on a phone each list is a++ rail you swipe, every item in it, the next one peeking in; a wider screen shows them as before. They used to be++ one item 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. */@@@ -34,7 +34,6 @@ const { csrfToken, checkCsrf } = require('../auth/forms');const cache = require('openvibe-shared/cache-policy');const { SITE_SUMMARY } = require('./discovery');const homePage = require('../render/home');--const { pickListRules } = require('../render/short');const { createHome } = require('../domain/home');// The front page in search results and share cards (≤ 160 characters).@@@ -119,8 +118,6 @@ function createPublicRoutes(ctx) {canonical, feeds, next: pager.next ? pager.next.href : null,jsonLd: homePage.homeJsonLd({ abs: (p) => publication.abs(p), topics: topicList, outlets }),styles: [showcase.STYLESHEET], bodyClass: 'news-home',-- // The rule that shows the picked item of each pick list: one per item, written for this page.-- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickListRules('stories', items.length)}${pickListRules('briefs', homeBriefs.length)}${pickListRules('wire', wire.length)}${pickListRules('topics', topicList.length)}}}`],body: homePage.homeBody({items, pager, feeds, stats, wire: pages.wireSection(wire), wireClusters: wire, topics: topicList, tally, outlets,briefs: homeBriefs,@@@ -9,7 +9,7 @@ const showcase = require('openvibe-shared/showcase');const { time, httpHref, storyList } = require('./pages');const { html: h, raw } = ssr;--const { pickList, pickListRule, foldBlock } = require('./short');++const { pickList, foldBlock } = require('./short');const num = (n) => Number(n || 0).toLocaleString('en-US');const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;const list = (xs) => (xs.length <= 1 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`);@@@ -2,6 +2,7 @@/*** The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.*++ * pickList() a list on the front page: a rail you swipe on a phone, every item shown (2026-10-31).* pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio* inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a* browser without :has()) shows them all and no chips.@@@ -15,26 +16,21 @@const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');/**-- * 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 (21 items hidden on the 11-03 home). `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 }) {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: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 ''; }/*** A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the@@@ -1,8 +1,8 @@'use strict';/**-- * The home on a phone (the owner's short-pages rule, 2026-10-30): the home's lists of the same kind are one pick-- * block, a row of chips picks one of them, and the parts a reader rarely needs are folds. Radio inputs and :has(),-- * no script for the chips; every list stays in the page for crawlers; a wider screen shows them all and no chips.++ * The home on a phone (the owner, 2026-10-31: never hide content to shorten a page): the home's lists are rails you++ * swipe, every item shown, and the parts a reader rarely needs are folds. They used to be one item at a time behind a++ * row of radio chips, the rest display:none.*/const assert = require('assert');const { boot, check, done, launchReports } = require('./helpers/boot');@@@ -12,42 +12,36 @@ const { boot, check, done, launchReports } = require('./helpers/boot');launchReports(t.sources);await t.pull();try {-- await check('the home is one pick block: a chip per item, the first picked, every item in the page', async () => {++ await check('the wire is a rail on a phone: every cluster shown, no chips, nothing hidden', async () => {const r = await t.get('/');assert.strictEqual(r.status, 200, r.text);const page = r.text;-- const block = page.slice(page.indexOf('<div class="pick" id="pick-wire">'), page.indexOf('<div class="pick" id="pick-topics">'));-- assert.ok(block.startsWith('<div class="pick" id="pick-wire">'), 'the pick block is on the home');-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="From the wire">/);-- const chips = [...block.matchAll(/<input type="radio" name="pick-wire" id="pk-wire-(\d+)" value="\1"( checked)?><label for="pk-wire-\1">([^<]+)<\/label>/g)];-- assert.ok(chips.length >= 1, `a chip per item (${chips.length})`);-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), [chips[0][1]], 'the first item is picked');-- // Every cluster stays in the page, with the index its chip picks.-- const clusters = [...block.matchAll(/<li class="wire-cluster" id="wire-[^"]+" data-i=(?:"|")?(\d+)/g)];-- assert.strictEqual(clusters.length, chips.length, 'every cluster is in the page');-- assert.deepStrictEqual(clusters.map((m) => m[1]), chips.map((m) => m[1]), 'each cluster knows its chip');++ const block = page.slice(page.indexOf('<div class="pick pick--rail" id="pick-wire">'), page.indexOf('<div class="pick pick--rail" id="pick-topics">'));++ assert.ok(block.startsWith('<div class="pick pick--rail" id="pick-wire">'), 'the wire rail is on the home');++ assert.ok(!/type="radio"|pick-row/.test(block), 'no radio chips');++ const clusters = [...block.matchAll(/<li class="wire-cluster" id="wire-[^"]+"/g)];++ assert.ok(clusters.length >= 1, `every cluster is in the page (${clusters.length})`);});-- await check('the topics are a pick block too: a chip per topic, every topic in the page', async () => {++ await check('the topics are a rail too: every topic in the page, no chips', async () => {const r = await t.get('/');const page = r.text;-- const block = page.slice(page.indexOf('<div class="pick" id="pick-topics">'), page.indexOf('<div class="pick" id="pick-topics">') + 4000);-- const chips = [...block.matchAll(/<input type="radio" name="pick-topics" id="pk-topics-(\d+)" value="\1"( checked)?><label for="pk-topics-\1">([^<]+)<\/label>/g)];-- assert.ok(chips.length >= 3, `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');++ const at = page.indexOf('<div class="pick pick--rail" id="pick-topics">');++ assert.ok(at > 0, 'the topics rail is on the home');++ const block = page.slice(at, at + 4000);++ assert.ok(!/type="radio"/.test(block), 'no radio chips');const items = [...block.matchAll(/<li data-i=(?:"|")?(\d+)"?><h3><a href="\/topics\/[a-z-]+">/g)];-- assert.strictEqual(items.length, chips.length, 'every topic stays in the page');++ assert.ok(items.length >= 3, `every topic stays in the page (${items.length})`);});-- await check('the CSS guard: the chips are hidden and the items 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 (the owner, 2026-10-31)', async () => {const r = await t.get('/');-- const page = r.text;-- assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-wire-0:checked\) \.pick-list>li\[data-i="0"\]\{display:block\}/, 'the rule that shows the picked item');++ assert.ok(!/\.pick:has\(#pk-/.test(r.text), 'no per-item rule in the page');const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'news.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 items 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.match(css, /\.pick--rail \.pick-list > li \{ flex: 0 0 84%/, 'the next item peeks in');++ assert.ok(!/pick-list > li \{ display: none/.test(css), 'no item is hidden');++ assert.ok(!/li:nth-child\(n\+\d+\) \{ display: none/.test(css), 'nothing is cut after the third or sixth');});await check('the parts a reader rarely needs are folds, open in the page and closed on a phone', async () => {
@@ -127,11 +127,7 @@ body.news-home .page { max-width: 1120px; }
@@ -127,11 +127,7 @@ body.news-home .page { max-width: 1120px; }
.nh-fold > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; }
.nh-fold > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; }
.nh-fold > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; border-radius: 14px; }
.nh-fold > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; border-radius: 14px; }
.nh-fold .sc-sec { margin: 0; max-width: none; padding: 0 16px 16px; }
.nh-fold .sc-sec { margin: 0; max-width: none; padding: 0 16px 16px; }
-@media (max-width: 599px) {
- .nh-cards > li:nth-child(n+4), .nh-outlets > li:nth-child(n+4) { display: none; }
- .nh-wire .wire-list > li:nth-child(n+5) { display: none; }
- .nh-topics > li:nth-child(n+7) { display: none; }
-}
+
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
@media (pointer: coarse) {
@media (pointer: coarse) {
@@ -151,27 +147,14 @@ body.news-home .page { max-width: 1120px; }
@@ -151,27 +147,14 @@ body.news-home .page { max-width: 1120px; }
section.wire > a::after { content: ''; position: absolute; left: 0; right: 0; top: -12px; bottom: -12px; }
section.wire > a::after { content: ''; position: absolute; left: 0; right: 0; top: -12px; bottom: -12px; }
}
}
-/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page's lists are one item at a time, picked
- with a row of chips. Radio inputs and :has(), no script: the chips and the one-item rule live under 640 px and only
- where :has() is understood, so a wider screen, or an older browser, shows every item 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-list > li { display: none; }
- /* The chip names the block, so the block's own heading gives way to it; the note under the wire says how it is
- grouped and stays for a reader who wants it (it is in the page for the crawlers either way). */
- .pick .sc-sec > h2, .pick .wire > h2 { display: none; }
- .pick .wire > p.meta { display: none; }
-} }
+/* The front page's lists (the owner, 2026-10-31: never hide content to shorten a page): on a phone each list is a
+ rail you swipe, every item in it, the next one peeking in; a wider screen shows them as before. They used to be
+ one item 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. */
@@ -34,7 +34,6 @@ const { csrfToken, checkCsrf } = require('../auth/forms');
@@ -34,7 +34,6 @@ const { csrfToken, checkCsrf } = require('../auth/forms');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const { SITE_SUMMARY } = require('./discovery');
const { SITE_SUMMARY } = require('./discovery');
const homePage = require('../render/home');
const homePage = require('../render/home');
-const { pickListRules } = require('../render/short');
const { createHome } = require('../domain/home');
const { createHome } = require('../domain/home');
// The front page in search results and share cards (≤ 160 characters).
// The front page in search results and share cards (≤ 160 characters).
@@ -119,8 +118,6 @@ function createPublicRoutes(ctx) {
@@ -119,8 +118,6 @@ function createPublicRoutes(ctx) {
canonical, feeds, next: pager.next ? pager.next.href : null,
canonical, feeds, next: pager.next ? pager.next.href : null,
jsonLd: homePage.homeJsonLd({ abs: (p) => publication.abs(p), topics: topicList, outlets }),
jsonLd: homePage.homeJsonLd({ abs: (p) => publication.abs(p), topics: topicList, outlets }),
styles: [showcase.STYLESHEET], bodyClass: 'news-home',
styles: [showcase.STYLESHEET], bodyClass: 'news-home',
- // The rule that shows the picked item of each pick list: one per item, written for this page.
- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickListRules('stories', items.length)}${pickListRules('briefs', homeBriefs.length)}${pickListRules('wire', wire.length)}${pickListRules('topics', topicList.length)}}}`],
body: homePage.homeBody({
body: homePage.homeBody({
items, pager, feeds, stats, wire: pages.wireSection(wire), wireClusters: wire, topics: topicList, tally, outlets,
items, pager, feeds, stats, wire: pages.wireSection(wire), wireClusters: wire, topics: topicList, tally, outlets,
briefs: homeBriefs,
briefs: homeBriefs,
@@ -9,7 +9,7 @@ const showcase = require('openvibe-shared/showcase');
@@ -9,7 +9,7 @@ const showcase = require('openvibe-shared/showcase');
const { time, httpHref, storyList } = require('./pages');
const { time, httpHref, storyList } = require('./pages');
const { html: h, raw } = ssr;
const { html: h, raw } = ssr;
-const { pickList, pickListRule, foldBlock } = require('./short');
+const { pickList, foldBlock } = require('./short');
const num = (n) => Number(n || 0).toLocaleString('en-US');
const num = (n) => Number(n || 0).toLocaleString('en-US');
const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;
const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;
const list = (xs) => (xs.length <= 1 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`);
const list = (xs) => (xs.length <= 1 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`);
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
/**
/**
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
*
*
+ * pickList() a list on the front page: a rail you swipe on a phone, every item shown (2026-10-31).
* pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio
* pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio
* inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a
* inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a
* browser without :has()) shows them all and no chips.
* browser without :has()) shows them all and no chips.
@@ -15,26 +16,21 @@
@@ -15,26 +16,21 @@
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
/**
/**
- * 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 (21 items hidden on the 11-03 home). `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 }) {
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: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 ''; }
/**
/**
* A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the
* A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
'use strict';
'use strict';
/**
/**
- * The home on a phone (the owner's short-pages rule, 2026-10-30): the home's lists of the same kind are one pick
- * block, a row of chips picks one of them, and the parts a reader rarely needs are folds. Radio inputs and :has(),
- * no script for the chips; every list stays in the page for crawlers; a wider screen shows them all and no chips.
+ * The home on a phone (the owner, 2026-10-31: never hide content to shorten a page): the home's lists are rails you
+ * swipe, every item shown, and the parts a reader rarely needs are folds. They used to be one item at a time behind a
+ * row of radio chips, the rest display:none.
*/
*/
const assert = require('assert');
const assert = require('assert');
const { boot, check, done, launchReports } = require('./helpers/boot');
const { boot, check, done, launchReports } = require('./helpers/boot');
@@ -12,42 +12,36 @@ const { boot, check, done, launchReports } = require('./helpers/boot');
@@ -12,42 +12,36 @@ const { boot, check, done, launchReports } = require('./helpers/boot');
launchReports(t.sources);
launchReports(t.sources);
await t.pull();
await t.pull();
try {
try {
- await check('the home is one pick block: a chip per item, the first picked, every item in the page', async () => {
+ await check('the wire is a rail on a phone: every cluster shown, no chips, nothing hidden', 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 block = page.slice(page.indexOf('<div class="pick" id="pick-wire">'), page.indexOf('<div class="pick" id="pick-topics">'));
- assert.ok(block.startsWith('<div class="pick" id="pick-wire">'), 'the pick block is on the home');
- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="From the wire">/);
- const chips = [...block.matchAll(/<input type="radio" name="pick-wire" id="pk-wire-(\d+)" value="\1"( checked)?><label for="pk-wire-\1">([^<]+)<\/label>/g)];
- assert.ok(chips.length >= 1, `a chip per item (${chips.length})`);
- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), [chips[0][1]], 'the first item is picked');
- // Every cluster stays in the page, with the index its chip picks.
- const clusters = [...block.matchAll(/<li class="wire-cluster" id="wire-[^"]+" data-i=(?:"|")?(\d+)/g)];
- assert.strictEqual(clusters.length, chips.length, 'every cluster is in the page');
- assert.deepStrictEqual(clusters.map((m) => m[1]), chips.map((m) => m[1]), 'each cluster knows its chip');
+ const block = page.slice(page.indexOf('<div class="pick pick--rail" id="pick-wire">'), page.indexOf('<div class="pick pick--rail" id="pick-topics">'));
+ assert.ok(block.startsWith('<div class="pick pick--rail" id="pick-wire">'), 'the wire rail is on the home');
+ assert.ok(!/type="radio"|pick-row/.test(block), 'no radio chips');
+ const clusters = [...block.matchAll(/<li class="wire-cluster" id="wire-[^"]+"/g)];
+ assert.ok(clusters.length >= 1, `every cluster is in the page (${clusters.length})`);
});
});
- await check('the topics are a pick block too: a chip per topic, every topic in the page', async () => {
+ await check('the topics are a rail too: every topic in the page, no chips', async () => {
const r = await t.get('/');
const r = await t.get('/');
const page = r.text;
const page = r.text;
- const block = page.slice(page.indexOf('<div class="pick" id="pick-topics">'), page.indexOf('<div class="pick" id="pick-topics">') + 4000);
- const chips = [...block.matchAll(/<input type="radio" name="pick-topics" id="pk-topics-(\d+)" value="\1"( checked)?><label for="pk-topics-\1">([^<]+)<\/label>/g)];
- assert.ok(chips.length >= 3, `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');
+ const at = page.indexOf('<div class="pick pick--rail" id="pick-topics">');
+ assert.ok(at > 0, 'the topics rail is on the home');
+ const block = page.slice(at, at + 4000);
+ assert.ok(!/type="radio"/.test(block), 'no radio chips');
const items = [...block.matchAll(/<li data-i=(?:"|")?(\d+)"?><h3><a href="\/topics\/[a-z-]+">/g)];
const items = [...block.matchAll(/<li data-i=(?:"|")?(\d+)"?><h3><a href="\/topics\/[a-z-]+">/g)];
- assert.strictEqual(items.length, chips.length, 'every topic stays in the page');
+ assert.ok(items.length >= 3, `every topic stays in the page (${items.length})`);
});
});
- await check('the CSS guard: the chips are hidden and the items 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 (the owner, 2026-10-31)', 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:has\(#pk-wire-0:checked\) \.pick-list>li\[data-i="0"\]\{display:block\}/, 'the rule that shows the picked item');
+ assert.ok(!/\.pick:has\(#pk-/.test(r.text), 'no per-item rule in the page');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'news.css'), 'utf8');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'news.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 items 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.match(css, /\.pick--rail \.pick-list > li \{ flex: 0 0 84%/, 'the next item peeks in');
+ assert.ok(!/pick-list > li \{ display: none/.test(css), 'no item is hidden');
+ assert.ok(!/li:nth-child\(n\+\d+\) \{ display: none/.test(css), 'nothing is cut after the third or sixth');
});
});
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 () => {