@bongos/core 1.19.396 → 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 +125 -15
- package/.claude/skills/design/SKILL.md +1 -1
- package/docs/file-map.md +1 -1
- package/docs/recipes/ui-look-before-you-ship.md +113 -0
- package/modules/hall-ui/public/ranks.states.json +15 -0
- 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 +3 -1
- package/tests/ui_design_kit.mjs +217 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.397",
|
|
6
|
+
"core_contract": "1.19.397",
|
|
7
|
+
"source_commit": "1474f9835d0443e4986671261eda1c4fbdc42f77",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-28T17:09:55.951Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 346,
|
|
13
13
|
"agent_docs_stubbed": 23,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1656,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2025,
|
|
20
|
+
"tree_sha256": "0f6d8ffa405c75bbf9238495bd46ce02aab1724213fee1856d77e34c261f7e34",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
{
|
|
128
128
|
"path": ".claude/skills/design/SKILL.md",
|
|
129
129
|
"mode": "0000644",
|
|
130
|
-
"sha256": "
|
|
130
|
+
"sha256": "3fff5beddcb4dc4b2091b27f70fe56a9117db25be3e1201c145ded8123762c16"
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"path": ".claude/skills/dev/SKILL.md",
|
|
@@ -1822,7 +1822,7 @@
|
|
|
1822
1822
|
{
|
|
1823
1823
|
"path": "docs/file-map.md",
|
|
1824
1824
|
"mode": "0000644",
|
|
1825
|
-
"sha256": "
|
|
1825
|
+
"sha256": "da6550986b40181599b2b8e3a4bf0fa02ac82ded0b75b65ec69d8ac6deb1606e"
|
|
1826
1826
|
},
|
|
1827
1827
|
{
|
|
1828
1828
|
"path": "docs/handoff-template.md",
|
|
@@ -2054,6 +2054,11 @@
|
|
|
2054
2054
|
"mode": "0000644",
|
|
2055
2055
|
"sha256": "02ae31810440b86f7c896b7014c75549497cab47345079986d54c9785cf5cc4a"
|
|
2056
2056
|
},
|
|
2057
|
+
{
|
|
2058
|
+
"path": "docs/recipes/ui-look-before-you-ship.md",
|
|
2059
|
+
"mode": "0000644",
|
|
2060
|
+
"sha256": "cd8272b75c5fe26f165cf95a45656109266c8ef1942a47705ab10f5f5affcdd5"
|
|
2061
|
+
},
|
|
2057
2062
|
{
|
|
2058
2063
|
"path": "docs/recipes/upgrading-the-core.md",
|
|
2059
2064
|
"mode": "0000644",
|
|
@@ -3999,6 +4004,11 @@
|
|
|
3999
4004
|
"mode": "0000644",
|
|
4000
4005
|
"sha256": "99f9338383c4940f199f8ad66d2140779b0262b628c135646c00778973a5b292"
|
|
4001
4006
|
},
|
|
4007
|
+
{
|
|
4008
|
+
"path": "modules/hall-ui/public/ranks.states.json",
|
|
4009
|
+
"mode": "0000644",
|
|
4010
|
+
"sha256": "3a2e1d649c217473b3706f9295b796c50a20327b78b2856daf85bf01a1e34b27"
|
|
4011
|
+
},
|
|
4002
4012
|
{
|
|
4003
4013
|
"path": "modules/hall-ui/public/roadmap.css",
|
|
4004
4014
|
"mode": "0000644",
|
|
@@ -5177,7 +5187,77 @@
|
|
|
5177
5187
|
{
|
|
5178
5188
|
"path": "modules/ui-design/kit/README.md",
|
|
5179
5189
|
"mode": "0000644",
|
|
5180
|
-
"sha256": "
|
|
5190
|
+
"sha256": "850e72f762d80951ed0ad6ca392a5c0619f26c10a58edd0a1d467038b74bbcfa"
|
|
5191
|
+
},
|
|
5192
|
+
{
|
|
5193
|
+
"path": "modules/ui-design/kit/check-mock.js",
|
|
5194
|
+
"mode": "0000644",
|
|
5195
|
+
"sha256": "9f38941d23b498feeac12f6f32a64d5f3c7749e1b6e01db7bf83dedd3cc2e549"
|
|
5196
|
+
},
|
|
5197
|
+
{
|
|
5198
|
+
"path": "modules/ui-design/kit/contrast.js",
|
|
5199
|
+
"mode": "0000644",
|
|
5200
|
+
"sha256": "239cc33a7896dbbe0bab25098904b2be8298ff5c440b7e2a44594e131d21ed9c"
|
|
5201
|
+
},
|
|
5202
|
+
{
|
|
5203
|
+
"path": "modules/ui-design/kit/fixtures/cost-summary.json",
|
|
5204
|
+
"mode": "0000644",
|
|
5205
|
+
"sha256": "057391fcbba1985a234cf1d4f56db36a89a79d1ee44a80b5cd30f51ed7175cdb"
|
|
5206
|
+
},
|
|
5207
|
+
{
|
|
5208
|
+
"path": "modules/ui-design/kit/fixtures/featured.json",
|
|
5209
|
+
"mode": "0000644",
|
|
5210
|
+
"sha256": "a5e075d343bc7d29e69896ba90de2fc96e2aeb5765f09e193cb26926b4ae697d"
|
|
5211
|
+
},
|
|
5212
|
+
{
|
|
5213
|
+
"path": "modules/ui-design/kit/fixtures/leaderboard.json",
|
|
5214
|
+
"mode": "0000644",
|
|
5215
|
+
"sha256": "53f4fbe364bee2295133ea55082b79069451b0deaf3e99e42d0963c63ee1da87"
|
|
5216
|
+
},
|
|
5217
|
+
{
|
|
5218
|
+
"path": "modules/ui-design/kit/fixtures/me.json",
|
|
5219
|
+
"mode": "0000644",
|
|
5220
|
+
"sha256": "71e560cd3ffcc86287f0ec9c0e527de5ce568a1a9c5c8a6cefea7efe2459d070"
|
|
5221
|
+
},
|
|
5222
|
+
{
|
|
5223
|
+
"path": "modules/ui-design/kit/fixtures/my-projects.json",
|
|
5224
|
+
"mode": "0000644",
|
|
5225
|
+
"sha256": "ff0195be4b19616f30fccde0dc980a47329587124bfac08158b1e8741386fc1b"
|
|
5226
|
+
},
|
|
5227
|
+
{
|
|
5228
|
+
"path": "modules/ui-design/kit/fixtures/progress.json",
|
|
5229
|
+
"mode": "0000644",
|
|
5230
|
+
"sha256": "bbc16593ef50e966647f54559834a956778ac59937ef3fc27794d98d1e12a67e"
|
|
5231
|
+
},
|
|
5232
|
+
{
|
|
5233
|
+
"path": "modules/ui-design/kit/fixtures/recent-shipped.json",
|
|
5234
|
+
"mode": "0000644",
|
|
5235
|
+
"sha256": "5c50e901d4e2e123b2c94f17b42e68cdb5dd9702edff840802777f0796e8f976"
|
|
5236
|
+
},
|
|
5237
|
+
{
|
|
5238
|
+
"path": "modules/ui-design/kit/lib.js",
|
|
5239
|
+
"mode": "0000644",
|
|
5240
|
+
"sha256": "9b0b5eb21711fd7f5c0857a89066270ac8c0911cec19d0b8f21ed369a1dde8e5"
|
|
5241
|
+
},
|
|
5242
|
+
{
|
|
5243
|
+
"path": "modules/ui-design/kit/png.js",
|
|
5244
|
+
"mode": "0000644",
|
|
5245
|
+
"sha256": "d4a7f9c791dec3857452642a1f87dac716c2f098a36d6f7b5a0655da9a110b4e"
|
|
5246
|
+
},
|
|
5247
|
+
{
|
|
5248
|
+
"path": "modules/ui-design/kit/probe.js",
|
|
5249
|
+
"mode": "0000644",
|
|
5250
|
+
"sha256": "d38d6168c3bd701a368726fbf53bfb9209711bd2cb68b7df79a3d2505edf2efe"
|
|
5251
|
+
},
|
|
5252
|
+
{
|
|
5253
|
+
"path": "modules/ui-design/kit/render.js",
|
|
5254
|
+
"mode": "0000644",
|
|
5255
|
+
"sha256": "15931b862ff62824b9916172d75df677bea186e1be23eefb99bbd4541932acfd"
|
|
5256
|
+
},
|
|
5257
|
+
{
|
|
5258
|
+
"path": "modules/ui-design/kit/serve.js",
|
|
5259
|
+
"mode": "0000644",
|
|
5260
|
+
"sha256": "f1d349f681525b78abb47df98bf26a1f7470a72b00b8d5696d5453bb936647c8"
|
|
5181
5261
|
},
|
|
5182
5262
|
{
|
|
5183
5263
|
"path": "modules/ui-design/module.json",
|
|
@@ -5212,12 +5292,12 @@
|
|
|
5212
5292
|
{
|
|
5213
5293
|
"path": "package-lock.json",
|
|
5214
5294
|
"mode": "0000644",
|
|
5215
|
-
"sha256": "
|
|
5295
|
+
"sha256": "261eccb023c862c7b005d3078f269afdee0db925ebb0834145cb8080a30137f9"
|
|
5216
5296
|
},
|
|
5217
5297
|
{
|
|
5218
5298
|
"path": "package.json",
|
|
5219
5299
|
"mode": "0000644",
|
|
5220
|
-
"sha256": "
|
|
5300
|
+
"sha256": "cd144d5c3d9102a17d79445708230c929987ac1d102ca82c3aa69493b8fdc242"
|
|
5221
5301
|
},
|
|
5222
5302
|
{
|
|
5223
5303
|
"path": "public-docs/index.html",
|
|
@@ -5549,6 +5629,16 @@
|
|
|
5549
5629
|
"mode": "0000644",
|
|
5550
5630
|
"sha256": "1e7ceb571c899be95c7a945a8bd4337bb000af2b90d6f8de5b4dfcef3a5e8891"
|
|
5551
5631
|
},
|
|
5632
|
+
{
|
|
5633
|
+
"path": "public-landing/index.probes.json",
|
|
5634
|
+
"mode": "0000644",
|
|
5635
|
+
"sha256": "eb90141b2ef676e06b3140d2e8caa61481991a748b36491502592987ed9c0880"
|
|
5636
|
+
},
|
|
5637
|
+
{
|
|
5638
|
+
"path": "public-landing/index.states.json",
|
|
5639
|
+
"mode": "0000644",
|
|
5640
|
+
"sha256": "8d5955085ab5059a15195d5c475262d114061d616dd9c82335a91a5992cc5602"
|
|
5641
|
+
},
|
|
5552
5642
|
{
|
|
5553
5643
|
"path": "public-landing/privacy.html",
|
|
5554
5644
|
"mode": "0000644",
|
|
@@ -5559,6 +5649,16 @@
|
|
|
5559
5649
|
"mode": "0000644",
|
|
5560
5650
|
"sha256": "1e1867918ec2a8fe489d9d21aa2e50fada9950b220c865c57f23c703a6c29f24"
|
|
5561
5651
|
},
|
|
5652
|
+
{
|
|
5653
|
+
"path": "public-landing/projects.probes.json",
|
|
5654
|
+
"mode": "0000644",
|
|
5655
|
+
"sha256": "e5594cda5b6c30569aaca6181dc39eb28d20e716cdebbca7ad618748bbb8ccb5"
|
|
5656
|
+
},
|
|
5657
|
+
{
|
|
5658
|
+
"path": "public-landing/projects.states.json",
|
|
5659
|
+
"mode": "0000644",
|
|
5660
|
+
"sha256": "26a1473a42005d89b4a15b3538b3e9fe9d1982633f1ec556524d83e114df5240"
|
|
5661
|
+
},
|
|
5562
5662
|
{
|
|
5563
5663
|
"path": "public-landing/terms.html",
|
|
5564
5664
|
"mode": "0000644",
|
|
@@ -6789,6 +6889,11 @@
|
|
|
6789
6889
|
"mode": "0000644",
|
|
6790
6890
|
"sha256": "cc6e8640376ada7ceff6a896007b3257c8ea4d943a900eae28433cd41bdd139d"
|
|
6791
6891
|
},
|
|
6892
|
+
{
|
|
6893
|
+
"path": "scripts/gds/ui-kit-transform.js",
|
|
6894
|
+
"mode": "0000644",
|
|
6895
|
+
"sha256": "8735394cfc5bcdd85b7addb78c3de302f4ba0fee37c25a595f5dcb46240ec529"
|
|
6896
|
+
},
|
|
6792
6897
|
{
|
|
6793
6898
|
"path": "scripts/gds/update-channel.js",
|
|
6794
6899
|
"mode": "0000644",
|
|
@@ -7032,7 +7137,7 @@
|
|
|
7032
7137
|
{
|
|
7033
7138
|
"path": "src/bongos/module-scope-map.js",
|
|
7034
7139
|
"mode": "0000644",
|
|
7035
|
-
"sha256": "
|
|
7140
|
+
"sha256": "9705e8a5176cfd1136c1b432b1d4ff71351d9f4654c636345597a4d96763e500"
|
|
7036
7141
|
},
|
|
7037
7142
|
{
|
|
7038
7143
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -7207,7 +7312,7 @@
|
|
|
7207
7312
|
{
|
|
7208
7313
|
"path": "src/module-api.js",
|
|
7209
7314
|
"mode": "0000644",
|
|
7210
|
-
"sha256": "
|
|
7315
|
+
"sha256": "dc2d98c7ece67a2c881914a1a9cf000736e7634161f774d09c5c66a7bef2c1a9"
|
|
7211
7316
|
},
|
|
7212
7317
|
{
|
|
7213
7318
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9954,6 +10059,11 @@
|
|
|
9954
10059
|
"mode": "0000644",
|
|
9955
10060
|
"sha256": "74798ce81280d4fcfa5844b575c3c64db582308f7a0726fc420317fe83eadd96"
|
|
9956
10061
|
},
|
|
10062
|
+
{
|
|
10063
|
+
"path": "tests/ui_design_kit.mjs",
|
|
10064
|
+
"mode": "0000644",
|
|
10065
|
+
"sha256": "2d3c4f767a3de04beddf22d6d0acf8ca57b7a85dc0f132930466eb4f33aa8c13"
|
|
10066
|
+
},
|
|
9957
10067
|
{
|
|
9958
10068
|
"path": "tests/update_channel.mjs",
|
|
9959
10069
|
"mode": "0000644",
|
|
@@ -25,7 +25,7 @@ Read, in this order, and quote nothing from memory:
|
|
|
25
25
|
|
|
26
26
|
Render the real surface before and after — never reason about layout from source alone:
|
|
27
27
|
|
|
28
|
-
-
|
|
28
|
+
- Run the kit — `node modules/ui-design/kit/render.js --page <path>` renders every state of the page's states file at **1440 / 390 / 320 × dark / light** through a stub that serves the surface the way the instance does, audits each render against the floors below, and writes the shots; `node modules/ui-design/kit/probe.js --page <path>` runs the page's interaction contract as PASS/FAIL lines. The loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). A page with no states file yet gets one first (copy the hub's). `/builder-stage` on a box shows the same change on a live preview.
|
|
29
29
|
- Show the screens, then caption them in one line. The person you're working with judges the picture.
|
|
30
30
|
- Grep the receiving page for class collisions before porting a component across pages (the landing's content sections are `.band`; the menu is `.menuBand`).
|
|
31
31
|
|
package/docs/file-map.md
CHANGED
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav.
|
|
130
130
|
│ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
|
|
131
131
|
│ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
|
|
132
|
-
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md).
|
|
132
|
+
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (public-landing/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. Reserved homes skills/ (1003321), styles/ (1003323), each with a README. config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
|
|
133
133
|
│ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
|
|
134
134
|
│ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
|
|
135
135
|
│ └── demo/ ← BV1.R52 acceptance-proof module: one rank-gated write route + one namespaced migration proving the loader mounts a scaffolded module with zero core edits. CLAUDE.md, module.json.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Look before you ship — the ui-design kit
|
|
2
|
+
|
|
3
|
+
> The `ui-design` module's kit (`modules/ui-design/kit/`, [task 1003320](https://cloudbongos.com/builders#/task/1003320), [ADR 0197](../adr/<redacted>.md)): a stub, a renderer/auditor, the interaction probes, the mock checker and the contrast probe any builder points at any surface of any instance. Plain Node; the browser is the system Edge or Chrome (nothing is downloaded). The `/design` playbook sends you here.
|
|
4
|
+
|
|
5
|
+
The loop is five commands and one habit: **stub → render → probe → read the shots → fix in one batch → re-run once.** A surface describes itself in two small JSON files beside it; the kit never hardcodes a page.
|
|
6
|
+
|
|
7
|
+
## 0. Once per machine
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install # playwright-core is a devDependency; no browser download
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Edge or Chrome must be installed. If neither launches, every kit command prints one line saying so and exits 3.
|
|
14
|
+
|
|
15
|
+
## 1. Stub — serve the surface the way the instance would
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
node modules/ui-design/kit/serve.js # public-landing at http://127.0.0.1:3412/
|
|
19
|
+
AUTH=1 STUB_PORT=3413 node modules/ui-design/kit/serve.js # the same, signed in (/me answers { builder })
|
|
20
|
+
SURFACE=hall-ui PREFIX=/builders/ node modules/ui-design/kit/serve.js
|
|
21
|
+
CHILD=1 node modules/ui-design/kit/serve.js # the child-instance shape (apex-only regions stripped)
|
|
22
|
+
LIVE=1 node modules/ui-design/kit/serve.js # public feeds proxied to the pack's public origin
|
|
23
|
+
FAIL=featured,progress node modules/ui-design/kit/serve.js # those feeds answer 500
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The stub runs the page through the **instance's real transforms** — the apex-only strip, the brand-token stamp, the pack's theme injection, run by the core-side command `scripts/gds/ui-kit-transform.js` (a module may not require core, so the stub spawns the command and memoises the page) — then the content-hash `?v=` stamp, with the instance's own branding pack, and answers the public feeds from `kit/fixtures/`. It also serves `docs/` raw, so a mock under `docs/design/mocks/` renders with its relative links: `http://127.0.0.1:3412/docs/design/mocks/hub/v2.html`.
|
|
27
|
+
|
|
28
|
+
`SURFACE` is a checkout directory (`public-landing`, `modules/hall-ui/public`) or a module key whose manifest declares a `webSurfaces` dir. `PREFIX` is the URL prefix the surface's pages and assets live under (the hall's is `/builders/`).
|
|
29
|
+
|
|
30
|
+
## 2. Render — every state, every width, both modes, audited
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
node modules/ui-design/kit/render.js --page /projects
|
|
34
|
+
node modules/ui-design/kit/render.js --page / # MSYS_NO_PATHCONV=1 in Git Bash, or the kit reads the mangled root back
|
|
35
|
+
node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
|
|
36
|
+
node modules/ui-design/kit/render.js --page /projects --only in-map,card --modes dark --sizes 390
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`render.js` starts its own two stubs (signed out, signed in), renders each state of the page's **states file** in dark + light at 1440×900 / 390×844 / 320×700, drives the state's actions, and audits every render against the **platform floors**:
|
|
40
|
+
|
|
41
|
+
| Floor | What fails it |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `console-errors` | a `console.error` or an uncaught page error (the file's `ignoreConsole` regexes excepted) |
|
|
44
|
+
| `failed-requests` | a 4xx/5xx response the file did not declare in `ignoreRequests` |
|
|
45
|
+
| `horizontal-overflow` | `scrollWidth` wider than the viewport |
|
|
46
|
+
| `one-header-main-h1` | not exactly one *rendered* `<header>`, `<main>` and `<h1>` |
|
|
47
|
+
| `controls-24px` | a visible control under 24px on either side (`ignoreControls` for the skip link and the like) |
|
|
48
|
+
| `top-bar-80px` | a `<header>` taller than 80px |
|
|
49
|
+
| `reduced-motion` | under `prefers-reduced-motion` an animation still runs or a `requestAnimationFrame` loop ticks |
|
|
50
|
+
|
|
51
|
+
Then a state's own `expect` (a band open, a dialog present, a field's value, a class, a query param). Shots and `report.json` land in `%TMP%/ui-design-kit/<surface>/<page>/` (or `--out`). Exit 0 is `ALL CLEAN`; 1 lists the problems; 2 is a broken states file, named line by line.
|
|
52
|
+
|
|
53
|
+
### The states file — `<surface>/<page>.states.json`
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"page": "/projects",
|
|
58
|
+
"surface": "public-landing",
|
|
59
|
+
"modeQuery": "mode",
|
|
60
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects"],
|
|
61
|
+
"ignoreConsole": ["\\b401\\b"],
|
|
62
|
+
"ignoreControls": [".skip", ".vh"],
|
|
63
|
+
"states": {
|
|
64
|
+
"out-map": { "auth": false, "expect": { "expanded": false } },
|
|
65
|
+
"in-map-projects": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProjects" } },
|
|
66
|
+
"card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb:not(#selfOrb)"], ["wait", 600]], "expect": { "dialog": true } }
|
|
67
|
+
},
|
|
68
|
+
"reducedMotion": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 100]] }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Actions: `click`, `focus`, `press`, `fill`, `wait`, `clickAt x y`, `clickIfVisible`, `dispatchClick`, `check`, `back`, `forward`. Expectations: `expanded`, `dialog`, `visible[]`, `hidden[]`, `open`, `closed`, `activeId`, `activeText`, `expandedIds[]`, `param{}`, `attr[[sel,name,value]]`, `text[[sel,value]]`, `href[[sel,value]]`, `count[[sel,n]]`, `value[[sel,v]]`, `hasClass[[sel,class,bool]]`, `checked`, `noOverflow`. The kit validates the file before it opens a browser and names every problem.
|
|
73
|
+
|
|
74
|
+
The worked examples: `public-landing/projects.states.json` (the hub's thirteen), `public-landing/index.states.json` (the landing's four), `modules/hall-ui/public/ranks.states.json` (a second surface under its own prefix — it renders **red**, deliberately: the hall predates the 24px floor and Ranks has no `<h1>`, and the states file declares the shell's un-fixtured feeds rather than hiding the findings; idea 1000863 carries them).
|
|
75
|
+
|
|
76
|
+
## 3. Probe — the interaction contract, as PASS/FAIL lines
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
node modules/ui-design/kit/probe.js --page /projects
|
|
80
|
+
node modules/ui-design/kit/probe.js --page / --only "Escape"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A **probes file** (`<surface>/<page>.probes.json`) is a sequence: each probe opens the page (or continues the previous probe's page with `"continue": true`), drives steps from the same action vocabulary, and states what must be true with the same expectations. An uncaught page error during a probe fails it. The disclosure contract ships first — `public-landing/projects.probes.json` (24 probes: Enter opens on the first item, arrows cycle and skip hidden cells, Escape returns focus, Tab out closes, a click outside closes, one band closes the other, the views, the card yields Escape, the fold under 480, signed out nothing throws) and `index.probes.json` (10, the landing).
|
|
84
|
+
|
|
85
|
+
## 4. Read the shots, fix in one batch, re-run once
|
|
86
|
+
|
|
87
|
+
Open the shots directory. Look at the 390 and 320 renders first (the fold, the bar, the bands), then dark, then the states you touched. Fix everything you see in **one** batch — a render pass costs a minute, a change-render-change loop costs the afternoon — then re-run `render.js` and `probe.js` once. Ship the tests with the change: a new interactive behaviour gets a pin in the page's own test file in the same diff.
|
|
88
|
+
|
|
89
|
+
## 5. Two more probes, for a mock and for colour
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
node modules/ui-design/kit/check-mock.js --file docs/design/mocks/hub/v2.html [--states out-map,card] [--keys THESIS,BAR]
|
|
93
|
+
node modules/ui-design/kit/contrast.js --pack
|
|
94
|
+
node modules/ui-design/kit/contrast.js --url http://127.0.0.1:3412/projects --glyphs "#projTrig,.foot a"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`check-mock.js` runs the mechanical rules over a self-contained mock before anyone looks at it: the **platform's** (no page-local `:root`, page tokens on `body`, every dark rule written twice byte-identical, no literal colour beyond black/white with alpha, no pack colour pinned, the `?state=` contract, a head comment after `<body>`, one header/main/h1 + a skip link, apex-only sentinels balanced, no `innerHTML`, no motion in JavaScript, transitions ≤ 400ms) and the **instance's taste bans**, read from its `DESIGN.md` "Don't" section (an em/en dash, emoji, gradient text, a coloured glow, a box shadow). The em dash ban is Cloud Bongos's, not the platform's: an instance whose `DESIGN.md` never mentions it is not held to it.
|
|
98
|
+
|
|
99
|
+
`contrast.js --pack` measures the instance's own pack — every ink on every ground, light values — with no browser. `--url`/`--file` walks every visible text element in both modes and computes the real WCAG ratio against its *composited* background; `--glyphs` screenshots named elements and reads the glyph pixels against the local ground's median, for text over a moving or textured ground where the stylesheet cannot say what is under it.
|
|
100
|
+
|
|
101
|
+
## When it goes wrong
|
|
102
|
+
|
|
103
|
+
| Symptom | Cause | Do |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `no states file beside the surface: …/Git.states.json` | Git Bash turned `--page /` into the MSYS root | the kit reads it back as `/`; or `MSYS_NO_PATHCONV=1 node …` |
|
|
106
|
+
| `no system browser` (exit 3) | neither Edge nor Chrome is installed | install one; the kit never downloads a browser |
|
|
107
|
+
| `failed-requests: 404 …/api/bongos/goals` on a hall page | the shell polls feeds the stub does not fixture | declare them in the states file's `ignoreRequests` (the hall's does) — or add a fixture under `kit/fixtures/` and the route in `serve.js` |
|
|
108
|
+
| `controls-24px: button.trig "builder" 13x44` | the chip sized itself by a missing avatar | the fixture builder's avatar is served by the stub (`/__kit/avatar.svg`); a page that rejects non-http avatars gets it absolutised |
|
|
109
|
+
| a probe times out on `click` | an earlier probe in the chain left the page in another state | fix the earlier FAIL first; chains are sequences |
|
|
110
|
+
|
|
111
|
+
## What the kit is not
|
|
112
|
+
|
|
113
|
+
Not CI: `tests/ui_design_kit.mjs` pins the contract (the floors, the file shapes, the audit, the mock rules, the pack, the exit codes, the stub) without a browser; the renders are yours to run and look at. Not a screenshot-diff tool: it audits against floors and expectations, not against a golden image. Not an instance's taste: everything <redacted> lives in the states/probes files beside Cloud Bongos's pages and in its `DESIGN.md`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The hall's Ranks page as the SECOND surface through modules/ui-design/kit/render.js (task 1003320): the hall serves under /builders/, signed out and in.",
|
|
3
|
+
"page": "/builders/ranks",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": { "prefix": "/builders/" },
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"_feeds": "The hall's shell polls goals + inbox + profile feeds on every page; the stub fixtures only the public feeds, so those 404s are declared here, not hidden by the kit. The 24px-floor and h1 findings the render reports are REAL (the hall predates the floor) and are deliberately not ignored.",
|
|
8
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/me/"],
|
|
9
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized", "404 \\(Not Found\\)", "goals load failed"],
|
|
10
|
+
"states": {
|
|
11
|
+
"out": { "auth": false },
|
|
12
|
+
"in": { "auth": true }
|
|
13
|
+
},
|
|
14
|
+
"reducedMotion": { "auth": true }
|
|
15
|
+
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
# `modules/ui-design/kit/` —
|
|
1
|
+
# `modules/ui-design/kit/` — look before you ship
|
|
2
2
|
|
|
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
|
+
|
|
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]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
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.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// check-mock.js — the mechanical rule set for a MOCK (a self-contained HTML page
|
|
4
|
+
// under docs/design/mocks/) before anyone looks at it (task 1003320, ADR 0197).
|
|
5
|
+
// Two kinds of rule, kept apart on purpose:
|
|
6
|
+
// • the PLATFORM's — token discipline (no page-local :root, page tokens on
|
|
7
|
+
// body, every dark rule written twice byte-identical, no literal colour
|
|
8
|
+
// beyond black/white with alpha, no pack colour pinned as a literal), the
|
|
9
|
+
// state contract (the mock reads ?state=), the head comment after <body>,
|
|
10
|
+
// one <header>/<main>/<h1> + a skip link, apex-only sentinels balanced, no
|
|
11
|
+
// innerHTML, no motion in JavaScript, state transitions ≤ 400ms;
|
|
12
|
+
// • the INSTANCE's taste bans — read from its DESIGN.md "Don't" section (an
|
|
13
|
+
// em/en dash in copy, emoji, gradient text, a coloured text glow, a box
|
|
14
|
+
// shadow). The em dash ban is Cloud Bongos's, not the platform's: an instance
|
|
15
|
+
// whose DESIGN.md never mentions it is not held to it.
|
|
16
|
+
//
|
|
17
|
+
// node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys THESIS,BAR] [--design <DESIGN.md>] [--no-taste]
|
|
18
|
+
//
|
|
19
|
+
// Prints PASS/FAIL/WARN per rule; exit 1 on any FAIL, 2 on usage.
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const lib = require('./lib.js');
|
|
23
|
+
|
|
24
|
+
// The taste bans an instance's DESIGN.md may switch on, by the words its "Don't"
|
|
25
|
+
// section uses. Pure: text in, a Set of ban keys out.
|
|
26
|
+
const TASTE_BANS = Object.freeze({
|
|
27
|
+
'em-dash': /em dash|en dash|em-dash|en-dash/i,
|
|
28
|
+
'emoji': /\bemoji\b/i,
|
|
29
|
+
'gradient-text': /gradient text/i,
|
|
30
|
+
'text-glow': /\bglow\b/i,
|
|
31
|
+
'box-shadow': /box[- ]shadow|\bshadow\b/i,
|
|
32
|
+
});
|
|
33
|
+
function readDesignBans(mdText) {
|
|
34
|
+
const bans = new Set();
|
|
35
|
+
const m = String(mdText || '').match(/^###\s*Don'?ts?\b[\s\S]*?(?=^##\s|^###\s|(?![\s\S]))/mi);
|
|
36
|
+
const section = m ? m[0] : '';
|
|
37
|
+
for (const [key, re] of Object.entries(TASTE_BANS)) if (re.test(section)) bans.add(key);
|
|
38
|
+
return bans;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripToText(html) {
|
|
42
|
+
return html.replace(/<!--[\s\S]*?-->/g, '').replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ');
|
|
43
|
+
}
|
|
44
|
+
// Top-level (or nested, when `selectorRe` names an @media) blocks of a stylesheet.
|
|
45
|
+
function cssBlocks(css, selectorRe) {
|
|
46
|
+
const out = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < css.length) {
|
|
49
|
+
const open = css.indexOf('{', i);
|
|
50
|
+
if (open < 0) break;
|
|
51
|
+
const selStart = Math.max(css.lastIndexOf('}', open), css.lastIndexOf(';', open)) + 1;
|
|
52
|
+
const sel = css.slice(selStart, open).trim();
|
|
53
|
+
let depth = 1, j = open + 1;
|
|
54
|
+
while (j < css.length && depth > 0) { if (css[j] === '{') depth++; else if (css[j] === '}') depth--; j++; }
|
|
55
|
+
const inner = css.slice(open + 1, j - 1);
|
|
56
|
+
if (selectorRe.test(sel)) out.push({ sel, inner });
|
|
57
|
+
i = /^@(media|supports)/.test(sel) ? open + 1 : j;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
const norm = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').replace(/\s*([{}:;,])\s*/g, '$1').trim();
|
|
62
|
+
const BLACK_OR_WHITE = /^(000|000000|fff|ffffff|0{8}|f{8}|000000[0-9a-f]{2}|ffffff[0-9a-f]{2})$/;
|
|
63
|
+
|
|
64
|
+
// Pure: the rules over a mock's source. `packColors` = the instance's fifteen
|
|
65
|
+
// (name → value) so a literal equal to a pack colour is caught; `bans` = the
|
|
66
|
+
// instance's taste bans; `states`/`keys` = names the mock must carry.
|
|
67
|
+
function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys = [] } = {}) {
|
|
68
|
+
const raw = String(html).replace(/\r\n/g, '\n');
|
|
69
|
+
const results = [];
|
|
70
|
+
const ok = (rule, pass, detail) => results.push([pass ? 'PASS' : 'FAIL', rule, detail || '']);
|
|
71
|
+
const warn = (rule, detail) => results.push(['WARN', rule, detail || '']);
|
|
72
|
+
const text = stripToText(raw);
|
|
73
|
+
const styles = [...raw.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]).join('\n');
|
|
74
|
+
const scripts = [...raw.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)].map((m) => m[1]).join('\n');
|
|
75
|
+
const css = styles.replace(/\/\*[\s\S]*?\*\//g, '').replace(/url\([^)]*\)/g, 'url()');
|
|
76
|
+
|
|
77
|
+
// ── token discipline (platform) ──
|
|
78
|
+
const rootBlocks = css.replace(/:root\[data-mode="dark"\]\s+body/g, 'X').replace(/:root:not\(\[data-mode="light"\]\)\s+body/g, 'X');
|
|
79
|
+
ok('no page-local :root block (the pack\'s :root is spliced at the end of <head>)', !/(^|[}\s;]):root\s*(\[[^\]]*\]\s*)?(:not\([^)]*\)\s*)?\{/m.test(rootBlocks.replace(/:root\[data-mode="dark"\]\s*\{/g, 'D{').replace(/:root:not\(\[data-mode="light"\]\)\s*\{/g, 'D{')));
|
|
80
|
+
const bodyBlocks = cssBlocks(styles, /^body$/);
|
|
81
|
+
ok('page tokens declared on body', bodyBlocks.some((b) => /--[\w-]+\s*:/.test(b.inner)), `${bodyBlocks.length} body block(s)`);
|
|
82
|
+
const darkAttr = cssBlocks(styles, /^:root\[data-mode="dark"\]/);
|
|
83
|
+
const darkMedia = cssBlocks(styles, /^@media[^{]*prefers-color-scheme:\s*dark/).map((b) => cssBlocks(b.inner, /^:root:not\(\[data-mode="light"\]\)/)).flat();
|
|
84
|
+
const a = darkAttr.map((b) => norm(b.sel.replace(/^:root\[data-mode="dark"\]/, '') + '{' + b.inner + '}')).sort();
|
|
85
|
+
const m = darkMedia.map((b) => norm(b.sel.replace(/^:root:not\(\[data-mode="light"\]\)/, '') + '{' + b.inner + '}')).sort();
|
|
86
|
+
if (!a.length && !m.length) warn('dark twins: the mock declares no dark rules at all', 'fine for a light-only fragment; a page needs both');
|
|
87
|
+
else ok('every dark rule written twice, byte-identical (attribute block == prefers-color-scheme twin)', a.length > 0 && JSON.stringify(a) === JSON.stringify(m), `${a.length} attribute block(s), ${m.length} media twin(s)`);
|
|
88
|
+
const hexes = [...css.matchAll(/#([0-9a-fA-F]{3,8})\b/g)].map((x) => x[1].toLowerCase()).filter((h) => !BLACK_OR_WHITE.test(h));
|
|
89
|
+
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
|
+
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
|
+
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);
|
|
93
|
+
ok('no pack colour pinned as a literal (a var() of the contract token instead)', pinned.length === 0, pinned.join(','));
|
|
94
|
+
|
|
95
|
+
// ── the taste bans the instance declared ──
|
|
96
|
+
if (bans.has('em-dash')) { const n = (text.match(/[–—]/g) || []).length; ok('taste: no em/en dash in visible copy', n === 0, n ? `${n} found` : ''); }
|
|
97
|
+
if (bans.has('emoji')) { const n = (text.match(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B50}\u{FE0F}]/gu) || []).length; ok('taste: no emoji in visible copy', n === 0, n ? `${n} found` : ''); }
|
|
98
|
+
if (bans.has('gradient-text')) ok('taste: no gradient text', !/background-clip\s*:\s*text/i.test(css));
|
|
99
|
+
if (bans.has('text-glow')) { const glow = [...css.matchAll(/text-shadow\s*:\s*([^;]+);/g)].map((x) => x[1]).filter((v) => !/^(var\(|none)/.test(v.trim()) && /rgba?\((?!0,\s*0,\s*0)|#(?!000)/.test(v)); ok('taste: no coloured text glow', glow.length === 0, glow.join(' | ')); }
|
|
100
|
+
if (bans.has('box-shadow')) { const sh = [...css.matchAll(/box-shadow\s*:\s*([^;]+);/g)].map((x) => x[1].trim()).filter((v) => !/^(var\(|none|inset|0 0 0 \d+px var\()/.test(v) && !/^0 0 0 0/.test(v)); ok('taste: no box shadow (hairlines only; a 0-spread halo for focus is the exception)', sh.length === 0, sh.join(' | ')); }
|
|
101
|
+
|
|
102
|
+
// ── structure + the state contract (platform) ──
|
|
103
|
+
ok('one <header>', (raw.match(/<header\b/g) || []).length === 1);
|
|
104
|
+
ok('one <main>', (raw.match(/<main\b/g) || []).length === 1);
|
|
105
|
+
ok('one <h1>', (raw.match(/<h1\b/g) || []).length === 1);
|
|
106
|
+
ok('a skip link', /class="[^"]*\bskip\b[^"]*"/.test(raw));
|
|
107
|
+
ok('state contract: the mock reads ?state=', /(searchParams|location\.search)/.test(scripts) && /['"]state['"]/.test(scripts));
|
|
108
|
+
if (states.length) { const missing = states.filter((s) => !new RegExp(`['"\`]${s}['"\`]`).test(scripts) && !new RegExp(`data-state="${s}"`).test(raw)); ok('every state of the contract is named in the script or markup', missing.length === 0, missing.join(', ')); }
|
|
109
|
+
const headComment = (raw.match(/<body[^>]*>\s*<!--([\s\S]*?)-->/) || [])[1];
|
|
110
|
+
ok('a head comment right after <body> (what the mock claims, for the reviewer)', !!headComment && headComment.trim().length > 40);
|
|
111
|
+
if (keys.length && headComment) { const missing = keys.filter((k) => !headComment.includes(k)); ok('the head comment names every required section', missing.length === 0, missing.join(', ')); }
|
|
112
|
+
const sentStart = (raw.match(/<!--apex-only:start-->/g) || []).length, sentEnd = (raw.match(/<!--apex-only:end-->/g) || []).length;
|
|
113
|
+
ok('apex-only sentinels balanced', sentStart === sentEnd, `${sentStart} start / ${sentEnd} end`);
|
|
114
|
+
ok('apex-only sentinels never nested', !/<!--apex-only:start-->(?:(?!<!--apex-only:end-->)[\s\S])*<!--apex-only:start-->/.test(raw));
|
|
115
|
+
const menus = (raw.match(/<button[^>]*aria-expanded="(true|false)"[^>]*aria-controls="[^"]+"|<button[^>]*aria-controls="[^"]+"[^>]*aria-expanded="(true|false)"/g) || []).length;
|
|
116
|
+
if (/aria-expanded/.test(raw)) ok('menus use the disclosure pattern (button aria-expanded + aria-controls)', menus >= 1);
|
|
117
|
+
|
|
118
|
+
// ── motion hygiene (platform: the kill switch rule) ──
|
|
119
|
+
ok('no motion in JavaScript (no requestAnimationFrame, no .animate()', !/requestAnimationFrame|\.animate\(/.test(scripts));
|
|
120
|
+
ok('no innerHTML', !/innerHTML/.test(scripts));
|
|
121
|
+
const durs = [...css.matchAll(/transition\s*:[^;]*?(\d+)ms/g)].map((x) => +x[1]).filter((d) => d > 400);
|
|
122
|
+
ok('state transitions at most 400ms', durs.length === 0, durs.join(','));
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function main() {
|
|
127
|
+
const args = process.argv.slice(2);
|
|
128
|
+
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); }
|
|
130
|
+
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'));
|
|
132
|
+
let bans = new Set();
|
|
133
|
+
if (!lib.hasFlag(args, '--no-taste')) {
|
|
134
|
+
if (fs.existsSync(designPath)) bans = readDesignBans(fs.readFileSync(designPath, 'utf8'));
|
|
135
|
+
else process.stdout.write(`WARN taste bans off: no DESIGN.md at ${designPath} (the platform rules still apply)\n`);
|
|
136
|
+
}
|
|
137
|
+
let packColors = {};
|
|
138
|
+
try { packColors = lib.packColors(lib.instancePack()); } catch { packColors = {}; }
|
|
139
|
+
const results = checkMock(fs.readFileSync(file, 'utf8'), { bans, packColors, states: lib.listArg(args, '--states', []), keys: lib.listArg(args, '--keys', []) });
|
|
140
|
+
for (const [s, rule, detail] of results) process.stdout.write(`${s.padEnd(4)} ${rule}${detail ? ' (' + detail + ')' : ''}\n`);
|
|
141
|
+
const fails = results.filter((r) => r[0] === 'FAIL').length;
|
|
142
|
+
process.stdout.write(`\n${fails ? `${fails} RULE(S) FAIL` : 'ALL RULES PASS'} (taste bans: ${[...bans].join(', ') || 'none declared'})\n`);
|
|
143
|
+
process.exit(fails ? lib.EXIT.PROBLEMS : lib.EXIT.CLEAN);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (require.main === module) main();
|
|
147
|
+
module.exports = { checkMock, readDesignBans, TASTE_BANS, cssBlocks };
|