openvibers/openvibe.community Commits
The home on phones: the roadmap and the pulse are chips, the questions a fold (#191)
Changes
@@@ -390,3 +390,55 @@ button.rx:hover { border-color: var(--accent); }.cm-faq summary { min-height: 44px; box-sizing: border-box; cursor: pointer; padding: 12px 16px; font-weight: 700; }.cm-faq details[open] summary { border-bottom: 1px solid var(--border); }.cm-faq p { margin: 0; padding: 12px 16px 14px; color: var(--text-secondary); line-height: 1.6; }++++/* 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-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; }++} }++++/* 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. */++.hm-fold-block { margin-top: 32px; }++.hm-fold-block > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 44px; cursor: pointer; list-style: none; }++.hm-fold-block > summary::-webkit-details-marker { display: none; }++.hm-fold-block > summary h2 { margin: 0; font: 700 clamp(19px, 2.2vw, 26px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }++.hm-fold-block > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; white-space: nowrap; }++.hm-fold-block > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++@media (min-width: 641px) {++ .hm-fold-block { border: 0; background: none; border-radius: 0; margin: 56px auto 0; padding: 0; }++ .hm-fold-block > summary { display: block; min-height: 0; }++ .hm-fold-block > summary h2 { margin: 0 0 6px; font: 700 clamp(22px, 2.6vw, 30px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }++ .hm-fold-block > summary span { display: none; }++ .hm-fold-block .hm-sec, .hm-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }++}++@media (max-width: 640px) { .hm-fold-block { margin-top: 24px; } .hm-fold-block > summary h2 { font-size: 1.05rem; } }++/* A fold that sits inside the section it replaces adds no space of its own. */++: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. */++.hm-fold-block .cm-faq { margin: 0; padding: 0 16px 16px; }@@@ -141,6 +141,7 @@ function renderPage(o) {appIcon.headTags({ site: 'community', iconBase: '/assets' }),...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),++ ...(o.inlineStyles || []).map((css) => `<style>${css}</style>`),`<link rel="stylesheet" href="${asset('css/community.css')}">`,'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',`<script src="${asset('js/community.js')}" defer></script>`,@@@ -14,6 +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');// 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']);@@@ -247,10 +248,31 @@ const { url } = await res.json(); // "/p/<slug>"` })}`,}function homeFaq() {-- return `<section class="cm-sec" id="questions" aria-labelledby="h-faq">-- <h2 id="h-faq" class="cm-h">Questions</h2>-- <div class="cm-faq">${HOME_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</div>--</section>`;++ // The questions are a fold on a phone (the owner's short-pages rule, 2026-10-30): they stay in the page for the++ // crawlers, and the script closes them where the reader has room for more.++ return `<section class="cm-sec" id="questions" aria-labelledby="h-faq">${foldBlock({++ id: 'faq-fold',++ head: '<h2 id="h-faq" class="cm-h">Questions</h2>',++ html: `<div class="cm-faq">${HOME_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</div>`,++ })}</section>`;++}++++/**++ * The roadmap and the pulse, one at a time on a phone (the owner's short-pages rule, 2026-10-30): the chips pick one++ * of the two shelves, and both stay in the page for the crawlers. On a wide screen they stay side by side.++ */++function pickWhatIsHappening(roadmap, activity) {++ const panes = [++ { id: 'shelf-roadmap', label: 'Roadmap', html: roadmapShelf(roadmap) },++ { id: 'shelf-pulse', label: 'Across OpenVibe', html: pulseShelf(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 = {} }) {@@@ -268,7 +290,7 @@ function homePage({ latest, fromStreams = [], languages, user, spaces = [], road${latestShelf(latest, languages)}${streamsShelf(fromStreams)}${forumShelf(spaces)}--<div class="cm-pair">${roadmapShelf(roadmap)}${pulseShelf(activity)}</div>++${pickWhatIsHappening(roadmap, activity)}<div class="cm-sec">${apiFold()}</div>${homeFaq()}<section class="section">${frame.shipped({ service: 'community', title: 'Recently shipped on OpenVibe.Community' })}</section>`;@@@ -285,6 +307,8 @@ ${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,});nnew file mode 100644@@@ -0,0 +1,86 @@++'use strict';++/**++ * 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.++ * 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 the site's own stylesheet; the rule that shows the picked block is written for the++ * page, because there is one per block.++ */++const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');++++/**++ * A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the++ * heading gives way to the chip that names it.++ *++ * pickBlock({ key, label, head, panes }) head: the heading of the block, or '' when the block already has one.++ * panes: [{ id, label, html }]++ */++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).++ 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>`;++ 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.++ *++ * foldBlock({ id, head, html })++ */++function foldBlock({ id, head, html }) {++ return `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${html}</details>${foldScript(id)}`;++}++++/** The script a fold carries: closed on a phone unless the reader opened it before, and it remembers that. */++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>`;++}++++/**++ * A section of the page's own markup as a fold. The section stays where it was; its own heading becomes the fold's++ * heading, and the fold is closed on a phone (the script) and open where there is room.++ *++ * foldSection(html, needle, id) needle: a mark of the part to fold (a class, for instance)++ */++function foldSection(html, needle, id) {++ const at = html.indexOf(needle);++ if (at < 0) return html;++ const start = html.lastIndexOf('<section', at);++ const close = html.indexOf('</section>', at);++ if (start < 0 || close < 0) return html;++ const openEnd = html.indexOf('>', start) + 1;++ const openTag = html.slice(start, openEnd);++ let inner = html.slice(openEnd, close);++ const h2 = /<h2[^>]*>[\s\S]*?<\/h2>/.exec(inner);++ if (!h2) return html;++ const head = h2[0];++ inner = inner.slice(h2.index + head.length);++ // The lede under the heading goes with it: the fold's heading says enough on a phone.++ const lede = /<p class="sc-lede"[^>]*>[\s\S]*?<\/p>/.exec(inner);++ if (lede) inner = inner.slice(0, lede.index) + inner.slice(lede.index + lede[0].length);++ const fold = `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${inner.trim()}</details>${foldScript(id)}`;++ return html.slice(0, start) + openTag + fold + html.slice(close);++}++++module.exports = { pickBlock, pickRule, pickRules, foldBlock, foldScript, foldSection };nnew file mode 100644@@@ -0,0 +1,58 @@++'use strict';++/**++ * The home on a phone (the owner's short-pages rule, 2026-10-30): the roadmap and the pulse are a pick block, a row++ * of chips picks one of the two shelves, and the questions are a fold. Radio inputs and :has(), no script for the++ * chips; every shelf stays in the page for crawlers; a wider screen shows both shelves and no chips.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++ try {++ // A roadmap item and a pulse item, so the home has something to show in its pick block.++ await t.db.prepare("INSERT INTO threads (space_id, slug, title, status, last_activity_at) VALUES (4, 'test-item', 'A test item', 'in_progress', ov_now())").run();++ await t.db.prepare("INSERT INTO pulse_items (source_service, source_type, source_id, title, url, origin, visibility, occurred_at) VALUES ('network', 'notice', 'n1', 'Something happened', 'https://openvibe.network/', 'system', 'public', ov_now())").run();++++ await check('the roadmap and the pulse are chips: one per shelf, the first picked, every shelf in the page', 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-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)];++ 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');++ 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.++ for (const title of ['On the roadmap', 'A test item', 'Across OpenVibe', 'Recently shipped on OpenVibe.Community']) {++ assert.ok(page.includes(title), `${title} is on the page`);++ }++ });++++ 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 () => {++ const r = await t.get('/');++ assert.ok(r.text.includes('<details class="hm-fold hm-fold-block" id="faq-fold" open>'), 'the questions are a fold, open in the page');++ assert.ok(r.text.includes('document.getElementById("faq-fold")'), 'the fold has the script that closes it on a phone');++ // The questions stay in the page.++ assert.ok(r.text.includes('id="questions"'));++ assert.match(r.text, /<div class="cm-faq">/);++ });++ } finally {++ await t.close();++ }++ done();++})().catch((e) => { console.error(e); process.exit(1); });@@@ -10,7 +10,7 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 41, // 39.4 the short home (2026-10-29: rails, a launcher and a fold in place of three grids, What lives here, Part of OpenVibe and the closing call), lowered back from 42; 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0++ htmlRawKB: 42, // 41.9 the phone home (2026-10-30: the roadmap and the pulse become chips and the questions a fold); 39.4 the short home (2026-10-29: rails, a launcher and a fold in place of three grids, What lives here, Part of OpenVibe and the closing call), lowered back from 42; 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0// 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six// questions with their FAQPage JSON-LD; was 26 at 22.0htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7@@@ -18,7 +18,7 @@ const BUDGETS = {jsRawKB: 336, // 335.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 332.4 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 329.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 322 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 314 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 306.5 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 300.7 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 286.6 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 276.8 with shared 4.11.0's seasonal navbar; 267.5 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 251.3 (was 208.8 before the shell's web-runtime.js)jsBrotliKB: 84, // 83.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.3 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 81.7 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 80.2 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 78.1 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 74.4 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 71.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 69.1 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 65.5 with shared 4.11.0's seasonal navbar; 59.9 (was 48.9)cssFiles: 5, // 5 the short home: + kit.css (numbers, lists, chips, the fold) and room.css (cards and rails), the shared kit's look; 3-- cssRawKB: 106, // 104.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 102.6 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 97.3 the short home (2026-10-29): kit.css 25.6 KB + room.css 15.7 KB, while community.css sheds the old shelves' rules and the page 10 of its 15 phone screens; 54.9 openvibe-shared 4.26.1 (hero visual); 47.2 with Shared 4.13.0 (2026-10-26); 40.6 on 10-22 (the home shelves' styles in community.css); was 40 at 36.5 with the showcase home++ cssRawKB: 110, // 109.0 the phone home (2026-10-30: the chips and the fold rules); 104.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 102.6 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 97.3 the short home (2026-10-29): kit.css 25.6 KB + room.css 15.7 KB, while community.css sheds the old shelves' rules and the page 10 of its 15 phone screens; 54.9 openvibe-shared 4.26.1 (hero visual); 47.2 with Shared 4.13.0 (2026-10-26); 40.6 on 10-22 (the home shelves' styles in community.css); was 40 at 36.5 with the showcase homecssBrotliKB: 23, // 22.1 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 21.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 20.4 the short home (kit.css + room.css, as above; was 11.5 once Kit 2 had its own files in openvibe-shared v4.37.0); 9.5 with Shared 4.13.0 (2026-10-26); 7.7 // raised 11 -> 12 for the openvibe-shared Kit 2 pin (kit.css grew)externalFiles: 2, // 1};
@@ -390,3 +390,55 @@ button.rx:hover { border-color: var(--accent); }
@@ -390,3 +390,55 @@ button.rx:hover { border-color: var(--accent); }
.cm-faq summary { min-height: 44px; box-sizing: border-box; cursor: pointer; padding: 12px 16px; font-weight: 700; }
.cm-faq summary { min-height: 44px; box-sizing: border-box; cursor: pointer; padding: 12px 16px; font-weight: 700; }
.cm-faq details[open] summary { border-bottom: 1px solid var(--border); }
.cm-faq details[open] summary { border-bottom: 1px solid var(--border); }
.cm-faq p { margin: 0; padding: 12px 16px 14px; color: var(--text-secondary); line-height: 1.6; }
.cm-faq p { margin: 0; padding: 12px 16px 14px; color: var(--text-secondary); line-height: 1.6; }
+
+/* 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-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; }
+} }
+
+/* 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. */
+.hm-fold-block { margin-top: 32px; }
+.hm-fold-block > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 44px; cursor: pointer; list-style: none; }
+.hm-fold-block > summary::-webkit-details-marker { display: none; }
+.hm-fold-block > summary h2 { margin: 0; font: 700 clamp(19px, 2.2vw, 26px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }
+.hm-fold-block > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; white-space: nowrap; }
+.hm-fold-block > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+@media (min-width: 641px) {
+ .hm-fold-block { border: 0; background: none; border-radius: 0; margin: 56px auto 0; padding: 0; }
+ .hm-fold-block > summary { display: block; min-height: 0; }
+ .hm-fold-block > summary h2 { margin: 0 0 6px; font: 700 clamp(22px, 2.6vw, 30px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }
+ .hm-fold-block > summary span { display: none; }
+ .hm-fold-block .hm-sec, .hm-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
+}
+@media (max-width: 640px) { .hm-fold-block { margin-top: 24px; } .hm-fold-block > summary h2 { font-size: 1.05rem; } }
+/* A fold that sits inside the section it replaces adds no space of its own. */
+: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. */
+.hm-fold-block .cm-faq { margin: 0; padding: 0 16px 16px; }
@@ -141,6 +141,7 @@ function renderPage(o) {
@@ -141,6 +141,7 @@ function renderPage(o) {
appIcon.headTags({ site: 'community', iconBase: '/assets' }),
appIcon.headTags({ site: 'community', iconBase: '/assets' }),
...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),
...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
+ ...(o.inlineStyles || []).map((css) => `<style>${css}</style>`),
`<link rel="stylesheet" href="${asset('css/community.css')}">`,
`<link rel="stylesheet" href="${asset('css/community.css')}">`,
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',
`<script src="${asset('js/community.js')}" defer></script>`,
`<script src="${asset('js/community.js')}" defer></script>`,
@@ -14,6 +14,7 @@ const showcase = require('openvibe-shared/showcase');
@@ -14,6 +14,7 @@ const showcase = require('openvibe-shared/showcase');
const kit = require('openvibe-shared/kit');
const kit = require('openvibe-shared/kit');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { renderMarkdown } = require('./markdown');
const { renderMarkdown } = require('./markdown');
+const { pickBlock, pickRules, foldBlock } = require('./short');
// Languages read as prose: their lines wrap by default (the Wrap button still toggles), so a phone shows the whole text.
// 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']);
const PROSE = new Set(['markdown', 'plaintext', 'text']);
@@ -247,10 +248,31 @@ const { url } = await res.json(); // "/p/<slug>"` })}`,
@@ -247,10 +248,31 @@ const { url } = await res.json(); // "/p/<slug>"` })}`,
}
}
function homeFaq() {
function homeFaq() {
- return `<section class="cm-sec" id="questions" aria-labelledby="h-faq">
- <h2 id="h-faq" class="cm-h">Questions</h2>
- <div class="cm-faq">${HOME_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</div>
-</section>`;
+ // The questions are a fold on a phone (the owner's short-pages rule, 2026-10-30): they stay in the page for the
+ // crawlers, and the script closes them where the reader has room for more.
+ return `<section class="cm-sec" id="questions" aria-labelledby="h-faq">${foldBlock({
+ id: 'faq-fold',
+ head: '<h2 id="h-faq" class="cm-h">Questions</h2>',
+ html: `<div class="cm-faq">${HOME_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</div>`,
+ })}</section>`;
+}
+
+/**
+ * The roadmap and the pulse, one at a time on a phone (the owner's short-pages rule, 2026-10-30): the chips pick one
+ * of the two shelves, and both stay in the page for the crawlers. On a wide screen they stay side by side.
+ */
+function pickWhatIsHappening(roadmap, activity) {
+ const panes = [
+ { id: 'shelf-roadmap', label: 'Roadmap', html: roadmapShelf(roadmap) },
+ { id: 'shelf-pulse', label: 'Across OpenVibe', html: pulseShelf(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 = {} }) {
function homePage({ latest, fromStreams = [], languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {
@@ -268,7 +290,7 @@ function homePage({ latest, fromStreams = [], languages, user, spaces = [], road
@@ -268,7 +290,7 @@ function homePage({ latest, fromStreams = [], languages, user, spaces = [], road
${latestShelf(latest, languages)}
${latestShelf(latest, languages)}
${streamsShelf(fromStreams)}
${streamsShelf(fromStreams)}
${forumShelf(spaces)}
${forumShelf(spaces)}
-<div class="cm-pair">${roadmapShelf(roadmap)}${pulseShelf(activity)}</div>
+${pickWhatIsHappening(roadmap, activity)}
<div class="cm-sec">${apiFold()}</div>
<div class="cm-sec">${apiFold()}</div>
${homeFaq()}
${homeFaq()}
<section class="section">${frame.shipped({ service: 'community', title: 'Recently shipped on OpenVibe.Community' })}</section>`;
<section class="section">${frame.shipped({ service: 'community', title: 'Recently shipped on OpenVibe.Community' })}</section>`;
@@ -285,6 +307,8 @@ ${homeFaq()}
@@ -285,6 +307,8 @@ ${homeFaq()}
],
],
// The kit's controls (kit.css) and its cards and rails (room.css, after it); kit-web.js comes with community.js.
// 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'],
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',
bodyClass: 'cm-home',
body,
body,
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
+'use strict';
+/**
+ * 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.
+ * 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 the site's own stylesheet; the rule that shows the picked block is written for the
+ * page, because there is one per block.
+ */
+const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
+
+/**
+ * A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the
+ * heading gives way to the chip that names it.
+ *
+ * pickBlock({ key, label, head, panes }) head: the heading of the block, or '' when the block already has one.
+ * panes: [{ id, label, html }]
+ */
+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).
+ 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>`;
+ 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.
+ *
+ * foldBlock({ id, head, html })
+ */
+function foldBlock({ id, head, html }) {
+ return `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${html}</details>${foldScript(id)}`;
+}
+
+/** The script a fold carries: closed on a phone unless the reader opened it before, and it remembers that. */
+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>`;
+}
+
+/**
+ * A section of the page's own markup as a fold. The section stays where it was; its own heading becomes the fold's
+ * heading, and the fold is closed on a phone (the script) and open where there is room.
+ *
+ * foldSection(html, needle, id) needle: a mark of the part to fold (a class, for instance)
+ */
+function foldSection(html, needle, id) {
+ const at = html.indexOf(needle);
+ if (at < 0) return html;
+ const start = html.lastIndexOf('<section', at);
+ const close = html.indexOf('</section>', at);
+ if (start < 0 || close < 0) return html;
+ const openEnd = html.indexOf('>', start) + 1;
+ const openTag = html.slice(start, openEnd);
+ let inner = html.slice(openEnd, close);
+ const h2 = /<h2[^>]*>[\s\S]*?<\/h2>/.exec(inner);
+ if (!h2) return html;
+ const head = h2[0];
+ inner = inner.slice(h2.index + head.length);
+ // The lede under the heading goes with it: the fold's heading says enough on a phone.
+ const lede = /<p class="sc-lede"[^>]*>[\s\S]*?<\/p>/.exec(inner);
+ if (lede) inner = inner.slice(0, lede.index) + inner.slice(lede.index + lede[0].length);
+ const fold = `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${inner.trim()}</details>${foldScript(id)}`;
+ return html.slice(0, start) + openTag + fold + html.slice(close);
+}
+
+module.exports = { pickBlock, pickRule, pickRules, foldBlock, foldScript, foldSection };
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
+'use strict';
+/**
+ * The home on a phone (the owner's short-pages rule, 2026-10-30): the roadmap and the pulse are a pick block, a row
+ * of chips picks one of the two shelves, and the questions are a fold. Radio inputs and :has(), no script for the
+ * chips; every shelf stays in the page for crawlers; a wider screen shows both shelves and no chips.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+ try {
+ // A roadmap item and a pulse item, so the home has something to show in its pick block.
+ await t.db.prepare("INSERT INTO threads (space_id, slug, title, status, last_activity_at) VALUES (4, 'test-item', 'A test item', 'in_progress', ov_now())").run();
+ await t.db.prepare("INSERT INTO pulse_items (source_service, source_type, source_id, title, url, origin, visibility, occurred_at) VALUES ('network', 'notice', 'n1', 'Something happened', 'https://openvibe.network/', 'system', 'public', ov_now())").run();
+
+ await check('the roadmap and the pulse are chips: one per shelf, the first picked, every shelf in the page', 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-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)];
+ 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');
+ 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.
+ for (const title of ['On the roadmap', 'A test item', 'Across OpenVibe', 'Recently shipped on OpenVibe.Community']) {
+ assert.ok(page.includes(title), `${title} is on the page`);
+ }
+ });
+
+ 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 () => {
+ const r = await t.get('/');
+ assert.ok(r.text.includes('<details class="hm-fold hm-fold-block" id="faq-fold" open>'), 'the questions are a fold, open in the page');
+ assert.ok(r.text.includes('document.getElementById("faq-fold")'), 'the fold has the script that closes it on a phone');
+ // The questions stay in the page.
+ assert.ok(r.text.includes('id="questions"'));
+ assert.match(r.text, /<div class="cm-faq">/);
+ });
+ } finally {
+ await t.close();
+ }
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -10,7 +10,7 @@ const { spawn } = require('child_process');
@@ -10,7 +10,7 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 41, // 39.4 the short home (2026-10-29: rails, a launcher and a fold in place of three grids, What lives here, Part of OpenVibe and the closing call), lowered back from 42; 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0
+ htmlRawKB: 42, // 41.9 the phone home (2026-10-30: the roadmap and the pulse become chips and the questions a fold); 39.4 the short home (2026-10-29: rails, a launcher and a fold in place of three grids, What lives here, Part of OpenVibe and the closing call), lowered back from 42; 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0
// 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six
// 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six
// questions with their FAQPage JSON-LD; was 26 at 22.0
// questions with their FAQPage JSON-LD; was 26 at 22.0
htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7
htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7
@@ -18,7 +18,7 @@ const BUDGETS = {
@@ -18,7 +18,7 @@ const BUDGETS = {
jsRawKB: 336, // 335.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 332.4 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 329.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 322 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 314 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 306.5 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 300.7 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 286.6 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 276.8 with shared 4.11.0's seasonal navbar; 267.5 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 251.3 (was 208.8 before the shell's web-runtime.js)
jsRawKB: 336, // 335.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 332.4 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 329.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 322 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 314 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 306.5 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 300.7 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 286.6 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 276.8 with shared 4.11.0's seasonal navbar; 267.5 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 251.3 (was 208.8 before the shell's web-runtime.js)
jsBrotliKB: 84, // 83.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.3 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 81.7 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 80.2 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 78.1 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 74.4 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 71.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 69.1 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 65.5 with shared 4.11.0's seasonal navbar; 59.9 (was 48.9)
jsBrotliKB: 84, // 83.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.3 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 81.7 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 80.2 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 78.1 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 74.4 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 71.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 69.1 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 65.5 with shared 4.11.0's seasonal navbar; 59.9 (was 48.9)
cssFiles: 5, // 5 the short home: + kit.css (numbers, lists, chips, the fold) and room.css (cards and rails), the shared kit's look; 3
cssFiles: 5, // 5 the short home: + kit.css (numbers, lists, chips, the fold) and room.css (cards and rails), the shared kit's look; 3
- cssRawKB: 106, // 104.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 102.6 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 97.3 the short home (2026-10-29): kit.css 25.6 KB + room.css 15.7 KB, while community.css sheds the old shelves' rules and the page 10 of its 15 phone screens; 54.9 openvibe-shared 4.26.1 (hero visual); 47.2 with Shared 4.13.0 (2026-10-26); 40.6 on 10-22 (the home shelves' styles in community.css); was 40 at 36.5 with the showcase home
+ cssRawKB: 110, // 109.0 the phone home (2026-10-30: the chips and the fold rules); 104.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 102.6 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 97.3 the short home (2026-10-29): kit.css 25.6 KB + room.css 15.7 KB, while community.css sheds the old shelves' rules and the page 10 of its 15 phone screens; 54.9 openvibe-shared 4.26.1 (hero visual); 47.2 with Shared 4.13.0 (2026-10-26); 40.6 on 10-22 (the home shelves' styles in community.css); was 40 at 36.5 with the showcase home
cssBrotliKB: 23, // 22.1 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 21.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 20.4 the short home (kit.css + room.css, as above; was 11.5 once Kit 2 had its own files in openvibe-shared v4.37.0); 9.5 with Shared 4.13.0 (2026-10-26); 7.7 // raised 11 -> 12 for the openvibe-shared Kit 2 pin (kit.css grew)
cssBrotliKB: 23, // 22.1 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 21.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 20.4 the short home (kit.css + room.css, as above; was 11.5 once Kit 2 had its own files in openvibe-shared v4.37.0); 9.5 with Shared 4.13.0 (2026-10-26); 7.7 // raised 11 -> 12 for the openvibe-shared Kit 2 pin (kit.css grew)
externalFiles: 2, // 1
externalFiles: 2, // 1
};
};