openvibers/openvibe.shared Commits
Shared: the updates log no longer jumps while it loads (one standalone loading line, own chips up front) (#69)
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).++## Unreleased++++- **The updates log no longer jumps while it loads.** `shipped.js` log showed "Loading…" inside the days, then the site++ chips and the patch notes arrived above it and pushed it down (CLS 0.17 on openvibe.live/updates on a phone). The++ first load now shows one standalone "Loading…" line, a site's own chips are drawn before the data, and the chips,++ patch notes and days land together.++## 4.1.2 — 2026-10-12- **"Recently shipped" tells visitors what changed, not which library was bumped.** The one-liner and the short@@@ -300,7 +300,10 @@const days = el('div', { class: 'ov-shipped-days' });days.style.cssText = 'display:flex;flex-direction:column;gap:24px;min-width:0';const more = el('button', { type: 'button', class: 'ov-shipped-more', hidden: '' }, 'Load more');-- node.append(bar, posts, days, more);++ // The first load shows one "Loading…" line on its own (and a site's own chips, known up front), so the chips,++ // the patch notes and the days arrive together instead of pushing a visible line down the page (layout shift).++ const loading = el('p', { class: 'ov-shipped-empty' }, 'Loading…');++ node.append(bar, posts, loading, days, more);let next = null;let seq = 0;@@@ -342,7 +345,7 @@if (reset) {next = null;days.textContent = '';-- days.appendChild(el('p', { class: 'ov-shipped-empty' }, 'Loading…'));++ if (!loading.parentNode) days.appendChild(el('p', { class: 'ov-shipped-empty' }, 'Loading…'));for (const b of bar.querySelectorAll('.ov-shipped-chip')) b.setAttribute('aria-pressed', 'false');}more.disabled = true;@@@ -350,6 +353,7 @@if (mine !== seq) return;if (reset) {days.textContent = '';++ loading.remove();if (data && data.sites) sitesSeen = data.sites;if (!own && !sitesSeen) {const all = await feed({ service: null, limit: 1, api: o.api });@@@ -373,6 +377,7 @@tick();}more.addEventListener('click', () => load(false));++ if (own) drawBar(null);await load(true);return node;}@@@ -123,6 +123,21 @@ const data = {assert.ok(logEl.textContent.includes('four'));assert.strictEqual(more.hidden, true, 'no next cursor: no button');++ // log()'s first load: one standalone "Loading…" line and the site's own chips up front, nothing inside the days++ // that the chips and patch notes would push down; all of it lands together.++ let release;++ globalThis.fetch = async () => { await new Promise((r) => { release = r; }); return { ok: true, json: async () => page2 }; };++ const steady = document.createElement('div');++ const done = shipped.log(steady, { service: 'wiki', api: 'https://example.test/api/v1/changelog-steady' });++ await new Promise((r) => setTimeout(r, 0));++ assert.deepStrictEqual([...steady.querySelectorAll('.ov-shipped-chip')].map((b) => b.textContent), ['Wiki', 'All of OpenVibe'], 'own chips before the data');++ assert.deepStrictEqual([...steady.children].map((c) => c.className), ['ov-shipped-bar', 'ov-shipped-posts', 'ov-shipped-empty', 'ov-shipped-days', 'ov-shipped-more']);++ assert.strictEqual(steady.querySelector('.ov-shipped-days').children.length, 0, 'the days stay empty while loading');++ release();++ await done;++ assert.strictEqual(steady.querySelectorAll('.ov-shipped-empty').length, 0, 'the loading line is gone once the data is in');++ assert.ok(steady.textContent.includes('four'));++// scan(): markup mounts once.globalThis.fetch = async () => ({ ok: true, json: async () => data });const autoEl = document.createElement('section');
@@ -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).
+## Unreleased
+
+- **The updates log no longer jumps while it loads.** `shipped.js` log showed "Loading…" inside the days, then the site
+ chips and the patch notes arrived above it and pushed it down (CLS 0.17 on openvibe.live/updates on a phone). The
+ first load now shows one standalone "Loading…" line, a site's own chips are drawn before the data, and the chips,
+ patch notes and days land together.
+
## 4.1.2 — 2026-10-12
## 4.1.2 — 2026-10-12
- **"Recently shipped" tells visitors what changed, not which library was bumped.** The one-liner and the short
- **"Recently shipped" tells visitors what changed, not which library was bumped.** The one-liner and the short
@@ -300,7 +300,10 @@
@@ -300,7 +300,10 @@
const days = el('div', { class: 'ov-shipped-days' });
const days = el('div', { class: 'ov-shipped-days' });
days.style.cssText = 'display:flex;flex-direction:column;gap:24px;min-width:0';
days.style.cssText = 'display:flex;flex-direction:column;gap:24px;min-width:0';
const more = el('button', { type: 'button', class: 'ov-shipped-more', hidden: '' }, 'Load more');
const more = el('button', { type: 'button', class: 'ov-shipped-more', hidden: '' }, 'Load more');
- node.append(bar, posts, days, more);
+ // The first load shows one "Loading…" line on its own (and a site's own chips, known up front), so the chips,
+ // the patch notes and the days arrive together instead of pushing a visible line down the page (layout shift).
+ const loading = el('p', { class: 'ov-shipped-empty' }, 'Loading…');
+ node.append(bar, posts, loading, days, more);
let next = null;
let next = null;
let seq = 0;
let seq = 0;
@@ -342,7 +345,7 @@
@@ -342,7 +345,7 @@
if (reset) {
if (reset) {
next = null;
next = null;
days.textContent = '';
days.textContent = '';
- days.appendChild(el('p', { class: 'ov-shipped-empty' }, 'Loading…'));
+ if (!loading.parentNode) days.appendChild(el('p', { class: 'ov-shipped-empty' }, 'Loading…'));
for (const b of bar.querySelectorAll('.ov-shipped-chip')) b.setAttribute('aria-pressed', 'false');
for (const b of bar.querySelectorAll('.ov-shipped-chip')) b.setAttribute('aria-pressed', 'false');
}
}
more.disabled = true;
more.disabled = true;
@@ -350,6 +353,7 @@
@@ -350,6 +353,7 @@
if (mine !== seq) return;
if (mine !== seq) return;
if (reset) {
if (reset) {
days.textContent = '';
days.textContent = '';
+ loading.remove();
if (data && data.sites) sitesSeen = data.sites;
if (data && data.sites) sitesSeen = data.sites;
if (!own && !sitesSeen) {
if (!own && !sitesSeen) {
const all = await feed({ service: null, limit: 1, api: o.api });
const all = await feed({ service: null, limit: 1, api: o.api });
@@ -373,6 +377,7 @@
@@ -373,6 +377,7 @@
tick();
tick();
}
}
more.addEventListener('click', () => load(false));
more.addEventListener('click', () => load(false));
+ if (own) drawBar(null);
await load(true);
await load(true);
return node;
return node;
}
}
@@ -123,6 +123,21 @@ const data = {
@@ -123,6 +123,21 @@ const data = {
assert.ok(logEl.textContent.includes('four'));
assert.ok(logEl.textContent.includes('four'));
assert.strictEqual(more.hidden, true, 'no next cursor: no button');
assert.strictEqual(more.hidden, true, 'no next cursor: no button');
+ // log()'s first load: one standalone "Loading…" line and the site's own chips up front, nothing inside the days
+ // that the chips and patch notes would push down; all of it lands together.
+ let release;
+ globalThis.fetch = async () => { await new Promise((r) => { release = r; }); return { ok: true, json: async () => page2 }; };
+ const steady = document.createElement('div');
+ const done = shipped.log(steady, { service: 'wiki', api: 'https://example.test/api/v1/changelog-steady' });
+ await new Promise((r) => setTimeout(r, 0));
+ assert.deepStrictEqual([...steady.querySelectorAll('.ov-shipped-chip')].map((b) => b.textContent), ['Wiki', 'All of OpenVibe'], 'own chips before the data');
+ assert.deepStrictEqual([...steady.children].map((c) => c.className), ['ov-shipped-bar', 'ov-shipped-posts', 'ov-shipped-empty', 'ov-shipped-days', 'ov-shipped-more']);
+ assert.strictEqual(steady.querySelector('.ov-shipped-days').children.length, 0, 'the days stay empty while loading');
+ release();
+ await done;
+ assert.strictEqual(steady.querySelectorAll('.ov-shipped-empty').length, 0, 'the loading line is gone once the data is in');
+ assert.ok(steady.textContent.includes('four'));
+
// scan(): markup mounts once.
// scan(): markup mounts once.
globalThis.fetch = async () => ({ ok: true, json: async () => data });
globalThis.fetch = async () => ({ ok: true, json: async () => data });
const autoEl = document.createElement('section');
const autoEl = document.createElement('section');