@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
|
@@ -40,9 +40,11 @@
|
|
|
40
40
|
// * Review. The prototype's story opens with Review a vacant lot; in this
|
|
41
41
|
// project Review is held (the grader and the testers sign work off), so the
|
|
42
42
|
// held recipe is drawn and the parcels leave through Release.
|
|
43
|
-
// * Pins. A building can hold more matters than the prototype's
|
|
44
|
-
// three pins
|
|
45
|
-
//
|
|
43
|
+
// * Pins (task 1004569). A building can hold more matters than the prototype's
|
|
44
|
+
// one, and three pins abreast crowded each other; now ONE pin stands over a
|
|
45
|
+
// building, where the prototype stands its pin, in the importance tier of
|
|
46
|
+
// its most important matter (buildingPin below, shared with the street so
|
|
47
|
+
// both views agree), with a count when more than one matter is open there.
|
|
46
48
|
|
|
47
49
|
(function (root) {
|
|
48
50
|
'use strict';
|
|
@@ -244,7 +246,6 @@
|
|
|
244
246
|
Object.freeze({ x0: 12, x1: 15, y0: 12, y1: 15, tall: 60, route: null }),
|
|
245
247
|
]);
|
|
246
248
|
const MAX_DISTRICTS = BASE_LOTS.length * 2;
|
|
247
|
-
const MAX_PINS_PER_BUILDING = 3;
|
|
248
249
|
|
|
249
250
|
function halve(L) {
|
|
250
251
|
const w = L.x1 - L.x0 + 1, h = L.y1 - L.y0 + 1;
|
|
@@ -327,6 +328,39 @@
|
|
|
327
328
|
return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
// ---------- importance tiers (task 1004569) ----------
|
|
332
|
+
// One pin per building, in the tier of its most important matter, so the city
|
|
333
|
+
// and the street read the same at a glance. Colour AND size carry the tier
|
|
334
|
+
// (city.css, street.css), so it also reads without colour:
|
|
335
|
+
// urgent needs you and due within 24 hours (or overdue): largest, orange, pulsing
|
|
336
|
+
// needs needs you: orange, medium (the prototype's pin)
|
|
337
|
+
// moving others are deciding: ink, small
|
|
338
|
+
// settled settled recently: a small grey dot, no label, no count
|
|
339
|
+
const TIERS = Object.freeze(['urgent', 'needs', 'moving', 'settled']);
|
|
340
|
+
const URGENT_MS = 24 * 3600 * 1000;
|
|
341
|
+
function matterTier(m, now) {
|
|
342
|
+
const st = pinState(m && m.state);
|
|
343
|
+
if (st !== 'needs') return st;
|
|
344
|
+
const t = m.deadline ? Date.parse(m.deadline) : NaN;
|
|
345
|
+
return Number(now) > 0 && isFinite(t) && t - Number(now) <= URGENT_MS ? 'urgent' : 'needs';
|
|
346
|
+
}
|
|
347
|
+
// A building's matters → its one pin: the tier of the most important, the
|
|
348
|
+
// count of the open ones (settled is not open), and every matter, most
|
|
349
|
+
// important first (the docket's own order inside a tier), for the card pager.
|
|
350
|
+
function buildingPin(matters, now) {
|
|
351
|
+
const ranked = (Array.isArray(matters) ? matters : []).filter(Boolean)
|
|
352
|
+
.map((m, i) => ({ m: m, t: matterTier(m, now), i: i }))
|
|
353
|
+
.sort((a, b) => TIERS.indexOf(a.t) - TIERS.indexOf(b.t) || a.i - b.i);
|
|
354
|
+
if (!ranked.length) return null;
|
|
355
|
+
return { tier: ranked[0].t, top: ranked[0].m, count: ranked.filter((r) => r.t !== 'settled').length, matters: ranked.map((r) => r.m) };
|
|
356
|
+
}
|
|
357
|
+
// What a pin says to a screen reader: '3 open at charter, the first needs you within a day'.
|
|
358
|
+
const TIER_WORDS = Object.freeze({ urgent: 'needs you within a day', needs: 'needs you', moving: 'is moving without you', settled: 'is settled' });
|
|
359
|
+
function pinAria(building, bp) {
|
|
360
|
+
const lead = bp.count > 1 ? bp.count + ' open at ' + building + ', the first ' : bp.count === 1 ? 'One open at ' + building + ': it ' : 'At ' + building + ': ';
|
|
361
|
+
return lead + TIER_WORDS[bp.tier] + '. ' + String(bp.top.title || bp.top.short || '');
|
|
362
|
+
}
|
|
363
|
+
|
|
330
364
|
// The home line, when the docket could not be read.
|
|
331
365
|
const FALLBACK_LINE = 'the docket could not be read just now';
|
|
332
366
|
|
|
@@ -335,6 +369,7 @@
|
|
|
335
369
|
// goals GET /goals goals[] (any status; only 'open' ones are drawn)
|
|
336
370
|
// rollups GET /goals/rollups rollups[] (goal_id → task counts)
|
|
337
371
|
// mode 'dark' | 'light'
|
|
372
|
+
// now ms since the epoch (a deadline inside a day is urgent)
|
|
338
373
|
function cityModel(input) {
|
|
339
374
|
const opts = input || {};
|
|
340
375
|
const docket = opts.docket && typeof opts.docket === 'object' ? opts.docket : null;
|
|
@@ -385,7 +420,7 @@
|
|
|
385
420
|
});
|
|
386
421
|
blds.sort((a, b) => a.depth - b.depth);
|
|
387
422
|
|
|
388
|
-
//
|
|
423
|
+
// one pin over each building with a matter, in its most important matter's tier
|
|
389
424
|
const slotOf = {};
|
|
390
425
|
CIVIC_SLOTS.forEach((s) => { slotOf[s[0]] = s; });
|
|
391
426
|
const matters = docket && Array.isArray(docket.matters) ? docket.matters : [];
|
|
@@ -397,14 +432,16 @@
|
|
|
397
432
|
const pins = [];
|
|
398
433
|
let n = 0;
|
|
399
434
|
CIVIC_SLOTS.forEach((s) => {
|
|
400
|
-
const
|
|
435
|
+
const bp = buildingPin(atBuilding[s[0]], opts.now);
|
|
436
|
+
if (!bp) return;
|
|
401
437
|
const top = P(s[1] + 1, s[2] + 1, ROOF[s[0]] + 8);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
n
|
|
438
|
+
const m = bp.top;
|
|
439
|
+
const g = glyph(m.rule, 7, 7, 13);
|
|
440
|
+
pins.push({
|
|
441
|
+
key: String(m.id), building: s[0], state: bp.tier, count: bp.tier === 'settled' ? 0 : bp.count, matters: bp.matters.map((x) => String(x.id)),
|
|
442
|
+
title: String(m.short || m.title || ''), aria: pinAria(s[0], bp), x: r1(top[0] - 15), y: r1(top[1] - 52), h: 52, gf: g.fill, gl: g.line, delay: -(n * 0.9),
|
|
407
443
|
});
|
|
444
|
+
n += 1;
|
|
408
445
|
});
|
|
409
446
|
|
|
410
447
|
// work on the move: parcels ride the ring road to Review, and leave through Release
|
|
@@ -423,7 +460,7 @@
|
|
|
423
460
|
|
|
424
461
|
const line = docket && docket.line ? String(docket.line) : FALLBACK_LINE;
|
|
425
462
|
const pinCount = { needs: 0, moving: 0, settled: 0 };
|
|
426
|
-
pins.forEach((p) => { pinCount[p.state] += 1; });
|
|
463
|
+
pins.forEach((p) => { pinCount[p.state === 'urgent' ? 'needs' : p.state] += 1; });
|
|
427
464
|
const label = 'The governor city: the nine civic buildings round the plaza and '
|
|
428
465
|
+ (districts.length === 1 ? 'one district' : districts.length + ' districts') + ', one for each open goal'
|
|
429
466
|
+ (pins.length ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.');
|
|
@@ -439,7 +476,8 @@
|
|
|
439
476
|
isoTheme: isoTheme, isoKit: isoKit, isoBox: isoBox, isoGable: isoGable, isoHip: isoHip, isoSaw: isoSaw,
|
|
440
477
|
isoTree: isoTree, isoLight: isoLight, isoInstitution: isoInstitution, isoWork: isoWork, glyph: glyph, rnd: rnd,
|
|
441
478
|
lotsFor: lotsFor, districtWork: districtWork, cityModel: cityModel, rulesAtWork: rulesAtWork,
|
|
442
|
-
|
|
479
|
+
matterTier: matterTier, buildingPin: buildingPin, pinAria: pinAria, TIERS: TIERS, URGENT_MS: URGENT_MS,
|
|
480
|
+
CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS,
|
|
443
481
|
FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
|
|
444
482
|
};
|
|
445
483
|
root.GovCity = api;
|
|
@@ -84,8 +84,15 @@ html[data-fullscreen="on"] .city-top { right: 184px; }
|
|
|
84
84
|
.city-pin__head path + path { fill: none; stroke: currentColor; stroke-width: 1.2; }
|
|
85
85
|
.city-pin__stem { width: 0; flex-grow: 1; border-left: 1px solid transparent; }
|
|
86
86
|
.city-pin__ring { position: absolute; inset: -1px; border-radius: 999px; border: 1px solid var(--accent); }
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
/* importance tiers (task 1004569): colour AND size, so a tier reads without colour.
|
|
88
|
+
urgent 26 > needs 22 (the prototype's) > moving 16 > settled 8, a plain dot */
|
|
89
|
+
.city-pin--urgent .city-pin__head, .city-pin--needs .city-pin__head { background: var(--accent); color: var(--accent-on); box-shadow: var(--city-glow); }
|
|
90
|
+
.city-pin--urgent .city-pin__head { width: 26px; height: 26px; }
|
|
91
|
+
.city-pin--urgent .city-pin__stem, .city-pin--needs .city-pin__stem { border-left-color: color-mix(in srgb, var(--accent) 60%, transparent); }
|
|
92
|
+
.city-pin--moving .city-pin__head { width: 16px; height: 16px; }
|
|
93
|
+
.city-pin--moving .city-pin__head svg { width: 10px; height: 10px; }
|
|
94
|
+
.city-pin--settled .city-pin__head { width: 8px; height: 8px; margin-top: 14px; }
|
|
95
|
+
.city-pin__count { position: absolute; top: -6px; right: -9px; min-width: 14px; height: 14px; padding: 0 3px; box-sizing: border-box; border-radius: 999px; background: var(--bg-card); color: var(--ink); border: 1px solid currentColor; font-size: 9.5px; font-weight: 600; line-height: 12px; text-align: center; font-variant-numeric: tabular-nums; }
|
|
89
96
|
.city-pin--moving .city-pin__head { background: var(--ink); color: var(--bg-card); }
|
|
90
97
|
.city-pin--moving .city-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 45%, transparent); }
|
|
91
98
|
.city-pin--settled .city-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); }
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
(tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
|
|
13
13
|
The prototype carried its own Google Fonts link for the same two faces. -->
|
|
14
14
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
15
|
-
<link rel="stylesheet" href="/builders/city.css?v=2026-10-
|
|
15
|
+
<link rel="stylesheet" href="/builders/city.css?v=2026-10-04-pins1">
|
|
16
16
|
</head>
|
|
17
17
|
<!-- THE GOVERNOR CITY, HOME (task 1004524, goal 1000125, R02). The home view
|
|
18
18
|
of docs/design/mocks/governor-city/Street.dc.html (homeView and the home
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
<script src="/builders/craft-mode.js?v=2026-10-04-nolegacy"></script>
|
|
88
88
|
<script src="/builders/shell.js?v=2026-10-04-nolegacy"></script>
|
|
89
89
|
<script src="/builders/palette.js" defer></script>
|
|
90
|
-
<script src="/builders/city-draw.js?v=2026-10-
|
|
91
|
-
<script src="/builders/city.js?v=2026-10-
|
|
90
|
+
<script src="/builders/city-draw.js?v=2026-10-04-pins1"></script>
|
|
91
|
+
<script src="/builders/city.js?v=2026-10-04-pins1"></script>
|
|
92
92
|
</body>
|
|
93
93
|
</html>
|
|
@@ -132,22 +132,33 @@
|
|
|
132
132
|
var el = span('city-pin city-pin--' + pin.state);
|
|
133
133
|
el.setAttribute('data-matter', pin.key);
|
|
134
134
|
el.setAttribute('data-building', pin.building);
|
|
135
|
+
if (pin.aria) el.setAttribute('title', pin.aria);
|
|
135
136
|
el.style.left = pin.x + 'px';
|
|
136
137
|
el.style.top = pin.y + 'px';
|
|
137
138
|
el.style.height = pin.h + 'px';
|
|
138
139
|
el.style.animationDelay = pin.delay + 's';
|
|
139
140
|
var head = span('city-pin__head');
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
141
|
+
// only the urgent tier pulses; a settled pin is a plain grey dot, no mark (task 1004569)
|
|
142
|
+
if (pin.state === 'urgent') head.appendChild(span('city-pin__ring'));
|
|
143
|
+
if (pin.state !== 'settled') {
|
|
144
|
+
var svg = document.createElementNS(SVG_NS, 'svg');
|
|
145
|
+
svg.setAttribute('width', '14');
|
|
146
|
+
svg.setAttribute('height', '14');
|
|
147
|
+
svg.setAttribute('viewBox', '0 0 14 14');
|
|
148
|
+
var fill = document.createElementNS(SVG_NS, 'path');
|
|
149
|
+
fill.setAttribute('d', pin.gf || '');
|
|
150
|
+
var line = document.createElementNS(SVG_NS, 'path');
|
|
151
|
+
line.setAttribute('d', pin.gl || '');
|
|
152
|
+
svg.appendChild(fill);
|
|
153
|
+
svg.appendChild(line);
|
|
154
|
+
head.appendChild(svg);
|
|
155
|
+
}
|
|
156
|
+
// the count of the building's open matters, when there is more than one
|
|
157
|
+
if (pin.count > 1) {
|
|
158
|
+
var badge = span('city-pin__count');
|
|
159
|
+
badge.textContent = String(pin.count);
|
|
160
|
+
head.appendChild(badge);
|
|
161
|
+
}
|
|
151
162
|
el.appendChild(head);
|
|
152
163
|
el.appendChild(span('city-pin__stem'));
|
|
153
164
|
return el;
|
|
@@ -162,7 +173,7 @@
|
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
function draw(mode) {
|
|
165
|
-
var model = City.cityModel({ docket: data.docket, goals: data.goals, rollups: data.rollups, mode: mode });
|
|
176
|
+
var model = City.cityModel({ docket: data.docket, goals: data.goals, rollups: data.rollups, mode: mode, now: Date.now() });
|
|
166
177
|
var flat = $('cityFlat'), blds = $('cityBlds'), parcels = $('cityParcels'), pins = $('cityPins');
|
|
167
178
|
clear(flat); clear(blds); clear(parcels); clear(pins);
|
|
168
179
|
model.flat.forEach(function (p) { flat.appendChild(pathEl(p)); });
|
|
@@ -2,35 +2,169 @@
|
|
|
2
2
|
"_": "THE GOVERNOR CITY's home (task 1004524, goal 1000125) through modules/ui-design/kit/render.js. The page draws GET /government/docket, GET /goals and GET /goals/rollups; against the kit's own stub none exists, so every state renders the city with no districts and no pins and the line saying the docket could not be read: a real state the floors must hold in. To look at the populated city, run it against the hall-preview harness, whose government__docket.json fixture carries the reference design's seven sample matters (3 need you, 2 moving, 2 settled) and whose goals fixtures carry the open goals:\n\n node scripts/hall-preview/server.js --port 4624 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/city.states.json --base-in http://localhost:4624 --base-out http://localhost:4624\n\nThe page follows the OS mode until the switch is pressed (then the hall's own 'hall-theme' preference), so the kit's dark/light emulation renders both modes.",
|
|
3
3
|
"page": "/builders/city",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
|
-
"stub": {
|
|
5
|
+
"stub": {
|
|
6
|
+
"prefix": "/builders/"
|
|
7
|
+
},
|
|
6
8
|
"modeQuery": false,
|
|
7
|
-
"ignoreRequests": [
|
|
8
|
-
|
|
9
|
+
"ignoreRequests": [
|
|
10
|
+
"/api/(gds|bongos)/",
|
|
11
|
+
"fonts\\.g(oogleapis|static)\\.com"
|
|
12
|
+
],
|
|
13
|
+
"ignoreConsole": [
|
|
14
|
+
"/api/(gds|bongos)/",
|
|
15
|
+
"\\b401\\b",
|
|
16
|
+
"\\b403\\b",
|
|
17
|
+
"\\b404\\b",
|
|
18
|
+
"Unauthorized",
|
|
19
|
+
"Forbidden",
|
|
20
|
+
"Not Found",
|
|
21
|
+
"load failed",
|
|
22
|
+
"fonts\\.g(oogleapis|static)\\.com"
|
|
23
|
+
],
|
|
9
24
|
"states": {
|
|
10
25
|
"city": {
|
|
11
26
|
"auth": true,
|
|
12
27
|
"_": "The home: the wordmark, the switch, the city, start governing and the one line.",
|
|
13
|
-
"actions": [
|
|
14
|
-
|
|
28
|
+
"actions": [
|
|
29
|
+
[
|
|
30
|
+
"wait",
|
|
31
|
+
900
|
|
32
|
+
]
|
|
33
|
+
],
|
|
34
|
+
"expect": {
|
|
35
|
+
"visible": [
|
|
36
|
+
"#cityStage",
|
|
37
|
+
"#cityStart",
|
|
38
|
+
"#cityLine",
|
|
39
|
+
"#cityMode",
|
|
40
|
+
"#app-topbar",
|
|
41
|
+
"#fullscreen-toggle"
|
|
42
|
+
],
|
|
43
|
+
"href": [
|
|
44
|
+
[
|
|
45
|
+
"#cityStart",
|
|
46
|
+
"/builders/street"
|
|
47
|
+
]
|
|
48
|
+
]
|
|
49
|
+
}
|
|
15
50
|
},
|
|
16
51
|
"fullscreen": {
|
|
17
52
|
"auth": true,
|
|
18
53
|
"_": "In the hall since task 1004544: the shell's full-screen switch pressed, the sidebar and the top bar gone, the city filling the window and the one exit in the corner (the sky's mechanism, task 1004327).",
|
|
19
|
-
"actions": [
|
|
20
|
-
|
|
54
|
+
"actions": [
|
|
55
|
+
[
|
|
56
|
+
"wait",
|
|
57
|
+
900
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"click",
|
|
61
|
+
"#fullscreen-toggle"
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"wait",
|
|
65
|
+
500
|
|
66
|
+
]
|
|
67
|
+
],
|
|
68
|
+
"expect": {
|
|
69
|
+
"visible": [
|
|
70
|
+
"#cityStage",
|
|
71
|
+
"#cityStart",
|
|
72
|
+
"#cityMode",
|
|
73
|
+
"#fullscreen-toggle"
|
|
74
|
+
],
|
|
75
|
+
"hidden": [
|
|
76
|
+
"#app-sidebar",
|
|
77
|
+
"#topbar-title"
|
|
78
|
+
],
|
|
79
|
+
"attr": [
|
|
80
|
+
[
|
|
81
|
+
"#fullscreen-toggle",
|
|
82
|
+
"aria-pressed",
|
|
83
|
+
"true"
|
|
84
|
+
]
|
|
85
|
+
]
|
|
86
|
+
}
|
|
21
87
|
},
|
|
22
88
|
"switched": {
|
|
23
89
|
"auth": true,
|
|
24
90
|
"_": "The sun/moon switch pressed once: the other mode, the city redrawn in it.",
|
|
25
|
-
"actions": [
|
|
26
|
-
|
|
91
|
+
"actions": [
|
|
92
|
+
[
|
|
93
|
+
"wait",
|
|
94
|
+
600
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"click",
|
|
98
|
+
"#cityMode"
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
"wait",
|
|
102
|
+
400
|
|
103
|
+
]
|
|
104
|
+
],
|
|
105
|
+
"expect": {
|
|
106
|
+
"visible": [
|
|
107
|
+
"#cityStage",
|
|
108
|
+
"#cityStart"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
27
111
|
},
|
|
28
112
|
"focus": {
|
|
29
113
|
"auth": true,
|
|
30
114
|
"_": "Keyboard: the start button focused, its ring drawn.",
|
|
31
|
-
"actions": [
|
|
32
|
-
|
|
115
|
+
"actions": [
|
|
116
|
+
[
|
|
117
|
+
"wait",
|
|
118
|
+
600
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
"focus",
|
|
122
|
+
"#cityStart"
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"wait",
|
|
126
|
+
100
|
|
127
|
+
]
|
|
128
|
+
],
|
|
129
|
+
"expect": {
|
|
130
|
+
"activeId": "cityStart"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"pins": {
|
|
134
|
+
"auth": true,
|
|
135
|
+
"_": "The home city over the dense docket with the same pin tiers as the street (task 1004569): one pin per building, urgent largest and pulsing, needs you orange, moving small and ink, a count where a building holds more than one open matter.",
|
|
136
|
+
"actions": [
|
|
137
|
+
[
|
|
138
|
+
"wait",
|
|
139
|
+
900
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"clickIfVisible",
|
|
143
|
+
"[data-craft-close]"
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
"wait",
|
|
147
|
+
300
|
|
148
|
+
]
|
|
149
|
+
],
|
|
150
|
+
"expect": {
|
|
151
|
+
"visible": [
|
|
152
|
+
"#cityPins",
|
|
153
|
+
".city-pin--urgent",
|
|
154
|
+
".city-pin--needs",
|
|
155
|
+
".city-pin--moving",
|
|
156
|
+
".city-pin__count"
|
|
157
|
+
]
|
|
158
|
+
}
|
|
33
159
|
}
|
|
34
160
|
},
|
|
35
|
-
"reducedMotion": {
|
|
161
|
+
"reducedMotion": {
|
|
162
|
+
"auth": true,
|
|
163
|
+
"actions": [
|
|
164
|
+
[
|
|
165
|
+
"wait",
|
|
166
|
+
600
|
|
167
|
+
]
|
|
168
|
+
]
|
|
169
|
+
}
|
|
36
170
|
}
|
|
@@ -84,6 +84,12 @@ body[data-page="street"] {
|
|
|
84
84
|
|
|
85
85
|
/* more: the detail, in place */
|
|
86
86
|
.mcard-more__toggle { margin-top: -2px; }
|
|
87
|
+
/* the pager: "1 of N" with previous and next, when a pin holds more than one matter (task 1004569) */
|
|
88
|
+
.mcard-pager { display: flex; align-items: center; gap: 4px; margin: -4px 0 2px; }
|
|
89
|
+
.mcard-pager__btn { width: 32px; height: 32px; border-radius: 999px; border: 1px solid var(--street-rule-strong); background: none; color: var(--ink); font-family: var(--font-body); font-size: 17px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0 0 2px; }
|
|
90
|
+
.mcard-pager__btn:hover { border-color: var(--ink-faint); }
|
|
91
|
+
.mcard-pager__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
92
|
+
.mcard-pager__at { min-width: 52px; text-align: center; font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
|
|
87
93
|
.mcard-more { display: flex; flex-direction: column; gap: 6px; padding-top: 10px; border-top: 1px solid var(--street-rule); }
|
|
88
94
|
.mcard-more__text { margin: 0 0 4px; font-size: 14px; line-height: 1.5; white-space: pre-line; overflow-wrap: anywhere; }
|
|
89
95
|
.mcard-more__meta { margin: 0; font-size: 13px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
|
|
@@ -852,12 +852,40 @@
|
|
|
852
852
|
return b;
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
+
// THE PAGER (task 1004569). A street pin stands for every matter at its
|
|
856
|
+
// building; the card opens on the most important, and when there are more it
|
|
857
|
+
// carries "1 of N" with previous and next, which wrap, so every matter there
|
|
858
|
+
// is reachable without leaving the street. Pure: the pin's keys (most
|
|
859
|
+
// important first) and the open key → the pager, or null for one matter.
|
|
860
|
+
function pager(keys, key) {
|
|
861
|
+
var ks = Array.isArray(keys) ? keys.map(str) : [];
|
|
862
|
+
var n = ks.length, i = ks.indexOf(str(key));
|
|
863
|
+
if (n < 2 || i < 0) return null;
|
|
864
|
+
return { i: i, n: n, words: (i + 1) + ' of ' + n, prev: ks[(i - 1 + n) % n], next: ks[(i + 1) % n] };
|
|
865
|
+
}
|
|
866
|
+
function pagerRow(doc, pg, on) {
|
|
867
|
+
var row = el(doc, 'div', 'mcard-pager');
|
|
868
|
+
var arrow = function (dir, glyph, words) {
|
|
869
|
+
var b = button(doc, 'mcard-pager__btn', glyph, 'page-' + dir, function () { on.page(pg[dir], dir); }, false);
|
|
870
|
+
b.setAttribute('aria-label', words);
|
|
871
|
+
return b;
|
|
872
|
+
};
|
|
873
|
+
row.appendChild(arrow('prev', '‹', 'Previous matter here'));
|
|
874
|
+
var at = el(doc, 'span', 'mcard-pager__at', pg.words);
|
|
875
|
+
at.setAttribute('aria-live', 'polite');
|
|
876
|
+
row.appendChild(at);
|
|
877
|
+
row.appendChild(arrow('next', '›', 'Next matter here'));
|
|
878
|
+
return row;
|
|
879
|
+
}
|
|
880
|
+
|
|
855
881
|
// Build the card's body into `host` (emptied first). on: { act(key, input),
|
|
856
|
-
// openForm(name), ask(key), more() }. ui: { form: 'revise'|null,
|
|
857
|
-
// more: false, error: '', busy: false, fresh: seatKey|null
|
|
882
|
+
// openForm(name), ask(key), more(), page(key, dir) }. ui: { form: 'revise'|null,
|
|
883
|
+
// ask: actKey|null, more: false, error: '', busy: false, fresh: seatKey|null,
|
|
884
|
+
// pager: pager()|null }.
|
|
858
885
|
function render(doc, host, cv, on, ui) {
|
|
859
886
|
var u = ui || {};
|
|
860
887
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
888
|
+
if (u.pager && on && on.page) host.appendChild(pagerRow(doc, u.pager, on));
|
|
861
889
|
if (cv.fact) host.appendChild(el(doc, 'p', 'mcard-fact', cv.fact));
|
|
862
890
|
if (cv.quote && cv.quote.words) host.appendChild(el(doc, 'p', 'mcard-quote', (cv.quote.by ? cv.quote.by + ': ' : '') + cv.quote.words));
|
|
863
891
|
if (cv.lines && cv.lines.length) {
|
|
@@ -1048,7 +1076,7 @@
|
|
|
1048
1076
|
|
|
1049
1077
|
var api = {
|
|
1050
1078
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
1051
|
-
model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
|
|
1079
|
+
model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render, pager: pager,
|
|
1052
1080
|
memberWords: memberWords, windowWords: windowWords, LINE: LINE, compact: compact,
|
|
1053
1081
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
1054
1082
|
change: changeOf,
|
|
@@ -38,9 +38,20 @@
|
|
|
38
38
|
// are the work queued at its door: up to three crates outside it and the
|
|
39
39
|
// prototype's dashed count pin, "waiting for a tester", instead of one pin
|
|
40
40
|
// each.
|
|
41
|
-
// * Pins. A building can hold more matters than the prototype's
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// * Pins (task 1004569). A building can hold more matters than the prototype's
|
|
42
|
+
// one, and three stacked labels per building ran into their neighbours'.
|
|
43
|
+
// Now ONE pin stands over a building, where the prototype stands its pin,
|
|
44
|
+
// in the importance tier of its most important matter (GovCity.buildingPin,
|
|
45
|
+
// shared with the home city so both views agree): urgent (needs you within
|
|
46
|
+
// a day) largest and pulsing, needs you orange, moving small and ink,
|
|
47
|
+
// settled a grey dot. It carries the count of the building's open matters.
|
|
48
|
+
// Only urgent and needs-you pins show a label (cut by the line rule's fit(),
|
|
49
|
+
// street-desk.js); a moving pin shows its label on hover or focus. A
|
|
50
|
+
// collision pass (placeLabels) keeps every shown label clear of every other:
|
|
51
|
+
// two that would meet stand on two alternating heights, and when they still
|
|
52
|
+
// meet the lower tier's label waits for hover (its count and colour stay).
|
|
53
|
+
// Tapping a pin opens its most important matter's card, with "1 of N" and
|
|
54
|
+
// previous/next inside the card when the building holds more.
|
|
44
55
|
// * State lines. The reading that reads a building gives its line when it is
|
|
45
56
|
// live (people, money, fairness, rules at work); Planning says how many goals
|
|
46
57
|
// are open; otherwise the line counts the building's matters.
|
|
@@ -264,8 +275,19 @@
|
|
|
264
275
|
const MORE_KEY = 'more';
|
|
265
276
|
const GOALS_HREF = '/builders/goals';
|
|
266
277
|
const ARTBOARD_W = 1440;
|
|
267
|
-
|
|
278
|
+
// A pin's label: at most PIN_LABEL characters (the line rule's fit()), about
|
|
279
|
+
// LABEL_CH px a character at 12 px (a little over, so the box never
|
|
280
|
+
// undercounts), centred on the stem and never wider than LABEL_MAX_W. A
|
|
281
|
+
// label that has to step aside stands PIN_STACK higher.
|
|
282
|
+
const PIN_W = 110;
|
|
283
|
+
const PIN_LABEL = 22;
|
|
284
|
+
const LABEL_CH = 6.6;
|
|
285
|
+
const LABEL_MAX_W = 150;
|
|
286
|
+
const LABEL_H = 15;
|
|
287
|
+
const LABEL_GAP = 4;
|
|
268
288
|
const PIN_STACK = 40;
|
|
289
|
+
// the tiers whose label shows without a hover, most important first
|
|
290
|
+
const LABEL_TIERS = Object.freeze(['urgent', 'needs', 'queue']);
|
|
269
291
|
const MAX_CRATES = 3;
|
|
270
292
|
const HINT = 'work moves left to right · orange waits on you · white is moving without you';
|
|
271
293
|
const READING_KEYS = Object.freeze(['flow', 'promises', 'money', 'people', 'fairness', 'rules']);
|
|
@@ -381,6 +403,56 @@
|
|
|
381
403
|
return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
|
|
382
404
|
}
|
|
383
405
|
|
|
406
|
+
// The line rule's one formatter (street-desk.js), read when a street is
|
|
407
|
+
// drawn: street-desk.js loads after this file and itself requires this one,
|
|
408
|
+
// so it is found on the global it hangs itself on, never required from here
|
|
409
|
+
// (that would be a require cycle). The page always loads it before it draws.
|
|
410
|
+
function lineFit(s, n) {
|
|
411
|
+
const D = root.GovDesk;
|
|
412
|
+
return D && D.fit ? D.fit(s, n) : String(s == null ? '' : s);
|
|
413
|
+
}
|
|
414
|
+
// One short label: the matter's short name, then its deadline in the
|
|
415
|
+
// prototype's words, the name cut so the deadline always survives.
|
|
416
|
+
function pinLabel(short, when) {
|
|
417
|
+
if (!when) return lineFit(short, PIN_LABEL);
|
|
418
|
+
return lineFit(short, Math.max(4, PIN_LABEL - when.length - 3)) + ' · ' + when;
|
|
419
|
+
}
|
|
420
|
+
// The box a shown label takes on the board, padded by LABEL_GAP: it sits at
|
|
421
|
+
// the top of the pin's column, centred on the stem.
|
|
422
|
+
function labelBox(p) {
|
|
423
|
+
const w = Math.min(LABEL_MAX_W, Math.ceil(String(p.label).length * LABEL_CH));
|
|
424
|
+
const cx = p.x + PIN_W / 2;
|
|
425
|
+
return { x0: cx - w / 2 - LABEL_GAP, x1: cx + w / 2 + LABEL_GAP, y0: p.y - LABEL_GAP / 2, y1: p.y + LABEL_H + LABEL_GAP / 2 };
|
|
426
|
+
}
|
|
427
|
+
function meets(a, b) { return a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1; }
|
|
428
|
+
// The collision pass. Labels are placed most important first (then left to
|
|
429
|
+
// right): each takes the pin's own height if it is clear there, else the
|
|
430
|
+
// alternate height PIN_STACK above, else it is quiet (shown on hover or
|
|
431
|
+
// focus only; the count and colour stay). Moving and settled pins never
|
|
432
|
+
// show a label without a hover, so they take no room. Mutates pins.
|
|
433
|
+
function placeLabels(pins) {
|
|
434
|
+
const rank = (p) => LABEL_TIERS.indexOf(p.rank);
|
|
435
|
+
const placed = [];
|
|
436
|
+
pins.filter((p) => p.label && LABEL_TIERS.indexOf(p.rank) >= 0)
|
|
437
|
+
.sort((a, b) => rank(a) - rank(b) || a.x - b.x)
|
|
438
|
+
.forEach((p) => {
|
|
439
|
+
const base = { y: p.y, h: p.h };
|
|
440
|
+
for (const lift of [0, PIN_STACK]) {
|
|
441
|
+
const q = { x: p.x, y: base.y - lift, label: p.label };
|
|
442
|
+
const bx = labelBox(q);
|
|
443
|
+
if (!placed.some((o) => meets(o, bx))) {
|
|
444
|
+
p.y = q.y; p.h = base.h + lift; p.shown = true; p.lifted = lift > 0;
|
|
445
|
+
placed.push(bx);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
p.shown = false;
|
|
450
|
+
});
|
|
451
|
+
return pins;
|
|
452
|
+
}
|
|
453
|
+
// Every shown label's box, for the tests and the proof renders.
|
|
454
|
+
function shownLabels(pins) { return (pins || []).filter((p) => p.shown).map((p) => Object.assign({ key: p.key }, labelBox(p))); }
|
|
455
|
+
|
|
384
456
|
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
385
457
|
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
386
458
|
// A deadline in the prototype's words: 'today', a weekday inside the week, else
|
|
@@ -498,28 +570,33 @@
|
|
|
498
570
|
return { key: n, name: n, x: x0, w: x1 - x0 };
|
|
499
571
|
});
|
|
500
572
|
|
|
501
|
-
//
|
|
573
|
+
// one pin per building, in the tier of its most important matter (task 1004569)
|
|
502
574
|
const pins = [];
|
|
503
575
|
const byB = {};
|
|
504
576
|
matters.forEach((m) => { if (m.building !== 'review') (byB[m.building] = byB[m.building] || []).push(m); });
|
|
505
577
|
CIVIC_KEYS.forEach((k) => {
|
|
578
|
+
const bp = City.buildingPin(byB[k], now);
|
|
579
|
+
if (!bp) return;
|
|
506
580
|
const s = at[k];
|
|
507
|
-
const here2 = (byB[k] || []).slice(0, MAX_PINS_PER_BUILDING);
|
|
508
581
|
const top = G - roofs[k] - 10;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
582
|
+
const m = bp.top;
|
|
583
|
+
const g = City.glyph(m.rule, 9, 9, 16);
|
|
584
|
+
const short = String(m.short || m.title || '');
|
|
585
|
+
pins.push({
|
|
586
|
+
key: String(m.id), building: k, state: bp.tier, rank: bp.tier, isCount: false,
|
|
587
|
+
count: bp.tier === 'settled' ? '' : String(bp.count), matters: bp.matters.map((x) => String(x.id)),
|
|
588
|
+
x: Math.round(s.x + s.w / 2 - 55 + (k === 'release' ? 4 : 0)), y: top - 78, h: 78,
|
|
589
|
+
label: bp.tier === 'settled' ? '' : pinLabel(short, whenWord(m.deadline, now)), shown: false, lifted: false,
|
|
590
|
+
aria: City.pinAria(k, bp), gf: g.fill, gl: g.line,
|
|
518
591
|
});
|
|
519
592
|
});
|
|
520
593
|
if (queue.length) {
|
|
521
|
-
|
|
594
|
+
// the queue's dashed count pin keeps the prototype's look; its label is
|
|
595
|
+
// placed in the rank of the most important work waiting
|
|
596
|
+
const qt = City.buildingPin(queue, now).tier;
|
|
597
|
+
pins.push({ key: 'queue', building: 'review', state: 'queue', rank: qt === 'urgent' || qt === 'needs' ? 'queue' : qt, isCount: true, count: String(queue.length), matters: ['queue'], x: Math.round(rv.x - 58), y: G - 70, h: 64, label: 'waiting for a tester', shown: false, lifted: false, aria: (queue.length === 1 ? 'One piece of work waits' : queue.length + ' pieces of work wait') + ' for a tester at Review', gf: '', gl: '' });
|
|
522
598
|
}
|
|
599
|
+
placeLabels(pins);
|
|
523
600
|
|
|
524
601
|
// every building and district on the street is a door
|
|
525
602
|
const doors = segs.map((s) => s.k
|
|
@@ -543,7 +620,7 @@
|
|
|
543
620
|
const moving = Math.max(0, Number(counts.moving) || 0), settled = Math.max(0, Number(counts.settled) || 0);
|
|
544
621
|
const share = monthShare(opts.earnings, now);
|
|
545
622
|
const pinCount = { needs: 0, moving: 0, settled: 0 };
|
|
546
|
-
pins.forEach((p) => { if (pinCount[
|
|
623
|
+
pins.forEach((p) => { const t = p.state === 'urgent' ? 'needs' : p.state; if (pinCount[t] != null) pinCount[t] += 1; });
|
|
547
624
|
return {
|
|
548
625
|
mode: mode, width: W, evening: evening, reviewHeld: reviewHeld,
|
|
549
626
|
flat: flat, blds: blds, crates: crates, cols: cols, groups: groups, pins: pins, doors: doors, signs: signs,
|
|
@@ -582,9 +659,9 @@
|
|
|
582
659
|
theme: theme, push: push, rect: rect, ellipse: ellipse, box: box, gable: gable, sawtooth: sawtooth, tree: tree, light: light, civic: civic, work: work,
|
|
583
660
|
layout: layout, districtWidth: districtWidth, districtItems: districtItems, districtLine: districtLine, countsLine: countsLine,
|
|
584
661
|
whenWord: whenWord, reviewHeldOf: reviewHeldOf, monthShare: monthShare, money: money, parseHash: parseHash, buildingHref: buildingHref, districtHref: districtHref,
|
|
585
|
-
streetModel: streetModel, cardFor: cardFor,
|
|
662
|
+
streetModel: streetModel, cardFor: cardFor, placeLabels: placeLabels, labelBox: labelBox, shownLabels: shownLabels, pinLabel: pinLabel,
|
|
586
663
|
G: G, SEGS: SEGS, GROUP_NAMES: GROUP_NAMES, CIVIC_KEYS: CIVIC_KEYS, READING_KEYS: READING_KEYS, ARTBOARD_W: ARTBOARD_W,
|
|
587
|
-
|
|
664
|
+
PIN_W: PIN_W, PIN_LABEL: PIN_LABEL, LABEL_MAX_W: LABEL_MAX_W, PIN_STACK: PIN_STACK, MAX_CRATES: MAX_CRATES, MAX_DISTRICTS: MAX_DISTRICTS, GOALS_HREF: GOALS_HREF, HINT: HINT, FALLBACK_LINE: FALLBACK_LINE, HOME_HREF: '/builders/city',
|
|
588
665
|
};
|
|
589
666
|
root.GovStreet = api;
|
|
590
667
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|