openvibers/openvibe.blog Commits

The week becomes a fold, so the home fits under four screens (#141)

OP OpenVibers committed 14 days ago · 4e6440dcfe · 3 files, +19 −41

The home on a phone is one or two screens (the owner's short-pages rule): the week's changes and the AI desks are pick blocks, a row of chips picks one of them, and the topics, the questions and the shipped list are folds. Radio inputs and :has(), no script for the chips; every item stays in the page for crawlers, and a wide screen shows them all and no chips.

Changes

ddiff --git a/server/http/public.js b/server/http/public.js
iindex d30ceac..1b3bd88 100644
---- a/server/http/public.js
++++ b/server/http/public.js
@@@ -141,7 +141,7 @@ 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('week', week.length)}${pickListRules('desks', deskCount)}}}`],
++ 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/home.js b/server/render/home.js
iindex 8a520ed..6216e8f 100644
---- a/server/render/home.js
++++ b/server/render/home.js
@@@ -71,14 +71,12 @@ function homeBody(o) {
${blog.description ? h`<p class="sc-lede">${blog.description}</p>` : ''}
${feeds.length ? h`<p class="feeds">Follow: ${feeds.map((f) => h`<a href="${f.href}" type="${f.mime}">${f.label}</a> `)}</p>` : ''}
${items.length ? h`<ol class="bh-cards">${items.map(postCard)}</ol>${raw(ssr.paginationHtml(pager))}<p class="bh-more bh-all"><a href="/@${blog.handle}">Every post, newest first</a></p>` : h`<p class="empty">Nothing published here yet.</p>`}</section>
--${week.length ? raw(pickList({
-- key: 'week',
-- label: 'This week across OpenVibe',
-- head: 'This week across OpenVibe',
-- labels: week.map((w) => siteName(w.service)),
-- html: h`<section class="sc-sec bh-sec" aria-labelledby="bh-week-h"><h2 id="bh-week-h">This week across OpenVibe</h2>
++${week.length ? raw(foldBlock({
++ id: 'week',
++ head: '<h2 id="bh-week-h">This week across OpenVibe</h2>',
++ html: h`<section class="sc-sec bh-sec" aria-labelledby="bh-week-h"><h2>This week across OpenVibe</h2>
<p class="sc-lede">${plural(stats.weekChanges, 'change')} shipped to ${plural(stats.weekSites, 'site')} in the last seven days. Patch notes on this blog round them up.</p>
--<ol class="bh-bars pick-list">${week.map((w, i) => h`<li data-i="${i}"><a href="https://openvibe.network/updates?site=${encodeURIComponent(w.service)}">${siteName(w.service)}</a><span class="bh-bar" aria-hidden="true"><span style="width:${String(Math.max(2, Math.round((w.changes / top) * 100)))}%"></span></span><span class="bh-n">${plural(w.changes, 'change')}</span></li>`)}</ol>
++<ol class="bh-bars">${week.map((w) => h`<li><a href="https://openvibe.network/updates?site=${encodeURIComponent(w.service)}">${siteName(w.service)}</a><span class="bh-bar" aria-hidden="true"><span style="width:${String(Math.max(2, Math.round((w.changes / top) * 100)))}%"></span></span><span class="bh-n">${plural(w.changes, 'change')}</span></li>`)}</ol>
<p class="bh-more"><a href="https://openvibe.network/updates">Every change, as it ships</a>${series.length ? h` · <a href="${series[0].url}">${series[0].title}</a>` : ''}</p></section>`,
})) : ''}
${desks.length ? raw(pickList({
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 56b99b6..2df68f6 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -1,9 +1,4 @@
'use strict';
--/**
-- * The home on a phone (the owner's short-pages rule, 2026-10-30): the 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 item stays in the page for crawlers; a wider screen shows them all and no chips.
-- */
const assert = require('assert');
const { boot, check, done } = require('./helpers/boot');
@@@ -17,64 +12,49 @@ const { boot, check, done } = require('./helpers/boot');
const entry = db.prepare('INSERT INTO changelog_entries (service, repo, sha, subject, deployed_at, release) VALUES (?, ?, ?, ?, ?, ?)');
await entry.run('live', 'OpenVibers/live', 'sha1', 'change 1', iso(now - 86400_000), 'r1');
await entry.run('wiki', 'OpenVibers/wiki', 'sha2', 'change 2', iso(now - 2 * 86400_000), 'r1');
-- await entry.run('news', 'OpenVibers/news', 'sha3', 'change 3', iso(now - 3 * 86400_000), 'r1');
const desk = db.prepare('INSERT INTO blog_desk_posts (id, desk, slug, status, title, dek, period_from, period_to, created_at, published_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
await desk.run('dp_news', 'news', 'headlines-oct-21', 'published', 'Headlines of Oct 21', 'What governments announced.', now - 2 * 86400_000, now - 86400_000, now, now, now);
await desk.run('dp_satire', 'satire', 'too-many-vibes', 'published', 'Is it possible to take too many vibes?', 'A sober inquiry.', now - 86400_000, now, now, now, now);
const post = await t.get('/api/v1/blogs/openvibe/posts', { as: t.official, json: { body: 'A letter from the team.\n\nThe network ships.', title: 'A letter from the team', tags: 'live,patch-notes' } });
assert.strictEqual(post.status, 201, post.text);
-- 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 week is one pick block: a chip per site, the first picked, every site in the page', async () => {
++ await check('the AI desks are a pick block: a chip per desk, the first picked, every desk in the page', 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-week">');
-- assert.ok(start > 0, 'the pick block is on the home');
-- const block = page.slice(start, page.indexOf('<div class="pick" id="pick-desks">'));
-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="This week across OpenVibe">/);
-- const chips = [...block.matchAll(/<input type="radio" name="pick-week" id="pk-week-(\d+)" value="\1"( checked)?><label for="pk-week-\1">([^<]+)<\/label>/g)];
-- assert.ok(chips.length >= 1, `a chip per site (${chips.length})`);
-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), [chips[0][1]], 'the first site is picked');
-- // Every site stays in the page, with the index its chip picks.
-- const bars = [...block.matchAll(/<li data-i="(\d+)"><a href="https:\/\/openvibe.network\/updates/g)];
-- assert.strictEqual(bars.length, chips.length, 'every site is in the page');
-- assert.deepStrictEqual(bars.map((m) => m[1]), chips.map((m) => m[1]), 'each site knows its chip');
-- });
--
-- await check('the AI desks are a pick block too: a chip per desk, every desk in the page', async () => {
-- const r = await t.get('/');
-- const page = r.text;
const start = page.indexOf('<div class="pick" id="pick-desks">');
-- assert.ok(start > 0, 'the desks are a pick block');
++ assert.ok(start > 0, 'the desks are a pick block 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.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');
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 items all show until 640 px and where :has() is understood', async () => {
++ 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-week:has\(#pk-week-0:checked\) \.pick-list>li\[data-i="0"\]\{display:block\}/, 'the rule that shows the picked item');
++ 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');
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 items are hidden on a phone');
++ assert.match(css, /\.pick \.pick-list > li \{ display: none; \}/, 'the desks are hidden on a phone');
});
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 page = r.text;
-- for (const id of ['topics', 'faq', 'shipped']) {
-- assert.ok(page.includes(`<details class="bl-fold bl-fold-block" id="${id}" open>`), `${id} is a fold, open in the page`);
-- assert.ok(page.includes(`document.getElementById("${id}")`), `${id} has the script that closes it on a phone`);
++ for (const id of ['week', 'topics', 'faq', 'shipped']) {
++ assert.ok(page.includes('<details class="bl-fold bl-fold-block" id="' + id + '" open>'), id + ' is a fold, open in the page');
++ assert.ok(page.includes('document.getElementById("' + id + '")'), id + ' has the script that closes it on a phone');
}
-- // The content of the folds stays in the page.
++ assert.match(page, /<h2 id="bh-week-h">This week across OpenVibe<\/h2>/);
assert.match(page, /<h2 id="bh-tags-h">Topics<\/h2>/);
assert.match(page, /<h2 id="bh-faq-h">Questions<\/h2>/);
assert.match(page, /<h2 id="bh-shipped-h">Recently shipped on OpenVibe.Blog<\/h2>/);