@bongos/core 1.24.38 → 1.24.39

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.
@@ -61,7 +61,7 @@
61
61
 
62
62
  // Every limit of the line rule, in characters unless named otherwise.
63
63
  const LIMITS = Object.freeze({
64
- nameWords: 3, name: 20, fact: 26, facts: 3, factLen: 24, actions: 2,
64
+ nameWords: 14, name: 90, fact: 60, facts: 3, factLen: 48, actions: 2,
65
65
  tiles: 3, tileValue: 8, tileWords: 2, tileLabel: 14,
66
66
  status: 44, clearLine: 30, clearSub: 40, record: 4, recordLen: 30,
67
67
  chips: 5, chip: 24, todayTarget: 10, pips: 12, saveLine: 48,
@@ -20,7 +20,8 @@
20
20
  // /goals/rollups) for the districts, and the viewer's earnings for "your share
21
21
  // this month". Where the real data had to bend the design, it is said once here
22
22
  // and in the ship notes:
23
- // * Districts. One per OPEN goal, in the order the goals come, between
23
+ // * Districts (only with `districts: true`; the page draws none since the
24
+ // owner's 2026-11-04 direction, and centres the civic row). One per OPEN goal, in the order the goals come, between
24
25
  // Automation and Review. Up to four keep the prototype's 90 px plots; past
25
26
  // four each plot narrows (never under 56 px) and past that the street grows
26
27
  // longer than the 1440 px artboard, so the page scrolls it sideways rather
@@ -330,9 +331,13 @@
330
331
  // between districts, 18 otherwise), starting at 43. Past MAX_DISTRICTS open
331
332
  // goals the first seven stand as districts and the eighth plot is the rest,
332
333
  // staked out, a door to the goals page.
333
- function layout(goals) {
334
- const shown = goals.length > MAX_DISTRICTS ? goals.slice(0, MAX_DISTRICTS - 1) : goals;
335
- const more = goals.length - shown.length;
334
+ // The street is the governance square (owner, 2026-11-04): the working areas
335
+ // are the city's own buildings, so by default no district stands on the
336
+ // street and the civic row is centred on the artboard. `districts` keeps the
337
+ // prototype's row (the kit's tests hold it to the prototype).
338
+ function layout(goals, districts) {
339
+ const shown = districts ? (goals.length > MAX_DISTRICTS ? goals.slice(0, MAX_DISTRICTS - 1) : goals) : [];
340
+ const more = districts ? goals.length - shown.length : 0;
336
341
  const dw = districtWidth(shown.length + (more ? 1 : 0));
337
342
  const segs = [];
338
343
  SEGS.forEach((s) => {
@@ -346,6 +351,8 @@
346
351
  if (i > 0) x += segs[i - 1].g !== s.g ? 30 : (s.d ? 10 : 18);
347
352
  s.x = x; x += s.w;
348
353
  });
354
+ const spare = ARTBOARD_W - (x + 43);
355
+ if (!districts && spare > 0) segs.forEach((s) => { s.x += Math.floor(spare / 2); });
349
356
  return { segs: segs, width: Math.max(ARTBOARD_W, x + 43), districtW: dw };
350
357
  }
351
358
 
@@ -510,6 +517,10 @@
510
517
  // living GovLiving.livingModel() over the same feeds, or null (no Leads,
511
518
  // no agents, no construction: the deep street with real data only)
512
519
  // roster GET /builders/roster body, or null (the people reading)
520
+ // districts true: stand a district per open goal between Automation and
521
+ // Review (the prototype's row). The page leaves it off: the
522
+ // street is the governance square and the working areas are the
523
+ // city's (owner, 2026-11-04)
513
524
  function streetModel(input) {
514
525
  const opts = input || {};
515
526
  const docket = opts.docket && typeof opts.docket === 'object' && Array.isArray(opts.docket.matters) ? opts.docket : null;
@@ -529,7 +540,7 @@
529
540
  const goals = (Array.isArray(opts.goals) ? opts.goals : []).filter((g) => g && g.status === 'open' && g.id != null);
530
541
  const byGoal = {};
531
542
  (Array.isArray(opts.rollups) ? opts.rollups : []).forEach((r) => { if (r && r.goal_id != null) byGoal[String(r.goal_id)] = r; });
532
- const L = layout(goals);
543
+ const L = layout(goals, opts.districts === true);
533
544
  const segs = L.segs, W = L.width;
534
545
  const at = {};
535
546
  segs.forEach((s) => { at[s.k || ('d:' + s.d)] = s; });
@@ -725,7 +725,23 @@
725
725
  return dv;
726
726
  }
727
727
 
728
+ // The floor picker (owner, 2026-11-04: a building is a place to work). On a
729
+ // wide window the section is cropped to the building itself (PICKER_CROP: its
730
+ // crown, storeys and ground, without the 800 x 708 section's empty margins)
731
+ // and scaled to the stage height, never under half size (a floor stays a
732
+ // 24 px target) nor over 0.72 (the desk keeps the room). The CSS variables
733
+ // street.js sets on the drawing's box and its column.
734
+ const PICKER_CROP = Object.freeze({ x: 50, y: 120, w: 590, h: 580 });
735
+ function pickerFit(stageH) {
736
+ const c = PICKER_CROP;
737
+ const k = Math.max(0.5, Math.min(0.72, Math.round(((Number(stageH) || 708) - 60) / c.h * 1000) / 1000));
738
+ const px = (n) => Math.round(n * k) + 'px';
739
+ return { k: k, box: { '--inside-art-scale': String(k), '--inside-art-x': px(-c.x), '--inside-art-y': px(-c.y) },
740
+ art: { '--inside-art-w': px(c.w), '--inside-art-h': px(c.h) } };
741
+ }
742
+
728
743
  const api = {
744
+ pickerFit: pickerFit, PICKER_CROP: PICKER_CROP,
729
745
  crown: crown, workItem: workItem, floorSymbol: floorSymbol, symbolPath: symbolPath, buildingView: buildingView, districtView: districtView,
730
746
  civicInfo: civicInfo, floorFor: floorFor, holderOf: holderOf, recordOf: recordOf, buildingModel: buildingModel,
731
747
  districtInfo: districtInfo, districtModel: districtModel,
@@ -530,6 +530,37 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
530
530
  .inside-name:focus { outline: none; }
531
531
  .inside-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
532
532
 
533
+ /* THE HALL IS FOR WORK (owner, 2026-11-04; task 1005075). The street's own
534
+ header strip and the six readings along the bottom repeat what the shell's
535
+ top bar and the buildings already say, and inside a building they squeezed
536
+ the floor you came to work on into a corner. Both go. */
537
+ .street-top, .street-signs { display: none; }
538
+
539
+ /* Inside a building: the drawing is the floor picker, at half size in its own
540
+ column (a floor stays a 24 px target); the chosen floor's desk takes the rest
541
+ of the window and scrolls on its own. The building's own facts (who holds it,
542
+ how it decides, its record) follow the desk instead of standing above it. */
543
+ @media (min-width: 900px) {
544
+ .inside--work { display: block; overflow: hidden; }
545
+ .inside--work .inside-sizer { width: auto; height: 100%; margin: 0; overflow: visible; }
546
+ .inside--work .inside-board { position: static; width: auto; height: 100%; transform: none; display: grid;
547
+ grid-template-columns: var(--inside-art-w, 400px) minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr);
548
+ column-gap: 40px; padding: 0 32px 0 16px; box-sizing: border-box; animation: none; }
549
+ .inside--work .inside-head { position: static; grid-column: 1 / -1; padding: 14px 0 6px 16px; }
550
+ .inside--work .inside-art { position: relative; width: var(--inside-art-w, 400px); height: var(--inside-art-h, 354px); grid-row: 2; align-self: start; overflow: hidden; }
551
+ .inside--work .inside-art__box { width: 800px; transform: translate(var(--inside-art-x, 0), var(--inside-art-y, 0)) scale(var(--inside-art-scale, 1)); }
552
+ .inside--work .inside-panel { position: static; grid-row: 2; width: auto; max-width: 880px; height: 100%; padding: 4px 4px 32px 0; box-sizing: border-box; gap: 20px; }
553
+ .inside--work .inside-title { order: 0; flex-direction: row; align-items: baseline; gap: 14px; flex-wrap: wrap; }
554
+ .inside--work .inside-name { font-size: 28px; }
555
+ .inside--work .inside-holds { font-size: 14px; }
556
+ .inside--work .inside-open { order: 1; }
557
+ .inside--work .govx { order: 2; }
558
+ .inside--work .inside-gov { order: 3; }
559
+ .inside--work .inside-record { order: 4; }
560
+ .inside--work .inside-open__head .inside-plate { width: 44px; height: 44px; border-radius: 10px; }
561
+ .inside--work .inside-open__title { font-size: 24px; }
562
+ }
563
+
533
564
  /* narrow: the drawing in its own sideways strip, the panel below it */
534
565
  @media (max-width: 899px) {
535
566
  .inside { display: block; overflow-x: hidden; overflow-y: auto; }
@@ -543,6 +574,11 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
543
574
  .inside-dhead { order: 1; }
544
575
  .inside-legend { order: 3; flex-direction: column; gap: 8px; }
545
576
  .inside-panel { order: 3; overflow: visible; }
577
+ /* the floor's desk straight under the name, the building's facts after it */
578
+ .inside-open { order: 1; }
579
+ .govx { order: 2; }
580
+ .inside-gov { order: 3; }
581
+ .inside-record { order: 4; }
546
582
  .inside-name { font-size: 32px; }
547
583
  .inside-gov__row { flex-wrap: wrap; row-gap: 4px; }
548
584
  .desk-row__main { flex-wrap: wrap; row-gap: 8px; }
@@ -77,7 +77,7 @@
77
77
  constitution: '/api/bongos/government/constitution',
78
78
  uat: function (id) { return '/api/bongos/done-when/' + encodeURIComponent(id) + '/uat'; },
79
79
  };
80
- var NARROW = 900;
80
+ var NARROW = 900, FOCUS_W = 1120; // FOCUS_W: the civic row and its pins, which a wide window fits
81
81
  var I = window.GovInside;
82
82
  // The artboard's street band: the prototype's 900-high board less its 64 px
83
83
  // header and 128 px footer, which this page lays out as real rows, and less
@@ -159,13 +159,10 @@
159
159
  function here() { return S.parseHash(window.location && window.location.hash); }
160
160
 
161
161
  // ---------- fit the artboard's street band to the stage ----------
162
- // 1440 x 708 at scale 1 is the prototype exactly. A wider or taller stage
163
- // grows it, a smaller one shrinks it; a phone keeps it near full size and the
164
- // street scrolls sideways inside its window, so a pin stays a pin.
165
- // A stage taller than the street needs (a wide window scales the street to its
166
- // width) gives the spare height to the foot of the page, under the readings, so
167
- // the street sits on them with no empty sky above it (task 1004718). The window
168
- // gives the height back whenever a building's inside is open.
162
+ // 1440 x 708 at scale 1 is the prototype exactly. A wide window fits the civic
163
+ // row (FOCUS_W), crops the haze either side and centres the street in the stage
164
+ // (owner, 2026-11-04); a phone keeps it near full size and scrolls it sideways
165
+ // from the first building, so a pin stays a pin.
169
166
  function fit() {
170
167
  var win = $('streetWindow'), inside = $('streetInside');
171
168
  var insideOn = !!(inside && !inside.hidden);
@@ -175,23 +172,29 @@
175
172
  if (!win || !sizer || !board) return;
176
173
  var W = view.model ? view.model.width : S.ARTBOARD_W;
177
174
  var cw = win.clientWidth || window.innerWidth || 1440, ch = win.clientHeight || 708;
178
- var s = Math.min(cw / W, ch / BAND_H);
179
- if (cw / W < 0.6) s = Math.min(1, ch / BAND_H);
175
+ var wide = cw >= NARROW;
176
+ var s = wide ? Math.min(cw / FOCUS_W, ch / BAND_H) : Math.min(cw / W, ch / BAND_H);
177
+ if (cw / W < 0.6 && !wide) s = Math.min(1, ch / BAND_H);
180
178
  s = Math.max(0.4, Math.round(s * 1000) / 1000);
181
179
  view.scale = s;
182
- sizer.style.width = Math.round(W * s) + 'px';
180
+ var sizerW = wide ? Math.min(Math.round(W * s), cw) : Math.round(W * s);
181
+ sizer.style.width = sizerW + 'px';
183
182
  sizer.style.height = Math.round(BAND_H * s) + 'px';
184
- if (!insideOn && win.style && Math.round(BAND_H * s) < ch - 1) win.style.flex = '0 0 ' + Math.round(BAND_H * s) + 'px';
183
+ board.style.left = wide ? Math.round((sizerW - W * s) / 2) + 'px' : '';
184
+ if (win.style) win.style.overflowX = wide ? 'hidden' : '';
185
185
  board.style.width = W + 'px';
186
186
  board.style.setProperty('--street-scale', String(s));
187
187
  var svg = $('streetSvg');
188
188
  if (svg) { svg.setAttribute('width', String(W)); svg.setAttribute('viewBox', '0 0 ' + W + ' 900'); }
189
+ var doors = $('streetDoors'), first = doors && (doors.firstElementChild || (doors.children && doors.children[0]));
190
+ if (!wide && !insideOn && !view.scrolled && first) { win.scrollLeft = Math.max(0, Math.round((parseFloat(first.style.left) || 0) * s) - 16); view.scrolled = true; }
189
191
  placeCard();
190
192
  }
191
193
 
192
- // The interior's artboard: 1440 x 708 scaled to the window like the street;
193
- // under NARROW px the board is a column and only the drawing scales, in its
194
- // own sideways strip, never under 0.6 so a floor stays a 24 px target.
194
+ // The interior: a building on a wide window is laid out, its drawing the floor
195
+ // picker (GovInside.pickerFit) beside the desk; a district's 1440 x 708 board
196
+ // scales to the window like the street; under NARROW px the board is a column
197
+ // and only the drawing scales, in its own sideways strip, never under 0.6.
195
198
  function fitInside() {
196
199
  var box = $('streetInside'), sizer = $('insideSizer'), board = $('insideBoard'), art = $('insideArt'), artBox = $('insideArtBox'), span = $('insideArtSpan');
197
200
  if (!box || !sizer || !board || !art || !artBox || box.hidden) return;
@@ -207,6 +210,14 @@
207
210
  if (span) { span.style.width = Math.round(artW * a) + 'px'; span.style.height = Math.round(708 * a) + 'px'; }
208
211
  return;
209
212
  }
213
+ var work = !(view.inside && view.inside.kind === 'district');
214
+ if (box.classList) box.classList.toggle('inside--work', work);
215
+ if (work) {
216
+ var f = I.pickerFit(ch);
217
+ sizer.style.width = ''; sizer.style.height = ''; board.style.setProperty('--inside-scale', '1');
218
+ [[artBox, f.box], [art, f.art]].forEach(function (e) { Object.keys(e[1]).forEach(function (k) { e[0].style.setProperty(k, e[1][k]); }); });
219
+ if (span) { span.style.width = '0px'; span.style.height = '0px'; } return;
220
+ }
210
221
  var s = Math.max(0.4, Math.round(Math.min(cw / 1440, ch / 708) * 1000) / 1000);
211
222
  sizer.style.width = Math.round(1440 * s) + 'px';
212
223
  sizer.style.height = Math.round(708 * s) + 'px';
@@ -292,7 +303,9 @@
292
303
  // GovStreetDepth.page's (street-depth.js); this file hands it the street's
293
304
  // data, its card opener and a redraw, and keeps the street's own wiring.
294
305
  var X = window.GovStreetDepth.page;
295
- function drawPin(p) { return X.pin(p, { open: view.open, openCard: openCard }); }
306
+ // a pin goes into its building, the matter a desk row there (owner, 2026-11-04: no pop-ups)
307
+ function goPin(key, el, keys) { var b = el && el.getAttribute && el.getAttribute('data-building'); if (b) window.location.hash = 'building=' + b; else openCard(key, el, keys); }
308
+ function drawPin(p) { return X.pin(p, { open: view.open, openCard: goPin }); }
296
309
  function drawSign(sg, tile) { return X.sign(sg, tile); }
297
310
  function drawDepth(dm, W) { return X.depth(dm, W); }
298
311
  // the living model arrives after the street's own reads: the street and the deck draw it
@@ -106,3 +106,15 @@ Owner decision 2026-10-04: the street should read at a glance without words, so
106
106
  - **The evening view is unchanged** ("nothing waits on you." and its line still show when nothing needs you).
107
107
 
108
108
  Tests: `tests/hall_street.mjs` asserts the model has no hint field, the module exports no `HINT`, and none of the street's four files carries the line; putting the markup or the constant back reds it.
109
+
110
+ ## The hall is for work: the street as a square, the building as a place to work (owner, 2026-11-04)
111
+
112
+ The owner's direction: the builders' hall is where you go to do things, not to look at them. No menu on menu, no pop-ups (go to the place), no page header or footer that a web app does not need. On the street that meant:
113
+
114
+ - **The street is the governance square.** The working areas are the city's own buildings, so no district stands on the street: `streetModel()` draws one only with `districts: true` (the kit's tests still hold that row to the prototype), and without it the civic row is centred on the artboard. A wide window scales the street to the row (`FOCUS_W`, 1120), not the whole 1440 artboard, so the buildings are as large as the height allows and the haze either side is cropped; a phone still scrolls it sideways and opens at the first building.
115
+ - **The header strip and the six readings are gone** (`.street-top`, `.street-signs` are not displayed; the markup stays, so the counts, the deck link and the readings still compute). The shell's top bar names the page and the buildings carry their own state lines.
116
+ - **A pin takes you into its building** (`#building=<key>`), where each matter is a desk row you act on. It no longer opens the short card; the card is reached only by a pin with no building, and retiring it is a follow-up.
117
+ - **Inside a building the drawing is the floor picker.** On a wide window (`.inside--work`) the interior is laid out, not scaled as one 1440 x 708 picture: the section is cropped to the building (`CROP` in `fitInside`, 590 x 580 of the 800 x 708 section), at 0.5 to 0.72 so a floor stays a 24 px target, in its own column; the chosen floor's desk takes the rest of the window and scrolls on its own. The building's name sits on one line, then the desk, then the government layer, who holds it and how it decides, then the record. A phone keeps its column with the same order. A district keeps its scaled board.
118
+ - **A desk row is one full line.** The board's 20 / 26 / 24 caps were sized for a desk in a corner of the interior; `LIMITS` is now name 90 (14 words), fact 60, facts 48.
119
+
120
+ Tests: `tests/hall_street.mjs` (nine doors and no district on the page, a pin goes inside and fills no card, the row scaled and centred, a phone opening at the first building), `tests/hall_street_inside.mjs` (the cropped picker, never under half size, the board not scaled, a district untouched), `tests/hall_street_depth.mjs` (with no plot to fly from every agent rides the road), `tests/hall_street_desk.mjs` (the new limits).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.38",
3
+ "version": "1.24.39",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.38",
9
+ "version": "1.24.39",
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.38",
3
+ "version": "1.24.39",
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",
@@ -11191,5 +11191,11 @@
11191
11191
  "id": "1005071",
11192
11192
  "text": "The hall sidebar is now seven labelled sections (Work, Make, Community, You, Govern, Run the project, Help), and every craft can reach Help."
11193
11193
  }
11194
+ ],
11195
+ "1.24.39": [
11196
+ {
11197
+ "id": "1005077",
11198
+ "text": "The governor street is now a work-first square: the working areas left the street, the extra header and footer are gone, a pin takes you straight into its building, and inside a building the drawing is a compact floor picker s"
11199
+ }
11194
11200
  ]
