openvibers/openvibe.media Commits

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

OP OpenVibers committed 11 days ago · 90d8541d7f · 3 files, +20 −35

Changes

ddiff --git a/server/public/browse.js b/server/public/browse.js
iindex 873abe5..a70fedb 100644
---- a/server/public/browse.js
++++ b/server/public/browse.js
@@@ -24,7 +24,7 @@ const cache = require('openvibe-shared/cache-policy');
const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
const kit = require('openvibe-shared/kit');
--const { pickBlock, pickRules, foldBlock } = require('./short');
++const { pickBlock, foldBlock } = require('./short');
const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const abs = (u) => (!u ? null : (/^https?:\/\//i.test(u) ? u : `${config.publicUrl}${u.startsWith('/') ? '' : '/'}${u}`));
@@@ -269,25 +269,19 @@ nav.tabs a .n{opacity:.75;font-size:.78rem;margin-left:.3rem}nav.tabs a.on .n{op
.faq{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:4px 16px;margin:8px 0}
.faq[open]{padding-bottom:12px}.faq summary{cursor:pointer;font-weight:600;padding:8px 0}.faq p{margin:4px 0 0;color:var(--muted)}
@media (pointer: coarse){.faq summary{min-height:44px}}
--/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the home'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-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}
++/* Pick: on a phone every pane is shown, stacked; 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}
.pick-pane{min-width:0}
--@supports selector(:has(*)){@media (max-width:640px){
-- .pick-head{display:block}
-- .pick-head h2{margin:0 0 4px;font-size:1.25rem}
++@media (max-width:640px){
.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(--line);color:var(--muted);font-size:.9rem;font-weight:700;white-space:nowrap;cursor:pointer}
-- .pick-row input:checked+label{color:var(--text);border-color:var(--acc);background:color-mix(in srgb,var(--acc) 12%,transparent)}
-- .pick-row input:focus-visible+label{outline:2px solid var(--acc);outline-offset:2px}
-- .pick-pane{display:none}
-- /* The section's own heading stays (its text comes before it otherwise); the pick block's second heading gives way. */
-- .md-pick-sec .pick-head 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(--line);color:var(--muted);font-size:.9rem;font-weight:700;white-space:nowrap;text-decoration:none}
++ .pick-chip:hover,.pick-chip:focus-visible{color:var(--text);border-color:var(--acc)}
++ .pick-chip:focus-visible{outline:2px solid var(--acc);outline-offset:2px}
++ .pick .pick-pane{scroll-margin-top:72px}
++ .pick .pick-pane+.pick-pane{margin-top:18px}
++}
/* 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. */
.md-fold-block{margin:32px 0}
@@@ -361,7 +355,6 @@ const { url } = await objects.signedUrl(obj.id, { ttl: 300 });` },
label: c.label,
html: `<figure><figcaption>${esc(c.label)}</figcaption><pre tabindex="0" aria-label="${esc(c.label)}"><code class="language-${esc(c.lang)}">${esc(c.code)}</code></pre></figure>`,
}));
-- if (rules) rules.push(pickRules('api', panes.map((p) => p.id)));
const api = `<section class="sc-sec md-pick-sec" id="api" aria-labelledby="api-h"><h2 id="api-h">From your app</h2>
<p class="sc-lede">Public media needs no key. Your own objects take a project token or a Zone key.</p>
${pickBlock({ key: 'api', label: 'The ways to call Media', head: 'From your app', bodyClass: 'sc-code', panes })}
ddiff --git a/server/public/short.js b/server/public/short.js
iindex 2758a8a..b867551 100644
---- a/server/public/short.js
++++ b/server/public/short.js
@@@ -22,16 +22,13 @@ const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</
*/
function pickBlock({ key, label, head = '', headId = '', bodyClass = '', panes }) {
if (!panes || !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="#pp-${esc(key)}-${esc(p.id)}">${esc(p.label)}</a>`).join('');
const body = panes.map((p) => `<div class="pick-pane" id="pp-${esc(key)}-${esc(p.id)}">${p.html}</div>`).join('');
const heading = `<h2${headId ? ` id="${esc(headId)}"` : ''}>${esc(head || label)}</h2>`;
-- return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar"><div class="pick-head">${heading}</div><div class="pick-row" role="radiogroup" aria-label="${esc(label)}">${chips}</div></div><div class="pick-body${bodyClass ? ` ${bodyClass}` : ''}">${body}</div></div>`;
++ return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar"><div class="pick-head">${heading}</div><nav class="pick-row" aria-label="${esc(label)}">${chips}</nav></div><div class="pick-body${bodyClass ? ` ${bodyClass}` : ''}">${body}</div></div>`;
}
/** The rules for every pane of a pick block, in the order they appear: one rule per pane, so the CSS is written for the page. */
--function pickRules(key, ids) {
-- return ids.map((id) => `.pick:has(#pk-${key}-${id}:checked) #pp-${key}-${id}{display:block}`).join('');
--}
/**
* A fold. The head is the heading the block had; the html is what it holds.
@@@ -47,4 +44,4 @@ function foldScript(id) {
return `<script>(function(){var d=document.getElementById(${JSON.stringify(id)});if(!d)return;var k='ov_fold_'+${JSON.stringify(id)};var s=null;try{s=localStorage.getItem(k)}catch(e){}if(s==='open'){d.open=true;return}if(s==='closed'){d.open=false;return}var t=0;if(window.matchMedia&&window.matchMedia('(max-width:640px)').matches){t=Date.now();d.open=false}d.addEventListener('toggle',function(){if(Date.now()-t<300)return;try{localStorage.setItem(k,d.open?'open':'closed')}catch(e){}})})();</script>`;
}
--module.exports = { pickBlock, pickRules, foldBlock, foldScript };
++module.exports = { pickBlock, foldBlock, foldScript };
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex e61227b..f44ddd4 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -1,6 +1,5 @@
'use strict';
// The home on a phone (the owner's short-pages rule, 2026-10-30): the code samples 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 block stays in the page for crawlers; a wider screen shows them all and no chips.
const assert = require('assert');
const fs = require('fs');
@@@ -37,17 +36,15 @@ const path = require('path');
// The code samples: one pick block, a chip per sample, the first one picked.
assert.ok(text.includes('<div class="pick" id="pick-api">'), 'the code samples are one pick block');
-- assert.match(text, /<div class="pick-row" role="radiogroup" aria-label="The ways to call Media">/);
-- const chips = [...text.matchAll(/<input type="radio" name="pick-api" id="pk-api-([a-z0-9-]+)" value="\1"( checked)?><label for="pk-api-\1">([^<]+)<\/label>/g)];
++ assert.match(text, /<nav class="pick-row" aria-label="The ways to call Media">/);
++ const chips = [...text.matchAll(/<a class="pick-chip" href="#(?:pp-api-)?([a-z0-9-]+)">()([^<]+)<\/a>/g)];
assert.deepStrictEqual(chips.map((m) => m[1]), ['thumbnail', 'node', 's3'], 'a chip per sample');
-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), ['thumbnail'], 'the first sample is picked');
++ assert.ok(!/type="radio"/.test(text), 'no chip is a radio: nothing is picked, every pane shows');
assert.strictEqual((text.match(/class="pick-pane"/g) || []).length, chips.length, 'one pane per chip');
// Every sample stays in the page for the crawlers.
for (const label of ['Thumbnail', 'Node (openvibe-sdk)', 'S3 (any tool)']) {
assert.ok(text.includes(`<figcaption>${label}</figcaption>`), `${label} is in the page`);
}
-- // The rule that shows the picked sample is written for this page.
-- assert.ok(text.includes('.pick:has(#pk-api-thumbnail:checked) #pp-api-thumbnail{display:block}'), 'the picked sample shows');
// The folds: open in the page, closed on a phone, and their content stays in the page.
for (const id of ['what', 'limits', 'questions', 'shipped']) {
@@@ -65,12 +62,10 @@ const path = require('path');
assert.ok(text.includes('class="sc-sec sc-media"'), 'the newest media rail is still there');
assert.ok(text.includes('id="api"'), 'the code samples are still there, under their own anchor');
-- // The CSS guard: the chips are hidden and the blocks all show until 640 px and where :has() is understood.
const css = fs.readFileSync(path.join(__dirname, '..', 'server', 'public', 'browse.js'), '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-pane\{display:none\}/, 'the blocks are hidden on a phone');
++ assert.match(css, /\.pick-head,\.pick-row\{display:none\}/, 'no chips where there is room');
++ assert.doesNotMatch(css, /\.pick-pane\{display:none\}/, 'no pane is ever hidden (the owner, 2026-10-31)');
++ assert.match(css, /\.pick-chip\{/, 'the chips are links');
console.log('home short: all checks passed');
} finally {