@bongos/core 1.19.457 → 1.19.459

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.
Files changed (55) hide show
  1. package/.bongos-core.json +126 -46
  2. package/.claude/skills/design/SKILL.md +10 -0
  3. package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/branding-contract.md +1 -1
  6. package/docs/copy-inventory.md +160 -177
  7. package/docs/copy-registry.json +445 -606
  8. package/docs/file-map.md +2 -2
  9. package/docs/module-api-changelog.md +4 -0
  10. package/docs/recipes/ui-look-before-you-ship.md +2 -0
  11. package/modules/hall-ui/public/gate.css +20 -88
  12. package/modules/hall-ui/public/gate.html +24 -12
  13. package/modules/hall-ui/public/gate.js +77 -60
  14. package/modules/hall-ui/public/gate.states.json +19 -1
  15. package/modules/hall-ui/public/government.css +96 -154
  16. package/modules/hall-ui/public/government.html +80 -52
  17. package/modules/hall-ui/public/government.js +227 -136
  18. package/modules/hall-ui/public/government.states.json +71 -1
  19. package/modules/hall-ui/public/harbor.css +9 -120
  20. package/modules/hall-ui/public/harbor.html +23 -11
  21. package/modules/hall-ui/public/harbor.js +109 -83
  22. package/modules/hall-ui/public/harbor.states.json +19 -1
  23. package/modules/hall-ui/public/oversight.css +296 -0
  24. package/modules/hall-ui/public/watch.css +56 -170
  25. package/modules/hall-ui/public/watch.html +48 -37
  26. package/modules/hall-ui/public/watch.js +363 -298
  27. package/modules/hall-ui/public/watch.states.json +48 -1
  28. package/modules/ui-design/kit/README.md +7 -5
  29. package/modules/ui-design/kit/check-mock.js +12 -5
  30. package/modules/ui-design/kit/contrast.js +13 -6
  31. package/modules/ui-design/kit/lib.js +39 -2
  32. package/modules/ui-design/kit/probe.js +4 -1
  33. package/modules/ui-design/kit/render.js +7 -4
  34. package/modules/ui-design/kit/serve.js +9 -2
  35. package/modules/ui-design/styles/README.md +54 -2
  36. package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
  37. package/modules/ui-design/styles/chrome-world/mock.html +275 -0
  38. package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
  39. package/modules/ui-design/styles/chrome-world/pack.json +33 -0
  40. package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
  41. package/modules/ui-design/styles/expedition/mock.html +285 -0
  42. package/modules/ui-design/styles/expedition/mock.states.json +11 -0
  43. package/modules/ui-design/styles/expedition/pack.json +33 -0
  44. package/modules/ui-design/styles/grove/DESIGN.md +148 -0
  45. package/modules/ui-design/styles/grove/mock.html +294 -0
  46. package/modules/ui-design/styles/grove/mock.states.json +11 -0
  47. package/modules/ui-design/styles/grove/pack.json +33 -0
  48. package/package-lock.json +2 -2
  49. package/package.json +1 -1
  50. package/src/module-api.js +1 -1
  51. package/tests/cosmos_token_contrast.mjs +9 -2
  52. package/tests/hall_oversight_world.mjs +118 -0
  53. package/tests/hall_tokens.mjs +15 -6
  54. package/tests/ui_design_styles.mjs +218 -0
  55. package/tests/world_tokens.mjs +9 -2
@@ -46,6 +46,52 @@
46
46
  },
47
47
  "out": {
48
48
  "auth": false
49
+ },
50
+ "operations": {
51
+ "auth": true,
52
+ "actions": [
53
+ [
54
+ "wait",
55
+ 700
56
+ ],
57
+ [
58
+ "click",
59
+ "#tab-operations"
60
+ ],
61
+ [
62
+ "wait",
63
+ 300
64
+ ]
65
+ ],
66
+ "expect": {
67
+ "visible": [
68
+ "#pane-operations",
69
+ "#sessions-stats"
70
+ ]
71
+ }
72
+ },
73
+ "security": {
74
+ "auth": true,
75
+ "actions": [
76
+ [
77
+ "wait",
78
+ 700
79
+ ],
80
+ [
81
+ "click",
82
+ "#tab-security"
83
+ ],
84
+ [
85
+ "wait",
86
+ 300
87
+ ]
88
+ ],
89
+ "expect": {
90
+ "visible": [
91
+ "#pane-security",
92
+ "#watch-scroll"
93
+ ]
94
+ }
49
95
  }
50
96
  },
51
97
  "reducedMotion": {
@@ -56,5 +102,6 @@
56
102
  600
57
103
  ]
58
104
  ]
59
- }
105
+ },
106
+ "_states": "task 1003469: the three tabs are three states, so the Operations figure bands and the recent-sessions ledger, and the Security panel's four groups, are each measured and not only the People tab the page opens on."
60
107
  }
