@bongos/core 1.19.413 → 1.19.414

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.
@@ -60,6 +60,8 @@ What the pick settles: the settled constraint (dark-first with a real, tested li
60
60
 
61
61
  **2026-08-28, the second note (task 1003334).** Seeing the before/after of task 1003333, the owner found the chrome bongo pair sitting on the top-right sweeping form did not fit ("the random bongos on the blobs background"). Offered three treatments (remove it from the hero; keep it off the blob on plain black or paper; phones only), the owner picked **remove it from the hero**. The pair is no longer a hero object: `.mark`, its arrival and its tokens leave; it stays as the closing figure, the second project orb and the bar's mark. The sweeping form stays as a bare backdrop. Under 900 the masthead is the big blob alone, with the words keeping the 56vw the pair used to hold above them.
62
62
 
63
+ **2026-08-28, the third note — dark only (task 1003336, [ADR 0204](../adr/<redacted>.md)).** Reviewing the planets (task 1003319) with both grounds in front of them, the owner took option 4 of §5 after all, widened to the whole apex: "only black or only white is a bit bland, frankly I am leaning towards only dark mode for this page and keep the dark/light mode to be solely for the builder hall … we could invest into a cool space themed backdrop for the blobs." Built: `data-mode="dark"` fixed on `<html>` of every apex page, the pre-paint script and the persisted `cb-color-mode` gone, the moon/sun glyph and its wiring out of both bars, the kit rendering dark only. What the 2026-08-27 pick settled about light — the cutouts on paper under a contact shadow, the toggle staying on the landing — no longer describes the apex; the cutouts remain the closing figure and the planets' masks. The light tier of `world.css` stays until its own removal. The space backdrop is the next task (1003337).
64
+
63
65
  ## 7. Shot list: real imagery still owed
64
66
 
65
67
  No version left a placeholder slot: every image on the three pages is a real plate (the round-2 chrome set, the single blob, the two bongo pairs, the two new blob cutouts) or the real Verro Labs medallion, and the proof is the ledger rendered as page content, so no hall or hub capture is used. What the build will still owe:
@@ -170,3 +170,7 @@ Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and mea
170
170
  **What was built.** In `cosmos.css` the orb's disc is a masked colour layer over a cutout (`.disc::before`: the plate as both background and mask, `background-blend-mode` by style), sized `--d` by the object's own aspect; `[data-shape]` carries the three crops, `[data-style]` the three blends; the relationship ring is a circle behind the object (a mask clips a box-shadow away), the search spotlight an accent drop-shadow on the disc's filter after the contact shadow (dark's `--orb-shadow` is `opacity(1)`, a no-op, so the two compose). The starfield, its 3.2s twinkle, the 74px gravity grid and `--star-display` are gone; the labels, the badge, the hint and the hover card moved onto the world's type (hairline pill, label face, no accent on a fact, no shadow). In `projects.html` `ORB_TEMPLATES` walks beside `ORB_ACCENTS` in `assignAccents` (consecutive positions change shape and style; nine against five repeat every forty-five projects), `templateOf()` prefers a curated name, an owned-only entry walks by its slug, the orb carries `data-template` / `data-shape` / `data-style`, the physics measures the disc's own height, and the slot widths grew a quarter (a cutout's silhouette reads smaller than a filled disc of the same box). The platform's own orb is the pair in chrome. On the catalog side migration `<redacted>` adds the nullable `template` column with its nine-value CHECK, `upsertProject` carries it COALESCE-preserved, `POST /projects` validates it against `PROJECT_TEMPLATES`, and the public `/projects/featured` projection returns it. The kit's fixture gives Example a curated `bead-wash` and the `in-map` state reads it back off the orb.
171
171
 
172
172
  **Not done, on purpose:** no owner-facing picker — a template is set through the curation route today, the way `featured` is; whether a project's owner chooses their own planet from the manage view is the owner's call. The manage view's small `.orb` spheres keep their old look (the scope named the sky's `a.worldOrb` only).
173
+
174
+ ## 12. Dark only (task 1003336)
175
+
176
+ **Date:** 2026-08-28 (decided), 2026-08-29 (built) · the owner's call on the planets review (§11): the apex renders dark only and the light/dark switch is the builders' hall's alone — [ADR 0204](../adr/<redacted>.md). On `/projects`: `data-mode="dark"` is fixed on `<html>`, the pre-paint script is gone, the `#modeToggle` glyph and its `resolvedMode` / `paintModeToggle` wiring leave the bar (Projects, then the chip), the `.modeBtn` rules and the icon-swap twins leave `cosmos.css`, the mode-toggle probe leaves `projects.probes.json`, and `projects.states.json` declares `"modes": ["dark"]` so the kit renders the hub's twelve states at three widths in one mode (48 shots, ALL CLEAN; probes 31/31). The light tier of `cosmos.css` stays until its own removal. Why first: the space backdrop for the field (task 1003337) is a black-ground plate; one mode is what lets the field commit to it.
@@ -1263,5 +1263,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1263
1263
  landed since 1.19.411 with no explicit bump. run 33259357678. (task 1002620)
