openvibers/openvibe.vision Commits

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

OP OpenVibers committed 11 days ago · 6279cd3571 · 4 files, +27 −56

Changes

ddiff --git a/public/css/vision.css b/public/css/vision.css
iindex 58c3311..127db7e 100644
---- a/public/css/vision.css
++++ b/public/css/vision.css
@@@ -268,27 +268,22 @@ html[data-ov-motion=reduced] .vi-race-img path, html[data-ov-motion=reduced] .vi
.vi-home .sc-room-chips a:hover, .vi-home .sc-room-chips a:focus-visible { color: var(--text-primary, #eef1f6); border-color: var(--room-accent); }
.vi-status { color: var(--text-primary, #eef1f6); background: color-mix(in srgb, var(--text-primary, #eef1f6) 8%, transparent); }
--/* 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/render/short.js b/server/render/short.js
iindex 776c9ee..95d5d7e 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/server/views/index.js b/server/views/index.js
iindex 380ac94..d438fc0 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -8,7 +8,7 @@
const { esc, time } = require('../render/html');
const sc = require('openvibe-shared/showcase');
// The short home (the owner's rule, 2026-10-30): pick blocks and folds.
--const { pickBlock, pickRules, foldAround } = require('../render/short');
++const { pickBlock, foldAround } = require('../render/short');
const MODEL_LABEL = (a) => (a && a.model ? `${a.provider ? `${a.provider}:` : ''}${a.model}` : 'the local engine');
@@@ -105,7 +105,6 @@ ${signedIn ? '' : '<p class="vi-fine">Reading, watching and the page check work
return {
body: `<div class="vi-wrap vi-home" id="vi-home">${html}</div>`,
// The rule that shows the picked scene of the pick block: one per scene, written for this page.
-- inlineStyles: [`@supports selector(:has(*)){@media (max-width:640px){${pickRules('scenes', arts.map((_, i) => `vi-scene-${i}`))}}}`],
};
}
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 0fb72a8..794173d 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -11,10 +11,10 @@ const { boot, check, done } = require('./helpers/boot');
const start = page.indexOf('<div class="pick" id="pick-scenes">');
assert.ok(start > 0, 'the pick block is on the home');
const block = page.slice(start, start + 8000);
-- assert.match(block, /<div class="pick-row" role="radiogroup" aria-label="What Vision does">/);
-- const chips = [...block.matchAll(/<input type="radio" name="pick-scenes" id="pk-scenes-(vi-scene-\d+)" value="(vi-scene-\d+)"( checked)?><label for="pk-scenes-vi-scene-\d+">([^<]+)<\/label>/g)];
++ assert.match(block, /<nav class="pick-row" aria-label="What Vision does">/);
++ const chips = [...block.matchAll(/<a class="pick-chip" href="#(?:pp-scenes-)?(vi-scene-\d+)">()()([^<]+)<\/a>/g)];
assert.strictEqual(chips.length, 4, 'a chip per scene (' + chips.length + ')');
-- assert.deepStrictEqual(chips.filter((m) => m[3]).map((m) => m[1]), [chips[0][1]], 'the first scene is picked');
++ assert.ok(!/type="radio"/.test(block), 'no chip is a radio: nothing is picked, every pane shows');
const scenes = [...block.matchAll(/<article class="sc-scene/g)];
assert.strictEqual(scenes.length, chips.length, 'every scene is in the page');
for (const m of chips) assert.ok(block.includes('id="' + m[1] + '"'), 'the pane of ' + m[1] + ' is in the page');
@@@ -22,12 +22,8 @@ const { boot, check, done } = require('./helpers/boot');
await check('the CSS guard: the chips are hidden and the scenes 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:has\(#pk-scenes-vi-scene-0:checked\) #vi-scene-0\{display:block\}/, 'the rule that shows the picked scene');
const css = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'css', 'vision.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 scenes 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('/');