@bongos/core 1.21.42 → 1.21.44

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 (68) hide show
  1. package/.bongos-core.json +72 -72
  2. package/docs/copy-inventory.md +21 -22
  3. package/docs/copy-registry.json +24 -33
  4. package/docs/design/craft-mode-direction.md +1 -1
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +5 -2
  7. package/docs/page-readings.json +3459 -3536
  8. package/modules/hall-ui/public/atlas.html +3 -3
  9. package/modules/hall-ui/public/blockers.html +3 -3
  10. package/modules/hall-ui/public/board-room.html +3 -3
  11. package/modules/hall-ui/public/city-draw.js +51 -13
  12. package/modules/hall-ui/public/city.css +9 -2
  13. package/modules/hall-ui/public/city.html +6 -6
  14. package/modules/hall-ui/public/city.js +23 -12
  15. package/modules/hall-ui/public/city.states.json +146 -12
  16. package/modules/hall-ui/public/collab.html +3 -3
  17. package/modules/hall-ui/public/commands.html +3 -3
  18. package/modules/hall-ui/public/copy-desk.html +3 -3
  19. package/modules/hall-ui/public/craft-mode.js +10 -15
  20. package/modules/hall-ui/public/deploy.html +3 -3
  21. package/modules/hall-ui/public/diagrams.html +3 -3
  22. package/modules/hall-ui/public/drachmae.html +3 -3
  23. package/modules/hall-ui/public/fleet.html +3 -3
  24. package/modules/hall-ui/public/gate.html +3 -3
  25. package/modules/hall-ui/public/goal-map.html +3 -3
  26. package/modules/hall-ui/public/goals.html +3 -3
  27. package/modules/hall-ui/public/government.html +3 -3
  28. package/modules/hall-ui/public/idea.html +3 -3
  29. package/modules/hall-ui/public/ideas.html +3 -3
  30. package/modules/hall-ui/public/index.html +3 -3
  31. package/modules/hall-ui/public/modules.html +3 -3
  32. package/modules/hall-ui/public/primer.html +3 -3
  33. package/modules/hall-ui/public/profile.html +3 -3
  34. package/modules/hall-ui/public/project-settings.html +3 -3
  35. package/modules/hall-ui/public/ranks.html +3 -3
  36. package/modules/hall-ui/public/roadmap.html +3 -3
  37. package/modules/hall-ui/public/roster.html +3 -3
  38. package/modules/hall-ui/public/sessions.html +3 -3
  39. package/modules/hall-ui/public/settings.html +3 -3
  40. package/modules/hall-ui/public/street-card.css +6 -0
  41. package/modules/hall-ui/public/street-card.js +31 -3
  42. package/modules/hall-ui/public/street-draw.js +96 -19
  43. package/modules/hall-ui/public/street.css +23 -7
  44. package/modules/hall-ui/public/street.html +11 -10
  45. package/modules/hall-ui/public/street.js +45 -13
  46. package/modules/hall-ui/public/street.states.json +68 -0
  47. package/modules/hall-ui/public/studio.html +3 -3
  48. package/modules/hall-ui/public/style.css +4 -22
  49. package/modules/hall-ui/public/task.html +3 -3
  50. package/modules/hall-ui/public/thinking.html +3 -3
  51. package/modules/hall-ui/public/tweak-editor.html +3 -3
  52. package/modules/hall-ui/public/watch.html +3 -3
  53. package/modules/hall-ui/public/work.html +3 -3
  54. package/modules/hall-ui/records/craft-mode.md +5 -5
  55. package/modules/hall-ui/records/governor-street.md +1 -0
  56. package/package-lock.json +2 -2
  57. package/package.json +1 -1
  58. package/release-notes.json +12 -0
  59. package/scripts/hall-preview/server.js +5 -2
  60. package/src/module-api.js +1 -1
  61. package/tests/craft_mode.mjs +27 -10
  62. package/tests/craft_mode_looks.mjs +5 -5
  63. package/tests/craft_mode_nav.mjs +7 -7
  64. package/tests/hall_city.mjs +42 -11
  65. package/tests/hall_matter_card.mjs +17 -0
  66. package/tests/hall_street.mjs +185 -12
  67. package/tests/hall_tweak_editor.mjs +1 -1
  68. package/tests/page_notes_panel.mjs +1 -1
@@ -80,7 +80,9 @@
80
80
  var data = { docket: null, goals: [], rollups: [], earnings: null, loaded: false, tasks: {}, room: null, constitution: null };
81
81
  // ask: the act whose chips stand open; more: the detail is open in place;
82
82
  // pick: which of the review queue the queue's card shows (task 1004566)
83
- var card = { cv: null, form: null, ask: null, more: false, pick: null, error: '', busy: false, fresh: null, answer: null };
83
+ // page: the matters of the pin that opened the card, most important first,
84
+ // for its "1 of N" pager (task 1004569); null when one matter or a desk row.
85
+ var card = { cv: null, form: null, ask: null, more: false, pick: null, error: '', busy: false, fresh: null, answer: null, page: null };
84
86
  var view = { open: null, opener: null, model: null, scale: 1, mode: null, inside: null, insideModel: null };
85
87
  // the desk's own state: rows you acted on this visit, what you did per floor,
