@bongos/core 1.19.395 → 1.19.397
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 +139 -24
- package/.claude/skills/design/SKILL.md +1 -1
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +4 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +10 -8
- package/docs/copy-registry.json +29 -11
- package/docs/file-map.md +1 -1
- package/docs/recipes/ui-look-before-you-ship.md +113 -0
- package/modules/hall-ui/public/my-projects.css +22 -0
- package/modules/hall-ui/public/my-projects.html +7 -1
- package/modules/hall-ui/public/my-projects.js +17 -11
- package/modules/hall-ui/public/ranks.states.json +15 -0
- package/modules/platform-identity/routes/my-projects.js +50 -3
- package/modules/ui-design/kit/README.md +12 -2
- package/modules/ui-design/kit/check-mock.js +147 -0
- package/modules/ui-design/kit/contrast.js +156 -0
- package/modules/ui-design/kit/fixtures/cost-summary.json +1 -0
- package/modules/ui-design/kit/fixtures/featured.json +1 -0
- package/modules/ui-design/kit/fixtures/leaderboard.json +1 -0
- package/modules/ui-design/kit/fixtures/me.json +9 -0
- package/modules/ui-design/kit/fixtures/my-projects.json +1 -0
- package/modules/ui-design/kit/fixtures/progress.json +1 -0
- package/modules/ui-design/kit/fixtures/recent-shipped.json +1 -0
- package/modules/ui-design/kit/lib.js +389 -0
- package/modules/ui-design/kit/png.js +51 -0
- package/modules/ui-design/kit/probe.js +100 -0
- package/modules/ui-design/kit/render.js +137 -0
- package/modules/ui-design/kit/serve.js +178 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/index.probes.json +37 -0
- package/public-landing/index.states.json +16 -0
- package/public-landing/projects.probes.json +79 -0
- package/public-landing/projects.states.json +25 -0
- package/scripts/gds/ui-kit-transform.js +43 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +5 -1
- package/tests/join_application_note.mjs +112 -0
- package/tests/ui_design_kit.mjs +217 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// render.js — render a page's states and audit every render against the platform
|
|
4
|
+
// floors (task 1003320, ADR 0197). The states live in a JSON file BESIDE the
|
|
5
|
+
// surface (<dir>/<page>.states.json: name → url + actions + expectations); each
|
|
6
|
+
// is rendered in dark + light at 1440x900 / 390x844 / 320x700 through two stubs
|
|
7
|
+
// this file starts itself (signed out, signed in), then a reduced-motion pass per
|
|
8
|
+
// mode. Writes the shots + a JSON report; exits non-zero on any problem.
|
|
9
|
+
//
|
|
10
|
+
// node modules/ui-design/kit/render.js --page /projects (public-landing/projects.states.json)
|
|
11
|
+
// node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
|
|
12
|
+
// node modules/ui-design/kit/render.js --states <file> (explicit file)
|
|
13
|
+
// --modes dark,light --sizes 1440,390,320 --only a,b --out <dir>
|
|
14
|
+
// --base-out <url> --base-in <url> use running stubs instead of starting two
|
|
15
|
+
//
|
|
16
|
+
// Exit 0 ALL CLEAN · 1 problems · 2 usage/contract · 3 no browser.
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const lib = require('./lib.js');
|
|
21
|
+
|
|
22
|
+
function usage(msg) {
|
|
23
|
+
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`);
|
|
24
|
+
process.exit(lib.EXIT.USAGE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Resolve the states file + surface from the argv (pure over the filesystem; the
|
|
28
|
+
// test drives the failure paths through the CLI's exit code).
|
|
29
|
+
function locate(args) {
|
|
30
|
+
const explicit = lib.argOf(args, '--states', null);
|
|
31
|
+
let file = explicit && path.resolve(explicit);
|
|
32
|
+
let json;
|
|
33
|
+
if (file) {
|
|
34
|
+
if (!fs.existsSync(file)) usage(`states file not found: ${file}`);
|
|
35
|
+
try { json = lib.readJson(file); } catch (e) { usage(`states file is not JSON: ${e.message}`); }
|
|
36
|
+
} else {
|
|
37
|
+
const page = lib.normalizePageArg(lib.argOf(args, '--page', null));
|
|
38
|
+
if (!page) usage('--page </path> or --states <file> is required');
|
|
39
|
+
const surfaceKey = lib.argOf(args, '--surface', 'public-landing');
|
|
40
|
+
const dir = lib.resolveSurfaceDir(surfaceKey);
|
|
41
|
+
if (!dir) usage(`--surface "${surfaceKey}" is neither a checkout dir nor a module key with a webSurfaces dir`);
|
|
42
|
+
file = lib.sidecarPath(dir, page, 'states');
|
|
43
|
+
if (!fs.existsSync(file)) usage(`no states file beside the surface: ${file} — write one (see docs/recipes/ui-look-before-you-ship.md)`);
|
|
44
|
+
try { json = lib.readJson(file); } catch (e) { usage(`states file is not JSON: ${e.message}`); }
|
|
45
|
+
}
|
|
46
|
+
const v = lib.validateStatesFile(json);
|
|
47
|
+
if (!v.ok) usage(`${path.relative(lib.REPO_ROOT, file)} breaks the states contract:\n - ${v.errors.join('\n - ')}`);
|
|
48
|
+
const surface = json.surface || lib.argOf(args, '--surface', null) || path.relative(lib.REPO_ROOT, path.dirname(file)).split(path.sep).join('/');
|
|
49
|
+
return { file, json, surface };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Runs IN THE PAGE after the reduced-motion context loads + acts: what still moves?
|
|
53
|
+
async function reducedMotionFacts(page) {
|
|
54
|
+
return page.evaluate(async () => {
|
|
55
|
+
const running = document.getAnimations().filter((a) => a.playState === 'running').map((a) => a.animationName || a.constructor.name);
|
|
56
|
+
const orig = window.requestAnimationFrame; let frames = 0;
|
|
57
|
+
window.requestAnimationFrame = function (cb) { frames += 1; return orig.call(window, cb); };
|
|
58
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
59
|
+
window.requestAnimationFrame = orig;
|
|
60
|
+
return { running, frames };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
const args = process.argv.slice(2);
|
|
66
|
+
if (lib.hasFlag(args, '--help')) usage('');
|
|
67
|
+
const { file, json, surface } = locate(args);
|
|
68
|
+
const modes = lib.listArg(args, '--modes', [...lib.MODES]).filter((m) => lib.MODES.includes(m));
|
|
69
|
+
const sizes = lib.listArg(args, '--sizes', Object.keys(lib.SIZES)).map(Number).filter((w) => lib.SIZES[w]);
|
|
70
|
+
const only = lib.listArg(args, '--only', null);
|
|
71
|
+
const names = Object.keys(json.states).filter((n) => !only || only.includes(n));
|
|
72
|
+
if (!names.length) usage(`--only names no state of ${path.basename(file)}`);
|
|
73
|
+
const out = path.resolve(lib.argOf(args, '--out', path.join(os.tmpdir(), 'ui-design-kit', surface.replace(/[\\/]/g, '_'), lib.pageName(json.page))));
|
|
74
|
+
fs.mkdirSync(out, { recursive: true });
|
|
75
|
+
|
|
76
|
+
const stubs = [];
|
|
77
|
+
let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
|
|
78
|
+
const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child };
|
|
79
|
+
if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
|
|
80
|
+
if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
|
|
81
|
+
const stop = () => stubs.forEach((s) => s.stop());
|
|
82
|
+
|
|
83
|
+
let browser;
|
|
84
|
+
try { browser = await lib.launchBrowser(); }
|
|
85
|
+
catch (e) { stop(); process.stderr.write(`render: ${e.message}\n`); process.exit(e instanceof lib.NoBrowserError ? lib.EXIT.NO_BROWSER : lib.EXIT.USAGE); }
|
|
86
|
+
|
|
87
|
+
const report = { page: json.page, surface, out, renders: {}, reducedMotion: {} };
|
|
88
|
+
const problems = [];
|
|
89
|
+
const collectorOpts = { ignoreRequests: json.ignoreRequests, ignoreConsole: json.ignoreConsole };
|
|
90
|
+
try {
|
|
91
|
+
for (const mode of modes) for (const w of sizes) for (const name of names) {
|
|
92
|
+
const st = json.states[name];
|
|
93
|
+
const [W, H] = lib.SIZES[w];
|
|
94
|
+
const ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1, colorScheme: mode });
|
|
95
|
+
const page = await ctx.newPage();
|
|
96
|
+
const logs = lib.attachCollectors(page, collectorOpts);
|
|
97
|
+
const base = st.auth ? baseIn : baseOut;
|
|
98
|
+
await page.goto(base + lib.withMode(st.url || json.page, mode, json.modeQuery), { waitUntil: 'load' });
|
|
99
|
+
await page.evaluate(() => document.fonts.ready);
|
|
100
|
+
await page.waitForTimeout(lib.SETTLE_MS);
|
|
101
|
+
let actionFailed = null;
|
|
102
|
+
try { await lib.runActions(page, st.actions); } catch (e) { actionFailed = String(e.message).split('\n')[0]; logs.push('action failed: ' + actionFailed); }
|
|
103
|
+
const facts = await page.evaluate(lib.factsInPage, { expect: st.expect, ignoreControls: json.ignoreControls });
|
|
104
|
+
const found = [...logs, ...lib.auditFloors(facts, { width: W, mode }), ...lib.expectProblems(facts, st.expect)];
|
|
105
|
+
const key = `${mode}-${w}-${name}`;
|
|
106
|
+
await page.screenshot({ path: path.join(out, key + '.png') });
|
|
107
|
+
report.renders[key] = { facts, problems: found };
|
|
108
|
+
process.stdout.write(`${mode} ${w} ${name}: ${found.length ? 'PROBLEMS' : 'clean'} bar ${facts.bar ? facts.bar.h : '-'} expanded [${facts.expanded.join('|')}] anims ${facts.running} focus ${facts.activeId || facts.activeText || '-'}\n`);
|
|
109
|
+
for (const f of found) { process.stdout.write(` ${f}\n`); problems.push(`${key}: ${f}`); }
|
|
110
|
+
await ctx.close();
|
|
111
|
+
}
|
|
112
|
+
// reduced motion: the kill switch, exercised with the file's own interaction
|
|
113
|
+
const rm = json.reducedMotion || {};
|
|
114
|
+
for (const mode of modes) {
|
|
115
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: mode, reducedMotion: 'reduce' });
|
|
116
|
+
const page = await ctx.newPage();
|
|
117
|
+
await page.goto((rm.auth === false ? baseOut : baseIn) + lib.withMode(rm.url || json.page, mode, json.modeQuery), { waitUntil: 'load' });
|
|
118
|
+
await page.waitForTimeout(lib.SETTLE_MS);
|
|
119
|
+
try { await lib.runActions(page, rm.actions); } catch (e) { problems.push(`${mode} reduced-motion: action failed: ${String(e.message).split('\n')[0]}`); }
|
|
120
|
+
const facts = await reducedMotionFacts(page);
|
|
121
|
+
const found = lib.reducedMotionProblems(facts);
|
|
122
|
+
report.reducedMotion[mode] = { ...facts, problems: found };
|
|
123
|
+
process.stdout.write(`${mode} reduced-motion: ${found.length ? 'ALIVE' : 'dead'} running ${facts.running.length} rAF ${facts.frames}\n`);
|
|
124
|
+
for (const f of found) { process.stdout.write(` ${f}\n`); problems.push(`${mode}: ${f}`); }
|
|
125
|
+
await ctx.close();
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
await browser.close();
|
|
129
|
+
stop();
|
|
130
|
+
}
|
|
131
|
+
report.problems = problems;
|
|
132
|
+
fs.writeFileSync(path.join(out, 'report.json'), JSON.stringify(report, null, 1));
|
|
133
|
+
process.stdout.write(`\nshots + report.json → ${out}\n${problems.length ? `${problems.length} PROBLEM(S)` : 'ALL CLEAN'}\n`);
|
|
134
|
+
process.exit(problems.length ? lib.EXIT.PROBLEMS : lib.EXIT.CLEAN);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main().catch((e) => { process.stderr.write(`render: ${e.stack || e.message}\n`); process.exit(lib.EXIT.USAGE); });
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// serve.js — the look-before-you-ship stub (task 1003320, ADR 0197): serves ANY
|
|
4
|
+
// surface directory of the checkout through the instance's REAL serving
|
|
5
|
+
// transforms — the apex-only strip, the brand-token stamp and the pack's theme
|
|
6
|
+
// injection, run by scripts/gds/ui-kit-transform.js (a core-side command: a
|
|
7
|
+
// module may not require core and the doorway may not import the serving domain,
|
|
8
|
+
// so the real code path is reached as a child process) — plus the content-hash
|
|
9
|
+
// ?v= stamp, with the instance's own branding pack. Answers the public feeds from
|
|
10
|
+
// fixtures/ so a page boots with real-shaped data and no database. Serves docs/
|
|
11
|
+
// raw so a mock under docs/design/mocks/ renders with its relative links.
|
|
12
|
+
//
|
|
13
|
+
// node modules/ui-design/kit/serve.js -> http://127.0.0.1:3412/ (public-landing)
|
|
14
|
+
// SURFACE=<dir or module key> a checkout dir ("public-landing", "modules/hall-ui/public") or a
|
|
15
|
+
// module key whose manifest declares a webSurfaces dir
|
|
16
|
+
// PREFIX=/builders/ the URL prefix the surface's pages + assets are served under
|
|
17
|
+
// STUB_REPO=<checkout> the checkout to serve (default: the one this file sits in)
|
|
18
|
+
// STUB_PORT=<n> default 3412 (0 = any free port; the port is printed)
|
|
19
|
+
// AUTH=1 /api/<ns>/me answers the fixture builder — { builder }, the real
|
|
20
|
+
// route's shape; my-projects + provisioning answer too
|
|
21
|
+
// CHILD=1 the child-instance shape: apex-only regions stripped
|
|
22
|
+
// LIVE=1 | LIVE=<origin> proxy the public feeds to the pack's public origin (or the named one)
|
|
23
|
+
// FAIL=<feed,feed> those feeds answer 500 (the names in FEEDS below)
|
|
24
|
+
//
|
|
25
|
+
// Exit 2 on a surface that does not resolve. Prints ONE line when listening
|
|
26
|
+
// ("stub on http://…") — render.js and probe.js wait for it.
|
|
27
|
+
const http = require('node:http');
|
|
28
|
+
const https = require('node:https');
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const crypto = require('node:crypto');
|
|
32
|
+
const { spawnSync } = require('node:child_process');
|
|
33
|
+
const lib = require('./lib.js');
|
|
34
|
+
|
|
35
|
+
const ROOT = process.env.STUB_REPO ? path.resolve(process.env.STUB_REPO) : lib.REPO_ROOT;
|
|
36
|
+
const SURFACE = process.env.SURFACE || 'public-landing';
|
|
37
|
+
const PREFIX = process.env.PREFIX || '/';
|
|
38
|
+
const PORT = process.env.STUB_PORT === undefined ? 3412 : Number(process.env.STUB_PORT);
|
|
39
|
+
const AUTH = process.env.AUTH === '1';
|
|
40
|
+
const CHILD = process.env.CHILD === '1';
|
|
41
|
+
const LIVE = process.env.LIVE || '';
|
|
42
|
+
const FAIL = new Set(String(process.env.FAIL || '').split(',').map((s) => s.trim()).filter(Boolean));
|
|
43
|
+
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
44
|
+
const DOCS = path.join(ROOT, 'docs');
|
|
45
|
+
const TRANSFORM = path.join(ROOT, 'scripts', 'gds', 'ui-kit-transform.js');
|
|
46
|
+
|
|
47
|
+
const DIR = lib.resolveSurfaceDir(SURFACE, ROOT);
|
|
48
|
+
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); }
|
|
49
|
+
if (!PREFIX.startsWith('/') || !PREFIX.endsWith('/')) { process.stderr.write('serve: PREFIX must start and end with "/"\n'); process.exit(lib.EXIT.USAGE); }
|
|
50
|
+
if (!fs.existsSync(TRANSFORM)) { process.stderr.write(`serve: the transform command is missing: ${TRANSFORM}\n`); process.exit(lib.EXIT.USAGE); }
|
|
51
|
+
|
|
52
|
+
const MIME = {
|
|
53
|
+
'.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript', '.mjs': 'application/javascript',
|
|
54
|
+
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp',
|
|
55
|
+
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2',
|
|
56
|
+
'.txt': 'text/plain; charset=utf-8', '.md': 'text/markdown; charset=utf-8', '.mmd': 'text/plain; charset=utf-8',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// The real transforms, as the instance runs them (see ui-kit-transform.js).
|
|
60
|
+
function transform(file) {
|
|
61
|
+
const r = spawnSync(process.execPath, [TRANSFORM, path.join(DIR, file), ...(CHILD ? ['--child'] : []), '--json'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
62
|
+
if (r.status !== 0) throw new Error(`ui-kit-transform exited ${r.status}: ${(r.stderr || r.stdout || '').trim().slice(0, 600)}`);
|
|
63
|
+
return JSON.parse(r.stdout);
|
|
64
|
+
}
|
|
65
|
+
let liveOrigin = LIVE;
|
|
66
|
+
// The content-hash ?v= stamp, by RESOLUTION (a URL is an asset if it names a file
|
|
67
|
+
// under the surface dir) — the shape platform-server.js and asset-version.js stamp
|
|
68
|
+
// with; the stub knows the dir + prefix, so it stamps here, LAST (their order).
|
|
69
|
+
const LOCAL_ASSET = /(href|src)="(\/[A-Za-z0-9._~/-]+\.[A-Za-z0-9]+)"/g;
|
|
70
|
+
function stampAssets(html) {
|
|
71
|
+
return String(html).replace(LOCAL_ASSET, (m, attr, url) => {
|
|
72
|
+
const rel = url.startsWith(PREFIX) ? url.slice(PREFIX.length) : url.replace(/^\//, '');
|
|
73
|
+
const abs = path.resolve(DIR, rel);
|
|
74
|
+
if (!abs.startsWith(DIR + path.sep)) return m;
|
|
75
|
+
try { return `${attr}="${url}?v=${crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex').slice(0, 8)}"`; }
|
|
76
|
+
catch { return m; }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const pages = new Map();
|
|
80
|
+
function page(file) {
|
|
81
|
+
if (!pages.has(file)) {
|
|
82
|
+
const t = transform(file);
|
|
83
|
+
if (LIVE === '1' && !liveOrigin.startsWith('http')) liveOrigin = t.publicOrigin || '';
|
|
84
|
+
pages.set(file, { html: stampAssets(t.html), brand: t.brand });
|
|
85
|
+
}
|
|
86
|
+
return pages.get(file);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The feeds a surface may read on boot → the fixture that answers. Both API
|
|
90
|
+
// namespaces the instance mounts resolve to the same fixture.
|
|
91
|
+
const FEEDS = {
|
|
92
|
+
'/public/progress': 'progress',
|
|
93
|
+
'/public/cost-summary': 'cost-summary',
|
|
94
|
+
'/public/recent-shipped': 'recent-shipped',
|
|
95
|
+
'/public/leaderboard': 'leaderboard',
|
|
96
|
+
'/projects/featured': 'featured',
|
|
97
|
+
};
|
|
98
|
+
const AUTHED = { '/my-projects': 'my-projects' };
|
|
99
|
+
const UNAUTH = { error: { code: 'unauthenticated', message: 'unauthenticated' } };
|
|
100
|
+
|
|
101
|
+
function json(res, status, body) { res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(body)); }
|
|
102
|
+
function fixture(name, res) {
|
|
103
|
+
if (FAIL.has(name)) return json(res, 500, { error: { code: 'stub_failure', message: `FAIL=${name}` } });
|
|
104
|
+
const file = path.join(FIXTURES, name + '.json');
|
|
105
|
+
if (!fs.existsSync(file)) return json(res, 404, { error: { code: 'no_fixture', message: name } });
|
|
106
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
107
|
+
res.end(fs.readFileSync(file));
|
|
108
|
+
}
|
|
109
|
+
function proxy(req, res) {
|
|
110
|
+
const origin = (liveOrigin === '1' ? '' : liveOrigin).replace(/\/$/, '');
|
|
111
|
+
if (!origin) return json(res, 502, { error: { code: 'no_live_origin', message: 'LIVE=1 needs a pack with domains.publicOrigin; or LIVE=<origin>' } });
|
|
112
|
+
const target = origin + req.url;
|
|
113
|
+
(target.startsWith('https:') ? https : http).get(target, { headers: { accept: 'application/json' } }, (up) => {
|
|
114
|
+
res.writeHead(up.statusCode, { 'content-type': up.headers['content-type'] || 'application/json' });
|
|
115
|
+
up.pipe(res);
|
|
116
|
+
}).on('error', (e) => json(res, 502, { error: { code: 'proxy_failed', message: e.message } }));
|
|
117
|
+
}
|
|
118
|
+
function sendFile(abs, res) {
|
|
119
|
+
res.writeHead(200, { 'content-type': MIME[path.extname(abs).toLowerCase()] || 'application/octet-stream', 'cache-control': 'no-store' });
|
|
120
|
+
fs.createReadStream(abs).pipe(res);
|
|
121
|
+
}
|
|
122
|
+
function fileUnder(base, rel) {
|
|
123
|
+
const abs = path.resolve(base, '.' + '/' + rel.replace(/^\/+/, ''));
|
|
124
|
+
return abs.startsWith(base + path.sep) && fs.existsSync(abs) && fs.statSync(abs).isFile() ? abs : null;
|
|
125
|
+
}
|
|
126
|
+
// The fixture builder's avatar is served by this stub (a page accepts only an
|
|
127
|
+
// http(s) avatar and sizes its chip by the image), so a root-relative avatar_url
|
|
128
|
+
// is absolutised to this origin per request.
|
|
129
|
+
const AVATAR_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><circle cx="32" cy="32" r="32" fill="#888"/><circle cx="32" cy="26" r="11" fill="#ddd"/><path d="M12 56c3-12 11-17 20-17s17 5 20 17" fill="#ddd"/></svg>';
|
|
130
|
+
function me(origin) {
|
|
131
|
+
const m = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'me.json'), 'utf8'));
|
|
132
|
+
if (m.builder && typeof m.builder.avatar_url === 'string' && m.builder.avatar_url.startsWith('/')) m.builder.avatar_url = origin + m.builder.avatar_url;
|
|
133
|
+
return m;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const server = http.createServer((req, res) => {
|
|
137
|
+
const u = new URL(req.url, 'http://x');
|
|
138
|
+
const p = u.pathname;
|
|
139
|
+
const origin = `http://127.0.0.1:${server.address().port}`;
|
|
140
|
+
if (p === '/__kit/avatar.svg') { res.writeHead(200, { 'content-type': MIME['.svg'], 'cache-control': 'no-store' }); return res.end(AVATAR_SVG); }
|
|
141
|
+
// ── the API: /api/gds/* and /api/bongos/* are one surface on the instance ──
|
|
142
|
+
const apiMatch = p.match(/^\/api\/(gds|bongos)(\/.*)$/);
|
|
143
|
+
if (apiMatch) {
|
|
144
|
+
const rest = apiMatch[2];
|
|
145
|
+
if (FEEDS[rest]) return LIVE ? proxy(req, res) : fixture(FEEDS[rest], res);
|
|
146
|
+
if (rest === '/me') return AUTH ? json(res, 200, me(origin)) : json(res, 401, UNAUTH);
|
|
147
|
+
if (AUTHED[rest]) return AUTH ? fixture(AUTHED[rest], res) : json(res, 401, UNAUTH);
|
|
148
|
+
if (rest === '/provisioning/instances') return AUTH ? json(res, 200, { instances: [] }) : json(res, 401, UNAUTH);
|
|
149
|
+
if (rest === '/auth/logout') return json(res, 200, { ok: true });
|
|
150
|
+
if (rest.startsWith('/auth/web/start')) { res.writeHead(302, { location: PREFIX }); return res.end(); }
|
|
151
|
+
if (rest === '/instance') { const b = pages.size ? pages.values().next().value.brand : {}; return json(res, 200, { brand: { productName: (b && b.productName) || '', project: {} }, modules: {} }); }
|
|
152
|
+
return json(res, 404, { error: { code: 'not_found', message: p } });
|
|
153
|
+
}
|
|
154
|
+
// the generated client every internal page loads (the real bundle when present)
|
|
155
|
+
if (/\/lib\/bongos-client\.js$/.test(p)) {
|
|
156
|
+
const bundle = fileUnder(ROOT, 'clients/bongos-client/bongos-client.global.js');
|
|
157
|
+
res.writeHead(200, { 'content-type': MIME['.js'], 'cache-control': 'no-store' });
|
|
158
|
+
return res.end(bundle ? fs.readFileSync(bundle, 'utf8') : '/* stub: no client bundle in this checkout */');
|
|
159
|
+
}
|
|
160
|
+
// docs/ raw (a mock's relative links resolve) and the surface dir by its checkout path
|
|
161
|
+
if (p.startsWith('/docs/')) { const abs = fileUnder(DOCS, p.slice('/docs/'.length)); if (abs) return sendFile(abs, res); }
|
|
162
|
+
const relSurface = path.relative(ROOT, DIR).split(path.sep).join('/');
|
|
163
|
+
if (p.startsWith('/' + relSurface + '/')) { const abs = fileUnder(DIR, p.slice(relSurface.length + 2)); if (abs) return sendFile(abs, res); }
|
|
164
|
+
// ── the surface: pages by name under PREFIX (and at the root), assets by path ──
|
|
165
|
+
const under = p.startsWith(PREFIX) ? p.slice(PREFIX.length) : (PREFIX !== '/' && (p === PREFIX.slice(0, -1)) ? '' : p.replace(/^\//, ''));
|
|
166
|
+
const asset = fileUnder(DIR, under);
|
|
167
|
+
if (asset && !asset.endsWith('.html')) return sendFile(asset, res);
|
|
168
|
+
const name = under === '' ? 'index' : under.replace(/\/$/, '').replace(/\.html$/, '');
|
|
169
|
+
if (!name.includes('/') && fs.existsSync(path.join(DIR, name + '.html'))) {
|
|
170
|
+
try { res.writeHead(200, { 'content-type': MIME['.html'], 'cache-control': 'no-store' }); return res.end(page(name + '.html').html); }
|
|
171
|
+
catch (e) { res.writeHead(500, { 'content-type': 'text/plain' }); return res.end('render failed: ' + e.stack); }
|
|
172
|
+
}
|
|
173
|
+
res.writeHead(404, { 'content-type': 'text/plain' }); res.end('404 ' + p);
|
|
174
|
+
});
|
|
175
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
176
|
+
const port = server.address().port;
|
|
177
|
+
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`);
|
|
178
|
+
});
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.397",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.397",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.397",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The landing's interaction contract for modules/ui-design/kit/probe.js (task 1003320): the same disclosure component as the hub, on the front page.",
|
|
3
|
+
"page": "/",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"probes": [
|
|
6
|
+
{ "name": "landing: Enter opens Projects on the first item", "auth": true, "width": 1440,
|
|
7
|
+
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150]],
|
|
8
|
+
"expect": { "open": "#menuProjects", "activeText": "Sky" } },
|
|
9
|
+
{ "name": "landing: arrows walk the visible items (Your projects shows signed in)", "continue": true,
|
|
10
|
+
"steps": [["press", "ArrowDown"], ["press", "ArrowDown"]],
|
|
11
|
+
"expect": { "activeText": "Your projects" } },
|
|
12
|
+
{ "name": "landing: Escape closes and returns focus", "continue": true,
|
|
13
|
+
"steps": [["press", "Escape"], ["wait", 100]],
|
|
14
|
+
"expect": { "closed": "#menuProjects", "activeId": "projTrig" } },
|
|
15
|
+
{ "name": "landing: the profile band closes the Projects band", "continue": true,
|
|
16
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "#who .trig"], ["wait", 120]],
|
|
17
|
+
"expect": { "open": "#menuProfile", "closed": "#menuProjects" } },
|
|
18
|
+
{ "name": "landing: the profile band names the person; sign out keeps the honest href", "continue": true,
|
|
19
|
+
"steps": [],
|
|
20
|
+
"expect": { "href": [["#menuProfile a.signout", "/api/bongos/auth/logout"]], "text": [["#bandWho", "builder"]], "count": [["#bandWho img", 1]] } },
|
|
21
|
+
{ "name": "landing: a click outside closes", "continue": true,
|
|
22
|
+
"steps": [["clickAt", 700, 700], ["wait", 120]],
|
|
23
|
+
"expect": { "closed": "#menuProfile", "expanded": false } },
|
|
24
|
+
{ "name": "landing: the band deep-links the hub views + About + Docs", "continue": true,
|
|
25
|
+
"steps": [["click", "#projTrig"], ["wait", 120]],
|
|
26
|
+
"expect": { "count": [["#menuProjects a", 6]], "href": [["#menuProjects .bandCell:nth-child(1) a", "/projects?view=map"], ["#menuProjects .bandCell:nth-child(2) a", "/projects?view=map&sub=table"], ["#menuProjects a[href=\"#about\"]", "#about"], ["#menuProjects a[href=\"/docs\"]", "/docs"]] } },
|
|
27
|
+
{ "name": "landing: #about exists", "continue": true,
|
|
28
|
+
"steps": [["press", "Escape"]],
|
|
29
|
+
"expect": { "count": [["#about", 1]] } },
|
|
30
|
+
{ "name": "landing signed out: the door, no chip, no Your-projects cell", "auth": false, "width": 1440,
|
|
31
|
+
"steps": [],
|
|
32
|
+
"expect": { "visible": ["#who a.signin"], "count": [["#who .trig", 0]], "hidden": ["#mineCell"] } },
|
|
33
|
+
{ "name": "landing signed out: nothing throws", "continue": true,
|
|
34
|
+
"steps": [["click", "#projTrig"], ["wait", 100], ["press", "Escape"], ["wait", 100]],
|
|
35
|
+
"expect": { "closed": "#menuProjects" } }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The landing's states for modules/ui-design/kit/render.js (task 1003320): the bar and its two bands, signed out and in.",
|
|
3
|
+
"page": "/",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modeQuery": "mode",
|
|
6
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/provisioning/"],
|
|
7
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
|
|
8
|
+
"ignoreControls": [".skip", ".vh"],
|
|
9
|
+
"states": {
|
|
10
|
+
"out": { "auth": false, "expect": { "expanded": false, "visible": ["#who a.signin"], "hidden": ["#mineCell"] } },
|
|
11
|
+
"in": { "auth": true, "expect": { "expanded": false, "visible": ["#who .trig"] } },
|
|
12
|
+
"out-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProjects", "count": [["#menuProjects [aria-current]", 0]] } },
|
|
13
|
+
"in-profile": { "auth": true, "actions": [["click", "#who .trig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProfile" } }
|
|
14
|
+
},
|
|
15
|
+
"reducedMotion": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 100]] }
|
|
16
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The hub's interaction contract for modules/ui-design/kit/probe.js (task 1003320): the disclosure bands, the views, the card, the fold under 480, signed out. A probe with `continue` keeps the previous page — the contract is a sequence.",
|
|
3
|
+
"page": "/projects",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"probes": [
|
|
6
|
+
{ "name": "Enter opens Projects and lands on the first item", "auth": true, "width": 1440,
|
|
7
|
+
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150]],
|
|
8
|
+
"expect": { "open": "#menuProjects", "activeText": "Sky" } },
|
|
9
|
+
{ "name": "ArrowDown walks to Table", "continue": true,
|
|
10
|
+
"steps": [["press", "ArrowDown"]],
|
|
11
|
+
"expect": { "activeText": "Table" } },
|
|
12
|
+
{ "name": "ArrowUp wraps to the last item (Docs, the About/Docs row)", "continue": true,
|
|
13
|
+
"steps": [["press", "ArrowUp"], ["press", "ArrowUp"]],
|
|
14
|
+
"expect": { "activeText": "Docs" } },
|
|
15
|
+
{ "name": "Escape closes and returns focus to the trigger", "continue": true,
|
|
16
|
+
"steps": [["press", "Escape"], ["wait", 100]],
|
|
17
|
+
"expect": { "closed": "#menuProjects", "activeId": "projTrig", "expanded": false } },
|
|
18
|
+
{ "name": "opening the profile band closes the Projects band", "continue": true,
|
|
19
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "#who"], ["wait", 120]],
|
|
20
|
+
"expect": { "open": "#menuProfile", "closed": "#menuProjects", "expandedIds": ["who"] } },
|
|
21
|
+
{ "name": "a click outside closes", "continue": true,
|
|
22
|
+
"steps": [["clickAt", 700, 600], ["wait", 120]],
|
|
23
|
+
"expect": { "closed": "#menuProfile", "expanded": false } },
|
|
24
|
+
{ "name": "Tab out of the band closes it", "continue": true,
|
|
25
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["focus", "#menuProjects a[href=\"/docs\"]"], ["press", "Tab"], ["wait", 120]],
|
|
26
|
+
"expect": { "closed": "#menuProjects" } },
|
|
27
|
+
{ "name": "Table from the band: the table on stage, Table current, band closed", "continue": true,
|
|
28
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"table\"]"], ["wait", 300]],
|
|
29
|
+
"expect": { "visible": ["#sub-table-body"], "closed": "#menuProjects", "text": [["[aria-current=\"page\"]", "Table"]] } },
|
|
30
|
+
{ "name": "Sky from the band: the sky back on stage, Sky current", "continue": true,
|
|
31
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"sky\"]"], ["wait", 300]],
|
|
32
|
+
"expect": { "hidden": ["#sub-table-body"], "text": [["[aria-current=\"page\"]", "Sky"]] } },
|
|
33
|
+
{ "name": "Your projects: the search hides, the URL says mine", "continue": true,
|
|
34
|
+
"steps": [["fill", "#mapQ", "demo"], ["wait", 200], ["click", "#projTrig"], ["wait", 120], ["click", "a[data-nav=\"mine\"]"], ["wait", 300]],
|
|
35
|
+
"expect": { "hidden": ["#qWrap", "#qToggle"], "param": { "view": "mine" } } },
|
|
36
|
+
{ "name": "Back to the map restores the typed filter", "continue": true,
|
|
37
|
+
"steps": [["back"], ["wait", 400]],
|
|
38
|
+
"expect": { "visible": ["#qWrap"], "value": [["#mapQ", "demo"]] } },
|
|
39
|
+
{ "name": "the profile band names the person and keeps the honest sign-out href", "continue": true,
|
|
40
|
+
"steps": [["fill", "#mapQ", ""], ["wait", 200], ["click", "#who"], ["wait", 120]],
|
|
41
|
+
"expect": { "href": [["#menuProfile a.signout", "/api/bongos/auth/logout"]], "text": [["#bandWho", "builder"]], "count": [["#bandWho img", 1]] } },
|
|
42
|
+
{ "name": "My projects (profile band) switches the view and closes the band", "continue": true,
|
|
43
|
+
"steps": [["click", "#menuProfile a[data-nav-to=\"mine\"]"], ["wait", 300]],
|
|
44
|
+
"expect": { "param": { "view": "mine" }, "closed": "#menuProfile", "text": [["[aria-current=\"page\"]", "Your projects"]] } },
|
|
45
|
+
{ "name": "an orb opens the card", "continue": true,
|
|
46
|
+
"steps": [["back"], ["wait", 400], ["dispatchClick", "#orbField a.worldOrb:not(#selfOrb)"], ["wait", 400]],
|
|
47
|
+
"expect": { "dialog": true } },
|
|
48
|
+
{ "name": "Escape closes the card when no band is open", "continue": true,
|
|
49
|
+
"steps": [["press", "Escape"], ["wait", 150]],
|
|
50
|
+
"expect": { "dialog": false } },
|
|
51
|
+
{ "name": "a solo Shift tap focuses the search on the map", "continue": true,
|
|
52
|
+
"steps": [["clickAt", 700, 600], ["press", "Shift"], ["wait", 100]],
|
|
53
|
+
"expect": { "activeId": "mapQ" } },
|
|
54
|
+
{ "name": "the mode toggle: the label says what the press does, and the press does it", "continue": true,
|
|
55
|
+
"steps": [["clickAt", 700, 600], ["click", "#modeToggle"], ["wait", 100]],
|
|
56
|
+
"expect": { "attr": [["#modeToggle", "aria-label", "Switch to light mode"], ["html", "data-mode", "dark"]] } },
|
|
57
|
+
{ "name": "signed out: no chip, the door shows, no switch, the search stays", "auth": false, "width": 1440,
|
|
58
|
+
"steps": [],
|
|
59
|
+
"expect": { "hidden": ["#who", ".switch"], "visible": ["#signinTop", "#qWrap"] } },
|
|
60
|
+
{ "name": "signed out the Projects band is Sky / Table / New project + About / Docs (no Your projects)", "continue": true,
|
|
61
|
+
"steps": [["click", "#projTrig"], ["wait", 120]],
|
|
62
|
+
"expect": { "count": [["#menuProjects .bandCell:not([hidden])", 5]], "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
|
|
63
|
+
{ "name": "signed out: the card still closes on Escape (nothing throws with no session)", "continue": true,
|
|
64
|
+
"steps": [["press", "Escape"], ["wait", 100], ["dispatchClick", "#orbField a.worldOrb:not(#selfOrb)"], ["wait", 400], ["press", "Escape"], ["wait", 150]],
|
|
65
|
+
"expect": { "dialog": false, "closed": "#menuProjects" } },
|
|
66
|
+
{ "name": "390: the magnifier opens the row and focuses the field", "auth": true, "width": 390,
|
|
67
|
+
"steps": [["click", "#qToggle"], ["wait", 150]],
|
|
68
|
+
"expect": { "activeId": "mapQ", "hasClass": [["#qWrap", "is-open"]] } },
|
|
69
|
+
{ "name": "390: Escape folds the row and returns focus to the magnifier", "continue": true,
|
|
70
|
+
"steps": [["press", "Escape"], ["wait", 100]],
|
|
71
|
+
"expect": { "activeId": "qToggle", "hasClass": [["#qWrap", "is-open", false]] } },
|
|
72
|
+
{ "name": "390: an opening band folds the search", "continue": true,
|
|
73
|
+
"steps": [["click", "#qToggle"], ["wait", 100], ["click", "#projTrig"], ["wait", 150]],
|
|
74
|
+
"expect": { "hasClass": [["#qWrap", "is-open", false]], "open": "#menuProjects" } },
|
|
75
|
+
{ "name": "390: a solo Shift tap opens the fold and focuses the field, without overflow", "continue": true,
|
|
76
|
+
"steps": [["press", "Escape"], ["wait", 100], ["press", "Shift"], ["wait", 150]],
|
|
77
|
+
"expect": { "hasClass": [["#qWrap", "is-open", true]], "activeId": "mapQ", "noOverflow": true } }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The hub's states for modules/ui-design/kit/render.js (task 1003320): name → url + actions + expectations. `auth` picks the signed-in stub. Rendered dark + light at 1440/390/320 and audited against the platform floors.",
|
|
3
|
+
"page": "/projects",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modeQuery": "mode",
|
|
6
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/provisioning/"],
|
|
7
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
|
|
8
|
+
"ignoreControls": [".skip", ".vh"],
|
|
9
|
+
"states": {
|
|
10
|
+
"out-map": { "auth": false, "expect": { "expanded": false } },
|
|
11
|
+
"in-map": { "auth": true, "expect": { "expanded": false } },
|
|
12
|
+
"in-map-projects": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProjects" } },
|
|
13
|
+
"in-map-profile": { "auth": true, "actions": [["click", "#who"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProfile" } },
|
|
14
|
+
"in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
|
|
15
|
+
"in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
|
|
16
|
+
"in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
|
|
17
|
+
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body"], "closed": "#menuProjects" } },
|
|
18
|
+
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap"] } },
|
|
19
|
+
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
20
|
+
"out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
21
|
+
"out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
|
|
22
|
+
"card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb:not(#selfOrb)"], ["wait", 600]], "expect": { "dialog": true } }
|
|
23
|
+
},
|
|
24
|
+
"reducedMotion": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 100]] }
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// ui-kit-transform.js — the instance's serving transforms as a command (task
|
|
4
|
+
// 1003320, ADR 0197). Reads one surface page and prints it transformed exactly as
|
|
5
|
+
// serve-internal.js / platform-server.js serve it: the apex-only strip, the
|
|
6
|
+
// brand-token stamp, the pack's theme injection — with the instance's own
|
|
7
|
+
// branding pack.
|
|
8
|
+
//
|
|
9
|
+
// WHY a script: the ui-design kit's stub (modules/ui-design/kit/serve.js) must
|
|
10
|
+
// serve a page through the REAL code path, and a module may not require core
|
|
11
|
+
// (ADR 0083), while the doorway may not import the serving domain (fitness Check
|
|
12
|
+
// 12, ADR 0091 §1). A command under scripts/gds/ is outside both walls — the
|
|
13
|
+
// memory / ideas / sessions precedent: a command travels with its domain but
|
|
14
|
+
// stays under scripts/. The stub spawns this per page and memoises the answer.
|
|
15
|
+
//
|
|
16
|
+
// node scripts/gds/ui-kit-transform.js <page.html> [--child] [--json]
|
|
17
|
+
// --child the child-instance shape (apex-only regions removed)
|
|
18
|
+
// --json { html, brand, publicOrigin, hallOrigin } instead of the bare HTML
|
|
19
|
+
//
|
|
20
|
+
// Exit 2 on usage; the transformed page on stdout, nothing else.
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const file = args.find((a) => !a.startsWith('--'));
|
|
26
|
+
if (!file) { process.stderr.write('usage: node scripts/gds/ui-kit-transform.js <page.html> [--child] [--json]\n'); process.exit(2); }
|
|
27
|
+
const child = args.includes('--child');
|
|
28
|
+
|
|
29
|
+
const { injectBrandingGlobal, applyBrandTokens } = require('../../src/bongos/serve-internal');
|
|
30
|
+
const { stripApexOnly } = require('../../src/platform-server');
|
|
31
|
+
const branding = require('../../src/branding');
|
|
32
|
+
|
|
33
|
+
const brand = branding.clientBranding();
|
|
34
|
+
let domains = {};
|
|
35
|
+
try { domains = (typeof branding.branding === 'function' ? branding.branding() : {}).domains || {}; } catch { domains = {}; }
|
|
36
|
+
const publicOrigin = domains.publicOrigin || '';
|
|
37
|
+
const hallOrigin = child ? '' : (domains.buildersOrigin || '');
|
|
38
|
+
|
|
39
|
+
const raw = fs.readFileSync(path.resolve(file), 'utf8');
|
|
40
|
+
// platform-server's order for the landing: strip, then inject; the token stamp is
|
|
41
|
+
// what the hall/status surfaces get on the way through serve-internal.
|
|
42
|
+
const html = injectBrandingGlobal(applyBrandTokens(stripApexOnly(raw, !child), brand, publicOrigin, hallOrigin, '', ''), brand);
|
|
43
|
+
process.stdout.write(args.includes('--json') ? JSON.stringify({ html, brand, publicOrigin, hallOrigin }) : html);
|
|
@@ -223,6 +223,7 @@ const MODULE_GLOBS = {
|
|
|
223
223
|
'ui-design': [
|
|
224
224
|
'modules/ui-design/',
|
|
225
225
|
'scripts/gds/design-sync.js', 'scripts/gds/figma-design-sync.js', 'scripts/gds/validate-design.js',
|
|
226
|
+
'scripts/gds/ui-kit-transform.js', // the kit's stub reaches the real serving transforms through this command (task 1003320)
|
|
226
227
|
'.claude/skills/design/', '.claude/skills/design-sync/', '.claude/skills/figma-design-sync/',
|
|
227
228
|
'docs/recipes/claude-design-loop.md', 'docs/recipes/figma-design-loop.md',
|
|
228
229
|
],
|
package/src/module-api.js
CHANGED
|
@@ -1230,8 +1230,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1230
1230
|
// landed since 1.19.393 with no explicit bump. run 33182535783. (task 1002620)
|
|
1231
1231
|
// 1.19.395 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1232
1232
|
// landed since 1.19.394 with no explicit bump. run 33188021384. (task 1002620)
|
|
1233
|
+
// 1.19.396 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1234
|
+
// landed since 1.19.395 with no explicit bump. run 33188525568. (task 1002620)
|
|
1235
|
+
// 1.19.397 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1236
|
+
// landed since 1.19.396 with no explicit bump. run 33193365480. (task 1002620)
|
|
1233
1237
|
// ---------------------------------------------------------------------------
|
|
1234
|
-
const CORE_VERSION = '1.19.
|
|
1238
|
+
const CORE_VERSION = '1.19.397'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1235
1239
|
|
|
1236
1240
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1237
1241
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|