openvibers/openvibe.shared Commits
4.93.2: a folded footer column says how many links it holds (#251)
Changes
@@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).++## 4.93.2 — 2026-11-02++++- **A folded footer column says how many links it holds** (from #208, 10-31). On a phone the full footer's columns are++ folds (4.53.0), and a fold showed only its heading. Each now carries its count on its line ("9 links", "1 link"), the way++ the network's other folds say what they hold. On a desktop, where every column is open, the count is hidden. Every link stays in the++ page.++## 4.93.1 — 2026-11-02- **An `ov:` icon the ring set lacks draws as its glyph** (`showcase.icon`). Before, it drew the bare OV mark: Today's@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.93.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.93.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.93.1",++ "version": "4.93.2","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -281,10 +281,12 @@function fullBody(c, { network, popular, discover, brand, tagline, compact = false }) {// Each link column is a fold: open on a desktop (its heading reads and behaves as a heading), folded on a phone// (a 48 px row with a chevron), so a phone's footer is about one screen shorter. Every link stays in the page.-- const fold = (label, heading, body) => `-- <nav class="ovf-col" aria-label="${esc(label)}"><details class="ovf-fold" open><summary><h3>${esc(heading)}</h3></summary>++ // The fold's line carries its count ("9 links"), so a folded column still says how much it holds (#208).++ const fold = (label, heading, body, count) => `++ <nav class="ovf-col" aria-label="${esc(label)}"><details class="ovf-fold" open><summary><h3>${esc(heading)}</h3><span class="ovf-fold-n">${esc(String(count))} ${count === 1 ? 'link' : 'links'}</span></summary><div class="ovf-fold-body">${body}</div></details></nav>`;-- const cols = (c.links || []).map(group => fold(group.heading, group.heading, group.items.map(linkTag).join(''))).join('');++ const cols = (c.links || []).map(group => fold(group.heading, group.heading, group.items.map(linkTag).join(''), group.items.length)).join('');++ const networkLinks = network.slice(0, 8);return `<div class="ovf-inner${compact ? ' ovf-inner--expanded' : ''}">${c.pulse === false ? '' : '<p class="ovf-pulse" data-ovf-pulse hidden></p>'}@@@ -298,12 +300,12 @@</div></div>${cols}-- ${c.showNetwork ? fold('The OpenVibe network', 'Network', `${network.slice(0, 8).map(linkTag).join('')}-- <a class="ovf-more" href="${NETWORK_URL}" target="_blank" rel="noopener">Everything else ${icon('arrow')}</a>`) : ''}++ ${c.showNetwork ? fold('The OpenVibe network', 'Network', `${networkLinks.map(linkTag).join('')}++ <a class="ovf-more" href="${NETWORK_URL}" target="_blank" rel="noopener">Everything else ${icon('arrow')}</a>`, networkLinks.length + 1) : ''}${popular.length ? fold('Popular tools', 'Popular tools', `${popular.slice(0, 6).concat(discover.slice(0, 2)).map(linkTag).join('')}-- <a class="ovf-more" href="https://openvibe.tools/all-tools">All tools ${icon('arrow')}</a>`) : ''}++ <a class="ovf-more" href="https://openvibe.tools/all-tools">All tools ${icon('arrow')}</a>`, Math.min(popular.length, 6) + Math.min(discover.length, 2) + 1) : ''}${fold('Legal', 'Legal', `${LEGAL.map(l => `<a href="${esc(legalHref(l, c))}">${esc(l.name)}</a>`).join('')}-- ${reportLink()}`)}++ ${reportLink()}`, LEGAL.length + 1)}</div>${compact ? '' : `<div class="ovf-account" id="ovf-account" hidden></div><div class="ovf-bar">@@@ -376,6 +378,7 @@.ovf-fold>summary{list-style:none;cursor:default;pointer-events:none}.ovf-fold>summary::-webkit-details-marker{display:none}.ovf-fold-body{display:flex;flex-direction:column;gap:8px;margin-top:8px}++.ovf-fold-n{display:none}.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}@media (pointer:coarse){.ovf-col a{display:inline-flex;align-items:center;min-height:32px}.ovf-col .ovf-social a{width:44px;height:44px}.ovf-legal-inline a,.ovf-compact-links a{position:relative}.ovf-legal-inline a::after,.ovf-compact-links a::after{content:'';position:absolute;left:0;right:0;top:-12px;bottom:-12px}}@@@ -447,6 +450,7 @@.ovf-col:not(.ovf-col--brand):last-child{border-bottom:1px solid var(--border,rgba(255,255,255,.08))}.ovf-fold>summary{pointer-events:auto;cursor:pointer;display:flex;align-items:center;justify-content:space-between;min-height:48px}.ovf-fold>summary h3{margin:0}++ .ovf-fold-n{display:inline;margin-left:auto;margin-right:10px;color:var(--text-muted,#8b93ad);font-size:.82rem;font-weight:600;font-variant-numeric:tabular-nums}.ovf-fold>summary::after{content:"";width:.5em;height:.5em;margin-right:4px;border:solid var(--text-muted,#8b93ad);border-width:0 1.8px 1.8px 0;transform:translateY(-.2em) rotate(45deg);transition:transform .2s}.ovf-fold[open]>summary::after{transform:translateY(.1em) rotate(-135deg)}.ovf-fold-body{margin-top:0;padding-bottom:14px}@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.93.1",++ "version": "4.93.2","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -15,8 +15,13 @@ const html = footer.buildHTML({ service: 'live', variant: 'full', links });const folds = html.match(/<details class="ovf-fold"[^>]*>/g) || [];assert.ok(folds.length >= 4, `the site's columns, Network and Legal are folds (${folds.length})`);assert.ok(folds.every((d) => / open\b/.test(d)), 'every fold is written open');++// Each fold's line says how many links it holds (4.93.2): a site's own columns, the network's 8 and Everything else, Legal.++const counts = { Watch: null, Create: null, Network: 9, Legal: null };for (const [label, heading] of [['Watch', 'Watch'], ['Create', 'Create'], ['The OpenVibe network', 'Network'], ['Legal', 'Legal']]) {-- const re = new RegExp(`<nav class="ovf-col" aria-label="${label}"><details class="ovf-fold" open><summary><h3>${heading}</h3></summary>`);++ const re = new RegExp(`<nav class="ovf-col" aria-label="${label}"><details class="ovf-fold" open><summary><h3>${heading}</h3><span class="ovf-fold-n">(\\d+) links?</span></summary>`);++ const m = re.exec(html);++ assert.ok(m && Number(m[1]) > 0, `${label}: the fold's line carries its count`);++ if (counts[heading]) assert.strictEqual(Number(m[1]), counts[heading], `${label}: ${counts[heading]} links`);assert.match(html, re, `${label}: a nav, its fold and its heading as the summary`);}for (const href of ['/browse', '/clips', '/go-live']) assert.ok(html.includes(`href="${href}"`), `${href} stays in the page`);
@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,13 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 4.93.2 — 2026-11-02
+
+- **A folded footer column says how many links it holds** (from #208, 10-31). On a phone the full footer's columns are
+ folds (4.53.0), and a fold showed only its heading. Each now carries its count on its line ("9 links", "1 link"), the way
+ the network's other folds say what they hold. On a desktop, where every column is open, the count is hidden. Every link stays in the
+ page.
+
## 4.93.1 — 2026-11-02
## 4.93.1 — 2026-11-02
- **An `ov:` icon the ring set lacks draws as its glyph** (`showcase.icon`). Before, it drew the bare OV mark: Today's
- **An `ov:` icon the ring set lacks draws as its glyph** (`showcase.icon`). Before, it drew the bare OV mark: Today's
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.93.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.93.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.93.1",
+ "version": "4.93.2",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -281,10 +281,12 @@
@@ -281,10 +281,12 @@
function fullBody(c, { network, popular, discover, brand, tagline, compact = false }) {
function fullBody(c, { network, popular, discover, brand, tagline, compact = false }) {
// Each link column is a fold: open on a desktop (its heading reads and behaves as a heading), folded on a phone
// Each link column is a fold: open on a desktop (its heading reads and behaves as a heading), folded on a phone
// (a 48 px row with a chevron), so a phone's footer is about one screen shorter. Every link stays in the page.
// (a 48 px row with a chevron), so a phone's footer is about one screen shorter. Every link stays in the page.
- const fold = (label, heading, body) => `
- <nav class="ovf-col" aria-label="${esc(label)}"><details class="ovf-fold" open><summary><h3>${esc(heading)}</h3></summary>
+ // The fold's line carries its count ("9 links"), so a folded column still says how much it holds (#208).
+ const fold = (label, heading, body, count) => `
+ <nav class="ovf-col" aria-label="${esc(label)}"><details class="ovf-fold" open><summary><h3>${esc(heading)}</h3><span class="ovf-fold-n">${esc(String(count))} ${count === 1 ? 'link' : 'links'}</span></summary>
<div class="ovf-fold-body">${body}</div></details></nav>`;
<div class="ovf-fold-body">${body}</div></details></nav>`;
- const cols = (c.links || []).map(group => fold(group.heading, group.heading, group.items.map(linkTag).join(''))).join('');
+ const cols = (c.links || []).map(group => fold(group.heading, group.heading, group.items.map(linkTag).join(''), group.items.length)).join('');
+ const networkLinks = network.slice(0, 8);
return `<div class="ovf-inner${compact ? ' ovf-inner--expanded' : ''}">
return `<div class="ovf-inner${compact ? ' ovf-inner--expanded' : ''}">
${c.pulse === false ? '' : '<p class="ovf-pulse" data-ovf-pulse hidden></p>'}
${c.pulse === false ? '' : '<p class="ovf-pulse" data-ovf-pulse hidden></p>'}
@@ -298,12 +300,12 @@
@@ -298,12 +300,12 @@
</div>
</div>
</div>
</div>
${cols}
${cols}
- ${c.showNetwork ? fold('The OpenVibe network', 'Network', `${network.slice(0, 8).map(linkTag).join('')}
- <a class="ovf-more" href="${NETWORK_URL}" target="_blank" rel="noopener">Everything else ${icon('arrow')}</a>`) : ''}
+ ${c.showNetwork ? fold('The OpenVibe network', 'Network', `${networkLinks.map(linkTag).join('')}
+ <a class="ovf-more" href="${NETWORK_URL}" target="_blank" rel="noopener">Everything else ${icon('arrow')}</a>`, networkLinks.length + 1) : ''}
${popular.length ? fold('Popular tools', 'Popular tools', `${popular.slice(0, 6).concat(discover.slice(0, 2)).map(linkTag).join('')}
${popular.length ? fold('Popular tools', 'Popular tools', `${popular.slice(0, 6).concat(discover.slice(0, 2)).map(linkTag).join('')}
- <a class="ovf-more" href="https://openvibe.tools/all-tools">All tools ${icon('arrow')}</a>`) : ''}
+ <a class="ovf-more" href="https://openvibe.tools/all-tools">All tools ${icon('arrow')}</a>`, Math.min(popular.length, 6) + Math.min(discover.length, 2) + 1) : ''}
${fold('Legal', 'Legal', `${LEGAL.map(l => `<a href="${esc(legalHref(l, c))}">${esc(l.name)}</a>`).join('')}
${fold('Legal', 'Legal', `${LEGAL.map(l => `<a href="${esc(legalHref(l, c))}">${esc(l.name)}</a>`).join('')}
- ${reportLink()}`)}
+ ${reportLink()}`, LEGAL.length + 1)}
</div>
</div>
${compact ? '' : `<div class="ovf-account" id="ovf-account" hidden></div>
${compact ? '' : `<div class="ovf-account" id="ovf-account" hidden></div>
<div class="ovf-bar">
<div class="ovf-bar">
@@ -376,6 +378,7 @@
@@ -376,6 +378,7 @@
.ovf-fold>summary{list-style:none;cursor:default;pointer-events:none}
.ovf-fold>summary{list-style:none;cursor:default;pointer-events:none}
.ovf-fold>summary::-webkit-details-marker{display:none}
.ovf-fold>summary::-webkit-details-marker{display:none}
.ovf-fold-body{display:flex;flex-direction:column;gap:8px;margin-top:8px}
.ovf-fold-body{display:flex;flex-direction:column;gap:8px;margin-top:8px}
+.ovf-fold-n{display:none}
.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}
.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}
.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
@media (pointer:coarse){.ovf-col a{display:inline-flex;align-items:center;min-height:32px}.ovf-col .ovf-social a{width:44px;height:44px}.ovf-legal-inline a,.ovf-compact-links a{position:relative}.ovf-legal-inline a::after,.ovf-compact-links a::after{content:'';position:absolute;left:0;right:0;top:-12px;bottom:-12px}}
@media (pointer:coarse){.ovf-col a{display:inline-flex;align-items:center;min-height:32px}.ovf-col .ovf-social a{width:44px;height:44px}.ovf-legal-inline a,.ovf-compact-links a{position:relative}.ovf-legal-inline a::after,.ovf-compact-links a::after{content:'';position:absolute;left:0;right:0;top:-12px;bottom:-12px}}
@@ -447,6 +450,7 @@
@@ -447,6 +450,7 @@
.ovf-col:not(.ovf-col--brand):last-child{border-bottom:1px solid var(--border,rgba(255,255,255,.08))}
.ovf-col:not(.ovf-col--brand):last-child{border-bottom:1px solid var(--border,rgba(255,255,255,.08))}
.ovf-fold>summary{pointer-events:auto;cursor:pointer;display:flex;align-items:center;justify-content:space-between;min-height:48px}
.ovf-fold>summary{pointer-events:auto;cursor:pointer;display:flex;align-items:center;justify-content:space-between;min-height:48px}
.ovf-fold>summary h3{margin:0}
.ovf-fold>summary h3{margin:0}
+ .ovf-fold-n{display:inline;margin-left:auto;margin-right:10px;color:var(--text-muted,#8b93ad);font-size:.82rem;font-weight:600;font-variant-numeric:tabular-nums}
.ovf-fold>summary::after{content:"";width:.5em;height:.5em;margin-right:4px;border:solid var(--text-muted,#8b93ad);border-width:0 1.8px 1.8px 0;transform:translateY(-.2em) rotate(45deg);transition:transform .2s}
.ovf-fold>summary::after{content:"";width:.5em;height:.5em;margin-right:4px;border:solid var(--text-muted,#8b93ad);border-width:0 1.8px 1.8px 0;transform:translateY(-.2em) rotate(45deg);transition:transform .2s}
.ovf-fold[open]>summary::after{transform:translateY(.1em) rotate(-135deg)}
.ovf-fold[open]>summary::after{transform:translateY(.1em) rotate(-135deg)}
.ovf-fold-body{margin-top:0;padding-bottom:14px}
.ovf-fold-body{margin-top:0;padding-bottom:14px}
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.93.1",
+ "version": "4.93.2",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -15,8 +15,13 @@ const html = footer.buildHTML({ service: 'live', variant: 'full', links });
@@ -15,8 +15,13 @@ const html = footer.buildHTML({ service: 'live', variant: 'full', links });
const folds = html.match(/<details class="ovf-fold"[^>]*>/g) || [];
const folds = html.match(/<details class="ovf-fold"[^>]*>/g) || [];
assert.ok(folds.length >= 4, `the site's columns, Network and Legal are folds (${folds.length})`);
assert.ok(folds.length >= 4, `the site's columns, Network and Legal are folds (${folds.length})`);
assert.ok(folds.every((d) => / open\b/.test(d)), 'every fold is written open');
assert.ok(folds.every((d) => / open\b/.test(d)), 'every fold is written open');
+// Each fold's line says how many links it holds (4.93.2): a site's own columns, the network's 8 and Everything else, Legal.
+const counts = { Watch: null, Create: null, Network: 9, Legal: null };
for (const [label, heading] of [['Watch', 'Watch'], ['Create', 'Create'], ['The OpenVibe network', 'Network'], ['Legal', 'Legal']]) {
for (const [label, heading] of [['Watch', 'Watch'], ['Create', 'Create'], ['The OpenVibe network', 'Network'], ['Legal', 'Legal']]) {
- const re = new RegExp(`<nav class="ovf-col" aria-label="${label}"><details class="ovf-fold" open><summary><h3>${heading}</h3></summary>`);
+ const re = new RegExp(`<nav class="ovf-col" aria-label="${label}"><details class="ovf-fold" open><summary><h3>${heading}</h3><span class="ovf-fold-n">(\\d+) links?</span></summary>`);
+ const m = re.exec(html);
+ assert.ok(m && Number(m[1]) > 0, `${label}: the fold's line carries its count`);
+ if (counts[heading]) assert.strictEqual(Number(m[1]), counts[heading], `${label}: ${counts[heading]} links`);
assert.match(html, re, `${label}: a nav, its fold and its heading as the summary`);
assert.match(html, re, `${label}: a nav, its fold and its heading as the summary`);
}
}
for (const href of ['/browse', '/clips', '/go-live']) assert.ok(html.includes(`href="${href}"`), `${href} stays in the page`);
for (const href of ['/browse', '/clips', '/go-live']) assert.ok(html.includes(`href="${href}"`), `${href} stays in the page`);