openvibers/openvibe.blog Commits

The AI desks are a rail on a phone, every desk shown (#191)

OP OpenVibers committed 10 days ago · 98a36d44ff · 4 files, +27 −57

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

Changes

ddiff --git a/public/css/blog.css b/public/css/blog.css
iindex 5579eb3..311fe8b 100644
---- a/public/css/blog.css
++++ b/public/css/blog.css
@@@ -150,27 +150,14 @@ body.blog-home .page { max-width: 1120px; }
}
--/* 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 desks 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/http/public.js b/server/http/public.js
iindex d47915d..b0ca569 100644
---- a/server/http/public.js
++++ b/server/http/public.js
@@@ -29,7 +29,7 @@ const pages = require('../render/pages');
const homePage = require('../render/home');
const deskCatalog = require('../desks/catalog');
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
--const { pickListRules, foldBlock } = require('../render/short');
++const { foldBlock } = require('../render/short');
const { createHome } = require('../domain/home');
// The home's own words in search results and share cards (the official blog's later pages keep the blog's).
@@@ -141,7 +141,6 @@ function createPublicRoutes(ctx) {
jsonLd: [blogLd, ...homePage.homeJsonLd({ base: config.baseUrl.replace(/\/+$/, ''), items })],
styles: [showcase.STYLESHEET], bodyClass: 'blog-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('desks', deskCount)}}}`],
body: homePage.homeBody({ blog, items, pager, feeds, stats, week, latestDesks, tags, series: seriesCounts, memberItems, signedIn: req.viewer.kind === 'user' })
// The shipped list is a part a reader rarely needs: a fold on a phone, the section it was where there is room.
+ foldBlock({ id: 'shipped', head: '<h2 id="bh-shipped-h">Recently shipped on OpenVibe.Blog</h2>', html: frame.shipped({ service: 'blog', title: 'Recently shipped on OpenVibe.Blog' }) }),
ddiff --git a/server/render/short.js b/server/render/short.js
iindex d5d0d96..f64a523 100644
---- a/server/render/short.js
++++ b/server/render/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 2df68f6..7806ab9 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -20,31 +20,20 @@ const { boot, check, done } = require('./helpers/boot');
await t.get('/api/v1/posts/' + post.json().post.id + '/publish', { as: t.official, json: {} });
});
-- await check('the AI desks are a pick block: a chip per desk, the first picked, every desk in the page', async () => {
++ await check('the AI desks are a rail on a phone: every desk 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-desks">');
-- assert.ok(start > 0, 'the desks are a pick block on the home');
++ const start = page.indexOf('<div class="pick pick--rail" id="pick-desks">');
++ assert.ok(start > 0, 'the desks rail is on the home');
const block = page.slice(start, start + 6000);
-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="From the AI desks">/);
-- const chips = [...block.matchAll(/<input type="radio" name="pick-desks" id="pk-desks-(\d+)" value="\1"( checked)?><label for="pk-desks-\1">([^<]+)<\/label>/g)];
-- assert.ok(chips.length >= 1, 'a chip per desk (' + chips.length + ')');
-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), [chips[0][1]], 'the first desk is picked');
++ assert.ok(!/type="radio"|pick-row/.test(block), 'no radio chips');
const desks = [...block.matchAll(/<li data-i="(\d+)"><p class="bh-desk">/g)];
-- assert.strictEqual(desks.length, chips.length, 'every desk is in the page');
-- assert.deepStrictEqual(desks.map((m) => m[1]), chips.map((m) => m[1]), 'each desk knows its chip');
-- });
--
-- await check('the CSS guard: the chips are hidden and the desks all show until 640 px and where :has() is understood', async () => {
-- const r = await t.get('/');
-- const page = r.text;
-- assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick#pick-desks:has\(#pk-desks-0:checked\) \.pick-list>li\[data-i="0"\]\{display:block\}/, 'the rule that shows the picked desk');
++ assert.ok(desks.length >= 1, 'every desk is in the page (' + desks.length + ')');
++ assert.ok(!/\.pick#pick-desks:has\(/.test(page), 'no per-item rule in the page');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'blog.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 desks 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 () => {