openvibers/openvibe.space Commits

Use shared page headers across Space and hosting pages (#245)

OP OpenVibers committed 4 days ago · 0b7fc6e074 · 11 files, +65 −58

* Use shared icon headings and actions on Space pages

* Pin the Shared fix for square section icon tiles

* Adopt shared page headers and mark horizontal content rows

* Merge origin/main; pin Shared 4.163.0 with the page head

Keeps main's newer pin round and readme/status, and reads the page-head Shared commit on top of 4.163.0. The home's pick block now grows past 9000 characters, so the home-short test reads it to the end of main instead of a fixed slice.

* Merge main's 4.163.0 pin round; keep the page-head pin

Takes main's newer v4.163.0 pin, readme and status, and its home-short slice fix, while keeping the page-head commit (1369721) on top of 4.163.0 and the scroll-hint assertion.

* Merge remote-tracking branch 'origin/main' into ui/sections-space-1109

# Conflicts:
#	package-lock.json
#	package.json

* Merge remote-tracking branch 'origin/main' into ui/sections-space-1109

Changes

ddiff --git a/package.json b/package.json
iindex 74b9605..c397ec9 100644
---- a/package.json
++++ b/package.json
@@@ -2,7 +2,7 @@
"name": "openvibe-space",
"version": "0.1.0",
"private": true,
-- "description": "OpenVibe.Space \u2014 staff-only static hosting for the OpenVibe network; dynamic pages and spaces are planned. Forum URLs redirect to OpenVibe.Community.",
++ "description": "OpenVibe.Space — staff-only static hosting for the OpenVibe network; dynamic pages and spaces are planned. Forum URLs redirect to OpenVibe.Community.",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
ddiff --git a/server/render/hosting-pages.js b/server/render/hosting-pages.js
iindex 48111f7..91cf987 100644
---- a/server/render/hosting-pages.js
++++ b/server/render/hosting-pages.js
@@@ -1,4 +1,5 @@
'use strict';
++const kit = require('openvibe-shared/kit');
/**
* Dashboard page bodies. Every value is escaped; every action is a POST form carrying the form
* token, so the dashboard works without JavaScript. Styles: public/css/hosting.css (hd-*), loaded
@@@ -40,8 +41,8 @@ function home({ projects, csrf, canCreate = true }) {
<label class="hd-field">Name <input name="name" required maxlength="80" placeholder="My website"></label>
<label class="hd-field">Environment <select name="environment"><option value="production">production</option><option value="sandbox">sandbox (smaller quotas, noindex, no custom domains)</option></select></label>
</div><button type="submit" class="hd-btn hd-btn--primary">${icon('fa-plus')} Create project</button>`, { cls: 'hd-form' });
-- return `<header class="hd-head"><p class="hd-eyebrow">${icon('fa-globe')} Static hosting</p><h1>Your projects</h1>
--<p class="hd-sub">A project holds sites, their deploys and domains, and the people who may change them.</p></header>
++ return `<div class="hd-head"><p class="hd-eyebrow">${icon('fa-globe')} Static hosting</p>${kit.pageHead({ title: `Your projects`, icon: 'server', back: { href: '/', label: 'Home' } })}
++<p class="hd-sub">A project holds sites, their deploys and domains, and the people who may change them.</p></div>
${projects.length ? `<div class="hd-tiles">${cards}</div>` : `<div class="hd-empty">${icon('fa-folder')}<p>You have no projects yet.</p></div>`}
${card('New project', create, { sub: canCreate ? 'Production sites can take custom domains; a sandbox is for trying things and is never indexed.' : '' })}`;
}
@@@ -70,14 +71,14 @@ function project({ project: p, role, sites, quota, usage, members, csrf, siteUrl
<span class="hd-row__side">${s.active_deploy_id ? `${badge('live', 'ok')} <code>${short(s.active_deploy_id)}</code>` : badge('nothing published')}</span></li>`).join('');
const memberRows = members.map((m) => `<tr><td><code>${esc(m.principal)}</code></td><td>${badge(m.role, m.role === 'owner' ? 'accent' : 'neutral')}</td><td class="hd-actions">${role === 'owner' && m.principal !== p.owner_subject
? form(`/projects/${p.id}/members/remove`, csrf, `<input type="hidden" name="principal" value="${esc(m.principal)}"><button type="submit" class="hd-link hd-link--danger">Remove</button>`, { cls: 'hd-inline' }) : ''}</td></tr>`).join('');
-- return `<header class="hd-head">${crumbs([['/dashboard', 'Projects']])}
--<h1>${esc(p.name)}</h1>
--<p class="hd-chips">${badge(p.environment, p.environment === 'sandbox' ? 'warn' : 'accent')} ${badge(`your role: ${role}`)} <code>${esc(p.id)}</code>${p.network_project_id ? ` <span class="hd-muted">Network project <code>${esc(p.network_project_id)}</code></span>` : ''}</p></header>
++ return `<div class="hd-head">${crumbs([['/dashboard', 'Projects']])}
++${kit.pageHead({ title: `${p.name}`, icon: 'server', back: { href: '/', label: 'Home' } })}
++<p class="hd-chips">${badge(p.environment, p.environment === 'sandbox' ? 'warn' : 'accent')} ${badge(`your role: ${role}`)} <code>${esc(p.id)}</code>${p.network_project_id ? ` <span class="hd-muted">Network project <code>${esc(p.network_project_id)}</code></span>` : ''}</p></div>
<div class="hd-split">
<div class="hd-split__main">
${card('Sites', `${sites.length ? `<ul class="hd-rows">${siteRows}</ul>` : `<div class="hd-empty hd-empty--small">${icon('fa-globe')}<p>No sites yet.</p></div>`}
${canMaintain ? form(`/projects/${p.id}/sites`, csrf, `<label class="hd-field">New site name <input name="name" required pattern="[a-z0-9-]{3,40}" maxlength="40" placeholder="my-site"></label><button type="submit" class="hd-btn hd-btn--primary">${icon('fa-plus')} Create site</button>`, { cls: 'hd-form hd-form--row' }) : ''}`, { sub: 'Each site is served at its own address under openvibe.website.' })}
--${card('Members', `<div class="hd-table-wrap"><table class="hd-table"><thead><tr><th>Principal</th><th>Role</th><th></th></tr></thead><tbody>${memberRows}</tbody></table></div>
++${card('Members', `<div class="hd-table-wrap" data-ovk-scroll><table class="hd-table"><thead><tr><th>Principal</th><th>Role</th><th></th></tr></thead><tbody>${memberRows}</tbody></table></div>
${role === 'owner' ? form(`/projects/${p.id}/members`, csrf, `<label class="hd-field">Principal <input name="principal" required placeholder="usr_… or app:app_…"></label>
<label class="hd-field">Role <select name="role"><option value="deployer">deployer</option><option value="maintainer">maintainer</option></select></label><button type="submit" class="hd-btn">Add or change</button>`, { cls: 'hd-form hd-form--row' }) : ''}`, { sub: 'Deployers upload and activate; maintainers also manage sites, domains and configuration.' })}
</div>
@@@ -93,7 +94,7 @@ function domainBlock(d, instructions, csrf) {
let body = `<li class="hd-domain"><div class="hd-domain__head"><strong>${esc(d.hostname)}</strong> ${badge(d.kind)} ${status}</div>${d.last_error ? `<p class="hd-muted">${esc(d.last_error)}</p>` : ''}`;
if (instructions) {
const rec = (r) => `<tr><td>${badge(r.type)}</td><td><code>${esc(r.name)}</code></td><td><code class="hd-wrap">${esc(r.value)}</code></td><td class="hd-muted">${esc(r.note)}</td></tr>`;
-- body += `<details class="hd-details"${d.status === 'verified' ? '' : ' open'}><summary>DNS records</summary><div class="hd-table-wrap"><table class="hd-table hd-table--dns"><thead><tr><th>Type</th><th>Name</th><th>Value</th><th></th></tr></thead><tbody>`
++ body += `<details class="hd-details"${d.status === 'verified' ? '' : ' open'}><summary>DNS records</summary><div class="hd-table-wrap" data-ovk-scroll><table class="hd-table hd-table--dns"><thead><tr><th>Type</th><th>Name</th><th>Value</th><th></th></tr></thead><tbody>`
+ rec(instructions.verification) + instructions.routing.map(rec).join('')
+ `</tbody></table></div><p class="hd-muted">${esc(instructions.tls)}</p></details>`;
body += '<div class="hd-domain__actions">';
@@@ -123,9 +124,9 @@ function site({ site: s, project: p, role, deploys, activations, domains, siteCo
const history = activations.slice(0, 10).map((a) => `<li>${when(a.created_at)} <span>${esc(a.kind)} → <code>${short(a.deploy_id)}</code>${a.previous_deploy_id ? ` <span class="hd-muted">(was <code>${short(a.previous_deploy_id)}</code>)</span>` : ''} <span class="hd-muted">by <code>${esc(a.actor)}</code></span></span></li>`).join('');
const status = s.active_deploy_id ? `${badge('live', 'ok')} <code>${short(s.active_deploy_id)}</code>` : badge('nothing published');
const rollback = canDeploy && s.active_deploy_id ? form(`/sites/${s.id}/rollback`, csrf, `<input type="hidden" name="expected_active" value="${esc(s.active_deploy_id)}"><button type="submit" class="hd-btn hd-btn--small">${icon('fa-clock-rotate-left')} Roll back to the previous deploy</button>`, { cls: 'hd-inline' }) : '';
-- return `<header class="hd-head">${crumbs([['/dashboard', 'Projects'], [`/projects/${p.id}`, p.name]])}
--<h1>${esc(s.name)}</h1>
--<p class="hd-chips"><a class="hd-url hd-url--big" href="${esc(url)}" rel="noopener">${esc(url.replace(/^https?:\/\//, ''))} ${icon('fa-arrow-up-right-from-square')}</a> ${status} <code>${esc(s.id)}</code></p></header>
++ return `<div class="hd-head">${crumbs([['/dashboard', 'Projects'], [`/projects/${p.id}`, p.name]])}
++${kit.pageHead({ title: `${s.name}`, icon: 'server', back: { href: '/', label: 'Home' } })}
++<p class="hd-chips"><a class="hd-url hd-url--big" href="${esc(url)}" rel="noopener">${esc(url.replace(/^https?:\/\//, ''))} ${icon('fa-arrow-up-right-from-square')}</a> ${status} <code>${esc(s.id)}</code></p></div>
${canDeploy ? card('Upload a deploy', form(`/sites/${s.id}/deploys`, csrf, `<div class="hd-sources">
<fieldset class="hd-source"><legend>${icon('fa-folder-open')} A folder</legend><input type="file" name="files" multiple webkitdirectory aria-label="A folder to upload"><input type="hidden" name="strip" value="folder"></fieldset>
<fieldset class="hd-source"><legend>${icon('fa-box-archive')} …or an archive (.tar.gz / .tar)</legend><input type="file" name="archive" accept=".tar,.tgz,.gz,application/gzip,application/x-tar" aria-label="An archive to upload (.tar.gz or .tar)"></fieldset>
@@@ -139,7 +140,7 @@ ${canDeploy ? card('Upload a deploy', form(`/sites/${s.id}/deploys`, csrf, `<div
<button type="submit" class="hd-btn hd-btn--primary">${icon('fa-cloud-arrow-up')} Upload</button>`, { cls: 'hd-form', enctype: 'multipart/form-data' }),
{ sub: 'Static files only: HTML, CSS, JavaScript, JSON, images, fonts, audio, video, PDF, WebAssembly. No build runs and nothing is executed. Hidden files (except <code>.well-known/</code>), links and server-side code are refused.' }) : ''}
${card('Deploys', `${previewId ? `<p class="hd-notice">${icon('fa-eye')} A preview of <code>${short(previewId)}</code> is open to members of this project at <a href="${esc(previewHref(previewId))}" target="_blank" rel="noopener">${esc(previewHref(previewId))}</a> until ${when(Number(s.preview_expires_at))}. It is never indexed, and activating or rolling back ends it.</p>` : ''}
--${deploys.length ? `<div class="hd-table-wrap"><table class="hd-table"><thead><tr><th>Deploy</th><th>State</th><th class="hd-num">Files</th><th class="hd-num">Size</th><th>Uploaded</th><th></th></tr></thead><tbody>${rows}</tbody></table></div>` : `<div class="hd-empty hd-empty--small">${icon('fa-box-open')}<p>No deploys yet.</p></div>`}
++${deploys.length ? `<div class="hd-table-wrap" data-ovk-scroll><table class="hd-table"><thead><tr><th>Deploy</th><th>State</th><th class="hd-num">Files</th><th class="hd-num">Size</th><th>Uploaded</th><th></th></tr></thead><tbody>${rows}</tbody></table></div>` : `<div class="hd-empty hd-empty--small">${icon('fa-box-open')}<p>No deploys yet.</p></div>`}
${history ? `<details class="hd-details"><summary>Activation history</summary><ul class="hd-history">${history}</ul></details>` : ''}`, { actions: rollback, sub: 'Every deploy is kept. Activating one switches the site at once; nothing is rebuilt.' })}
${card('Domains', `<ul class="hd-domains">${domains.map((d) => domainBlock(d.domain, d.instructions, csrf)).join('')}</ul>
${canMaintain && p.environment !== 'sandbox' ? form(`/sites/${s.id}/domains`, csrf, `<label class="hd-field">Custom domain <input name="hostname" required placeholder="www.example.org"></label><button type="submit" class="hd-btn">${icon('fa-plus')} Add domain</button>`, { cls: 'hd-form hd-form--row' }) : ''}`, { sub: 'A custom domain is served once its TXT record is found.' })}
@@@ -155,17 +156,17 @@ function deploy({ deploy: d, site: s, project: p, files, log, active }) {
const LEVEL_TONE = { error: 'bad', warn: 'warn', info: 'neutral' };
const logLines = log.map((l) => `<li class="hd-log__line hd-log__line--${esc(l.level)}">${badge(l.level, LEVEL_TONE[l.level] || 'neutral')}<span>${esc(l.message)}</span></li>`).join('');
const fileRows = files.slice(0, 2000).map((f) => `<tr><td><code class="hd-wrap">${esc(f.path)}</code></td><td class="hd-muted">${esc(f.content_type)}</td><td class="hd-num">${bytes(f.size)}</td><td><code title="${esc(f.sha256)}">${esc(f.sha256.slice(0, 12))}</code></td></tr>`).join('');
-- return `<header class="hd-head">${crumbs([['/dashboard', 'Projects'], [`/projects/${p.id}`, p.name], [`/sites/${s.id}`, s.name]])}
--<h1>Deploy <code>${short(d.id)}</code></h1>
++ return `<div class="hd-head">${crumbs([['/dashboard', 'Projects'], [`/projects/${p.id}`, p.name], [`/sites/${s.id}`, s.name]])}
++${kit.pageHead({ title: `Deploy ${short(d.id)}`, icon: 'server', back: { href: '/', label: 'Home' } })}
<p class="hd-chips">${badge(d.state, STATE_TONE[d.state] || 'neutral')}${active ? ` ${badge('active', 'accent')}` : ''}</p>
--<dl class="hd-facts hd-facts--wide"><div><dt>Files</dt><dd>${esc(d.file_count)}</dd></div><div><dt>Size</dt><dd>${bytes(d.total_bytes)}</dd></div><div><dt>Uploaded</dt><dd>${when(d.created_at)}</dd></div><div><dt>By</dt><dd><code>${esc(d.created_by)}</code></dd></div>${d.manifest_sha256 ? `<div><dt>Manifest</dt><dd><code title="${esc(d.manifest_sha256)}">${esc(d.manifest_sha256.slice(0, 16))}</code></dd></div>` : ''}</dl></header>
++<dl class="hd-facts hd-facts--wide"><div><dt>Files</dt><dd>${esc(d.file_count)}</dd></div><div><dt>Size</dt><dd>${bytes(d.total_bytes)}</dd></div><div><dt>Uploaded</dt><dd>${when(d.created_at)}</dd></div><div><dt>By</dt><dd><code>${esc(d.created_by)}</code></dd></div>${d.manifest_sha256 ? `<div><dt>Manifest</dt><dd><code title="${esc(d.manifest_sha256)}">${esc(d.manifest_sha256.slice(0, 16))}</code></dd></div>` : ''}</dl></div>
${card('Upload log', `<ul class="hd-log">${logLines}</ul>`)}
--${files.length ? card('Files', `<div class="hd-table-wrap"><table class="hd-table"><thead><tr><th>Path</th><th>Type</th><th class="hd-num">Size</th><th>sha256</th></tr></thead><tbody>${fileRows}</tbody></table></div>${files.length > 2000 ? `<p class="hd-muted">…and ${files.length - 2000} more.</p>` : ''}`, { sub: 'Stored once by content: a file that did not change since an earlier deploy of this project is not stored again.' }) : ''}`;
++${files.length ? card('Files', `<div class="hd-table-wrap" data-ovk-scroll><table class="hd-table"><thead><tr><th>Path</th><th>Type</th><th class="hd-num">Size</th><th>sha256</th></tr></thead><tbody>${fileRows}</tbody></table></div>${files.length > 2000 ? `<p class="hd-muted">…and ${files.length - 2000} more.</p>` : ''}`, { sub: 'Stored once by content: a file that did not change since an earlier deploy of this project is not stored again.' }) : ''}`;
}
function errorPage({ status, message }) {
return `<div class="hd-error">${icon(status === 404 ? 'fa-circle-question' : status === 403 ? 'fa-lock' : status === 401 ? 'fa-right-to-bracket' : 'fa-triangle-exclamation')}
--<h1>${status === 404 ? 'Not found' : status === 403 ? 'Not allowed' : status === 401 ? 'Sign in first' : 'Something went wrong'}</h1><p>${esc(message)}</p><p><a class="hd-btn" href="/dashboard">Back to your projects</a></p></div>`;
++${kit.pageHead({ title: `${status === 404 ? 'Not found' : status === 403 ? 'Not allowed' : status === 401 ? 'Sign in first' : 'Something went wrong'}`, icon: 'server', back: { href: '/', label: 'Home' } })}<p>${esc(message)}</p><p><a class="hd-btn" href="/dashboard">Back to your projects</a></p></div>`;
}
module.exports = { home, project, site, deploy, errorPage, bytes };
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 420b9e8..4e19297 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -136,14 +136,14 @@ function renderPage(o) {
summary: description, url: canonical, updated: o.modified,
navbar: nav, home: '/', navLinks: nav.links.map(({ label, href }) => ({ label, href })),
footer: { service: 'space', variant: 'auto', path: o.canonicalPath, updates: '/updates' }, // full on the front page only (Shared 4.19)
-- head: [
++ head: [`<script src="${ovServe.url('kit-web.js')}" defer></script>`,
o.published ? `<meta property="article:published_time" content="${escapeHtml(o.published)}">` : '',
o.modified ? `<meta property="article:modified_time" content="${escapeHtml(o.modified)}">` : '',
appIcon.headTags({ site: 'space', iconBase: '/assets' }),
// The shared card is 1200×630; a page's own image carries its own dimensions.
image === DEFAULT_OG_IMAGE ? '<meta property="og:image:width" content="1200">\n<meta property="og:image:height" content="630">' : '',
...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),
-- ...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
++ ...[...new Set(['kit.css', ...(o.styles || [])])].map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
`<link rel="stylesheet" href="${asset('css/space.css')}">`,
// Page-only stylesheets of this site (the dashboard's hosting.css), after space.css; the home page loads none.
...(o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`),
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex 5c3910f..e48d32e 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -1,4 +1,6 @@
'use strict';
++const kit = require('openvibe-shared/kit');
++const sectionIcon = (name) => kit.icon(name, { cls: 'ovk-heading-icon' });
/**
* Page templates. Plain template strings, every value escaped, no framework. The markup is the whole
@@@ -102,7 +104,7 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
source: 'https://github.com/OpenVibers/OpenVibe.Space/blob/main/server/hosting-config.js',
}) },
];
-- const body = sc.hero({
++ const body = sc.hero({ pageHead: { icon: 'home' },
eyebrow: 'OpenVibe.Space · spaces',
title: 'Make a space.',
// The hero's picture is this site's own (2026-11-01): it drew the generic aura, the same on six homes.
@@@ -140,8 +142,8 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
],
}) + foldBlock({
id: 'questions',
-- head: '<h2 id="space-faq-h">Questions</h2>',
-- html: `<section class="sc-sec" id="questions" aria-labelledby="space-faq-h"><h2>Questions</h2>${HOME_FAQ.map((f) => `<details class="space-faq"><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</section>`,
++ head: `<h2 id="space-faq-h" class="ovk-titled">${sectionIcon('help')}Questions</h2>`,
++ html: `<section class="sc-sec" id="questions" aria-labelledby="space-faq-h"><h2 class="ovk-titled">${sectionIcon('help')}Questions</h2>${HOME_FAQ.map((f) => `<details class="space-faq"><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</section>`,
}) + sc.cta({
title: 'Looking for the forum?',
text: 'The forum moved to OpenVibe.Community.',
@@@ -149,7 +151,7 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
});
const site = String(config.publicUrl || config.baseUrl || 'https://openvibe.space').replace(/\/+$/, '');
return renderPage({
-- canonicalPath: '/', active: 'home', styles: [sc.STYLESHEET], body, description: HOME_DESCRIPTION,
++ canonicalPath: '/', active: 'home', styles: ['kit.css', sc.STYLESHEET], body, description: HOME_DESCRIPTION,
agentLayer: true, llms: '/llms.txt',
// The rule that shows the picked part of the home's pick block: one per part, written for this page.
jsonLd: [
@@@ -164,7 +166,7 @@ function errorPage({ status = 500, title = 'Something went wrong', message = '',
const body = `
<section class="error">
<p class="eyebrow">${status}</p>
-- <h1>${esc(title)}</h1>
++ ${kit.pageHead({ title: `${title}`, icon: 'home', back: { href: '/', label: 'Home' } })}
${message ? `<p class="lede">${esc(message)}</p>` : ''}
${links ? `<p><a class="btn" href="/">Home</a> <a class="btn" href="${esc(FORUM_URL)}">The forum</a></p>` : ''}
</section>`;
ddiff --git a/server/render/short.js b/server/render/short.js
iindex 8f46299..e18e17c 100644
---- a/server/render/short.js
++++ b/server/render/short.js
@@@ -26,7 +26,7 @@ function pickBlock({ key, label, head = '', panes }) {
// The pane carries the id the page already links to (an anchor in the hero, for instance), so a link to it still
// opens that pane on a phone (the CSS shows the pane a link points at).
const body = panes.map((p) => `<div class="pick-pane" id="${esc(p.id)}">${p.html}</div>`).join('');
-- return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar"><div class="pick-head"><h2>${esc(head || label)}</h2></div><nav class="pick-row" aria-label="${esc(label)}">${chips}</nav></div><div class="pick-body">${body}</div></div>`;
++ return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar"><div class="pick-head"><h2>${esc(head || label)}</h2></div><nav class="pick-row" data-ovk-scroll aria-label="${esc(label)}">${chips}</nav></div><div class="pick-body">${body}</div></div>`;
}
ddiff --git a/server/render/website.js b/server/render/website.js
iindex e2c7c45..50b8866 100644
---- a/server/render/website.js
++++ b/server/render/website.js
@@@ -63,7 +63,7 @@ function templateCard(t, base) {
function home({ base }) {
const sites = templates.sites();
const body = `<div class="ov-room wb" style="${rooms.style('website')}">
--${sc.hero({
++${sc.hero({ pageHead: { icon: 'home' },
eyebrow: NAME,
title: 'Websites that look finished.',
accent: 'From the first preview.',
@@@ -77,7 +77,7 @@ ${sc.hero({
})}
<section class="sc-sec" id="templates" aria-labelledby="wb-tpl-h">
<div class="wb-head"><h2 id="wb-tpl-h">Start from a real site</h2><p>${sites.length} whole sites, each one page you can change line by line. Open one full size to see it the way your visitors will.</p></div>
--<div class="wb-tpls">${sites.map((t) => templateCard(t, base)).join('')}</div>
++<div class="wb-tpls" data-ovk-scroll>${sites.map((t) => templateCard(t, base)).join('')}</div>
</section>
${sc.steps({ id: 'how', title: 'From idea to address', items: [
{ title: 'Pick a site, or describe yours', text: 'A template is a working site from the first second; OpenVibe AI writes a first version from a sentence.' },
@@@ -102,7 +102,7 @@ ${sc.faq({ id: 'questions', items: FAQ })}
}
function notFound({ base }) {
-- return page({ base, title: `Not found · ${NAME}`, canonicalPath: '/', robots: 'noindex', body: `<div class="ov-room wb" style="${rooms.style('website')}">${sc.hero({
++ return page({ base, title: `Not found · ${NAME}`, canonicalPath: '/', robots: 'noindex', body: `<div class="ov-room wb" style="${rooms.style('website')}">${sc.hero({ pageHead: { icon: 'home' },
eyebrow: NAME, title: 'Nothing lives at this address.',
lede: 'Sites people make live at their own name, like rise-and-crumb.openvibe.website. This is OpenVibe.Website\'s front door.',
actions: [{ label: 'Go to the front door', href: '/', primary: true }],
ddiff --git a/server/spaces/render.js b/server/spaces/render.js
iindex bfc7e5c..f43ea3c 100644
---- a/server/spaces/render.js
++++ b/server/spaces/render.js
@@@ -6,6 +6,7 @@
*/
const sc = require('openvibe-shared/showcase');
const kit = require('openvibe-shared/kit');
++const sectionIcon = (name) => kit.icon(name, { cls: 'ovk-heading-icon' });
const { renderPage, escapeHtml: esc, asset } = require('../render/layout');
const CAP_TEXT = {
@@@ -24,7 +25,7 @@ const ago = (ms) => {
function page(o) {
return renderPage({
title: o.title, description: o.description, canonicalPath: o.path, robots: o.robots || 'index,follow',
-- active: 'spaces', styles: [...(o.showcase ? [sc.STYLESHEET] : []), ...(o.styles || [])], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,
++ active: 'spaces', styles: [...new Set(['kit.css', ...(o.showcase ? [sc.STYLESHEET] : []), ...(o.styles || [])])], localStyles: ['css/spaces.css'], body: o.body, jsonLd: o.jsonLd,
});
}
@@@ -53,7 +54,7 @@ function templateCards(templates, { viewer, csrf, compact = false }) {
}
function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {
-- const hero = sc.hero({
++ const hero = sc.hero({ pageHead: { icon: 'home' },
eyebrow: 'OpenVibe.Space · spaces',
title: 'Make a space.',
accent: 'An app on your OpenVibe account.',
@@@ -64,11 +65,11 @@ function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {
],
});
const list = spaces.length ? `<div class="sp-grid">${spaces.map((s) => card(s)).join('')}</div>`
-- : `<p class="sp-empty">${q ? `No published space matches “${esc(q)}”.` : 'Published spaces appear here. Start one from a template below and run it in the editor right away.'}</p>`;
++ : kit.empty({ icon: 'grid', text: q ? `No published space matches “${q}”.` : 'Published spaces appear here. Start one from a template below and run it in the editor right away.', action: q ? { href: '/spaces', label: 'Clear search', icon: 'search' } : { href: viewer ? '/spaces/new' : signInHref('/spaces/new'), label: 'Make a space', icon: 'plus' } });
const body = `${hero}
<section class="sp-section" aria-labelledby="sp-gallery">
<div class="sp-head">
-- <h2 id="sp-gallery">Spaces people made</h2>
++ <h2 id="sp-gallery" class="ovk-titled">${sectionIcon('members')}Spaces people made</h2>
<form class="sp-search" method="get" action="/spaces" role="search">
<input name="q" value="${esc(q || '')}" placeholder="Search spaces" aria-label="Search spaces" maxlength="80">
<select name="sort" aria-label="Sort"><option value="new"${sort !== 'popular' ? ' selected' : ''}>Newest</option><option value="popular"${sort === 'popular' ? ' selected' : ''}>Most forked</option></select>
@@@ -78,11 +79,11 @@ function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {
${list}
</section>
<section class="sp-section" aria-labelledby="sp-starters">
-- <div class="sp-head"><h2 id="sp-starters">Start from a template</h2><p>Each one is a working app you can change line by line.</p></div>
++ <div class="sp-head"><h2 id="sp-starters" class="ovk-titled">${sectionIcon('grid')}Start from a template</h2><p>Each one is a working app you can change line by line.</p></div>
${templateCards(templates, { viewer, csrf })}
</section>
<section class="sp-section sp-how" aria-labelledby="sp-how">
-- <h2 id="sp-how">What a space can do</h2>
++ <h2 id="sp-how" class="ovk-titled">${sectionIcon('members')}What a space can do</h2>
<div class="sp-how-grid">
<div><i class="fa-solid fa-right-to-bracket" aria-hidden="true"></i><h3>Sign in with OpenVibe</h3><p>Visitors sign in with their OpenVibe account and approve what the space may do, once.</p></div>
<div><i class="fa-solid fa-list" aria-hidden="true"></i><h3>Shared lists</h3><p>Guestbooks, polls, leaderboards: things visitors add, under their name, that everyone can read.</p></div>
@@@ -91,7 +92,7 @@ function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {
</div>
<p class="sp-note">Spaces are plain HTML, CSS and JavaScript with the space SDK (<code>/_openvibe/sdk.js</code>). The AI builder, the editor and the preview work for everyone signed in. Publishing a space at its own address opens together with OpenVibe.Space's public hosting.</p>
</section>`;
-- return page({ title: 'Spaces', description: 'Make a space: a small app on your OpenVibe account, built by OpenVibe AI or from a template, with sign-in, shared lists, private storage and AI.', path: '/spaces', showcase: true, body });
++ return page({ title: 'Spaces', description: 'Make a space: a small app on your OpenVibe account, built by OpenVibe AI or from a template, with sign-in, shared lists, private storage and AI.', path: '/spaces', showcase: true, styles: ['kit.css'], body });
}
// A description handed over from another OpenVibe site: say so, and put the Build button one press away.
@@@ -102,12 +103,12 @@ const handedOver = (from, prompt) => (prompt && from === 'codes'
function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pick = null, from = null }) {
const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
<span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
-- <div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
++ <div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h" class="ovk-titled">${sectionIcon('grid')}${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
<form method="post" action="/spaces/new"><input type="hidden" name="csrf" value="${esc(csrf)}"><input type="hidden" name="template" value="${esc(pick.id)}"><button class="sp-btn">Start from this</button></form>
</section>` : '';
const body = `<div class="sp-wrap">
<p class="eyebrow">New space</p>
-- <h1 class="sp-title">${pick ? 'Make it yours' : 'What do you want to make?'}</h1>
++ ${kit.pageHead({ title: `${pick ? 'Make it yours' : 'What do you want to make?'}`, icon: 'home', back: { href: '/', label: 'Home' } })}
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${picked}
${handedOver(from, prompt)}
@@@ -119,7 +120,7 @@ function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pic
<span class="sp-muted">${aiOn ? `${Number(buildsLeft)} AI builds left today. It takes about a minute; you can change everything after.` : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div>
<div class="sp-ideas" aria-label="Ideas">${['A daily mood check-in with a shared chart', 'A pixel art board everyone can paint on', 'A recipe idea generator that asks what is in my fridge', 'A study flashcard deck I can quiz myself with'].map((i) => `<button type="button" class="sp-chip" data-idea="${esc(i)}">${esc(i)}</button>`).join('')}</div>
</form>
-- <h2 class="sp-subtitle">Or start from a template</h2>
++ <h2 class="ovk-titled sp-subtitle">${sectionIcon('grid')}Or start from a template</h2>
${templateCards(templates, { viewer, csrf, compact: true })}
</div>
<script>document.querySelectorAll('.sp-chip').forEach(function (b) { b.addEventListener('click', function () { var t = document.getElementById('sp-prompt'); t.value = b.getAttribute('data-idea'); t.focus(); }); });</script>`;
@@@ -128,7 +129,7 @@ function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pic
function minePage({ spaces }) {
const body = `<div class="sp-wrap">
-- <div class="sp-head"><h1 class="sp-title">Your spaces</h1><a class="sp-btn" href="/spaces/new"><i class="fa-solid fa-plus" aria-hidden="true"></i> New space</a></div>
++ <div class="sp-head">${kit.pageHead({ title: `Your spaces`, icon: 'home', back: { href: '/', label: 'Home' } })}<a class="sp-btn" href="/spaces/new"><i class="fa-solid fa-plus" aria-hidden="true"></i> New space</a></div>
${spaces.length ? `<div class="sp-grid">${spaces.map((s) => card(s, { mine: true })).join('')}</div>` : '<p class="sp-empty">You have not made a space yet. <a href="/spaces/new">Make your first one</a>: describe it, or start from a template.</p>'}
</div>`;
return page({ title: 'Your spaces', path: '/spaces/mine', robots: 'noindex,nofollow', body });
@@@ -141,7 +142,7 @@ function minePage({ spaces }) {
function sharedGate({ id, signedInAs = null, signInUrl = '/auth/login' }) {
const body = `<div class="sp-wrap">
<p class="eyebrow">A space on OpenVibe.Space</p>
-- <h1 class="sp-title">This is shared with specific people.</h1>
++ ${kit.pageHead({ title: `This is shared with specific people.`, icon: 'home', back: { href: '/', label: 'Home' } })}
<div id="share-gate" data-ov-share-gate data-kind="space" data-id="${esc(String(id))}" data-signed-in-as="${esc(signedInAs || '')}" data-sign-in-url="${esc(signInUrl)}">
<p class="sp-muted">This space is shared with specific people on OpenVibe.Space.</p>
<p><a class="sp-btn" href="${esc(signInUrl)}">Sign in</a></p>
@@@ -163,12 +164,12 @@ function spacePage({ viewer, space, owner, url, csrf, forkedFrom }) {
].join('');
const body = `<div class="sp-wrap">
<p class="eyebrow">${live ? 'Space' : 'Draft space'}</p>
-- <div class="sp-head"><h1 class="sp-title">${esc(space.name)}</h1><div class="sp-actions">${actions}</div></div>
++ <div class="sp-head">${kit.pageHead({ title: `${space.name}`, icon: 'home', back: { href: '/', label: 'Home' } })}<div class="sp-actions">${actions}</div></div>
<p class="sp-lede">${esc(space.description || 'No description yet.')}</p>
<p class="sp-muted">by <a href="https://openvibe.network/@${esc(encodeURIComponent(space.owner_username))}">@${esc(space.owner_username)}</a>${forkedFrom ? ` · forked from <a href="/spaces/${esc(forkedFrom.slug)}">${esc(forkedFrom.name)}</a>` : ''}${space.forks ? ` · ${Number(space.forks)} fork${space.forks === 1 ? '' : 's'}` : ''}${live ? ` · <a href="${esc(url)}">${esc(url.replace(/^https?:\/\//, ''))}</a>` : ''}</p>
${live ? `<div class="sp-frame"><iframe src="${esc(url)}" title="${esc(space.name)}" loading="lazy" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-top-navigation-by-user-activation"></iframe></div>`
: `<p class="sp-flash">This space is a draft: only its maker can open it, in the editor.</p>`}
-- <h2 class="sp-subtitle">What it can do when you sign in</h2>
++ <h2 class="ovk-titled sp-subtitle">${sectionIcon('members')}What it can do when you sign in</h2>
${capList(caps(space)) || '<p class="sp-muted">Nothing: it does not use your account.</p>'}
</div>`;
return page({
@@@ -221,7 +222,7 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
<div class="sp-ed-switch" role="tablist" aria-label="Show"><button type="button" role="tab" data-view="preview" aria-selected="true"><i class="fa-solid fa-eye" aria-hidden="true"></i> Preview</button><button type="button" role="tab" data-view="code" aria-selected="false"><i class="fa-solid fa-code" aria-hidden="true"></i> Code</button></div>
<div class="sp-ed-main">
<section class="sp-ed-code" aria-label="Files">
-- <div class="sp-ed-tabs" id="ed-tabs" role="tablist"></div>
++ <div class="sp-ed-tabs" data-ovk-scroll id="ed-tabs" role="tablist"></div>
<textarea id="ed-code" spellcheck="false" aria-label="File contents"></textarea>
</section>
<section class="sp-ed-preview" aria-label="Preview">
@@@ -230,10 +231,10 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
<details class="sp-ed-fnlog" id="ed-fnlog" hidden><summary><i class="fa-solid fa-terminal" aria-hidden="true"></i> Function log <span class="sp-muted" id="ed-fnlog-last"></span></summary><ol id="ed-fnlog-list"></ol></details>
</section>
</div>
-- <div class="sp-ed-building" id="ed-building" hidden><div><i class="fa-solid fa-wand-magic-sparkles fa-bounce" aria-hidden="true"></i><h2 id="ed-building-title">OpenVibe AI is building…</h2><p id="ed-building-text"></p><p class="sp-muted">This usually takes under a minute or two. You can leave and come back.</p></div></div>
++ <div class="sp-ed-building" id="ed-building" hidden><div><i class="fa-solid fa-wand-magic-sparkles fa-bounce" aria-hidden="true"></i><h2 id="ed-building-title" class="ovk-titled">${sectionIcon('grid')}OpenVibe AI is building…</h2><p id="ed-building-text"></p><p class="sp-muted">This usually takes under a minute or two. You can leave and come back.</p></div></div>
<dialog class="sp-ed-dialog" id="ed-dialog">
<form method="dialog" id="ed-settings-form">
-- <h2>Settings</h2>
++ <h2 class="ovk-titled">${sectionIcon('grid')}Settings</h2>
<label>Name<input name="name" maxlength="40" required></label>
<label>Description<textarea name="description" rows="2" maxlength="400" placeholder="What it does, for the gallery"></textarea></label>
<label>Its AI's instructions<textarea name="instructions" rows="4" maxlength="4000" placeholder="Who the space's AI is and how it answers (used by OV.ai.ask)"></textarea></label>
@@@ -271,7 +272,7 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
function guestPage({ templates, error, prompt, aiOn, pick = null, from = null }) {
const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
<span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
-- <div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
++ <div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h" class="ovk-titled">${sectionIcon('grid')}${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
<form method="post" action="/spaces/guest"><input type="hidden" name="template" value="${esc(pick.id)}"><button class="sp-btn">Start from this</button></form>
</section>` : '';
const cards = templates.map((t) => `<div class="sp-template" style="--tpl:${esc(t.accent)}">
@@@ -281,8 +282,7 @@ function guestPage({ templates, error, prompt, aiOn, pick = null, from = null })
</div>`).join('');
const body = `<div class="sp-wrap">
<p class="eyebrow">OpenVibe.Space</p>
-- <h1 class="sp-title">Make a site.</h1>
-- <p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>
++ ${kit.pageHead({ title: `Make a site.`, icon: 'home', back: { href: '/', label: 'Home' } , lede: `No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.` })}
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${picked}
${handedOver(from, prompt)}
@@@ -292,7 +292,7 @@ function guestPage({ templates, error, prompt, aiOn, pick = null, from = null })
<div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}${prompt && aiOn ? ' autofocus' : ''}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>
<span class="sp-muted">${aiOn ? 'It takes about a minute; you can change everything after.' : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div>
</form>
-- <h2 class="sp-subtitle">Or start from a template</h2>
++ <h2 class="ovk-titled sp-subtitle">${sectionIcon('grid')}Or start from a template</h2>
<div class="sp-templates is-compact">${cards}</div>
</div>`;
return page({ title: 'Make a site', path: '/spaces/guest', robots: 'noindex,nofollow', body });
@@@ -303,7 +303,7 @@ function connectPage({ viewer, space, csrf, challenge, state, returnPath }) {
const body = `<div class="sp-wrap sp-connect">
<div class="sp-connect-card">
<p class="eyebrow">Sign in to a space</p>
-- <h1 class="sp-title">${esc(space.name)}</h1>
++ ${kit.pageHead({ title: `${space.name}`, icon: 'home', back: { href: '/', label: 'Home' } })}
<p class="sp-muted">by @${esc(space.owner_username)} · ${esc(`${space.slug}`)}.openvibe.website</p>
<p>Signed in as <strong>${esc(u.display_name || u.username || 'you')}</strong>. If you continue, this space may:</p>
${capList(caps(space).length ? caps(space) : ['profile'])}
@@@ -326,15 +326,14 @@ function connectPage({ viewer, space, csrf, challenge, state, returnPath }) {
function signInPage({ path, next, title = 'Sign in to continue' }) {
const body = `<div class="sp-wrap">
<p class="eyebrow">OpenVibe.Space</p>
-- <h1 class="sp-title">${esc(title)}</h1>
-- <p class="sp-lede">Spaces are made with your OpenVibe account. Sign in and you come straight back here.</p>
++ ${kit.pageHead({ title: `${title}`, icon: 'home', back: { href: '/', label: 'Home' } , lede: `Spaces are made with your OpenVibe account. Sign in and you come straight back here.` })}
<p><a class="sp-btn" href="${esc(signInHref(next))}">Sign in with OpenVibe</a></p>
</div>`;
return page({ title, path, robots: 'noindex,nofollow', body });
}
function errorBody(status, message) {
-- return page({ title: status === 404 ? 'Space not found' : 'Spaces', path: '/spaces', robots: 'noindex,nofollow', body: `<div class="sp-wrap"><p class="eyebrow">Error ${Number(status)}</p><h1 class="sp-title">${status === 404 ? 'No such space' : 'That did not work'}</h1><p class="sp-lede">${esc(message)}</p><p><a class="sp-btn" href="/spaces">Back to spaces</a></p></div>` });
++ return page({ title: status === 404 ? 'Space not found' : 'Spaces', path: '/spaces', robots: 'noindex,nofollow', body: `<div class="sp-wrap"><p class="eyebrow">Error ${Number(status)}</p>${kit.pageHead({ title: `${status === 404 ? 'No such space' : 'That did not work'}`, icon: 'home', back: { href: '/', label: 'Home' } , lede: `${message}` })}<p><a class="sp-btn" href="/spaces">Back to spaces</a></p></div>` });
}
module.exports = { sharedGate, galleryPage, newPage, minePage, spacePage, editorPage, guestPage, connectPage, signInPage, errorBody, CAP_TEXT };
ddiff --git a/test/boot.test.js b/test/boot.test.js
iindex cd7da0b..7db448e 100644
---- a/test/boot.test.js
++++ b/test/boot.test.js
@@@ -68,7 +68,7 @@ async function check(name, fn) {
assert.strictEqual(r.status, 200);
const html = await r.text();
assert.match(html, /<title>OpenVibe\.Space — spaces and static hosting<\/title>/);
-- assert.match(html, /<h1>Make a space\.<span class="sc-accent"> An app on your OpenVibe account\.<\/span><\/h1>/);
++ assert.match(html, /<h1>Make a space\. An app on your OpenVibe account\.<\/h1>/);
assert.match(html, /The forum moved to OpenVibe\.Community/);
assert.match(html, /href="https:\/\/openvibe\.community\/forum"/);
});
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex 83d9067..ebb436e 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -19,7 +19,7 @@ const { boot, check, done } = require('./helpers/app');
// From the block to the end of the page: its panes grew past a fixed window when section heads gained an icon (Shared 4.160.0).
const block = page.slice(page.indexOf('<div class="pick" id="pick-parts">'));
assert.ok(block.startsWith('<div class="pick" id="pick-parts">'), 'the pick block is on the home');
-- assert.match(block, /<nav class="pick-row" aria-label="What Space does">/);
++ assert.match(block, /<nav class="pick-row" data-ovk-scroll aria-label="What Space does">/);
const chips = [...block.matchAll(/<a class="pick-chip" href="#(?:pp-parts-)?([a-z]+)">()([^<]+)<\/a>/g)];
assert.ok(chips.length >= 5, `a chip per part (${chips.length})`);
assert.ok(!/type="radio"/.test(block), 'no chip is a radio: nothing is picked, every pane shows');
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 96b059c..c400cde 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -24,7 +24,7 @@ const { boot, check, done } = require('./helpers/app');
await check('the home page leads with spaces, describes staff static hosting and links the forum on OpenVibe.Community', async () => {
const r = await t.get('/');
assert.strictEqual(r.status, 200);
-- assert.match(r.text, /<h1>Make a space\.<span class="sc-accent"> An app on your OpenVibe account\.<\/span><\/h1>/);
++ assert.match(r.text, /<h1>Make a space\. An app on your OpenVibe account\.<\/h1>/);
assert.match(r.text, /href="\/spaces\/new"/, 'the home page starts a space');
assert.match(r.text, /<h2[^>]*>Static hosting: publish a site, keep every deploy\.<\/h2>/);
assert.match(r.text, /Immutable, content-addressed deploys with one-step rollback/);
ddiff --git a/test/website-home.test.js b/test/website-home.test.js
iindex 1958820..6fa493e 100644
---- a/test/website-home.test.js
++++ b/test/website-home.test.js
@@@ -6,6 +6,7 @@
* cookie, serves nothing of the API or sign-in, and a template at full size runs no script.
*/
const assert = require('assert');
++const { parseHTML } = require('linkedom');
const fs = require('fs');
const path = require('path');
const { boot, check, done } = require('./stageb/boot');
@@@ -91,7 +92,9 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
await check('the guest start page leads with a website template picked on the front door', async () => {
const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/guest?template=site-event' });
assert.strictEqual(r.status, 200, r.text.slice(0, 200));
-- assert.match(r.text, /<section class="sp-pick"[^>]*>[\s\S]*?<h2 id="sp-pick-h">Event page<\/h2>[\s\S]*?name="template" value="site-event"/);
++ const pick = parseHTML(r.text).document.querySelector('section.sp-pick');
++ assert.strictEqual(pick.querySelector('#sp-pick-h').textContent, 'Event page');
++ assert.strictEqual(pick.querySelector('[name=template]').getAttribute('value'), 'site-event');
const app = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/guest?template=poll' });
assert.ok(!app.text.includes('class="sp-pick"'), 'an app template is not a website to start from here');
});
@@@ -99,7 +102,9 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
await check('openvibe.space leads with a template picked on the front door', async () => {
const r = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=site-portfolio', session: alice });
assert.strictEqual(r.status, 200, r.text.slice(0, 200));
-- assert.match(r.text, /<section class="sp-pick"[^>]*>[\s\S]*?<h2 id="sp-pick-h">Portfolio<\/h2>[\s\S]*?name="template" value="site-portfolio"/);
++ const pick = parseHTML(r.text).document.querySelector('section.sp-pick');
++ assert.strictEqual(pick.querySelector('#sp-pick-h').textContent, 'Portfolio');
++ assert.strictEqual(pick.querySelector('[name=template]').getAttribute('value'), 'site-portfolio');
const plain = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=nope', session: alice });
assert.ok(!plain.text.includes('class="sp-pick"'), 'an unknown template picks nothing');
});