@@ -3,11 +3,13 @@
3
3
  The stub, the renderer/auditor, the interaction probes, the mock checker and the contrast probe any builder points at any surface of any instance ([task 1003320](https://cloudbongos.com/builders#/task/1003320), [ADR 0197](../../../docs/adr/<redacted>.md)). Plain Node; the browser is the system Edge or Chrome via `playwright-core` (a devDependency — nothing is downloaded).
4
4
 
5
5
  ```
6
- node modules/ui-design/kit/serve.js # the stub (SURFACE, PREFIX, AUTH=1, CHILD=1, LIVE=…, FAIL=…, STUB_PORT)
7
- node modules/ui-design/kit/render.js --page /projects # every state × 1440/390/320 × dark/light, audited against the floors
8
- node modules/ui-design/kit/probe.js --page /projects # the interaction contract as PASS/FAIL lines
9
- node modules/ui-design/kit/check-mock.js --file docs/design/mocks/hub/v2.html
10
- node modules/ui-design/kit/contrast.js --pack | --url <url> [--glyphs sel,sel]
6
+ node modules/ui-design/kit/serve.js # the stub (SURFACE, PREFIX, AUTH=1, CHILD=1, LIVE=…, FAIL=…, STYLE=<look>, STUB_PORT)
7
+ node modules/ui-design/kit/render.js --page /projects # every state × 1440/390/320 × dark/light, audited against the floors (--style <look>)
8
+ node modules/ui-design/kit/probe.js --page /projects # the interaction contract as PASS/FAIL lines (--style <look>)
9
+ node modules/ui-design/kit/check-mock.js --file docs/design/mocks/hub/v2.html # (--pack <look> for a style's own mock)
10
+ node modules/ui-design/kit/contrast.js --pack [look] | --url <url> [--glyphs sel,sel]
11
11
  ```
12
12
 
13
+ A **look** is a branding pack from the module's style library (`modules/ui-design/styles/<name>/`, task 1003323): `STYLE=<name>` lays it over the instance's own pack through `GDS_BRANDING_FILE`, so any surface renders under any look with no page edit.
14
+
13
15
  A surface describes itself in two JSON files beside it — `<page>.states.json` and `<page>.probes.json` — in the vocabularies `lib.js` defines and validates. The full loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). Exit codes everywhere: 0 clean · 1 problems · 2 a broken file or usage · 3 no browser.
@@ -14,8 +14,10 @@
14
14
  // shadow). The em dash ban is Cloud Bongos's, not the platform's: an instance
15
15
  // whose DESIGN.md never mentions it is not held to it.
16
16
  //
17
- // node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys THESIS,BAR] [--design <DESIGN.md>] [--no-taste]
17
+ // node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys THESIS,BAR] [--design <DESIGN.md>] [--no-taste] [--pack <style>]
18
18
  //
19
+ // `--pack <style>` checks a style's mock against that look's own colours (the pinned-literal
20
+ // rule) and, unless --design says otherwise, its own DESIGN.md bans (task 1003323).
19
21
  // Prints PASS/FAIL/WARN per rule; exit 1 on any FAIL, 2 on usage.
20
22
  const fs = require('node:fs');
21
23
  const path = require('node:path');
@@ -89,7 +91,8 @@ function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys
89
91
  ok('no literal hex colour (the fifteen via var()/color-mix(); black or white with alpha only)', hexes.length === 0, hexes.length ? [...new Set(hexes)].slice(0, 12).join(',') : '');
90
92
  const rgbLit = [...css.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g)].filter((x) => !((x[1] === '0' && x[2] === '0' && x[3] === '0') || (x[1] === '255' && x[2] === '255' && x[3] === '255')));
91
93
  ok('rgb() literals only as black or white with alpha', rgbLit.length === 0, rgbLit.length ? `${rgbLit.length} found` : '');
92
- const pinned = Object.entries(packColors).filter(([, v]) => typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v) && new RegExp(v.replace('#', '#'), 'i').test(css)).map(([k]) => k);
94
+ // a pack whose card is pure white (the expedition's) must not turn the one legal literal into a pin
95
+ const pinned = Object.entries(packColors).filter(([, v]) => typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v) && !BLACK_OR_WHITE.test(v.slice(1).toLowerCase()) && new RegExp(v.replace('#', '#'), 'i').test(css)).map(([k]) => k);
93
96
  ok('no pack colour pinned as a literal (a var() of the contract token instead)', pinned.length === 0, pinned.join(','));
94
97
 
95
98
  // ── the taste bans the instance declared ──
