openvibers/openvibe.shared Commits

4.36.1: charts never widen a phone page, and kit headings hold their look on every site (#173)

OP OpenVibers committed 15 days ago · 7795adc4ee · 10 files, +36 −19

- A chart's screen-reader table sat in a 1 px box, but a table cannot shrink below its content, so a board with long
  model names laid a 390 px phone page out at 601 px (Report, Quality and cost). The table now sits in a hidden div.
  The chart also clips to its own box, and kit-web.js moves a scatter label that would cross the right edge to the
  left of its dot.
- A site rule like `.rp h2` has the same specificity as the kit's `.ovk-section > h2` and loads later, so it restyled
  kit headings (Report drew a section title at display size). Heading and paragraph rules in kit.css and room.css
  double their class (`.ovk-section.ovk-section > h2`, `.ovk-card .ovk-card-title`). No markup changed.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 40ba2a3..2484737 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,17 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
++## 4.36.1 — 2026-10-29
++
++- **Kit 2 charts:** a scatter label right of centre could run past the chart and widen a phone page (a 390 px
++ phone laid the page out at 601 px). The chart clips to its own box, and kit-web.js moves a label that would cross the
++ right edge to the left of its dot. The real cause on Report was the screen-reader copy of the numbers: a table
++ cannot shrink below its content, so even visually hidden it set the page's width; it now sits in a hidden box.
++- **Kit headings hold their look on every site:** a site rule such as `.rp h2` had the same specificity as the kit's
++ `.ovk-section > h2` and loads later, so it restyled kit headings (Report showed a section title at display size).
++ The kit's heading and paragraph rules now double their class (`.ovk-section.ovk-section > h2`, `.ovk-card .ovk-card-title`),
++ which outranks any one-class site rule. No markup changed.
++
## 4.36.0 — 2026-10-29
- **Kit 2:** the pieces data and tool pages need, in `kit.js`/`kit.css` (`kit-web.js` adds their behaviour):
ddiff --git a/README.md b/README.md
iindex 1b4d8a1..9dcf754 100644
---- a/README.md
++++ b/README.md
@@@ -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.36.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.36.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
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
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex a741d69..2703afe 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.36.0",
++ "version": "4.36.1",
"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)",
ddiff --git a/kit-web.js b/kit-web.js
iindex 142e203..3203221 100644
---- a/kit-web.js
++++ b/kit-web.js
@@@ -201,7 +201,10 @@
const names = Array.prototype.slice.call(plot.querySelectorAll('.ovk-chart-name'));
if (names.length < 2) return;
const place = () => {
-- names.forEach((n) => n.classList.remove('is-hidden'));
++ names.forEach((n) => { n.classList.remove('is-hidden'); n.parentNode.classList.remove('is-flip'); });
++ // A label that would run past the plot's right edge goes to the left of its dot.
++ const edge = plot.getBoundingClientRect().right + 4;
++ names.forEach((n) => { if (!n.parentNode.classList.contains('is-right') && n.getBoundingClientRect().right > edge) n.parentNode.classList.add('is-flip'); });
const order = names.slice().sort((a, b) => (b.parentNode.classList.contains('is-best') - a.parentNode.classList.contains('is-best')) || (parseFloat(b.parentNode.style.bottom) - parseFloat(a.parentNode.style.bottom)));
const kept = [];
order.forEach((n) => {
ddiff --git a/kit.css b/kit.css
iindex 94f11ac..37320cc 100644
---- a/kit.css
++++ b/kit.css
@@@ -33,6 +33,9 @@
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
}
++/* Heading and paragraph rules double their class (.ovk-section.ovk-section > h2): a site's own ".site h2" has the same
++ * specificity as ".ovk-section > h2" and loads later, so it used to restyle the kit's headings (Report, 2026-10-29). */
++
/* ── Basics ── */
.ovk-i { width: 1.15em; height: 1.15em; flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -.2em; }
.ovk-sr { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
@@@ -76,7 +79,7 @@
.ovk-head-row { display: flex; align-items: center; gap: 6px; min-height: 48px; padding-bottom: 6px; }
.ovk-head-row > .ovk-back { margin-left: -8px; }
.ovk-head-id { flex: 1; min-width: 0; }
--.ovk-head-id h1, .ovk-head-id h2 { margin: 0; color: var(--ovk-text); font-family: var(--ovk-font); font-size: clamp(1.15rem, 4.2vw, 1.5rem); font-weight: 800; letter-spacing: -.01em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.ovk-head-id.ovk-head-id h1, .ovk-head-id.ovk-head-id h2 { margin: 0; color: var(--ovk-text); font-family: var(--ovk-font); font-size: clamp(1.15rem, 4.2vw, 1.5rem); font-weight: 800; letter-spacing: -.01em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }
.ovk-head-tools form { margin: 0; }
@@@ -90,7 +93,7 @@
/* ── Lists: rows that read like a phone's (a mark, the name and time on one line, one line under it) ── */
.ovk-list-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 18px 0 8px; }
--.ovk-list-head h2 { margin: 0; color: var(--ovk-muted); font-family: inherit; font-size: .76rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
++.ovk-list-head.ovk-list-head h2 { margin: 0; color: var(--ovk-muted); font-family: inherit; font-size: .76rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
.ovk-list-head a { color: var(--ovk-accent); font-size: .84rem; font-weight: 700; text-decoration: none; }
.ovk-list { list-style: none; display: grid; grid-template-columns: minmax(0, 1fr); margin: 0 0 14px; padding: 0; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; }
.ovk-list--flush { margin: 0; border: 0; border-radius: 0; border-top: 1px solid var(--ovk-line); background: transparent; }
@@@ -117,7 +120,7 @@
/* ── Sections (settings groups, a page's parts) and folds ── */
.ovk-section { margin: 0 0 12px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
--.ovk-section > h2 { display: flex; align-items: center; gap: 6px; margin: 0; padding: 12px 14px 6px; color: var(--ovk-muted); font-family: inherit; font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
++.ovk-section.ovk-section > h2 { display: flex; align-items: center; gap: 6px; margin: 0; padding: 12px 14px 6px; color: var(--ovk-muted); font-family: inherit; font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
.ovk-count { color: var(--ovk-text); font-weight: 800; letter-spacing: 0; }
.ovk-note { margin: 0; padding: 0 14px 10px; color: var(--ovk-muted); font-size: .86rem; }
.ovk-section > .ovk-form, .ovk-section > form:not(.ovk-find):not(.ovk-inline) { padding: 4px 14px 14px; }
@@@ -171,7 +174,7 @@ div.ovk-set-row { cursor: default; }
.ovk-form input:not([type=checkbox]):not([type=radio]):not([type=range]), .ovk-form select, .ovk-form textarea { width: 100%; box-sizing: border-box; min-height: var(--ovk-tap); padding: 10px 12px; border-radius: 10px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 1rem; }
.ovk-form textarea { min-height: 96px; resize: vertical; }
.ovk-form input:focus-visible, .ovk-form select:focus-visible, .ovk-form textarea:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 0; border-color: transparent; }
--.ovk-form > p { margin: 0; }
++.ovk-form.ovk-form > p { margin: 0; }
/* A settings row inside a form stays a row: the label on the left, its control (not stretched) on the right. */
.ovk-form label.ovk-set-row { display: flex; color: var(--ovk-text); font-size: 1rem; }
.ovk-form .ovk-set-row select { width: auto; min-height: 0; padding: 7px 8px; font-size: .9rem; }
@@@ -212,7 +215,7 @@ div.ovk-set-row { cursor: default; }
.ovk-stats dd { margin: 0; color: var(--ovk-text); font-size: 1.2rem; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; }
.ovk-empty { display: grid; justify-items: center; gap: 8px; margin: 0 0 14px; padding: 24px 16px; border: 1px dashed var(--ovk-line); border-radius: var(--ovk-radius); color: var(--ovk-muted); text-align: center; }
.ovk-empty > .ovk-i { width: 28px; height: 28px; }
--.ovk-empty p { margin: 0; max-width: 46ch; }
++.ovk-empty.ovk-empty p { margin: 0; max-width: 46ch; }
.ovk-empty .ovk-empty-title { color: var(--ovk-text); font-weight: 750; }
.ovk-notice { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 12px; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); font-size: .92rem; }
.ovk-notice > .ovk-i { margin-top: 2px; }
@@@ -229,7 +232,7 @@ div.ovk-set-row { cursor: default; }
.ovk-sheet::backdrop { background: rgba(0, 0, 0, .55); }
.ovk-sheet[open] { display: flex; flex-direction: column; }
.ovk-sheet-head { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--ovk-line); }
--.ovk-sheet-head h2 { flex: 1; margin: 0; font-size: 1.05rem; }
++.ovk-sheet-head.ovk-sheet-head h2 { flex: 1; margin: 0; font-size: 1.05rem; }
.ovk-sheet-body { padding: 14px 16px calc(16px + env(safe-area-inset-bottom, 0px)); overflow-y: auto; }
/* ── Phones (< 600 px): bottom sheets, full-width forms ── */
@@@ -317,7 +320,7 @@ div.ovk-set-row { cursor: default; }
}
/* Charts: the lines are an SVG stretched to the box; every label is real text at its real size */
--.ovk-chart { margin: 0 0 14px; padding: 14px 14px 10px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); color: var(--ovk-text); font-family: var(--ovk-font); }
++.ovk-chart { overflow: clip; margin: 0 0 14px; padding: 14px 14px 10px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); color: var(--ovk-text); font-family: var(--ovk-font); }
.ovk-chart figcaption { display: grid; gap: 2px; margin: 0 0 8px; }
.ovk-chart figcaption strong { font-size: 1rem; font-weight: 800; }
.ovk-chart figcaption span { color: var(--ovk-muted); font-size: .84rem; }
@@@ -353,7 +356,7 @@ div.ovk-set-row { cursor: default; }
.ovk-chart-dot.is-right .ovk-chart-pop { left: auto; right: -6px; transform: none; }
.ovk-chart-name.is-hidden { visibility: hidden; }
.ovk-chart-name { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ovk-text); font-size: .74rem; font-weight: 600; white-space: nowrap; pointer-events: none; text-shadow: 0 0 3px var(--ovk-panel), 0 0 3px var(--ovk-panel); }
--.ovk-chart-dot.is-right .ovk-chart-name { left: auto; right: 14px; }
++.ovk-chart-dot.is-right .ovk-chart-name, .ovk-chart-dot.is-flip .ovk-chart-name { left: auto; right: 14px; }
.ovk-chart-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: clamp(1px, calc(12% / var(--n, 10)), 6px); }
.ovk-chart-bar { position: relative; flex: 1 1 0; min-width: 2px; height: var(--h, 0%); border-radius: 3px 3px 0 0; background: var(--ovk-c1); outline: none; }
.ovk-chart-bar:hover, .ovk-chart-bar:focus-visible { filter: brightness(1.15); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ovk-accent) 45%, transparent); }
@@@ -414,7 +417,7 @@ div.ovk-set-row { cursor: default; }
.ovk-receipt--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, var(--ovk-line)); }
.ovk-receipt--bad { border-color: color-mix(in srgb, var(--ovk-bad) 45%, var(--ovk-line)); }
.ovk-receipt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 8px 0 14px; min-height: 44px; }
--.ovk-receipt-head h2 { margin: 0; font-size: 1rem; font-weight: 800; }
++.ovk-receipt-head.ovk-receipt-head h2 { margin: 0; font-size: 1rem; font-weight: 800; }
.ovk-receipt-acts { display: inline-flex; gap: 2px; }
.ovk-receipt-body { padding: 10px 14px 14px; line-height: 1.5; overflow-wrap: anywhere; }
.ovk-receipt-body > :first-child { margin-top: 0; } .ovk-receipt-body > :last-child { margin-bottom: 0; }
ddiff --git a/kit.js b/kit.js
iindex 4cca87c..501ce43 100644
---- a/kit.js
++++ b/kit.js
@@@ -576,7 +576,7 @@
const axisNames = yo.label ? `<span class="ovk-chart-yname" aria-hidden="true">${esc(yo.label)}</span>` : '';
return `<figure class="ovk-chart ovk-chart--${kind}"${id ? ` id="${esc(id)}"` : ''} style="--ovk-chart-h:${Math.max(120, Math.min(600, Number(height) || 220))}px;--ovk-chart-g:${gutter}px">${cap}${legend}${axisNames}`
+ `<div class="ovk-chart-plot"${data ? ` tabindex="0" role="img" aria-label="${esc(name)}" data-ovk-chart="${esc(JSON.stringify(data))}"` : ` role="group" aria-label="${esc(name)}"`}>${plot}${data ? '<span class="ovk-chart-rule" hidden></span><span class="ovk-chart-tip" hidden></span>' : ''}</div>`
-- + `${xo.label ? `<p class="ovk-chart-xname" aria-hidden="true">${esc(xo.label)}</p>` : ''}<table class="ovk-sr"><caption>${esc(name)}</caption>${table}</table></figure>`;
++ + `${xo.label ? `<p class="ovk-chart-xname" aria-hidden="true">${esc(xo.label)}</p>` : ''}<div class="ovk-sr"><table><caption>${esc(name)}</caption>${table}</table></div></figure>`;
}
/** The grid and the tick labels: x [[pos%, text]], y [[pos%, text]] — real text, placed by percent. */
function axes(xt, yt) {
ddiff --git a/package-lock.json b/package-lock.json
iindex f8a578c..6b282e4 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.36.0",
++ "version": "4.36.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.36.0",
++ "version": "4.36.1",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
ddiff --git a/package.json b/package.json
iindex fdf725f..527a33c 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.36.0",
++ "version": "4.36.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/room.css b/room.css
iindex 406804d..a2187b7 100644
---- a/room.css
++++ b/room.css
@@@ -27,7 +27,7 @@
.ovk-card-body { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
.ovk-card-body > .ovk-mark { flex: none; }
.ovk-card-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
--.ovk-card-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
++.ovk-card .ovk-card-title { margin: 0; font-size: 15px; font-weight: 700; line-height: 1.3; letter-spacing: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
.ovk-card-link { color: inherit; text-decoration: none; transition: color .2s; }
.ovk-card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: var(--ovk-radius); }
.ovk-card-link:focus-visible { outline: none; }
@@@ -48,7 +48,7 @@
/* ── Rails (kit.rail): a row that scrolls sideways and snaps ── */
.ovk-rail { margin: 26px 0 0; min-width: 0; --ovk-rail-bleed: 0px; }
.ovk-rail-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 12px; }
--.ovk-rail-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; }
++.ovk-rail-head.ovk-rail-head h2 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.01em; }
.ovk-rail-more { display: inline-flex; align-items: center; gap: 2px; min-height: 32px; color: var(--room-strong, var(--ovk-accent)); font-weight: 700; font-size: .9rem; text-decoration: none; white-space: nowrap; }
.ovk-rail-more:hover { text-decoration: underline; }
.ovk-rail-wrap { position: relative; }
ddiff --git a/test/kit.test.js b/test/kit.test.js
iindex 6bb12b5..4f7bb74 100644
---- a/test/kit.test.js
++++ b/test/kit.test.js
@@@ -155,7 +155,7 @@ const kit = require('../kit');
assert.ok(line.includes('class="ovk-chart-yt"') && line.includes('$12.00'), 'y ticks are real text, formatted');
assert.ok(line.includes('>Sep 1<') || line.includes('>Sep 2<'), 'x ticks fall on days');
assert.ok(line.includes('Price &lt;x&gt; <b>$9.99</b>'), 'the legend says the latest value, escaped');
-- assert.ok(line.includes('<table class="ovk-sr"><caption>Price</caption>'), 'a table with the same numbers for screen readers');
++ assert.ok(line.includes('<div class="ovk-sr"><table><caption>Price</caption>'), 'a table with the same numbers for screen readers, in a box that can be 1 px wide (a table cannot)');
const raw = line.match(/data-ovk-chart="([^"]*)"/)[1].replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&#39;/g, "'").replace(/&amp;/g, '&');
const data = JSON.parse(raw);
assert.strictEqual(data.x.length, 4);