@bongos/core 1.24.31 → 1.24.33

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.
@@ -216,20 +216,29 @@
216
216
  // under the brand (artHtml, the picker's own art, so the two never drift).
217
217
  // Building and no mode have no entry here and wear nothing.
218
218
  var LOOKS = { artistry: 'the studio · night', ideating: 'the sky', governing: 'the city' };
219
+ // The plate is the Home button in every mode (owner, 2026-11-04, task 1005065): the
220
+ // mode's scene with a home mark over it. Building wears no look, so its plate shows
221
+ // the city, the scene its Home is.
222
+ var PLATE_ART = { building: 'governing', artistry: 'artistry', ideating: 'ideating', governing: 'governing' };
223
+ var HOME_MARK = '<span class="craft-plate__home" aria-hidden="true"><svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round">' +
224
+ '<path d="M3 8.5 9 3l6 5.5V15a1 1 0 0 1-1 1h-3.5v-4h-3v4H4a1 1 0 0 1-1-1V8.5Z"/></svg></span>';
219
225
  function remove(el) { if (el && el.parentNode) el.parentNode.removeChild(el); }
220
226
  function applyLook(mode) {
221
227
  var look = Object.prototype.hasOwnProperty.call(LOOKS, mode) ? LOOKS[mode] : null;
228
+ var art = Object.prototype.hasOwnProperty.call(PLATE_ART, mode) ? PLATE_ART[mode] : null;
222
229
  var plate = document.getElementById('craft-plate');
223
- if (plate && plate.getAttribute('data-plate') !== (look ? mode : null)) { remove(plate); plate = null; }
230
+ if (plate && plate.getAttribute('data-plate') !== (art ? mode : null)) { remove(plate); plate = null; }
224
231
  var nav = document.getElementById('app-sidebar');
225
- var brand = look && nav && nav.querySelector ? nav.querySelector('.app-sidebar__brand') : null;
232
+ var brand = art && nav && nav.querySelector ? nav.querySelector('.app-sidebar__brand') : null;
226
233
  if (!plate && brand && brand.parentNode) {
227
- plate = document.createElement('div');
234
+ plate = document.createElement('a');
228
235
  plate.id = 'craft-plate';
229
236
  plate.className = 'craft-plate';
237
+ plate.setAttribute('href', '/');
238
+ plate.setAttribute('aria-label', 'Home');
239
+ plate.setAttribute('title', 'Home');
230
240
  plate.setAttribute('data-plate', mode);
231
- plate.setAttribute('aria-hidden', 'true');
232
- plate.innerHTML = artHtml(mode);
241
+ plate.innerHTML = artHtml(art) + HOME_MARK;
233
242
  brand.parentNode.insertBefore(plate, brand.nextSibling);
234
243
  }
235
244
  var mark = document.getElementById('craft-eyebrow');
@@ -22,6 +22,10 @@
22
22
  // (home-city-draw.js skySvg) from the same read, and the ladder ranks the week's
23
23
  // ideators — ideas filed, votes cast, ideas that became work. Every other craft keeps
24
24
  // the builder's city exactly as it was.
25
+ //
26
+ // The governor's Home is the street (task 1005064; the v16 round's v16-governing.html):
27
+ // choosing Governing on Home, or opening Home with it chosen, goes to the street page
28
+ // (GovCity.STREET_HREF), which is never redrawn here.
25
29
  (function () {
26
30
  'use strict';
27
31
  if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
@@ -295,7 +299,17 @@
295
299
  host.hidden = !(art || shown);
296
300
  }
297
301
 
302
+ // ---- the governor's Home is the street; tests swap the hop for a recorder ----
303
+ function streetHop() {
304
+ const href = window.GovCity && window.GovCity.STREET_HREF;
305
+ if (document.documentElement.dataset.craft !== 'governing' || !href) return false;
306
+ (window.OTBHomeStreetHop || ((h) => window.location.assign(h)))(href);
307
+ return true;
308
+ }
309
+
298
310
  function mount(hostEl) {
311
+ document.addEventListener('otb:craft', streetHop);
312
+ if (streetHop()) return; // leaving for the street: build nothing, read nothing
299
313
  host = hostEl;
300
314
  host.innerHTML =
301
315
  '<h2 id="home-city-h" class="sr-only">The project this week</h2>' +
@@ -1614,6 +1614,9 @@ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing
1614
1614
  never redrawn: each scene's pieces are placed from its edges or its centre. */
1615
1615
  .craft-plate {
1616
1616
  position: relative;
1617
+ display: block;
1618
+ color: inherit;
1619
+ text-decoration: none;
1617
1620
  height: 40px;
1618
1621
  margin: 10px 8px 0;
1619
1622
  border-radius: var(--radius-sm);
@@ -1631,6 +1634,26 @@ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing
1631
1634
  transform-origin: 0 0;
1632
1635
  }
1633
1636
  .craft-plate .craft-art__city { top: 0; }
1637
+ /* At the plate's size the studio's houses vanished into the night (owner, 2026-11-04):
1638
+ lift them off the sky so the scene reads. The picker's larger door is unchanged. */
1639
+ .craft-plate .craft-art__house { background: #232b4d; }
1640
+ /* The plate is the Home button (task 1005065): a home mark sits over the scene. */
1641
+ .craft-plate__home {
1642
+ position: absolute;
1643
+ left: 50%;
1644
+ top: 50%;
1645
+ width: 24px;
1646
+ height: 24px;
1647
+ margin: -12px 0 0 -12px;
1648
+ display: grid;
1649
+ place-items: center;
1650
+ border-radius: 999px;
1651
+ color: #fff;
1652
+ background: var(--scrim);
1653
+ }
1654
+ .craft-plate__home svg { width: 14px; height: 14px; }
1655
+ .craft-plate:hover .craft-plate__home { background: rgba(0, 0, 0, .8); }
1656
+ .craft-plate:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
1634
1657
  .app-sidebar:hover .craft-plate,
1635
1658
  .app-sidebar:focus-within .craft-plate { height: 64px; margin: 10px 16px 0; }
1636
1659
  .app-sidebar:hover .craft-plate .craft-art,
@@ -175,6 +175,16 @@ leaderboard.
175
175
  - Pinned in `tests/home_studio.mjs` and `tests/home_studio_widget.mjs` (real browser, empty states included); harness
176
176
  fixtures `copy-desk__studio.json` and `home__studio__works.json`.
177
177
 
178
+ ## The governor's Home is the street (task 1005064)
179
+
180
+ The owner, 2026-11-04, after the four Homes went live: "when you tab between the pages the home screen
181
+ doesn't change." Building, Ideating and Artistry had their own Homes, but Governing still showed the
182
+ builder's city. The v16 round settled it ([`v16-governing.html`](../../../docs/design/mocks/hall/home/v16-governing.html)):
183
+ Governing's Home **is** the street page, never redrawn. Governing's nav already leads with `city`, the street.
184
+ So choosing Governing on Home, or opening Home with it chosen, goes to `GovCity.STREET_HREF`
185
+ (`home-city.js` `streetHop`). In Governing the street's "← Home" link therefore lands back on the street;
186
+ switching craft from the top bar leaves it. Pinned in `tests/home_city_widget.mjs`.
187
+
178
188
  ## Private areas
179
189
 
180
190
  A private goal the viewer is neither a member of nor an Archon over still stands as a tower (its counts are structure), but the server sends no title for it or for the ideas tied to it; the drawing labels them "A private area" and "A private idea" (ADR 0112, as the goal map and the sky apply it). Pinned in `tests/home_city.mjs`.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.31",
3
+ "version": "1.24.33",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.31",
9
+ "version": "1.24.33",
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.24.31",
3
+ "version": "1.24.33",
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",
@@ -11145,5 +11145,17 @@
11145
11145
  "id": "1005054",
11146
11146
  "text": "Home now shows the whole project as a living city: towers that grow as work ships, the people building them, and a slider to replay the last ten weeks."
11147
11147
  }
11148
+ ],
11149
+ "1.24.32": [
11150
+ {
11151
+ "id": "1005064",
11152
+ "text": "The governor's Home is now the street: choosing Owner on Home takes you straight to it, so each of the four crafts has its own Home."
11153
+ }
11154
+ ],
11155
+ "1.24.33": [
11156
+ {
11157
+ "id": "1005065",
11158
+ "text": "The small picture at the top of the sidebar is now the Home button in every mode, with a home icon on it."
11159
+ }
11148
11160
  ]
11149
11161
  }
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.24.31'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.33'; // 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');
@@ -86,25 +86,33 @@ test('each looked mode frames its own door scene under the brand, and names itse
86
86
  const plate = p.plates()[0];
87
87
  assert.equal(p.brand.nextSibling, plate, `${m}: the plate sits right under the brand`);
88
88
  assert.equal(plate.getAttribute('data-plate'), m);
89
- assert.equal(plate.getAttribute('aria-hidden'), 'true', 'the plate is decoration');
90
- assert.equal(plate.innerHTML, p.craft.artHtml(m), `${m}: the plate IS the picker's door art, not a second copy`);
89
+ assert.equal(plate.tagName, 'A', `${m}: the plate is a link`);
90
+ assert.equal(plate.getAttribute('href'), '/', `${m}: the plate goes Home`);
91
+ assert.equal(plate.getAttribute('aria-label'), 'Home', `${m}: named Home for a screen reader`);
92
+ assert.ok(plate.innerHTML.startsWith(p.craft.artHtml(m)), `${m}: the plate IS the picker's door art, not a second copy`);
93
+ assert.match(plate.innerHTML, /class="craft-plate__home" aria-hidden="true"/, `${m}: a home mark sits over the scene`);
91
94
  assert.equal(p.marks().length, 1, `${m}: exactly one eyebrow`);
92
95
  assert.equal(p.title.nextSibling, p.marks()[0], `${m}: the eyebrow sits right after the page title`);
93
96
  assert.equal(p.marks()[0].textContent, WORDS[m], `${m}: in the prototype's own words`);
94
97
  }
95
98
  });