@@ -126,16 +129,20 @@ function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys
126
129
  function main() {
127
130
  const args = process.argv.slice(2);
128
131
  const file = lib.argOf(args, '--file', null);
129
- if (!file || lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys A,B] [--design <DESIGN.md>] [--no-taste]\n'); process.exit(lib.EXIT.USAGE); }
132
+ if (!file || lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys A,B] [--design <DESIGN.md>] [--no-taste] [--pack <style>]\n'); process.exit(lib.EXIT.USAGE); }
130
133
  if (!fs.existsSync(file)) { process.stderr.write(`check-mock: not found: ${file}\n`); process.exit(lib.EXIT.USAGE); }
131
- const designPath = lib.argOf(args, '--design', path.join(lib.REPO_ROOT, 'DESIGN.md'));
134
+ const styleArg = lib.argOf(args, '--pack', null);
135
+ const style = styleArg ? lib.resolveStyle(styleArg) : null;
136
+ if (styleArg && !style) { process.stderr.write(`check-mock: --pack "${styleArg}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'})\n`); process.exit(lib.EXIT.USAGE); }
137
+ const designPath = lib.argOf(args, '--design', style ? path.join(style.dir, 'DESIGN.md') : path.join(lib.REPO_ROOT, 'DESIGN.md'));
132
138
  let bans = new Set();
133
139
  if (!lib.hasFlag(args, '--no-taste')) {
134
140
  if (fs.existsSync(designPath)) bans = readDesignBans(fs.readFileSync(designPath, 'utf8'));
135
141
  else process.stdout.write(`WARN taste bans off: no DESIGN.md at ${designPath} (the platform rules still apply)\n`);
136
142
  }
137
143
  let packColors = {};
138
- try { packColors = lib.packColors(lib.instancePack()); } catch { packColors = {}; }
144
+ if (style) packColors = lib.packColors(style.pack);
145
+ else { try { packColors = lib.packColors(lib.instancePack()); } catch { packColors = {}; } }
139
146
  const results = checkMock(fs.readFileSync(file, 'utf8'), { bans, packColors, states: lib.listArg(args, '--states', []), keys: lib.listArg(args, '--keys', []) });
140
147
  for (const [s, rule, detail] of results) process.stdout.write(`${s.padEnd(4)} ${rule}${detail ? ' (' + detail + ')' : ''}\n`);
141
148
  const fails = results.filter((r) => r[0] === 'FAIL').length;
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  // contrast.js — the contrast probe (task 1003320, ADR 0197), three ways:
4
- // --pack the instance's branding pack: every ink on every ground,
5
- // light values from the pack (pure; no browser)
4
+ // --pack [name] the instance's branding pack — or, named, a look from the
5
+ // style library (modules/ui-design/styles/<name>/pack.json, or a
6
+ // pack.json path): every ink on every ground, light values
7
+ // from the pack (pure; no browser)
6
8
  // --url <url> | --file <html> walk every visible text element in dark + light
7
9
  // and compute the real WCAG ratio against its COMPOSITED
8
10
  // background (the degraded static detector cannot do this)
@@ -11,7 +13,7 @@
11
13
  // ground's median — for text over a moving or textured
12
14
  // ground where the stylesheet cannot say what is under it
13
15
  //
14
- // node modules/ui-design/kit/contrast.js --pack
16
+ // node modules/ui-design/kit/contrast.js --pack [expedition]
15
17
  // node modules/ui-design/kit/contrast.js --url http://127.0.0.1:3412/projects [--modes dark,light] [--glyphs "#projTrig,.foot a"]
16
18
  // node modules/ui-design/kit/contrast.js --file docs/design/mocks/hub/v2.html
17
19
  //
@@ -110,12 +112,17 @@ async function glyphContrast(page, selector) {
110
112
 
111
113
  async function main() {
112
114
  const args = process.argv.slice(2);
113
- if (lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/contrast.js --pack | --url <url> | --file <html> [--modes dark,light] [--glyphs sel,sel]\n'); process.exit(lib.EXIT.USAGE); }
115
+ if (lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/contrast.js --pack [name] | --url <url> | --file <html> [--modes dark,light] [--glyphs sel,sel]\n'); process.exit(lib.EXIT.USAGE); }
114
116
  const url = lib.argOf(args, '--url', null), file = lib.argOf(args, '--file', null);
115
117
  let failures = 0;
116
118
  if (lib.hasFlag(args, '--pack') || (!url && !file)) {
117
- const rows = packContrast(lib.instancePack());
118
- process.stdout.write('the pack (light values):\n');
119
+ // `--pack` alone is the instance's; `--pack <name>` is a look from the style library
120
+ const packArg = lib.argOf(args, '--pack', null);
121
+ const named = packArg && !packArg.startsWith('--') ? packArg : null;
122
+ const style = named ? lib.resolveStyle(named) : null;
123
+ if (named && !style) { process.stderr.write(`contrast: --pack "${named}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'}) nor a pack.json path\n`); process.exit(lib.EXIT.USAGE); }
124
+ const rows = packContrast(style ? style.pack : lib.instancePack());
125
+ process.stdout.write(`${style ? `the ${style.name} pack` : 'the pack'} (light values):\n`);
119
126
  for (const r of rows) { process.stdout.write(` ${r.ok === null ? 'SKIP' : r.ok ? 'PASS' : 'FAIL'} ${String(r.fg).padEnd(11)} on ${String(r.bg).padEnd(8)} ${r.ratio === null ? r.note : r.ratio + ':1 (need ' + r.need + ')'}\n`); if (r.ok === false) failures++; }
120
127
  if (!url && !file) { process.stdout.write(failures ? `\n${failures} pack pair(s) below the floor\n` : '\nPACK CLEAN\n'); process.exit(failures ? lib.EXIT.PROBLEMS : lib.EXIT.CLEAN); }
121
128
  }
@@ -10,12 +10,14 @@
10
10
  // Exit codes shared by every kit CLI: 0 clean · 1 problems/FAILs · 2 usage or a
11
11
  // contract error (a bad file, a missing page) · 3 no browser / no playwright-core.
12
12
  const fs = require('node:fs');
13
+ const os = require('node:os');
13
14
  const path = require('node:path');
14
15
  const net = require('node:net');
15
16
  const { spawn } = require('node:child_process');
16
17
 
17
18
  const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
18
19
  const KIT_DIR = __dirname;
20
+ const STYLES_DIR = path.join(__dirname, '..', 'styles');
19
21
  const EXIT = Object.freeze({ CLEAN: 0, PROBLEMS: 1, USAGE: 2, NO_BROWSER: 3 });
20
22
 
21
23
  // The floors (modules/ui-design/CLAUDE.md "The platform's floors"). The names are
@@ -342,10 +344,11 @@ function freePort() {
342
344
  s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
343
345
  });
344
346
  }
345
- async function startStub({ surface, auth = false, port, prefix, child = false, fixtures } = {}) {
347
+ async function startStub({ surface, auth = false, port, prefix, child = false, fixtures, style } = {}) {
346
348
  const p = port || await freePort();
347
349
  const env = { ...process.env, STUB_PORT: String(p), AUTH: auth ? '1' : '0', CHILD: child ? '1' : '0' };
348
350
  if (surface) env.SURFACE = surface;
351
+ if (style) env.STYLE = style;
349
352
  if (prefix) env.PREFIX = prefix;
350
353
  if (fixtures) env.FIXTURE_ALIAS = JSON.stringify(fixtures);
351
354
  const proc = spawn(process.execPath, [path.join(KIT_DIR, 'serve.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -375,6 +378,39 @@ function composite(fg, bg) { const a = fg[3] == null ? 1 : fg[3]; return [fg[0]
375
378
  function luminance(c) { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(c[0]) + 0.7152 * f(c[1]) + 0.0722 * f(c[2]); }
376
379
  function contrastRatio(a, b) { const l1 = luminance(a), l2 = luminance(b); const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1]; return (hi + 0.05) / (lo + 0.05); }
377
380
 
381
+ // ── the style library (task 1003323) ──────────────────────────────────────────
382
+ // A look is a branding-pack overlay under modules/ui-design/styles/<name>/pack.json
383
+ // (theme.ui: the thirteen colours + the two font stacks), never a fork of a sheet.
384
+ function listStyles(dir = STYLES_DIR) {
385
+ return fs.existsSync(dir) ? fs.readdirSync(dir).filter((n) => fs.existsSync(path.join(dir, n, 'pack.json'))).sort() : [];
386
+ }
387
+ // A style by NAME (a dir under styles/) or by PATH (a pack.json anywhere) → { name, dir, file, pack }, or null.
388
+ function resolveStyle(nameOrPath, dir = STYLES_DIR) {
389
+ if (!nameOrPath || typeof nameOrPath !== 'string') return null;
390
+ const byName = /^[a-z0-9][a-z0-9-]*$/.test(nameOrPath) ? path.join(dir, nameOrPath, 'pack.json') : null;
391
+ const file = byName && fs.existsSync(byName) ? byName : (nameOrPath.endsWith('.json') && fs.existsSync(nameOrPath) ? path.resolve(nameOrPath) : null);
392
+ if (!file) return null;
393
+ const pack = readJson(file);
394
+ return { name: pack.name || path.basename(path.dirname(file)), dir: path.dirname(file), file, pack };
395
+ }
396
+ // The pack the stub serves under a style: the instance's own pack (config/branding.json,
397
+ // where it has one) with the style's theme.ui laid over it, written to a temp file the
398
+ // core-side transform reads through GDS_BRANDING_FILE (src/branding.js's fixed knob — the
399
+ // one that lets one checkout run two instances). The instance's identity strings stay;
400
+ // only the fifteen change, which is the whole point: a look is a pack, never a page edit.
401
+ function styleBrandingFile(style, root = REPO_ROOT) {
402
+ const instanceFile = path.join(root, 'config', 'branding.json');
403
+ const instance = fs.existsSync(instanceFile) ? readJson(instanceFile) : {};
404
+ const theme = instance.theme || {};
405
+ const merged = { ...instance, theme: { ...theme, ui: { ...(theme.ui || {}), ...((style.pack.theme || {}).ui || {}) } } };
406
+ // a pack.json loaded by PATH names itself; the temp file takes a slug of that name, never the name
407
+ const slug = String(style.name).replace(/[^a-z0-9-]/gi, '_').slice(0, 64) || 'style';
408
+ const out = path.join(os.tmpdir(), 'ui-design-kit', 'styles', `${slug}.branding.json`);
409
+ fs.mkdirSync(path.dirname(out), { recursive: true });
410
+ fs.writeFileSync(out, JSON.stringify(merged));
411
+ return out;
412
+ }
413
+
378
414
  // The instance's resolved branding pack through the doorway — the only core
379
415
  // reach the kit makes (task 1003320). Loaded lazily: the pure helpers above never
380
416
  // touch it, so the test pins them without booting the doorway.
@@ -391,9 +427,10 @@ function packColors(pack) {
391
427
  }
392
428
 
393
429
  module.exports = {
394
- REPO_ROOT, KIT_DIR, EXIT, FLOORS, SIZES, MODES, CHANNELS, SETTLE_MS, ACTIONS, EXPECT_KEYS,
430
+ REPO_ROOT, KIT_DIR, STYLES_DIR, EXIT, FLOORS, SIZES, MODES, CHANNELS, SETTLE_MS, ACTIONS, EXPECT_KEYS,
395
431
  argOf, hasFlag, listArg, pageName, normalizePageArg, resolveSurfaceDir, sidecarPath, readJson,
396
432
  validateActions, runActions, validateExpect, factsInPage, expectProblems, auditFloors, reducedMotionProblems,
397
433
  validateStatesFile, validateProbesFile, NoBrowserError, launchBrowser, attachCollectors, withMode,
398
434
  freePort, startStub, parseColor, composite, luminance, contrastRatio, instancePack, packColors,
435
+ listStyles, resolveStyle, styleBrandingFile,
399
436
  };
@@ -10,6 +10,7 @@
10
10
  // dialog yields Escape; signed out nothing throws.
11
11
  //
12
12
  // node modules/ui-design/kit/probe.js --page /projects [--surface <dir|key>] [--probes <file>] [--only <substring>]
13
+ // --style <name> drive the page under a look from the style library (task 1003323)
13
14
  // --base-out <url> --base-in <url> use running stubs instead of starting two
14
15
  //
15
16
  // A probe with `continue: true` keeps the previous probe's page (a contract is a
@@ -52,7 +53,9 @@ async function main() {
52
53
 
53
54
  const stubs = [];
54
55
  let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
55
- const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures };
56
+ const style = lib.argOf(args, '--style', null);
57
+ if (style && !lib.resolveStyle(style)) usage(`--style "${style}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'})`);
58
+ const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures, style };
56
59
  if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
57
60
  if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
58
61
  const stop = () => stubs.forEach((s) => s.stop());
@@ -12,6 +12,7 @@
12
12
  // node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
13
13
  // node modules/ui-design/kit/render.js --states <file> (explicit file)
14
14
  // --modes dark,light --sizes 1440,390,320 --only a,b --out <dir>
15
+ // --style <name> render under a look from the style library (STYLE= on the stubs; task 1003323)
15
16
  // --base-out <url> --base-in <url> use running stubs instead of starting two
16
17
  //
17
18
  // Exit 0 ALL CLEAN · 1 problems · 2 usage/contract · 3 no browser.
@@ -21,7 +22,7 @@ const path = require('node:path');
21
22
  const lib = require('./lib.js');
22
23
 
23
24
  function usage(msg) {
24
- process.stderr.write(`render: ${msg}\n node modules/ui-design/kit/render.js --page /projects [--surface <dir|key>] [--states <file>] [--modes dark,light] [--sizes 1440,390,320] [--only a,b] [--out <dir>] [--base-out <url> --base-in <url>]\n`);
25
+ process.stderr.write(`render: ${msg}\n node modules/ui-design/kit/render.js --page /projects [--surface <dir|key>] [--states <file>] [--modes dark,light] [--sizes 1440,390,320] [--only a,b] [--style <name>] [--out <dir>] [--base-out <url> --base-in <url>]\n`);
25
26
  process.exit(lib.EXIT.USAGE);
26
27
  }
27
28
 
@@ -71,12 +72,14 @@ async function main() {
71
72
  const only = lib.listArg(args, '--only', null);
72
73
  const names = Object.keys(json.states).filter((n) => !only || only.includes(n));
73
74
  if (!names.length) usage(`--only names no state of ${path.basename(file)}`);
74
- const out = path.resolve(lib.argOf(args, '--out', path.join(os.tmpdir(), 'ui-design-kit', surface.replace(/[\\/]/g, '_'), lib.pageName(json.page))));
75
+ const style = lib.argOf(args, '--style', null);
76
+ if (style && !lib.resolveStyle(style)) usage(`--style "${style}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'})`);
77
+ const out = path.resolve(lib.argOf(args, '--out', path.join(os.tmpdir(), 'ui-design-kit', surface.replace(/[\\/]/g, '_'), lib.pageName(json.page) + (style ? `@${style}` : ''))));
75
78
  fs.mkdirSync(out, { recursive: true });
76
79
 
77
80
  const stubs = [];
78
81
  let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
79
- const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures };
82
+ const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures, style };
80
83
  if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
81
84
  if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
82
85
  const stop = () => stubs.forEach((s) => s.stop());
@@ -85,7 +88,7 @@ async function main() {
85
88
  try { browser = await lib.launchBrowser(); }
86
89
  catch (e) { stop(); process.stderr.write(`render: ${e.message}\n`); process.exit(e instanceof lib.NoBrowserError ? lib.EXIT.NO_BROWSER : lib.EXIT.USAGE); }
87
90
 
88
- const report = { page: json.page, surface, out, renders: {}, reducedMotion: {} };
91
+ const report = { page: json.page, surface, style: style || null, out, renders: {}, reducedMotion: {} };
89
92
  const problems = [];
90
93
  const collectorOpts = { ignoreRequests: json.ignoreRequests, ignoreConsole: json.ignoreConsole };
91
94
  try {
@@ -21,6 +21,8 @@
21
21
  // CHILD=1 the child-instance shape: apex-only regions stripped
22
22
  // LIVE=1 | LIVE=<origin> proxy the public feeds to the pack's public origin (or the named one)
23
23
  // FAIL=<feed,feed> those feeds answer 500 (the names in FEEDS below)
24
+ // STYLE=<name | pack.json> serve under a look from the style library (modules/ui-design/
25
+ // styles/<name>/pack.json laid over the instance's pack; task 1003323)
24
26
  //
25
27
  // Exit 2 on a surface that does not resolve. Prints ONE line when listening
26
28
  // ("stub on http://…") — render.js and probe.js wait for it.
@@ -40,6 +42,7 @@ const AUTH = process.env.AUTH === '1';
40
42
  const CHILD = process.env.CHILD === '1';
41
43
  const LIVE = process.env.LIVE || '';
42
44
  const FAIL = new Set(String(process.env.FAIL || '').split(',').map((s) => s.trim()).filter(Boolean));
45
+ const STYLE = process.env.STYLE || '';
43
46
  // a states file may point a fixture NAME at another FILE (stub.fixtures, task 1003345) — the
44
47
  // way the mine view's empty state is rendered without a second stub shape
45
48
  const ALIAS = JSON.parse(process.env.FIXTURE_ALIAS || '{}');
@@ -51,6 +54,10 @@ const DIR = lib.resolveSurfaceDir(SURFACE, ROOT);
51
54
  if (!DIR) { process.stderr.write(`serve: SURFACE "${SURFACE}" is neither a directory of ${ROOT} nor a module key with a webSurfaces dir\n`); process.exit(lib.EXIT.USAGE); }
52
55
  if (!PREFIX.startsWith('/') || !PREFIX.endsWith('/')) { process.stderr.write('serve: PREFIX must start and end with "/"\n'); process.exit(lib.EXIT.USAGE); }
53
56
  if (!fs.existsSync(TRANSFORM)) { process.stderr.write(`serve: the transform command is missing: ${TRANSFORM}\n`); process.exit(lib.EXIT.USAGE); }
57
+ const style = STYLE ? lib.resolveStyle(STYLE) : null;
58
+ if (STYLE && !style) { process.stderr.write(`serve: STYLE "${STYLE}" is neither a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'}) nor a pack.json path\n`); process.exit(lib.EXIT.USAGE); }
59
+ // the transform resolves the pack itself (src/branding.js); under a style it reads the merged file
60
+ const TRANSFORM_ENV = style ? { ...process.env, GDS_BRANDING_FILE: lib.styleBrandingFile(style, ROOT) } : process.env;
54
61
 
55
62
  const MIME = {
56
63
  '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript', '.mjs': 'application/javascript',
@@ -61,7 +68,7 @@ const MIME = {
61
68
 
62
69
  // The real transforms, as the instance runs them (see ui-kit-transform.js).
63
70
  function transform(file) {
64
- const r = spawnSync(process.execPath, [TRANSFORM, path.join(DIR, file), ...(CHILD ? ['--child'] : []), '--json'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
71
+ const r = spawnSync(process.execPath, [TRANSFORM, path.join(DIR, file), ...(CHILD ? ['--child'] : []), '--json'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: TRANSFORM_ENV });
65
72
  if (r.status !== 0) throw new Error(`ui-kit-transform exited ${r.status}: ${(r.stderr || r.stdout || '').trim().slice(0, 600)}`);
66
73
  return JSON.parse(r.stdout);
67
74
  }
@@ -303,5 +310,5 @@ const server = http.createServer((req, res) => {
303
310
  });
304
311
  server.listen(PORT, '127.0.0.1', () => {
305
312
  const port = server.address().port;
306
- process.stdout.write(`stub on http://127.0.0.1:${port}${PREFIX} surface=${path.relative(ROOT, DIR) || '.'} auth=${AUTH} child=${CHILD} live=${LIVE || '-'} fail=${[...FAIL].join(',') || '-'}\n`);
313
+ process.stdout.write(`stub on http://127.0.0.1:${port}${PREFIX} surface=${path.relative(ROOT, DIR) || '.'} auth=${AUTH} child=${CHILD} live=${LIVE || '-'} fail=${[...FAIL].join(',') || '-'} style=${style ? style.name : '-'}\n`);
307
314
  });
@@ -1,3 +1,55 @@
1
- # `modules/ui-design/styles/` — reserved home for the style library
1
+ # `modules/ui-design/styles/` — the style library: looks as branding packs
2
2
 
3
- Lands with [task 1003323](https://cloudbongos.com/builders#/task/1003323): looks shipped as branding packs (the fifteen host-overridable tokens plus a `DESIGN.md` per look), with two alternates beside the chrome world, so an instance picks a world instead of inheriting Cloud Bongos's. A look is a pack; it is never a page's private palette.
3
+ A **look** is something a builder or an instance owner picks for a new surface or a whole instance. The library ships looks as **branding packs** ([task 1003323](https://cloudbongos.com/builders#/task/1003323), [ADR 0219](../../../docs/adr/<redacted>.md)): the thirteen colours and the two font stacks of `theme.ui` (the fifteen host-overridable tokens, [ADR 0062 §3](../../../docs/adr/<redacted>.md) / [ADR 0196](../../../docs/adr/<redacted>.md)), a `DESIGN.md` of the look's derived recipes in the frontmatter shape the chrome world uses, and one specimen mock. **Never a fork of a sheet.** The drift tests already prove every served sheet reads only the contract, so a look that needs a sixteenth token is a contract change and an ADR, not a style.
4
+
5
+ ## The catalogue
6
+
7
+ | Style | `pack.json` | What it is | Origin |
8
+ |---|---|---|---|
9
+ | `chrome-world` | the neutral pack, byte for byte | The canonical entry: one iridescent chrome sculpture in a black void, pale grey paper in light, one warm salmon that lights only the pressable and the live. Its recipes are the repo-root [`DESIGN.md`](../../../DESIGN.md); [`chrome-world/DESIGN.md`](chrome-world/DESIGN.md) points there. | the world round's pick (task 1003296 → 1003298) |
10
+ | `expedition` | warm paper, lamp yellow, Archivo | A printed field journal: a halftone screen over the paper, hairline frames instead of a grid, big heavy uppercase headlines, the yellow as a fill only. [`expedition/DESIGN.md`](expedition/DESIGN.md) | `docs/design/mocks/world/natgeo-editorial` (kept in round 1) |
11
+ | `grove` | pale sage, tan, sea green, Nunito | A sculpted paper garden: soft corners on everything that holds content, tinted shadows, the sea as a slab a whole band sits on, the tan as the one lit thing. [`grove/DESIGN.md`](grove/DESIGN.md) | `docs/design/mocks/world/organic-cards` (kept in round 1) |
12
+
13
+ The owner looked at both alternates on the sheet on 2026-09-01 and kept them ("styles look good for a start"). Both alternates were **retuned against the contract**, which is a measurement and not a taste: a pack's thirteen are laid through three sheets that already exist (the landing's `world.css`, the hub's `cosmos.css`, the hall's `style.css`) and every pair those sheets paint must clear AA under it. The grove's sage ground lifted and its inks deepened until the hub's fixed status vocabulary and the hall's chips cleared; the expedition's `ink-faint` deepened one step. What a pack cannot carry is **dark**: the contract is light values only, and every sheet's dark tier is its own black, which a look reaches through its accent and its faces. So the grove's green dark ground stays in the world round's mock and does not travel.
14
+
15
+ ## What a style dir is (the contract `tests/ui_design_styles.mjs` enforces)
16
+
17
+ ```
18
+ modules/ui-design/styles/<name>/
19
+ pack.json { name, title, summary, origin, theme: { ui: { colors: <the thirteen>, fonts: { display, body, links } } } }
20
+ DESIGN.md frontmatter: colors (the thirteen, value for value with pack.json; the dark facts; the derived recipes),
21
+ typography, rounded, spacing, components; then the look's prose in the root DESIGN.md's sections
22
+ mock.html the specimen: no :root, tokens on body, dark written twice, every colour a var()/color-mix() of the pack,
23
+ one header / main / h1, a skip link, ?state= and ?mode=, served through the stub with the pack spliced in
24
+ ```
25
+
26
+ - `name` is the dir name, kebab-case. `theme.ui.colors` carries **exactly** the thirteen keys of the neutral pack, `fonts` the two stacks and a `links` line for the faces.
27
+ - `chrome-world/pack.json` equals `config/branding.neutral.json`'s `theme.ui` and is pinned to it.
28
+ - Every pack clears the pack floor (every ink on every ground at 4.5:1) and the three token suites in both modes; every mock passes `check-mock`'s platform rules under its own colours and its own `DESIGN.md` bans.
29
+ - The roster above, the module's [`CLAUDE.md`](../CLAUDE.md) and [`/design`](../../../.claude/skills/design/SKILL.md) name every style; a dir that is not named is not a look.
30
+
31
+ ## Using a look
32
+
33
+ **On the kit** (any surface, any look, no page edit): `STYLE=<name>` on the stub lays the pack over the instance's own (its identity strings stay; only the fifteen change) through `GDS_BRANDING_FILE`, the fixed knob `src/branding.js` already has for running one checkout as two instances.
34
+
35
+ ```
36
+ STYLE=grove node modules/ui-design/kit/serve.js # the front door under the grove
37
+ node modules/ui-design/kit/render.js --page / --style expedition # every state × width × mode, audited
38
+ node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui --style grove
39
+ node modules/ui-design/kit/probe.js --page /projects --style grove
40
+ node modules/ui-design/kit/contrast.js --pack expedition # the pack floor, no browser
41
+ node modules/ui-design/kit/check-mock.js --file modules/ui-design/styles/grove/mock.html --pack grove
42
+ SURFACE=modules/ui-design/styles/expedition STYLE=expedition node modules/ui-design/kit/serve.js # the specimen at /mock
43
+ ```
44
+
45
+ **On an instance** (the switch is the contract, not a page): copy the look's `pack.json` `theme` into `config/branding.json`, the file `cloudbongos init` writes ([`docs/branding-contract.md`](../../../docs/branding-contract.md) §Branding a new instance). Every served page re-themes at once, because the server splices `theme.ui` into `:root` at the end of `<head>` and every sheet reads only that. To leave a look, remove the block: the neutral pack (the chrome world) resolves.
46
+
47
+ **What stays the platform's, whichever look is on:** the dark ground (black), the hall's fixed green / amber / red **verification** vocabulary and the status ledger's pillars (a look must not repaint a status chip, ADR 0196 Context §2), the derived recipes each sheet carries (a corner radius, a shadow, a glass are a sheet's tier, not a pack's), and the platform floors.
48
+
49
+ ## Adding a look
50
+
51
+ 1. Make the dir with the three files; start from the alternate nearest to it.
52
+ 2. `node modules/ui-design/kit/contrast.js --pack <name>` until the floor is clean; then `node --test tests/ui_design_styles.mjs`, which runs the world / cosmos / hall suites under the pack and tells you the exact pair that fails. Tune the thirteen, never the sheets.
53
+ 3. `node modules/ui-design/kit/check-mock.js --file modules/ui-design/styles/<name>/mock.html --pack <name>` until ALL RULES PASS.
54
+ 4. `node modules/ui-design/kit/render.js --page / --style <name>` and look at the shots; the hall's `/builders/ranks` shows the light tier.
55
+ 5. Add the row above, the line in the module's `CLAUDE.md` and in `/design`. Ship under a claimed task.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: The Chrome World
3
+ description: One iridescent chrome sculpture in a black void, one warm accent that lights only what is pressable or live. The canonical entry of the style library; its recipes are the repo-root DESIGN.md.
4
+ colors:
5
+ # ---- contract tier, LIGHT: the thirteen this look's pack.json carries, value for value
6
+ # (tests/ui_design_styles.mjs holds the two in lockstep). The other two host-overridable
7
+ # tokens are the font stacks, under `typography`.
8
+ bg: "#edecee"
9
+ bg-card: "#fbfafc"
10
+ bg-deep: "#e2e1e4"
11
+ bg-frame: "#f6f5f7"
12
+ ink: "#17181a"
13
+ ink-soft: "#55565a"
14
+ ink-faint: "#65656a"
15
+ rule: "#c6c6c8"
16
+ rule-soft: "#dcdbdd"
17
+ accent: "#fb9569"
18
+ accent-deep: "#a3481a"
19
+ sea: "#5b6fd0"
20
+ terracotta: "#c9512c"
21
+ typography:
22
+ display: { fontFamily: "Manrope, Segoe UI, system-ui, -apple-system, sans-serif", fontSize: "clamp(34px, 6.7vw, 96px)", fontWeight: 300, lineHeight: 0.92, letterSpacing: "-0.042em", textTransform: "lowercase" }
23
+ body: { fontFamily: "Manrope, Segoe UI, system-ui, -apple-system, sans-serif", fontSize: "17px", fontWeight: 400, lineHeight: 1.5 }
24
+ label: { fontFamily: "JetBrains Mono, ui-monospace, Cascadia Mono, Consolas, monospace", fontSize: "11px", fontWeight: 400, letterSpacing: "0.12em", textTransform: "uppercase" }
25
+ rounded:
26
+ pill: "999px"
27
+ panel: "16px"
28
+ tile: "10px"
29
+ ---
30
+
31
+ # Style: The Chrome World (the canonical look)
32
+
33
+ This is the look Cloud Bongos itself runs on and the **first entry** of the style library ([task 1003323](https://cloudbongos.com/builders#/task/1003323)). It is the canonical one in a precise sense: its `pack.json` is byte-for-byte the `theme.ui` block of [`config/branding.neutral.json`](../../../../config/branding.neutral.json), the neutral pack every instance resolves over, and its recipes are **not restated here**. The world's derived tiers, faces, materials, motion grammar, components, Do's and Don'ts and accessibility floors are the repo-root [`DESIGN.md`](../../../../DESIGN.md), recorded from the built landing ([ADR 0196](../../../../docs/adr/<redacted>.md)). Read that file; this one only holds the thirteen so the catalogue's lockstep test has something to hold.
34
+
35
+ ## What this entry is for
36
+
37
+ - **The reference every alternate is measured against.** An alternate is a different set of the same fifteen tokens laid through the same sheets; the chrome world is what those sheets were built for, so `STYLE=chrome-world` on the kit's stub is the same render as no `STYLE=` at all on a neutral instance.
38
+ - **The specimen.** [`mock.html`](mock.html) beside this file is the world's specimen sheet (the hairline bar, the lowercase display voice, the door pill, the lifecycle rail, three ledger rows), served through the stub with the pack spliced in:
39
+
40
+ ```
41
+ SURFACE=modules/ui-design/styles/chrome-world STYLE=chrome-world node modules/ui-design/kit/serve.js
42
+ # http://127.0.0.1:3412/mock (?mode=light · ?state=signed-in)
43
+ ```
44
+
45
+ ## Do's and Don'ts
46
+
47
+ The root `DESIGN.md`'s, restated here only so the kit reads the look's bans from this dir (`check-mock.js --pack chrome-world`); the root file is the authority.
48
+
49
+ ### Don't
50
+
51
+ - **Don't** use an em dash or en dash in visible copy, an emoji, gradient text, a coloured text glow, or a box shadow under content.
52
+ - **Don't** declare a `:root` in a page, pin the accent in a dark block, or spend the accent on anything that is not pressable or live.
53
+
54
+ ## Adopting it
55
+
56
+ An instance that starts from the neutral pack already has this look; there is nothing to copy. An instance that adopted an alternate and wants back: copy this `pack.json`'s `theme` into its `config/branding.json` (or delete the `theme.ui` override so the neutral values resolve). No page changes: the sheets read the contract and nothing else.