@bongos/core 1.24.55 → 1.24.57
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 +67 -37
- package/docs/copy-inventory.md +16 -16
- package/docs/copy-registry.json +17 -17
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +18 -18
- package/modules/hall-ui/public/approval-queue.css +9 -9
- package/modules/hall-ui/public/atlas.css +5 -1
- package/modules/hall-ui/public/atlas.js +14 -11
- package/modules/hall-ui/public/city-draw.js +14 -14
- package/modules/hall-ui/public/sky-draw.js +24 -6
- package/modules/hall-ui/public/sky-interaction.js +1 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/studio.css +22 -22
- package/modules/hall-ui/public/thinking.css +46 -48
- package/modules/hall-ui/public/tweak-editor.css +18 -18
- package/modules/hall-ui/public/welcome-boot.css +5 -8
- package/modules/hall-ui/public/welcome-boot.js +11 -15
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/css-literal-scan.js +144 -0
- package/scripts/bongos/design-generators-fresh.js +32 -0
- package/scripts/bongos/fitness-ratchets.js +70 -3
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/gds/css-literal-scan.js +7 -0
- package/scripts/gds/design-generators-fresh.js +6 -0
- package/src/bongos/module-scope-map.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/design_counts_ratchet.mjs +134 -0
- package/tests/hall_catalog_world.mjs +7 -2
- package/tests/hall_city.mjs +5 -4
- package/tests/hall_private_palettes.mjs +148 -0
- package/tests/hall_sky.mjs +29 -32
- package/tests/hall_street.mjs +3 -2
- package/tests/helpers.mjs +25 -1
- package/tests/welcome_boot.mjs +1 -1
- package/tests/welcome_boot_browser.mjs +1 -1
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
// docs-entropy scanner's three counts, reused as a library
|
|
28
28
|
// source_text_only_tests — test files that only read source text and match
|
|
29
29
|
// its wording, never running the code (task 1004868)
|
|
30
|
+
// design counts — PER STYLESHEET, the typed-in colours, font sizes, radii,
|
|
31
|
+
// shadows and breakpoints (task 1005093, goal 1000138; detector:
|
|
32
|
+
// css-literal-scan.js). Kept in their own `design_counts` object rather than
|
|
33
|
+
// `metrics` — ~60 sheets × 5 kinds would drown the metric line — but with the
|
|
34
|
+
// same verdict: higher than baseline fails, lower nudges a ratchet-down. A
|
|
35
|
+
// sheet with no baseline row has a baseline of zero, so a NEW sheet starts on
|
|
36
|
+
// the --ds-* tokens.
|
|
30
37
|
//
|
|
31
38
|
// The knip dead-code ratchet needs node_modules + ~seconds, so it is NOT part
|
|
32
39
|
// of the in-suite check: CI runs `node scripts/bongos/fitness-ratchets.js --knip`
|
|
@@ -535,6 +542,46 @@ function skillListingMetrics() {
|
|
|
535
542
|
}
|
|
536
543
|
}
|
|
537
544
|
|
|
545
|
+
// ---------- design counts (task 1005093) ----------
|
|
546
|
+
|
|
547
|
+
const DESIGN_HINT = ' — use a --ds-* scale token (modules/ui-design/public/tokens.css) or one of the fifteen colour '
|
|
548
|
+
+ 'tokens instead of a typed-in value (DESIGN.md, the Fifteen Rule). Run '
|
|
549
|
+
+ '`node scripts/bongos/css-literal-scan.js` to see every sheet\'s counts and the rule.';
|
|
550
|
+
|
|
551
|
+
// Current per-sheet counts, or null when the scan found no stylesheet at all —
|
|
552
|
+
// a collapsed enumeration is a scan defect, never a flattering all-zero tree.
|
|
553
|
+
function designCounts() {
|
|
554
|
+
const { scanCssLiterals } = require('./css-literal-scan.js');
|
|
555
|
+
const { sheets, scanned } = scanCssLiterals();
|
|
556
|
+
if (!scanned) {
|
|
557
|
+
process.stderr.write('fitness-ratchets: design counts NOT MEASURED — the stylesheet scan enumerated ZERO files, which is a scan defect, not a clean tree\n');
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
return sheets;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Pure verdict over { sheet: { kind: n } } maps. Keys starting with `_` are notes.
|
|
564
|
+
function compareDesignCounts(current, baseline) {
|
|
565
|
+
const { KINDS } = require('./css-literal-scan.js');
|
|
566
|
+
const violations = [];
|
|
567
|
+
const warnings = [];
|
|
568
|
+
const sheets = new Set([...Object.keys(current || {}), ...Object.keys(baseline || {})].filter((k) => !k.startsWith('_')));
|
|
569
|
+
for (const sheet of [...sheets].sort()) {
|
|
570
|
+
const cur = (current || {})[sheet] || {};
|
|
571
|
+
const base = (baseline || {})[sheet] || {};
|
|
572
|
+
for (const kind of KINDS) {
|
|
573
|
+
const value = cur[kind] || 0;
|
|
574
|
+
const floor = base[kind] || 0;
|
|
575
|
+
if (value > floor) {
|
|
576
|
+
violations.push(`design count ${kind} in ${sheet} regressed: ${value} > baseline ${floor} — fix the regression; never raise the baseline to pass${DESIGN_HINT}`);
|
|
577
|
+
} else if (value < floor) {
|
|
578
|
+
warnings.push(`ratchet-down available: design count ${kind} in ${sheet} improved to ${value} (baseline ${floor}) — lower it in config/fitness-baselines.json design_counts`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { violations, warnings };
|
|
583
|
+
}
|
|
584
|
+
|
|
538
585
|
function computeMetrics({ withDocs = true } = {}) {
|
|
539
586
|
const files = trackedJsFiles();
|
|
540
587
|
const contents = new Map(files.map((f) => [f, readRepoFile(f)]));
|
|
@@ -560,15 +607,28 @@ function computeMetrics({ withDocs = true } = {}) {
|
|
|
560
607
|
// computeMetrics() sweep — git ls-files plus every tracked .js plus the
|
|
561
608
|
// docs-entropy scan, measured at 23.7s + 2.5s + 2.8s, i.e. ~29s of this file's
|
|
562
609
|
// 29s runtime, which is what pushed it into the 90s timeout in the first place.
|
|
563
|
-
|
|
610
|
+
// `designCounts` (task 1005093) is injectable the same way; when a test injects
|
|
611
|
+
// `metrics` alone, the stylesheet scan is skipped rather than run for real.
|
|
612
|
+
function checkQualityRatchets({ baselinePath = BASELINE_PATH, metrics: injectedMetrics = null, designCounts: injectedDesign = null } = {}) {
|
|
564
613
|
const metrics = injectedMetrics === null ? computeMetrics() : injectedMetrics;
|
|
565
|
-
const { metrics: baselines, error: baselineError } = readBaselines(baselinePath);
|
|
614
|
+
const { metrics: baselines, error: baselineError, parsed } = readBaselines(baselinePath);
|
|
566
615
|
const { violations, warnings } = compareToBaselines(metrics, baselines);
|
|
616
|
+
const design = injectedDesign !== null ? injectedDesign : (injectedMetrics === null ? designCounts() : null);
|
|
617
|
+
if (design) {
|
|
618
|
+
const designBaseline = parsed && parsed.design_counts;
|
|
619
|
+
if (!baselineError && (!designBaseline || typeof designBaseline !== 'object')) {
|
|
620
|
+
violations.push('config/fitness-baselines.json has no "design_counts" object — the per-stylesheet design ratchet cannot verify anything, so it fails CLOSED. Bootstrap deliberately: node scripts/bongos/fitness-ratchets.js --update-baselines');
|
|
621
|
+
} else if (designBaseline) {
|
|
622
|
+
const d = compareDesignCounts(design, designBaseline);
|
|
623
|
+
violations.push(...d.violations);
|
|
624
|
+
warnings.push(...d.warnings);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
567
627
|
// task 1003227: an unreadable baseline file is a hard failure, not silence. Put it
|
|
568
628
|
// FIRST so it is the headline rather than buried under per-metric noise.
|
|
569
629
|
if (baselineError) violations.unshift(baselineError);
|
|
570
630
|
return {
|
|
571
|
-
name: 'quality ratchets — size, duplication, cycles, console, doc entropy, legacy GDS literals, source-text-only tests only tighten (task 1003129)',
|
|
631
|
+
name: 'quality ratchets — size, duplication, cycles, console, doc entropy, legacy GDS literals, source-text-only tests, per-stylesheet design counts only tighten (task 1003129)',
|
|
572
632
|
hardFail: violations.length > 0,
|
|
573
633
|
violations,
|
|
574
634
|
warnings,
|
|
@@ -598,6 +658,12 @@ function main() {
|
|
|
598
658
|
// this file cannot be eaten by a refresh either.
|
|
599
659
|
const preserved = existingFile && typeof existingFile === 'object' ? { ...existingFile } : {};
|
|
600
660
|
delete preserved.metrics;
|
|
661
|
+
// design_counts is recomputed too (task 1005093), keeping its `_` notes.
|
|
662
|
+
const design = designCounts();
|
|
663
|
+
if (design) {
|
|
664
|
+
const notes = Object.fromEntries(Object.entries(preserved.design_counts || {}).filter(([k]) => k.startsWith('_')));
|
|
665
|
+
preserved.design_counts = { ...notes, ...design };
|
|
666
|
+
}
|
|
601
667
|
if (!preserved._comment) {
|
|
602
668
|
preserved._comment = 'fitness-ratchets baselines (task 1003129): quality debt recorded at gate-adoption. Shrink-only — lower a value when the metric improves; raising one to pass CI is out of policy.';
|
|
603
669
|
}
|
|
@@ -647,6 +713,7 @@ module.exports = {
|
|
|
647
713
|
readBaselines,
|
|
648
714
|
computeMetrics,
|
|
649
715
|
compareToBaselines,
|
|
716
|
+
compareDesignCounts,
|
|
650
717
|
sourceTextTestMetrics,
|
|
651
718
|
duplicateWindowCount,
|
|
652
719
|
normalizeLines,
|
|
@@ -200,7 +200,7 @@ function checkNestedDocsPresent() {
|
|
|
200
200
|
// here would false-fail every PR (the gitignored whole file is absent from a clean checkout, so it
|
|
201
201
|
// always reads as "would change"); `--check-block` is the exact, non-noisy gate. (docs/file-map.md +
|
|
202
202
|
// the OpenAPI docs are still fully committed and unchanged by ADR 0110 — they keep the plain --check.)
|
|
203
|
-
function checkGeneratedArtifactsFresh() {
|
|
203
|
+
function checkGeneratedArtifactsFresh({ designRoot = REPO_ROOT } = {}) { // designRoot: tests only (task 1005093)
|
|
204
204
|
const violations = [];
|
|
205
205
|
try {
|
|
206
206
|
execFileSync(process.execPath, [path.join(__dirname, 'gen-repo-map.js'), '--check-block'], { cwd: REPO_ROOT, stdio: 'pipe' });
|
|
@@ -254,9 +254,10 @@ function checkGeneratedArtifactsFresh() {
|
|
|
254
254
|
} catch {
|
|
255
255
|
violations.push('docs/page-inventory.json + docs/page-inventory.md are STALE (or two pages claim one id) — a page was added, renamed, retitled or changed its scripts/states. Run `node scripts/bongos/page-inventory.js` and commit (task 1004314).');
|
|
256
256
|
}
|
|
257
|
+
violations.push(...require('./design-generators-fresh.js').checkDesignGeneratorsFresh(designRoot)); // task 1005093
|
|
257
258
|
// NB the role packs' responsibility blocks are generated too, but their freshness
|
|
258
259
|
// is asserted by role-pack-guard.js — one check owns "the packs are correct".
|
|
259
|
-
return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus the CLAUDE.md §2 interaction-posture block (the knob defaults), docs/file-map.md skills/scheduled-tasks sections, the API spec + typed client GENERATE from the routes (both gitignored, task 1004866), docs/copy-registry.json + copy-inventory.md (the reader-facing copy, task 1003556) and docs/page-inventory.json + page-inventory.md (every page and its stable id, task 1004314). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
|
|
260
|
+
return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus the CLAUDE.md §2 interaction-posture block (the knob defaults), docs/file-map.md skills/scheduled-tasks sections, the API spec + typed client GENERATE from the routes (both gitignored, task 1004866), docs/copy-registry.json + copy-inventory.md (the reader-facing copy, task 1003556) and docs/page-inventory.json + page-inventory.md (every page and its stable id, task 1004314), and the design system config/design-tokens.neutral.json + public /brand reference (task 1005093). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
// ---- deterministic cron routine boundary (System 4 / ADR 0078) -------------
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/css-literal-scan.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/css-literal-scan.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'css-literal-scan.js');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/gds/design-generators-fresh.js — @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
// The real script is scripts/bongos/design-generators-fresh.js. This forwarder keeps callers that still name
|
|
4
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
'use strict';
|
|
6
|
+
require('../bongos/forwarder.js').forward(module, 'design-generators-fresh.js');
|
|
@@ -473,7 +473,7 @@ const MODULE_GLOBS = {
|
|
|
473
473
|
'scripts/bongos/adr-namespace.js', 'scripts/bongos/baseline-staleness.js',
|
|
474
474
|
'scripts/bongos/category-advisory-guard.js', 'scripts/bongos/client-baseurl-guard.js',
|
|
475
475
|
'scripts/bongos/doc-cli-guard.js', 'scripts/bongos/doc-control-claims.js', 'scripts/bongos/doc-comment-coverage.js',
|
|
476
|
-
'scripts/bongos/exec-path-guard.js', 'scripts/bongos/fitness-lib.js',
|
|
476
|
+
'scripts/bongos/exec-path-guard.js', 'scripts/bongos/fitness-lib.js', 'scripts/bongos/design-generators-fresh.js',
|
|
477
477
|
'scripts/bongos/fitness-checks-error-envelope.js', 'scripts/bongos/fitness-checks-identity.js',
|
|
478
478
|
'scripts/bongos/fitness-checks-packaging.js', 'scripts/bongos/fitness-checks-route-pins.js',
|
|
479
479
|
'scripts/bongos/fitness-checks-vocabulary.js', 'scripts/bongos/fitness-checks-write-validation.js',
|
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.57'; // 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,134 @@
|
|
|
1
|
+
// tests/design_counts_ratchet.mjs — design counts that can only go down, and the
|
|
2
|
+
// design generators wired into the fitness freshness gate (task 1005093, goal 1000138).
|
|
3
|
+
//
|
|
4
|
+
// Done when: adding a new hard-coded font-size to a hall sheet fails fitness, and
|
|
5
|
+
// editing the shared token sheet without regenerating fails the freshness gate.
|
|
6
|
+
// Both are proved here on the real tree: the first by scanning the real sheets with
|
|
7
|
+
// one extra declaration, the second by running the gate on a copied tree.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/design_counts_ratchet.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const scan = require(path.join(ROOT, 'scripts', 'bongos', 'css-literal-scan.js'));
|
|
22
|
+
const ratchets = require(path.join(ROOT, 'scripts', 'bongos', 'fitness-ratchets.js'));
|
|
23
|
+
const fitness = require(path.join(ROOT, 'scripts', 'bongos', 'fitness.js'));
|
|
24
|
+
const { checkDesignGeneratorsFresh } = require(path.join(ROOT, 'scripts', 'bongos', 'design-generators-fresh.js'));
|
|
25
|
+
|
|
26
|
+
const HALL_SHEET = 'modules/hall-ui/public/style.css';
|
|
27
|
+
const readReal = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
|
|
28
|
+
|
|
29
|
+
test('the detector counts typed-in values and leaves tokens alone', () => {
|
|
30
|
+
const c = scan.countSheet(`
|
|
31
|
+
:root { --ds-x: 14px; --tone: #123456; } /* token definitions: not counted */
|
|
32
|
+
.a { font-size: 13px; border-radius: 6px; color: #abc; }
|
|
33
|
+
.b { font-size: var(--ds-text-sm); border-radius: var(--ds-radius-tile); color: var(--ink); }
|
|
34
|
+
.c { box-shadow: 0 2px 4px rgba(0, 0, 0, .2); } /* a shadow; the black scrim is allowed */
|
|
35
|
+
.d { box-shadow: none; background: url("data:image/svg+xml;fill='#fff'"); }
|
|
36
|
+
.e { background: color-mix(in srgb, var(--ink) 40%, rgb(12 34 56)); padding: 0; }
|
|
37
|
+
@media (max-width: 720px) { .f { border-radius: 0; } }
|
|
38
|
+
@media (prefers-reduced-motion: reduce) { .g { transition: none; } }
|
|
39
|
+
`);
|
|
40
|
+
assert.deepEqual(c, { colours: 2, font_sizes: 1, radii: 1, shadows: 1, breakpoints: 1 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a selector with a colon is not read as a declaration', () => {
|
|
44
|
+
assert.equal(scan.countSheet('a:hover, b:focus-visible { color: var(--ink); }').colours, 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the real tree matches its committed design-count baselines', () => {
|
|
48
|
+
const { sheets, scanned } = scan.scanCssLiterals();
|
|
49
|
+
assert.ok(scanned > 40, `the scan should see the live stylesheets (saw ${scanned})`);
|
|
50
|
+
const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: sheets });
|
|
51
|
+
assert.deepEqual(r.violations, [], 'design counts regressed against config/fitness-baselines.json');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a NEW hard-coded font-size in a hall sheet fails fitness', () => {
|
|
55
|
+
const { sheets } = scan.scanCssLiterals({
|
|
56
|
+
read: (f) => readReal(f) + (f === HALL_SHEET ? '\n.planted { font-size: 13px; }\n' : ''),
|
|
57
|
+
});
|
|
58
|
+
const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: sheets });
|
|
59
|
+
assert.equal(r.hardFail, true);
|
|
60
|
+
assert.ok(r.violations.some((v) => v.includes(`font_sizes in ${HALL_SHEET} regressed`)), r.violations.join('\n'));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a new sheet starts at zero: any typed-in colour in it fails', () => {
|
|
64
|
+
const d = ratchets.compareDesignCounts({ 'modules/x/public/new.css': { colours: 1 } }, { _comment: 'note' });
|
|
65
|
+
assert.equal(d.violations.length, 1);
|
|
66
|
+
assert.match(d.violations[0], /colours in modules\/x\/public\/new\.css regressed: 1 > baseline 0/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('a lower count nudges a ratchet-down and does not fail', () => {
|
|
70
|
+
const d = ratchets.compareDesignCounts({ 'a.css': { radii: 2 } }, { 'a.css': { radii: 3 } });
|
|
71
|
+
assert.deepEqual(d.violations, []);
|
|
72
|
+
assert.match(d.warnings[0], /ratchet-down available: design count radii in a\.css improved to 2/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a baseline file with no design_counts fails closed', () => {
|
|
76
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-counts-'));
|
|
77
|
+
const file = path.join(dir, 'baselines.json');
|
|
78
|
+
fs.writeFileSync(file, JSON.stringify({ metrics: {} }));
|
|
79
|
+
try {
|
|
80
|
+
const r = ratchets.checkQualityRatchets({ baselinePath: file, metrics: {}, designCounts: {} });
|
|
81
|
+
assert.equal(r.hardFail, true);
|
|
82
|
+
assert.match(r.violations[0], /no "design_counts" object/);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The generators resolve their sources from their own location, so a copy of the
|
|
89
|
+
// files they read is a complete, isolated tree the gate can run against.
|
|
90
|
+
const GENERATOR_SOURCES = [
|
|
91
|
+
'config/branding.neutral.json',
|
|
92
|
+
'config/design-tokens.neutral.json',
|
|
93
|
+
'modules/ui-design/public/tokens.css',
|
|
94
|
+
'modules/ui-design/public/brand.html',
|
|
95
|
+
'modules/ui-design/public/brand',
|
|
96
|
+
'modules/ui-design/scripts/gen-design-tokens.js',
|
|
97
|
+
'modules/ui-design/scripts/gen-design-reference.js',
|
|
98
|
+
'modules/hall-ui/public/style.css',
|
|
99
|
+
'modules/hall-ui/public/controls.css',
|
|
100
|
+
'modules/hall-ui/public/dom-utils.js',
|
|
101
|
+
'scripts/bongos/gen-brand.js',
|
|
102
|
+
'DESIGN.md',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function copiedTree() {
|
|
106
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-fresh-'));
|
|
107
|
+
for (const rel of GENERATOR_SOURCES) {
|
|
108
|
+
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
|
|
109
|
+
fs.cpSync(path.join(ROOT, rel), path.join(dir, rel), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test('the freshness gate passes on a fresh tree and fails when the token sheet changes without a regeneration', () => {
|
|
115
|
+
const dir = copiedTree();
|
|
116
|
+
try {
|
|
117
|
+
assert.deepEqual(checkDesignGeneratorsFresh(dir), [], 'the copied tree should start fresh');
|
|
118
|
+
const sheet = path.join(dir, 'modules/ui-design/public/tokens.css');
|
|
119
|
+
const before = fs.readFileSync(sheet, 'utf8');
|
|
120
|
+
const after = before.replace(/--ds-space-xs:\s*[^;]+;/, '--ds-space-xs: 11px;');
|
|
121
|
+
assert.notEqual(after, before, 'the token sheet should declare --ds-space-xs');
|
|
122
|
+
fs.writeFileSync(sheet, after);
|
|
123
|
+
const v = checkDesignGeneratorsFresh(dir);
|
|
124
|
+
assert.ok(v.some((x) => x.startsWith('config/design-tokens.neutral.json is STALE')), v.join('\n'));
|
|
125
|
+
assert.ok(v.some((x) => x.startsWith('modules/ui-design/public/brand.html')), v.join('\n'));
|
|
126
|
+
// …and the registered fitness check carries that verdict, so the build goes red.
|
|
127
|
+
assert.ok(fitness.CHECKS.includes(fitness.checkGeneratedArtifactsFresh));
|
|
128
|
+
const gate = fitness.checkGeneratedArtifactsFresh({ designRoot: dir });
|
|
129
|
+
assert.equal(gate.hardFail, true);
|
|
130
|
+
assert.ok(gate.violations.some((x) => x.startsWith('config/design-tokens.neutral.json is STALE')), gate.violations.join('\n'));
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
@@ -168,10 +168,15 @@ test('the shell no longer aliases .mod-chip onto the pill', () => {
|
|
|
168
168
|
|
|
169
169
|
test('atlas: the nine semantic colours stay fixed literals, and nothing else in the sheet is one', () => {
|
|
170
170
|
const css = read('atlas.css');
|
|
171
|
-
for (const t of ['--ab-sun', '--ab-sky', '--ab-coral', '--ab-paper', '--ab-
|
|
171
|
+
for (const t of ['--ab-sun', '--ab-sky', '--ab-coral', '--ab-paper', '--ab-ghost']) {
|
|
172
172
|
assert.match(css, new RegExp(`${t}:#[0-9a-f]{6}`), `${t} is a fixed literal (ADR 0094)`);
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
// task 1005087: the four STATE colours are the hall's status vocabulary — fixed per instance
|
|
175
|
+
// (never the pack's), so ADR 0094's no-scramble invariant still holds, and no longer Tailwind's.
|
|
176
|
+
for (const [t, tok] of [['--ab-good', '--ok'], ['--ab-core', '--warn'], ['--ab-db', '--danger'], ['--ab-danger', '--danger']]) {
|
|
177
|
+
assert.match(css, new RegExp(`${t}:var\\(${tok}\\)`), `${t} is the hall's ${tok}`);
|
|
178
|
+
}
|
|
179
|
+
const rest = decomment(css).replace(/--ab-(sun|sky|coral|paper|ghost):#[0-9a-f]{6}/g, '');
|
|
175
180
|
assert.deepEqual(rest.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'no other literal colour');
|
|
176
181
|
noAlias(css, 'atlas.css');
|
|
177
182
|
tokensDefined(css, 'atlas.css');
|
package/tests/hall_city.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import path from 'node:path';
|
|
|
25
25
|
import vm from 'node:vm';
|
|
26
26
|
import { createRequire } from 'node:module';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { cityTokens } from './helpers.mjs';
|
|
28
29
|
|
|
29
30
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
31
|
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
@@ -66,13 +67,13 @@ test('the nine civic recipes draw exactly the prototype paths, in both modes', (
|
|
|
66
67
|
const a = []; const b = [];
|
|
67
68
|
p.isoInstitution(a, pk, kind, x, y, { fixes: false });
|
|
68
69
|
City.isoInstitution(b, ck, kind, x, y, { fixes: false });
|
|
69
|
-
assert.deepEqual(b, a, `${mode} ${kind}`);
|
|
70
|
+
assert.deepEqual(b, cityTokens(a), `${mode} ${kind}`);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
});
|
|
73
74
|
|
|
74
75
|
test('the iso sub-palette is the prototype theme().iso, both modes', () => {
|
|
75
|
-
for (const mode of ['dark', 'light']) assert.deepEqual(City.isoTheme(mode), new Proto({ mode }).theme(mode).iso, mode);
|
|
76
|
+
for (const mode of ['dark', 'light']) assert.deepEqual(City.isoTheme(mode), cityTokens(new Proto({ mode }).theme(mode).iso), mode);
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
test('every work kind the prototype draws is drawn the same way', () => {
|
|
@@ -83,7 +84,7 @@ test('every work kind the prototype draws is drawn the same way', () => {
|
|
|
83
84
|
const a = []; const b = [];
|
|
84
85
|
p.isoWork(a, pk, 6, 2, kind, 58);
|
|
85
86
|
City.isoWork(b, ck, 6, 2, kind, 58);
|
|
86
|
-
assert.deepEqual(b, a, kind);
|
|
87
|
+
assert.deepEqual(b, cityTokens(a), kind);
|
|
87
88
|
}
|
|
88
89
|
});
|
|
89
90
|
|
|
@@ -95,7 +96,7 @@ test('the ground, ring road, lanes and plaza are the prototype home view, path f
|
|
|
95
96
|
const home = p.homeView(T, p.matters(), false);
|
|
96
97
|
const model = City.cityModel({ docket: docket([matter()]), goals: [], rollups: [], mode: 'light' });
|
|
97
98
|
// The prototype's flat list is ground, 4 roads, lanes, plaza, then its 4 plots and their grid.
|
|
98
|
-
assert.deepEqual(model.flat.slice(0, 7), home.flat.slice(0, 7));
|
|
99
|
+
assert.deepEqual(model.flat.slice(0, 7), cityTokens(home.flat.slice(0, 7)));
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
test('with four goals the districts sit on the prototype\'s four plots, in its order', () => {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// tests/hall_private_palettes.mjs — the hall's private-palette rooms mix the shared tokens only
|
|
2
|
+
// (task 1005087, goal 1000138 "One Cloud Bongos design system").
|
|
3
|
+
//
|
|
4
|
+
// The UI audit found Studio, Thinking (the sky), the Tweak editor, City, Approvals, Atlas and the
|
|
5
|
+
// welcome screen each re-typing the salmon and the ink, their own font stacks, and off-the-shelf
|
|
6
|
+
// green / amber / red — so a re-branded instance saw those rooms in Cloud Bongos's colours. This
|
|
7
|
+
// scans every sheet and script of those rooms (comments stripped) and fails on:
|
|
8
|
+
// * a hex, an rgb()/rgba() or a bare [r, g, b] triple equal to one of the pack's colours, the
|
|
9
|
+
// hall's dark-tier inks, the accent's light tint, or an off-the-shelf status green/amber/red;
|
|
10
|
+
// * a font family typed in by name instead of the pack's stacks;
|
|
11
|
+
// * an overshooting (bounce) easing;
|
|
12
|
+
// * the welcome screen's beacon or star twinkle coming back.
|
|
13
|
+
// Black and white (with or without alpha) stay legal: they are scrims, halos and the glass rooms'
|
|
14
|
+
// inks. Each guard was watched go red on the pre-task sheets.
|
|
15
|
+
import { test, after } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const kit = require('../modules/ui-design/kit/lib.js');
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
27
|
+
const ROOMS = /^(studio|thinking|sky-|tweak-editor|city|approval-queue|atlas|welcome-boot)/;
|
|
28
|
+
const FILES = fs.readdirSync(PUB).filter((f) => ROOMS.test(f) && /\.(css|js|html)$/.test(f));
|
|
29
|
+
|
|
30
|
+
const strip = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1');
|
|
31
|
+
|
|
32
|
+
const PACK = Object.values(JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8')).theme.ui.colors);
|
|
33
|
+
const BRAND = new Set([...PACK,
|
|
34
|
+
// the hall's dark-tier inks and grounds, and the accent's light tint the glass rooms used for links
|
|
35
|
+
'#101012', '#08080a', '#c0c0c0', '#a8a8a8', '#fcbfa4', '#96410f',
|
|
36
|
+
// off-the-shelf status green / amber / red (Tailwind's, and the rooms' own copies of them)
|
|
37
|
+
'#10b981', '#22c55e', '#4ade80', '#16a34a', '#15803d', '#166534', '#1f7a44',
|
|
38
|
+
'#f59e0b', '#fbbf24', '#facc15', '#d97706', '#b45309', '#92400e', '#8f6a0c',
|
|
39
|
+
'#ef4444', '#dc2626', '#b91c1c', '#991b1b', '#f87171', '#bb1f1f',
|
|
40
|
+
].map((h) => h.toLowerCase()));
|
|
41
|
+
const tripleOf = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16)).join(',');
|
|
42
|
+
const TRIPLES = new Set([...BRAND].map(tripleOf));
|
|
43
|
+
|
|
44
|
+
function brandLiterals(src) {
|
|
45
|
+
const s = strip(src);
|
|
46
|
+
const hits = [];
|
|
47
|
+
for (const m of s.matchAll(/#[0-9a-f]{6}\b/gi)) if (BRAND.has(m[0].toLowerCase())) hits.push(m[0]);
|
|
48
|
+
for (const m of s.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/gi)) if (TRIPLES.has(`${+m[1]},${+m[2]},${+m[3]}`)) hits.push(m[0]);
|
|
49
|
+
for (const m of s.matchAll(/\[\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\]/g)) if (TRIPLES.has(`${+m[1]},${+m[2]},${+m[3]}`)) hits.push(m[0]);
|
|
50
|
+
for (const m of s.matchAll(/--[a-z-]+-rgb\s*:\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/gi)) if (TRIPLES.has(`${+m[1]},${+m[2]},${+m[3]}`)) hits.push(m[0]);
|
|
51
|
+
return hits;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('the rooms are all here (the scan is not vacuous)', () => {
|
|
55
|
+
for (const f of ['studio.css', 'thinking.css', 'sky-draw.js', 'tweak-editor.css', 'city.css', 'city-draw.js', 'approval-queue.css', 'atlas.css', 'atlas.js', 'welcome-boot.css', 'welcome-boot.js']) {
|
|
56
|
+
assert.ok(FILES.includes(f), `${f} is scanned`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('no hex, rgb or [r, g, b] literal for a brand colour or an off-the-shelf status colour remains', () => {
|
|
61
|
+
const found = {};
|
|
62
|
+
for (const f of FILES) { const h = brandLiterals(fs.readFileSync(path.join(PUB, f), 'utf8')); if (h.length) found[f] = [...new Set(h)]; }
|
|
63
|
+
assert.deepEqual(found, {}, 'mix the shared tokens (the pack\'s, the status vocabulary) instead');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the scan catches each shape of brand literal, and spares black, white and comments', () => {
|
|
67
|
+
assert.deepEqual(brandLiterals('a { color: #FB9569; }'), ['#FB9569']);
|
|
68
|
+
assert.deepEqual(brandLiterals('a { color: rgba(23, 24, 26, .4); }'), ['rgba(23, 24, 26']);
|
|
69
|
+
assert.deepEqual(brandLiterals('const ACC = [251, 149, 105];'), ['[251, 149, 105]']);
|
|
70
|
+
assert.deepEqual(brandLiterals('--sky-accent-rgb: 251,149,105;'), ['--sky-accent-rgb: 251,149,105']);
|
|
71
|
+
assert.deepEqual(brandLiterals("dot: '#f59e0b'"), ['#f59e0b']);
|
|
72
|
+
assert.deepEqual(brandLiterals('/* was #fb9569 */ a { color: rgba(0, 0, 0, .5); background: #ffffff; }'), []);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('no room types a font family by name: the pack\'s stacks only', () => {
|
|
76
|
+
const hits = [];
|
|
77
|
+
for (const f of FILES.filter((n) => /\.(css|js)$/.test(n))) {
|
|
78
|
+
const s = strip(fs.readFileSync(path.join(PUB, f), 'utf8'));
|
|
79
|
+
if (/Manrope|JetBrains Mono|Segoe UI|Helvetica|Arial\b/.test(s)) hits.push(f);
|
|
80
|
+
}
|
|
81
|
+
assert.deepEqual(hits, [], 'use var(--font-body) / var(--font-display) / var(--font-mono)');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('no bounce: no easing in the rooms overshoots (use --ds-ease / --ds-ease-arrive)', () => {
|
|
85
|
+
const hits = [];
|
|
86
|
+
for (const f of FILES) {
|
|
87
|
+
const s = strip(fs.readFileSync(path.join(PUB, f), 'utf8'));
|
|
88
|
+
for (const m of s.matchAll(/cubic-bezier\(\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\s*\)/g)) {
|
|
89
|
+
if (+m[2] > 1 || +m[4] > 1 || +m[2] < 0 || +m[4] < 0) hits.push(`${f}: ${m[0]}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
assert.deepEqual(hits, []);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('the welcome screen keeps no beacon and no twinkle', () => {
|
|
96
|
+
const css = strip(fs.readFileSync(path.join(PUB, 'welcome-boot.css'), 'utf8'));
|
|
97
|
+
const js = strip(fs.readFileSync(path.join(PUB, 'welcome-boot.js'), 'utf8'));
|
|
98
|
+
assert.doesNotMatch(css, /wb-beacon|wb-twinkle/, 'no beacon blink, no star twinkle in the sheet');
|
|
99
|
+
assert.doesNotMatch(js, /wb-beacon/, 'the script draws no beacon');
|
|
100
|
+
assert.doesNotMatch(js, /\.wb-star[^]*animation-duration/, 'no star carries a twinkle timing');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── the done-when, in a browser: a re-branded look renders the rooms in its own colours ──
|
|
104
|
+
// The ui-design kit's stub serves the hall under a look from the style library (grove: a
|
|
105
|
+
// sand accent), and every computed colour on each room is checked for the neutral pack's
|
|
106
|
+
// salmon. Skips when the machine has no system Edge or Chrome, as ui_design_kit_fit does.
|
|
107
|
+
let browser = null;
|
|
108
|
+
try { browser = await kit.launchBrowser(); } catch (e) { if (!(e instanceof kit.NoBrowserError)) throw e; }
|
|
109
|
+
after(() => browser && browser.close());
|
|
110
|
+
|
|
111
|
+
test('under the grove look, Thinking, the Tweak editor, Atlas, City and Studio show no salmon in either mode', { skip: !browser && 'no system Edge or Chrome', timeout: 180000 }, async () => {
|
|
112
|
+
const stub = await kit.startStub({ surface: 'hall-ui', prefix: '/builders/', style: 'grove', auth: true });
|
|
113
|
+
try {
|
|
114
|
+
for (const mode of ['dark', 'light']) {
|
|
115
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 860 }, colorScheme: mode });
|
|
116
|
+
await ctx.addInitScript(() => { try { sessionStorage.setItem('hall-craft-asked', '1'); } catch (e) { /* private mode */ } });
|
|
117
|
+
for (const room of ['thinking', 'tweak-editor', 'atlas', 'city', 'studio']) {
|
|
118
|
+
const page = await ctx.newPage();
|
|
119
|
+
await page.goto(`${stub.base}/builders/${room}`, { waitUntil: 'load' });
|
|
120
|
+
await page.waitForTimeout(800);
|
|
121
|
+
const r = await page.evaluate(() => {
|
|
122
|
+
const SALMON = /\b251,\s*149,\s*105\b/;
|
|
123
|
+
const hits = [];
|
|
124
|
+
for (const el of document.querySelectorAll('*')) {
|
|
125
|
+
const cs = getComputedStyle(el);
|
|
126
|
+
for (const k of ['color', 'backgroundColor', 'borderTopColor', 'fill', 'stroke', 'outlineColor', 'boxShadow', 'backgroundImage']) {
|
|
127
|
+
if (SALMON.test(cs[k] || '')) hits.push(`${el.tagName.toLowerCase()} ${k}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { accent: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim().toLowerCase(), hits };
|
|
131
|
+
});
|
|
132
|
+
await page.close();
|
|
133
|
+
assert.notEqual(r.accent, '#fb9569', `${mode} ${room}: the look's accent is applied`);
|
|
134
|
+
assert.deepEqual(r.hits, [], `${mode} ${room}: no salmon left`);
|
|
135
|
+
}
|
|
136
|
+
await ctx.close();
|
|
137
|
+
}
|
|
138
|
+
} finally { stub.stop(); }
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the sky reads its colours off the stage as tokens, not channels', () => {
|
|
142
|
+
const css = strip(fs.readFileSync(path.join(PUB, 'thinking.css'), 'utf8'));
|
|
143
|
+
assert.doesNotMatch(css, /--sky-[a-z]+-rgb/, 'no rgb channel tokens');
|
|
144
|
+
assert.match(css, /--void:\s*var\(--bg\)/, 'the void is the hall\'s ground');
|
|
145
|
+
assert.match(css, /--accent:\s*var\(--accent-ink\)/, 'the accent is the pack\'s (darkened on paper by the hall\'s own rule)');
|
|
146
|
+
const draw = fs.readFileSync(path.join(PUB, 'sky-draw.js'), 'utf8');
|
|
147
|
+
for (const tok of ['--ink', '--void', '--sky-cold', '--accent', '--chart']) assert.match(draw, new RegExp(`fill\\('${tok}'`), `sky-draw reads ${tok}`);
|
|
148
|
+
});
|
package/tests/hall_sky.mjs
CHANGED
|
@@ -559,65 +559,62 @@ const skyBlocks = () => {
|
|
|
559
559
|
return { dark: dark && dark[1], light: light && light[1], twin: twin && twin[1] };
|
|
560
560
|
};
|
|
561
561
|
|
|
562
|
+
// What the stage's computed style answers in each theme: the two blocks above,
|
|
563
|
+
// read the way the browser would.
|
|
564
|
+
const stageStyles = (body) => Object.fromEntries([...body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map((m) => [m[1], m[2].trim()]));
|
|
565
|
+
|
|
562
566
|
await test('LIGHT IS WRITTEN TWICE AND THE TWO ARE ONE — the data-theme block and its first-paint twin declare the same lines', () => {
|
|
563
567
|
const { dark, light, twin } = skyBlocks();
|
|
564
|
-
assert.ok(dark, 'the
|
|
568
|
+
assert.ok(dark, 'the stage token block is found');
|
|
565
569
|
assert.ok(light, 'the html[data-theme="light"] block is found');
|
|
566
570
|
assert.ok(twin, 'the prefers-color-scheme: light twin is found');
|
|
567
571
|
same(declLines(twin), declLines(light), 'the twin is the data-theme block, line for line');
|
|
568
|
-
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
await test('light re-declares every colour the dark set declares, and every channel is three bytes', () => {
|
|
572
|
-
const { dark, light } = skyBlocks();
|
|
573
|
-
const type = new Set(['--label', '--mono', '--face']); // the faces are not a theme
|
|
574
|
-
const want = [...declNames(dark)].filter((n) => !type.has(n));
|
|
575
|
-
assert.ok(want.length >= 15, `the dark set names its colours (${want.length})`);
|
|
576
|
-
const have = declNames(light);
|
|
577
|
-
same(want.filter((n) => !have.has(n)), [], 'no dark colour is left for light to inherit');
|
|
578
|
-
for (const [name, body] of [['dark', dark], ['light', light]]) {
|
|
579
|
-
const chans = [...body.matchAll(/(--sky-[\w-]+-rgb)\s*:\s*([^;]+);/g)];
|
|
580
|
-
assert.ok(chans.length >= 5, `${name} declares the canvas's channels`);
|
|
581
|
-
for (const [, n, v] of chans) {
|
|
582
|
-
const parts = v.split(',').map((x) => Number(x.trim()));
|
|
583
|
-
assert.ok(parts.length === 3 && parts.every((x) => Number.isInteger(x) && x >= 0 && x <= 255), `${name} ${n} is r,g,b (got "${v}")`);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
572
|
+
same([...declNames(light)].sort(), ['--sky-drop', '--sky-temp', 'color-scheme'], 'and it carries only what the hall\'s own tier cannot: the card shadow and the canvas warmth');
|
|
586
573
|
});
|
|
587
574
|
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
|
|
575
|
+
// task 1005087: the sky's colours are the HALL's tokens, which already flip with the theme and
|
|
576
|
+
// follow the pack, so a re-branded instance's sky wears its colours. No literal, no rgb channel.
|
|
577
|
+
await test('the stage mixes the hall\'s tokens only: every colour is a var() or a color-mix of them', () => {
|
|
578
|
+
const { dark } = skyBlocks();
|
|
579
|
+
const type = new Set(['--label', '--mono', '--face', '--sky-temp', '--sky-drop']);
|
|
580
|
+
const colours = Object.entries(stageStyles(dark)).filter(([n]) => !type.has(n));
|
|
581
|
+
assert.ok(colours.length >= 10, `the stage names its colours (${colours.length})`);
|
|
582
|
+
for (const [n, v] of colours) assert.match(v, /^(var\(--[\w-]+\)|color-mix\(in srgb, var\(--[\w-]+\) [\d.]+%, (transparent|var\(--[\w-]+\))\))$/, `${n}: ${v}`);
|
|
583
|
+
assert.doesNotMatch(SKY_CSS.replace(/\/\*[\s\S]*?\*\//g, ''), /--sky-[\w-]+-rgb/, 'no rgb channel is left');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// The stage's computed colours in each theme, as the browser answers the probe sky-draw.js reads.
|
|
587
|
+
const DARK = { '--ink': 'rgb(255, 255, 255)', '--void': 'rgb(0, 0, 0)', '--sky-cold': 'color(srgb 0.08 0.08 0.08)', '--accent': 'rgb(251, 149, 105)', '--chart': 'rgb(251, 191, 36)', '--sky-temp': '1' };
|
|
588
|
+
const LIGHT = { '--ink': 'rgb(23, 24, 26)', '--void': 'rgb(237, 236, 238)', '--sky-cold': 'color(srgb 0.88 0.88 0.89)', '--accent': 'rgb(163, 72, 26)', '--chart': 'rgb(180, 83, 9)', '--sky-temp': '.42' };
|
|
591
589
|
|
|
592
590
|
await test('THE CANVAS FOLLOWS THE TOGGLE — a theme flip re-reads the stage and repaints, with no reload', async () => {
|
|
593
|
-
const {
|
|
594
|
-
const styles = stageStyles(dark);
|
|
591
|
+
const styles = { ...DARK };
|
|
595
592
|
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions(), styles });
|
|
596
593
|
const SKY = p.SKY(); const acc = SKY.ACC;
|
|
597
594
|
assert.ok(p.themeObservers() >= 1, 'the page watches html[data-theme]');
|
|
598
595
|
p.frame();
|
|
599
|
-
same(SKY.INK, [255, 255, 255], 'dark: the ink is the
|
|
596
|
+
same(SKY.INK, [255, 255, 255], 'dark: the ink is the hall\'s white');
|
|
600
597
|
same(SKY.VOID, [0, 0, 0], 'dark: the void is black');
|
|
601
598
|
same(SKY.tone([255, 238, 222]), [255, 238, 222], 'dark: a thought keeps its temperature');
|
|
602
599
|
|
|
603
|
-
Object.assign(styles,
|
|
600
|
+
Object.assign(styles, LIGHT);
|
|
604
601
|
p.S().dirty = false;
|
|
605
602
|
p.setTheme('light');
|
|
606
603
|
assert.equal(!!p.S().dirty, true, 'the flip marks the sky for a redraw');
|
|
607
604
|
p.frame();
|
|
608
|
-
same(SKY.INK,
|
|
609
|
-
same(SKY.VOID,
|
|
610
|
-
same(SKY.ACC,
|
|
605
|
+
same(SKY.INK, [23, 24, 26], 'light: the ink is the stage\'s');
|
|
606
|
+
same(SKY.VOID, [237, 236, 238], 'light: the void is paper');
|
|
607
|
+
same(SKY.ACC, [163, 72, 26], 'light: the accent darkens');
|
|
611
608
|
assert.equal(SKY.ACC === acc, true, 'refilled in place, so SKY.ACC is still the object every file holds');
|
|
612
|
-
|
|
613
|
-
same(SKY.tone([255, 238, 222]), [255, 238, 222].map((v) => Math.round(v * k)), 'light: a near-white thought darkens to read on paper');
|
|
609
|
+
same(SKY.tone([255, 238, 222]), [255, 238, 222].map((v) => Math.round(v * 0.42)), 'light: a near-white thought darkens to read on paper');
|
|
614
610
|
assert.equal(SKY.ink(0.5), `rgba(${SKY.INK.join(',')},0.5)`, 'ink() paints in the new ink');
|
|
615
611
|
|
|
616
|
-
Object.assign(styles,
|
|
612
|
+
Object.assign(styles, DARK);
|
|
617
613
|
p.setTheme('dark');
|
|
618
614
|
p.frame();
|
|
619
615
|
same(SKY.INK, [255, 255, 255], 'and back: the white returns');
|
|
620
616
|
assert.equal(SKY.ink(0.5), 'rgba(255,255,255,0.5)', 'ink() forgot the light strings');
|
|
621
617
|
});
|
|
622
618
|
|
|
619
|
+
|
|
623
620
|
summary();
|