@bongos/core 1.19.412 → 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.
Files changed (38) hide show
  1. package/.bongos-core.json +57 -42
  2. package/clients/bongos-client/index.d.ts +4 -4
  3. package/docs/adr/0204-the-apex-is-dark-only.md +36 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/api/openapi.json +25 -10
  6. package/docs/api-reference.md +2 -2
  7. package/docs/copy-inventory.md +405 -403
  8. package/docs/copy-registry.json +538 -520
  9. package/docs/design/landing-page-direction-v2.md +2 -0
  10. package/docs/design/projects-hub-direction-v2.md +4 -0
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/recipes/ui-look-before-you-ship.md +4 -1
  13. package/modules/hall-ui/public/settings.html +18 -0
  14. package/modules/hall-ui/public/settings.js +93 -10
  15. package/modules/platform-identity/migrations/platform_identity_012_account_privacy.sql +59 -0
  16. package/modules/platform-identity/platform-identity.js +91 -1
  17. package/modules/platform-identity/routes/profile.js +68 -17
  18. package/modules/ui-design/kit/lib.js +3 -1
  19. package/modules/ui-design/kit/render.js +5 -4
  20. package/package-lock.json +2 -2
  21. package/package.json +1 -1
  22. package/public-landing/assets/cosmos.css +3 -17
  23. package/public-landing/contact.html +2 -6
  24. package/public-landing/index.html +5 -44
  25. package/public-landing/index.states.json +3 -2
  26. package/public-landing/privacy.html +2 -6
  27. package/public-landing/projects.html +4 -41
  28. package/public-landing/projects.probes.json +0 -3
  29. package/public-landing/projects.states.json +3 -2
  30. package/public-landing/terms.html +2 -6
  31. package/scripts/gds/worktree.js +11 -0
  32. package/src/module-api.js +1 -1
  33. package/tests/account_privacy_flags.mjs +299 -0
  34. package/tests/landing_legal_pages.mjs +10 -0
  35. package/tests/landing_page.mjs +16 -19
  36. package/tests/projects_hub.mjs +1 -1
  37. package/tests/projects_hub_pre_uat.mjs +4 -3
  38. package/tests/ui_design_kit.mjs +10 -0
@@ -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', () => {