openvibers/openvibe.reviews Commits
Reviews: the front page is a scoreboard (score, meter and what it rests on), with six features (#50)
Changes
@@@ -73,6 +73,18 @@ table { border-collapse: collapse; }@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }/* Reviews: aggregate, signals, summary */.rv-entities { list-style: none; padding: 0; }++/* The front page's board: one card per entity, its score with a meter and what it rests on. */++.rv-board { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); }++.rv-card { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border: 1px solid var(--rv-line); border-radius: 12px; background: var(--rv-panel); color: var(--rv-text); text-decoration: none; }++.rv-card:hover { border-color: var(--rv-accent); }++.rv-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }++.rv-card-head b { font-size: 1.02rem; line-height: 1.3; }++.rv-card-head .rv-tag { flex: none; white-space: nowrap; }++.rv-score { display: flex; align-items: baseline; gap: 6px; margin-top: auto; }++.rv-big { font-size: 1.7rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }++.rv-meter { display: block; height: 6px; border-radius: 3px; background: var(--rv-line); overflow: hidden; }++.rv-meter > span { display: block; height: 100%; border-radius: 3px; background: var(--rv-accent); }++.rv-basis { font-size: 13px; color: var(--rv-muted); }.rv-entities li { padding: 8px 0; border-top: 1px solid var(--rv-line); }.rv-agg-box { background: var(--rv-panel); border: 1px solid var(--rv-line); border-radius: 8px; padding: 10px 14px; }.rv-agg { font-size: 1.15rem; margin: 4px 0; }@@@ -86,16 +86,44 @@ function reviewsShowcase() {{ icon: 'ov:check', title: 'An aggregate you can check', text: 'The inputs and the method are listed. An editor can leave a source out only with the reason shown.' },{ icon: 'ov:history', title: 'Changes on the record', text: 'Replaced and withdrawn signals stay in the history, beside every revision of the aggregate.' },{ icon: 'ov:docs', title: 'Summaries by people', text: 'Written or approved by an editor, citing the signals they rest on, and never carrying a rating.' },++ { icon: 'ov:shield', title: 'Corrections from readers', text: 'Anyone signed in with OpenVibe can send the editors a correction; an accepted one is published with its note.' },++ { icon: 'ov:feeds', title: 'Feeds and an API', text: 'New and changed scores arrive in Atom and JSON Feed, and the API serves the same data to OpenVibe apps.' },],});}++/**++ * One entity on the front page's board: its headline number (the recommendation share, else the rating on its scale,++ * else the rating share), a meter for it, and how many signals it rests on. No aggregate: the card says so.++ */++function boardCard(x) {++ const c = x.aggregate && x.aggregate.result ? x.aggregate.result.components : null;++ let big = null;++ let label = '';++ let share = null;++ let basis = '';++ if (c && c.recommendation) {++ big = `${c.recommendation.percent}%`; label = 'recommend'; share = c.recommendation.percent;++ basis = `${n(c.recommendation.positive)} of ${n(c.recommendation.total)} reviews`;++ } else if (c && c.rating && c.rating.on_scale) {++ big = `${c.rating.on_scale.value}`; label = `out of ${c.rating.on_scale.best}`; share = (100 * c.rating.on_scale.value) / c.rating.on_scale.best;++ basis = `${n(c.rating.count)} ratings`;++ } else if (c && c.rating) {++ big = `${c.rating.percent}%`; label = 'rating'; share = c.rating.percent;++ basis = `${n(c.rating.count)} ratings`;++ }++ const pct = share == null ? 0 : Math.max(0, Math.min(100, Number(share)));++ return html`<li><a class="rv-card" href="${epath(x.entity)}"><span class="rv-card-head"><b>${x.entity.name}</b> <span class="rv-tag">${KIND_LABEL[x.entity.kind]}</span></span>${big ? html`<span class="rv-score"><span class="rv-big">${big}</span> <span class="rv-muted">${label}</span></span>++<span class="rv-meter" aria-hidden="true"><span style="width:${pct.toFixed(1)}%"></span></span><span class="rv-basis">${basis}${c && c.recommendation && c.rating ? html` · ${c.rating.on_scale ? `${c.rating.on_scale.value} / ${c.rating.on_scale.best}` : `${c.rating.percent}%`} from ${n(c.rating.count)} ratings` : ''}</span>`++ : html`<span class="rv-basis">No score yet: no source has said enough.</span>`}</a></li>`;++}++function home({ entities, total, editor }) {return html`${raw(reviewsShowcase())}--<section id="entities"><h2>Entities with source data</h2>--${entities.length ? html`<ul class="rv-entities">${entities.map((x) => html`<li><a href="${epath(x.entity)}"><strong>${x.entity.name}</strong></a> <span class="rv-tag">${KIND_LABEL[x.entity.kind]}</span><br><span class="rv-muted">${aggregateLine(x.aggregate)}</span></li>`)}</ul>`++<section id="entities" class="sc-sec" aria-labelledby="rv-board-h"><h2 id="rv-board-h">What is reviewed</h2>++${entities.length ? html`<p class="sc-lede">Each score comes from named sources. Open one to see every signal behind it, when it was read, and how the number was worked out.</p><ul class="rv-board">${entities.map(boardCard)}</ul>`: html`<p class="rv-muted">Nothing yet. Entities appear here once a source has stated something about them (or an editor has published a summary).</p>`}--<p class="rv-muted">${n(total)} entit${total === 1 ? 'y' : 'ies'} in total. <a href="/feed.atom">Atom</a> · <a href="/feed.json">JSON Feed</a>${editor ? html` · <a href="/editor">Editor desk</a>` : ''}</p></section>`;++<p class="rv-muted">${n(total)} reviewed in total. <a href="/feed.atom">Atom</a> · <a href="/feed.json">JSON Feed</a>${editor ? html` · <a href="/editor">Editor desk</a>` : ''}</p></section>`;}function aboutPage() {@@@ -12,14 +12,14 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 25, // measured 23.7 (fresh database; 17.8 before the showcase hero and features)-- htmlBrotliKB: 6.5, // 5.9 (4.6 before the showcase)++ htmlRawKB: 28.5, // measured 27.0 (fresh database; 23.7 before 2026-10-15's two more features with their ring icons, 17.8 before the showcase)++ htmlBrotliKB: 7, // 6.6 (5.9, 4.6 before the showcase)jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime)jsRawKB: 257, // 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)jsBrotliKB: 61.5, // 56.3 (49.9 before web-runtime.js)cssFiles: 2, // 2 (reviews.css, openvibe-shared showcase.css)-- cssRawKB: 19.5, // 18.2 (6.7 before showcase.css)-- cssBrotliKB: 4.8, // 4.3 (1.7 before showcase.css)++ cssRawKB: 20.5, // 19.4 (18.2 before 2026-10-15's scoreboard, 6.7 before showcase.css)++ cssBrotliKB: 5, // 4.6 (4.3, 1.7 before showcase.css)externalFiles: 1, // 0};
@@ -73,6 +73,18 @@ table { border-collapse: collapse; }
@@ -73,6 +73,18 @@ table { border-collapse: collapse; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
/* Reviews: aggregate, signals, summary */
/* Reviews: aggregate, signals, summary */
.rv-entities { list-style: none; padding: 0; }
.rv-entities { list-style: none; padding: 0; }
+/* The front page's board: one card per entity, its score with a meter and what it rests on. */
+.rv-board { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); }
+.rv-card { display: flex; flex-direction: column; gap: 6px; height: 100%; padding: 14px 16px; border: 1px solid var(--rv-line); border-radius: 12px; background: var(--rv-panel); color: var(--rv-text); text-decoration: none; }
+.rv-card:hover { border-color: var(--rv-accent); }
+.rv-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
+.rv-card-head b { font-size: 1.02rem; line-height: 1.3; }
+.rv-card-head .rv-tag { flex: none; white-space: nowrap; }
+.rv-score { display: flex; align-items: baseline; gap: 6px; margin-top: auto; }
+.rv-big { font-size: 1.7rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
+.rv-meter { display: block; height: 6px; border-radius: 3px; background: var(--rv-line); overflow: hidden; }
+.rv-meter > span { display: block; height: 100%; border-radius: 3px; background: var(--rv-accent); }
+.rv-basis { font-size: 13px; color: var(--rv-muted); }
.rv-entities li { padding: 8px 0; border-top: 1px solid var(--rv-line); }
.rv-entities li { padding: 8px 0; border-top: 1px solid var(--rv-line); }
.rv-agg-box { background: var(--rv-panel); border: 1px solid var(--rv-line); border-radius: 8px; padding: 10px 14px; }
.rv-agg-box { background: var(--rv-panel); border: 1px solid var(--rv-line); border-radius: 8px; padding: 10px 14px; }
.rv-agg { font-size: 1.15rem; margin: 4px 0; }
.rv-agg { font-size: 1.15rem; margin: 4px 0; }
@@ -86,16 +86,44 @@ function reviewsShowcase() {
@@ -86,16 +86,44 @@ function reviewsShowcase() {
{ icon: 'ov:check', title: 'An aggregate you can check', text: 'The inputs and the method are listed. An editor can leave a source out only with the reason shown.' },
{ icon: 'ov:check', title: 'An aggregate you can check', text: 'The inputs and the method are listed. An editor can leave a source out only with the reason shown.' },
{ icon: 'ov:history', title: 'Changes on the record', text: 'Replaced and withdrawn signals stay in the history, beside every revision of the aggregate.' },
{ icon: 'ov:history', title: 'Changes on the record', text: 'Replaced and withdrawn signals stay in the history, beside every revision of the aggregate.' },
{ icon: 'ov:docs', title: 'Summaries by people', text: 'Written or approved by an editor, citing the signals they rest on, and never carrying a rating.' },
{ icon: 'ov:docs', title: 'Summaries by people', text: 'Written or approved by an editor, citing the signals they rest on, and never carrying a rating.' },
+ { icon: 'ov:shield', title: 'Corrections from readers', text: 'Anyone signed in with OpenVibe can send the editors a correction; an accepted one is published with its note.' },
+ { icon: 'ov:feeds', title: 'Feeds and an API', text: 'New and changed scores arrive in Atom and JSON Feed, and the API serves the same data to OpenVibe apps.' },
],
],
});
});
}
}
+/**
+ * One entity on the front page's board: its headline number (the recommendation share, else the rating on its scale,
+ * else the rating share), a meter for it, and how many signals it rests on. No aggregate: the card says so.
+ */
+function boardCard(x) {
+ const c = x.aggregate && x.aggregate.result ? x.aggregate.result.components : null;
+ let big = null;
+ let label = '';
+ let share = null;
+ let basis = '';
+ if (c && c.recommendation) {
+ big = `${c.recommendation.percent}%`; label = 'recommend'; share = c.recommendation.percent;
+ basis = `${n(c.recommendation.positive)} of ${n(c.recommendation.total)} reviews`;
+ } else if (c && c.rating && c.rating.on_scale) {
+ big = `${c.rating.on_scale.value}`; label = `out of ${c.rating.on_scale.best}`; share = (100 * c.rating.on_scale.value) / c.rating.on_scale.best;
+ basis = `${n(c.rating.count)} ratings`;
+ } else if (c && c.rating) {
+ big = `${c.rating.percent}%`; label = 'rating'; share = c.rating.percent;
+ basis = `${n(c.rating.count)} ratings`;
+ }
+ const pct = share == null ? 0 : Math.max(0, Math.min(100, Number(share)));
+ return html`<li><a class="rv-card" href="${epath(x.entity)}"><span class="rv-card-head"><b>${x.entity.name}</b> <span class="rv-tag">${KIND_LABEL[x.entity.kind]}</span></span>${big ? html`<span class="rv-score"><span class="rv-big">${big}</span> <span class="rv-muted">${label}</span></span>
+<span class="rv-meter" aria-hidden="true"><span style="width:${pct.toFixed(1)}%"></span></span><span class="rv-basis">${basis}${c && c.recommendation && c.rating ? html` · ${c.rating.on_scale ? `${c.rating.on_scale.value} / ${c.rating.on_scale.best}` : `${c.rating.percent}%`} from ${n(c.rating.count)} ratings` : ''}</span>`
+ : html`<span class="rv-basis">No score yet: no source has said enough.</span>`}</a></li>`;
+}
+
function home({ entities, total, editor }) {
function home({ entities, total, editor }) {
return html`${raw(reviewsShowcase())}
return html`${raw(reviewsShowcase())}
-<section id="entities"><h2>Entities with source data</h2>
-${entities.length ? html`<ul class="rv-entities">${entities.map((x) => html`<li><a href="${epath(x.entity)}"><strong>${x.entity.name}</strong></a> <span class="rv-tag">${KIND_LABEL[x.entity.kind]}</span><br><span class="rv-muted">${aggregateLine(x.aggregate)}</span></li>`)}</ul>`
+<section id="entities" class="sc-sec" aria-labelledby="rv-board-h"><h2 id="rv-board-h">What is reviewed</h2>
+${entities.length ? html`<p class="sc-lede">Each score comes from named sources. Open one to see every signal behind it, when it was read, and how the number was worked out.</p><ul class="rv-board">${entities.map(boardCard)}</ul>`
: html`<p class="rv-muted">Nothing yet. Entities appear here once a source has stated something about them (or an editor has published a summary).</p>`}
: html`<p class="rv-muted">Nothing yet. Entities appear here once a source has stated something about them (or an editor has published a summary).</p>`}
-<p class="rv-muted">${n(total)} entit${total === 1 ? 'y' : 'ies'} in total. <a href="/feed.atom">Atom</a> · <a href="/feed.json">JSON Feed</a>${editor ? html` · <a href="/editor">Editor desk</a>` : ''}</p></section>`;
+<p class="rv-muted">${n(total)} reviewed in total. <a href="/feed.atom">Atom</a> · <a href="/feed.json">JSON Feed</a>${editor ? html` · <a href="/editor">Editor desk</a>` : ''}</p></section>`;
}
}
function aboutPage() {
function aboutPage() {
@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
@@ -12,14 +12,14 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 25, // measured 23.7 (fresh database; 17.8 before the showcase hero and features)
- htmlBrotliKB: 6.5, // 5.9 (4.6 before the showcase)
+ htmlRawKB: 28.5, // measured 27.0 (fresh database; 23.7 before 2026-10-15's two more features with their ring icons, 17.8 before the showcase)
+ htmlBrotliKB: 7, // 6.6 (5.9, 4.6 before the showcase)
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime)
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell adds web-runtime)
jsRawKB: 257, // 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsRawKB: 257, // 239.1 (212.2 before openvibe-shared/shell's web-runtime.js)
jsBrotliKB: 61.5, // 56.3 (49.9 before web-runtime.js)
jsBrotliKB: 61.5, // 56.3 (49.9 before web-runtime.js)
cssFiles: 2, // 2 (reviews.css, openvibe-shared showcase.css)
cssFiles: 2, // 2 (reviews.css, openvibe-shared showcase.css)
- cssRawKB: 19.5, // 18.2 (6.7 before showcase.css)
- cssBrotliKB: 4.8, // 4.3 (1.7 before showcase.css)
+ cssRawKB: 20.5, // 19.4 (18.2 before 2026-10-15's scoreboard, 6.7 before showcase.css)
+ cssBrotliKB: 5, // 4.6 (4.3, 1.7 before showcase.css)
externalFiles: 1, // 0
externalFiles: 1, // 0
};
};