@bongos/core 1.24.47 → 1.24.49
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 +165 -110
- package/config/branding.neutral.json +2 -1
- package/docs/copy-inventory.md +1028 -1028
- package/docs/copy-registry.json +1448 -1448
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +625 -625
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.css +3 -22
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/controls.css +223 -0
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.css +4 -23
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +5 -5
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/money.html +2 -2
- package/modules/hall-ui/public/my-agents.css +3 -25
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/oversight.css +3 -17
- package/modules/hall-ui/public/panel.css +6 -48
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +1 -1
- package/modules/hall-ui/public/speciality-tools.js +14 -14
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +31 -255
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/welcome-boot.css +3 -4
- package/modules/hall-ui/public/welcome-boot.js +5 -3
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/public-landing/public/account.html +1 -5
- package/modules/public-landing/public/assets/cosmos.css +7 -6
- package/modules/public-landing/public/community.html +1 -5
- package/modules/public-landing/public/connections.html +1 -5
- package/modules/public-landing/public/contact.html +1 -5
- package/modules/public-landing/public/guild.html +1 -5
- package/modules/public-landing/public/index.html +5 -8
- package/modules/public-landing/public/invitations.html +1 -5
- package/modules/public-landing/public/privacy.html +1 -5
- package/modules/public-landing/public/projects.html +1 -5
- package/modules/public-landing/public/terms.html +1 -5
- package/modules/raise/views/gate.html +4 -3
- package/modules/raise/views/nda.html +2 -1
- package/modules/raise/views/page.html +4 -3
- package/modules/status-ui/public/index.html +1 -1
- package/modules/status-ui/public/style.css +2 -2
- package/modules/ui-design/public/brand/apple-touch-icon.png +0 -0
- package/modules/ui-design/public/brand/favicon.svg +1 -0
- package/modules/ui-design/public/brand/mark-body.svg +1 -0
- package/modules/ui-design/public/brand/mark-dark.svg +1 -0
- package/modules/ui-design/public/brand/mark-dot.svg +1 -0
- package/modules/ui-design/public/brand/mark-light.svg +1 -0
- package/modules/ui-design/public/brand/mark-mono.svg +1 -0
- package/modules/ui-design/public/brand/mark.svg +21 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/fitness.js +1 -2
- package/scripts/bongos/gen-brand.js +99 -0
- package/scripts/gds/gen-brand.js +7 -0
- package/src/bongos/platform-visibility-gate.js +6 -3
- package/src/bongos/prelaunch-gate.js +6 -3
- package/src/bongos/serve-internal.js +1 -0
- package/src/brand-mark.js +96 -0
- package/src/branding.js +2 -0
- package/src/hall-look.js +7 -7
- package/src/module-api.js +1 -1
- package/tests/brand_mark.mjs +195 -0
- package/tests/bulk_toolbar.mjs +4 -1
- package/tests/cloud_bongos_pack.mjs +5 -2
- package/tests/hall_board_world.mjs +6 -2
- package/tests/hall_builders_page_pass.mjs +6 -4
- package/tests/hall_controls.mjs +274 -0
- package/tests/hall_loading.mjs +8 -10
- package/tests/hall_oversight_world.mjs +7 -4
- package/tests/hall_record_world.mjs +21 -7
- package/tests/hall_settings_world.mjs +5 -2
- package/tests/loading_mark.mjs +7 -13
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/raise.mjs +3 -1
- package/tests/welcome_boot.mjs +5 -2
- package/modules/raise/public/favicon.svg +0 -1
- package/modules/raise/public/logo-ink.svg +0 -1
- package/modules/raise/public/logo-night.svg +0 -1
package/src/hall-look.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
const fs = require('node:fs');
|
|
25
25
|
const path = require('node:path');
|
|
26
|
+
const { faviconSvg, svgDataUri } = require('./brand-mark');
|
|
26
27
|
|
|
27
28
|
const HEX = /^#[0-9a-f]{6}$/i;
|
|
28
29
|
const LOOK_NAME = /^[a-z][a-z-]{0,39}$/;
|
|
@@ -53,13 +54,10 @@ function readLibraryLook(name, { root } = {}) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// The favicon a looked hall shows without a logo: the Bongos mark (task 1004846) in the
|
|
56
|
-
// look's own ink with its accent as the moon, and the Night ink twin in dark mode.
|
|
57
|
+
// look's own ink with its accent as the moon, and the Night ink twin in dark mode. Drawn from
|
|
58
|
+
// the mark's one source (task 1005088); the colours are the look's validated hex.
|
|
57
59
|
function lookFavicon(colors) {
|
|
58
|
-
|
|
59
|
-
return "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'>" +
|
|
60
|
-
`<style>.r{stroke:${c('ink')}}.p{fill:${c('ink')}}@media (prefers-color-scheme:dark){.r{stroke:%23e2e1e4}.p{fill:%23e2e1e4}}</style>` +
|
|
61
|
-
"<path class='r' d='M137 31A78 78 0 1 0 170 66' fill='none' stroke-width='24' stroke-linecap='round'/>" +
|
|
62
|
-
`<circle class='p' cx='100' cy='100' r='50'/><circle cx='156' cy='46' r='18' fill='${c('accent')}'/></svg>`;
|
|
60
|
+
return svgDataUri(faviconSvg({ ink: colors.ink, accent: colors.accent }));
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
// What the hall wears: { name, ui } or null for its own pack. PURE given readLook.
|
|
@@ -91,8 +89,10 @@ function applyLook(resolved, readLook = readLibraryLook) {
|
|
|
91
89
|
ui.look = look.name;
|
|
92
90
|
ui.nightTint = project.nightTint !== 'off';
|
|
93
91
|
ui.favicon = lookFavicon(ui.colors);
|
|
92
|
+
// the home-screen icon is the pack's own colours; a hall in other colours goes without
|
|
93
|
+
ui.appleTouchIcon = '';
|
|
94
94
|
}
|
|
95
|
-
if (logo) { ui.logo = logo; ui.favicon = logo; }
|
|
95
|
+
if (logo) { ui.logo = logo; ui.favicon = logo; ui.appleTouchIcon = ''; }
|
|
96
96
|
resolved.theme.ui = ui;
|
|
97
97
|
return resolved;
|
|
98
98
|
}
|
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.49'; // 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,195 @@
|
|
|
1
|
+
// tests/brand_mark.mjs — one logo and one favicon set, served from one place (task 1005088,
|
|
2
|
+
// goal 1000138 "One Cloud Bongos design system").
|
|
3
|
+
//
|
|
4
|
+
// The ring-and-dot mark used to be pasted ~22 times (the landing pages, the gates, the hall
|
|
5
|
+
// shell, hall-render, welcome-boot, raise, three loaders' masks) beside four favicon schemes.
|
|
6
|
+
// Now it lives in modules/ui-design/public/brand/mark.svg and nothing else draws it by hand.
|
|
7
|
+
// What this pins, DB-free and browser-free:
|
|
8
|
+
// * grep finds ONE source: no file outside the brand folder carries the mark's geometry
|
|
9
|
+
// (comments stripped), and the brand folder's variants are exactly what
|
|
10
|
+
// scripts/bongos/gen-brand.js writes from the source (so they are outputs, not copies);
|
|
11
|
+
// * every page uses it: the landing and hub pages, the hall shell, hall-render and the
|
|
12
|
+
// welcome reference mark.svg; the three loaders mask with mark-body/mark-dot; raise shows
|
|
13
|
+
// mark-light/mark-dark; the gates and a looked hall's favicon draw it through
|
|
14
|
+
// src/brand-mark.js; every page's fallback favicon and the pack's favicon are the one
|
|
15
|
+
// favicon.svg, with the apple-touch-icon beside it;
|
|
16
|
+
// * a project's own logo (theme.logo) still takes the mark's place.
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test tests/brand_mark.mjs
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
30
|
+
const BRAND = '/modules/ui-design/public/brand/';
|
|
31
|
+
const BRAND_DIR = path.join('modules', 'ui-design', 'public', 'brand');
|
|
32
|
+
|
|
33
|
+
// The mark's fingerprints: the ring's arc and the moon's centre, in any quoting or spacing.
|
|
34
|
+
const FINGERPRINTS = [/A\s*78[\s,]+78\b/, /cx=\\?['"]156\\?['"]\s+cy=\\?['"]46\\?['"]/, /\bcx%3D%27156/];
|
|
35
|
+
// Strip comments so a doc line naming the arc is not a copy (block, line and HTML comments).
|
|
36
|
+
const decomment = (s) => s
|
|
37
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
38
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
39
|
+
.replace(/(^|[^:'"\\])\/\/.*$/gm, '$1');
|
|
40
|
+
|
|
41
|
+
const SCAN_ROOTS = ['src', 'modules', 'scripts', 'config', 'public', 'public-docs', 'server.js'];
|
|
42
|
+
const TEXT = /\.(js|mjs|cjs|html|css|json|svg)$/;
|
|
43
|
+
function walk(rel, out = []) {
|
|
44
|
+
const abs = path.join(ROOT, rel);
|
|
45
|
+
if (!fs.existsSync(abs)) return out;
|
|
46
|
+
const st = fs.statSync(abs);
|
|
47
|
+
if (st.isFile()) { if (TEXT.test(rel)) out.push(rel); return out; }
|
|
48
|
+
for (const name of fs.readdirSync(abs)) {
|
|
49
|
+
if (name === 'node_modules' || name.startsWith('.')) continue;
|
|
50
|
+
walk(path.join(rel, name), out);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Pure: the files (outside the brand folder) that carry the mark's geometry.
|
|
56
|
+
function pastedCopies(files, readFile) {
|
|
57
|
+
return files.filter((f) => !f.startsWith(BRAND_DIR + path.sep))
|
|
58
|
+
.filter((f) => { const code = decomment(readFile(f)); return FINGERPRINTS.some((re) => re.test(code)); });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('grep finds one source for the mark: no pasted copy outside the brand folder', () => {
|
|
62
|
+
const files = SCAN_ROOTS.flatMap((r) => walk(r));
|
|
63
|
+
assert.ok(files.length > 500, 'the scan reads the tree');
|
|
64
|
+
assert.deepEqual(pastedCopies(files, (f) => read(f)), [],
|
|
65
|
+
'draw the mark by reference to modules/ui-design/public/brand/mark.svg (or src/brand-mark.js on the server), never a pasted copy');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('the guard is real: a pasted copy in any of the three spellings is found', () => {
|
|
69
|
+
const fake = {
|
|
70
|
+
'modules/x/public/a.html': '<svg><path d="M137 31 A78 78 0 1 0 170 66"/></svg>',
|
|
71
|
+
'modules/x/public/b.css': ".l{--s:url(\"data:image/svg+xml,%3Csvg%3E%3Ccircle cx='156' cy='46' r='12'/%3E%3C/svg%3E\")}",
|
|
72
|
+
'src/c.js': "const s = \"<path class='r' d='M137 31A78 78 0 1 0 170 66'/>\";",
|
|
73
|
+
'src/d.js': '// the arc A78 78 is the ring\nconst ok = 1;',
|
|
74
|
+
[path.join(BRAND_DIR, 'mark.svg')]: '<path d="M137 31 A78 78 0 1 0 170 66"/>',
|
|
75
|
+
};
|
|
76
|
+
assert.deepEqual(pastedCopies(Object.keys(fake), (f) => fake[f]).sort(),
|
|
77
|
+
['modules/x/public/a.html', 'modules/x/public/b.css', 'src/c.js']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the source carries the three parts and their two colours', () => {
|
|
81
|
+
const src = read(BRAND_DIR, 'mark.svg');
|
|
82
|
+
for (const id of ['mark', 'mark-ring', 'mark-disc', 'mark-dot']) assert.match(src, new RegExp(`id="${id}"`));
|
|
83
|
+
// A browser reads a referenced SVG as XML: a comment holding "--" (say, a custom property's
|
|
84
|
+
// name) makes the whole file unreadable and every page's mark silently vanishes.
|
|
85
|
+
for (const [, body] of src.matchAll(/<!--([\s\S]*?)-->/g)) assert.ok(!body.includes('--'), 'no "--" inside an XML comment');
|
|
86
|
+
assert.equal((src.match(/<!--/g) || []).length, (src.match(/-->/g) || []).length, 'every comment closes');
|
|
87
|
+
const { markParts } = require('../src/brand-mark.js');
|
|
88
|
+
const parts = markParts(src);
|
|
89
|
+
assert.ok(parts, 'src/brand-mark.js reads the parts');
|
|
90
|
+
assert.match(parts.ring + parts.disc, /var\(--mark-ink,currentColor\)/);
|
|
91
|
+
assert.match(parts.dot, /var\(--mark-dot,var\(--accent,currentColor\)\)/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('the brand folder holds exactly what gen-brand.js writes from the source (light, dark, mono, favicon)', () => {
|
|
95
|
+
const { brandFiles, packColors } = require('../scripts/bongos/gen-brand.js');
|
|
96
|
+
const files = brandFiles(read(BRAND_DIR, 'mark.svg'), packColors());
|
|
97
|
+
for (const [name, text] of Object.entries(files)) {
|
|
98
|
+
assert.equal(read(BRAND_DIR, name), text, `${name} is stale — run node scripts/bongos/gen-brand.js`);
|
|
99
|
+
}
|
|
100
|
+
const c = packColors();
|
|
101
|
+
assert.match(files['mark-light.svg'], new RegExp(c.ink));
|
|
102
|
+
assert.match(files['mark-light.svg'], new RegExp(c.accent));
|
|
103
|
+
assert.match(files['mark-dark.svg'], /#e2e1e4/);
|
|
104
|
+
assert.doesNotMatch(files['mark-mono.svg'], new RegExp(c.accent), 'mono is one ink');
|
|
105
|
+
assert.match(files['favicon.svg'], /prefers-color-scheme:dark/, 'the tab icon has its night twin');
|
|
106
|
+
const png = fs.readFileSync(path.join(ROOT, BRAND_DIR, 'apple-touch-icon.png'));
|
|
107
|
+
assert.equal(png.toString('ascii', 1, 4), 'PNG');
|
|
108
|
+
assert.deepEqual([png.readUInt32BE(16), png.readUInt32BE(20)], [180, 180], 'the home-screen icon is 180x180');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('every landing and hub page draws the mark by reference', () => {
|
|
112
|
+
const dir = path.join('modules', 'public-landing', 'public');
|
|
113
|
+
const pages = fs.readdirSync(path.join(ROOT, dir)).filter((n) => n.endsWith('.html'))
|
|
114
|
+
.filter((n) => read(dir, n).includes('class="bongosMark"'));
|
|
115
|
+
assert.ok(pages.length >= 10, `the landing pages with a lockup (${pages.length})`);
|
|
116
|
+
for (const n of pages) {
|
|
117
|
+
assert.match(read(dir, n), new RegExp(`<svg class="bongosMark"[^>]*><use href="${BRAND}mark\\.svg#mark"/></svg>`), n);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the hall shell, hall-render and the welcome reference the mark, and a project logo still wins', () => {
|
|
122
|
+
const shell = read('modules', 'hall-ui', 'public', 'shell.js');
|
|
123
|
+
assert.ok(shell.includes(`<use href="${BRAND}mark.svg#mark"/>`), 'the sidebar mark');
|
|
124
|
+
assert.match(shell, /brand\.theme && brand\.theme\.logo\s*\?\s*'<img class="brand-mark__logo"/, 'the logo override');
|
|
125
|
+
const render = read('modules', 'hall-ui', 'public', 'hall-render.js');
|
|
126
|
+
assert.ok(render.includes(`<use href="${BRAND}mark.svg#mark"/>`), 'the signed-out card mark');
|
|
127
|
+
assert.match(render, /logo\s*\?\s*`<img class="landing__logo"/, 'the logo override');
|
|
128
|
+
const wb = read('modules', 'hall-ui', 'public', 'welcome-boot.js');
|
|
129
|
+
assert.ok(wb.includes(`var MARK = '${BRAND}mark.svg'`), 'the welcome banner');
|
|
130
|
+
for (const part of ['#mark-ring', '#mark-disc', '#mark-dot']) assert.ok(wb.includes(part), `the welcome animates ${part}`);
|
|
131
|
+
assert.match(wb, /logo \? '<img class="wb__logo"/, 'the logo override');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('the three loaders cut their shapes from the brand files', () => {
|
|
135
|
+
for (const [file, v] of [
|
|
136
|
+
[['modules', 'hall-ui', 'public', 'style.css'], '--hl-shape'],
|
|
137
|
+
[['modules', 'status-ui', 'public', 'style.css'], '--lm-shape'],
|
|
138
|
+
[['modules', 'public-landing', 'public', 'assets', 'cosmos.css'], '--lm-shape'],
|
|
139
|
+
]) {
|
|
140
|
+
const css = read(...file);
|
|
141
|
+
assert.ok(new RegExp(`${v}:\\s*url\\("${BRAND}mark-body\\.svg"\\)`).test(css), `${file.join('/')} ring and planet`);
|
|
142
|
+
assert.ok(new RegExp(`${v}:\\s*url\\("${BRAND}mark-dot\\.svg"\\)`).test(css), `${file.join('/')} moon`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('raise shows the shared logo pair and favicon', () => {
|
|
147
|
+
for (const v of ['gate', 'page', 'nda']) {
|
|
148
|
+
const html = read('modules', 'raise', 'views', `${v}.html`);
|
|
149
|
+
assert.ok(html.includes(`<link rel="icon" href="${BRAND}favicon.svg"`), `${v}: favicon`);
|
|
150
|
+
assert.ok(html.includes(`<link rel="apple-touch-icon" href="${BRAND}apple-touch-icon.png">`), `${v}: touch icon`);
|
|
151
|
+
if (html.includes('class="logo')) {
|
|
152
|
+
assert.ok(html.includes(`src="${BRAND}mark-light.svg"`) && html.includes(`src="${BRAND}mark-dark.svg"`), `${v}: logo pair`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the gates draw the mark from the source and still load nothing', () => {
|
|
158
|
+
const { markSvg } = require('../src/brand-mark.js');
|
|
159
|
+
const svg = markSvg({ className: 'mark' });
|
|
160
|
+
assert.match(svg, /^<svg class="mark" viewBox="0 0 200 200" aria-hidden="true"><path [^>]*\/><circle [^>]*\/><circle [^>]*\/><\/svg>$/);
|
|
161
|
+
assert.doesNotMatch(svg, /\sid=/, 'an inline copy carries no ids');
|
|
162
|
+
for (const f of ['prelaunch-gate.js', 'platform-visibility-gate.js']) {
|
|
163
|
+
const src = read('src', 'bongos', f);
|
|
164
|
+
assert.ok(src.includes("${markSvg({ className: 'mark' })}"), `${f} draws the mark`);
|
|
165
|
+
assert.match(src, /\.mark\{[^}]*--mark-ink:#17181a;--mark-dot:#fb9569\}/, `${f} colours it`);
|
|
166
|
+
assert.match(src, /prefers-color-scheme:dark\)\{\.mark\{--mark-ink:#e2e1e4\}\}/, `${f} night twin`);
|
|
167
|
+
assert.doesNotMatch(src, /<use href=/, `${f} references nothing — it is served before any asset`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('one favicon: every page fallback and the pack point at favicon.svg; a look or a logo overrides', () => {
|
|
172
|
+
for (const dir of [['modules', 'hall-ui', 'public'], ['modules', 'status-ui', 'public']]) {
|
|
173
|
+
for (const n of fs.readdirSync(path.join(ROOT, ...dir)).filter((x) => x.endsWith('.html'))) {
|
|
174
|
+
const icons = read(...dir, n).match(/<link rel="icon"[^>]*>/g) || [];
|
|
175
|
+
for (const l of icons) assert.ok(l.includes(`href="${BRAND}favicon.svg"`), `${dir.join('/')}/${n}: ${l.slice(0, 80)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const pack = JSON.parse(read('config', 'branding.neutral.json')).theme.ui;
|
|
179
|
+
assert.equal(pack.favicon, `${BRAND}favicon.svg`);
|
|
180
|
+
assert.equal(pack.appleTouchIcon, `${BRAND}apple-touch-icon.png`);
|
|
181
|
+
|
|
182
|
+
const { themeMarkup } = require('../src/bongos/serve-internal.js');
|
|
183
|
+
const m = themeMarkup({ favicon: pack.favicon, appleTouchIcon: pack.appleTouchIcon, fonts: { links: [] } });
|
|
184
|
+
assert.ok(m.includes(`<link rel="icon" href="${BRAND}favicon.svg">`));
|
|
185
|
+
assert.ok(m.includes(`<link rel="apple-touch-icon" href="${BRAND}apple-touch-icon.png">`));
|
|
186
|
+
assert.ok(!themeMarkup({ favicon: 'x', fonts: { links: [] } }).includes('apple-touch-icon'), 'no touch icon unless the pack names one');
|
|
187
|
+
|
|
188
|
+
const { applyLook } = require('../src/hall-look.js');
|
|
189
|
+
const looked = applyLook({ theme: { ui: { ...pack } }, project: { look: 'custom', lookAccent: '#3366cc' } },
|
|
190
|
+
() => ({ colors: { ink: '#101010', accent: '#fb9569' }, fonts: {} }));
|
|
191
|
+
assert.match(looked.theme.ui.favicon, /^data:image\/svg\+xml;utf8,<svg /);
|
|
192
|
+
assert.match(looked.theme.ui.favicon, /%233366cc/, 'drawn in the look\'s accent');
|
|
193
|
+
assert.match(looked.theme.ui.favicon, /stroke-width='24'/, 'the small cut');
|
|
194
|
+
assert.equal(looked.theme.ui.appleTouchIcon, '', 'the pack-coloured touch icon steps aside');
|
|
195
|
+
});
|
package/tests/bulk_toolbar.mjs
CHANGED
|
@@ -196,7 +196,10 @@ test('goals.html no longer borrows work.css, and work.css no longer holds what i
|
|
|
196
196
|
assert.ok(!new RegExp(`^${sel.replace('.', '\\.')}(\\s|:|,|\\{)`, 'm').test(work), `work.css must not restate ${sel}`);
|
|
197
197
|
}
|
|
198
198
|
const style = read('style.css');
|
|
199
|
-
for (const sel of ['.claim-count-line', '.
|
|
199
|
+
for (const sel of ['.claim-count-line', '.wip__select']) {
|
|
200
200
|
assert.ok(new RegExp(`^${sel.replace('.', '\\.')} \\{`, 'm').test(style), `style.css carries ${sel}`);
|
|
201
201
|
}
|
|
202
|
+
// task 1005084: .wip__release is the shared ghost (controls.css); style.css keeps its warning hover
|
|
203
|
+
assert.match(read('controls.css'), /\.wip__release/, 'the shared sheet draws .wip__release');
|
|
204
|
+
assert.match(style, /^\.wip__release:hover:not\(:disabled\) \{/m, 'style.css keeps the release\'s warning hover');
|
|
202
205
|
});
|
|
@@ -43,7 +43,9 @@ test('R74: the vanilla pack carries the Cloud Bongos LOOK (chrome palette, Manro
|
|
|
43
43
|
assert.match(c.theme.fonts.display, /Manrope/);
|
|
44
44
|
assert.match(c.theme.fonts.body, /Manrope/);
|
|
45
45
|
assert.ok(c.theme.fonts.links.some((l) => /JetBrains\+Mono/.test(l) && /Manrope/.test(l)), 'loads Manrope + JetBrains Mono');
|
|
46
|
-
|
|
46
|
+
// the one favicon set (task 1005088): the moon-in-the-gap mark, served from the brand folder
|
|
47
|
+
assert.equal(c.theme.favicon, '/modules/ui-design/public/brand/favicon.svg', 'the moon-in-the-gap favicon, not the Greek Α');
|
|
48
|
+
assert.equal(c.theme.appleTouchIcon, '/modules/ui-design/public/brand/apple-touch-icon.png');
|
|
47
49
|
assert.doesNotMatch(c.theme.favicon, /font-family='serif'>Α/);
|
|
48
50
|
// the backdrop is the TOKEN, not a hex: the served body follows the mode,
|
|
49
51
|
// because the sheets' dark blocks are (0,2,0) and outrank the injected :root.
|
|
@@ -60,7 +62,8 @@ test('R74: serving the hall on a vanilla boot injects the Cloud Bongos chrome th
|
|
|
60
62
|
// palette override + the token-valued body backdrop + bongo favicon + the type links
|
|
61
63
|
assert.match(out, /:root\{[^}]*--accent:#fb9569;/);
|
|
62
64
|
assert.match(out, /body\{background:var\(--bg\);\}/);
|
|
63
|
-
assert.match(out, /<link rel="icon" href="
|
|
65
|
+
assert.match(out, /<link rel="icon" href="\/modules\/ui-design\/public\/brand\/favicon\.svg">/);
|
|
66
|
+
assert.match(out, /<link rel="apple-touch-icon" href="\/modules\/ui-design\/public\/brand\/apple-touch-icon\.png">/);
|
|
64
67
|
assert.match(out, /fonts\.googleapis\.com\/css2\?family=JetBrains\+Mono/);
|
|
65
68
|
// and it lands AFTER the stylesheet so the Cloud Bongos look wins
|
|
66
69
|
assert.ok(out.indexOf('/builders/style.css') < out.indexOf('--accent:#fb9569'));
|
|
@@ -42,6 +42,7 @@ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
|
42
42
|
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
43
43
|
|
|
44
44
|
const styleCss = read('style.css');
|
|
45
|
+
const controlsCss = read('controls.css');
|
|
45
46
|
const workCss = read('work.css');
|
|
46
47
|
const workHtml = read('work.html');
|
|
47
48
|
|
|
@@ -104,7 +105,8 @@ test('a prose link buys BOTH axes — padding for width, negative margin to give
|
|
|
104
105
|
// ---- 3. the world's own bans ------------------------------------------------
|
|
105
106
|
|
|
106
107
|
test('nothing on the board paints text white on the accent', () => {
|
|
107
|
-
|
|
108
|
+
// task 1005084: the claim is the shared primary door, drawn in controls.css
|
|
109
|
+
const claim = controlsCss.replace(/\/\*[\s\S]*?\*\//g, '').match(/([^{}]*\.task__claim\b[^{}]*)\{([^{}]*background:[^{}]*)\}/)?.[2] ?? null;
|
|
108
110
|
assert.ok(claim, '.task__claim has a rule');
|
|
109
111
|
assert.match(claim, /background:\s*var\(--accent\)/, 'the claim button is the page\'s one accent fill');
|
|
110
112
|
assert.match(claim, /color:\s*var\(--accent-on\)/,
|
|
@@ -116,7 +118,9 @@ test('nothing on the board paints text white on the accent', () => {
|
|
|
116
118
|
});
|
|
117
119
|
|
|
118
120
|
test('the shared task + bulk-toolbar rules live in style.css (the shared base), not the page-local work.css', () => {
|
|
119
|
-
|
|
121
|
+
// .task__claim left for the shared controls.css (task 1005084); work.css still must not restate it
|
|
122
|
+
assert.equal(rule(workCss, '.task__claim'), null, 'work.css must not restate .task__claim');
|
|
123
|
+
for (const sel of ['.task__age', '.task__age--stale', '.task__tag--newcomer', '.task__tag--mine', '.task__tag--rank', '.task__tag--rank-open', '.task__touches', '.task', '.bulk-toolbar', '.bulk-toolbar__count', '.bulk-toolbar__actions', '.bulk-toolbar__btn', '.bulk-toolbar__clear']) {
|
|
120
124
|
assert.ok(rule(styleCss, sel), `style.css carries ${sel}, beside .task__tag`);
|
|
121
125
|
assert.equal(rule(workCss, sel), null, `work.css must not restate ${sel}`);
|
|
122
126
|
}
|
|
@@ -147,10 +147,12 @@ test('A: an older branding pack with no joinability falls back to the apply door
|
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
test('A: the quiet fill is a defined variant, not an inline colour', () => {
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
// task 1005084: .landing__btn--quiet is the shared ghost in controls.css
|
|
151
|
+
const css = read('controls.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
152
|
+
const m = css.match(/([^{}]*\.landing__btn--quiet[\s,][^{}]*)\{([^{}]*)\}/);
|
|
153
|
+
assert.ok(m, 'controls.css defines .landing__btn--quiet');
|
|
154
|
+
const block = m[2];
|
|
155
|
+
assert.match(block, /background:\s*transparent/, 'the quiet variant is the ghost');
|
|
154
156
|
assert.deepEqual(block.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'the variant carries no literal colour');
|
|
155
157
|
// Same geometry as the request button means it inherits .landing__btn's
|
|
156
158
|
// min-height and padding rather than restating them.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// tests/hall_controls.mjs — the hall's one button and one field (task 1005084,
|
|
2
|
+
// goal 1000138 "One Cloud Bongos design system").
|
|
3
|
+
//
|
|
4
|
+
// The UI audit counted eight main-button heights and five field styles in the
|
|
5
|
+
// hall. modules/hall-ui/public/controls.css is the one of each that replaced
|
|
6
|
+
// them, and this file is what keeps them one:
|
|
7
|
+
// * THE IMPORT — style.css pulls the sheet in right after the shared token
|
|
8
|
+
// sheet, so every hall page gets it and a page sheet can still place a
|
|
9
|
+
// button (it loads later) without re-drawing it;
|
|
10
|
+
// * THE SIZES — primary is DESIGN.md's 46–48px door, the row size and the
|
|
11
|
+
// text button keep the 24px floor, the field clears it;
|
|
12
|
+
// * CONTRAST, MEASURED — the button's ink on its fill (rest AND hover), the
|
|
13
|
+
// ghost's and the text button's ink on both grounds, the field's text and
|
|
14
|
+
// its edge (3:1, WCAG 1.4.11), in both modes, resolved from the real
|
|
15
|
+
// sheets' tokens the way tests/hall_tokens.mjs does. No white on the salmon;
|
|
16
|
+
// * THE MIGRATION — every older button name the pages still write sits in the
|
|
17
|
+
// shared rules, no page sheet re-draws one (its own height, fill, corner or
|
|
18
|
+
// type size), the dead white-on-salmon setup button stays gone, and the
|
|
19
|
+
// pages the task named (Goals, Work, the setup screen, Settings) write
|
|
20
|
+
// names the shared rules own.
|
|
21
|
+
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
31
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
32
|
+
const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
33
|
+
|
|
34
|
+
const styleSrc = read('style.css');
|
|
35
|
+
const controls = decomment(read('controls.css'));
|
|
36
|
+
|
|
37
|
+
// Every flat rule of a sheet as { sels: [selector…], body }. The hall's sheets
|
|
38
|
+
// nest only @media / @supports, whose inner rules this still reaches (the
|
|
39
|
+
// at-rule's own prelude is dropped by the [^{}] match).
|
|
40
|
+
const rulesOf = (css) => {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const m of decomment(css).matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
43
|
+
const sels = m[1].trim().split(/,(?![^(]*\))/).map((s) => s.trim().replace(/\s+/g, ' ')).filter(Boolean);
|
|
44
|
+
out.push({ sels, body: m[2] });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
const prop = (body, name) => {
|
|
49
|
+
const m = body.match(new RegExp(`(?:^|;)\\s*${name}\\s*:\\s*([^;]+)`));
|
|
50
|
+
return m ? m[1].trim() : null;
|
|
51
|
+
};
|
|
52
|
+
// the body of the first controls.css rule whose selector list holds `sel`
|
|
53
|
+
const sharedRule = (sel) => {
|
|
54
|
+
const r = rulesOf(controls).find((x) => x.sels.includes(sel));
|
|
55
|
+
assert.ok(r, `controls.css has a rule for ${sel}`);
|
|
56
|
+
return r.body;
|
|
57
|
+
};
|
|
58
|
+
// the body of the LAST controls.css rule that holds `sel` AND declares `name` —
|
|
59
|
+
// the cascade's answer when the rules are equally specific
|
|
60
|
+
const sharedWith = (sel, name) => {
|
|
61
|
+
const r = rulesOf(controls).filter((x) => x.sels.includes(sel) && prop(x.body, name) !== null);
|
|
62
|
+
assert.ok(r.length, `controls.css gives ${sel} a ${name}`);
|
|
63
|
+
return r[r.length - 1].body;
|
|
64
|
+
};
|
|
65
|
+
const px = (v) => { const m = String(v).match(/^([\d.]+)px$/); return m ? +m[1] : NaN; };
|
|
66
|
+
|
|
67
|
+
// ── the tokens, resolved (the hall_tokens.mjs resolver, copied: a test file is
|
|
68
|
+
// not a module) ───────────────────────────────────────────────────────────
|
|
69
|
+
const declarations = (body) => {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const m of decomment(body).matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) out[m[1]] = m[2].replace(/\s+/g, ' ').trim();
|
|
72
|
+
return out;
|
|
73
|
+
};
|
|
74
|
+
const bodyAt = (src, open) => {
|
|
75
|
+
let depth = 0;
|
|
76
|
+
for (let i = open; i < src.length; i++) {
|
|
77
|
+
if (src[i] === '{') depth++;
|
|
78
|
+
else if (src[i] === '}' && --depth === 0) return src.slice(open + 1, i);
|
|
79
|
+
}
|
|
80
|
+
throw new Error('unbalanced braces');
|
|
81
|
+
};
|
|
82
|
+
const blockAfter = (src, marker) => {
|
|
83
|
+
const at = src.indexOf(marker);
|
|
84
|
+
assert.notEqual(at, -1, `missing ${marker}`);
|
|
85
|
+
return declarations(bodyAt(src, src.indexOf('{', at)));
|
|
86
|
+
};
|
|
87
|
+
const tokensSrc = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css'), 'utf8');
|
|
88
|
+
const camelToVar = (k) => '--' + k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
89
|
+
const PACK = Object.fromEntries(Object.entries(JSON.parse(
|
|
90
|
+
fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8')).theme.ui.colors)
|
|
91
|
+
.map(([k, v]) => [camelToVar(k), v]));
|
|
92
|
+
const root = blockAfter(styleSrc, '\n:root {');
|
|
93
|
+
const RAW = {
|
|
94
|
+
light: { ...blockAfter(tokensSrc, ':root {'), ...root, ...PACK },
|
|
95
|
+
dark: { ...blockAfter(tokensSrc, ':root {'), ...root, ...PACK, ...blockAfter(styleSrc, 'html[data-theme="dark"] {') },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function resolve(expr, mode, seen = new Set()) {
|
|
99
|
+
const e = String(expr).trim();
|
|
100
|
+
if (e === 'transparent') return [0, 0, 0, 0];
|
|
101
|
+
let m = e.match(/^#([0-9a-f]{6})$/i);
|
|
102
|
+
if (m) { const n = parseInt(m[1], 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1]; }
|
|
103
|
+
m = e.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
|
|
104
|
+
if (m) return [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]];
|
|
105
|
+
m = e.match(/^var\(\s*(--[\w-]+)\s*(?:,([\s\S]+))?\)$/);
|
|
106
|
+
if (m) {
|
|
107
|
+
assert.ok(!seen.has(m[1]), `cyclic ${m[1]}`);
|
|
108
|
+
const next = RAW[mode][m[1]] ?? m[2];
|
|
109
|
+
assert.ok(next !== undefined, `${m[1]} is not defined in ${mode}`);
|
|
110
|
+
return resolve(next, mode, new Set([...seen, m[1]]));
|
|
111
|
+
}
|
|
112
|
+
m = e.match(/^color-mix\(\s*in\s+srgb\s*,\s*([\s\S]+?)\s+([\d.]+)%\s*,\s*([\s\S]+?)\s*\)$/);
|
|
113
|
+
if (m) {
|
|
114
|
+
const p = +m[2] / 100, a = resolve(m[1], mode, seen), b = resolve(m[3], mode, seen);
|
|
115
|
+
const alpha = a[3] * p + b[3] * (1 - p);
|
|
116
|
+
if (alpha === 0) return [0, 0, 0, 0];
|
|
117
|
+
return [0, 1, 2].map((i) => (a[i] * a[3] * p + b[i] * b[3] * (1 - p)) / alpha).concat(alpha);
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`cannot resolve ${e}`);
|
|
120
|
+
}
|
|
121
|
+
// The ratio is the KIT's own WCAG maths (modules/ui-design/kit/lib.js — what
|
|
122
|
+
// render.js and contrast.js measure with), so "the kit passes AA" is the kit's
|
|
123
|
+
// number, not a second implementation of it.
|
|
124
|
+
const require = createRequire(import.meta.url);
|
|
125
|
+
const kit = require('../modules/ui-design/kit/lib.js');
|
|
126
|
+
const contrast = (fgExpr, bgExpr, mode) => {
|
|
127
|
+
const ground = resolve(bgExpr, mode);
|
|
128
|
+
return kit.contrastRatio(kit.composite(resolve(fgExpr, mode), ground), ground);
|
|
129
|
+
};
|
|
130
|
+
// a fill painted on a ground (the ghost's transparent fill is the ground itself)
|
|
131
|
+
const onGround = (fill, ground, mode) => {
|
|
132
|
+
const f = resolve(fill, mode);
|
|
133
|
+
return f[3] === 0 ? ground : fill;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ── 1. the sheet is the hall's ───────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
test('style.css imports controls.css right after the shared token sheet', () => {
|
|
139
|
+
const imports = [...styleSrc.matchAll(/@import\s+url\("([^"]+)"\)/g)].map((m) => m[1]);
|
|
140
|
+
const at = imports.indexOf('/modules/ui-design/public/tokens.css');
|
|
141
|
+
assert.notEqual(at, -1, 'the shared token sheet is still imported');
|
|
142
|
+
assert.equal(imports[at + 1], 'controls.css', 'the shared button + field come straight after the scales');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── 2. the sizes ─────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
test('primary is the 46–48px pill; the row size and the text button keep the 24px floor', () => {
|
|
148
|
+
const base = sharedRule('.btn');
|
|
149
|
+
const h = px(prop(base, 'min-height'));
|
|
150
|
+
assert.ok(h >= 46 && h <= 48, `.btn is ${h}px — DESIGN.md's door is 46–48px`);
|
|
151
|
+
assert.equal(prop(base, 'border-radius'), 'var(--radius-pill)', 'pill or nothing (DESIGN.md, Shapes)');
|
|
152
|
+
assert.match(prop(base, 'border'), /transparent$/, 'the edge is reserved, so a modifier that sets only its colour draws');
|
|
153
|
+
for (const sel of ['.btn--sm', '.btn--text']) {
|
|
154
|
+
assert.ok(px(prop(sharedWith(sel, 'min-height'), 'min-height')) >= 24, `${sel} keeps 24px of height`);
|
|
155
|
+
assert.ok(px(prop(sharedWith(sel, 'min-width'), 'min-width')) >= 24, `${sel} keeps 24px of width`);
|
|
156
|
+
}
|
|
157
|
+
const field = sharedRule('select');
|
|
158
|
+
assert.ok(px(prop(field, 'min-height')) >= 24, 'the field clears the floor');
|
|
159
|
+
assert.equal(prop(field, 'border-radius'), 'var(--radius-pill)', 'a field is a pill too');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── 3. AA, measured, in both modes ───────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
for (const mode of ['light', 'dark']) {
|
|
165
|
+
test(`${mode}: every button and field pair clears AA`, () => {
|
|
166
|
+
const primary = sharedWith('.btn', 'background');
|
|
167
|
+
const hover = sharedRule('.btn:hover:not(:disabled)');
|
|
168
|
+
const ghost = sharedWith('.btn--ghost', 'color');
|
|
169
|
+
const text = sharedWith('.btn--text', 'color');
|
|
170
|
+
const field = sharedRule('select');
|
|
171
|
+
const fieldEdge = prop(field, 'border').replace(/^var\(--line\)\s+solid\s+/, '');
|
|
172
|
+
for (const ground of ['var(--bg)', 'var(--bg-card)']) {
|
|
173
|
+
const pairs = [
|
|
174
|
+
['primary ink on the accent', prop(primary, 'color'), prop(primary, 'background'), 4.5],
|
|
175
|
+
['primary ink on the hover lift', prop(hover, 'color'), prop(hover, 'background'), 4.5],
|
|
176
|
+
['ghost ink', prop(ghost, 'color'), onGround(prop(ghost, 'background'), ground, mode), 4.5],
|
|
177
|
+
['text-button ink', prop(text, 'color'), ground, 4.5],
|
|
178
|
+
['field text', prop(field, 'color'), onGround(prop(field, 'background'), ground, mode), 4.5],
|
|
179
|
+
['field edge (WCAG 1.4.11)', fieldEdge, ground, 3],
|
|
180
|
+
];
|
|
181
|
+
for (const [what, fg, bg, floor] of pairs) {
|
|
182
|
+
const r = contrast(fg, bg, mode);
|
|
183
|
+
assert.ok(r >= floor, `${mode} ${what} on ${ground}: ${r.toFixed(2)}:1 < ${floor}:1`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
test('no hall sheet puts white ink on an accent fill', () => {
|
|
190
|
+
const sheets = fs.readdirSync(HALL).filter((f) => f.endsWith('.css'));
|
|
191
|
+
const hits = [];
|
|
192
|
+
for (const f of sheets) {
|
|
193
|
+
for (const { sels, body } of rulesOf(read(f))) {
|
|
194
|
+
if (!/background(-color)?:\s*var\(--accent\)\s*(;|$)/.test(body)) continue;
|
|
195
|
+
if (/(^|;)\s*color:\s*(#fff\b|#ffffff\b|white\b)/i.test(body)) hits.push(`${f}: ${sels.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
assert.deepEqual(hits, [], 'white on the salmon fails AA and DESIGN.md bans it — --accent-on is the ink');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── 4. the migration ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
const PRIMARY = ['.btn', '.btn-accent', '.landing__btn', '.task__claim', '.setup-gate__save', '.td-btn--primary', '.pbtn--save', '.craft-save'];
|
|
204
|
+
const GHOST = ['.btn--ghost', '.btn-ghost', '.landing__btn--quiet', '.td-btn', '.wip__release', '.pbtn', '.settings-btn'];
|
|
205
|
+
const SMALL = ['.btn--sm', '.btn-ghost', '.task__claim', '.td-btn', '.wip__release'];
|
|
206
|
+
|
|
207
|
+
test('every older button name sits in the shared rule it means', () => {
|
|
208
|
+
const fill = (sel) => {
|
|
209
|
+
const r = rulesOf(controls).filter((x) => x.sels.includes(sel) && prop(x.body, 'background'));
|
|
210
|
+
return r.length ? prop(r[0].body, 'background') : null;
|
|
211
|
+
};
|
|
212
|
+
for (const sel of PRIMARY) assert.equal(fill(sel), 'var(--accent)', `${sel} is a primary door`);
|
|
213
|
+
for (const sel of GHOST) assert.equal(fill(sel), 'transparent', `${sel} is a ghost`);
|
|
214
|
+
for (const sel of SMALL) assert.equal(prop(sharedWith(sel, 'min-height'), 'min-height'), '30px', `${sel} is the row size`);
|
|
215
|
+
// and the primary-on-a-ghost-base names come AFTER the ghost rule, or the ghost wins on order
|
|
216
|
+
const list = rulesOf(controls);
|
|
217
|
+
const ghostAt = list.findIndex((x) => x.sels.includes('.td-btn') && prop(x.body, 'background') === 'transparent');
|
|
218
|
+
const primaryAt = list.findIndex((x) => x.sels.includes('.td-btn--primary') && prop(x.body, 'background'));
|
|
219
|
+
assert.ok(primaryAt > ghostAt, '.td-btn--primary follows .td-btn, so the shared order paints it');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('no page sheet re-draws a shared button or field', () => {
|
|
223
|
+
const NAMES = [...new Set([...PRIMARY, ...GHOST, '.btn--sm', '.btn--text', '.goal-claim-btn', '.goal-release-btn', '.setup__btn',
|
|
224
|
+
'.pinput', '.psel', '.cb-reply-input', '.curve__filter-select'])];
|
|
225
|
+
const DRAW = ['min-height', 'height', 'padding', 'font-size', 'background', 'border', 'border-radius', 'color'];
|
|
226
|
+
const hits = [];
|
|
227
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.css') && x !== 'controls.css')) {
|
|
228
|
+
for (const { sels, body } of rulesOf(read(f))) {
|
|
229
|
+
for (const s of sels) {
|
|
230
|
+
// the bare name, or the name on an element (button.craft-save) — a
|
|
231
|
+
// descendant or a state selector is the page placing it, not re-drawing it
|
|
232
|
+
const bare = s.replace(/^[a-z]+(?=\.)/, '');
|
|
233
|
+
if (!NAMES.includes(bare)) continue;
|
|
234
|
+
const drawn = DRAW.filter((p) => prop(body, p) !== null);
|
|
235
|
+
if (drawn.length) hits.push(`${f}: ${s} { ${drawn.join(', ')} }`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
assert.deepEqual(hits, [], 'a page sheet re-draws a shared control — move the difference into controls.css or drop it');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('the old field duplicates reduce to the shared field', () => {
|
|
243
|
+
const ov = rulesOf(read('oversight.css')).find((x) => x.sels.includes('.ov-field input'));
|
|
244
|
+
assert.ok(!ov || ['min-height', 'padding', 'border', 'border-radius', 'background'].every((p) => prop(ov.body, p) === null),
|
|
245
|
+
'oversight.css re-states the field the shared sheet already draws');
|
|
246
|
+
const uat = rulesOf(read('goals.css')).find((x) => x.sels.includes('.uat-field textarea'));
|
|
247
|
+
assert.ok(!uat || ['padding', 'border', 'border-radius', 'background'].every((p) => prop(uat.body, p) === null),
|
|
248
|
+
'goals.css re-draws the UAT textarea');
|
|
249
|
+
for (const sel of ['.filter select', '.filter input[type="search"]', '.td-field input', '.cb-reply-input', '.psel', '.pinput']) {
|
|
250
|
+
for (const f of ['style.css', 'collab.css', 'panel.css']) {
|
|
251
|
+
const r = rulesOf(read(f)).find((x) => x.sels.includes(sel));
|
|
252
|
+
if (!r) continue;
|
|
253
|
+
const drawn = ['min-height', 'padding', 'font-size', 'border', 'border-radius', 'background'].filter((p) => prop(r.body, p) !== null);
|
|
254
|
+
assert.deepEqual(drawn, [], `${f}: ${sel} re-draws the shared field`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('the pages the task named write names the shared rules own', () => {
|
|
260
|
+
const ALL = new Set([...PRIMARY, ...GHOST, ...SMALL].map((s) => s.slice(1)));
|
|
261
|
+
const cases = [
|
|
262
|
+
['goals-render.js', /<button class="(task__claim)" type="button" data-claim-task/, 'the Goals claim'],
|
|
263
|
+
['goals-render.js', /<button class="(wip__release)" type="button" data-release-claim/, 'the Goals release'],
|
|
264
|
+
['setup-gate.js', /<button type="button" class="(setup-gate__save)" data-save-crafts/, 'the setup screen\'s save'],
|
|
265
|
+
['work.js', /class="(task__claim)"/, 'the Work claim'],
|
|
266
|
+
['settings.html', /class="(?:[^"]*\s)?(pbtn|settings-btn|btn)(?:\s[^"]*)?"/, 'a Settings button'],
|
|
267
|
+
];
|
|
268
|
+
for (const [file, re, what] of cases) {
|
|
269
|
+
const m = read(file).match(re);
|
|
270
|
+
assert.ok(m, `${file}: ${what} is still where this test looks`);
|
|
271
|
+
assert.ok(ALL.has(m[1]), `${file}: ${what} writes .${m[1]}, which the shared rules own`);
|
|
272
|
+
}
|
|
273
|
+
assert.doesNotMatch(styleSrc, /\.setup__btn\b/, 'the dead white-on-salmon setup button stays gone');
|
|
274
|
+
});
|