1264
1264
  1.19.413 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1265
1265
  landed since 1.19.412 with no explicit bump. run 33259477562. (task 1002620)
1266
+ 1.19.414 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1267
+ landed since 1.19.413 with no explicit bump. run 33260596712. (task 1002620)
1266
1268
  ---------------------------------------------------------------------------
1267
1269
  ```
@@ -56,7 +56,8 @@ Then a state's own `expect` (a band open, a dialog present, a field's value, a c
56
56
  {
57
57
  "page": "/projects",
58
58
  "surface": "public-landing",
59
- "modeQuery": "mode",
59
+ "modes": ["dark"],
60
+ "modeQuery": false,
60
61
  "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects"],
61
62
  "ignoreConsole": ["\\b401\\b"],
62
63
  "ignoreControls": [".skip", ".vh"],
@@ -69,6 +70,8 @@ Then a state's own `expect` (a band open, a dialog present, a field's value, a c
69
70
  }
70
71
  ```
71
72
 
73
+ `modes` names the modes the page renders (default both). A page that pins one mode — the apex pins `data-mode="dark"` on `<html>` since task 1003336 — declares it here so a plain `--page` render never walks a mode the page cannot show; `--modes` on the command line still overrides. `modeQuery: false` says no query walks a mode.
74
+
72
75
  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`, `top[[sel,px]]` (an element's viewport top — assert it closed, open a band, assert it again: the proof an overlay did not push the page). The kit validates the file before it opens a browser and names every problem.
73
76
 
74
77
  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).
@@ -235,7 +235,8 @@ function reducedMotionProblems({ running = [], frames = 0 } = {}) {
235
235
 
236
236
  // ── the states file ───────────────────────────────────────────────────────────
237
237
  // { "page": "/projects", "surface"?: "public-landing", "stub"?: { prefix?, child? },
238
- // "modeQuery"?: "mode" | false, "ignoreRequests"?: [regex strings], "ignoreConsole"?: [...],
238
+ // "modes"?: ["dark" | "light", …] (the modes the page renders; default both — a page that pins
239
+ // one mode declares it here), "modeQuery"?: "mode" | false, "ignoreRequests"?: [regex strings], "ignoreConsole"?: [...],
239
240
  // "ignoreControls"?: [selectors], "states": { name: { url?, auth?, actions?, expect? } },
240
241
  // "reducedMotion"?: { auth?, url?, actions? } }
241
242
  function validateStatesFile(json) {
@@ -245,6 +246,7 @@ function validateStatesFile(json) {
245
246
  if (json.surface !== undefined && typeof json.surface !== 'string') errors.push('surface: a string (a checkout dir or a module key)');
246
247
  errors.push(...validateStub(json.stub, 'stub'));
247
248
  if (json.modeQuery !== undefined && json.modeQuery !== false && typeof json.modeQuery !== 'string') errors.push('modeQuery: the query param the page reads for its colour mode (default "mode"), or false');
249
+ if (json.modes !== undefined && !(Array.isArray(json.modes) && json.modes.length && json.modes.every((m) => MODES.includes(m)))) errors.push('modes: a non-empty array of "dark" | "light" — the modes the page renders (default both); a page that pins one mode declares it here');
248
250
  for (const k of ['ignoreRequests', 'ignoreConsole', 'ignoreControls']) if (json[k] !== undefined && !(Array.isArray(json[k]) && json[k].every((s) => typeof s === 'string'))) errors.push(`${k}: an array of strings`);
249
251
  const states = json.states;
250
252
  if (!states || typeof states !== 'object' || Array.isArray(states) || !Object.keys(states).length) errors.push('states: required — a non-empty object of name → { url?, auth?, actions?, expect? }');
@@ -3,9 +3,10 @@
3
3
  // render.js — render a page's states and audit every render against the platform
4
4
  // floors (task 1003320, ADR 0197). The states live in a JSON file BESIDE the
5
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.
6
+ // is rendered at 1440x900 / 390x844 / 320x700 in the modes the states file names
7
+ // (`"modes"`; both by default — the apex pins dark, task 1003336; `--modes` overrides)
8
+ // through two stubs this file starts itself (signed out, signed in), then a
9
+ // reduced-motion pass per mode. Writes the shots + a JSON report; exits non-zero on any problem.
9
10
  //
10
11
  // node modules/ui-design/kit/render.js --page /projects (public-landing/projects.states.json)
11
12
  // node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
@@ -65,7 +66,7 @@ async function main() {
65
66
  const args = process.argv.slice(2);
66
67
  if (lib.hasFlag(args, '--help')) usage('');
67
68
  const { file, json, surface } = locate(args);
68
- const modes = lib.listArg(args, '--modes', [...lib.MODES]).filter((m) => lib.MODES.includes(m));
69
+ const modes = lib.listArg(args, '--modes', json.modes || [...lib.MODES]).filter((m) => lib.MODES.includes(m));
69
70
  const sizes = lib.listArg(args, '--sizes', Object.keys(lib.SIZES)).map(Number).filter((w) => lib.SIZES[w]);
70
71
  const only = lib.listArg(args, '--only', null);
71
72
  const names = Object.keys(json.states).filter((n) => !only || only.includes(n));
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.413",
3
+ "version": "1.19.414",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.413",
9
+ "version": "1.19.414",
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.413",
3
+ "version": "1.19.414",
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",
@@ -16,8 +16,9 @@
16
16
  · dark mode — a true-black ground with the card lifted just off it, white
17
17
  ink, and the accent pair flipping direction so the link tone goes BRIGHTER
18
18
 
19
- Mode resolution: prefers-color-scheme by default; an explicit choice
20
- sets data-mode="light|dark" on <html> (persisted by the page script).
19
+ Mode resolution: the apex pages pin data-mode="dark" on <html> (task 1003336,
20
+ ADR 0204) — dark is their only mode. The light tier below stays until its
21
+ own removal (the light-pair contrast tests still measure it).
21
22
 
22
23
  TOKEN TIERS:
23
24
  · CONTRACT tier — names the branding pack overrides (themeMarkup()
@@ -585,19 +586,6 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
585
586
  .who{gap:9px;}
586
587
  .who img{width:26px;height:26px;border-radius:var(--radius-pill);object-fit:cover;flex:none;
587
588
  border:1px solid var(--rule);}
588
- .modeBtn{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;
589
- cursor:pointer;background:none;border:0;border-radius:var(--radius-pill);color:var(--ink-soft);
590
- transition:color .2s cubic-bezier(.2,.7,.3,1);}
591
- .modeBtn:hover{color:var(--ink);}
592
- .modeBtn svg{width:20px;height:20px;}
593
- /* the glyph shows the mode you would GET: the moon in light, the sun in dark */
594
- .modeBtn .sun{display:none;}
595
- :root[data-mode="dark"] .modeBtn .sun{display:block;}
596
- :root[data-mode="dark"] .modeBtn .moon{display:none;}
597
- @media (prefers-color-scheme:dark){
598
- :root:not([data-mode="light"]) .modeBtn .sun{display:block;}
599
- :root:not([data-mode="light"]) .modeBtn .moon{display:none;}
600
- }
601
589
  /* the one filled shape on a bar with no pills: the landing's sign-in door */
602
590
  .signinDoor{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 26px;
603
591
  border-radius:var(--radius-pill);background:color-mix(in srgb,var(--ink) 92%,var(--bg));color:var(--bg);
@@ -685,8 +673,6 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
685
673
  .qWrap{flex:1 1 90px;max-width:480px;gap:10px;}
686
674
  .trig{font-size:13.5px;}
687
675
  .who>span{display:none;}
688
- .modeBtn{width:40px;height:40px;}
689
- .modeBtn svg{width:18px;height:18px;}
690
676
  .signinDoor{padding:0 16px;font-size:13.5px;}
691
677
  .bandIn{padding:0 12px 16px;}
692
678
  .bandRow{grid-template-columns:1fr;gap:0;}
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-mode="dark">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -17,11 +17,7 @@
17
17
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
18
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
19
19
  <link rel="stylesheet" href="/assets/cosmos.css">
20
- <script>
21
- /* Mode, resolved before first paint — the same contract as every landing page. */
22
- (function(){try{var m=localStorage.getItem('cb-color-mode');
23
- if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
24
- </script>
20
+ <!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
25
21
  <style>
26
22
  /* The reading column only. No :root block — the pack's injected theme owns the
27
23
  tokens (landing direction C1); everything below reads them. */
@@ -1,13 +1,11 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-mode="dark">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <script>
6
- /* Mode, resolved before first paint so an explicit choice never flashes. ?mode=
7
- wins (it is how a reviewer walks both modes on one URL), then the stored
8
- choice, then the OS preference. */
9
- (function(){try{var q=new URLSearchParams(location.search).get('mode');var m=(q==='light'||q==='dark')?q:localStorage.getItem('cb-color-mode');if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
10
- </script>
5
+ <!-- The apex is dark only (task 1003336, ADR 0204): data-mode="dark" is fixed on
6
+ <html> above — no pre-paint script, no stored choice, no OS preference, no
7
+ ?mode=. Light is the builders' hall's. The light tier of world.css stays
8
+ until its own removal. -->
11
9
  <meta name="viewport" content="width=device-width, initial-scale=1">
12
10
  <!-- ── S0 · HEAD METADATA — SERVER-STAMPED ──────────────────────────────────
13
11
  The {{…}} tokens below are replaced by landingPage() (src/platform-server.js)
@@ -168,11 +166,6 @@ main:focus{outline:none;box-shadow:none}
168
166
  .who{gap:9px}
169
167
  .who img{width:26px;height:26px;border-radius:var(--radius-pill);object-fit:cover;flex:none;border:1px solid var(--rule)}
170
168
  .who-name{max-width:14ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
171
- .modeBtn{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;cursor:pointer;
172
- background:none;border:0;border-radius:var(--radius-pill);color:var(--ink-soft);transition:color .2s cubic-bezier(.2,.7,.3,1)}
173
- .modeBtn:hover{color:var(--ink)}
174
- .modeBtn svg{width:20px;height:20px}
175
- .sun{display:none}
176
169
  /* the sign-in door: the one filled shape on a bar with no pills — inverted ink, lowercase */
177
170
  .pill{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 22px;
178
171
  border-radius:var(--radius-pill);font-family:inherit;font-size:15px;color:var(--ink);cursor:pointer;
@@ -180,10 +173,6 @@ main:focus{outline:none;box-shadow:none}
180
173
  .signin{background:color-mix(in srgb,var(--ink) 92%,var(--bg));color:var(--bg);border:0;
181
174
  text-transform:lowercase;font-weight:500;padding:0 26px}
182
175
  .signin:hover{background:var(--ink);color:var(--bg)}
183
- :root[data-mode="dark"] .sun{display:block}
184
- :root[data-mode="dark"] .moon{display:none}
185
- @media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .sun{display:block}
186
- :root:not([data-mode="light"]) .moon{display:none}}
187
176
 
188
177
  /* the menu bands (.menuBand — .band is this page's content sections): not floating cards. Full width on the card ground, closed by one hairline, a LAYER
189
178
  over the page under the bar — the page does not reflow (owner, 2026-08-28; task 1003332). Cells in a
@@ -491,8 +480,6 @@ body{
491
480
  .mark-word{display:none}
492
481
  .trig{font-size:13.5px}
493
482
  .who-name{display:none}
494
- .modeBtn{width:40px;height:40px}
495
- .modeBtn svg{width:18px;height:18px}
496
483
  .pill{min-height:42px;font-size:13.5px}
497
484
  .signin{padding:0 16px}
498
485
  .bandIn{padding:0 12px 16px}
@@ -683,17 +670,6 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
683
670
  </div>
684
671
  <div class="barRight">
685
672
  <button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
686
- <button class="modeBtn" id="mode" type="button" aria-label="Toggle light or dark">
687
- <svg class="moon" viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor"
688
- stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
689
- <path d="M16.2 12.4A6.8 6.8 0 0 1 7.6 3.8a6.8 6.8 0 1 0 8.6 8.6z"/>
690
- </svg>
691
- <svg class="sun" viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor"
692
- stroke-width="1.4" stroke-linecap="round">
693
- <circle cx="10" cy="10" r="3.9"/>
694
- <path d="M10 1.6v2.1M10 16.3v2.1M1.6 10h2.1M16.3 10h2.1M4.1 4.1l1.5 1.5M14.4 14.4l1.5 1.5M15.9 4.1l-1.5 1.5M5.6 14.4l-1.5 1.5"/>
695
- </svg>
696
- </button>
697
673
  <!-- the identity slot: the sign-in door signed out; the chip (the profile trigger) once
698
674
  the session resolves — swapped client-side, never a wrong-state flash -->
699
675
  <span class="whoSlot" id="who">
@@ -933,24 +909,9 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
933
909
 
934
910
  <script>
935
911
  (function () {
936
- var root = document.documentElement;
937
912
  var API = '/api/bongos';
938
913
  var GDS = '/api/gds';
939
914
 
940
- /* ── the light/dark switch. The pre-paint script in <head> already applied the
941
- stored choice; this only flips it. The icon swap is CSS. ── */
942
- function isDark() {
943
- var m = root.getAttribute('data-mode');
944
- if (m === 'dark') return true;
945
- if (m === 'light') return false;
946
- return matchMedia('(prefers-color-scheme: dark)').matches;
947
- }
948
- document.getElementById('mode').addEventListener('click', function () {
949
- var next = isDark() ? 'light' : 'dark';
950
- root.setAttribute('data-mode', next);
951
- try { localStorage.setItem('cb-color-mode', next); } catch (e) {}
952
- });
953
-
954
915
  /* ── the tagline. It is ONE server-stamped string, but the composition wants it
955
916
  on two lines, so the two halves are baked as separate spans. A host whose
956
917
  tagline is not those two halves gets it back as one wrapping line rather
@@ -1,8 +1,9 @@
1
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.",
2
+ "_": "The landing's states for modules/ui-design/kit/render.js (task 1003320): the bar and its two bands, signed out and in. Dark only (task 1003336): the apex pins data-mode=\"dark\", so `modes` names the one mode and there is no query to walk.",
3
3
  "page": "/",
4
4
  "surface": "public-landing",
5
- "modeQuery": "mode",
5
+ "modes": ["dark"],
6
+ "modeQuery": false,
6
7
  "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/provisioning/"],
7
8
  "ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
8
9
  "ignoreControls": [".skip", ".vh"],
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-mode="dark">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -15,11 +15,7 @@
15
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
17
17
  <link rel="stylesheet" href="/assets/cosmos.css">
18
- <script>
19
- /* Mode, resolved before first paint — the same contract as every landing page. */
20
- (function(){try{var m=localStorage.getItem('cb-color-mode');
21
- if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
22
- </script>
18
+ <!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
23
19
  <style>
24
20
  /* The reading column only. No :root block — the pack's injected theme owns the
25
21
  tokens (landing direction C1); everything below reads them. */
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-mode="dark">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -20,11 +20,9 @@
20
20
  re-injects the branding :root AFTER stylesheets, so the pack stays
21
21
  authoritative. Design contract: docs/design/projects-surfaces-redesign-direction.md -->
22
22
  <link rel="stylesheet" href="/assets/cosmos.css">
23
- <script>
24
- /* apply the saved color mode BEFORE first paint so there's no flash;
25
- no saved choice = follow the OS (prefers-color-scheme) */
26
- try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')document.documentElement.setAttribute('data-mode',_m);}catch(e){}
27
- </script>
23
+ <!-- The apex is dark only (task 1003336, ADR 0204): data-mode="dark" is fixed on
24
+ <html> above — no pre-paint script, no stored choice, no OS preference. Light
25
+ is the builders' hall's. cosmos.css keeps its light tier until its own removal. -->
28
26
  <style>
29
27
  /* Page-local layout only. /projects is the project HUB (task 1002743): one
30
28
  surface holding the map, your projects, and creating a new one. Every
@@ -381,19 +379,6 @@ summary{min-height:24px;padding:3px 0;}
381
379
  </div>
382
380
  <div class="barRight">
383
381
  <button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
384
- <!-- the glyph shows the mode you would GET: the moon in light, the sun in
385
- dark, swapped by cosmos.css on data-mode; the script labels it -->
386
- <button type="button" class="modeBtn" id="modeToggle" aria-label="Switch light or dark mode">
387
- <svg class="moon" viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor"
388
- stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
389
- <path d="M16.2 12.4A6.8 6.8 0 0 1 7.6 3.8a6.8 6.8 0 1 0 8.6 8.6z"/>
390
- </svg>
391
- <svg class="sun" viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor"
392
- stroke-width="1.4" stroke-linecap="round">
393
- <circle cx="10" cy="10" r="3.9"/>
394
- <path d="M10 1.6v2.1M10 16.3v2.1M1.6 10h2.1M16.3 10h2.1M4.1 4.1l1.5 1.5M14.4 14.4l1.5 1.5M15.9 4.1l-1.5 1.5M5.6 14.4l-1.5 1.5"/>
395
- </svg>
396
- </button>
397
382
  <!-- the identity chip IS the profile trigger; renderWho fills it. Signed
398
383
  out it does not exist and the sign-in door takes its slot. -->
399
384
  <button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden></button>
@@ -1098,28 +1083,6 @@ summary{min-height:24px;padding:3px 0;}
1098
1083
 
1099
1084
  function $(id) { return document.getElementById(id); }
1100
1085
 
1101
- /* ── light/dark: the sky is constant, the mode governs the cards. No saved
1102
- choice = follow the OS; the toggle stores an explicit one. ── */
1103
- function resolvedMode() {
1104
- var m = document.documentElement.getAttribute('data-mode');
1105
- if (m === 'light' || m === 'dark') return m;
1106
- return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
1107
- }
1108
- /* the glyph shows the mode you would GET (the moon in light, the sun in
1109
- dark — cosmos.css swaps them on data-mode), so the label must say the same.
1110
- No aria-pressed — this swaps a mode, it is not a two-state toggle button. */
1111
- function paintModeToggle() {
1112
- var dark = resolvedMode() === 'dark';
1113
- var el = $('modeToggle');
1114
- el.setAttribute('aria-label', dark ? 'Switch to light mode' : 'Switch to dark mode');
1115
- }
1116
- $('modeToggle').addEventListener('click', function () {
1117
- var next = resolvedMode() === 'dark' ? 'light' : 'dark';
1118
- document.documentElement.setAttribute('data-mode', next);
1119
- try { localStorage.setItem('cb-color-mode', next); } catch (e) {}
1120
- paintModeToggle();
1121
- });
1122
- paintModeToggle();
1123
1086
  function esc(s) {
1124
1087
  return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
1125
1088
  return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
@@ -82,9 +82,6 @@
82
82
  { "name": "a solo Shift tap focuses the search on the map", "continue": true,
83
83
  "steps": [["clickAt", 700, 600], ["press", "Shift"], ["wait", 100]],
84
84
  "expect": { "activeId": "mapQ" } },
85
- { "name": "the mode toggle: the label says what the press does, and the press does it", "continue": true,
86
- "steps": [["clickAt", 700, 600], ["click", "#modeToggle"], ["wait", 100]],
87
- "expect": { "attr": [["#modeToggle", "aria-label", "Switch to light mode"], ["html", "data-mode", "dark"]] } },
88
85
  { "name": "the Table deep link (the landing's band item) lands on the table, Table current (task 1003332)", "auth": true, "width": 1440, "url": "/projects?view=map&sub=table",
89
86
  "steps": [["wait", 300]],
90
87
  "expect": { "visible": ["#sub-table-body", ".tblMast"], "hidden": ["#sub-sky-body"], "text": [["[aria-current=\"page\"]", "Table"], [".tblTitle", "every project"]] } },
@@ -1,8 +1,9 @@
1
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. The card states (task 1003301) walk the poster's ladder: the full poster, the artless two-up band, the all-null floor, the platform's own card.",
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 only at 1440/390/320 (the apex pins data-mode=\"dark\", task 1003336) and audited against the platform floors. The card states (task 1003301) walk the poster's ladder: the full poster, the artless two-up band, the all-null floor, the platform's own card.",
3
3
  "page": "/projects",
4
4
  "surface": "public-landing",
5
- "modeQuery": "mode",
5
+ "modes": ["dark"],
6
+ "modeQuery": false,
6
7
  "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/provisioning/"],
7
8
  "ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
8
9
  "ignoreControls": [".skip", ".vh"],
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="en" data-mode="dark">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -15,11 +15,7 @@
15
15
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
16
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
17
17
  <link rel="stylesheet" href="/assets/cosmos.css">
18
- <script>
19
- /* Mode, resolved before first paint — the same contract as every landing page. */
20
- (function(){try{var m=localStorage.getItem('cb-color-mode');
21
- if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
22
- </script>
18
+ <!-- Dark only, like every apex page (task 1003336, ADR 0204): data-mode="dark" is fixed on <html>. -->
23
19
  <style>
24
20
  /* The reading column only. No :root block — the pack's injected theme owns the
25
21
  tokens (landing direction C1); everything below reads them. */
@@ -11,6 +11,17 @@
11
11
  // junction BEFORE git worktree remove. Worktrees INSIDE the repo
12
12
  // (.claude/worktrees/<name>, the project default) need no junction at all.
13
13
  //
14
+ // THE OTHER SHARED-STATE TRAP — `git stash` (task 1003344). The stash stack
15
+ // belongs to the REPOSITORY, not the worktree: every worktree shares one stack
16
+ // with the main checkout and with each other, so a stash here and a pop there
17
+ // hands one agent another's tree and silently drops the original entry. Commit
18
+ // on your own branch instead — a commit is worktree-local, a stash is not. The
19
+ // full rule (named-stash escape hatch, clean-baseline advice, and the
20
+ // `git fsck --unreachable` recovery path) has ONE canonical copy, in
21
+ // scripts/gds/CLAUDE.md under "Gotchas / invariants", and is learning 1000215,
22
+ // which claim.js surfaces at claim time. Deliberately short here: two full
23
+ // copies of the same prose would drift apart.
24
+ //
14
25
  // Usage:
15
26
  // node scripts/gds/worktree.js add <task-id|name> [--path <dir> | --outside] [--base <ref>]
16
27
  // node scripts/gds/worktree.js remove <name|path> [--force]
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.413'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.414'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -160,3 +160,13 @@ for (const p of PAGES) {
160
160
  }
161
161
  });
162
162
  }
163
+
164
+ // ── dark only (task 1003336) ──────────────────────────────────────────────
165
+
166
+ test('the legal pages are dark only — data-mode="dark" fixed on <html>, no mode script (task 1003336)', () => {
167
+ for (const p of PAGES) {
168
+ const html = read(p.file);
169
+ assert.match(html, /^<html lang="en" data-mode="dark">/m, `${p.file}: dark is fixed on <html>`);
170
+ assert.doesNotMatch(html, /cb-color-mode|setAttribute\('data-mode'/, `${p.file}: nothing reads or writes a mode`);
171
+ }
172
+ });
@@ -55,8 +55,6 @@ const PAGE_CSS = PAGE_STYLES.replace(/\/\*[\s\S]*?\*\//g, '');
55
55
  // The page script minus its comments — these assertions are about what the code
56
56
  // DOES, and the comments quote the very shapes being ruled out.
57
57
  const CODE = HTML.split('<script>').pop().replace(/\/\*[\s\S]*?\*\//g, '');
58
- // The pre-paint mode script, which is the FIRST <script> in <head>.
59
- const PREPAINT = HTML.split('<script>')[1].split('</script>')[0];
60
58
 
61
59
  // ── (1) the apex conditional ───────────────────────────────────────────────
62
60
 
@@ -255,11 +253,11 @@ test('every dark-mode rule is written twice — the block and its media twin (C2
255
253
  const explicit = (PAGE_CSS.match(/:root\[data-mode="dark"\]/g) || []).length;
256
254
  const media = (PAGE_CSS.match(/:root:not\(\[data-mode="light"\]\)/g) || []).length;
257
255
  assert.equal(explicit, media, 'a dark rule with no prefers-color-scheme twin only half-works');
258
- // the shipped page writes four: the mode-toggle icon swap (.sun and .moon are
259
- // one selector list each), the wide plate/geometry token block, and the two
260
- // narrow ones. The floor is the count observed at the build, so deleting a
261
- // twin fails even when the pair stays balanced.
262
- assert.ok(explicit >= 4, `expected at least 4 dark rules, found ${explicit}`);
256
+ // the shipped page writes two: the wide plate/geometry token block and the
257
+ // narrow one (the mode-toggle icon swap left with the toggle, task 1003336).
258
+ // The floor is the count observed at the build, so deleting a twin fails even
259
+ // when the pair stays balanced.
260
+ assert.ok(explicit >= 2, `expected at least 2 dark rules, found ${explicit}`);
263
261
  });
264
262
 
265
263
  test('the page links world.css root-absolute, and does not fork the hub sheet', () => {
@@ -504,16 +502,15 @@ test('the tagline is one server-stamped string, split for the composition only',
504
502
  assert.match(PAGE_CSS, /h1 \.whole\{[^}]*white-space:normal/, 'the whole-tagline fallback must wrap');
505
503
  });
506
504
 
507
- test('the mode is resolved before first paint, and ?mode= wins', () => {
508
- // A stored dark choice applied by the deferred script flashes the light page
509
- // first; and ?mode= is how a reviewer walks both modes on one URL, which the
510
- // render probes depend on.
511
- assert.match(PREPAINT, /URLSearchParams\(location\.search\)\.get\('mode'\)/);
512
- assert.match(PREPAINT, /localStorage\.getItem\('cb-color-mode'\)/);
513
- assert.match(PREPAINT, /setAttribute\('data-mode'/);
514
- // it is in <head>, ahead of the stylesheet link and the body
515
- assert.ok(HTML.indexOf('cb-color-mode') < HTML.indexOf('<body>'));
516
- assert.match(CODE, /localStorage\.setItem\('cb-color-mode', next\)/, 'the toggle persists the choice');
505
+ test('the apex is dark only: data-mode="dark" is fixed on <html>, nothing reads or writes a mode (task 1003336)', () => {
506
+ // The owner's call on the planets review: light stays the builders' hall's.
507
+ // A fixed attribute beats a script — nothing to flash, nothing to persist,
508
+ // and the OS preference never reaches the page.
509
+ assert.match(HTML, /^<html lang="en" data-mode="dark">/m);
510
+ assert.doesNotMatch(HTML, /cb-color-mode/, 'no persisted choice is read or written');
511
+ assert.doesNotMatch(HTML, /URLSearchParams\(location\.search\)\.get\('mode'\)/, '?mode= no longer walks a second mode');
512
+ assert.doesNotMatch(HTML, /matchMedia\('\(prefers-color-scheme: dark\)'\)/, 'no OS-preference read in script');
513
+ assert.doesNotMatch(CODE, /setAttribute\('data-mode'/, 'nothing flips the mode');
517
514
  });
518
515
 
519
516
  // ── the assertions tests/platform_boot.mjs will make once the apex flips ───
@@ -530,13 +527,13 @@ test('the page satisfies the parked platform_boot assertions', () => {
530
527
  // the profile band. Everything a visitor can reach is markup; the script only
531
528
  // opens and closes. These pin the shape the grader could not see from a diff.
532
529
 
533
- test('the bar is markup: the mark, the Projects trigger, the mode glyph, the sign-in door (task 1003300)', () => {
530
+ test('the bar is markup: the mark, the Projects trigger, the sign-in door — and no mode glyph (tasks 1003300, 1003336)', () => {
534
531
  const body = HTML.split('<body>')[1].split('<script>')[0];
535
532
  assert.match(body, /<header class="topbar bar">/, 'one bar, the hub\'s class beside the page\'s');
536
533
  assert.match(body, /<a class="barMark" href="\/" aria-label="\{\{productName\}\}">\s*<svg viewBox="0 0 26 20"/, 'the static mark is the way home, labelled from the stamped name');
537
534
  assert.match(body, /<span class="mark-word">\{\{productName\}\}<\/span>/, 'the product name as plain type, folded away under 900');
538
535
  assert.match(body, /<button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv"/);
539
- assert.match(body, /<button class="modeBtn" id="mode" type="button" aria-label="Toggle light or dark">/);
536
+ assert.doesNotMatch(body, /modeBtn|id="mode"|class="moon"|class="sun"/, 'the mode glyph left the bar — the apex is dark only (task 1003336)');
540
537
  assert.match(body, /<span class="whoSlot" id="who">\s*<a class="pill signin" href="\/api\/bongos\/auth\/web\/start\?return=\/">Sign in<\/a>\s*<\/span>/, 'the door stands in the identity slot until the session resolves');
541
538
  // what left with the pill bar
542
539
  assert.doesNotMatch(body, /class="pill nav-pill"|class="right-pills"|class="pill mark-pill"|data-nav="home"/, 'no nav pill, no right pills, no Home');
@@ -297,7 +297,7 @@ test('no id is declared twice — the wizard merge had four collisions to clear'
297
297
  // id makes the second node unreachable by getElementById and the first
298
298
  // silently wins. Scan the real markup only (JS builds ids like `dom-<iid>`
299
299
  // from fragments that would false-positive here).
300
- const afterHead = HUB.slice(HUB.indexOf('</script>') + '</script>'.length);
300
+ const afterHead = HUB.slice(HUB.indexOf('<body'));
301
301
  const markup = afterHead.slice(0, afterHead.indexOf('<script>'));
302
302
  assert.ok(markup.includes('id="view-new"'), 'the markup slice must span the views');
303
303
  const ids = [...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
@@ -581,13 +581,14 @@ test('completed rail steps are a way back, the repo picker is honest ARIA, the c
581
581
  assert.equal((HUB.match(/aria-controls="projectsList"/g) || []).length, 5, 'the five state filters name the list they filter');
582
582
  });
583
583
 
584
- test('control nits: orb cursor, caret under reduced motion, mode label, home label, held reduce-motion query, target sizes', () => {
584
+ test('control nits: orb cursor, caret under reduced motion, no mode switch, home label, held reduce-motion query, target sizes', () => {
585
585
  assert.doesNotMatch(COSMOS, /\.orb\{flex:0 0 54px;[^}]*cursor:pointer/, 'the base orb is not a pointer');
586
586
  assert.match(COSMOS, /\na\.orb\{cursor:pointer;\}/, 'only the linked orb is');
587
587
  assert.doesNotMatch(COSMOS, /\.orb\.ghost\{[^}]*cursor:default/);
588
588
  assert.doesNotMatch(HUB, /\.shell \.caret/, 'no caret to hide — the hub mark is static (task 1003300)');
589
- assert.match(fn('paintModeToggle'), /el\.setAttribute\('aria-label', dark \? 'Switch to light mode' : 'Switch to dark mode'\);/);
590
- assert.doesNotMatch(fn('paintModeToggle'), /aria-pressed/);
589
+ assert.doesNotMatch(SCRIPT, /modeToggle|paintModeToggle|resolvedMode|cb-color-mode/, 'the mode switch left the hub — the apex is dark only (task 1003336)');
590
+ assert.doesNotMatch(HUB, /id="modeToggle"|class="modeBtn"/);
591
+ assert.match(HUB, /^<html lang="en" data-mode="dark">/m, 'dark is fixed on <html>');
591
592
  assert.match(SCRIPT, /\$\('barMark'\)\.setAttribute\('aria-label', productName \+ ' — home'\);/, 'the mark — the only way home — is labelled from the product name');
592
593
  assert.match(SCRIPT, /var REDUCED_MQ = window\.matchMedia \? window\.matchMedia\('\(prefers-reduced-motion: reduce\)'\) : null;/);
593
594
  assert.match(SCRIPT, /if \(REDUCED\) stopSky\(\); else startSky\(\);/, 'a mid-session flip stops or restarts the sky');
@@ -39,6 +39,12 @@ test('the shipped states files validate and sit beside the page they name', () =
39
39
  assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
40
40
  assert.equal(path.basename(rel), `${lib.pageName(json.page)}.states.json`, `${rel} is named for its page`);
41
41
  }
42
+ // the apex is dark only (task 1003336): its states files name the one mode, and
43
+ // render.js reads it as the default so a plain `--page /` never renders light
44
+ for (const rel of ['public-landing/projects.states.json', 'public-landing/index.states.json']) {
45
+ assert.deepEqual(readJson(rel).modes, ['dark'], `${rel}: the apex renders dark only`);
46
+ assert.equal(readJson(rel).modeQuery, false, `${rel}: no query walks a second mode`);
47
+ }
42
48
  });
43
49
 
44
50
  test('the shipped probes files validate', () => {
@@ -60,6 +66,10 @@ test('a states file that breaks the contract is refused with the reason named',
60
66
  assert.ok(bad.errors.some((e) => /states\.ok\.auth/.test(e)));
61
67
  const stub = lib.validateStatesFile({ page: '/x', stub: { prefix: 'builders' }, states: { a: {} } });
62
68
  assert.ok(stub.errors.some((e) => /stub\.prefix/.test(e)));
69
+ const modes = lib.validateStatesFile({ page: '/x', modes: ['sepia'], states: { a: {} } });
70
+ assert.ok(modes.errors.some((e) => e.startsWith('modes:')), 'an unknown mode is refused');
71
+ assert.ok(lib.validateStatesFile({ page: '/x', modes: [], states: { a: {} } }).errors.some((e) => e.startsWith('modes:')), 'an empty modes list is refused');
72
+ assert.deepEqual(lib.validateStatesFile({ page: '/x', modes: ['dark'], states: { a: {} } }).errors, [], 'one declared mode is the dark-only shape');
63
73
  });
64
74
 
65
75
  test('a probes file needs a name and an expectation per probe, and cannot continue from nothing', () => {