86
88
  // the floors you cleared, the row whose facts or chips are open
@@ -247,25 +249,31 @@
247
249
  return el;
248
250
  }
249
251
 
252
+ // One pin per building (task 1004569): its tier is its class, a label shows
253
+ // only when the collision pass placed it (street-pin--shown; the rest show
254
+ // on hover or focus), and a count badge says how many matters are open there.
250
255
  function drawPin(p) {
251
- var el = node('button', 'street-pin street-pin--' + p.state);
256
+ var keys = p.matters && p.matters.length ? p.matters : [p.key];
257
+ var el = node('button', 'street-pin street-pin--' + p.state + (p.shown ? ' street-pin--shown' : ''));
252
258
  el.setAttribute('type', 'button');
253
259
  el.setAttribute('aria-label', p.aria);
254
- el.setAttribute('aria-expanded', view.open === p.key ? 'true' : 'false');
260
+ el.setAttribute('aria-expanded', keys.indexOf(view.open) >= 0 ? 'true' : 'false');
255
261
  el.setAttribute('aria-controls', 'streetCard');
256
262
  el.setAttribute('data-matter', p.key);
263
+ el.setAttribute('data-matters', keys.join(' '));
257
264
  el.setAttribute('data-building', p.building);
258
- at(el, p.x, p.y, 110, p.h);
265
+ at(el, p.x, p.y, S.PIN_W, p.h);
259
266
  var label = node('span', 'street-pin__label', p.label);
260
- label.setAttribute('title', p.label);
267
+ if (p.label) label.setAttribute('title', p.label);
261
268
  el.appendChild(label);
262
269
  var head = node('span', 'street-pin__head');
263
- if (p.state === 'needs') head.appendChild(node('span', 'street-pin__ring'));
270
+ if (p.state === 'urgent') head.appendChild(node('span', 'street-pin__ring'));
264
271
  if (p.isCount) head.appendChild(node('span', 'street-pin__count', p.count));
265
- else {
272
+ else if (p.state !== 'settled') {
266
273
  var svg = document.createElementNS(SVG_NS, 'svg');
267
274
  svg.setAttribute('width', '18');
268
275
  svg.setAttribute('height', '18');
276
+ svg.setAttribute('viewBox', '0 0 18 18');
269
277
  svg.setAttribute('aria-hidden', 'true');
270
278
  var fill = document.createElementNS(SVG_NS, 'path');
271
279
  fill.setAttribute('d', p.gf || '');
@@ -275,9 +283,10 @@
275
283
  svg.appendChild(line);
276
284
  head.appendChild(svg);
277
285
  }
286
+ if (!p.isCount && Number(p.count) > 1) head.appendChild(node('span', 'street-pin__badge', p.count));
278
287
  el.appendChild(head);
279
288
  el.appendChild(node('span', 'street-pin__stem'));
280
- el.addEventListener('click', function () { openCard(p.key, el); });
289
+ el.addEventListener('click', function () { openCard(keys[0], el, keys); });
281
290
  return el;
282
291
  }
283
292
 
@@ -366,6 +375,7 @@
366
375
  $('hudMoving').textContent = m.hud.moving;
367
376
  $('hudShare').textContent = m.hud.share;
368
377
  var dot = $('hudDot');
378
+ // the header's dots are the pins' own colours, so they double as the legend
369
379
  if (dot) dot.className = 'street-hud__dot' + (m.hud.hot ? ' street-hud__dot--hot' : '');
370
380
  var board = $('streetBoard');
371
381
  if (board) board.setAttribute('aria-label', data.loaded ? m.label : 'The street, reading the docket');
@@ -1055,7 +1065,8 @@
1055
1065
  $('streetCardWhere').textContent = cv.where;
1056
1066
  $('streetCardWhen').textContent = cv.when;
1057
1067
  $('streetCardTitle').textContent = cv.title;
1058
- C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm, ask: askFor, more: toggleMore, pick: pickQueued }, { form: card.form, ask: card.ask, more: card.more, error: card.error, busy: card.busy, fresh: card.fresh });
1068
+ var pager = C.pager ? C.pager(card.page, view.open) : null;
1069
+ C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm, ask: askFor, more: toggleMore, pick: pickQueued, page: turnPage }, { form: card.form, ask: card.ask, more: card.more, error: card.error, busy: card.busy, fresh: card.fresh, pager: pager });
1059
1070
  var frame = $('streetCard'); if (frame) frame.setAttribute('data-more', card.more ? 'open' : 'closed');
1060
1071
  placeCard();
1061
1072
  return cv;
@@ -1073,6 +1084,26 @@
1073
1084
  var mb = $('streetCardBody'), t = mb && mb.querySelector ? mb.querySelector('[data-act="more"]') : null;
1074
1085
  if (t && t.focus) t.focus();
1075
1086
  }
