openvibers/openvibe.reviews Commits

Home: every pane shows on a phone; the chips are links, not a picker (#181)

OP OpenVibers committed 11 days ago · 4cf953eb41 · 4 files, +26 −56

Changes

ddiff --git a/public/css/reviews.css b/public/css/reviews.css
iindex 3012d24..137a5c5 100644
---- a/public/css/reviews.css
++++ b/public/css/reviews.css
@@@ -197,27 +197,22 @@ main .rv-h-panel-h { margin: 0 0 12px; padding: 0; border: 0; font-size: 1rem; }
}
--/* 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: on a phone every pane is shown, stacked, each with its own heading; the chips are a row of links that
++ jump to a pane (the owner's rule, 2026-10-31: never hide content to shorten). A wide screen shows the panes as
++ before, with no chips. */
++.pick-head, .pick-row { display: none; }
++@media (max-width: 640px) {
++ .pick-bar { margin-top: 30px; }
.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-head h2 { margin: 0 0 4px; font: 700 clamp(20px, 2.4vw, 28px)/1.2 var(--ov-font, 'Inter', system-ui, 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; }
--} }
++ .pick-chip { 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; text-decoration: none; }
++ .pick-chip:hover, .pick-chip:focus-visible { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); }
++ .pick-chip:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
++ .pick .pick-pane { scroll-margin-top: 72px; }
++ .pick .pick-pane + .pick-pane { margin-top: 26px; }
++}
/* 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/pages.js b/server/http/pages.js
iindex ec9f27e..59b9664 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -29,7 +29,6 @@ const seo = require('openvibe-publishing/seo');
const { renderPage, SITE_SUMMARY } = require('../render/layout');
const views = require('../render/views');
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
--const { pickRules } = require('../render/short');
const { ratingForStructuredData } = require('../reviews/aggregate');
const { actorMiddleware, crossSite } = require('./common');
const everywhere = require('../reviews/everywhere');
@@@ -163,7 +162,6 @@ function createPages({ svc, platform, sync, viewers, config, log = console, limi
send(req, res, 200, views.home({ entities, total: all.total, editor: isEditor(req), recent, kinds, stats, sites: everywhere.SITES.length, inspections, hazardous, complaints }), {
robots: 'index, follow', cache: 'public', active: 'home', path: '/', summary: SITE_SUMMARY, styles: [showcase.STYLESHEET], description: HOME_DESCRIPTION,
jsonLd: views.homeJsonLd({ base: config.baseUrl.replace(/\/+$/, ''), entities }),
-- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickRules('records', recordIds)}}}`],
});
}));
// What shipped on OpenVibe.Reviews: the shared update log every OpenVibe site has.
ddiff --git a/server/render/short.js b/server/render/short.js
iindex 5db4998..1b0af15 100644
---- a/server/render/short.js
++++ b/server/render/short.js
@@@ -2,16 +2,14 @@
/**
* The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
*
-- * 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.
-- * pickList() a list of things of the same kind, one at a time on a phone, picked with a row of chips.
++ * pickBlock() several blocks of the same kind, all shown, each with its own heading; on a phone the chips are a
++ * row of links that jump to them (the owner, 2026-10-31: never hide content to shorten).
++ * pickList() a list of things of the same kind under one heading, every item shown.
* foldBlock() a part a reader rarely needs, closed on a phone and open where there is room. The page's own markup
* keeps it open, so a browser without the script shows it as before; the script closes it on a narrow
* screen and remembers the reader's choice.
*
-- * The CSS for both lives in this site's own stylesheet; the rule that shows the picked block is written for the
-- * page, because there is one per block.
++ * The CSS lives in this site's own stylesheet.
*/
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
@@@ -25,43 +23,26 @@ function pickBlock({ key, label, head = '', panes }) {
// A pane with nothing to show is left out, so the chips and the rules that show them match the page.
panes = (panes || []).filter((p) => p.html);
if (!panes.length) return '';
-- const chips = panes.map((p, i) => '<input type="radio" name="pick-' + esc(key) + '" id="pk-' + esc(key) + '-' + esc(p.id) + '" value="' + esc(p.id) + '"' + (i === 0 ? ' checked' : '') + '><label for="pk-' + esc(key) + '-' + esc(p.id) + '">' + esc(p.label) + '</label>').join('');
++ const chips = panes.map((p) => '<a class="pick-chip" href="#' + esc(p.id) + '">' + esc(p.label) + '</a>').join('');
const body = panes.map((p) => '<div class="pick-pane" id="' + esc(p.id) + '">' + p.html + '</div>').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><div class="pick-body">' + body + '</div></div>';
++ return '<div class="pick" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div><nav class="pick-row" aria-label="' + esc(label) + '">' + chips + '</nav></div><div class="pick-body">' + body + '</div></div>';
}
--/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
--function pickRule(key, id) {
-- return '.pick:has(#pk-' + key + '-' + id + ':checked) #' + id + '{display:block}';
--}
--/** The rules for every block of a pick block, in the order they appear. */
--function pickRules(key, ids) {
-- return ids.map((id) => pickRule(key, id)).join('');
--}
/**
-- * 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 pick list: a list of things of the same kind under one heading, every item shown (2026-10-31). The list keeps
++ * its own markup; `labels` is kept for callers and no longer drawn.
*
* pickList({ key, label, head, labels, html }) labels: the chip labels, in the order of the items
*/
function pickList({ key, label, head = '', 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" id="pick-' + esc(key) + '"><div class="pick-bar"><div class="pick-head"><h2>' + esc(head || label) + '</h2></div></div>' + 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('');
--}
/**
* A fold. The head is the heading the block had; the html is what it holds.
@@@ -106,4 +87,4 @@ function foldAround(html, needle, id, head) {
return html.slice(0, p.start) + foldBlock({ id, head, html: p.html }) + html.slice(p.end);
}
--module.exports = { pickBlock, pickRule, pickRules, pickList, pickListRule, pickListRules, foldBlock, foldScript, part, swap, foldAround };
++module.exports = { pickBlock, pickList, foldBlock, foldScript, part, swap, foldAround };
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 55f8576..9be3ed6 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -25,10 +25,10 @@ t('the records are one pick block: a chip per list, the first picked, every list
const start = page.indexOf('<div class="pick" id="pick-records">');
assert.ok(start > 0, 'the pick block is on the home');
const block = page.slice(start, page.indexOf('<details class="rv-h-fold"'));
-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="Public records">/);
-- const chips = [...block.matchAll(/<input type="radio" name="pick-records" id="pk-records-([a-z-]+)" value="([a-z-]+)"( checked)?><label for="pk-records-[a-z-]+">([^<]+)<\/label>/g)];
++ assert.match(block, /<nav class="pick-row" aria-label="Public records">/);
++ const chips = [...block.matchAll(/<a class="pick-chip" href="#(?:pp-records-)?([a-z-]+)">()()([^<]+)<\/a>/g)];
assert.strictEqual(chips.length, 3, `a chip per list (${chips.length})`);
-- assert.deepStrictEqual(chips.filter((m) => m[3]).map((m) => m[1]), [chips[0][1]], 'the first list is picked');
++ assert.ok(!/type="radio"/.test(block), 'no chip is a radio: nothing is picked, every pane shows');
// Every list stays in the page, each in its own pane.
for (const id of chips.map((m) => m[1])) assert.ok(block.includes(`id="${id}"`), `the pane of ${id} is in the page`);
assert.ok(block.includes('The Coffee Club') && block.includes('Eldert Lane') && block.includes('TransUnion'), 'every list is in the page');
@@@ -40,12 +40,8 @@ t('the CSS guard: the chips are hidden and the lists all show until 640 px and w
try {
await importItem(h, nyc({ inspection_date: '2026-10-01T00:00:00.000', violation_code: '06D', violation: 'Food contact surface not washed.', critical_flag: 'Critical' }));
const page = (await req(h, 'GET', '/')).text;
-- assert.match(page, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-records-rv-insp:checked\) #rv-insp\{display:block\}/, 'the rule that shows the picked list');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'reviews.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-pane \{ display: none; \}/, 'the lists are hidden on a phone');
} finally { await h.stop(); }
});