openvibers/openvibe.design Commits
Changes: month folds with counts, a release per row (#266)
Changes
@@@ -53,3 +53,27 @@.chg-shot-row { grid-template-columns: 1fr 1fr; gap: 8px; }.chg-shot figcaption { display: block; }}++++/* The months after the newest twelve (2026-11-11): each a fold with its count, each release one row. */++.chg-month { margin: 0 0 8px; border: 1px solid var(--border, #2c2c38); border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 2%, transparent); overflow: hidden; }++.chg-month > summary { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 18px; cursor: pointer; list-style: none; }++.chg-month > summary::-webkit-details-marker { display: none; }++.chg-month > summary b { font-size: 16px; font-weight: 750; }++.chg-month-n { margin-left: auto; color: var(--text-muted, #8b93ad); font-size: 13px; font-variant-numeric: tabular-nums; }++.chg-month > summary::after { content: ""; width: 8px; height: 8px; margin: 0 2px 4px 6px; border: solid var(--text-muted, #8b93ad); border-width: 0 2px 2px 0; transform: rotate(45deg); transition: transform .2s; }++.chg-month[open] > summary::after { transform: translateY(3px) rotate(225deg); }++.chg-month > summary:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 7%, transparent); }++.chg-month > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }++.chg-month-rows { display: grid; padding: 4px 8px 10px; border-top: 1px solid var(--border, #2c2c38); }++.chg-month-rows > a { display: grid; grid-template-columns: 5.6em minmax(0, 1fr) auto 10px; align-items: center; gap: 12px; min-height: 44px; padding: 6px 10px; border-radius: 10px; color: var(--text-primary, #e6edf7); text-decoration: none; }++.chg-month-rows > a:hover, .chg-month-rows > a:focus-visible { background: color-mix(in srgb, var(--accent, #3b82f6) 9%, transparent); }++.chg-month-rows > a > b { font: 650 13px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--accent, #5b9dff); font-variant-numeric: tabular-nums; }++.chg-month-rows > a > span { color: var(--text-primary, #e6edf7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14.5px; }++.chg-month-rows > a > small { color: var(--text-muted, #8b93ad); font-size: 12.5px; font-variant-numeric: tabular-nums; }++.chg-month-rows > a > i { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #5b9dff); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent); }++@media (max-width: 640px) {++ .chg-month-rows > a { grid-template-columns: 5em minmax(0, 1fr) 10px; grid-template-rows: auto auto; row-gap: 2px; }++ .chg-month-rows > a > span { grid-column: 2; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }++ .chg-month-rows > a > small { grid-column: 2; grid-row: 2; }++ .chg-month-rows > a > i { grid-column: 3; grid-row: 1; }++}@@@ -47,7 +47,8 @@ function releaseCard(r) {function releaseRow(r) {const hasShots = screenshots(r.version).length > 0;-- return `<a href="/changes/${esc(r.version)}">${esc(r.version)} · ${esc(r.title)} · ${esc(r.date)}${hasShots ? ' ●' : ''}</a>`;++ // Lean on purpose (328 rows): the fold's styles read the parts by element, so no class or attribute per row.++ return `<a href="/changes/${esc(r.version)}"><b>${esc(r.version)}</b><span>${esc(r.title)}</span><small>${esc(r.date)}</small>${hasShots ? '<i title="Has screenshots"></i>' : ''}</a>`;}function monthLabel(date) {@@@ -90,7 +91,7 @@ ${kit.stats([[all.length, 'releases'], [counted, 'with screenshots'], [pin.versiif (!monthGroups.has(key)) monthGroups.set(key, { label: monthLabel(r.date), releases: [] });monthGroups.get(key).releases.push(r);}-- const folds = [...monthGroups.values()].map((group) => `<details class="chg-month"><summary>${esc(group.label)} · ${group.releases.length} ${group.releases.length === 1 ? 'release' : 'releases'}</summary>${group.releases.map(releaseRow).join('')}</details>`).join('');++ const folds = [...monthGroups.values()].map((group) => `<details class="chg-month"><summary><b>${esc(group.label)}</b><span class="chg-month-n">${group.releases.length} ${group.releases.length === 1 ? 'release' : 'releases'}</span></summary><div class="chg-month-rows">${group.releases.map(releaseRow).join('')}</div></details>`).join('');return layout({title: `Changes: ${all.length} OpenVibe.Shared releases`,description: `Every OpenVibe.Shared release in the pinned changelog, newest first: what changed, the component pages it touches, the rules it carries out, and its screenshots.`,@@@ -22,11 +22,12 @@ const { PUBLIC_DIR } = require('../server/assets');await check('/changes lists every release in the pinned CHANGELOG, newest first, in its own words', async () => {const r = await t.get('/changes');assert.strictEqual(r.status, 200);-- assert.ok(Buffer.byteLength(r.text) < 120 * 1024, `${Buffer.byteLength(r.text)} bytes`);++ // 136 KB (2026-11-11, was 120): each folded release is a row with its version, title and date as parts.++ assert.ok(Buffer.byteLength(r.text) < 136 * 1024, `${Buffer.byteLength(r.text)} bytes`);assert.match(r.text, /Every shared release, and where it shows/);assert.match(r.text, new RegExp(`openvibe-shared@${changes.SHARED_VERSION.replace(/\./g, '\\.')}`));assert.strictEqual((r.text.match(/data-release="/g) || []).length, 12, 'exactly twelve full releases');-- const folded = [...r.text.matchAll(/<summary>([^<]+) · (\d+) releases?<\/summary>/g)];++ const folded = [...r.text.matchAll(/<summary><b>([^<]+)<\/b><span class="chg-month-n">(\d+) releases?<\/span><\/summary>/g)];assert.strictEqual(folded.reduce((n, m) => n + Number(m[2]), 0), all.length - 12, 'fold counts cover every remaining release');assert.strictEqual((r.text.match(/<details class="chg-month">/g) || []).length, folded.length);let last = -1;
@@ -53,3 +53,27 @@
@@ -53,3 +53,27 @@
.chg-shot-row { grid-template-columns: 1fr 1fr; gap: 8px; }
.chg-shot-row { grid-template-columns: 1fr 1fr; gap: 8px; }
.chg-shot figcaption { display: block; }
.chg-shot figcaption { display: block; }
}
}
+
+/* The months after the newest twelve (2026-11-11): each a fold with its count, each release one row. */
+.chg-month { margin: 0 0 8px; border: 1px solid var(--border, #2c2c38); border-radius: 16px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 2%, transparent); overflow: hidden; }
+.chg-month > summary { display: flex; align-items: center; gap: 10px; min-height: 52px; padding: 0 18px; cursor: pointer; list-style: none; }
+.chg-month > summary::-webkit-details-marker { display: none; }
+.chg-month > summary b { font-size: 16px; font-weight: 750; }
+.chg-month-n { margin-left: auto; color: var(--text-muted, #8b93ad); font-size: 13px; font-variant-numeric: tabular-nums; }
+.chg-month > summary::after { content: ""; width: 8px; height: 8px; margin: 0 2px 4px 6px; border: solid var(--text-muted, #8b93ad); border-width: 0 2px 2px 0; transform: rotate(45deg); transition: transform .2s; }
+.chg-month[open] > summary::after { transform: translateY(3px) rotate(225deg); }
+.chg-month > summary:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 7%, transparent); }
+.chg-month > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }
+.chg-month-rows { display: grid; padding: 4px 8px 10px; border-top: 1px solid var(--border, #2c2c38); }
+.chg-month-rows > a { display: grid; grid-template-columns: 5.6em minmax(0, 1fr) auto 10px; align-items: center; gap: 12px; min-height: 44px; padding: 6px 10px; border-radius: 10px; color: var(--text-primary, #e6edf7); text-decoration: none; }
+.chg-month-rows > a:hover, .chg-month-rows > a:focus-visible { background: color-mix(in srgb, var(--accent, #3b82f6) 9%, transparent); }
+.chg-month-rows > a > b { font: 650 13px/1 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--accent, #5b9dff); font-variant-numeric: tabular-nums; }
+.chg-month-rows > a > span { color: var(--text-primary, #e6edf7); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14.5px; }
+.chg-month-rows > a > small { color: var(--text-muted, #8b93ad); font-size: 12.5px; font-variant-numeric: tabular-nums; }
+.chg-month-rows > a > i { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #5b9dff); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent); }
+@media (max-width: 640px) {
+ .chg-month-rows > a { grid-template-columns: 5em minmax(0, 1fr) 10px; grid-template-rows: auto auto; row-gap: 2px; }
+ .chg-month-rows > a > span { grid-column: 2; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
+ .chg-month-rows > a > small { grid-column: 2; grid-row: 2; }
+ .chg-month-rows > a > i { grid-column: 3; grid-row: 1; }
+}
@@ -47,7 +47,8 @@ function releaseCard(r) {
@@ -47,7 +47,8 @@ function releaseCard(r) {
function releaseRow(r) {
function releaseRow(r) {
const hasShots = screenshots(r.version).length > 0;
const hasShots = screenshots(r.version).length > 0;
- return `<a href="/changes/${esc(r.version)}">${esc(r.version)} · ${esc(r.title)} · ${esc(r.date)}${hasShots ? ' ●' : ''}</a>`;
+ // Lean on purpose (328 rows): the fold's styles read the parts by element, so no class or attribute per row.
+ return `<a href="/changes/${esc(r.version)}"><b>${esc(r.version)}</b><span>${esc(r.title)}</span><small>${esc(r.date)}</small>${hasShots ? '<i title="Has screenshots"></i>' : ''}</a>`;
}
}
function monthLabel(date) {
function monthLabel(date) {
@@ -90,7 +91,7 @@ ${kit.stats([[all.length, 'releases'], [counted, 'with screenshots'], [pin.versi
@@ -90,7 +91,7 @@ ${kit.stats([[all.length, 'releases'], [counted, 'with screenshots'], [pin.versi
if (!monthGroups.has(key)) monthGroups.set(key, { label: monthLabel(r.date), releases: [] });
if (!monthGroups.has(key)) monthGroups.set(key, { label: monthLabel(r.date), releases: [] });
monthGroups.get(key).releases.push(r);
monthGroups.get(key).releases.push(r);
}
}
- const folds = [...monthGroups.values()].map((group) => `<details class="chg-month"><summary>${esc(group.label)} · ${group.releases.length} ${group.releases.length === 1 ? 'release' : 'releases'}</summary>${group.releases.map(releaseRow).join('')}</details>`).join('');
+ const folds = [...monthGroups.values()].map((group) => `<details class="chg-month"><summary><b>${esc(group.label)}</b><span class="chg-month-n">${group.releases.length} ${group.releases.length === 1 ? 'release' : 'releases'}</span></summary><div class="chg-month-rows">${group.releases.map(releaseRow).join('')}</div></details>`).join('');
return layout({
return layout({
title: `Changes: ${all.length} OpenVibe.Shared releases`,
title: `Changes: ${all.length} OpenVibe.Shared releases`,
description: `Every OpenVibe.Shared release in the pinned changelog, newest first: what changed, the component pages it touches, the rules it carries out, and its screenshots.`,
description: `Every OpenVibe.Shared release in the pinned changelog, newest first: what changed, the component pages it touches, the rules it carries out, and its screenshots.`,
@@ -22,11 +22,12 @@ const { PUBLIC_DIR } = require('../server/assets');
@@ -22,11 +22,12 @@ const { PUBLIC_DIR } = require('../server/assets');
await check('/changes lists every release in the pinned CHANGELOG, newest first, in its own words', async () => {
await check('/changes lists every release in the pinned CHANGELOG, newest first, in its own words', async () => {
const r = await t.get('/changes');
const r = await t.get('/changes');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
- assert.ok(Buffer.byteLength(r.text) < 120 * 1024, `${Buffer.byteLength(r.text)} bytes`);
+ // 136 KB (2026-11-11, was 120): each folded release is a row with its version, title and date as parts.
+ assert.ok(Buffer.byteLength(r.text) < 136 * 1024, `${Buffer.byteLength(r.text)} bytes`);
assert.match(r.text, /Every shared release, and where it shows/);
assert.match(r.text, /Every shared release, and where it shows/);
assert.match(r.text, new RegExp(`openvibe-shared@${changes.SHARED_VERSION.replace(/\./g, '\\.')}`));
assert.match(r.text, new RegExp(`openvibe-shared@${changes.SHARED_VERSION.replace(/\./g, '\\.')}`));
assert.strictEqual((r.text.match(/data-release="/g) || []).length, 12, 'exactly twelve full releases');
assert.strictEqual((r.text.match(/data-release="/g) || []).length, 12, 'exactly twelve full releases');
- const folded = [...r.text.matchAll(/<summary>([^<]+) · (\d+) releases?<\/summary>/g)];
+ const folded = [...r.text.matchAll(/<summary><b>([^<]+)<\/b><span class="chg-month-n">(\d+) releases?<\/span><\/summary>/g)];
assert.strictEqual(folded.reduce((n, m) => n + Number(m[2]), 0), all.length - 12, 'fold counts cover every remaining release');
assert.strictEqual(folded.reduce((n, m) => n + Number(m[2]), 0), all.length - 12, 'fold counts cover every remaining release');
assert.strictEqual((r.text.match(/<details class="chg-month">/g) || []).length, folded.length);
assert.strictEqual((r.text.match(/<details class="chg-month">/g) || []).length, folded.length);
let last = -1;
let last = -1;