1087
+ // The pager turns the card to another matter of the same pin, in place.
1088
+ function turnPage(key, dir) {
1089
+ view.open = key;
1090
+ card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.fresh = null;
1091
+ if (!fillCard()) return;
1092
+ draw();
1093
+ var b = findAct($('streetCardBody'), 'page-' + dir);
1094
+ if (b && b.focus) b.focus();
1095
+ if (card.cv && card.cv.loading) loadRoom();
1096
+ }
1097
+ // The first node under `host` with data-act=`act` (a walk, so it needs no selector engine).
1098
+ function findAct(host, act) {
1099
+ var kids = host && host.children ? Array.prototype.slice.call(host.children) : [];
1100
+ for (var i = 0; i < kids.length; i++) {
1101
+ if (kids[i].getAttribute && kids[i].getAttribute('data-act') === act) return kids[i];
1102
+ var hit = findAct(kids[i], act);
1103
+ if (hit) return hit;
1104
+ }
1105
+ return null;
1106
+ }
1076
1107
  function pickQueued(key) {
1077
1108
  card.pick = key; card.ask = null; card.more = false; card.form = null; card.error = '';
1078
1109
  fillCard();
@@ -1083,7 +1114,7 @@
1083
1114
  var card = $('streetCard');
1084
1115
  if (!card || !view.open || !view.model) return;
1085
1116
  var pin = null;
1086
- view.model.pins.forEach(function (p) { if (p.key === view.open) pin = p; });
1117
+ view.model.pins.forEach(function (p) { if (p.key === view.open || (p.matters && p.matters.indexOf(view.open) >= 0)) pin = p; });
1087
1118
  var win = $('streetWindow'), sizer = $('streetSizer');
1088
1119
  var stageW = (card.parentNode && card.parentNode.clientWidth) || window.innerWidth || 1440;
1089
1120
  var originX = (sizer && sizer.offsetLeft != null ? sizer.offsetLeft : 0) - (win && win.scrollLeft ? win.scrollLeft : 0);
@@ -1091,9 +1122,10 @@
1091
1122
  var x = Math.max(16, Math.min(stageW - CARD_W - 16, Math.round(cx - CARD_W / 2)));
1092
1123
  card.style.setProperty('--street-card-x', x + 'px');
1093
1124
  }
1094
- function openCard(key, opener) {
1125
+ function openCard(key, opener, keys) {
1095
1126
  view.open = key;
1096
1127
  view.opener = opener || null;
1128
+ card.page = keys && keys.length > 1 ? keys.slice() : null;
1097
1129
  card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.fresh = null;
1098
1130
  var el = $('streetCard');
1099
1131
  if (!fillCard()) return;
@@ -1107,7 +1139,7 @@
1107
1139
  var was = view.opener;
1108
1140
  view.open = null;
1109
1141
  view.opener = null;
1110
- card.cv = null; card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.busy = false; card.fresh = null;
1142
+ card.cv = null; card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.busy = false; card.fresh = null; card.page = null;
1111
1143
  var el = $('streetCard');
1112
1144
  if (el) el.hidden = true;
1113
1145
  draw();
@@ -1195,7 +1227,7 @@
1195
1227
  var blds = ($('streetBlds') && $('streetBlds').children) || [];
1196
1228
  for (var i = 0; i < blds.length; i++) if (blds[i].getAttribute('data-key') === a.building) blds[i].setAttribute('class', 'street-answer street-answer--' + a.kind);
1197
1229
  var pins = ($('streetPins') && $('streetPins').children) || [];
1198
- for (var j = 0; j < pins.length; j++) if (pins[j].getAttribute('data-matter') === a.matter) pins[j].className += ' street-pin--answer';
1230
+ for (var j = 0; j < pins.length; j++) if (String(pins[j].getAttribute('data-matters') || pins[j].getAttribute('data-matter')).split(' ').indexOf(a.matter) >= 0) pins[j].className += ' street-pin--answer';
1199
1231
  }
1200
1232
  var toastTimer = null;
1201
1233
  function say(text) {
@@ -805,6 +805,74 @@
805
805
  ".desk-status--hot"
806
806
  ]
807
807
  }
808
+ },
809
+ "pins": {
810
+ "auth": true,
811
+ "_": "The street over the dense docket (task 1004569): one pin per building in the tier of its most important matter — urgent (needs you within a day: people, rules) largest and pulsing, needs you orange, moving small and ink, settled a grey dot — with a count where a building holds more than one open matter. Only urgent and needs-you pins show a label, and no two labels meet. Render at 1440 and 390 in both modes.",
812
+ "actions": [
813
+ [
814
+ "wait",
815
+ 900
816
+ ],
817
+ [
818
+ "clickIfVisible",
819
+ "[data-craft-close]"
820
+ ],
821
+ [
822
+ "wait",
823
+ 300
824
+ ]
825
+ ],
826
+ "expect": {
827
+ "visible": [
828
+ "#streetBoard",
829
+ ".street-pin--urgent",
830
+ ".street-pin--needs",
831
+ ".street-pin__badge"
832
+ ]
833
+ }
834
+ },
835
+ "pins-pager": {
836
+ "auth": true,
837
+ "_": "Planning holds four matters: its pin opens the most important one with \"1 of 4\" and previous/next inside the card; next turns to the second without leaving the street (task 1004569).",
838
+ "actions": [
839
+ [
840
+ "wait",
841
+ 900
842
+ ],
843
+ [
844
+ "clickIfVisible",
845
+ "[data-craft-close]"
846
+ ],
847
+ [
848
+ "wait",
849
+ 300
850
+ ],
851
+ [
852
+ "dispatchClick",
853
+ ".street-pin[data-building=\"planning\"]"
854
+ ],
855
+ [
856
+ "wait",
857
+ 600
858
+ ],
859
+ [
860
+ "click",
861
+ "[data-act=\"page-next\"]"
862
+ ],
863
+ [
864
+ "wait",
865
+ 400
866
+ ]
867
+ ],
868
+ "expect": {
869
+ "visible": [
870
+ "#streetCard",
871
+ ".mcard-pager",
872
+ "[data-act=\"page-prev\"]",
873
+ "[data-act=\"page-next\"]"
874
+ ]
875
+ }
808
876
  }
