@bongos/core 1.25.3 → 1.25.4

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.
@@ -443,8 +443,19 @@
443
443
  window.location.assign(menuPath() + (rest || '?from=' + encodeURIComponent(here)));
444
444
  }
445
445
  }
446
- function buildOverlay(rest) {
447
- var src = menuPath() + withEmbed(rest);
446
+ // Your profile in the same window (task 1005249): the visor's you-block opens it over
447
+ // the place, as Options does. Its own page is where a window cannot be drawn.
448
+ function profilePath() { return base() + '/profile'; }
449
+ function openProfile() {
450
+ try { buildOverlay('', { path: profilePath(), title: 'Your profile' }); } catch (_) {
451
+ overlay = null; dialog = null;
452
+ window.location.assign(profilePath());
453
+ }
454
+ }
455
+ // `page` is { path, title } for a page other than the menu; with none, the menu.
456
+ function buildOverlay(rest, page) {
457
+ var title = page ? page.title : 'Options';
458
+ var src = (page ? page.path : menuPath()) + withEmbed(rest);
448
459
  if (!overlay) {
449
460
  if (!window.OTBDialog || typeof window.OTBDialog.bind !== 'function') throw new Error('no dialog');
450
461
  overlay = document.createElement('div');
@@ -464,7 +475,14 @@
464
475
  });
465
476
  }
466
477
  var frame = overlay.querySelector('.opt-window__frame');
467
- // A fresh address reloads the menu; the same one keeps it as it was left.
478
+ // The window says which page it holds: the box, its close button and the frame.
479
+ var named = [['.opt-window__box', 'aria-label', title], ['.opt-window__x', 'aria-label', 'Close ' + title.toLowerCase()]];
480
+ for (var n = 0; n < named.length; n += 1) {
481
+ var el = overlay.querySelector(named[n][0]);
482
+ if (el && el.setAttribute) el.setAttribute(named[n][1], named[n][2]);
483
+ }
484
+ frame.setAttribute('title', title);
485
+ // A fresh address reloads the page; the same one keeps it as it was left.
468
486
  if (frame.getAttribute('src') !== src) frame.setAttribute('src', src);
469
487
  dialog.open();
470
488
  }
@@ -477,7 +495,7 @@
477
495
  // The menu's own page never opens the window (nor a page in a frame: see the if below).
478
496
  function overlayHere() { return on() && !(document.body && document.body.getAttribute('data-page') === 'settings'); }
