openvibers/openvibe.community Commits

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

OP OpenVibers committed 11 days ago · d63e7ac1f5 · 4 files, +17 −43

Changes

ddiff --git a/public/css/community.css b/public/css/community.css
iindex b9b641a..21c85bb 100644
---- a/public/css/community.css
++++ b/public/css/community.css
@@@ -407,29 +407,22 @@ button.rx:hover { border-color: var(--accent); }
/* The phone home (2026-10-30, the owner's short-pages rule): the roadmap and the pulse are chips, one shelf at a
time, and the questions are a fold. Both only work under 640 px, so a wider screen shows both shelves and no chips. */
--/* 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. */
@@@ -451,7 +444,6 @@ button.rx:hover { border-color: var(--accent); }
:where(.sc-sec, .cm-sec, .hm-sec, .section) > .hm-fold-block { margin: 0; }
/* The chip names the shelf, so the shelf's own heading gives way to it; the link to the whole list stays. */
@supports selector(:has(*)) { @media (max-width: 640px) {
-- .pick .pick-pane .ovk-rail-head h2 { display: none; }
.pick .pick-pane .ovk-rail-head { justify-content: flex-end; }
} }
/* The fold holds the questions, so its list gets the room the card gives it. */
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 3d7eebb..039dd05 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -14,7 +14,7 @@ const showcase = require('openvibe-shared/showcase');
const kit = require('openvibe-shared/kit');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { renderMarkdown } = require('./markdown');
--const { pickBlock, pickRules, foldBlock } = require('./short');
++const { pickBlock, foldBlock } = require('./short');
// Languages read as prose: their lines wrap by default (the Wrap button still toggles), so a phone shows the whole text.
const PROSE = new Set(['markdown', 'plaintext', 'text']);
@@@ -275,11 +275,6 @@ function pickWhatIsHappening(roadmap, activity) {
return pickBlock({ key: 'happening', label: 'What is happening', head: 'What is happening', panes, bodyClass: 'cm-pair' });
}
--/** The rules that show the picked shelf of the home's pick block: one per shelf, written for this page. */
--function happeningRules() {
-- const ids = ['shelf-roadmap', 'shelf-pulse'];
-- return `@supports selector(:has(*)){@media (max-width:640px){${pickRules('happening', ids)}}}`;
--}
function homePage({ latest, fromStreams = [], languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {
const body = showcase.hero({
@@@ -314,7 +309,6 @@ ${homeFaq()}
// The kit's controls (kit.css) and its cards and rails (room.css, after it); kit-web.js comes with community.js.
styles: [showcase.STYLESHEET, 'kit.css', 'room.css'],
// The rule that shows the picked shelf: one per shelf, so the CSS is written for this page.
-- inlineStyles: [happeningRules()],
bodyClass: 'cm-home',
body,
});
ddiff --git a/server/render/short.js b/server/render/short.js
iindex d201b02..07bcabc 100644
---- a/server/render/short.js
++++ b/server/render/short.js
@@@ -25,23 +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('');
++ const chips = panes.map((p) => `<a class="pick-chip" href="#${esc(p.id)}">${esc(p.label)}</a>`).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).
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. */
--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 fold. The head is the heading the block had; the html is what it holds.
@@@ -83,4 +75,4 @@ function foldSection(html, needle, id) {
return html.slice(0, start) + openTag + fold + html.slice(close);
}
--module.exports = { pickBlock, pickRule, pickRules, foldBlock, foldScript, foldSection };
++module.exports = { pickBlock, foldBlock, foldScript, foldSection };
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 1e56cbc..403661c 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -22,9 +22,9 @@ const { boot, check, done } = require('./helpers/app');
const page = r.text;
assert.ok(page.includes('<div class="pick" id="pick-happening">'), 'the pick block is on the home');
const block = page.slice(page.indexOf('<div class="pick" id="pick-happening">'));
-- const chips = [...block.matchAll(/<input type="radio" name="pick-happening" id="pk-happening-([a-z-]+)" value="\1"( checked)?><label for="pk-happening-\1">([^<]+)<\/label>/g)];
++ const chips = [...block.matchAll(/<a class="pick-chip" href="#(?:pp-happening-)?([a-z-]+)">()([^<]+)<\/a>/g)];
assert.deepStrictEqual(chips.map((m) => m[1]), ['shelf-roadmap', 'shelf-pulse'], 'a chip per shelf');
-- assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), ['shelf-roadmap'], 'the first shelf is picked');
++ assert.ok(!/type="radio"/.test(block), 'no chip is a radio: nothing is picked, every pane shows');
const panes = [...block.matchAll(/<div class="pick-pane" id="([a-z-]+)">/g)].map((m) => m[1]);
assert.deepStrictEqual(panes, ['shelf-roadmap', 'shelf-pulse'], 'every shelf is in the page, one per chip');
// Nothing was removed: both shelves are still there.
@@@ -35,12 +35,8 @@ const { boot, check, done } = require('./helpers/app');
await check('the CSS guard: the chips are hidden and both shelves show until 640 px and where :has() is understood', async () => {
const r = await t.get('/');
-- assert.match(r.text, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-happening-shelf-roadmap:checked\) #shelf-roadmap\{display:block\}/, 'the rule that shows the picked shelf');
const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'community.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 shelves are hidden on a phone');
});
await check('the questions are a fold, open in the page and closed on a phone', async () => {