openvibers/openvibe.surf Commits

Every scene shows on a phone; the chips jump to them (#67)

OP OpenVibers committed 10 days ago · 4ec48a7bbd · 4 files, +32 −44

The owner's rule of 10-31: never hide content to shorten a page. On a phone the home showed one of its three scenes
behind radio chips and hid the other two. All three are now shown, stacked, and the chips are links that jump to
each, as on Food.

Changes

ddiff --git a/public/css/surf.css b/public/css/surf.css
iindex 711352e..7d2aed9 100644
---- a/public/css/surf.css
++++ b/public/css/surf.css
@@@ -262,29 +262,22 @@ html[data-ov-motion=reduced] .sf-desk-page[data-sf-desk-open="1"] .sf-desk-state
/* The phone home (2026-10-30, the owner's short-pages rule): the three doors are one sideways row, the scenes are
chips, one at a time, and the questions are a fold. Everything stays in the page for the crawlers. */
--/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page's blocks of the same kind are one at a
-- time, picked with a row of chips. Radio inputs and :has(), no script: the chips and the one-pane rule live under
-- 640 px and only where :has() is understood, so a wider screen, or an older browser, shows every pane and no chips.
-- Every pane stays in the page for crawlers. */
--.pick-head { display: none; }
--.pick-row { display: none; }
--@supports selector(:has(*)) { @media (max-width: 640px) {
++/* 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', 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; }
-- /* A link that points at a pane opens that pane on a phone too. */
-- .pick .pick-pane:target { display: block !important; }
-- .pick:has(.pick-pane:target) .pick-pane:not(:target) { display: none !important; }
-- /* The chip names the pane, so the pane's own heading gives way to it; the link to more stays. */
-- .pick .pick-pane > h2, .pick .pick-pane > h3 { 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/views/index.js b/server/views/index.js
iindex bd1352f..5b11f6f 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -86,13 +86,11 @@ function home() {
});
const at = room.indexOf('<section class="sc-room-scenes"');
let body = at < 0 ? room.replace(/<\/div>$/, `${tiles}</div>`) : room.slice(0, at) + tiles + room.slice(at);
-- // The home on a phone (the owner's short-pages rule, 2026-10-30): the three scenes are chips, one at a time, and
-- // the questions are a fold. Both stay in the page for the crawlers.
++ // The home on a phone (the owner, 2026-10-31: never hide content to shorten a page): the three scenes are all shown,
++ // the chips jump to them, and the questions are a counted fold.
body = S.pickScenes(body, { key: 'scenes', label: 'What you can do', head: 'What you can do', labels: ['Ride maps', 'Surf the web', 'Watch'] });
body = S.foldSection(body, 'id="sc-questions"', 'questions-fold');
-- // The rule that shows the picked scene: one per scene, so the CSS is written for this page.
-- const rules = `@supports selector(:has(*)){@media (max-width:640px){${S.pickRules('scenes', ['scene-0', 'scene-1', 'scene-2'])}}}`;
-- return layout({ title: null, path: '/', body: `<div class="sf-wrap">${body}</div>`, active: '/', jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Surf', url: 'https://openvibe.surf/', description: 'Ride CS-style surf maps in the browser, surf what is live on OpenVibe.Live, and let an agent browse the web for you.' }), seo.jsonLd.organization()], inlineStyles: [rules] });
++ return layout({ title: null, path: '/', body: `<div class="sf-wrap">${body}</div>`, active: '/', jsonLd: [seo.jsonLd.website({ name: 'OpenVibe.Surf', url: 'https://openvibe.surf/', description: 'Ride CS-style surf maps in the browser, surf what is live on OpenVibe.Live, and let an agent browse the web for you.' }), seo.jsonLd.organization()] });
}
function mapCard(m) {
ddiff --git a/server/views/short.js b/server/views/short.js
iindex d457bd4..d444539 100644
---- a/server/views/short.js
++++ b/server/views/short.js
@@@ -25,15 +25,15 @@ function pickBlock({ key, label, head = '', panes, bodyClass = '' }) {
// 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('');
-- // The pane carries the id the page already links to (an anchor in the hero, for instance), so a link to it still
-- // opens that pane on a phone (the CSS shows the pane a link points at).
++ // Every pane is shown (the owner, 2026-10-31: never hide content to shorten a page); the chips are links that jump
++ // to a pane. They used to be radio chips that showed one pane and hid the others.
++ 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('');
-- const bar = `${head ? `<div class="pick-head"><h2>${esc(head)}</h2></div>` : ''}<div class="pick-row" role="radiogroup" aria-label="${esc(label)}">${chips}</div>`;
++ const bar = `${head ? `<div class="pick-head"><h2>${esc(head)}</h2></div>` : ''}<nav class="pick-row" aria-label="${esc(label)}">${chips}</nav>`;
return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar">${bar}</div><div class="pick-body${bodyClass ? ' ' + esc(bodyClass) : ''}">${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. */
++/** No longer needed (every pane shows): kept so a caller that still asks for the rules gets none. */
function pickRule(key, id) {
return `.pick:has(#pk-${key}-${id}:checked) #${id}{display:block}`;
}
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 0120aba..96f313b 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -1,8 +1,8 @@
'use strict';
/**
-- * The home on a phone (the owner's short-pages rule, 2026-10-30): the three scenes are a pick block, a row of chips
-- * picks one of them, and the questions are a fold. Radio inputs and :has(), no script for the chips; every scene stays
-- * in the page for crawlers; a wider screen shows them all and no chips.
++ * The home on a phone (the owner, 2026-10-31: never hide content to shorten a page): the three scenes are all shown,
++ * a row of chips links to each, and the questions are a counted fold. The chips used to pick one scene and hide the
++ * others.
*/
const assert = require('assert');
const fs = require('fs');
@@@ -12,31 +12,28 @@ const { boot, check, done } = require('./helpers/boot');
(async () => {
const t = await boot();
try {
-- await check('the scenes are chips: one per scene, the first picked, every scene in the page', async () => {
++ await check('every scene is shown; the chips are links that jump to one (the owner, 2026-10-31)', async () => {
const r = await t.get('/');
assert.strictEqual(r.status, 200, r.text);
const page = r.text;
-- assert.ok(page.includes('<div class="pick" id="pick-scenes">'), 'the pick block is on the home');
++ assert.ok(page.includes('<div class="pick" id="pick-scenes">'), 'the scenes block is on the home');
const block = page.slice(page.indexOf('<div class="pick" id="pick-scenes">'));
-- const chips = [...block.matchAll(/<input type="radio" name="pick-scenes" id="pk-scenes-(scene-\d+)" value="\1"( checked)?><label for="pk-scenes-\1">([^<]+)<\/label>/g)];
-- assert.deepStrictEqual(chips.map((m) => m[1]), ['scene-0', 'scene-1', 'scene-2'], 'a chip per scene');
-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), ['scene-0'], 'the first scene is picked');
++ assert.ok(!/type="radio"/.test(block), 'no radio chips');
++ const chips = [...block.matchAll(/<a class="pick-chip" href="#(scene-\d+)">([^<]+)<\/a>/g)];
++ assert.deepStrictEqual(chips.map((m) => m[1]), ['scene-0', 'scene-1', 'scene-2'], 'a link per scene');
const panes = [...block.matchAll(/<div class="pick-pane" id="scene-(\d+)">/g)].map((m) => m[1]);
-- assert.deepStrictEqual(panes, ['0', '1', '2'], 'every scene is in the page, one per chip');
-- // Nothing was removed: the scenes and the doors are still there.
++ assert.deepStrictEqual(panes, ['0', '1', '2'], 'every scene is in the page');
for (const title of ['Press play, you are on the ramp', 'Surf the web with Vibe', 'Lean back and surf what is on', 'Ride', 'Web', 'Watch']) {
assert.ok(page.includes(title), `${title} is on the page`);
}
});
-- await check('the CSS guard: the chips are hidden and every scene shows until 640 px and where :has() is understood', async () => {
++ await check('the CSS: on a phone nothing is hidden; the chips are a row of 44 px links', async () => {
const r = await t.get('/');
-- assert.match(r.text, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-scenes-scene-0:checked\) #scene-0\{display:block\}/, 'the rule that shows the picked scene');
++ assert.ok(!/\.pick:has\(#pk-scenes/.test(r.text), 'no per-scene rule in the page');
const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'surf.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');
++ assert.ok(!/pick-pane \{ display: none/.test(css), 'no scene is hidden');
++ assert.match(css, /\.pick-chip \{[^}]*min-height: 44px/, 'a chip is a 44 px target');
assert.match(css, /\.sf-tiles \{ display: flex; overflow-x: auto;/, 'the three doors are one sideways row');
});