96
99
 
97
- test('BUILDING AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to them takes the look away', () => {
100
+ test('BUILDING AND NO MODE WEAR NO LOOK: no eyebrow; Building keeps only the Home plate, showing the city', () => {
98
101
  const none = boot();
99
102
  assert.equal(none.root.dataset.craft, undefined);
100
103
  assert.equal(none.plates().length + none.marks().length, 0, 'no mode draws nothing');
101
104
  for (const m of ['building']) {
102
105
  const p = boot({ 'hall-craft': m });
103
- assert.equal(p.plates().length + p.marks().length, 0, `${m} at boot draws nothing`);
106
+ assert.equal(p.marks().length, 0, `${m} at boot names no look`);
107
+ assert.equal(p.plates().length, 1, `${m}: the Home plate is there too (task 1005065)`);
108
+ assert.equal(p.plates()[0].getAttribute('data-plate'), m);
109
+ assert.ok(p.plates()[0].innerHTML.startsWith(p.craft.artHtml('governing')), `${m}: its plate shows the city`);
104
110
  p.craft.choose('governing');
105
111
  assert.equal(p.plates().length, 1);
112
+ assert.equal(p.plates()[0].getAttribute('data-plate'), 'governing', 'switching mode swaps the plate');
106
113
  p.craft.choose(m);
107
- assert.equal(p.plates().length + p.marks().length, 0, `switching to ${m} removes the plate and the eyebrow`);
114
+ assert.equal(p.marks().length, 0, `switching to ${m} removes the eyebrow`);
115
+ assert.equal(p.plates()[0].getAttribute('data-plate'), m, 'and swaps the plate back');
108
116
  }
109
117
  assert.deepEqual(Object.keys(none.craft.LOOKS).sort(), [...LOOKED].sort(), 'only the three crafts have a look');
110
118
  });
@@ -112,7 +120,7 @@ test('BUILDING AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to
112
120
  test('the art is drawn once: craft-mode.js has one artHtml and exports it', () => {
113
121
  assert.equal(CRAFT.match(/function artHtml\(/g).length, 1);
114
122
  assert.match(CRAFT, /artHtml: artHtml,/);
115
- assert.match(CRAFT, /plate\.innerHTML = artHtml\(mode\);/);
123
+ assert.match(CRAFT, /plate\.innerHTML = artHtml\(art\) \+ HOME_MARK;/);
116
124
  });
117
125
 
118
126
  // Every selector in style.css that names a craft mode.
@@ -163,6 +171,8 @@ test('the shared treatment, the eyebrow and the plate are written as the directi
163
171
  }
164
172
  assert.match(CSS, /@media \(max-width: 640px\) \{ \.craft-eyebrow \{ display: none; \} \}/, 'the eyebrow is hidden on a phone');
165
173
  assert.match(CSS, /\.craft-plate \{[^}]*height: 40px;/, '40px in the closed rail');
174
+ assert.match(CSS, /\.craft-plate__home \{[^}]*width: 24px;[^}]*height: 24px;/, 'the home mark clears the 24px floor');
175
+ assert.match(CSS, /\.craft-plate:focus-visible \{ outline: 2px solid var\(--focus\);/, 'the Home plate shows keyboard focus');
166
176
  assert.match(CSS, /\.app-sidebar:focus-within \.craft-plate \{ height: 64px;/, '64px when the rail opens');
167
177
  assert.match(CSS, /html\[data-theme="light"\] \.craft-plate \.craft-art__city \{ filter: invert\(1\) hue-rotate\(180deg\); \}/,
168
178
  'the light city is the dark one inverted, not a second drawing');
@@ -41,7 +41,10 @@ const stubs = (status, craft) => `
41
41
  return p.startsWith('/api/bongos/home/city') ? { ok: true, status: 200, data: window.__CITY } : { ok: false, status: 404, data: null };
42
42
  } }) };
43
43
  window.__chosen = [];
44
+ window.__hops = [];
45
+ window.OTBHomeStreetHop = (h) => window.__hops.push(h);
44
46
  window.__craft = ${JSON.stringify(craft || null)};
47
+ if (window.__craft) document.documentElement.dataset.craft = window.__craft;
45
48
  window.OTBCraftMode = { iconHtml: (k, cls) => '<svg class="' + cls + '" data-icon="' + k + '"></svg>', choose: (m) => window.__chosen.push(m), current: () => window.__craft };
46
49
  window.OTB = { contributeHallWidget(w) { window.__widget = w; } };`;
47
50
 
@@ -162,6 +165,23 @@ test('the museum and the band of stars switch the craft; the core and a building
162
165
  assert.match(r.tower, /^\/goals#\/goal\/\d+$/);
163
166
  });
164
167
 
168
+ test('the governor\'s Home is the street: Governing on Home goes there, opened or chosen; other crafts stay', { skip }, async () => {
169
+ const opened = await boot({ craft: 'governing' });
170
+ const onOpen = await opened.page.evaluate(() => ({ hops: window.__hops, asked: window.__asked, built: !!document.querySelector('.hc-svg') }));
171
+ await opened.ctx.close();
172
+ assert.deepEqual(onOpen.hops, ['/builders/street'], 'Home opened with Governing chosen goes to the street');
173
+ assert.deepEqual(onOpen.asked, [], 'and asks the server for nothing on the way');
174
+ assert.equal(onOpen.built, false, 'and builds no city it is leaving');
175
+
176
+ const { ctx, page } = await boot();
177
+ await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
178
+ const flip = (m) => page.evaluate((mode) => { document.documentElement.dataset.craft = mode; window.__craft = mode; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode } })); return window.__hops.slice(); }, m);
179
+ assert.deepEqual(await flip('artistry'), [], 'Artistry stays on Home');
180
+ assert.deepEqual(await flip('ideating'), [], 'Ideating stays on Home');
181
+ assert.deepEqual(await flip('governing'), ['/builders/street'], 'choosing Governing on Home goes to the street');
182
+ await ctx.close();
183
+ });
184
+
165
185
  test('a refused read leaves the section hidden: never an empty plate', { skip }, async () => {
166
186
  const { ctx, page } = await boot({ status: 401 });
167
187
  await page.evaluate(() => new Promise((r) => setTimeout(r, 0)));