openvibers/openvibe.blog Commits
The AI desks are a rail on a phone, every desk shown (#191)
Changes
@@@ -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 itreplaces: a heading and its content, with no card around it. */@@@ -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' }) }),@@@ -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):@@@ -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 () => {
@@ -150,27 +150,14 @@ body.blog-home .page { max-width: 1120px; }
@@ -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
/* 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. */
@@ -29,7 +29,7 @@ const pages = require('../render/pages');
@@ -29,7 +29,7 @@ const pages = require('../render/pages');
const homePage = require('../render/home');
const homePage = require('../render/home');
const deskCatalog = require('../desks/catalog');
const deskCatalog = require('../desks/catalog');
// 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 { pickListRules, foldBlock } = require('../render/short');
+const { foldBlock } = require('../render/short');
const { createHome } = require('../domain/home');
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).
// 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) {
@@ -141,7 +141,6 @@ function createPublicRoutes(ctx) {
jsonLd: [blogLd, ...homePage.homeJsonLd({ base: config.baseUrl.replace(/\/+$/, ''), items })],
jsonLd: [blogLd, ...homePage.homeJsonLd({ base: config.baseUrl.replace(/\/+$/, ''), items })],
styles: [showcase.STYLESHEET], bodyClass: 'blog-home',
styles: [showcase.STYLESHEET], bodyClass: 'blog-home',
// The rule that shows the picked item of each pick list: one per item, written for this page.
// 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' })
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.
// 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' }) }),
+ 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' }) }),
@@ -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):
@@ -20,31 +20,20 @@ const { boot, check, done } = require('./helpers/boot');
@@ -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 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('/');
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-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);
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)];
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');
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 () => {
await check('the parts a reader rarely needs are folds, open in the page and closed on a phone', async () => {