openvibers/openvibe.media Commits
Media: an empty browse list draws the kit's empty state (#381)
Changes
@@@ -406,7 +406,7 @@ function renderPage(tab, page, data, counts, { home = false } = {}) {// On the home the list is a section like the ones around it (the same inset, so its heading, tabs and grid line up// with the hero and the developer sections); later pages and tabs keep the plain index layout.const list = `<nav class="tabs" aria-label="Media types">${TABS.map(([k, label]) => `<a class="${k === tab ? 'on' : ''}" href="/?tab=${k}">${label}<span class="n">${(counts[k] ?? 0).toLocaleString()}</span></a>`).join('')}</nav>--${data.cards.length ? `<div class="grid">${data.cards.map(renderCard).join('')}</div>` : '<div class="empty">Nothing here yet.</div>'}++${data.cards.length ? `<div class="grid">${data.cards.map(renderCard).join('')}</div>` : kit.empty({ icon: 'grid', title: 'Nothing here yet', text: 'No public media is in this list yet.', action: { href: '/me', label: 'Open your media', icon: 'plus' } })}<div class="pager">${nav(page - 1, '‹ Prev', page <= 1)}<span>Page ${page} / ${pages} · ${data.total.toLocaleString()} items</span>${nav(page + 1, 'Next ›', page >= pages)}</div>`;const body = `${heading}${first ? `<section class="sc-sec sc-media">${kit.rail({ id: 'media', wrap: true, title: 'Latest public media', more: { href: '/?tab=all', label: `See all ${data.total.toLocaleString('en-US')}` }, items: data.cards.slice(0, 12).map(asCard) })}<p class="sc-kinds" aria-label="Browse by kind">${TABS.filter(([k]) => k !== 'all' && (counts[k] ?? 0) > 0).map(([k, label]) => kit.button({ href: `/?tab=${k}`, label: `${label} ${(counts[k] ?? 0).toLocaleString('en-US')}`, kind: 'quiet', small: true })).join(' ')}</p></section>` : list}@@@ -421,6 +421,7 @@ ${first ? foldBlock({ id: 'shipped', head: '<h2 id="shipped-h">Recently shipped},css: BROWSE_CSS + (rules.length ? `@supports selector(:has(*)){@media (max-width:640px){${rules.join('')}}}` : ''),kit: first,++ kitCss: !data.cards.length && !first,body,// Later pages are noindex and carry no structured data, the summary's WebPage included.summary: page === 1 ? {@@@ -90,7 +90,7 @@ function fmtDate(dt) {* `shell.page`'s `head`, so the shell still owns title, description, canonical, robots, og/twitter* mirroring and JSON-LD.*/--function extraHead(seo, css, kit = false) {++function extraHead(seo, css, kit = false, kitCss = false) {const image = seo.image || DEFAULT_OG_IMAGE;const videoTags = seo.video && seo.video.url ? [`<meta property="og:video" content="${esc(seo.video.url)}">`,@@@ -104,6 +104,8 @@ function extraHead(seo, css, kit = false) {// A page with the kit's cards and rails (the front page's latest media) carries room.css, their look; it needs// neither kit.css nor kit-web.js (no menus on these cards, and wide screens wrap the rail instead of scrolling it).kit ? `<link rel="stylesheet" href="${ovServe.url('room.css')}">` : '',++ // An empty list draws the kit's empty state, whose look and button live in kit.css: load it only on that page.++ kitCss ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">` : '',// A page with a video plays it in the network's player (openvibe-shared player.js/css on video[data-ov-player]).seo.video ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>` : '',`<style>${baseCss()}${css}</style>`,@@@ -213,7 +215,7 @@ function oneGraph(nodes) {return { '@context': 'https://schema.org', '@graph': [].concat(nodes).map(({ '@context': _context, ...node }) => node) };}--function page({ seo, css = '', body, history, footer, summary, agentLayer = false, kit = false }) {++function page({ seo, css = '', body, history, footer, summary, agentLayer = false, kit = false, kitCss = false }) {const sum = summary == null ? null : typeof summary === 'object' ? summary : { summary };const summaryData = sum ? seoLib.pageSummary({ title: seo.title, summary: sum.summary, facts: sum.facts, url: sum.url || seo.canonical, updated: sum.updated, ...(sum.name ? { title: sum.name } : {}) }) : null;const html = shell.page({@@@ -229,7 +231,7 @@ function page({ seo, css = '', body, history, footer, summary, agentLayer = falstype: seo.ogType,robots: seo.robots || 'index, follow',jsonLd: oneGraph([...[].concat(seo.jsonLd || []), ...(summaryData ? [summaryData.jsonLd] : [])]),-- head: extraHead(seo, css, kit) + (summaryData && summaryData.meta ? `<meta name="ai-summary" content="${summaryData.meta}">` : '') + footerBootScript(footer),++ head: extraHead(seo, css, kit, kitCss) + (summaryData && summaryData.meta ? `<meta name="ai-summary" content="${summaryData.meta}">` : '') + footerBootScript(footer),body: `${summaryData ? summaryData.body : ''}<div id="navbar-mount"></div>\n<main id="main">\n${body}\n</main>`,navLinks: [{ label: 'Videos', href: '/?tab=videos' }, { label: 'Clips', href: '/?tab=clips' }],navbar: navOptions({ history }),@@@ -34,6 +34,14 @@ const path = require('path');assert.strictEqual(r.status, 200);const text = r.body;++ // The index with no public media draws the shared kit's empty state and one button forward.++ const all = await get('/?tab=all');++ assert.strictEqual(all.status, 200);++ assert.match(all.body, /class="ovk-empty"/, 'an empty browse list draws the kit empty state');++ assert.match(all.body, /No public media is in this list yet/);++ assert.match(all.body, /class="ovk-btn[^"]*" href="\/me"/, 'the empty state offers one button forward');++ assert.match(all.body, /\/shared\/kit\.css\?v=[0-9a-f]+/, 'the page loads kit.css for it');++// The code samples: one pick block, a chip per sample, the first one picked.assert.ok(text.includes('<div class="pick" id="pick-api">'), 'the code samples are one pick block');assert.match(text, /<nav class="pick-row" aria-label="The ways to call Media">/);
@@ -406,7 +406,7 @@ function renderPage(tab, page, data, counts, { home = false } = {}) {
@@ -406,7 +406,7 @@ function renderPage(tab, page, data, counts, { home = false } = {}) {
// On the home the list is a section like the ones around it (the same inset, so its heading, tabs and grid line up
// On the home the list is a section like the ones around it (the same inset, so its heading, tabs and grid line up
// with the hero and the developer sections); later pages and tabs keep the plain index layout.
// with the hero and the developer sections); later pages and tabs keep the plain index layout.
const list = `<nav class="tabs" aria-label="Media types">${TABS.map(([k, label]) => `<a class="${k === tab ? 'on' : ''}" href="/?tab=${k}">${label}<span class="n">${(counts[k] ?? 0).toLocaleString()}</span></a>`).join('')}</nav>
const list = `<nav class="tabs" aria-label="Media types">${TABS.map(([k, label]) => `<a class="${k === tab ? 'on' : ''}" href="/?tab=${k}">${label}<span class="n">${(counts[k] ?? 0).toLocaleString()}</span></a>`).join('')}</nav>
-${data.cards.length ? `<div class="grid">${data.cards.map(renderCard).join('')}</div>` : '<div class="empty">Nothing here yet.</div>'}
+${data.cards.length ? `<div class="grid">${data.cards.map(renderCard).join('')}</div>` : kit.empty({ icon: 'grid', title: 'Nothing here yet', text: 'No public media is in this list yet.', action: { href: '/me', label: 'Open your media', icon: 'plus' } })}
<div class="pager">${nav(page - 1, '‹ Prev', page <= 1)}<span>Page ${page} / ${pages} · ${data.total.toLocaleString()} items</span>${nav(page + 1, 'Next ›', page >= pages)}</div>`;
<div class="pager">${nav(page - 1, '‹ Prev', page <= 1)}<span>Page ${page} / ${pages} · ${data.total.toLocaleString()} items</span>${nav(page + 1, 'Next ›', page >= pages)}</div>`;
const body = `${heading}
const body = `${heading}
${first ? `<section class="sc-sec sc-media">${kit.rail({ id: 'media', wrap: true, title: 'Latest public media', more: { href: '/?tab=all', label: `See all ${data.total.toLocaleString('en-US')}` }, items: data.cards.slice(0, 12).map(asCard) })}<p class="sc-kinds" aria-label="Browse by kind">${TABS.filter(([k]) => k !== 'all' && (counts[k] ?? 0) > 0).map(([k, label]) => kit.button({ href: `/?tab=${k}`, label: `${label} ${(counts[k] ?? 0).toLocaleString('en-US')}`, kind: 'quiet', small: true })).join(' ')}</p></section>` : list}
${first ? `<section class="sc-sec sc-media">${kit.rail({ id: 'media', wrap: true, title: 'Latest public media', more: { href: '/?tab=all', label: `See all ${data.total.toLocaleString('en-US')}` }, items: data.cards.slice(0, 12).map(asCard) })}<p class="sc-kinds" aria-label="Browse by kind">${TABS.filter(([k]) => k !== 'all' && (counts[k] ?? 0) > 0).map(([k, label]) => kit.button({ href: `/?tab=${k}`, label: `${label} ${(counts[k] ?? 0).toLocaleString('en-US')}`, kind: 'quiet', small: true })).join(' ')}</p></section>` : list}
@@ -421,6 +421,7 @@ ${first ? foldBlock({ id: 'shipped', head: '<h2 id="shipped-h">Recently shipped
@@ -421,6 +421,7 @@ ${first ? foldBlock({ id: 'shipped', head: '<h2 id="shipped-h">Recently shipped
},
},
css: BROWSE_CSS + (rules.length ? `@supports selector(:has(*)){@media (max-width:640px){${rules.join('')}}}` : ''),
css: BROWSE_CSS + (rules.length ? `@supports selector(:has(*)){@media (max-width:640px){${rules.join('')}}}` : ''),
kit: first,
kit: first,
+ kitCss: !data.cards.length && !first,
body,
body,
// Later pages are noindex and carry no structured data, the summary's WebPage included.
// Later pages are noindex and carry no structured data, the summary's WebPage included.
summary: page === 1 ? {
summary: page === 1 ? {
@@ -90,7 +90,7 @@ function fmtDate(dt) {
@@ -90,7 +90,7 @@ function fmtDate(dt) {
* `shell.page`'s `head`, so the shell still owns title, description, canonical, robots, og/twitter
* `shell.page`'s `head`, so the shell still owns title, description, canonical, robots, og/twitter
* mirroring and JSON-LD.
* mirroring and JSON-LD.
*/
*/
-function extraHead(seo, css, kit = false) {
+function extraHead(seo, css, kit = false, kitCss = false) {
const image = seo.image || DEFAULT_OG_IMAGE;
const image = seo.image || DEFAULT_OG_IMAGE;
const videoTags = seo.video && seo.video.url ? [
const videoTags = seo.video && seo.video.url ? [
`<meta property="og:video" content="${esc(seo.video.url)}">`,
`<meta property="og:video" content="${esc(seo.video.url)}">`,
@@ -104,6 +104,8 @@ function extraHead(seo, css, kit = false) {
@@ -104,6 +104,8 @@ function extraHead(seo, css, kit = false) {
// A page with the kit's cards and rails (the front page's latest media) carries room.css, their look; it needs
// A page with the kit's cards and rails (the front page's latest media) carries room.css, their look; it needs
// neither kit.css nor kit-web.js (no menus on these cards, and wide screens wrap the rail instead of scrolling it).
// neither kit.css nor kit-web.js (no menus on these cards, and wide screens wrap the rail instead of scrolling it).
kit ? `<link rel="stylesheet" href="${ovServe.url('room.css')}">` : '',
kit ? `<link rel="stylesheet" href="${ovServe.url('room.css')}">` : '',
+ // An empty list draws the kit's empty state, whose look and button live in kit.css: load it only on that page.
+ kitCss ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">` : '',
// A page with a video plays it in the network's player (openvibe-shared player.js/css on video[data-ov-player]).
// A page with a video plays it in the network's player (openvibe-shared player.js/css on video[data-ov-player]).
seo.video ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>` : '',
seo.video ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>` : '',
`<style>${baseCss()}${css}</style>`,
`<style>${baseCss()}${css}</style>`,
@@ -213,7 +215,7 @@ function oneGraph(nodes) {
@@ -213,7 +215,7 @@ function oneGraph(nodes) {
return { '@context': 'https://schema.org', '@graph': [].concat(nodes).map(({ '@context': _context, ...node }) => node) };
return { '@context': 'https://schema.org', '@graph': [].concat(nodes).map(({ '@context': _context, ...node }) => node) };
}
}
-function page({ seo, css = '', body, history, footer, summary, agentLayer = false, kit = false }) {
+function page({ seo, css = '', body, history, footer, summary, agentLayer = false, kit = false, kitCss = false }) {
const sum = summary == null ? null : typeof summary === 'object' ? summary : { summary };
const sum = summary == null ? null : typeof summary === 'object' ? summary : { summary };
const summaryData = sum ? seoLib.pageSummary({ title: seo.title, summary: sum.summary, facts: sum.facts, url: sum.url || seo.canonical, updated: sum.updated, ...(sum.name ? { title: sum.name } : {}) }) : null;
const summaryData = sum ? seoLib.pageSummary({ title: seo.title, summary: sum.summary, facts: sum.facts, url: sum.url || seo.canonical, updated: sum.updated, ...(sum.name ? { title: sum.name } : {}) }) : null;
const html = shell.page({
const html = shell.page({
@@ -229,7 +231,7 @@ function page({ seo, css = '', body, history, footer, summary, agentLayer = fals
@@ -229,7 +231,7 @@ function page({ seo, css = '', body, history, footer, summary, agentLayer = fals
type: seo.ogType,
type: seo.ogType,
robots: seo.robots || 'index, follow',
robots: seo.robots || 'index, follow',
jsonLd: oneGraph([...[].concat(seo.jsonLd || []), ...(summaryData ? [summaryData.jsonLd] : [])]),
jsonLd: oneGraph([...[].concat(seo.jsonLd || []), ...(summaryData ? [summaryData.jsonLd] : [])]),
- head: extraHead(seo, css, kit) + (summaryData && summaryData.meta ? `<meta name="ai-summary" content="${summaryData.meta}">` : '') + footerBootScript(footer),
+ head: extraHead(seo, css, kit, kitCss) + (summaryData && summaryData.meta ? `<meta name="ai-summary" content="${summaryData.meta}">` : '') + footerBootScript(footer),
body: `${summaryData ? summaryData.body : ''}<div id="navbar-mount"></div>\n<main id="main">\n${body}\n</main>`,
body: `${summaryData ? summaryData.body : ''}<div id="navbar-mount"></div>\n<main id="main">\n${body}\n</main>`,
navLinks: [{ label: 'Videos', href: '/?tab=videos' }, { label: 'Clips', href: '/?tab=clips' }],
navLinks: [{ label: 'Videos', href: '/?tab=videos' }, { label: 'Clips', href: '/?tab=clips' }],
navbar: navOptions({ history }),
navbar: navOptions({ history }),
@@ -34,6 +34,14 @@ const path = require('path');
@@ -34,6 +34,14 @@ const path = require('path');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
const text = r.body;
const text = r.body;
+ // The index with no public media draws the shared kit's empty state and one button forward.
+ const all = await get('/?tab=all');
+ assert.strictEqual(all.status, 200);
+ assert.match(all.body, /class="ovk-empty"/, 'an empty browse list draws the kit empty state');
+ assert.match(all.body, /No public media is in this list yet/);
+ assert.match(all.body, /class="ovk-btn[^"]*" href="\/me"/, 'the empty state offers one button forward');
+ assert.match(all.body, /\/shared\/kit\.css\?v=[0-9a-f]+/, 'the page loads kit.css for it');
+
// The code samples: one pick block, a chip per sample, the first one picked.
// The code samples: one pick block, a chip per sample, the first one picked.
assert.ok(text.includes('<div class="pick" id="pick-api">'), 'the code samples are one pick block');
assert.ok(text.includes('<div class="pick" id="pick-api">'), 'the code samples are one pick block');
assert.match(text, /<nav class="pick-row" aria-label="The ways to call Media">/);
assert.match(text, /<nav class="pick-row" aria-label="The ways to call Media">/);