openvibers/openvibe.services Commits

On a phone, a wide table shows each row as a labelled card (#180)

OP OpenVibers committed 12 days ago · 0bd2e17627 · 6 files, +25 −9

* On a phone, a wide table shows each row as a labelled card

* Tests read a table cell with its column label

* Budget: 2026-11-01: phone table cards

Changes

ddiff --git a/public/css/services.css b/public/css/services.css
iindex 039ee3e..67a5855 100644
---- a/public/css/services.css
++++ b/public/css/services.css
@@@ -110,3 +110,18 @@ meter.headroom { width: 8rem; max-width: 100%; vertical-align: middle; }
.svc-fold-block > summary span { display: none; }
.svc-fold-block .ov-shipped-home { margin-left: 0; margin-right: 0; }
}
++
++/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more shows each row as a card, the first
++ cell its title and every other cell named by its column (html.js gives each cell its data-label). Narrower tables
++ stay tables. Nothing is left out and nothing scrolls sideways. */
++@media (max-width: 640px) {
++ .table-wrap:has(> table thead th:nth-child(3)) { overflow: visible; }
++ .table-wrap > table:has(thead th:nth-child(3)) { display: block; }
++ .table-wrap > table:has(thead th:nth-child(3)) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
++ .table-wrap > table:has(thead th:nth-child(3)) tbody { display: grid; gap: 10px; }
++ .table-wrap > table:has(thead th:nth-child(3)) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }
++ .table-wrap > table:has(thead th:nth-child(3)) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }
++ .table-wrap > table:has(thead th:nth-child(3)) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }
++ .table-wrap > table:has(thead th:nth-child(3)) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
++ .table-wrap > table:has(thead th:nth-child(3)) td:empty { display: none; }
++}
ddiff --git a/server/render/html.js b/server/render/html.js
iindex c0f69d9..1dfe9ed 100644
---- a/server/render/html.js
++++ b/server/render/html.js
@@@ -24,10 +24,11 @@ function render(v) {
const code = (s) => html`<code>${s}</code>`;
const time = (iso) => (iso ? html`<time datetime="${iso}">${String(iso).replace('T', ' ').replace(/\.\d+Z$/, ' UTC').replace(/Z$/, ' UTC')}</time>` : '—');
--/** A <table> from headers and rows of cells (cells are already html or plain values). */
++/** A <table> from headers and rows of cells (cells are already html or plain values). Each cell names its column
++ * (data-label), so on a phone a wide table can show each row as a card (the site's stylesheet). */
function table(headers, rows, { cls = '', empty = 'Nothing here yet.' } = {}) {
if (!rows.length) return html`<p class="muted">${empty}</p>`;
-- return html`<div class="table-wrap"><table class="${cls}"><thead><tr>${headers.map((h) => html`<th scope="col">${h}</th>`)}</tr></thead><tbody>${rows.map((r) => html`<tr>${r.map((c) => html`<td>${c}</td>`)}</tr>`)}</tbody></table></div>`;
++ return html`<div class="table-wrap"><table class="${cls}"><thead><tr>${headers.map((h) => html`<th scope="col">${h}</th>`)}</tr></thead><tbody>${rows.map((r) => html`<tr>${r.map((c, i) => (typeof headers[i] === 'string' && headers[i] ? html`<td data-label="${headers[i]}">${c}</td>` : html`<td>${c}</td>`))}</tr>`)}</tbody></table></div>`;
}
/** A problem as returned by Network, Events or Media — shown as it came, never softened. */
ddiff --git a/test/docs.test.js b/test/docs.test.js
iindex 7187eb1..258d3c7 100644
---- a/test/docs.test.js
++++ b/test/docs.test.js
@@@ -75,7 +75,7 @@ const sdkPkg = require('openvibe-sdk/package.json');
const r = await t.get('/docs/events');
const expected = new Set(contracts.services.manifests.flatMap((m) => m.eventsProduced || []));
for (const e of expected) assert.ok(r.text.includes(`<code>${e}</code>`), e);
-- assert.strictEqual([...r.text.matchAll(/<tr><td><code>/g)].length, expected.size);
++ assert.strictEqual([...r.text.matchAll(/<tr><td[^>]*><code>/g)].length, expected.size);
});
await check('the SDK reference has one page per exported types entry, declarations verbatim', async () => {
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 0794917..eb4559a 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -18,8 +18,8 @@ const BUDGETS = {
jsRawKB: 331, // 328.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 324.8 with openvibe-shared 4.66.0: the mark with depth and motion, ov-fx loaded on request (2026-10-31); 323.2 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 321.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 318.1 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 315.6 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); 307.8 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 300.5 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; 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 263.4 with shared 4.11.0's seasonal navbar; 254.1 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 246.2 with openvibe-shared 4.2.x (Ask Actor in the navbar, 2026-10-15); 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 80.8, // 80.2 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 79.5 with openvibe-shared 4.59.0: the Pair mark beside the name, set in the brand face (2026-10-31); 78.2 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 77.6 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); 76.1 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 74.2 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; 70.5 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 67.9 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 65.3 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 61.7 with shared 4.11.0's seasonal navbar; 59.1 with openvibe-shared 4.4.0 (every site in the launcher, 2026-10-17); 56.3 (49.9 before web-runtime.js)
cssFiles: 3, // 3 with kit.css (2026-10-29: the home's numbers strip, folds and examples list); 2; 1
-- cssRawKB: 77, // 76.2 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 72.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 68.7 with openvibe-shared 4.62.0: the shared sidebar in kit.css, the glyphs and their fonts (2026-10-31); 63.4 with the phone fold rules (2026-10-30); 61.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); 58.2 with kit.css (25.6 KB raw; 34.0 before on Shared 4.38.0): the numbers as one kit.stats strip, what is read once in kit.fold and the examples as kit rows, which took the phone home from 16.1 screens to 5.8; 31.8 openvibe-shared 4.26.1 (hero visual); 26.3 with Shared 4.13.0 (2026-10-26: account links); 20.3 with the numbers and FAQ styles (2026-10-22); 17.3 (services.css + the cached /shared/showcase.css)
-- cssBrotliKB: 16, // 15.8 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 14.8 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 13.2 with the phone fold rules (2026-10-30); 12.1 with kit.css (2026-10-29; 7.3 before); 5.8 with Shared 4.13.0 (2026-10-26: account links); 4.1
++ cssRawKB: 79, // 78.2 with 2026-11-01: phone table cards (2026-11-01); 76.2 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 72.1 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 68.7 with openvibe-shared 4.62.0: the shared sidebar in kit.css, the glyphs and their fonts (2026-10-31); 63.4 with the phone fold rules (2026-10-30); 61.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); 58.2 with kit.css (25.6 KB raw; 34.0 before on Shared 4.38.0): the numbers as one kit.stats strip, what is read once in kit.fold and the examples as kit rows, which took the phone home from 16.1 screens to 5.8; 31.8 openvibe-shared 4.26.1 (hero visual); 26.3 with Shared 4.13.0 (2026-10-26: account links); 20.3 with the numbers and FAQ styles (2026-10-22); 17.3 (services.css + the cached /shared/showcase.css)
++ cssBrotliKB: 16.5, // 16.2 with 2026-11-01: phone table cards (2026-11-01); 15.8 with openvibe-shared 4.82.0: the motion tokens in kit.css and room.css (2026-11-01); 14.8 openvibe-shared 4.70.0 (2026-11-01: the app bar, busy(), skeletons, view transitions); 13.2 with the phone fold rules (2026-10-30); 12.1 with kit.css (2026-10-29; 7.3 before); 5.8 with Shared 4.13.0 (2026-10-26: account links); 4.1
externalFiles: 2, // 0
};
ddiff --git a/test/project-resources.test.js b/test/project-resources.test.js
iindex 6c26c4e..0434370 100644
---- a/test/project-resources.test.js
++++ b/test/project-resources.test.js
@@@ -62,7 +62,7 @@ const { boot, check, done } = require('./helpers/boot');
assert.ok(r.text.includes(`<code class="small">ovrn:services:${projectId}:release/${mine.rel}</code>`), 'the release by its OVRN');
assert.ok(r.text.includes(`<code class="small">ovrn:services:${projectId}:manifest/${mine.mfs}</code>`), 'the manifest by its OVRN');
assert.match(r.text, /<span class="badge ok">published<\/span>/);
-- assert.match(r.text, /<td>you<\/td>/);
++ assert.match(r.text, /<td[^>]*>you<\/td>/);
assert.ok(r.text.includes(`<a href="${page}?kind=services.release"><code>services.release</code></a>`), 'a kind narrows the list');
assert.ok(!r.text.includes(theirs.rel) && !r.text.includes(theirs.mfs) && !r.text.includes('Secret app'), 'nothing of another project');
assert.ok(!r.text.includes('<img src=x'), 'an authority\'s name is escaped');
@@@ -76,7 +76,7 @@ const { boot, check, done } = require('./helpers/boot');
const v = await t.get(page, { as: viewer });
assert.strictEqual(v.status, 200);
assert.ok(v.text.includes(mine.rel));
-- assert.ok(!/<td>you<\/td>/.test(v.text), 'the owner column names the owner, not the viewer');
++ assert.ok(!/<td[^>]*>you<\/td>/.test(v.text), 'the owner column names the owner, not the viewer');
const before = asked.length;
const s = await t.get(page, { as: stranger });
assert.strictEqual(s.status, 404, s.text.slice(0, 200));
ddiff --git a/test/webhooks.test.js b/test/webhooks.test.js
iindex 615258e..aa2b275 100644
---- a/test/webhooks.test.js
++++ b/test/webhooks.test.js
@@@ -183,8 +183,8 @@ const hmacV1 = (body, secret) => `sha256=${crypto.createHmac('sha256', secret).u
assert.strictEqual(r.status, 200);
assert.match(r.text, /X-OpenVibe-Signature-V2/);
assert.match(r.text, /X-OpenVibe-Timestamp/);
-- assert.match(r.text, /<td><code>X-OpenVibe-Signature-V2<\/code><\/td>/);
-- assert.doesNotMatch(r.text, /<td><code>X-OpenVibe-Signature<\/code><\/td>/, 'no v1 header row in the sample');
++ assert.match(r.text, /<td[^>]*><code>X-OpenVibe-Signature-V2<\/code><\/td>/);
++ assert.doesNotMatch(r.text, /<td[^>]*><code>X-OpenVibe-Signature<\/code><\/td>/, 'no v1 header row in the sample');
assert.match(r.text, /X-OpenVibe-Signature-V2: t=\d+,v2=[0-9a-f]{64}/, 'the curl replay carries v2');
assert.doesNotMatch(r.text, /X-OpenVibe-Signature: /, 'no v1 header in the curl replay');
assert.doesNotMatch(r.text, /sha256=[0-9a-f]{64}/);