@bongos/core 1.21.43 → 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.
- package/.bongos-core.json +31 -31
- package/docs/copy-inventory.md +11 -11
- package/docs/copy-registry.json +13 -13
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -2
- package/docs/page-readings.json +391 -401
- package/modules/hall-ui/public/city-draw.js +51 -13
- package/modules/hall-ui/public/city.css +9 -2
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/city.js +23 -12
- package/modules/hall-ui/public/city.states.json +146 -12
- package/modules/hall-ui/public/street-card.css +6 -0
- package/modules/hall-ui/public/street-card.js +31 -3
- package/modules/hall-ui/public/street-draw.js +96 -19
- package/modules/hall-ui/public/street.css +23 -7
- package/modules/hall-ui/public/street.html +8 -7
- package/modules/hall-ui/public/street.js +45 -13
- package/modules/hall-ui/public/street.states.json +68 -0
- package/modules/hall-ui/records/governor-street.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/hall-preview/server.js +5 -2
- package/src/module-api.js +1 -1
- package/tests/hall_city.mjs +42 -11
- package/tests/hall_matter_card.mjs +17 -0
- package/tests/hall_street.mjs +185 -12
|
@@ -76,8 +76,11 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
76
76
|
.street-kicker { margin: 0; font-size: 11.5px; font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); white-space: nowrap; }
|
|
77
77
|
.street-hud { display: flex; align-items: center; gap: 36px; margin-left: auto; }
|
|
78
78
|
.street-hud__item { display: flex; align-items: center; gap: 12px; }
|
|
79
|
-
|
|
79
|
+
/* the header's dots are the pins' colours, so the counts double as the legend
|
|
80
|
+
(task 1004569): orange for needs you (a ring while nothing does), ink for moving */
|
|
81
|
+
.street-hud__dot { width: 8px; height: 8px; border-radius: 999px; box-sizing: border-box; background: transparent; border: 1.5px solid var(--accent); flex: none; }
|
|
80
82
|
.street-hud__dot--hot { background: var(--accent); }
|
|
83
|
+
.street-hud__dot--moving { width: 6px; height: 6px; border: 0; background: var(--ink); }
|
|
81
84
|
.street-hud__n { font-size: 24px; font-weight: 300; font-variant-numeric: tabular-nums; }
|
|
82
85
|
.street-mode { margin-left: 36px; flex: none; background: none; border: 1px solid var(--street-rule-strong); border-radius: 999px; height: 36px; padding: 0 14px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--ink); font-family: var(--font-body); font-size: 13px; text-transform: lowercase; }
|
|
83
86
|
.street-mode svg { display: block; stroke-width: 1.4; }
|
|
@@ -116,7 +119,7 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
116
119
|
|
|
117
120
|
/* pins: orange and pulsing waits on you; ink is moving; grey is settled; dashed is the queue */
|
|
118
121
|
.street-pin { position: absolute; z-index: 3; display: flex; flex-direction: column; align-items: center; gap: 6px; background: transparent; border: 0; padding: 0; cursor: pointer; font-family: var(--font-body); color: var(--ink); }
|
|
119
|
-
.street-pin__label { max-width:
|
|
122
|
+
.street-pin__label { max-width: 150px; flex-shrink: 0; min-height: 15px; overflow: hidden; text-overflow: ellipsis; font-size: 12px; line-height: 1.2; white-space: nowrap; visibility: hidden; }
|
|
120
123
|
.street-pin__head { position: relative; width: 28px; height: 28px; flex-shrink: 0; border-radius: 999px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; transition: transform 200ms cubic-bezier(.2,.7,.3,1); }
|
|
121
124
|
.street-pin:hover .street-pin__head { transform: scale(1.12); }
|
|
122
125
|
.street-pin__head svg { display: block; }
|
|
@@ -125,14 +128,27 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
125
128
|
.street-pin__stem { width: 0; flex-grow: 1; border-left: 1px solid transparent; }
|
|
126
129
|
.street-pin__ring { position: absolute; inset: -1px; border-radius: 999px; border: 1px solid var(--accent); }
|
|
127
130
|
.street-pin__count { font-size: 12.5px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
131
|
+
/* importance tiers (task 1004569): one pin per building, colour AND size, so a
|
|
132
|
+
tier reads without colour. urgent 34 > needs 28 (the prototype's) > moving 20
|
|
133
|
+
> settled 10, a plain grey dot. A label shows only where the collision pass
|
|
134
|
+
placed it (--shown); any other shows on hover or focus, on a card so it reads
|
|
135
|
+
over its neighbours. */
|
|
136
|
+
.street-pin--shown .street-pin__label, .street-pin:hover .street-pin__label, .street-pin:focus-visible .street-pin__label { visibility: visible; }
|
|
137
|
+
.street-pin:hover, .street-pin:focus-visible { z-index: 4; }
|
|
138
|
+
.street-pin:not(.street-pin--shown):hover .street-pin__label, .street-pin:not(.street-pin--shown):focus-visible .street-pin__label { background: var(--bg-card); border-radius: 4px; padding: 0 4px; }
|
|
139
|
+
.street-pin--urgent .street-pin__label { color: var(--street-accent-text); font-weight: 500; }
|
|
128
140
|
.street-pin--needs .street-pin__label { color: var(--street-accent-text); }
|
|
129
|
-
.street-pin--needs .street-pin__head { background: var(--accent); color: var(--accent-on); box-shadow: var(--street-glow); }
|
|
130
|
-
.street-pin--
|
|
131
|
-
.street-pin--
|
|
141
|
+
.street-pin--urgent .street-pin__head, .street-pin--needs .street-pin__head { background: var(--accent); color: var(--accent-on); box-shadow: var(--street-glow); }
|
|
142
|
+
.street-pin--urgent .street-pin__head { width: 34px; height: 34px; }
|
|
143
|
+
.street-pin--urgent .street-pin__head svg { width: 21px; height: 21px; }
|
|
144
|
+
.street-pin--urgent .street-pin__stem, .street-pin--needs .street-pin__stem { border-left-color: color-mix(in srgb, var(--accent) 65%, transparent); }
|
|
145
|
+
.street-pin--moving .street-pin__head { width: 20px; height: 20px; background: var(--ink); color: var(--bg-card); }
|
|
146
|
+
.street-pin--moving .street-pin__head svg { width: 13px; height: 13px; }
|
|
132
147
|
.street-pin--moving .street-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 45%, transparent); }
|
|
133
|
-
.street-pin--settled .street-
|
|
134
|
-
.street-pin--settled .street-pin__head { background: color-mix(in srgb, var(--ink) 10%, var(--bg)); border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent); color: var(--ink-faint); }
|
|
148
|
+
.street-pin--settled .street-pin__head { width: 10px; height: 10px; margin-top: 9px; background: color-mix(in srgb, var(--ink) 38%, var(--bg)); }
|
|
135
149
|
.street-pin--settled .street-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 22%, transparent); }
|
|
150
|
+
.street-pin__badge { position: absolute; top: -7px; right: -12px; min-width: 17px; height: 17px; padding: 0 4px; box-sizing: border-box; border-radius: 999px; background: var(--bg-card); border: 1px solid currentColor; color: var(--ink); font-size: 10.5px; font-weight: 600; line-height: 15px; text-align: center; font-variant-numeric: tabular-nums; }
|
|
151
|
+
.street-pin--urgent .street-pin__badge, .street-pin--needs .street-pin__badge { color: var(--street-accent-text); }
|
|
136
152
|
.street-pin--queue .street-pin__head { background: transparent; border: 1px dashed var(--street-crate); color: var(--ink); }
|
|
137
153
|
.street-pin--queue .street-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 22%, transparent); }
|
|
138
154
|
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
links, injected into this head by the server like every hall page's
|
|
12
12
|
(tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet). -->
|
|
13
13
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
14
|
-
<link rel="stylesheet" href="/builders/street.css?v=2026-10-
|
|
15
|
-
<link rel="stylesheet" href="/builders/street-card.css?v=2026-10-
|
|
14
|
+
<link rel="stylesheet" href="/builders/street.css?v=2026-10-04-pins1">
|
|
15
|
+
<link rel="stylesheet" href="/builders/street-card.css?v=2026-10-04-pins1">
|
|
16
16
|
</head>
|
|
17
17
|
<!-- THE STREET (task 1004525, goal 1000125, R03). The street view of
|
|
18
18
|
docs/design/mocks/governor-city/Street.dc.html (renderVals' street branch,
|
|
@@ -73,11 +73,12 @@
|
|
|
73
73
|
<span class="street-today__n" id="streetTodayN">0 decided</span>
|
|
74
74
|
</div>
|
|
75
75
|
<div class="street-hud__item">
|
|
76
|
-
<span class="street-hud__dot" id="hudDot"></span>
|
|
76
|
+
<span class="street-hud__dot" id="hudDot" aria-hidden="true"></span>
|
|
77
77
|
<span class="street-label">needs you</span>
|
|
78
78
|
<span class="street-hud__n" id="hudNeeds">—</span>
|
|
79
79
|
</div>
|
|
80
80
|
<div class="street-hud__item">
|
|
81
|
+
<span class="street-hud__dot street-hud__dot--moving" aria-hidden="true"></span>
|
|
81
82
|
<span class="street-label">moving</span>
|
|
82
83
|
<span class="street-hud__n" id="hudMoving">—</span>
|
|
83
84
|
</div>
|
|
@@ -207,12 +208,12 @@
|
|
|
207
208
|
<script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
|
|
208
209
|
<script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
|
|
209
210
|
<script src="/builders/palette.js" defer></script>
|
|
210
|
-
<script src="/builders/city-draw.js?v=2026-10-
|
|
211
|
-
<script src="/builders/street-draw.js?v=2026-10-
|
|
212
|
-
<script src="/builders/street-card.js?v=2026-10-
|
|
211
|
+
<script src="/builders/city-draw.js?v=2026-10-04-pins1"></script>
|
|
212
|
+
<script src="/builders/street-draw.js?v=2026-10-04-pins1"></script>
|
|
213
|
+
<script src="/builders/street-card.js?v=2026-10-04-pins1"></script>
|
|
213
214
|
<script src="/builders/street-inside.js?v=2026-10-03-desk1"></script>
|
|
214
215
|
<script src="/builders/street-desk.js?v=2026-10-03-desk1"></script>
|
|
215
216
|
<script src="/builders/street-amend.js?v=2026-10-02-amend1"></script>
|
|
216
|
-
<script src="/builders/street.js?v=2026-10-
|
|
217
|
+
<script src="/builders/street.js?v=2026-10-04-pins1"></script>
|
|
217
218
|
</body>
|
|
218
219
|
</html>
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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 === '
|
|
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(
|
|
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
|
-
|
|
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')
|
|
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": {
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9022,5 +9022,11 @@
|
|
|
9022
9022
|
"id": "1004567",
|
|
9023
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
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
|
+
}
|
|
9025
9031
|
]
|
|
9026
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 =
|
|
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[
|
|
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.
|
|
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');
|
package/tests/hall_city.mjs
CHANGED
|
@@ -142,12 +142,38 @@ test('the line is the docket\'s own line, word for word', () => {
|
|
|
142
142
|
assert.equal(m.line, '1 matter waits on you');
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
// task 1004569 (owner, 2026-10-04): deliberate change from the prototype's
|
|
146
|
+
// drawing — ONE pin per building, in its most important matter's tier, the
|
|
147
|
+
// same tiers as the street (GovCity.buildingPin is the one rule both use).
|
|
148
|
+
test('one pin per building, in the tier of its most important matter, with the count of its open matters', () => {
|
|
149
|
+
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
150
|
+
const ms = ['moving', 'needs', 'moving', 'settled'].map((state, i) => matter({ id: `blockers:${i}`, state }));
|
|
151
|
+
ms.push(matter({ id: 'blockers:9', state: 'needs', deadline: '2026-10-03T06:00:00.000Z' }));
|
|
152
|
+
const m = City.cityModel({ docket: docket(ms), mode: 'dark', now: NOW });
|
|
153
|
+
assert.equal(m.pins.length, 1, 'one pin, never more');
|
|
154
|
+
assert.deepEqual([m.pins[0].key, m.pins[0].state, m.pins[0].count], ['blockers:9', 'urgent', 4], 'urgent (needs you, due within a day); four open');
|
|
155
|
+
assert.deepEqual(m.pins[0].matters, ['blockers:9', 'blockers:1', 'blockers:0', 'blockers:2', 'blockers:3']);
|
|
156
|
+
const k = City.isoKit(60, 30, 720, 176, 30 / 28, City.isoTheme('dark'), false);
|
|
157
|
+
const top = k.P(6, 6, City.ROOF.planning + 8);
|
|
158
|
+
assert.equal(m.pins[0].x, Math.round((top[0] - 15) * 10) / 10, 'where the prototype stands a single pin');
|
|
159
|
+
const tier = (list) => City.cityModel({ docket: docket(list), mode: 'dark', now: NOW }).pins[0].state;
|
|
160
|
+
assert.equal(tier([matter({ state: 'needs', deadline: '2026-10-05T12:00:00.000Z' })]), 'needs', 'due after a day');
|
|
161
|
+
assert.equal(tier([matter({ state: 'settled' }), matter({ id: 'b:2', state: 'moving' })]), 'moving');
|
|
162
|
+
const settled = City.cityModel({ docket: docket([matter({ state: 'settled' })]), mode: 'dark', now: NOW }).pins[0];
|
|
163
|
+
assert.deepEqual([settled.state, settled.count], ['settled', 0], 'settled: a dot, no count');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('the city and the street agree: the same docket gives each building the same tier and the same top matter', () => {
|
|
167
|
+
const S = require(path.join(PUB, 'street-draw.js'));
|
|
168
|
+
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
169
|
+
const ms = [
|
|
170
|
+
matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: '2026-10-02T20:00:00.000Z' }), matter({ id: 'board:2', building: 'charter', state: 'moving' }),
|
|
171
|
+
matter({ id: 'blockers:3', state: 'moving' }), matter({ id: 'blockers:4', state: 'needs' }),
|
|
172
|
+
matter({ id: 'override_requests:5', building: 'release', state: 'settled' }),
|
|
173
|
+
];
|
|
174
|
+
const city = City.cityModel({ docket: docket(ms), mode: 'dark', now: NOW }).pins.map((p) => [p.building, p.state, p.key, p.matters.join(' ')]).sort();
|
|
175
|
+
const street = S.streetModel({ docket: docket(ms), goals: [], rollups: [], mode: 'dark', now: NOW }).pins.map((p) => [p.building, p.state, p.key, p.matters.join(' ')]).sort();
|
|
176
|
+
assert.deepEqual(city, street);
|
|
151
177
|
});
|
|
152
178
|
|
|
153
179
|
test('a matter at a building the city does not have is not drawn, and an unknown state is never "needs"', () => {
|
|
@@ -346,16 +372,21 @@ const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1
|
|
|
346
372
|
});
|
|
347
373
|
|
|
348
374
|
test('the page reads the three feeds and draws the docket\'s pins and line', async () => {
|
|
349
|
-
const
|
|
375
|
+
const soon = new Date(Date.now() + 2 * 3600000).toISOString();
|
|
376
|
+
const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'needs' }), matter({ id: 'blockers:4', state: 'moving' }), matter({ id: 'gate_approvals:3', building: 'release', state: 'moving' }), matter({ id: 'removal_proposals:5', building: 'disputes', state: 'settled' })];
|
|
350
377
|
const p = await bootPage({ feeds: FEEDS(ms) });
|
|
351
378
|
assert.deepEqual([...p.calls].sort(), ['/api/bongos/goals', '/api/bongos/goals/rollups', '/api/bongos/government/docket']);
|
|
352
379
|
const pins = p.byId.get('cityPins').children;
|
|
353
|
-
assert.equal(pins.length,
|
|
354
|
-
assert.deepEqual(pins.map((s) => s.className).sort(), ['city-pin city-pin--moving', 'city-pin city-pin--needs', 'city-pin city-pin--
|
|
355
|
-
const
|
|
356
|
-
assert.equal(
|
|
380
|
+
assert.equal(pins.length, 4, 'one per building');
|
|
381
|
+
assert.deepEqual(pins.map((s) => s.className).sort(), ['city-pin city-pin--moving', 'city-pin city-pin--needs', 'city-pin city-pin--settled', 'city-pin city-pin--urgent']);
|
|
382
|
+
const urgent = pins.find((s) => s.attrs['data-matter'] === 'board:1');
|
|
383
|
+
assert.equal(urgent.children[0].children[0].className, 'city-pin__ring', 'the urgent pin pulses');
|
|
384
|
+
const needs = pins.find((s) => s.className.includes('needs'));
|
|
385
|
+
assert.ok(!needs.children[0].children.some((c) => c.className === 'city-pin__ring'), 'a needs-you pin is solid orange, no ring');
|
|
386
|
+
assert.equal(needs.children[0].children.find((c) => c.className === 'city-pin__count').textContent, '2', 'it counts its building\'s two open matters');
|
|
357
387
|
const moving = pins.find((s) => s.className.includes('moving'));
|
|
358
388
|
assert.ok(!moving.children[0].children.some((c) => c.className === 'city-pin__ring'), 'a moving pin only bobs');
|
|
389
|
+
assert.equal(pins.find((s) => s.className.includes('settled')).children[0].children.length, 0, 'a settled pin is a plain dot');
|
|
359
390
|
assert.equal(p.byId.get('cityLine').textContent, '2 matters wait on you · 1 moving');
|
|
360
391
|
assert.equal(p.byId.get('cityStart').attrs.href, '/builders/street');
|
|
361
392
|
assert.ok(p.byId.get('cityBlds').children.length > 9, 'the civic quarter and the district are drawn');
|
|
@@ -590,3 +590,20 @@ test('the Board Room labels a building\'s matter as that building\'s matter, and
|
|
|
590
590
|
assert.equal(subjectLine({ subject_type: 'constitutional_amendment', subject: { kind: 'decision_rules' } }), 'Who decides what [amendment]');
|
|
591
591
|
assert.equal(subjectLine({ subject_type: 'constitutional_amendment', subject: { proposed: {} } }), 'Constitutional amendment [amendment]');
|
|
592
592
|
});
|
|
593
|
+
|
|
594
|
+
// task 1004569: a street pin stands for every matter at its building; the card
|
|
595
|
+
// pages through them so each is reachable without leaving the street.
|
|
596
|
+
test('the card pager: "i of N" over the pin\'s matters, previous and next wrapping; none for one matter', () => {
|
|
597
|
+
assert.equal(C.pager(['a'], 'a'), null, 'one matter, no pager');
|
|
598
|
+
assert.equal(C.pager(null, 'a'), null);
|
|
599
|
+
assert.equal(C.pager(['a', 'b'], 'zz'), null, 'a key the pin does not hold');
|
|
600
|
+
assert.deepEqual(C.pager(['a', 'b', 'c'], 'a'), { i: 0, n: 3, words: '1 of 3', prev: 'c', next: 'b' });
|
|
601
|
+
assert.deepEqual(C.pager(['a', 'b', 'c'], 'c'), { i: 2, n: 3, words: '3 of 3', prev: 'b', next: 'a' });
|
|
602
|
+
// walking next from the first visits every key exactly once
|
|
603
|
+
const keys = ['k1', 'k2', 'k3', 'k4', 'k5'];
|
|
604
|
+
const seen = [];
|
|
605
|
+
let k = keys[0];
|
|
606
|
+
for (let i = 0; i < keys.length; i++) { seen.push(k); k = C.pager(keys, k).next; }
|
|
607
|
+
assert.deepEqual(seen, keys);
|
|
608
|
+
assert.equal(k, keys[0]);
|
|
609
|
+
});
|