11195
11201
  }
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.38'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.39'; // 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');
@@ -290,9 +290,16 @@ const BASE = (d) => ({
290
290
  '/api/bongos/government/constitution': { board: { membership: 'rank:metic+', pass_rule: 'unanimous', window_minutes: 1440, close_early_on_full_turnout: true } },
291
291
  });
292
292
 
293
- test('a pin opens the full card: the ballot is read, then the seats, counsel and acts are drawn as text', async () => {
293
+ // A pin goes into its building now (owner, 2026-11-04: no pop-ups); the card
294
+ // still opens for a pin with no building, so these tests open it directly.
295
+ const openCard = (p) => {
296
+ const pin = p.byId.get('streetPins').children[0];
297
+ p.window.GovStreetPage.openCard(pin.attrs['data-matter'], pin, pin.attrs['data-matters'].split(' '));
298
+ };
299
+
300
+ test('the full card: the ballot is read, then the seats, counsel and acts are drawn as text', async () => {
294
301
  const p = await bootPage({ feeds: BASE(docket([matter({ title: '<b>x</b>' })])) });
295
- p.byId.get('streetPins').children[0].fire('click');
302
+ openCard(p);
296
303
  await settle(); await settle();
297
304
  assert.ok(p.calls.some((c) => c.url === '/api/bongos/government/board/items'), 'the room is read when a sitting opens');
298
305
  const body = p.byId.get('streetCardBody');
@@ -308,7 +315,7 @@ test('vote for: one POST to the vote route, a toast, the docket re-read, and the
308
315
  const feeds = BASE((() => { const d0 = docket([matter()]); return () => (reads++ === 0 ? d0 : after); })());
309
316
  feeds['/api/bongos/government/docket'] = (() => { const d0 = docket([matter()]); return () => (reads++ === 0 ? d0 : after); })();
310
317
  const p = await bootPage({ feeds, posts: { 'POST /api/bongos/government/board/items/41/votes': { ok: true, vote: {}, already_voted: false, karma_delta: 1 } } });
311
- p.byId.get('streetPins').children[0].fire('click');
318
+ openCard(p);
312
319
  await settle(); await settle();
313
320
  byAct(p.byId.get('streetCardBody'), 'for').fire('click');
314
321
  await settle(); await settle(); await settle();
@@ -324,7 +331,7 @@ test('vote for: one POST to the vote route, a toast, the docket re-read, and the
324
331
 
325
332
  test('object: "no" opens the desk row\'s one-tap reasons, and a tap puts the reason and the part on the record', async () => {
326
333
  const p = await bootPage({ feeds: BASE(docket([matter()])), posts: { 'POST /api/bongos/government/board/items/41/votes': { ok: true, vote: {}, item_closed: true, outcome: 'returned' } } });
327
- p.byId.get('streetPins').children[0].fire('click');
334
+ openCard(p);
328
335
  await settle(); await settle();
329
336
  const body = () => p.byId.get('streetCardBody');
330
337
  assert.equal(byAct(body(), 'object').textContent, D.ACT_WORDS.object, 'the desk row\'s word');
@@ -341,7 +348,7 @@ test('object: "no" opens the desk row\'s one-tap reasons, and a tap puts the rea
341
348
 
342
349
  test('a refused act says why and records nothing; abstain sends nothing at all', async () => {
343
350
  const p = await bootPage({ feeds: BASE(docket([matter()])), posts: { 'POST /api/bongos/government/board/items/41/votes': { __status: 403, error: { code: 'not_board_member' } } } });
344
- p.byId.get('streetPins').children[0].fire('click');
351
+ openCard(p);
345
352
  await settle(); await settle();
346
353
  byAct(p.byId.get('streetCardBody'), 'for').fire('click');
347
354
  await settle(); await settle();
@@ -361,7 +368,7 @@ test('release: the override is approved, the card closes, Release rises, and wit
361
368
  feeds['/api/bongos/government/docket'] = () => (reads++ === 0 ? docket([ov]) : docket([]));
362
369
  const p = await bootPage({ feeds, posts: { 'POST /api/bongos/override-requests/9/decide': { ok: true } } });
363
370
  assert.equal(p.byId.get('streetEvening').hidden, true);
364
- p.byId.get('streetPins').children[0].fire('click');
371
+ openCard(p);
365
372
  byAct(p.byId.get('streetCardBody'), 'release').fire('click');
366
373
  await settle(); await settle(); await settle();
367
374
  assert.deepEqual(p.calls.filter((c) => c.method !== 'GET').map((c) => [c.method, c.url, c.body]), [['POST', '/api/bongos/override-requests/9/decide', { decision: 'approve' }]]);
@@ -476,7 +483,7 @@ test('more expands the same card in place: the full text, who and when, and the
476
483
  const { d } = SOURCE_FIXTURES();
477
484
  d.matters = d.matters.filter((m) => m.source === 'blockers');
478
485
  const p = await bootPage({ feeds: { ...BASE(d) } });
479
- p.byId.get('streetPins').children[0].fire('click');
486
+ openCard(p);
480
487
  await settle(); await settle();
481
488
  const body = () => p.byId.get('streetCardBody');
482
489
  const toggle = () => byAct(body(), 'more');
@@ -72,7 +72,9 @@ function docket(matters = [], over = {}) {
72
72
  }
73
73
  const goal = (id, status = 'open', title = `Goal ${id}`) => ({ id: String(id), title, status });
74
74
  const rollup = (id, c) => ({ goal_id: String(id), done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, ...c });
75
- const model = (over = {}) => S.streetModel({ docket: docket(), goals: [], rollups: [], mode: 'dark', now: NOW, ...over });
75
+ // The kit is held to the prototype's row, districts and all (`districts: true`); the
76
+ // page itself draws the civic row alone (owner, 2026-11-04), pinned further down.
77
+ const model = (over = {}) => S.streetModel({ docket: docket(), goals: [], rollups: [], mode: 'dark', now: NOW, districts: true, ...over });
76
78
 
77
79
  // ── the drawing is the prototype's ───────────────────────────────────────────
78
80
 
@@ -299,7 +301,7 @@ test('the collision pass places in tier order: when only one of two can show, th
299
301
  });
300
302
 
301
303
  test('no docket: no pins, the readings say so, and nothing is invented', () => {
302
- const m = S.streetModel({ docket: null, goals: [goal(1)], rollups: [], mode: 'dark', now: NOW });
304
+ const m = S.streetModel({ docket: null, goals: [goal(1)], rollups: [], mode: 'dark', now: NOW, districts: true });
303
305
  assert.equal(m.pins.length, 0);
304
306
  assert.equal(m.crates.length, 0);
305
307
  assert.ok(m.signs.every((s) => s.figure === '—' && !s.hot && s.line === S.FALLBACK_LINE));
@@ -617,32 +619,30 @@ test('the page reads the four feeds and draws the docket\'s pins, the readings a
617
619
  assert.equal(p.byId.get('hudMoving').textContent, '2');
618
620
  assert.equal(p.byId.get('hudShare').textContent, '$24');
619
621
  assert.equal(p.byId.get('hudDot').className, 'street-hud__dot street-hud__dot--hot');
620
- assert.equal(p.byId.get('streetDoors').children.length, 10, 'nine buildings and one district, each a door');
622
+ assert.equal(p.byId.get('streetDoors').children.length, 9, 'the nine civic buildings, each a door; no district stands on the street (owner, 2026-11-04)');
623
+ assert.ok(p.byId.get('streetDoors').children.every((a) => !/district=/.test(a.attrs.href)), 'no door to a working area');
621
624
  assert.equal(p.byId.get('streetHome').attrs.href, '/builders/city');
622
625
  });
623
626
 
624
- test('a pin opens its short card, Escape closes it and focus goes back to the pin', async () => {
625
- const p = await bootPage({ feeds: FEEDS([matter({ id: 'board:1', building: 'charter', title: '<img src=x onerror=alert(1)>', href: '/builders/board-room' })]) });
627
+ // Owner, 2026-11-04: no pop-ups, go to the place. A pin takes you into its
628
+ // building, where each matter is a desk row you act on; it opens no card.
629
+ test('a pin takes you into its building and opens no card', async () => {
630
+ const p = await bootPage({ feeds: FEEDS([matter({ id: 'board:1', building: 'charter', title: 'A vote', href: '/builders/board-room' })]) });
626
631
  const pin = p.byId.get('streetPins').children[0];
632
+ assert.equal(pin.attrs['data-building'], 'charter');
627
633
  pin.fire('click');
628
- const card = p.byId.get('streetCard');
629
- assert.equal(card.hidden, false);
630
- assert.equal(p.byId.get('streetCardTitle').textContent, '<img src=x onerror=alert(1)>', 'docket text lands as text');
631
- assert.equal(p.byId.get('streetCardOpen').attrs.href, undefined, 'the card links nowhere: it acts in place (task 1004566)');
632
- assert.equal(p.focused.el, p.byId.get('streetCardClose'));
633
- assert.equal(p.byId.get('streetPins').children[0].attrs['aria-expanded'], 'true');
634
- const e = { key: 'Escape', prevented: false, preventDefault() { this.prevented = true; } };
635
- p.key('keydown', e);
636
- assert.equal(card.hidden, true);
637
- assert.equal(e.prevented, true, 'the press is used, so nothing else answers it');
638
- assert.equal(p.focused.el.attrs['data-matter'], 'board:1');
639
- });
640
-
641
- test('the card pager: a pin with several matters opens the most important with "1 of N", and previous/next reach every matter there', async () => {
634
+ assert.equal(p.window.location.hash, 'building=charter', 'the address names the building, so its inside opens');
635
+ assert.equal(p.byId.get('streetCardTitle').textContent, '', 'no card is filled');
636
+ assert.notEqual(p.byId.get('streetPins').children[0].attrs['aria-expanded'], 'true', 'and the pin does not say one is open');
637
+ });
638
+
639
+ // The card itself still works for the code paths that open it (a pin with no
640
+ // building); its pager is held here by opening it directly.
641
+ test('the card pager: a card over several matters opens the most important with "1 of N", and previous/next reach every matter there', async () => {
642
642
  const ms = [matter({ id: 'blockers:1', state: 'moving', title: 'Third' }), matter({ id: 'blockers:2', state: 'needs', title: 'First' }), matter({ id: 'blockers:3', state: 'needs', title: 'Second' }), matter({ id: 'blockers:4', state: 'settled', title: 'Fourth' })];
643
643
  const p = await bootPage({ feeds: FEEDS(ms) });
644
644
  const pin = p.byId.get('streetPins').children.find((s) => s.attrs['data-building'] === 'planning');
645
- pin.fire('click');
645
+ p.window.GovStreetPage.openCard(pin.attrs['data-matter'], pin, pin.attrs['data-matters'].split(' '));
646
646
  const body = p.byId.get('streetCardBody');
647
647
  const pager = () => body.children[0];
648
648
  const btn = (dir) => pager().children.find((c) => c.attrs['data-act'] === 'page-' + dir);
@@ -664,7 +664,8 @@ test('the card pager: a pin with several matters opens the most important with "
664
664
  assert.equal(p.focused.el.attrs['data-building'], 'planning', 'Escape returns focus to the pin');
665
665
  // one matter: no pager
666
666
  const q = await bootPage({ feeds: FEEDS([matter({ id: 'board:1', building: 'charter' })]) });
667
- q.byId.get('streetPins').children[0].fire('click');
667
+ const one = q.byId.get('streetPins').children[0];
668
+ q.window.GovStreetPage.openCard(one.attrs['data-matter'], one, one.attrs['data-matters'].split(' '));
668
669
  assert.ok(!q.byId.get('streetCardBody').children.some((c) => c.className === 'mcard-pager'), 'one matter, no pager');
669
670
  });
670
671
 
@@ -737,17 +738,22 @@ test('arriving through a door marks that building, and a failed docket says so',
737
738
  assert.equal(q.byId.get('hudShare').textContent, '—');
738
739
  });
739
740
 
740
- // task 1004718: a wide window scales the street to its width, and the height
741
- // it does not need goes under the readings, never as empty sky over the street.
742
- test('a stage taller than the street gives its spare height to the foot, not the sky', async () => {
741
+ // Owner, 2026-11-04 (supersedes task 1004718's foot): the readings left the
742
+ // page, so a wide window scales the street to its civic row, not the whole
743
+ // artboard, crops the haze either side, and centres the street in its stage.
744
+ test('a wide window scales the street to its civic row and centres it; a phone scrolls it from the first building', async () => {
743
745
  const p = await bootPage({ feeds: FEEDS([]), width: 1440 });
744
- const win = p.byId.get('streetWindow'), sizer = p.byId.get('streetSizer');
745
- win.clientHeight = 900;
746
- p.key('resize', {});
747
- const h = parseFloat(sizer.style.height);
748
- assert.ok(h < 900, 'the street is width-bound here');
749
- assert.equal(win.style.flex, '0 0 ' + h + 'px', 'the window shrinks to the street, so the readings sit right under it');
750
- win.clientHeight = 300;
746
+ const win = p.byId.get('streetWindow'), sizer = p.byId.get('streetSizer'), board = p.byId.get('streetBoard');
747
+ win.clientWidth = 1376; win.clientHeight = 844;
751
748
  p.key('resize', {});
752
- assert.equal(win.style.flex, '', 'a height-bound street fills its window, nothing to give back');
749
+ const s = Number(board.style['--street-scale']);
750
+ assert.ok(s > 1376 / 1440, 'larger than the whole artboard would allow');
751
+ assert.equal(win.style.flex, '', 'the window keeps the stage, so the street sits centred in it');
752
+ assert.equal(win.style.overflowX, 'hidden', 'no sideways scroll on a wide window');
753
+ assert.equal(parseFloat(sizer.style.width), 1376, 'the sizer is the window');
754
+ assert.ok(parseFloat(board.style.left) < 0, 'the artboard is centred, its haze cropped either side');
755
+ const q = await bootPage({ feeds: FEEDS([]), width: 390 });
756
+ const qwin = q.byId.get('streetWindow');
757
+ assert.equal(qwin.style.overflowX, '', 'a phone scrolls the street sideways');
758
+ assert.ok(qwin.scrollLeft > 0, 'starting at the first building, not the empty haze before it');
753
759
  });
@@ -70,7 +70,7 @@ function feeds(over = {}) {
70
70
  function living(f) { return L.livingModel({ ...f, now: NOW }); }
71
71
  function street(over = {}, mode = 'dark') {
72
72
  const f = feeds(over);
73
- return S.streetModel({ docket: f.docket, goals: f.goals.goals, rollups: f.rollups.rollups, mode, now: NOW, depth: true, living: f.living === null ? null : living(f), roster: f.roster });
73
+ return S.streetModel({ docket: f.docket, goals: f.goals.goals, rollups: f.rollups.rollups, mode, now: NOW, depth: true, living: f.living === null ? null : living(f), roster: f.roster, districts: true });
74
74
  }
75
75
  const openGoals = () => fx('goals').goals.filter((g) => g.status === 'open');
76
76
 
@@ -338,7 +338,7 @@ test('an overlay handed in changes nothing; the Leads not read, no Lead, capsule
338
338
  const c = street({ leads: null }).depth;
339
339
  assert.deepEqual([c.plates.length, c.capsules.length, c.sites.length], [0, 0, 0], 'no Leads read: no plate, no capsule, no build-out going up');
340
340
  // without the living model at all the deep street still draws its real skyline and windows
341
- const none = S.streetModel({ docket: fx('government__docket'), goals: fx('goals').goals, rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, living: null, roster: null }).depth;
341
+ const none = S.streetModel({ docket: fx('government__docket'), goals: fx('goals').goals, rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, living: null, roster: null, districts: true }).depth;
342
342
  assert.deepEqual(none.clusters, a.clusters);
343
343
  assert.deepEqual(none.windows, street({ leads: null, docket: fx('government__docket') }).depth.windows, 'no living model and the hub as it is light the same, the docket\'s own');
344
344
  assert.equal(none.agents, 0);
@@ -361,7 +361,7 @@ test('street-depth.js: its model half is pure, and the two drawing files require
361
361
  const before = globalThis.GovStreetDepth;
362
362
  delete globalThis.GovStreetDepth;
363
363
  try {
364
- const flat = S.streetModel({ docket: fx('government__docket'), goals: openGoals(), rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true });
364
+ const flat = S.streetModel({ docket: fx('government__docket'), goals: openGoals(), rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, districts: true });
365
365
  assert.equal(flat.depth, undefined);
366
366
  } finally { globalThis.GovStreetDepth = before; }
367
367
  });
@@ -500,9 +500,11 @@ test('the page draws the living street: the skyline, a pod per claim, the plates
500
500
  assert.equal(cls('streetDepthFar', 'street-depth-sil__face').length, 1);
501
501
  const claims = fx('tasks__claimable').active_claims.length;
502
502
  assert.equal(cls('streetDepthSky', 'street-depth-pod').length + cls('streetDepthFront', 'street-depth-pod').length, claims, 'one pod per agent');
503
- const pod = cls('streetDepthSky', 'street-depth-pod')[0];
504
- assert.match(pod.style.offsetPath, /^path\('M/);
505
- assert.match(pod.style.animationDelay, /^-[\d.]+s$/);
503
+ // The page stands no district (owner, 2026-11-04), so no claim flies a skylane
504
+ // from its goal's plot: every agent rides the road (the kit's skylanes are held
505
+ // with districts on, above).
506
+ assert.equal(cls('streetDepthSky', 'street-depth-pod').length, 0, 'no plot to fly from');
507
+ assert.equal(cls('streetDepthFront', 'street-depth-pod--road').length, claims, 'every agent on the road');
506
508
  assert.equal(cls('streetDepthFront', 'street-depth-plate').length, 9);
507
509
  assert.equal(all('streetDepthFront').concat(all('streetDepthSky')).filter((e) => 'data-proto' in e.attrs).length, 0, 'nothing is marked prototype');
508
510
  assert.equal(cls('streetDepthFront', 'street-depth-jib').length, 1, 'the live site\'s crane');
@@ -105,11 +105,14 @@ test('fit() is the board\'s fit(), with a word cap on top', () => {
105
105
  for (const n of [5, 12, 20]) for (const w of [1, 2, 3]) assert.ok(D.fit(LONG, n, w).length <= n, 'never longer than its cap');
106
106
  });
107
107
 
108
- test('the limits are the board\'s numbers: name 20, fact 26, facts 24 (three), status 44, record 30 (four), three tiles, ten pips', () => {
108
+ // A row's name and fact were the board's 20 / 26 / 24 while the desk was a
109
+ // corner of the interior; the desk takes the window's width now (owner,
110
+ // 2026-11-04), so a row is one full line: name 90 (14 words), fact 60, facts 48.
111
+ test('the limits: a row is one full line (name 90, fact 60, facts 48, three), status 44, record 30 (four), three tiles, ten pips', () => {
109
112
  const L = D.LIMITS;
110
- assert.deepEqual([L.name, L.nameWords, L.fact, L.factLen, L.facts, L.actions, L.status, L.recordLen, L.record, L.tiles, L.tileWords, L.todayTarget], [20, 3, 26, 24, 3, 2, 44, 30, 4, 3, 2, 10]);
113
+ assert.deepEqual([L.name, L.nameWords, L.fact, L.factLen, L.facts, L.actions, L.status, L.recordLen, L.record, L.tiles, L.tileWords, L.todayTarget], [90, 14, 60, 48, 3, 2, 44, 30, 4, 3, 2, 10]);
111
114
  const src = fs.readFileSync(path.join(ROOT, 'docs/design/mocks/governor-city/Desks.dc.html'), 'utf8');
112
- for (const pin of ['this.fit(r.t, 20)', 'this.fit(r.f, 26)', 'this.fit(t, 24)', '.slice(0, 3)', "this.fit('nothing needs you · ' + F.quiet, 44)", "this.fit(t + ' · now', 30)", '.slice(0, 4)', 'const target = 10']) assert.ok(src.includes(pin), `the board says ${pin}`);
115
+ for (const pin of ['.slice(0, 3)', "this.fit('nothing needs you · ' + F.quiet, 44)", "this.fit(t + ' · now', 30)", '.slice(0, 4)', 'const target = 10']) assert.ok(src.includes(pin), `the board says ${pin}`);
113
116
  });
114
117
 
115
118
  test('THE LINE RULE: no row, tile or status any docket produces breaks it, on every floor of every building', () => {
@@ -140,8 +143,8 @@ test('breaks() names every way a desk can break the rule (so the rule test can f
140
143
  const good = D.deskModel({ key: 'people', floor: 4, docket: dk, now: NOW });
141
144
  assert.deepEqual(D.breaks(good), []);
142
145
  const bad = j(good);
143
- bad.rows[0].name = 'Four words in here';
144
- bad.rows[0].fact = 'x'.repeat(27);
146
+ bad.rows[0].name = Array(16).fill('word').join(' ');
147
+ bad.rows[0].fact = 'x'.repeat(61);
145
148
  bad.rows[0].actions = [{ label: 'a' }, { label: 'b' }, { label: 'c' }];
146
149
  bad.rows[0].facts = ['a', 'b', 'c', 'd'];
147
150
  bad.tiles = [{ v: '0', l: 'need you' }, { v: '3', l: 'three whole words' }, { v: '4', l: 'ok' }, { v: '5', l: 'ok' }];
@@ -555,6 +555,24 @@ test('the markup carries the interior: one persistent lift, two flip blocks, the
555
555
  assert.match(html, /<button type="button" class="inside-textbtn" id="insideAmend"/);
556
556
  });
557
557
 
558
+ // Owner, 2026-11-04: a building is a place to work. On a wide window the
559
+ // drawing is a floor picker cropped to the building, at half size or more (a
560
+ // floor stays a 24 px target), and the desk has the rest of the window; the
561
+ // whole interior is no longer one scaled picture. A district keeps its board.
562
+ test('inside a building the drawing is a cropped floor picker, never under half size, and the board is not scaled', async () => {
563
+ const p = await bootPage({ hash: '#building=planning', feeds: FEEDS([]) });
564
+ const box = p.byId.get('insideArtBox'), art = p.byId.get('insideArt'), board = p.byId.get('insideBoard');
565
+ const k = Number(box.style['--inside-art-scale']);
566
+ assert.ok(k >= 0.5 && k <= 0.72, `picker scale ${k}`);
567
+ assert.equal(board.style['--inside-scale'], '1', 'the interior is laid out, not shrunk as one picture');
568
+ assert.equal(art.style['--inside-art-w'], Math.round(590 * k) + 'px', 'cropped to the building, not the 800 px section');
569
+ assert.equal(box.style['--inside-art-x'], Math.round(-50 * k) + 'px');
570
+ assert.ok(47 * k >= 24, 'a floor stays a 24 px target');
571
+ const d = await bootPage({ hash: '#district=1', feeds: FEEDS([]) });
572
+ assert.equal(d.byId.get('insideArtBox').style['--inside-art-scale'], '1', 'a district keeps its whole board');
573
+ assert.equal(d.byId.get('insideArtBox').style['--inside-art-x'], undefined, 'and is not cropped');
574
+ });
575
+
558
576
  test('a building address draws the interior in place of the street, from the docket', async () => {
559
577
  const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people', state: 'needs', title: 'Admit Kai' })];
560
578
  const p = await bootPage({ hash: '#building=people', feeds: FEEDS(ms) });