openvibers/openvibe.design Commits
openvibe.design/kit/layout: layout without breakpoints (Shared 4.78.0) (#90)
Changes
@@@ -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 -->@@@ -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": [@@@ -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",@@@ -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"},@@@ -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); }@@@ -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);@@@ -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><body></code> or <code><main></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 };@@@ -66,6 +66,15 @@ const { boot, check, done } = require('./helpers/boot');assert.match(part.text, /href="\/components\?c=button&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>');});
@@ -229,5 +229,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by
@@ -229,5 +229,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.236.0
- openvibe-contracts: v0.236.0
- openvibe-sdk: v0.74.0
- openvibe-sdk: v0.74.0
-- openvibe-shared: v4.77.0
+- openvibe-shared: v4.78.0
<!-- versions:end -->
<!-- versions:end -->
@@ -10,11 +10,11 @@
@@ -10,11 +10,11 @@
"domain": "openvibe.design",
"domain": "openvibe.design",
"contracts": "openvibe-contracts v0.236.0",
"contracts": "openvibe-contracts v0.236.0",
"sdk": "openvibe-sdk v0.74.0",
"sdk": "openvibe-sdk v0.74.0",
- "shared": "openvibe-shared v4.77.0",
+ "shared": "openvibe-shared v4.78.0",
"packages": [
"packages": [
"openvibe-contracts v0.236.0",
"openvibe-contracts v0.236.0",
"openvibe-sdk v0.74.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.",
"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": [
"works": [
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.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-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",
"pg": "^8.23.1",
"subset-font": "^2.9.0"
"subset-font": "^2.9.0"
},
},
@@ -1365,9 +1365,9 @@
@@ -1365,9 +1365,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.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-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",
"pg": "^8.23.1",
"subset-font": "^2.9.0"
"subset-font": "^2.9.0"
},
},
@@ -104,3 +104,47 @@
@@ -104,3 +104,47 @@
.kt-pn .kt-pn-all { grid-column: 1 / -1; grid-row: 2; }
.kt-pn .kt-pn-all { grid-column: 1 / -1; grid-row: 2; }
}
}
@media (prefers-reduced-motion: reduce) { .kt-tile { transition: none; } }
@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); }
@@ -214,6 +214,7 @@ function createPageRoutes(ctx) {
@@ -214,6 +214,7 @@ function createPageRoutes(ctx) {
// ── Components: the design system's components for web, Android and iOS (views/components.js) ──
// ── Components: the design system's components for web, Android and iOS (views/components.js) ──
// The OpenVibe Kit, part by part (plan L54 K3).
// The OpenVibe Kit, part by part (plan L54 K3).
r.get('/kit', (req, res) => page(res, 200, kitView.kitIndex(), req.viewer));
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) => {
r.get('/kit/:id', (req, res) => {
const html = kitView.kitPart(String(req.params.id || ''));
const html = kitView.kitPart(String(req.params.id || ''));
return html ? page(res, 200, html, req.viewer) : page(res, 404, views.notFound(), req.viewer);
return html ? page(res, 200, html, req.viewer) : page(res, 404, views.notFound(), req.viewer);
@@ -108,6 +108,7 @@ function kitIndex() {
@@ -108,6 +108,7 @@ function kitIndex() {
<h1>Every piece an OpenVibe page is made of</h1>
<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>
<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}
${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()}
${useCards()}
${groups}
${groups}
${helpers}
${helpers}
@@ -163,4 +164,67 @@ ${native ? `<p>It is also drawn natively: <a href="/components?c=${esc(part.id)}
@@ -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><body></code> or <code><main></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 };
@@ -66,6 +66,15 @@ const { boot, check, done } = require('./helpers/boot');
@@ -66,6 +66,15 @@ const { boot, check, done } = require('./helpers/boot');
assert.match(part.text, /href="\/components\?c=button&p=android">Jetpack Compose/, 'native where it is');
assert.match(part.text, /href="\/components\?c=button&p=android">Jetpack Compose/, 'native where it is');
assert.match(part.text, /rel="next"><small>Next<\/small>Icon button/);
assert.match(part.text, /rel="next"><small>Next<\/small>Icon button/);
assert.strictEqual((await t.get('/kit/nope')).status, 404);
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');
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>');
assert.strictEqual((page.text.match(/<main\b/g) || []).length, 1, 'the page sample is not a second <main>');
});
});