479
497
  if (!embedded) {
480
- window.OTBOptionsOverlay = { open: openOverlay, close: closeOverlay, isOpen: overlayOpen };
498
+ window.OTBOptionsOverlay = { open: openOverlay, openProfile: openProfile, close: closeOverlay, isOpen: overlayOpen };
481
499
  // A link to the menu (the visor's Options row, the profile chip, the start
482
500
  // screen's Options) opens the window in game mode instead of leaving the place.
483
501
  if (document.addEventListener) document.addEventListener('click', function (e) {
@@ -883,6 +883,7 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
883
883
  html[data-game="on"] .visor__me, html[data-game="on"] .visor__hints { transform: none; }
884
884
  html[data-game="on"] .visor__me { gap: var(--ds-space-hair); }
885
885
  html[data-game="on"] .visor__sprite { width: 36px; height: 36px; }
886
+ html[data-game="on"] .visor__card { display: none; }
886
887
  html[data-game="on"] .visor__num { font-size: var(--ds-text-4xl); }
887
888
  html[data-game="on"] .visor__credits { flex-direction: column; align-items: flex-start; gap: 2px; }
888
889
  html[data-game="on"] .visor__unit { font-size: var(--ds-text-2xs); line-height: 1; }
@@ -4337,36 +4338,6 @@ html[data-door="out"] #hall-main { width: 100%; }
4337
4338
  box-sizing: border-box;
4338
4339
  }
4339
4340
 
4340
- /* ---------- Home: the Standing card as the mock draws it (task 1003464) ----------
4341
- docs/design/mocks/hall/board/v3.html: an identity LINE (a 36px avatar, the
4342
- name over the handle, the rank on the right) and a three-row fact ledger —
4343
- Credits, On the board, Preferred work. With .view-zone--band stretching its
4344
- two cards, this content is what makes Standing end level with Goals. */
4345
- .home-who { display: flex; align-items: center; gap: 12px; padding-bottom: 14px; }
4346
- .home-who__avatar {
4347
- width: 36px; height: 36px; flex: none;
4348
- border-radius: 50%;
4349
- background: var(--skeleton);
4350
- border: 1px solid var(--rule);
4351
- object-fit: cover;
4352
- }
4353
- .home-who__meta { display: flex; flex-direction: column; min-width: 0; }
4354
- .home-who__name { margin: 0; font-size: 15px; font-weight: 500; color: var(--ink); }
4355
- .home-who__handle { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); }
4356
- .home-who__rank { margin-left: auto; }
4357
- .home-facts { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 0; }
4358
- .home-facts > div { display: contents; }
4359
- .home-facts dt, .home-facts dd { margin: 0; padding: 9px 0; border-top: 1px solid var(--rule-soft); }
4360
- .home-facts dt {
4361
- font-family: var(--label-face);
4362
- font-size: var(--label-size);
4363
- letter-spacing: var(--label-space);
4364
- text-transform: var(--label-case);
4365
- color: var(--ink-faint);
4366
- }
4367
- .home-facts dd { text-align: right; font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; }
4368
- .home-facts > div:first-child dt, .home-facts > div:first-child dd { border-top: 0; padding-top: 0; }
4369
-
4370
4341
  /* ---------- hall-kit components (task 2166) ----------
4371
4342
  Styles for the shared kit (hall-kit.js / window.OTBKit): pager bar,
4372
4343
  filter-bar disclosure + chips, tab strip, empty state, skeleton shimmer,
@@ -4857,7 +4828,7 @@ html.palette-open { overflow: hidden; }
4857
4828
  under it, Standing as a chip at the top right with a hover card, the
4858
4829
  Needs-attention slices as cards that open a window, and Your work beside
4859
4830
  Recently shipped. */
4860
- body[data-page="home"] .page-head--hall { position: relative; margin: 0 0 12px; padding-right: 180px; }
4831
+ body[data-page="home"] .page-head--hall { position: relative; margin: 0 0 12px; }
4861
4832
  body[data-page="home"] .stat-tile { padding: 12px 22px; min-height: 0; gap: 4px; }
4862
4833
  body[data-page="home"] .stat-tile__num { font-size: clamp(20px, 2vw, 26px); }
4863
4834
  body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
@@ -4875,38 +4846,6 @@ body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; bor
4875
4846
  .version-tab__fill { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 4px; }
4876
4847
  .version-tab__pct { color: var(--ink-soft); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
4877
4848
 
4878
- .hall-standing { position: absolute; top: 0; right: 0; height: 44px; outline: none; }
4879
- .hall-standing[hidden] { display: none; }
4880
- /* The chip IS the top of the card: anchored at the right, it grows LEFT as the
4881
- name slides in, and only .hall-standing__more drops down below it. */
4882
- .hall-standing__card {
4883
- position: absolute; top: 0; right: 0; z-index: 20; min-width: 0;
4884
- background: var(--bg-card); border-radius: 22px; padding: 4px 6px 4px 4px;
4885
- transition: min-width .2s ease, border-radius .2s ease, padding .2s ease, box-shadow .2s ease;
4886
- }
4887
- .hall-standing__card .home-who { gap: 8px; padding: 0; }
4888
- .hall-standing__card .home-who__avatar { width: 36px; height: 36px; }
4889
- .hall-standing__card .home-who__meta {
4890
- max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap;
4891
- transition: max-width .2s ease, opacity .2s ease;
4892
- }
4893
- .hall-standing__more {
4894
- max-height: 0; overflow: hidden; opacity: 0;
4895
- transition: max-height .25s ease, opacity .2s ease;
4896
- }
4897
- .hall-standing__more .home-facts { margin-top: 12px; }
4898
- .hall-standing__more .card-foot { margin: 12px 0 0; }
4899
- .hall-standing:hover .hall-standing__card,
4900
- .hall-standing:focus-within .hall-standing__card {
4901
- min-width: 300px; border-radius: var(--radius); padding: 14px 16px 16px;
4902
- box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
4903
- }
4904
- .hall-standing:hover .home-who__meta,
4905
- .hall-standing:focus-within .home-who__meta { max-width: 200px; opacity: 1; }
4906
- .hall-standing:hover .hall-standing__more,
4907
- .hall-standing:focus-within .hall-standing__more { max-height: 420px; opacity: 1; }
4908
- .hall-standing:focus-visible .hall-standing__card { outline: 2px solid var(--focus); outline-offset: 2px; }
4909
-
4910
4849
  .attn-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
4911
4850
  .attn-card {
4912
4851
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-height: 100px;
@@ -4926,20 +4865,11 @@ body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; bor
4926
4865
  .home-split > .home-split__col[hidden] + .home-split__col { grid-column: 1 / -1; }
4927
4866
 
4928
4867
  @media (max-width: 760px) {
4929
- body[data-page="home"] .page-head--hall { padding-right: 0; }
4930
- .hall-standing { position: relative; display: block; margin-top: 10px; }
4931
- .hall-standing__card { left: 0; right: auto; }
4932
- .hall-standing:hover .hall-standing__card,
4933
- .hall-standing:focus-within .hall-standing__card { min-width: min(300px, calc(100vw - 32px)); }
4934
4868
  .home-split { grid-template-columns: 1fr; }
4935
4869
  }
4936
4870
  /* With Standing gone from the band, a lone card there (Goals) takes the row. */
4937
4871
  .view-zone--band > .scroll:only-child { grid-column: 1 / -1; }
4938
- /* The Standing hover card's stat row: four figures in a 2x2 grid, label over number. */
4939
- .home-facts--grid { grid-template-columns: 1fr 1fr; gap: 10px 14px; padding: 12px 0; border-top: 1px solid var(--rule-soft); border-bottom: 1px solid var(--rule-soft); }
4940
- .home-facts--grid > div { display: flex; flex-direction: column; gap: 2px; }
4941
- .home-facts--grid dt, .home-facts--grid dd { border-top: 0; padding: 0; text-align: left; }
4942
- .home-facts--grid dd { font-family: var(--font-display); font-size: 20px; font-weight: 300; }
4872
+
4943
4873
 
4944
4874
  /* NO TOP BLOCK (task 1004836, owner 2026-10-23). The left rail highlights the
4945
4875
  page and the top bar names it, so a page draws nothing above its first
@@ -86,10 +86,40 @@ html[data-game="on"] .visor:not([hidden]) {
86
86
  position: absolute; left: var(--visor-inset-left); right: var(--visor-inset-right); bottom: var(--visor-inset-bottom);
87
87
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; column-gap: var(--ds-space-sm);
88
88
  }
89
+ /* you are a link (task 1005249): hover or focus shows your card above, a click opens your profile */
89
90
  .visor__me {
90
- justify-self: start; display: flex; align-items: center; gap: var(--ds-space-sm);
91
+ pointer-events: auto; position: relative; justify-self: start; display: flex; align-items: center; gap: var(--ds-space-sm);
92
+ color: inherit; text-decoration: none; cursor: pointer;
91
93
  transform: perspective(900px) rotateY(16deg); transform-origin: left center;
92
94
  }
95
+ .visor__me:hover { text-decoration: none; }
96
+ .visor__me:hover .visor__name, .visor__me:focus-visible .visor__name { color: var(--ink); }
97
+ .visor__me:focus-visible { outline: 0; box-shadow: var(--press-focus); }
98
+ /* the card: the sheet's night panel between two strong hairlines, standing over you */
99
+ .visor__card {
100
+ position: absolute; left: 0; bottom: calc(100% + var(--ds-space-xs)); z-index: 1;
101
+ box-sizing: border-box; width: 16rem; padding: var(--ds-space-sm);
102
+ display: flex; flex-direction: column; gap: var(--ds-space-xs);
103
+ background: var(--bg-card); color: var(--ink);
104
+ border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
105
+ opacity: 0; visibility: hidden; transform: translateY(4px);
106
+ transition: opacity var(--ds-dur-fast) var(--ds-ease), transform var(--ds-dur-fast) var(--ds-ease), visibility 0s linear var(--ds-dur-fast);
107
+ }
108
+ .visor__me:hover .visor__card, .visor__me:focus-visible .visor__card {
109
+ opacity: 1; visibility: visible; transform: none; transition-delay: 0s;
110
+ }
111
+ .visor__card-who { display: flex; flex-direction: column; gap: 2px; }
112
+ .visor__card-name { font-size: var(--ds-text-lg); color: var(--ink); }
113
+ .visor__card-handle { font-family: var(--font-mono); font-size: var(--ds-text-sm); color: var(--ink-soft); }
114
+ .visor__facts { display: flex; flex-direction: column; }
115
+ .visor__fact {
116
+ display: flex; align-items: baseline; justify-content: space-between; gap: var(--ds-space-xs);
117
+ padding: var(--ds-space-hair) 0; border-top: var(--ds-line) solid var(--rule-soft);
118
+ }
119
+ .visor__fact-k { font-size: var(--ds-text-sm); color: var(--ink-soft); }
120
+ .visor__fact-v { font-size: var(--ds-text-md); color: var(--ink); text-align: right; font-variant-numeric: tabular-nums; }
121
+ .visor__card-open { font-size: var(--ds-text-sm); color: var(--accent-ink); }
122
+ .visor__card-name, .visor__card-open, .visor__fact-k { font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case); letter-spacing: var(--name-space); }
93
123
  .visor__sprite {
94
124
  flex: none; box-sizing: border-box; width: 56px; height: 56px; object-fit: cover; image-rendering: pixelated;
95
125
  background: var(--bg-card); border: var(--line-strong) solid var(--char, var(--ink-soft)); clip-path: var(--cut-path-sm);
@@ -10,6 +10,10 @@
10
10
  // - the foot row. Left: your sprite (your character's head, wearing the visor, its rim in
11
11
  // the character's colour; task 1005218), your character's name and your credits
12
12
  // (builders.total_credits — credits only, never a payout; the spec's "does not exist").
13
+ // That block is you: on hover or focus it shows your details (name, handle, rank,
14
+ // credits, karma, preferred work), and a click opens your whole profile in the game's
15
+ // window over the place (task 1005249; the Standing chip that sat top right of Home
16
+ // went with it).
13
17
  // Centre: your character's main action, the one accent button, with its key. Right: the
14
18
  // quiet key hints, no boxes: your other actions and "Actions" (key A), which opens the
15
19
  // sheet (your character's actions, then the keys of the place you are in, then Options on
@@ -67,7 +71,9 @@
67
71
  var AVOID = ['l', '0', '/', '?', 'f', 'k', '+', '-', '='];
68
72
  var NAMES = { artisan: 'Artisan', artist: 'Artist', patron: 'Patron' };
69
73
 
70
- function esc(v) { return window.OTB && window.OTB.escapeHtml ? window.OTB.escapeHtml(v) : String(v == null ? '' : v).replace(/[^\w .,:'-]/g, ''); }
74
+ // The hall's own escaper; without it, only safe characters pass. A link's '/' and a
75
+ // handle's '@' are among them (task 1005249: stripping them broke the profile link).
76
+ function esc(v) { return window.OTB && window.OTB.escapeHtml ? window.OTB.escapeHtml(v) : String(v == null ? '' : v).replace(/[^\w .,:@\/-]/g, ''); }
71
77
  function craft() { return window.OTBCraftMode || null; }
72
78
  // The character on the glass: the held one, else this session's mode, else none.
73
79
  function character() {
@@ -91,6 +97,22 @@
91
97
  var n = me && me.builder ? Number(me.builder.total_credits) : NaN;
92
98
  return isFinite(n) ? n : null;
93
99
  }
100
+ // You, from /me: the hover card's facts. Null until /me answers. Pure, for the tests.
101
+ function meFacts(me) {
102
+ var b = me && me.builder;
103
+ if (!b) return null;
104
+ var karma = Number(b.karma);
105
+ var rank = String(b.rank || '');
106
+ var prefs = Array.isArray(b.preferred_disciplines) ? b.preferred_disciplines.filter(Boolean) : [];
107
+ return {
108
+ name: String(b.display_name || b.github_login || 'Builder'),
109
+ handle: b.github_login ? '@' + String(b.github_login) : '',
110
+ rank: rank ? rank.charAt(0).toUpperCase() + rank.slice(1) : '',
111
+ credits: credits(),
112
+ karma: isFinite(karma) ? Math.round(karma) : 0,
113
+ prefs: prefs.join(', '),
114
+ };
115
+ }
94
116
  function creditsText(n) {
95
117
  if (n == null) return '';
96
118
  var v = Math.round(n);
@@ -299,11 +321,12 @@
299
321
  progressHtml() +
300
322
  focusHtml() +
301
323
  '<div class="visor__foot">' +
302
- '<div class="visor__me">' +
324
+ '<a class="visor__me" data-visor-me href="' + esc(hallHref('/profile')) + '">' +
303
325
  (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="32" height="32" decoding="async">' : '') +
304
326
  '<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
305
327
  '<span class="visor__credits" data-visor-credits>' + creditsHtml(n) + '</span></span>' +
306
- '</div>' +
328
+ cardHtml() +
329
+ '</a>' +
307
330
  mainHtml(main) +
308
331
  '<div class="visor__hints">' + scene.actions.map(function (a, i) { return a.lead ? placeHintHtml(a, i, keys.place[i]) : ''; }).join('') + acts.slice(1).map(hintHtml).join('') +
309
332
  '<button class="visor__hint visor__hint--sheet" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
@@ -311,6 +334,24 @@
311
334
  '</div>' +
312
335
  '</div>';
313
336
  }
337
+ // The hover card over the you-block (task 1005249): who you are and your standing, then
338
+ // what a click does. Text inside the link, so a reader hears it as part of the link.
339
+ function cardHtml() {
340
+ var C = craft();
341
+ var f = meFacts(C && typeof C.me === 'function' ? C.me() : null);
342
+ if (!f) return '<span class="visor__card"><span class="visor__card-open">Open your profile</span></span>';
343
+ var fact = function (k, v) { return '<span class="visor__fact"><span class="visor__fact-k">' + esc(k) + '</span><span class="visor__fact-v">' + esc(v) + '</span></span>'; };
344
+ return '<span class="visor__card">' +
345
+ '<span class="visor__card-who"><span class="visor__card-name">' + esc(f.name) + '</span>' +
346
+ (f.handle ? '<span class="visor__card-handle">' + esc(f.handle) + '</span>' : '') + '</span>' +
347
+ '<span class="visor__facts">' +
348
+ (f.rank ? fact('Rank', f.rank) : '') +
349
+ fact('Credits', f.credits == null ? '—' : creditsText(f.credits).replace(/ credits?$/, '')) +
350
+ fact('Karma', String(f.karma)) +
351
+ fact('Preferred work', f.prefs || 'Not set yet') +
352
+ '</span>' +
353
+ '<span class="visor__card-open">Open your profile</span></span>';
354
+ }
314
355
  // The kit's segments (controls.css .seg), always beside the words that say the same thing.
315
356
  function progressHtml() {
316
357
  if (!progress) return '';
@@ -434,6 +475,14 @@
434
475
  if (t.closest('[data-visor-unfocus]')) { dropFocus(); return; }
435
476
  if (t.closest('[data-visor-lead]')) { if (focus && focus.lead && !focus.lead.busy) { closeSheet(false); focus.lead.run(); } return; }
436
477
  if (t.closest('[data-visor-sheet]')) { openSheet(); return; }
478
+ // You: your profile in the game's window, the place still drawn behind it. With no
479
+ // window (shell-game.js absent), the link opens the profile page.
480
+ if (t.closest('[data-visor-me]')) {
481
+ var W = window.OTBOptionsOverlay;
482
+ closeSheet(false);
483
+ if (W && typeof W.openProfile === 'function' && !e.metaKey && !e.ctrlKey && !e.shiftKey) { e.preventDefault(); W.openProfile(); }
484
+ return;
485
+ }
437
486
  var sc = t.closest('[data-visor-scene]');
438
487
  if (sc && scene) { var a = scene.actions[Number(sc.getAttribute('data-visor-scene'))]; closeSheet(false); if (a) a.run(); return; }
439
488
  var act = t.closest('[data-visor-act]');
@@ -508,6 +557,7 @@
508
557
  keyFor: keyFor,
509
558
  resolveKeys: resolveKeys,
510
559
  creditsText: creditsText,
560
+ meFacts: meFacts,
511
561
  character: character,
512
562
  pickVersion: pickVersion,
513
563
  segStates: segStates,
@@ -6,7 +6,7 @@ The owner's Builders' Hall feedback (idea 1001262), home-page half, built to the
6
6
  - **Banner + version tab.** The stat row is slimmer (`body[data-page="home"]` overrides), and under it
7
7
  `#home-version` draws the version the greeting's sub-line names, from the same `currentVersion()`
8
8
  read — `renderVersionTab` in `hall-render.js`. Hidden while nothing is being built.
9
- - **Standing is a chip, not a card.** The `home-standing` widget is gone; `#hall-standing` sits in the
9
+ - **Standing is a chip, not a card.** *(Removed in task 1005249: your details are the visor's you-block now, `records/visor.md`.)* The `home-standing` widget is gone; `#hall-standing` sits in the
10
10
  page head (it is chrome, not a `<section class="scroll">`, so the registry rule is untouched). The
11
11
  card it opens on hover or keyboard focus is still `#home-standing-body`, filled by `renderProfile`.
12
12
  The place row reads "Leaderboard place" — the owner asked for no "on the board" wording. The chip
@@ -19,6 +19,16 @@ The visor is the control and the scene is the view: every main call to action in
19
19
  - **The glass binds keys in game mode only.** `onKey` now returns unless `html[data-game="on"]`: the glass was never drawn outside game mode, so its keys no longer act there either (before, a held character's letter keys still ran on a classic page).
20
20
  - **The Governor City keeps ADR 0403.** In game mode (`html.visor-on`) the door's button (`#cityStart`) and the archived Street links leave the scene (`city.css`). The door is the lead action, on D, in the door's own words and case ("decide 37 in a row", or "start governing"; lowercase is the door's, task 1005210); it re-registers only when those words change. The archived links are rows in the sheet ("Archived: Street", "Archived: Building Level Views"), read off the page's own archived line so the words and the addresses cannot drift; reached by A and the keyboard. The city line stays on the scene as words, raised to `bottom: 224px` because the extra hint wraps the hints onto a second line at a desk width and the centred accent button rises with it. A building's card is a pop-up, not the scene, and keeps its own buttons and keys. Classic mode is as it was.
21
21
 
22
+ ## You on the glass (task 1005249)
23
+
24
+ - **The Standing chip is gone; you are on the visor.** The owner (2026-11-12): the profile card top right of Home (`#hall-standing`, task 1004774) is removed in both modes, and the you-block bottom left of the visor carries it. Home's two reads that fed only that chip (`/me/recent-ships?limit=5` and `/public/leaderboard`) went with it, so the Standing chip's leaderboard place and "#N as an artist" are on no page now; your shipped count is on the Profile page.
25
+ - **Hover or focus shows your card.** `.visor__me` is a link (`pointer-events: auto`), and above it `.visor__card` shows your name and handle, then Rank, Credits, Karma and Preferred work, then "Open your profile". All from `/me` through `OTBCraftMode.me()` (`meFacts`, pure); no extra read. The card is the sheet's night panel between two strong hairlines. A phone has no hover, so the card is not drawn there (the shared phone block) and a tap goes straight to the profile.
26
+ - **A click opens your whole profile in the game's window**, over the place, the way Options opens: `OTBOptionsOverlay.openProfile()` (`shell-game.js`) frames `/profile?embed=1` in the same `.opt-window`, which now names the page it holds (box, close button, frame title). Escape, the scrim and the close button shut it. With no window to draw, the link opens the profile page.
27
+ - **Keyboard order is reading order.** You are the first stop on the glass, then the accent button.
28
+ - **A fix that came with it.** The visor's fallback escaper (used when the hall's own `OTB.escapeHtml` is not loaded) stripped `/` and `@`, which would have broken the link; it lets both through now.
29
+
30
+ Tests: `tests/hall_visor_profile.mjs` (the facts in a vm; the card, the window, Escape and the plain-link fallback in a real browser).
31
+
22
32
  ## The sky on the glass (task 1005210)
23
33
 
24
34
  - **What left the sky in game mode** (`thinking.css`, under `html.visor-on`): the top row's lens switch and every pill, the jump field, the thought composer, and the foot's clock and legend. The brand line and its figures stay, as words. A building-like pop-up (the panel, the list, the start dialog, the tour) is not the scene and keeps its buttons.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.3",
3
+ "version": "1.25.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.3",
9
+ "version": "1.25.4",
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.25.3",
3
+ "version": "1.25.4",
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",
@@ -11712,5 +11712,11 @@
11712
11712
  "id": "1005199",
11713
11713
  "text": "Founding a project is faster: each finished stage goes to the Board Room by itself, every stage can be voted on at once with one \"vote yes on all\" button, and the Board Room no longer looks locked during genesis."
11714
11714
  }
11715
+ ],
11716
+ "1.25.4": [
11717
+ {
11718
+ "id": "1005249",
11719
+ "text": "Your details now sit on your character at the bottom left of the game: hover to see them, click to open your full profile without leaving the scene. The old profile card at the top right is gone."
11720
+ }
11715
11721
  ]
11716
11722
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.25.3'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.4'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -270,23 +270,6 @@ test('the band ends level: no align-items on the grid, so the two cards stretch'
270
270
  'and the single-column fallback still exists below 768px (the task text said 860; the sheet says 768)');
271
271
  });
272
272
 
273
- test('the Standing card is the mock\'s: an identity line and a three-row fact ledger, no banner', () => {
274
- const block = hallRenderJs.match(/const homeStanding = document\.getElementById\('home-standing-body'\);[\s\S]*?renderSystemNotices\(/);
275
- assert.ok(block, 'the Home card render is where it was');
276
- assert.match(block[0], /class="home-who"/, 'the identity line');
277
- assert.match(block[0], /<dt>\$\{escapeHtml\(curLabel\(true\)\)\}<\/dt>/,
278
- 'the currency row takes its label from the branding contract at render time — a data-currency-label attribute is rebranded once at load, before this row exists');
279
- for (const label of ['Leaderboard place', 'Preferred work']) {
280
- assert.match(block[0], new RegExp(`<dt>${label}</dt>`), `the ledger row: ${label}`);
281
- }
282
- assert.match(block[0], /: PREFS_EMPTY\}/, 'the empty-preference copy is the one string both Standing surfaces share');
283
- assert.doesNotMatch(block[0], /Banner\}/, 'no banner is stacked into the card any more');
284
- for (const sel of ['.home-who', '.home-facts', '.home-facts dt', '.home-facts dd']) {
285
- assert.ok(rule(styleCss, sel), `style.css styles ${sel}`);
286
- }
287
- assert.match(rule(styleCss, '.home-who__avatar') || '', /var\(--skeleton\)/, 'the avatar placeholder is the skeleton tone, not --bg-deep (pure black in dark)');
288
- });
289
-
290
273
  test('the system notices are inbox LINES, first in the card, and counted by its refresh()', () => {
291
274
  // Anchored on the statement AFTER the markup, so a ';' inside the markup or its comments cannot cut it short.
292
275
  const mount = goalInboxJs.match(/host\.innerHTML =([\s\S]*?);\n host\.addEventListener/);
@@ -322,7 +305,7 @@ test('the greeting reads /me only; the version tab and its progress read left Ho
322
305
 
323
306
  // Boot hall-render.js against a stub DOM and a ctx of getters (the way
324
307
  // builders.js builds it) and read what the greeting and the card actually say.
325
- function bootHallRender({ me, progress = null, leaderboard = [], ctxExtra = {} }) {
308
+ function bootHallRender({ me, progress = null, ctxExtra = {} }) {
326
309
  const els = new Map();
327
310
  const makeEl = () => ({
328
311
  dataset: {}, style: {}, textContent: '', hidden: false, _html: '', innerHTML: '',
@@ -364,7 +347,6 @@ function bootHallRender({ me, progress = null, leaderboard = [], ctxExtra = {} }
364
347
  ...ctxExtra,
365
348
  };
366
349
  const api = windowObj.OTBHallRender(ctx);
367
- api.lbState.rows = leaderboard;
368
350
  return { api, el: (id) => documentObj.getElementById(id) };
369
351
  }
370
352
 
@@ -374,7 +356,6 @@ const ME = {
374
356
  cost: { level: 'warn', spend_mtd_usd: 80, budget_mtd_usd: 100, pct_used: 80 },
375
357
  needs_rebase_tasks: [{ id: 1003460 }],
376
358
  };
377
- const LEADERBOARD = [{ github_login: 'a', total_credits: 20000 }, { github_login: 'percy', total_credits: 14441 }, { github_login: 'b', total_credits: 3 }, { github_login: 'c', total_credits: 1 }];
378
359
  const PROGRESS = { progress: [{ version_id: 'BONGOS-V1', status: 'building', total_count: 605, percent_complete: 75 }, { version_id: 'V2', status: 'planning', total_count: 9000, percent_complete: 0 }] };
379
360
 
380
361
  test('RUN: the greeting names the builder, and writes nothing else', () => {
@@ -386,22 +367,13 @@ test('RUN: the greeting names the builder, and writes nothing else', () => {
386
367
  assert.equal(el('home-version').textContent || '', '', 'no version tab is drawn');
387
368
  });
388
369
 
389
- test('RUN: the Standing card carries the three facts, the place as "2nd of 4", and no banner', () => {
390
- const { api, el } = bootHallRender({ me: ME, leaderboard: LEADERBOARD });
370
+ test('RUN: Home draws no Standing chip, your details are on the visor now (task 1005249)', () => {
371
+ const { api, el } = bootHallRender({ me: ME });
372
+ assert.equal(api.lbState, undefined, 'no leaderboard rows are kept for a place nobody shows');
373
+ assert.equal(api.setRecentShips, undefined, 'no shipped count is kept for a chip that is gone');
391
374
  api.renderProfile(ME.builder);
392
- const html = el('home-standing-body').innerHTML;
393
- assert.match(html, /<p class="home-who__name">Percy Jackson<\/p>/);
394
- assert.match(html, /<p class="home-who__handle">@percy<\/p>/);
395
- // written from curLabel(true), not a data-currency-label attribute: brand-apply
396
- // rebrands those once at load, and this row is stamped after /me answers
397
- assert.match(html, /<dt>Credits<\/dt><dd>14,441<\/dd>/);
398
- assert.match(html, /<dt>Leaderboard place<\/dt><dd>2nd of 4<\/dd>/);
399
- assert.match(html, /<dt>Preferred work<\/dt><dd>Not set yet<\/dd>/, 'the mock\'s copy for an empty preference');
400
- assert.doesNotMatch(html, /profile__(needs|budget|rebase)/, 'the banners are not in the card');
401
- const withPrefs = bootHallRender({ me: ME, leaderboard: [] });
402
- withPrefs.api.renderProfile({ ...ME.builder, preferred_disciplines: ['artisan', 'ui'] });
403
- assert.match(withPrefs.el('home-standing-body').innerHTML, /<dd>artisan, ui<\/dd>/);
404
- assert.match(withPrefs.el('home-standing-body').innerHTML, /<dt>Leaderboard place<\/dt><dd>—<\/dd>/, 'no roll yet: a dash, not a wrong place');
375
+ // the stub document hands back a blank element for any id, so "drew nothing" is an empty one
376
+ assert.equal(el('home-standing-body').innerHTML || '', '', 'no Standing card is written');
405
377
  });
406
378
 
407
379
  // task 1005046: "Where you stand" left the retired Ranks page for Accomplishments,
@@ -7,9 +7,8 @@
7
7
  // table and a count. This pins the bounded shape:
8
8
  //
9
9
  // 1. the route answers a small page plus `total`, counting only when it must;
10
- // 2. hall-render.js keeps Home on that small page, and fetches the full ledger
11
- // only when the Accomplishments view is open — and again only when the
12
- // newest ships or the count moved.
10
+ // 2. Home asks for no ships at all (task 1005249 removed the last count it
11
+ // showed); the Profile page reads its own ledger.
13
12
  //
14
13
  // (builders.js's own request is pinned in tests/hall_landing_boot.mjs, which
15
14
  // already boots the real page.) DB-free: vm + stubs.
@@ -120,31 +119,16 @@ function loadRender({ view = 'home' } = {}) {
120
119
  return { r, el, fetched, showView };
121
120
  }
122
121
 
123
- const homePage = (total, firstId = 3000) => ({ ships: [ship(firstId), ship(firstId - 1)], total });
124
-
125
- test('Home: the Standing chip\'s count comes from the small page, with no ledger fetch', async () => {
126
- // task 1005054: the Shipped tile and the five-row table left Home with the stats
127
- // strip and "Your work"; the small page still feeds the Standing chip's count.
128
- const { r, el, fetched } = loadRender();
129
- assert.equal(r.HOME_SHIPS_LIMIT, 5);
130
- r.setRecentShips(homePage(3000));
131
- await new Promise((done) => setTimeout(done, 0));
132
- assert.deepEqual(fetched, [], 'Home never asks for the full history');
133
- assert.equal(el('standing-shipped').textContent, '3000', 'the chip shows the server total, not rows held');
134
- assert.equal(el('recent-ships tbody').innerHTML, '', 'the ledger is untouched until its view opens');
135
- });
136
-
137
- // task 1005050: the full ledger was the Accomplishments view's, and that view
138
- // folded into the Profile page (whose Works ledger reads its own record). Home
139
- // now NEVER asks for the whole history — not on a view change, not on a refresh.
140
- test('Home never downloads the whole history, whatever view event or refresh arrives', async () => {
141
- const { r, el, fetched, showView } = loadRender({ view: 'standing' });
142
- r.setRecentShips(homePage(3000));
122
+ // task 1005249: the Standing chip, the last thing on Home that showed a shipped
123
+ // count, is gone, so Home keeps no ships at all — not a small page, not a count —
124
+ // and asks for none, whatever view event arrives.
125
+ test('Home keeps and asks for no ships at all, whatever view event arrives', async () => {
126
+ const { r, fetched, showView } = loadRender({ view: 'home' });
127
+ assert.equal(r.setRecentShips, undefined, 'no ships setter');
128
+ assert.equal(r.HOME_SHIPS_LIMIT, undefined, 'no small page size');
129
+ assert.equal(r.loadShipLedger, undefined, 'no ledger loader');
143
130
  showView('standing');
144
- r.setRecentShips(homePage(3001, 3001)); // this builder shipped
145
131
  await new Promise((done) => setTimeout(done, 0));
146
- assert.deepEqual(fetched, [], 'no ?limit=all read from Home');
147
- assert.equal(el('standing-shipped').textContent, '3001', 'the chip follows the newest total');
148
- assert.equal(r.loadShipLedger, undefined, 'the ledger loader went with the view');
132
+ assert.deepEqual(fetched, [], 'no ships read from Home');
149
133
  });
150
134
 
@@ -91,10 +91,9 @@ async function boot({ meStatus, settle = false }) {
91
91
  wirePager: () => {},
92
92
  },
93
93
  OTBHallRender: () => ({
94
- analyticsState: {}, lbState: {},
94
+ analyticsState: {},
95
95
  shipsPager: { page: 1 },
96
- // task 1004854: Home asks for this many ships; the ledger's full read lives in hall-render.js.
97
- HOME_SHIPS_LIMIT: 5, setRecentShips: spy('setRecentShips'), populateShipVersionFilter: spy('populateShipVersionFilter'),
96
+ populateShipVersionFilter: spy('populateShipVersionFilter'),
98
97
  mountHallWidgets: spy('mountHallWidgets'),
99
98
  renderLanding: spy('renderLanding'),
100
99
  renderCurveBody: spy('renderCurveBody'), renderGreeting: spy('renderGreeting'),
@@ -154,10 +153,10 @@ test('a signed-in builder still gets the hall', async () => {
154
153
 
155
154
  // task 1004854: Home used to download the builder's WHOLE ship history here, on
156
155
  // every load and every live refresh — ~3.4 s of database time for 3,000 ships, to
157
- // fill a five-row table and a count. It asks for the small page only.
158
- test('Home asks for its few newest ships, never the full history', async () => {
156
+ // fill a five-row table and a count. Since task 1005249 it shows no ship count at
157
+ // all (the Standing chip went), so it asks for no ships and no leaderboard.
158
+ test('Home asks for no ships and no leaderboard', async () => {
159
159
  const calls = await boot({ meStatus: 200, settle: true });
160
- const ships = calls.requests.filter((r) => r.includes('/me/recent-ships'));
161
- assert.deepEqual(ships, ['GET /api/bongos/me/recent-ships?limit=5'], `ship requests were: ${ships.join(', ') || 'none'}`);
162
- assert.ok(calls.rendered.includes('setRecentShips'), 'the small page is handed to the renderer');
160
+ const reads = calls.requests.filter((r) => r.includes('/me/recent-ships') || r.includes('/leaderboard'));
161
+ assert.deepEqual(reads, [], `Home still read: ${reads.join(', ')}`);
163
162
  });
@@ -905,9 +905,9 @@ test('Accomplishments is gone from the sidebar, and every old link lands on your
905
905
  }
906
906
  // No view is left for them to open on the index.
907
907
  assert.ok(!/id="view-standing"/.test(fs.readFileSync(path.join(HALL, 'index.html'), 'utf8')), 'the view is gone');
908
- // Home's Standing card still names the shipped count and links to the profile.
908
+ // Home's Standing card went in task 1005249: the visor's you-block opens the profile now
909
+ // (tests/hall_visor_profile.mjs runs it).
909
910
  const render = fs.readFileSync(path.join(HALL, 'hall-render.js'), 'utf8');
910
- assert.match(render, /View full profile →/);
911
911
  assert.ok(!/\$\('#profile-body'\)/.test(render), 'the Accomplishments band is not drawn any more');
912
912
  const fn = render.slice(render.indexOf(' function renderProfile(builder) {'), render.indexOf(' function renderSystemNotices('));
913
913
  assert.match(fn, /renderSystemNotices\(\{ rebaseTasks, need, actionNeeds, cost \}\)/, 'Home still gets every notice');
@@ -503,9 +503,16 @@ test('the glass is the kit\'s: the cut accent button, ice hints with no box, the
503
503
  assert.equal(g.placeShadow, 'none', 'type on a plate needs no halo');
504
504
  assert.equal(g.radius, '0px', 'no round thing on the glass');
505
505
  assert.match(g.spriteClip, /^polygon\(/, 'the sprite sits in a cut box');
506
+ // task 1005249: you (left of the foot row) are a link to your profile now, so the
507
+ // stops run in reading order: you, then the accent button. Both take the inset ring.
508
+ const focused = () => page.evaluate(() => { const a = document.activeElement; const s = getComputedStyle(a); return { cls: a.className, outline: s.outlineStyle, ring: s.boxShadow }; });
506
509
  await page.keyboard.press('Tab');
507
- const f = await page.evaluate(() => { const a = document.activeElement; const s = getComputedStyle(a); return { cls: a.className, outline: s.outlineStyle, ring: s.boxShadow }; });
508
- assert.equal(f.cls, 'visor__main', 'the accent button is the first stop');
510
+ const me = await focused();
511
+ assert.equal(me.cls, 'visor__me', 'you are the first stop, reading left to right');
512
+ assert.match(me.ring, /inset/, 'you take the inset ring too');
513
+ await page.keyboard.press('Tab');
514
+ const f = await focused();
515
+ assert.equal(f.cls, 'visor__main', 'the accent button is the next stop');
509
516
  assert.equal(f.outline, 'none');
510
517
  assert.match(f.ring, /inset/, 'focus is the inset two-tone ring (--press-focus)');
511
518
  await ctx.close();