809
877
  },
810
878
  "reducedMotion": {
@@ -176,9 +176,9 @@
176
176
 
177
177
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
178
178
 
179
- <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
180
- <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
181
- <script src="/builders/shell.js?v=2026-10-04-palette"></script>
179
+ <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
180
+ <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
181
+ <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
182
182
  <script src="/builders/palette.js" defer></script>
183
183
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
184
184
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -1204,7 +1204,7 @@ html[data-fullscreen="on"][data-fullscreen-plain="on"] .app-content { padding-to
1204
1204
  }
1205
1205
 
1206
1206
  /* THE CRAFT MODE (task 1004492, craft-mode.js). The top bar's switch: one pill
1207
- holding the four crafts and Legacy, the current one lit. Narrower windows
1207
+ holding the four crafts, the current one lit. Narrower windows
1208
1208
  drop the words, and a phone shows only the current mode (or "Mode"), which
1209
1209
  opens the picker. The picker below asks once per session. Each mode's look
1210
1210
  (its colour on the current pill) is THE CRAFT MODE LOOKS, further down. */
@@ -1251,7 +1251,7 @@ html[data-fullscreen="on"][data-fullscreen-plain="on"] .app-content { padding-to
1251
1251
  .craft-switch--unset .craft-switch__open { display: inline-flex; padding: 0 7px; }
1252
1252
  }
1253
1253
 
1254
- /* The picker: a dialog over a scrim, four doors in a row and Legacy beside the
1254
+ /* The picker: a dialog over a scrim, four doors in a row above the
1255
1255
  "ask me again" box. Each door carries its scene from the approved prototype,
1256
1256
  which is night-dark in either theme because every scene is a night one. */
1257
1257
  html.craft-picker-open { overflow: hidden; }
@@ -1396,24 +1396,7 @@ html.craft-picker-open { overflow: hidden; }
1396
1396
  cursor: pointer;
1397
1397
  }
1398
1398
  .craft-picker__again input { width: 18px; height: 18px; margin: 0; accent-color: var(--accent); }
1399
- .craft-picker__side { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; }
1400
1399
  .craft-picker__hint { font-size: 13px; color: var(--ink-faint); }
1401
- .craft-picker__legacy {
1402
- display: inline-flex;
1403
- align-items: center;
1404
- height: 40px;
1405
- padding: 0 18px;
1406
- border: 0;
1407
- border-radius: var(--radius-pill);
1408
- background: none;
1409
- color: var(--ink);
1410
- font: inherit;
1411
- font-size: 14px;
1412
- cursor: pointer;
1413
- box-shadow: inset 0 0 0 1px var(--rule);
1414
- }
1415
- .craft-picker__legacy:hover { background: var(--glass); }
1416
- .craft-picker__legacy:focus-visible,
1417
1400
  .craft-picker__close:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
