openvibers/openvibe.design Commits

openvibe.design/kit/layout: layout without breakpoints (Shared 4.78.0) (#90)

OP OpenVibers committed 12 days ago · 419747be99 · 8 files, +127 −9

* openvibe-shared v4.78.0 (was v4.77.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v4.78.0 release.

* openvibe.design/kit/layout: layout without breakpoints (Shared 4.78.0)

kit-layout.css on its own page: the fluid type and space scale (its sizes read from the release's
own file), the eight primitives each live in a box a reader can drag narrower so the switcher
stacks and the sidebar goes under, and the classless base as a finished demo; the HTML of each in
a counted fold. The kit's index leads to it.

Changes

ddiff --git a/README.md b/README.md
iindex 7f021d0..105ed3d 100644
---- a/README.md
++++ b/README.md
@@@ -229,5 +229,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by
<!-- versions:start -->
- openvibe-contracts: v0.236.0
- openvibe-sdk: v0.74.0
--- openvibe-shared: v4.77.0
++- openvibe-shared: v4.78.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex a0b7982..9ec05dd 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -10,11 +10,11 @@
"domain": "openvibe.design",
"contracts": "openvibe-contracts v0.236.0",
"sdk": "openvibe-sdk v0.74.0",
-- "shared": "openvibe-shared v4.77.0",
++ "shared": "openvibe-shared v4.78.0",
"packages": [
"openvibe-contracts v0.236.0",
"openvibe-sdk v0.74.0",
-- "openvibe-shared v4.77.0"
++ "openvibe-shared v4.78.0"
],
"summary": "The service skeleton for openvibe.design: Network sign-in as OAuth client `design` (PKCE S256, httpOnly design_at/design_rt cookies) and a theme API proxy to OpenVibe.Network's /api/themes. The proxy serves the public catalogue (cached 5 minutes), one theme, the person's active theme and custom variables, saving a theme with source 'studio', its history and a restore by id, and a submission to the gallery. Writes need this site's own Origin; Network 4xx answers pass through and a Network failure is 502. The studio is designed: a gallery, previews on a Live channel, a Chat room and a shop's grid (with colour-vision views), a six-colour editor that derives the full token set, WCAG checks that gate Use, sharing to the gallery, and History with Restore. The Arena queues one prompt for three models through OpenVibe.AI (client credentials, ai.run.create), stores sanitized HTML with reported model/provider/cost/time/tokens, renders it only in sandboxed CSP-capped iframes, draws blind pairs, records one vote per person per round, and keeps Elo ratings per category plus median cost and time. The prompts, outputs and votes are an open JSONL dataset; rounds and votes follow ADR-033 export and deletion.",
"works": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 025617d..2653cac 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.77.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.78.0",
"pg": "^8.23.1",
"subset-font": "^2.9.0"
},
@@@ -1365,9 +1365,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.77.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.77.0",
-- "integrity": "sha512-F3z8SzNcSqh5N2T3XF7nrZEXg39MXWvPo5Or/PbRaU7tgmsU0aXW3ZLB6bL6WPuswdY9Vg0n2wzWlrkEI0Mb3g==",
++ "version": "4.78.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.78.0",
++ "integrity": "sha512-FVMXzMk6kwe0NosmHr0MUbkp5stcw8HSkn1BcrDMwy+mPKtZQKPBGZkcExzf5O17Lczi/TbGXChFLl4DF3bFTw==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex 04b9ca2..4a36e82 100644
---- a/package.json
++++ b/package.json
@@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.77.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.78.0",
"pg": "^8.23.1",
"subset-font": "^2.9.0"
},
ddiff --git a/public/css/kit-pages.css b/public/css/kit-pages.css
iindex 4669001..0a990c8 100644
---- a/public/css/kit-pages.css
++++ b/public/css/kit-pages.css
@@@ -104,3 +104,47 @@
.kt-pn .kt-pn-all { grid-column: 1 / -1; grid-row: 2; }
}
@media (prefers-reduced-motion: reduce) { .kt-tile { transition: none; } }
++
++/* ── /kit/layout: the fluid scale, the eight primitives (each in a box the reader can drag narrower) and the base ── */
++.kt-layout-card { display: grid; gap: 4px; margin: 18px 0 0; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--room-strong) 45%, var(--border, #26344d)); border-radius: 16px;
++ background: linear-gradient(120deg, color-mix(in srgb, var(--room-strong) 14%, var(--bg-card, #0f1729)), var(--bg-card, #0f1729)); color: var(--text-primary, #e6edf7); text-decoration: none; max-width: 760px; }
++.kt-layout-card .kt-kicker { margin: 0; }
++.kt-layout-card b { font-size: 1.1rem; }
++.kt-layout-card span:last-child { color: var(--text-secondary, #96a7c2); font-size: .9rem; line-height: 1.5; }
++.kt-layout-card:hover { border-color: var(--room-strong); }
++.kt-layout-card:focus-visible { outline: 2px solid var(--room-strong); outline-offset: 2px; }
++.kt-sub { margin: 6px 0 0; max-width: 70ch; color: var(--text-secondary, #96a7c2); line-height: 1.55; }
++.kt-scale, .kt-prims, .kt-basesec { margin-top: 34px; }
++.kt-scale > h2, .kt-prims > h2, .kt-basesec > h2 { margin: 0; font-size: clamp(1.15rem, 2.4vw, 1.45rem); }
++.kt-steps { display: grid; gap: 2px; margin-top: 14px; }
++.kt-step { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: baseline; gap: 14px; padding: 8px 12px; border-radius: 10px; }
++.kt-step:nth-child(odd) { background: color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent); }
++.kt-step-aa { font-weight: 800; letter-spacing: -.02em; line-height: 1.1; color: var(--text-primary, #e6edf7); }
++.kt-step code, .kt-space code { color: var(--room-strong); font-size: .8rem; }
++.kt-step small, .kt-space small { color: var(--text-muted, #7386a3); font-size: .78rem; font-variant-numeric: tabular-nums; min-width: 7.5em; text-align: right; }
++.kt-spaces { display: grid; gap: 6px; margin-top: 18px; }
++.kt-space { display: grid; grid-template-columns: 8.5em minmax(0, 1fr) auto; align-items: center; gap: 12px; }
++.kt-space i { display: block; height: 12px; border-radius: 4px; background: color-mix(in srgb, var(--room-strong) 70%, transparent); }
++.kt-prim { margin-top: 26px; }
++.kt .kt-prim h3 { margin: 0; display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; font-size: 1.05rem; font-weight: 800; letter-spacing: 0; text-transform: none; color: var(--text-primary, #e6edf7); }
++.kt-prim h3 code { color: var(--room-strong); font-size: .82rem; font-weight: 700; }
++.kt-prim-h p { margin: 4px 0 0; color: var(--text-secondary, #96a7c2); font-size: .9rem; }
++/* The box a reader can drag narrower; its corner says so */
++.kt-resize { position: relative; box-sizing: border-box; width: 100%; max-width: 100%; min-width: 200px; margin: 12px 0 10px; padding: 14px; overflow: auto; resize: horizontal;
++ border: 1px dashed color-mix(in srgb, var(--room-strong) 45%, var(--border, #26344d)); border-radius: 14px; background: var(--bg-primary, #0a0f1c); }
++.kt-resize > * > *:not(img) { box-sizing: border-box; min-height: 44px; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--room-strong) 14%, var(--bg-card, #0f1729));
++ border: 1px solid color-mix(in srgb, var(--room-strong) 30%, transparent); color: var(--text-primary, #e6edf7); font-weight: 700; font-size: .9rem; margin: 0; }
++.kt-resize > .ov-cluster > * { min-height: 0; padding: 5px 12px; border-radius: 999px; font-size: .84rem; }
++.kt-resize > .ov-cover { border-radius: 10px; background: linear-gradient(160deg, color-mix(in srgb, var(--room-strong) 18%, transparent), transparent); }
++.kt-resize > .ov-cover > * { background: none; border: 0; min-height: 0; padding: 0; }
++.kt-resize > .ov-cover > h2 { font-size: var(--step-3); text-align: center; }
++.kt-resize > .ov-frame { border-radius: 12px; background: var(--bg-card, #0f1729); }
++@media (hover: hover) and (pointer: fine) { .kt-resize::after { content: '↘ drag to narrow'; position: absolute; right: 18px; bottom: 2px; font-size: .68rem; color: var(--text-muted, #7386a3); pointer-events: none; } }
++.kt-base-demo { margin: 14px 0 10px; padding: clamp(16px, 3vw, 26px); border: 1px solid var(--border, #26344d); border-radius: 16px; background: var(--bg-primary, #0a0f1c); }
++.kt-base-demo > :first-child { margin-top: 0; }
++@media (max-width: 759px) { .kt-step { grid-template-columns: minmax(0, 1fr) auto; } .kt-step small { grid-column: 2; } .kt-space { grid-template-columns: 6.5em minmax(0, 1fr) auto; gap: 8px; } .kt-space small { min-width: 0; font-size: .7rem; } }
++.kt-resize > .ov-frame { max-width: 560px; }
++/* This page's own heading rules (.ds h2, .ds h3) would outweigh the base's :where() — which is the point of it — so
++ the demo states the base's own sizes, to show what a page without such rules gets. */
++.kt .kt-base-demo h2 { margin: 0 0 .5em; font-size: var(--step-3); font-weight: 700; line-height: 1.15; letter-spacing: -.015em; text-transform: none; color: var(--ovk-text, #e6edf7); }
++.kt .kt-base-demo p { font-size: var(--step-0); }
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex f1d906b..48e34d2 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -214,6 +214,7 @@ function createPageRoutes(ctx) {
// ── Components: the design system's components for web, Android and iOS (views/components.js) ──
// The OpenVibe Kit, part by part (plan L54 K3).
r.get('/kit', (req, res) => page(res, 200, kitView.kitIndex(), req.viewer));
++ r.get('/kit/layout', (req, res) => page(res, 200, kitView.kitLayout(), req.viewer));
r.get('/kit/:id', (req, res) => {
const html = kitView.kitPart(String(req.params.id || ''));
return html ? page(res, 200, html, req.viewer) : page(res, 404, views.notFound(), req.viewer);
ddiff --git a/server/views/kit.js b/server/views/kit.js
iindex eaf0c5c..0120067 100644
---- a/server/views/kit.js
++++ b/server/views/kit.js
@@@ -108,6 +108,7 @@ function kitIndex() {
<h1>Every piece an OpenVibe page is made of</h1>
<p class="kt-lede">${PARTS.length} parts, each one function call that returns plain HTML: escaped, phone first, on the site's own theme. Pages work without JavaScript; one small file adds the menus, copy buttons and sheets. All of it is ${weight.kb(css)} of CSS.</p></header>
${nav}
++<a class="kt-layout-card" href="/kit/layout"><span class="kt-kicker">New in 4.78</span><b>Layout without breakpoints</b><span>A fluid type and space scale, eight primitives that lay out by their own width, and a classless base. ${weight.kb(require('zlib').brotliCompressSync(Buffer.from(LAYOUT_FILE || ' ')).length)}.</span></a>
${useCards()}
${groups}
${helpers}
@@@ -163,4 +164,67 @@ ${native ? `<p>It is also drawn natively: <a href="/components?c=${esc(part.id)}
});
}
--module.exports = { kitIndex, kitPart, pretty, still };
++// ── /kit/layout: kit-layout.css (Shared 4.78.0, plan L54 K1/K2) ──
++const LAYOUT_FILE = (() => { try { return require('fs').readFileSync(require('path').join(require('path').dirname(require.resolve('openvibe-shared/kit')), 'kit-layout.css'), 'utf8'); } catch { return ''; } })();
++/** A token's clamp() read from the release's own file: its smallest and largest size in px. */
++function clampOf(name) {
++ const m = new RegExp(`${name.replace(/[-]/g, '\\-')}:\\s*clamp\\(([\\d.]+)rem,[^,]+,\\s*([\\d.]+)rem\\)`).exec(LAYOUT_FILE);
++ return m ? [Math.round(Number(m[1]) * 160) / 10, Math.round(Number(m[2]) * 160) / 10] : null;
++}
++const box = (text, extra = '') => `<div class="kt-db"${extra}>${esc(text)}</div>`;
++const PRIMITIVES = [
++ ['stack', 'Stack', 'One under another, --gap apart. The rhythm of a column.', `<div class="ov-stack" style="--gap: var(--space-xs)">\n ${['Title', 'One line about it', 'The action'].map((t) => `<div>${t}</div>`).join('\n ')}\n</div>`],
++ ['cluster', 'Cluster', 'A wrapping row: tags, buttons, a toolbar. It never overflows; it wraps.', `<div class="ov-cluster">\n ${['IRL', 'Seattle', 'Night', 'Music', 'Chat picks', 'Walking'].map((t) => `<span>${t}</span>`).join('\n ')}\n</div>`],
++ ['sidebar', 'Sidebar', 'A side part --side wide beside the rest; it goes under once the rest would be narrower than --min.', `<div class="ov-sidebar" style="--side: 12rem; --min: 60%">\n <aside>The side, 12rem</aside>\n <div>The rest of the page</div>\n</div>`],
++ ['switcher', 'Switcher', 'All side by side, or all stacked once the box is narrower than --min: never two and one.', `<div class="ov-switcher" style="--min: 36rem">\n <div>Free</div>\n <div>Creator</div>\n <div>Studio</div>\n</div>`],
++ ['grid', 'Grid', 'As many columns of at least --min as fit, and one on a phone.', `<div class="ov-grid" style="--min: 9rem">\n ${[1, 2, 3, 4, 5, 6].map((n) => `<div>Card ${n}</div>`).join('\n ')}\n</div>`],
++ ['rail', 'Rail', 'One row that scrolls sideways and snaps: a long list that never makes a long page.', `<div class="ov-rail" style="--item: 10rem">\n ${[1, 2, 3, 4, 5, 6, 7].map((n) => `<div>Clip ${n}</div>`).join('\n ')}\n</div>`],
++ ['cover', 'Cover', 'A box at least --min tall, its .is-centre child in the middle: a hero, an empty page.', `<section class="ov-cover" style="--min: 15rem">\n <p>Above</p>\n <h2 class="is-centre">In the middle</h2>\n <p>Below</p>\n</section>`],
++ ['frame', 'Frame', 'A box of --ratio that crops a picture or a video to fill it.', `<div class="ov-frame" style="--ratio: 21 / 9">\n <img src="/brand/openvibe-mark-wide.svg" alt="">\n</div>`],
++];
++
++function kitLayout() {
++ const kb = weight.kb;
++ const zlib = require('zlib');
++ const size = LAYOUT_FILE ? zlib.brotliCompressSync(Buffer.from(LAYOUT_FILE)).length : 0;
++ const steps = [-2, -1, 0, 1, 2, 3, 4, 5].map((n) => [n, clampOf(`--step-${n < 0 ? `-${-n}` : n}`)]).filter(([, v]) => v);
++ const spaces = ['3xs', '2xs', 'xs', 's', 'm', 'l', 'xl', '2xl', '3xl'].map((k) => [k, clampOf(`--space-${k}`)]).filter(([, v]) => v);
++ const scale = `<div class="kt-steps">${steps.map(([n, [a, b]]) => `<div class="kt-step"><span class="kt-step-aa" style="font-size:var(--step-${n < 0 ? `-${-n}` : n})">Aa</span><code>--step-${n < 0 ? `-${-n}` : n}</code><small>${a}–${b} px</small></div>`).join('')}</div>
++<div class="kt-spaces">${spaces.map(([k, [a, b]]) => `<div class="kt-space"><code>--space-${esc(k)}</code><i style="width:var(--space-${esc(k)})"></i><small>${a}–${b} px</small></div>`).join('')}</div>`;
++ const demo = ([id, name, line, code]) => `<section class="kt-prim" id="kt-l-${esc(id)}" aria-labelledby="kt-l-${esc(id)}-h">
++<div class="kt-prim-h"><h3 id="kt-l-${esc(id)}-h">${esc(name)} <code>.ov-${esc(id)}</code></h3><p>${esc(line)}</p></div>
++<div class="kt-resize" data-prim="${esc(id)}">${code}</div>
++${kit.fold({ title: 'The HTML', meta: `${code.split('\n').length} lines`, body: kit.snippet({ id: `kt-l-${id}-code`, code }) })}</section>`;
++ const baseDemo = `<h2>A plain heading</h2>
++<p>Plain text keeps to a readable measure, with <a href="/kit/layout">a link</a>, some <code>code</code> and <mark>a mark</mark>.</p>
++<ul><li>A list, spaced to breathe</li><li>On the site's own theme</li></ul>
++<blockquote>A quote stands apart.</blockquote>
++<table><tr><th>Plan</th><th>Price</th></tr><tr><td>Creator</td><td>$6</td></tr></table>
++<label for="kt-base-name">Your name</label><input id="kt-base-name" placeholder="Ana">
++<button type="button">Save</button>`;
++ const body = `<div class="kt kt-part kt-layout" id="kt-layout">
++<nav class="kt-crumb" aria-label="Breadcrumb"><a href="/kit">The Kit</a><span aria-hidden="true">/</span><span>Layout</span></nav>
++<header class="kt-part-h"><h1>Layout without breakpoints</h1>
++<p class="kt-lede">A fluid scale for type and space, eight primitives that lay out by their own width (so they behave the same in a sidebar, a card or a whole page), and an opt-in base that finishes plain HTML.</p>
++<ul class="kt-facts"><li><code>kit-layout.css</code></li><li>${size ? `${kb(size)} brotli` : 'not in this release'}</li><li>after kit.css</li><li>No script needed</li></ul></header>
++<section class="kt-code" aria-labelledby="kt-l-use-h"><h2 id="kt-l-use-h">Use it</h2>${kit.snippet({ id: 'kt-l-use', code: `<link rel="stylesheet" href="${CDN}/kit.css">\n<link rel="stylesheet" href="${CDN}/kit-layout.css">` })}</section>
++<section class="kt-scale" aria-labelledby="kt-l-scale-h"><h2 id="kt-l-scale-h">The fluid scale</h2>
++<p class="kt-sub">Every step grows with the screen from 320 to 1240 px by <code>clamp()</code>: 16 px text on a phone, 18 px on a wide screen, and the headings more. Resize the window and watch it move.</p>${scale}</section>
++<section class="kt-prims" aria-labelledby="kt-l-prims-h"><h2 id="kt-l-prims-h">Eight primitives</h2>
++<p class="kt-sub">Each box below is the real thing. On a computer, drag its corner to make it narrower: the switcher stacks, the sidebar goes under, the grid drops a column.</p>
++${PRIMITIVES.map(demo).join('')}</section>
++<section class="kt-basesec" aria-labelledby="kt-l-base-h"><h2 id="kt-l-base-h">The classless base</h2>
++<p class="kt-sub">Put <code>.ov-base</code> on <code>&lt;body&gt;</code> or <code>&lt;main&gt;</code>, and plain HTML looks finished. Every rule is inside <code>:where()</code>, so your own rules always win.</p>
++<div class="ov-base kt-base-demo">${baseDemo}</div>
++${kit.fold({ title: 'The HTML', meta: `${baseDemo.split('\n').length + 2} lines`, body: kit.snippet({ id: 'kt-l-base-code', code: `<main class="ov-base">\n ${baseDemo.split('\n').join('\n ')}\n</main>` }) })}</section>
++<nav class="kt-pn" aria-label="More of the kit"><a href="/kit" class="kt-pn-all">All of the Kit</a></nav>
++</div>`;
++ return layout({
++ title: 'Layout · The OpenVibe Kit',
++ description: 'A fluid type and space scale, eight layout primitives that need no breakpoints, and an opt-in classless base: kit-layout.css, about 2.5 KB.',
++ path: '/kit/layout', active: '/kit', body: `<div class="ds-wrap">${body}</div>`,
++ styles: ['css/kit-pages.css'], sharedStyles: ['kit-layout.css'],
++ });
++}
++
++module.exports = { kitIndex, kitPart, kitLayout, pretty, still, PRIMITIVES };
ddiff --git a/test/kit-pages.test.js b/test/kit-pages.test.js
iindex 18de820..3caccab 100644
---- a/test/kit-pages.test.js
++++ b/test/kit-pages.test.js
@@@ -66,6 +66,15 @@ const { boot, check, done } = require('./helpers/boot');
assert.match(part.text, /href="\/components\?c=button&amp;p=android">Jetpack Compose/, 'native where it is');
assert.match(part.text, /rel="next"><small>Next<\/small>Icon button/);
assert.strictEqual((await t.get('/kit/nope')).status, 404);
++ const lay = await t.get('/kit/layout');
++ assert.strictEqual(lay.status, 200, 'the layout page, not a part called layout');
++ assert.strictEqual((lay.text.match(/<section class="kt-prim" id="kt-l-/g) || []).length, 8, 'eight primitives');
++ assert.strictEqual((lay.text.match(/class="kt-step"/g) || []).length, 8, 'eight type steps');
++ assert.strictEqual((lay.text.match(/class="kt-space"/g) || []).length, 9, 'nine spaces');
++ assert.match(lay.text, /<code>--step-0<\/code><small>16–18 px<\/small>/, 'sizes read from the release\'s own file');
++ assert.match(lay.text, /<div class="ov-switcher" style="--min: 36rem">/, 'the real primitive, not a picture');
++ assert.ok(lay.text.includes('/shared/kit-layout.css'), 'drawn with the file it documents');
++ assert.match(index.text, /<a class="kt-layout-card" href="\/kit\/layout">/, 'the index leads to it');
const page = await t.get('/kit/page');
assert.strictEqual((page.text.match(/<main\b/g) || []).length, 1, 'the page sample is not a second <main>');
});