@bongos/core 1.24.58 → 1.24.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +47 -32
- package/docs/architecture.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +1 -1
- package/docs/recipes/ops-gotchas.md +2 -2
- package/modules/status-ui/public/style.css +5 -2
- package/modules/ui-design/public/brand/png-sources.json +10 -0
- package/modules/ui-design/public/brand.css +6 -0
- package/modules/ui-design/public/brand.html +2 -2
- package/modules/ui-design/scripts/gen-design-reference.js +2 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-docs/index.html +3 -1
- package/release-notes.json +10 -0
- package/scripts/bongos/design-generators-fresh.js +14 -6
- package/scripts/bongos/gen-brand.js +82 -36
- package/scripts/status-mirror-sync.js +4 -1
- package/src/bongos/routes/auth.js +4 -9
- package/src/bongos/serve-internal.js +5 -70
- package/src/bongos/sign-in-pages.js +20 -4
- package/src/brand-mark.js +10 -1
- package/src/error-page.js +11 -10
- package/src/module-api.js +1 -1
- package/src/theme-markup.js +81 -0
- package/tests/brand_mark.mjs +11 -1
- package/tests/brand_surfaces.mjs +5 -2
- package/tests/design_counts_ratchet.mjs +38 -0
- package/tests/design_reference.mjs +1 -1
- package/tests/wordmark_and_faces.mjs +111 -0
|
@@ -17,17 +17,23 @@
|
|
|
17
17
|
// og-card.png 1200x630, the link-preview card every public page names (task 1005091):
|
|
18
18
|
// the dark mark and the lowercase wordmark on the void (--png; needs a browser)
|
|
19
19
|
//
|
|
20
|
-
// Deterministic text, so `--check` is a clean gate (tests/brand_mark.mjs runs it
|
|
21
|
-
// rendered by the system browser
|
|
20
|
+
// Deterministic text, so `--check` is a clean gate (tests/brand_mark.mjs runs it; fitness runs it
|
|
21
|
+
// through scripts/bongos/design-generators-fresh.js). The PNGs are rendered by the system browser
|
|
22
|
+
// and only written with --png; a browser's pixels are not byte-stable across machines, so they
|
|
23
|
+
// are checked by hash instead (task 1005133): --png records, in png-sources.json beside them,
|
|
24
|
+
// a hash of the exact page each was rendered from and a hash of the PNG written. --check fails
|
|
25
|
+
// when the page a PNG would be rendered from today differs from the one recorded (a source or a
|
|
26
|
+
// colour moved without a re-render), or when the PNG is not the one that render wrote.
|
|
22
27
|
//
|
|
23
28
|
// Usage:
|
|
24
29
|
// node scripts/bongos/gen-brand.js # write the SVG variants
|
|
25
30
|
// node scripts/bongos/gen-brand.js --png # … and render apple-touch-icon.png + og-card.png
|
|
26
|
-
// node scripts/bongos/gen-brand.js --check # write nothing; exit 2 if a variant is stale
|
|
31
|
+
// node scripts/bongos/gen-brand.js --check # write nothing; exit 2 if a variant or a PNG is stale
|
|
27
32
|
|
|
28
33
|
const fs = require('node:fs');
|
|
29
34
|
const path = require('node:path');
|
|
30
|
-
const
|
|
35
|
+
const crypto = require('node:crypto');
|
|
36
|
+
const { readMarkSource, markParts, markFile, faviconSvg } = require('../../src/brand-mark');
|
|
31
37
|
const { escapeHtml } = require('../../src/html-escape');
|
|
32
38
|
|
|
33
39
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -59,22 +65,71 @@ function ogCardHtml({ markSvg, name, fontLinks = [], fontFamily = 'sans-serif' }
|
|
|
59
65
|
'</body></html>';
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
// The page apple-touch-icon.png is rendered from: the light mark on the pack's ground. Pure.
|
|
69
|
+
function touchIconHtml({ markSvg, bg }) {
|
|
70
|
+
return `<html><body style="margin:0;background:${bg}">` +
|
|
71
|
+
`<img src="data:image/svg+xml;base64,${Buffer.from(markSvg).toString('base64')}" style="display:block;width:128px;height:128px;margin:26px">` +
|
|
72
|
+
'</body></html>';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Each PNG as the exact page and viewport it is rendered from, given the SVG variants it
|
|
76
|
+
// draws (the text gen-brand writes, not the files on disk, so a stale SVG shows here too).
|
|
77
|
+
function pngPages(files, pack) {
|
|
78
|
+
return {
|
|
79
|
+
'apple-touch-icon.png': {
|
|
80
|
+
width: 180, height: 180, wait: 'load',
|
|
81
|
+
html: touchIconHtml({ markSvg: files['mark-light.svg'], bg: pack.theme.ui.colors.bg }),
|
|
82
|
+
},
|
|
83
|
+
'og-card.png': {
|
|
84
|
+
width: 1200, height: 630, wait: 'networkidle',
|
|
85
|
+
html: ogCardHtml({
|
|
86
|
+
markSvg: files['mark-dark.svg'],
|
|
87
|
+
name: pack.identity.productName,
|
|
88
|
+
fontLinks: pack.theme.ui.fonts.links,
|
|
89
|
+
fontFamily: pack.theme.ui.fonts.display,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
|
96
|
+
// The hash of a render's input: the page and the viewport it is shot at.
|
|
97
|
+
function pageHash(p) { return sha256(JSON.stringify([p.width, p.height, p.html])); }
|
|
98
|
+
|
|
99
|
+
const PNG_MANIFEST = 'png-sources.json';
|
|
100
|
+
|
|
101
|
+
// The PNGs that do not match what --png would write today, each with the reason. Pure I/O
|
|
102
|
+
// read of the brand folder; writes nothing.
|
|
103
|
+
function stalePngs(dir, pages) {
|
|
104
|
+
let manifest = {};
|
|
105
|
+
try { manifest = JSON.parse(fs.readFileSync(path.join(dir, PNG_MANIFEST), 'utf8')); } catch { manifest = {}; }
|
|
106
|
+
const stale = [];
|
|
107
|
+
for (const [name, p] of Object.entries(pages)) {
|
|
108
|
+
const rec = manifest[name];
|
|
109
|
+
const file = path.join(dir, name);
|
|
110
|
+
if (!rec || !fs.existsSync(file)) { stale.push(`${name} (not recorded in ${PNG_MANIFEST})`); continue; }
|
|
111
|
+
if (rec.page !== pageHash(p)) stale.push(`${name} (its source page changed since it was rendered)`);
|
|
112
|
+
else if (rec.png !== sha256(fs.readFileSync(file))) stale.push(`${name} (the file is not the one --png wrote)`);
|
|
113
|
+
}
|
|
114
|
+
return stale;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function renderPngs(dir, pages) {
|
|
63
118
|
const { launchBrowser } = require('../../modules/ui-design/kit/lib'); // eslint-disable-line global-require
|
|
64
|
-
const pack = readPack();
|
|
65
119
|
const browser = await launchBrowser();
|
|
120
|
+
const manifest = {};
|
|
66
121
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
await page.screenshot({ path: path.join(dir, 'og-card.png'), omitBackground: false });
|
|
122
|
+
for (const [name, p] of Object.entries(pages)) {
|
|
123
|
+
const page = await browser.newPage({ viewport: { width: p.width, height: p.height }, deviceScaleFactor: 1 });
|
|
124
|
+
await page.setContent(p.html);
|
|
125
|
+
await page.waitForLoadState(p.wait);
|
|
126
|
+
await page.evaluate(() => document.fonts.ready);
|
|
127
|
+
await page.screenshot({ path: path.join(dir, name), omitBackground: false });
|
|
128
|
+
await page.close();
|
|
129
|
+
manifest[name] = { page: pageHash(p), png: sha256(fs.readFileSync(path.join(dir, name))) };
|
|
130
|
+
}
|
|
77
131
|
} finally { await browser.close(); }
|
|
132
|
+
fs.writeFileSync(path.join(dir, PNG_MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
// { filename: text } for every SVG variant. Pure given the source and the colours.
|
|
@@ -84,33 +139,21 @@ function brandFiles(src, colors) {
|
|
|
84
139
|
const mono = '#000000';
|
|
85
140
|
return {
|
|
86
141
|
'mark-light.svg': markFile(parts, { ink: colors.ink, dot: colors.accent, label: 'logo' }),
|
|
87
|
-
'mark-dark.svg': markFile(parts, { ink:
|
|
142
|
+
'mark-dark.svg': markFile(parts, { ink: colors.bgDeep, dot: colors.accent, label: 'logo' }),
|
|
88
143
|
'mark-mono.svg': markFile(parts, { ink: mono, dot: mono, label: 'logo' }),
|
|
89
144
|
'mark-body.svg': markFile(parts, { ink: mono, dot: mono, only: ['ring', 'disc'] }),
|
|
90
145
|
'mark-dot.svg': markFile(parts, { ink: mono, dot: mono, only: ['dot'] }),
|
|
91
|
-
'favicon.svg': faviconSvg({ ink: colors.ink, accent: colors.accent }, src) + '\n',
|
|
146
|
+
'favicon.svg': faviconSvg({ ink: colors.ink, accent: colors.accent, night: colors.bgDeep }, src) + '\n',
|
|
92
147
|
};
|
|
93
148
|
}
|
|
94
149
|
|
|
95
|
-
async function renderTouchIcon(dir, colors) {
|
|
96
|
-
const { launchBrowser } = require('../../modules/ui-design/kit/lib'); // eslint-disable-line global-require
|
|
97
|
-
const svg = fs.readFileSync(path.join(dir, 'mark-light.svg'), 'utf8');
|
|
98
|
-
const browser = await launchBrowser();
|
|
99
|
-
try {
|
|
100
|
-
const page = await browser.newPage({ viewport: { width: 180, height: 180 }, deviceScaleFactor: 1 });
|
|
101
|
-
await page.setContent(`<html><body style="margin:0;background:${colors.bg}">` +
|
|
102
|
-
`<img src="data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}" style="display:block;width:128px;height:128px;margin:26px">` +
|
|
103
|
-
'</body></html>');
|
|
104
|
-
await page.waitForLoadState('load');
|
|
105
|
-
await page.screenshot({ path: path.join(dir, 'apple-touch-icon.png'), omitBackground: false });
|
|
106
|
-
} finally { await browser.close(); }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
150
|
async function main() {
|
|
110
151
|
const args = process.argv.slice(2);
|
|
111
152
|
const dir = brandDir();
|
|
112
|
-
const
|
|
153
|
+
const pack = readPack();
|
|
154
|
+
const colors = pack.theme.ui.colors;
|
|
113
155
|
const files = brandFiles(readMarkSource({ root: ROOT }), colors);
|
|
156
|
+
const pages = pngPages(files, pack);
|
|
114
157
|
const stale = [];
|
|
115
158
|
for (const [name, text] of Object.entries(files)) {
|
|
116
159
|
const file = path.join(dir, name);
|
|
@@ -120,11 +163,14 @@ async function main() {
|
|
|
120
163
|
if (!args.includes('--check')) fs.writeFileSync(file, text);
|
|
121
164
|
}
|
|
122
165
|
if (args.includes('--check')) {
|
|
123
|
-
|
|
124
|
-
process.
|
|
166
|
+
const pngs = stalePngs(dir, pages);
|
|
167
|
+
if (stale.length) process.stderr.write(`gen-brand: stale — ${stale.join(', ')}. Run: node scripts/bongos/gen-brand.js\n`);
|
|
168
|
+
if (pngs.length) process.stderr.write(`gen-brand: stale PNG — ${pngs.join('; ')}. Run: node scripts/bongos/gen-brand.js --png\n`);
|
|
169
|
+
if (stale.length || pngs.length) process.exit(2);
|
|
170
|
+
process.stdout.write('gen-brand: the logo and favicon set match mark.svg, and the PNGs match their sources\n');
|
|
125
171
|
return;
|
|
126
172
|
}
|
|
127
|
-
if (args.includes('--png'))
|
|
173
|
+
if (args.includes('--png')) await renderPngs(dir, pages);
|
|
128
174
|
process.stdout.write(`gen-brand: ${stale.length ? `wrote ${stale.join(', ')}` : 'nothing to write'}${args.includes('--png') ? ' + apple-touch-icon.png + og-card.png' : ''}\n`);
|
|
129
175
|
}
|
|
130
176
|
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
// outage resilience (ADR 0029) from a SEPARATE repo. Before this script there
|
|
13
13
|
// was no sync, so every change to the status surface silently missed the live page
|
|
14
14
|
// (that's how #726's reorg + #731's theming never showed up). This makes the
|
|
15
|
-
// publish deterministic
|
|
15
|
+
// publish deterministic. It is NOT CI-driven: no workflow in .github/workflows/ runs
|
|
16
|
+
// it (there is no sync-status-mirror.yml in this repo), so a status-surface change reaches the
|
|
17
|
+
// off-box page only when someone runs this by hand with a mirror checkout — see USAGE
|
|
18
|
+
// and docs/recipes/ops-gotchas.md (task 1005131 / task 1005133).
|
|
16
19
|
//
|
|
17
20
|
// CONFIG (task 1002510 — no host identity is baked in here; ADR 0062 §3):
|
|
18
21
|
// domains.publicOrigin — the origin the mirror calls cross-origin
|
|
@@ -51,7 +51,7 @@ const { escapeHtml } = require('../../html-escape'); // the one server escaper (
|
|
|
51
51
|
// the browser-approved device flow the CLI gets when Google is offered.
|
|
52
52
|
const { googleConfigured } = require('../auth-google');
|
|
53
53
|
const { startBrowserDeviceFlow, isBrowserDeviceCode, pollBrowserDeviceFlow } = require('../device-approval');
|
|
54
|
-
const { renderSignInChooser } = require('../sign-in-pages');
|
|
54
|
+
const { renderSignInChooser, signInHead } = require('../sign-in-pages');
|
|
55
55
|
|
|
56
56
|
// Render a small standalone HTML page in the builders' hall palette. Used for
|
|
57
57
|
// OAuth error branches so the visitor sees something coherent rather than a
|
|
@@ -103,10 +103,7 @@ function renderAuthErrorPage(heading, detail, opts = {}) {
|
|
|
103
103
|
<meta charset="utf-8">
|
|
104
104
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
105
105
|
<title>${escapeHtml(worldName)} — ${escapeHtml(copy.titleSuffix)}</title>
|
|
106
|
-
|
|
107
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
108
|
-
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=EB+Garamond:ital,wght@0,400;0,500&display=swap" rel="stylesheet">
|
|
109
|
-
<link rel="stylesheet" href="/builders/style.css">
|
|
106
|
+
${signInHead()}
|
|
110
107
|
</head><body>
|
|
111
108
|
<header class="hall">
|
|
112
109
|
<p class="hall__epigraph">${escapeHtml(copy.epigraph)}</p>
|
|
@@ -167,10 +164,7 @@ function renderAccessPendingPage(login, startHref) {
|
|
|
167
164
|
<meta charset="utf-8">
|
|
168
165
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
169
166
|
<title>${escapeHtml(worldName)} — confirming access</title>
|
|
170
|
-
|
|
171
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
172
|
-
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=EB+Garamond:ital,wght@0,400;0,500&display=swap" rel="stylesheet">
|
|
173
|
-
<link rel="stylesheet" href="/builders/style.css">
|
|
167
|
+
${signInHead()}
|
|
174
168
|
</head><body>
|
|
175
169
|
<header class="hall">
|
|
176
170
|
<p class="hall__epigraph">${escapeHtml(epigraph)}</p>
|
|
@@ -1494,6 +1488,7 @@ module.exports.webRedirectOrigin = () => WEB_REDIRECT_ORIGIN;
|
|
|
1494
1488
|
// The auth-error page renderer (task 1439): pure over the branding pack, so a
|
|
1495
1489
|
// test can prove every visible string is brand-owned (no OTB lore baked in).
|
|
1496
1490
|
module.exports.renderAuthErrorPage = renderAuthErrorPage;
|
|
1491
|
+
module.exports.renderAccessPendingPage = renderAccessPendingPage;
|
|
1497
1492
|
// The admitting-rank label for per-branch strings (task 1002616) — exported for
|
|
1498
1493
|
// the same reason: the test proves the label is pack-owned, never "Archon".
|
|
1499
1494
|
module.exports.admitterLabel = admitterLabel;
|
|
@@ -44,7 +44,7 @@ const { buildStampedHtml, hashFile } = require('../asset-version');
|
|
|
44
44
|
const { buildInfo } = require('../build-info');
|
|
45
45
|
const { buildBongosDownloads, installScriptReader } = require('../bongos-downloads');
|
|
46
46
|
const { clientBranding, branding, applyBrandConditionals } = require('../branding');
|
|
47
|
-
const {
|
|
47
|
+
const { themeMarkup } = require('../theme-markup');
|
|
48
48
|
const { linkPreviewTags } = require('../link-preview');
|
|
49
49
|
const { prelaunchGate } = require('./prelaunch-gate');
|
|
50
50
|
const { platformVisibilityGate } = require('./platform-visibility-gate');
|
|
@@ -84,75 +84,10 @@ function tryLoadInstanceConfig(name, roots = [resolveInstanceRoot(), ROOT]) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
bg: '--bg', bgCard: '--bg-card', bgDeep: '--bg-deep', bgFrame: '--bg-frame',
|
|
92
|
-
ink: '--ink', inkSoft: '--ink-soft', inkFaint: '--ink-faint',
|
|
93
|
-
rule: '--rule', ruleSoft: '--rule-soft',
|
|
94
|
-
accent: '--accent', accentDeep: '--accent-deep', sea: '--sea', terracotta: '--terracotta',
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Build the THEME markup for a served page from the client-safe pack's theme:
|
|
98
|
-
// a <style> overriding the :root palette + font-family vars, the web-font <link>s,
|
|
99
|
-
// and the favicon <link>. Returns '' when the pack carries no theme (so a pack
|
|
100
|
-
// without a `theme.ui` block is a no-op and the stylesheet's own defaults stand).
|
|
101
|
-
// The <style> is emitted so it lands AFTER the page's own stylesheet <link>
|
|
102
|
-
// (which sits earlier in <head>) — equal specificity, later wins — making the
|
|
103
|
-
// pack authoritative over style.css. For OTB the values equal style.css's
|
|
104
|
-
// parchment defaults, so the render is byte-identical; a neutral instance gets
|
|
105
|
-
// its plain palette + system fonts. R57 (task 1196, ADR 0062 §3).
|
|
106
|
-
function themeMarkup(theme) {
|
|
107
|
-
if (!theme) return '';
|
|
108
|
-
const decls = [];
|
|
109
|
-
for (const [key, cssVar] of Object.entries(COLOR_VARS)) {
|
|
110
|
-
const v = cssSafe(theme.colors?.[key]);
|
|
111
|
-
if (v) decls.push(`${cssVar}:${v};`);
|
|
112
|
-
}
|
|
113
|
-
const display = cssSafe(theme.fonts?.display);
|
|
114
|
-
if (display) decls.push(`--font-display:${display};`);
|
|
115
|
-
const body = cssSafe(theme.fonts?.body);
|
|
116
|
-
if (body) decls.push(`--font-body:${body};`);
|
|
117
|
-
let out = '';
|
|
118
|
-
for (const href of theme.fonts?.links || []) {
|
|
119
|
-
out += `<link rel="stylesheet" href="${esc(href)}">`;
|
|
120
|
-
}
|
|
121
|
-
// `backdrop` paints the page background (e.g. the Cloud Bongos sunrise gradient,
|
|
122
|
-
// ADR 0064 / R74) over the flat var(--bg); omitted packs (OTB) keep --bg.
|
|
123
|
-
const css = [];
|
|
124
|
-
if (decls.length) css.push(`:root{${decls.join('')}}`);
|
|
125
|
-
const backdrop = cssSafe(theme.backdrop);
|
|
126
|
-
if (backdrop) css.push(`body{background:${backdrop};}`);
|
|
127
|
-
// The night tint (ADR 0358): a hall wearing a look takes a hint of its accent into
|
|
128
|
-
// its dark grounds. Written as the sheet's own two dark twins, after the sheet, so
|
|
129
|
-
// equal specificity wins on order. clientBranding only sets it with a look, so a
|
|
130
|
-
// hall that chose none keeps the sheet's pure black.
|
|
131
|
-
if (theme.nightTint === true) css.push(nightTintCss());
|
|
132
|
-
if (css.length) out += `<style>${css.join('')}</style>`;
|
|
133
|
-
// esc() is lossless in an attribute: the HTML parser decodes the entities back
|
|
134
|
-
// before anything reads the URL, so the neutral pack's inline `data:image/svg+xml,
|
|
135
|
-
// <svg …>` favicon resolves to exactly the same bytes it does today.
|
|
136
|
-
if (theme.favicon) out += `<link rel="icon" href="${esc(theme.favicon)}">`;
|
|
137
|
-
if (theme.appleTouchIcon) out += `<link rel="apple-touch-icon" href="${esc(theme.appleTouchIcon)}">`;
|
|
138
|
-
return out;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// A pack-supplied value on its way into <style> TEXT. esc() is the wrong tool
|
|
142
|
-
// here and would corrupt the output: CSS is not entity-decoded, so a font stack
|
|
143
|
-
// like `"Cinzel", serif` would reach the browser as `"Cinzel", serif`
|
|
144
|
-
// and simply not parse. There is no encoding that both survives CSS and
|
|
145
|
-
// neutralizes markup, so this fails closed instead. The only way out of a <style>
|
|
146
|
-
// element is a literal `</style`, every route to it begins with `<`, and no
|
|
147
|
-
// colour, font stack, or backdrop has a legitimate use for one — so a value
|
|
148
|
-
// carrying `<` is dropped whole rather than emitted half-escaped. Dropping one
|
|
149
|
-
// declaration loses that pack's colour and falls back to style.css's default;
|
|
150
|
-
// emitting it loses the page.
|
|
151
|
-
function cssSafe(v) {
|
|
152
|
-
const s = String(v == null ? '' : v);
|
|
153
|
-
return s.includes('<') ? '' : s;
|
|
154
|
-
}
|
|
155
|
-
|
|
87
|
+
// The pack's THEME markup (the :root palette + faces <style>, the font links, the
|
|
88
|
+
// favicon) is built by src/theme-markup.js, so a page rendered outside this file —
|
|
89
|
+
// the sign-in screens — gets the identical splice without requiring the server
|
|
90
|
+
// (task 1005135). Re-exported below for every caller that already reads it here.
|
|
156
91
|
// Serialize a pack value for injection INSIDE a <script> element. JSON.stringify
|
|
157
92
|
// alone is not enough: its output is valid JSON, but the browser reads a script
|
|
158
93
|
// body as raw text FIRST, so a `</script>` anywhere in the pack ends the element
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// auth error page in routes/auth.js, so every sign-in screen reads as one place.
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const { branding } = require('../branding');
|
|
13
|
+
const { branding, clientBranding } = require('../branding');
|
|
14
14
|
const { escapeHtml } = require('../html-escape');
|
|
15
|
+
const { themeMarkup } = require('../theme-markup');
|
|
15
16
|
|
|
16
17
|
const GITHUB_MARK = '<svg aria-hidden="true" width="18" height="18" viewBox="0 0 16 16" style="vertical-align:-3px;margin-right:8px"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59 .4 .07 .55-.17 .55-.38 0-.19-.01-.82-.01-1.49-2.01 .37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53 .63-.01 1.08 .58 1.23 .82 .72 1.21 1.87 .87 2.33 .66 .07-.52 .28-.87 .51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87 .31-1.59 .82-2.15-.08-.2-.36-1.02 .08-2.12 0 0 .67-.21 2.2 .82 .64-.18 1.32-.27 2-.27 .68 0 1.36 .09 2 .27 1.53-1.04 2.2-.82 2.2-.82 .44 1.1 .16 1.92 .08 2.12 .51 .56 .82 1.27 .82 2.15 0 3.07-1.87 3.75-3.65 3.95 .29 .25 .54 .73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21 .15 .46 .55 .38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>';
|
|
17
18
|
const GOOGLE_MARK = '<svg aria-hidden="true" width="18" height="18" viewBox="0 0 48 48" style="vertical-align:-3px;margin-right:8px"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>';
|
|
@@ -20,7 +21,22 @@ function worldName() {
|
|
|
20
21
|
try { return branding().identity?.worldName || 'Sign in'; } catch { return 'Sign in'; }
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
+
// The look every sign-in screen shares (task 1005135): the hall stylesheet, then
|
|
25
|
+
// the pack's own palette, faces and favicon, spliced exactly as every served page
|
|
26
|
+
// gets them (src/theme-markup.js, the same splice serve-internal applies). The pack's faces are the only faces;
|
|
27
|
+
// these pages once loaded a retired pair of inscription faces on top of the hall sheet. The
|
|
28
|
+
// header and main are lifted over the hall's fixed plate veil (body::after, black in
|
|
29
|
+
// dark), which otherwise dims the whole page — the hall lifts its shell the same way.
|
|
30
|
+
// A branding hiccup degrades to the hall sheet's own neutral values, never a crash.
|
|
31
|
+
function signInHead() {
|
|
32
|
+
let theme = '';
|
|
33
|
+
try { theme = themeMarkup(clientBranding().theme); } catch { theme = ''; }
|
|
34
|
+
return `<link rel="stylesheet" href="/builders/style.css">
|
|
35
|
+
${theme}
|
|
36
|
+
<style>body > header, body > main { position: relative; z-index: 1; } body > header.hall { padding: var(--ds-space-md) var(--ds-space-gutter-narrow) 0; } body > main { padding: var(--ds-space-sm) var(--ds-space-gutter-narrow) var(--ds-space-xl); }</style>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// One page shell: the shared head, a header, and a body the caller built
|
|
24
40
|
// (already escaped). Never a script: these pages work with forms and links alone.
|
|
25
41
|
function renderPage({ title, heading, subtitle, bodyHtml }) {
|
|
26
42
|
return `<!doctype html>
|
|
@@ -29,7 +45,7 @@ function renderPage({ title, heading, subtitle, bodyHtml }) {
|
|
|
29
45
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
30
46
|
<meta name="referrer" content="no-referrer">
|
|
31
47
|
<title>${escapeHtml(worldName())} — ${escapeHtml(title)}</title>
|
|
32
|
-
|
|
48
|
+
${signInHead()}
|
|
33
49
|
<style>
|
|
34
50
|
.signinChoices{display:flex;flex-direction:column;gap:12px;max-width:340px;margin:8px auto 0}
|
|
35
51
|
.signinChoices .landing__btn{display:block;text-align:center}
|
|
@@ -154,4 +170,4 @@ ${choiceButtons(view)}
|
|
|
154
170
|
});
|
|
155
171
|
}
|
|
156
172
|
|
|
157
|
-
module.exports = { renderSignInChooser, renderExistingAccountHint, renderDeviceApprovalPage };
|
|
173
|
+
module.exports = { renderSignInChooser, renderExistingAccountHint, renderDeviceApprovalPage, signInHead };
|
package/src/brand-mark.js
CHANGED
|
@@ -16,7 +16,16 @@ const fs = require('node:fs');
|
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
|
|
18
18
|
// The Night ink: the chrome world's ink on a dark ground, which every dark twin of the mark uses.
|
|
19
|
-
|
|
19
|
+
// It is the pack's deep ground (theme.ui.colors.bgDeep in config/branding.neutral.json), read
|
|
20
|
+
// from the pack the core ships rather than typed in here, so a palette change reaches the mark
|
|
21
|
+
// (task 1005133). Read relative to this file: src/ sits in the core root beside config/.
|
|
22
|
+
function readNightInk() {
|
|
23
|
+
try {
|
|
24
|
+
const pack = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config', 'branding.neutral.json'), 'utf8'));
|
|
25
|
+
return pack.theme.ui.colors.bgDeep || 'currentColor';
|
|
26
|
+
} catch { return 'currentColor'; }
|
|
27
|
+
}
|
|
28
|
+
const NIGHT_INK = readNightInk();
|
|
20
29
|
// The two custom properties the source colours itself by, exactly as written there.
|
|
21
30
|
const INK_VAR = 'var(--mark-ink,currentColor)';
|
|
22
31
|
const DOT_VAR = 'var(--mark-dot,var(--accent,currentColor))';
|
package/src/error-page.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// A person who mistypes an address, or lands on a page that broke, used to get a bare
|
|
4
4
|
// "404 — not found" in plain text: the one screen of the product with no product on it.
|
|
5
5
|
// This is that screen in the chrome world: the shared mark, the product name, a heading,
|
|
6
|
-
// one sentence and a way back, on the pack's colours and faces in both modes.
|
|
6
|
+
// one sentence and a way back, on the pack's colours and faces in both modes. The name
|
|
7
|
+
// beside the mark is the pack's productName, shown lowercase (DESIGN.md "Naming"); the
|
|
8
|
+
// colours are the hall sheet's tier, never a block of this page's own (task 1005135).
|
|
7
9
|
//
|
|
8
10
|
// WHO GETS IT: a browser asking for a page. An API-shaped path keeps its JSON (the
|
|
9
11
|
// self-describing 404 of api-path-404.js; the trailing error net answers the R01
|
|
@@ -44,24 +46,23 @@ function errorPageTemplate(status, { notFound = 'Page not found.' } = {}) {
|
|
|
44
46
|
<meta name="robots" content="noindex">
|
|
45
47
|
<title>${escapeHtml(heading)} · {{productName}}</title>
|
|
46
48
|
<link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
|
|
47
|
-
|
|
49
|
+
<!-- The hall's sheet carries the shared scales (tokens.css), the derived colour tier
|
|
50
|
+
and both dark twins, so this page declares no colour of its own (task 1005135). -->
|
|
51
|
+
<link rel="stylesheet" href="/modules/hall-ui/public/style.css">
|
|
48
52
|
<style>
|
|
49
|
-
/* The fallback contract tier at zero specificity: the pack's spliced :root always wins. */
|
|
50
|
-
:where(html){--bg:#edecee;--bg-card:#fbfafc;--ink:#17181a;--ink-soft:#55565a;--ink-faint:#65656a;--rule-soft:#dcdbdd;--accent:#fb9569;--accent-deep:#a3481a;--font-display:"Manrope","Segoe UI",system-ui,sans-serif;--font-body:"Manrope","Segoe UI",system-ui,sans-serif}
|
|
51
|
-
html{color-scheme:light;--accent-ink:color-mix(in srgb,var(--accent) 45%,#240e00);--focus:color-mix(in srgb,var(--accent-deep) 82%,#3a1400)}
|
|
52
|
-
html[data-theme="dark"]{color-scheme:dark;--bg:#000000;--bg-card:#101012;--ink:#ffffff;--ink-soft:#c0c0c0;--ink-faint:#a8a8a8;--rule-soft:rgba(255,255,255,.08);--accent-ink:var(--accent);--focus:var(--accent)}
|
|
53
|
-
@media (prefers-color-scheme:dark){html:not([data-theme]){color-scheme:dark;--bg:#000000;--bg-card:#101012;--ink:#ffffff;--ink-soft:#c0c0c0;--ink-faint:#a8a8a8;--rule-soft:rgba(255,255,255,.08);--accent-ink:var(--accent);--focus:var(--accent)}}
|
|
54
53
|
*{box-sizing:border-box}
|
|
55
54
|
body{margin:0;min-height:100vh;display:flex;flex-direction:column;background:var(--bg);color:var(--ink);font-family:var(--font-body);font-size:var(--ds-text-2xl,17px);line-height:1.5}
|
|
55
|
+
header,main{position:relative;z-index:1}
|
|
56
56
|
header{display:flex;align-items:center;gap:var(--ds-space-xs,10px);min-height:56px;padding:0 var(--ds-space-gutter-narrow,20px);border-bottom:var(--ds-line,1px) solid var(--rule-soft)}
|
|
57
57
|
.mark{width:28px;height:28px;--mark-ink:var(--ink)}
|
|
58
|
-
.name{font-family:var(--font-display);font-size:var(--ds-text-
|
|
58
|
+
.name{font-family:var(--font-display);font-size:var(--ds-text-lg,15px);font-weight:300;letter-spacing:-.01em;text-transform:lowercase;color:var(--ink);text-decoration:none}
|
|
59
59
|
main{flex:1;display:flex;align-items:center;justify-content:center;padding:var(--ds-space-xl,52px) var(--ds-space-gutter-narrow,20px)}
|
|
60
60
|
.card{max-width:34rem}
|
|
61
|
-
.code{margin:0 0 var(--ds-space-sm,14px);font-family:
|
|
61
|
+
.code{margin:0 0 var(--ds-space-sm,14px);font-family:var(--font-mono);font-size:var(--ds-text-2xs,11px);letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft)}
|
|
62
62
|
h1{margin:0 0 var(--ds-space-sm,14px);font-family:var(--font-display);font-size:var(--ds-text-headline,clamp(26px,3.1vw,42px));font-weight:300;line-height:1.06;letter-spacing:-.035em}
|
|
63
63
|
p.sub{margin:0 0 var(--ds-space-md,22px);color:var(--ink-soft)}
|
|
64
|
-
a.back{display:inline-flex;align-items:center;min-height:46px;padding:0 var(--ds-space-md,22px);border-radius:var(--ds-radius-pill,999px);background:var(--accent);color
|
|
64
|
+
a.back{display:inline-flex;align-items:center;min-height:46px;padding:0 var(--ds-space-md,22px);border-radius:var(--ds-radius-pill,999px);background:var(--accent);color:var(--accent-on);font-weight:500;text-decoration:none}
|
|
65
|
+
a.back:hover{background:var(--accent-lift);text-decoration:none}
|
|
65
66
|
a:focus-visible{outline:2px solid var(--focus);outline-offset:3px;box-shadow:0 0 0 8px var(--bg)}
|
|
66
67
|
</style>
|
|
67
68
|
</head>
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.59'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/theme-markup.js — the branding pack's THEME markup for a served page (R57, task
|
|
2
|
+
// 1196, ADR 0062 §3). Moved out of src/bongos/serve-internal.js (which re-exports it)
|
|
3
|
+
// so a page built outside the server's HTML maps — the sign-in screens in
|
|
4
|
+
// src/bongos/sign-in-pages.js — splices the pack exactly as every other page does,
|
|
5
|
+
// without requiring the server module that mounts the routes that render it
|
|
6
|
+
// (task 1005135). Pure over its input.
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { escapeHtml: esc } = require('./html-escape');
|
|
10
|
+
const { nightTintCss } = require('./night-tint');
|
|
11
|
+
|
|
12
|
+
// Map the pack's camelCase theme color keys → the kebab-case CSS custom
|
|
13
|
+
// properties the hall/status stylesheets read (public-{builders,status}/style.css
|
|
14
|
+
// :root). R57 (task 1196).
|
|
15
|
+
const COLOR_VARS = {
|
|
16
|
+
bg: '--bg', bgCard: '--bg-card', bgDeep: '--bg-deep', bgFrame: '--bg-frame',
|
|
17
|
+
ink: '--ink', inkSoft: '--ink-soft', inkFaint: '--ink-faint',
|
|
18
|
+
rule: '--rule', ruleSoft: '--rule-soft',
|
|
19
|
+
accent: '--accent', accentDeep: '--accent-deep', sea: '--sea', terracotta: '--terracotta',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Build the THEME markup for a served page from the client-safe pack's theme:
|
|
23
|
+
// a <style> overriding the :root palette + font-family vars, the web-font <link>s,
|
|
24
|
+
// and the favicon <link>. Returns '' when the pack carries no theme (so a pack
|
|
25
|
+
// without a `theme.ui` block is a no-op and the stylesheet's own defaults stand).
|
|
26
|
+
// The <style> is emitted so it lands AFTER the page's own stylesheet <link>
|
|
27
|
+
// (which sits earlier in <head>) — equal specificity, later wins — making the
|
|
28
|
+
// pack authoritative over style.css. For OTB the values equal style.css's
|
|
29
|
+
// parchment defaults, so the render is byte-identical; a neutral instance gets
|
|
30
|
+
// its plain palette + system fonts. R57 (task 1196, ADR 0062 §3).
|
|
31
|
+
function themeMarkup(theme) {
|
|
32
|
+
if (!theme) return '';
|
|
33
|
+
const decls = [];
|
|
34
|
+
for (const [key, cssVar] of Object.entries(COLOR_VARS)) {
|
|
35
|
+
const v = cssSafe(theme.colors?.[key]);
|
|
36
|
+
if (v) decls.push(`${cssVar}:${v};`);
|
|
37
|
+
}
|
|
38
|
+
const display = cssSafe(theme.fonts?.display);
|
|
39
|
+
if (display) decls.push(`--font-display:${display};`);
|
|
40
|
+
const body = cssSafe(theme.fonts?.body);
|
|
41
|
+
if (body) decls.push(`--font-body:${body};`);
|
|
42
|
+
let out = '';
|
|
43
|
+
for (const href of theme.fonts?.links || []) {
|
|
44
|
+
out += `<link rel="stylesheet" href="${esc(href)}">`;
|
|
45
|
+
}
|
|
46
|
+
// `backdrop` paints the page background (e.g. the Cloud Bongos sunrise gradient,
|
|
47
|
+
// ADR 0064 / R74) over the flat var(--bg); omitted packs (OTB) keep --bg.
|
|
48
|
+
const css = [];
|
|
49
|
+
if (decls.length) css.push(`:root{${decls.join('')}}`);
|
|
50
|
+
const backdrop = cssSafe(theme.backdrop);
|
|
51
|
+
if (backdrop) css.push(`body{background:${backdrop};}`);
|
|
52
|
+
// The night tint (ADR 0358): a hall wearing a look takes a hint of its accent into
|
|
53
|
+
// its dark grounds. Written as the sheet's own two dark twins, after the sheet, so
|
|
54
|
+
// equal specificity wins on order. clientBranding only sets it with a look, so a
|
|
55
|
+
// hall that chose none keeps the sheet's pure black.
|
|
56
|
+
if (theme.nightTint === true) css.push(nightTintCss());
|
|
57
|
+
if (css.length) out += `<style>${css.join('')}</style>`;
|
|
58
|
+
// esc() is lossless in an attribute: the HTML parser decodes the entities back
|
|
59
|
+
// before anything reads the URL, so the neutral pack's inline `data:image/svg+xml,
|
|
60
|
+
// <svg …>` favicon resolves to exactly the same bytes it does today.
|
|
61
|
+
if (theme.favicon) out += `<link rel="icon" href="${esc(theme.favicon)}">`;
|
|
62
|
+
if (theme.appleTouchIcon) out += `<link rel="apple-touch-icon" href="${esc(theme.appleTouchIcon)}">`;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A pack-supplied value on its way into <style> TEXT. esc() is the wrong tool
|
|
67
|
+
// here and would corrupt the output: CSS is not entity-decoded, so a font stack
|
|
68
|
+
// like `"Cinzel", serif` would reach the browser as `"Cinzel", serif`
|
|
69
|
+
// and simply not parse. There is no encoding that both survives CSS and
|
|
70
|
+
// neutralizes markup, so this fails closed instead. The only way out of a <style>
|
|
71
|
+
// element is a literal `</style`, every route to it begins with `<`, and no
|
|
72
|
+
// colour, font stack, or backdrop has a legitimate use for one — so a value
|
|
73
|
+
// carrying `<` is dropped whole rather than emitted half-escaped. Dropping one
|
|
74
|
+
// declaration loses that pack's colour and falls back to style.css's default;
|
|
75
|
+
// emitting it loses the page.
|
|
76
|
+
function cssSafe(v) {
|
|
77
|
+
const s = String(v == null ? '' : v);
|
|
78
|
+
return s.includes('<') ? '' : s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { themeMarkup };
|
package/tests/brand_mark.mjs
CHANGED
|
@@ -100,7 +100,7 @@ test('the brand folder holds exactly what gen-brand.js writes from the source (l
|
|
|
100
100
|
const c = packColors();
|
|
101
101
|
assert.match(files['mark-light.svg'], new RegExp(c.ink));
|
|
102
102
|
assert.match(files['mark-light.svg'], new RegExp(c.accent));
|
|
103
|
-
assert.match(files['mark-dark.svg'],
|
|
103
|
+
assert.match(files['mark-dark.svg'], new RegExp(c.bgDeep), 'the dark logo is drawn in the pack\'s Night ink (bgDeep)');
|
|
104
104
|
assert.doesNotMatch(files['mark-mono.svg'], new RegExp(c.accent), 'mono is one ink');
|
|
105
105
|
assert.match(files['favicon.svg'], /prefers-color-scheme:dark/, 'the tab icon has its night twin');
|
|
106
106
|
const png = fs.readFileSync(path.join(ROOT, BRAND_DIR, 'apple-touch-icon.png'));
|
|
@@ -193,3 +193,13 @@ test('one favicon: every page fallback and the pack point at favicon.svg; a look
|
|
|
193
193
|
assert.match(looked.theme.ui.favicon, /stroke-width='24'/, 'the small cut');
|
|
194
194
|
assert.equal(looked.theme.ui.appleTouchIcon, '', 'the pack-coloured touch icon steps aside');
|
|
195
195
|
});
|
|
196
|
+
|
|
197
|
+
// task 1005133: the Night ink every dark twin of the mark uses is the pack's bgDeep, read
|
|
198
|
+
// from config/branding.neutral.json — not a hex typed into src/brand-mark.js.
|
|
199
|
+
test('the Night ink comes from the brand pack, not a typed-in colour', () => {
|
|
200
|
+
const { NIGHT_INK, faviconSvg } = require('../src/brand-mark.js');
|
|
201
|
+
const pack = JSON.parse(read('config', 'branding.neutral.json'));
|
|
202
|
+
assert.equal(NIGHT_INK, pack.theme.ui.colors.bgDeep);
|
|
203
|
+
assert.doesNotMatch(read('src', 'brand-mark.js').replace(/\/\/.*$/gm, ''), /#[0-9a-f]{3,8}\b/i, 'src/brand-mark.js types in no colour');
|
|
204
|
+
assert.match(faviconSvg({ ink: '#111111', accent: '#222222' }), new RegExp(`--i:${pack.theme.ui.colors.bgDeep}`), 'a looked favicon\'s night twin');
|
|
205
|
+
});
|
package/tests/brand_surfaces.mjs
CHANGED
|
@@ -144,7 +144,7 @@ test('a page route that throws answers the branded 500 to a browser and JSON to
|
|
|
144
144
|
assert.ok(errorPageTemplate(404, { notFound: 'Gone.' }).includes('<h1>Gone.</h1>'));
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
test('the
|
|
147
|
+
test('the docs write their dark tier twice, byte-identical; the error page writes none of its own', () => {
|
|
148
148
|
const twins = (src) => {
|
|
149
149
|
const a = src.match(/html\[data-theme="dark"\]\s*\{([^}]*)\}/);
|
|
150
150
|
const b = src.match(/@media \(prefers-color-scheme:\s*dark\)\s*\{\s*html:not\(\[data-theme\]\)\s*\{([^}]*)\}/);
|
|
@@ -152,7 +152,10 @@ test('the error page and the docs write their dark tier twice, byte-identical',
|
|
|
152
152
|
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
153
153
|
assert.equal(norm(b[1]), norm(a[1]));
|
|
154
154
|
};
|
|
155
|
-
twins(
|
|
155
|
+
// the error page takes its colours and both dark twins from the hall's sheet (task 1005135)
|
|
156
|
+
const ep = errorPageTemplate(404);
|
|
157
|
+
assert.ok(ep.includes('<link rel="stylesheet" href="/modules/hall-ui/public/style.css">'), 'the error page loads the shared sheet');
|
|
158
|
+
assert.doesNotMatch(ep, /data-theme="dark"|prefers-color-scheme/, 'no private dark tier');
|
|
156
159
|
twins(fs.readFileSync(path.join(ROOT, 'public-docs', 'index.html'), 'utf8'));
|
|
157
160
|
});
|
|
158
161
|
|
|
@@ -99,6 +99,8 @@ const GENERATOR_SOURCES = [
|
|
|
99
99
|
'modules/hall-ui/public/controls.css',
|
|
100
100
|
'modules/hall-ui/public/dom-utils.js',
|
|
101
101
|
'scripts/bongos/gen-brand.js',
|
|
102
|
+
'src/brand-mark.js',
|
|
103
|
+
'src/html-escape.js',
|
|
102
104
|
'DESIGN.md',
|
|
103
105
|
];
|
|
104
106
|
|
|
@@ -132,3 +134,39 @@ test('the freshness gate passes on a fresh tree and fails when the token sheet c
|
|
|
132
134
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
133
135
|
}
|
|
134
136
|
});
|
|
137
|
+
|
|
138
|
+
// task 1005133: the logo set joins the gate. Editing the one source (mark.svg) without
|
|
139
|
+
// running gen-brand.js fails it; so does a PNG whose source page moved (the pack's ground)
|
|
140
|
+
// or a PNG swapped by hand — the PNGs are checked by hash, not just their pixel size.
|
|
141
|
+
test('the freshness gate covers the logo set: mark.svg, the pack and both PNGs', () => {
|
|
142
|
+
const dir = copiedTree();
|
|
143
|
+
const brand = path.join(dir, 'modules/ui-design/public/brand');
|
|
144
|
+
const gate = () => checkDesignGeneratorsFresh(dir).filter((x) => x.startsWith('the logo set'));
|
|
145
|
+
try {
|
|
146
|
+
assert.deepEqual(gate(), [], 'the copied logo set should start fresh');
|
|
147
|
+
|
|
148
|
+
const mark = path.join(brand, 'mark.svg');
|
|
149
|
+
const src = fs.readFileSync(mark, 'utf8');
|
|
150
|
+
const moved = src.replace(/(id="mark-dot"[^>]*?\br=")(\d+)/, (_, a, r) => a + (Number(r) + 1));
|
|
151
|
+
assert.notEqual(moved, src, 'mark.svg should carry the moon\'s radius');
|
|
152
|
+
fs.writeFileSync(mark, moved);
|
|
153
|
+
assert.ok(gate().some((x) => /mark-light\.svg/.test(x)), `editing mark.svg must fail the gate: ${gate().join('\n')}`);
|
|
154
|
+
fs.writeFileSync(mark, src);
|
|
155
|
+
assert.deepEqual(gate(), [], 'restoring mark.svg restores the gate');
|
|
156
|
+
|
|
157
|
+
const pngFile = path.join(brand, 'og-card.png');
|
|
158
|
+
const png = fs.readFileSync(pngFile);
|
|
159
|
+
const swapped = Buffer.from(png); swapped[swapped.length - 20] ^= 0xff; // same size, other bytes
|
|
160
|
+
fs.writeFileSync(pngFile, swapped);
|
|
161
|
+
assert.ok(gate().some((x) => /og-card\.png \(the file is not the one --png wrote\)/.test(x)), gate().join('\n'));
|
|
162
|
+
fs.writeFileSync(pngFile, png);
|
|
163
|
+
|
|
164
|
+
const packFile = path.join(dir, 'config/branding.neutral.json');
|
|
165
|
+
const pack = JSON.parse(fs.readFileSync(packFile, 'utf8'));
|
|
166
|
+
pack.theme.ui.colors.bg = '#edeced';
|
|
167
|
+
fs.writeFileSync(packFile, JSON.stringify(pack, null, 2));
|
|
168
|
+
assert.ok(gate().some((x) => /apple-touch-icon\.png \(its source page changed/.test(x)), gate().join('\n'));
|
|
169
|
+
} finally {
|
|
170
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -88,7 +88,7 @@ test('the shared components, the logo and the naming rule are on the page', () =
|
|
|
88
88
|
}
|
|
89
89
|
assert.equal((PAGE.match(/data-ref="tones"[^>]*>(.*?)<\/div>/) || [])[1].match(/status-pill--/g).length, 6, 'six tones');
|
|
90
90
|
assert.match(PAGE, /<use href="\/modules\/ui-design\/public\/brand\/mark\.svg#mark"\/>/, 'the mark is drawn from its one source');
|
|
91
|
-
assert.match(PAGE, /<span class="ref-lockup__name"
|
|
91
|
+
assert.match(PAGE, /<span class="ref-lockup__name">\{\{productName\}\}<\/span>/, 'the wordmark is the pack\'s name, stamped at serve time (task 1005135)');
|
|
92
92
|
assert.match(PAGE, /Never "CloudBongos"/, 'the naming rule is read from DESIGN.md');
|
|
93
93
|
for (const f of ['mark-light.svg', 'mark-dark.svg', 'mark-mono.svg', 'favicon.svg']) assert.ok(PAGE.includes(`brand/${f}"`), `lists ${f}`);
|
|
94
94
|
});
|