1418
1401
  @media (max-width: 900px) {
1419
1402
  .craft-picker__doors { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@@ -1428,12 +1411,11 @@ html.craft-picker-open { overflow: hidden; }
1428
1411
  .craft-door__head { padding: 10px 16px 0; }
1429
1412
  .craft-door__name { font-size: 18px; }
1430
1413
  .craft-door__line { flex: 0 0 100%; padding: 0 16px; box-sizing: border-box; }
1431
- .craft-picker__legacy { height: auto; min-height: 40px; white-space: normal; text-align: left; }
1432
1414
  }
1433
1415
 
1434
1416
  /* THE CRAFT MODE LOOKS (task 1004495; direction docs/design/craft-mode-direction.md).
1435
- Three modes wear a look: Artistry, Ideating, Governing. Building, Legacy and no
1436
- mode wear none — nothing below names them, so they render as the hall always
1417
+ Three modes wear a look: Artistry, Ideating, Governing. Building and no mode
1418
+ wear none — nothing below names them, so they render as the hall always
1437
1419
  did (tests/craft_mode_looks.mjs pins it). --craft marks WHERE you are, never
1438
1420
  what to press: the accent stays the action colour in every mode. Light values
1439
1421
  clear 5.4:1 on the hall's paper; the dark values are the prototype's. Light
@@ -54,9 +54,9 @@
54
54
 
55
55
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
56
56
  time so the chrome exists before the deferred page scripts execute. -->
57
- <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
58
- <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
59
- <script src="/builders/shell.js?v=2026-10-04-palette"></script>
57
+ <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
58
+ <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
59
+ <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -138,9 +138,9 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
142
- <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
143
- <script src="/builders/shell.js?v=2026-10-04-palette"></script>
141
+ <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
142
+ <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
143
+ <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
 
146
146
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -142,9 +142,9 @@
142
142
 
143
143
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
144
144
 
145
- <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
146
- <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
147
- <script src="/builders/shell.js?v=2026-10-04-palette"></script>
145
+ <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
146
+ <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
147
+ <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
148
148
  <script src="/builders/palette.js" defer></script>
149
149
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -162,9 +162,9 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
166
- <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
167
- <script src="/builders/shell.js?v=2026-10-04-palette"></script>
165
+ <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
166
+ <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
167
+ <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
168
168
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
169
169
  <script src="/builders/palette.js" defer></script>
170
170
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,9 +171,9 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
176
- <script src="/builders/shell.js?v=2026-10-04-palette"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-04-nolegacy"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
176
+ <script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
177
177
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
178
178
  <script src="/builders/palette.js" defer></script>
179
179
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -4,9 +4,9 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
4
4
 
5
5
  ## What it is
6
6
 
7
- - On entering the hall, once per browser session, a signed-in builder is asked **"What are you doing now?"**: Building, Artistry, Ideating, Governing, or **Legacy mode** (the owner's fifth choice, 2026-10-01: the hall exactly as it was). The approved prototype is the design canvas linked on the task; the doors carry its four scenes (the work desk, the night room, the sky, the governor city).
8
- - The answer is ONE attribute, `html[data-craft="building|artistry|ideating|governing|legacy"]`, readable by every hall page. Every change also fires `otb:craft` on `document` with `{ detail: { mode } }`, so a page can react without polling.
9
- - The top bar carries a switch with all five; pressing one changes the mode without a reload. Under 1180px it drops the words; at phone width only the current mode shows (or a "Choose your mode" button when there is none), and tapping it opens the picker.
7
+ - On entering the hall, once per browser session, a signed-in builder is asked **"What are you doing now?"**: Building, Artistry, Ideating or Governing. A fifth choice, **Legacy mode** (2026-10-01: the hall exactly as it was), was removed in task 1004567 (owner, 2026-10-04: "builder has all those tabs anyways"); a browser that still has `legacy` stored reads it as no mode, because `valid()` ignores any id that is not a mode. The approved prototype is the design canvas linked on the task; the doors carry its four scenes (the work desk, the night room, the sky, the governor city).
8
+ - The answer is ONE attribute, `html[data-craft="building|artistry|ideating|governing"]`, readable by every hall page. Every change also fires `otb:craft` on `document` with `{ detail: { mode } }`, so a page can react without polling.
9
+ - The top bar carries a switch with all four; pressing one changes the mode without a reload. Under 1180px it drops the words; at phone width only the current mode shows (or a "Choose your mode" button when there is none), and tapping it opens the picker.
10
10
  - Artistry is pre-selected for a builder whose `main_discipline` is `artist` (or, with no main role, whose first craft is).
11
11
 
12
12
  ## The rules an edit here must keep
@@ -23,14 +23,14 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
23
23
 
24
24
  ## The nav follows the mode (task 1004493)
25
25
 
26
- - **Each craft has a named tab list**, `MODE_TABS` in `craft-mode.js`, by NAV id. `ALWAYS_TABS` is the owner's list of tabs no mode may hide (2026-10-01: Builders, NAV id `roster`, first), and every mode's list is `ALWAYS_TABS` plus its own. Legacy and no mode have no entry, so they show every tab, as before.
26
+ - **Each craft has a named tab list**, `MODE_TABS` in `craft-mode.js`, by NAV id. `ALWAYS_TABS` is the owner's list of tabs no mode may hide (2026-10-01: Builders, NAV id `roster`, first), and every mode's list is `ALWAYS_TABS` plus its own. No mode has no entry, so it shows every tab, as before.
27
27
  - **Government shows only in Governing**, and outside that group Governing is the shortest list (Blockers, Board Room). Building is every tab except Government and the Board Room; Artistry is Studio, Goals, Your thinking; Ideating is Sky, Your thinking, Goals. The prototype names rooms that do not exist yet (Store, Page reviews, Treasury); a list names only real NAV items, and `tests/craft_mode_nav.mjs` requires every NAV item to sit in some mode.
28
28
  - **The mode only ever takes away, and never through `hidden`.** The access gate owns `hidden`; `applyNav()` marks what the mode leaves out with `[data-craft-off]`, which `style.css` hides. So a mode can never show a tab the gate hid, and switching mode just moves the attribute. A group head goes too once nothing under it shows.
29
29
  - **Two callers.** `apply()` runs it on every mode change (live, no reload), and `shell.js` `applyAccess` runs it right after its gate loop, so a later `/me` answer (a promotion, a permission) brings a head back with its tabs. Hidden tabs still open by their URL; nothing is enforced (ADR 0016).
30
30
 
31
31
  ## Each mode's look (task 1004495)
32
32
 
33
- - **Three modes wear a look; Building, Legacy and no mode wear none.** Artistry, Ideating and Governing each set `--craft` (where you are) and `--craft-on` (text on it) on `html[data-craft="<mode>"]`, with a dark twin on `html[data-theme="dark"][data-craft="<mode>"]` that also darkens the ground and tints the glass (Artistry, Ideating). Values and contrast are the direction's, `docs/design/craft-mode-direction.md`. No rule in `style.css` names `building` or `legacy`, and none matches any mode at all; `tests/craft_mode_looks.mjs` pins both, and re-measures every colour pair.
33
+ - **Three modes wear a look; Building and no mode wear none.** Artistry, Ideating and Governing each set `--craft` (where you are) and `--craft-on` (text on it) on `html[data-craft="<mode>"]`, with a dark twin on `html[data-theme="dark"][data-craft="<mode>"]` that also darkens the ground and tints the glass (Artistry, Ideating). Values and contrast are the direction's, `docs/design/craft-mode-direction.md`. No rule in `style.css` names `building`, and none matches any mode at all; `tests/craft_mode_looks.mjs` pins both, and re-measures every colour pair.
34
34
  - **The accent stays the action colour.** `--craft` paints only the switch's current pill, the current tab (tint, icon, label, marker) and the top bar's hairline.
35
35
  - **Two pieces are drawn by `craft-mode.js` `applyLook()`, on every mode change:** the eyebrow beside `#topbar-title` (`LOOKS`: "the studio · night", "the sky", "the city"; hidden at <=640px, where it overflowed phones) and the scene plate under `.app-sidebar__brand` (40px closed rail, 64px open). The plate's markup IS `artHtml(mode)`, the picker's door art, exported rather than copied; light mode recolours it to each scene's day (the city is the dark one through `invert(1) hue-rotate(180deg)`). It does not animate.
36
36
  - **Always-present tabs widened** (owner, 2026-10-03): `ALWAYS_TABS` is Builders, Collab and Profile, so every mode keeps the You and Community groups.
@@ -10,6 +10,7 @@ What it settled:
10
10
  - **The artboard is the street band.** The prototype's 1440 x 900 board less its 64 px header and 128 px footer, which the page lays out as real rows; `street.js` scales the 1440 x 708 band to the window, and on a phone keeps it near full size and lets the street scroll sideways inside its own window, so a pin stays a pin. The header fits the 80 px top-bar floor at 390 and 320 by going to two rows, its three figures as their own sideways strip.
11
11
  - **Real data where the prototype had samples.** One district per open goal (up to eight plots; past that seven stand and the eighth is a staked lot that opens the goals page), each goal's tasks drawn in proportion (built, scaffold, the prototype's `ready` slab), Review held while any source that stands in it can be read, its open matters as crates and the dashed "waiting for a tester" count pin, a reading off drawn as a dash and its reason. The full list is the header of `street-draw.js`.
12
12
  - **Every building and district is a door** to `/street#building=<key>` or `/street#district=<goal id>`, where its interior opens in place of the street (below). A pin opens the matter's short card (task 1004528 makes it act); its link is offered only for an on-site path.
13
+ - **One pin per building, its importance at a glance** (task 1004569, owner direction 2026-10-04: the pins stacked and their text overlapped). The prototype stood one pin per matter, up to three stacked; now one pin stands over a building where the prototype stands its pin, in the tier of its most important matter (`GovCity.buildingPin`, shared with the home city so both agree): urgent (needs you, due within 24 hours or overdue) largest, orange and pulsing; needs you orange, the prototype's size; moving small and ink; settled a grey dot with no label and no count. A badge counts the building's open matters when there is more than one. Only urgent and needs-you pins show a label, one short line cut by the line rule's `fit()` (the deadline always survives the cut); a moving pin shows its label on hover or focus. A collision pass (`placeLabels`) places labels most important first: a label that would meet one already placed steps up to an alternate height, and if it still meets one it waits for hover (its count and colour stay). Tapping a pin opens the most important matter's card; with more, the card carries "1 of N" with previous and next, which wrap. The header's dots wear the pin colours, so the counts double as the legend. Rendered through `street.states.json` (`pins`, `pins-pager`) and `city.states.json` (`pins`) at 1440 and 390, light and dark, on the hall-preview harness, whose docket fixture now holds every tier.
13
14
  - **A card is only as tall as its words** (task 1004568). Every part of the compact card has a cap (`LINE` in `street-card.js`): the kicker, the fact and a listed line one line, the title about two, a voice's reason two, the proposer's words three. Longer text is cut at a whole word with an ellipsis, and whatever was cut is whole under "more". The kicker is building · floor only: the automation switch's off-reason is no longer joined into it, it is the card's fact. Nothing on the card has a min-height. Rendered through `street-card.states.json` (`automation`, `automation-more`, beside the blocker and Full Idea states) at 1440 and 390, light and dark, on the hall-preview harness.
14
15
 
15
16
  Tests: `tests/hall_street.mjs` holds the kit, the row, the pins, the doors and the crates equal to the prototype by running its own `Component` class beside `street-draw.js` in both modes, then the docket rules and the page itself in a vm. The states file is `public/street.states.json`; render it against the hall-preview harness, whose `government__docket.json` fixture carries the seven sample matters.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.42",
3
+ "version": "1.21.44",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.42",
9
+ "version": "1.21.44",
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.21.42",
3
+ "version": "1.21.44",
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",
@@ -9016,5 +9016,17 @@
9016
9016
  "id": "1004568",
9017
9017
  "text": "Street cards are now only as tall as their words need. Long titles, reasons and lines are cut short with an ellipsis at a whole word, and the full text is one tap away under \"more\". The automation card no longer stretches: its"
9018
9018
  }
9019
+ ],
9020
+ "1.21.43": [
9021
+ {
9022
+ "id": "1004567",
9023
+ "text": "Legacy mode is gone from the builders hall. The mode picker now offers four doors (Building, Artistry, Ideating, Governing) and the top-bar switch shows those four. Anyone who still had Legacy saved simply lands with no mode c"
9024
+ }
9025
+ ],
9026
+ "1.21.44": [
9027
+ {
9028
+ "id": "1004569",
9029
+ "text": "The street's pins no longer pile up or talk over each other. Each building now has one pin, and its colour and size show how much it matters: largest and pulsing orange when something needs you within a day, orange when it nee"
9030
+ }
9019
9031
  ]
9020
9032
  }
@@ -195,18 +195,21 @@ const CANNED_WRITES = [
195
195
  // year from now. Any string field may use it; nothing else changes, so a fixture
196
196
  // with absolute dates is served exactly as before.
197
197
  //
198
+ // A deadline still to come is "@+20.5h" (task 1004569: the street's urgent
199
+ // tier is a matter that needs you within a day, so it must stay a day ahead).
200
+ //
198
201
  // WRITE A FRACTIONAL OFFSET. "@-3d" lands exactly on the 3-day band boundary, so
199
202
  // whether it reads as 2 days or 3 depends on the milliseconds between this
200
203
  // resolve and the browser's own clock — the row flickers between two bands
201
204
  // across reloads and a screenshot pass cannot be compared. "@-3.4d" cannot.
202
- const RELATIVE_AT = /^@-(\d+(?:\.\d+)?)([dh])$/;
205
+ const RELATIVE_AT = /^@([-+])(\d+(?:\.\d+)?)([dh])$/;
203
206
  const HOUR_MS = 60 * 60 * 1000;
204
207
 
205
208
  function resolveRelativeDates(v, nowMs) {
206
209
  if (typeof v === 'string') {
207
210
  const m = RELATIVE_AT.exec(v);
208
211
  if (!m) return v;
209
- const hours = Number(m[1]) * (m[2] === 'd' ? 24 : 1);
212
+ const hours = Number(m[2]) * (m[3] === 'd' ? 24 : 1) * (m[1] === '+' ? -1 : 1);
210
213
  return new Date(nowMs - hours * HOUR_MS).toISOString();
211
214
  }
212
215
  if (Array.isArray(v)) return v.map((x) => resolveRelativeDates(x, nowMs));
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.42'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.44'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -1,7 +1,7 @@
1
1
  // tests/craft_mode.mjs — the hall's craft mode picker (task 1004492, goal 1000095 WA6).
2
2
  //
3
3
  // On entering the hall a picker asks once per session "What are you doing
4
- // now?" (Building, Artistry, Ideating, Governing, or Legacy mode); the answer is
4
+ // now?" (Building, Artistry, Ideating or Governing); the answer is
5
5
  // html[data-craft], and a switch in the top bar changes it without a reload.
6
6
  // These cases boot the REAL shell-trail.js + craft-mode.js + shell.js in a DOM
7
7
  // stub, in the order every hall page loads them, and drive them the way a
@@ -142,7 +142,7 @@ function boot({ craft = true, session = {}, store = {}, throws = false, optOut =
142
142
  picker,
143
143
  signIn: (builder = { id: 7, rank: 'thetes' }) => shell.applyAccess({ authed: true, rank: builder.rank, builder }),
144
144
  signOut: () => shell.applyAccess({ authed: false }),
145
- // Press a picker button: a door, Legacy, or the close button.
145
+ // Press a picker button: a door or the close button.
146
146
  pick(attr, value, { askAgain = true } = {}) {
147
147
  const p = picker();
148
148
  const box = p.querySelector('[data-craft-again]');
@@ -161,7 +161,7 @@ function boot({ craft = true, session = {}, store = {}, throws = false, optOut =
161
161
  };
162
162
  }
163
163
 
164
- const MODES = ['building', 'artistry', 'ideating', 'governing', 'legacy'];
164
+ const MODES = ['building', 'artistry', 'ideating', 'governing'];
165
165
  const noSwitch = (html) => html.replace(/<div class="craft-switch[\s\S]*?<\/div>/, '');
166
166
 
167
167
  test('NO MODE IS THE HALL AS TODAY: with no mode chosen, nothing but the switch differs from a hall without craft modes', () => {
@@ -184,7 +184,8 @@ test('a signed-in builder is asked ONCE per session, and a reload does not ask a
184
184
  assert.match(html, /role="dialog" aria-modal="true"/);
185
185
  assert.match(html, /What are you doing now\?/);
186
186
  for (const m of ['building', 'artistry', 'ideating', 'governing']) assert.match(html, new RegExp(`data-craft-pick="${m}"`), `a ${m} door`);
187
- assert.match(html, /data-craft-pick="legacy"[^>]*>Legacy mode/, 'Legacy mode is the fifth choice (owner, 2026-10-01)');
187
+ assert.equal([...html.matchAll(/data-craft-pick="/g)].length, 4, 'four doors and nothing else to pick');
188
+ assert.doesNotMatch(html, /legacy/i, 'Legacy mode is gone (owner, 2026-10-04: Building has all those tabs anyway)');
188
189
  p.escape();
189
190
  assert.equal(p.picker(), null, 'Escape closes it without choosing');
190
191
  assert.equal(p.mode(), undefined, 'and leaves the hall with no mode');
@@ -229,17 +230,33 @@ test('unticking "Ask me again next session" keeps the mode for later sessions',
229
230
  assert.equal(tomorrow.picker(), null, 'and nobody is asked');
230
231
  });
231
232
 
232
- test('THE HEADER SWITCH changes the mode without a reload, and offers all five', () => {
233
+ test('THE HEADER SWITCH changes the mode without a reload, and offers the four crafts', () => {
233
234
  const p = boot();
234
235
  const opts = [...p.topbar.innerHTML.matchAll(/data-craft-opt="([a-z]+)"/g)].map((m) => m[1]);
235
- assert.deepEqual(opts, MODES, 'the four crafts, then Legacy');
236
+ assert.deepEqual(opts, MODES, 'the four crafts, and no Legacy');
236
237
  p.press('artistry');
237
238
  assert.equal(p.mode(), 'artistry');
238
- p.press('legacy');
239
- assert.equal(p.mode(), 'legacy', 'anyone can go back to the old view at any time');
240
- assert.deepEqual(p.pressed(), ['legacy']);
239
+ p.press('building');
240
+ assert.equal(p.mode(), 'building', 'anyone can switch at any time');
241
+ assert.deepEqual(p.pressed(), ['building']);
241
242
  assert.equal(p.location.reloads, 0, 'no navigation happened');
242
- assert.equal(p.session['hall-craft'], 'legacy', 'and the session remembers it');
243
+ assert.equal(p.session['hall-craft'], 'building', 'and the session remembers it');
244
+ });
245
+
246
+ test('a browser that still has Legacy stored falls back to NO MODE (task 1004567)', () => {
247
+ const session = { 'hall-craft': 'legacy', 'hall-craft-asked': '1' };
248
+ const store = { 'hall-craft-keep': 'legacy' };
249
+ const p = boot({ session, store });
250
+ p.signIn();
251
+ assert.equal(p.mode(), undefined, 'no html[data-craft]: the hall with every tab');
252
+ assert.deepEqual(p.pressed(), [], 'no switch option lit');
253
+ assert.equal(p.picker(), null, 'this session already saw the picker, so it is not asked again');
254
+ const kept = boot({ store });
255
+ kept.signIn();
256
+ assert.equal(kept.mode(), undefined, 'a kept Legacy from an earlier session is no mode too');
257
+ assert.ok(kept.picker(), 'and a new session is asked to pick one of the four');
258
+ p.press('building');
259
+ assert.equal(p.mode(), 'building', 'and any of the four can be chosen from there');
243
260
  });
244
261
 
245
262
  test('on a phone the switch shows only the current mode, and tapping it opens the picker', () => {
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Artistry, Ideating and Governing each wear a look: --craft / --craft-on tokens
5
5
  // (light + dark twins) in style.css, the header eyebrow beside the page title and
6
- // the door's scene framed under the brand. Building, Legacy and no mode wear
6
+ // the door's scene framed under the brand. Building and no mode wear
7
7
  // NOTHING — the hall as it always was. These cases run the REAL craft-mode.js over
8
8
  // a small DOM and read the REAL style.css, so a look that leaks into Building, a
9
9
  // plate that is a second copy of the art, or a colour that loses its contrast reds.
@@ -94,11 +94,11 @@ test('each looked mode frames its own door scene under the brand, and names itse
94
94
  }
95
95
  });
96
96
 
97
- test('BUILDING, LEGACY AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to them takes the look away', () => {
97
+ test('BUILDING AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to them takes the look away', () => {
98
98
  const none = boot();
99
99
  assert.equal(none.root.dataset.craft, undefined);
100
100
  assert.equal(none.plates().length + none.marks().length, 0, 'no mode draws nothing');
101
- for (const m of ['building', 'legacy']) {
101
+ for (const m of ['building']) {
102
102
  const p = boot({ 'hall-craft': m });
103
103
  assert.equal(p.plates().length + p.marks().length, 0, `${m} at boot draws nothing`);
104
104
  p.craft.choose('governing');
@@ -118,10 +118,10 @@ test('the art is drawn once: craft-mode.js has one artHtml and exports it', () =
118
118
  // Every selector in style.css that names a craft mode.
119
119
  const CRAFT_SELECTORS = [...CSS.matchAll(/data-craft="([a-z]+)"/g)].map((m) => m[1]);
120
120
 
121
- test('style.css names only the three looked modes — Building and Legacy have no rule at all', () => {
121
+ test('style.css names only the three looked modes — Building has no rule at all', () => {
122
122
  assert.ok(CRAFT_SELECTORS.length >= 12, `found ${CRAFT_SELECTORS.length} craft selectors`);
123
123
  for (const m of CRAFT_SELECTORS) assert.ok(LOOKED.includes(m), `a rule names data-craft="${m}"`);
124
- assert.doesNotMatch(CSS, /data-craft="(building|legacy)"/);
124
+ assert.doesNotMatch(CSS, /data-craft="building"/);
125
125
  assert.doesNotMatch(CSS, /data-craft\]/, 'no rule matches ANY mode, which would dress Building too');
126
126
  });
127
127