@bongos/core 1.21.10 → 1.21.11

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.
@@ -0,0 +1,568 @@
1
+ // modules/hall-ui/public/street-inside.js — INSIDE EVERY BUILDING, and inside
2
+ // every district, as data (task 1004526, goal 1000125; R04 of the Governor City).
3
+ //
4
+ // WHAT THIS IS. The two interior views of the reference design,
5
+ // docs/design/mocks/governor-city/Street.dc.html: buildingView() (the building
6
+ // in section: every storey a real floor with its slab, cut walls and glazing,
7
+ // three lamps, a partition with a doorway, a framed sign carrying the floor's
8
+ // symbol, its name and state lettered on the wall, its count and level number;
9
+ // dark empty floors up to the building's capacity; the crown; the lift in its
10
+ // shaft; the annex lot; and the right panel) and districtView() (a goal's work
11
+ // as small buildings on a road, shape the craft, stage the status). The kit
12
+ // pieces are the prototype's, ported verbatim: crown(), workItem(),
13
+ // floorSymbol(), symbolPath() and the section drawing itself.
14
+ // tests/hall_street_inside.mjs holds them equal by running the prototype's own
15
+ // Component class beside this file, on the prototype's own sample floors.
16
+ // The front-on kit (box, gable, sawtooth, light) is street-draw.js's and the
17
+ // decision glyphs city-draw.js's: one copy, never a fork.
18
+ //
19
+ // ONE CHANGE OF INPUT, as on the street. The prototype filled each building
20
+ // from civicInfo(), its sample council, and each district from districtInfo().
21
+ // Here:
22
+ // * A building's floors are the prototype's floor STRUCTURE (PLAN below: the
23
+ // same floors, in the same order, at the same capacity, with the names that
24
+ // are about the government rather than the sample project). A floor the
25
+ // governor docket (GET /government/docket) already reads is BOUND to it: a
26
+ // source (its open matters, their count, needs you when one does) or a
27
+ // reading (its figure and line), or, for Planning's goals, the open goals the
28
+ // street already holds. Every other floor stands in an honest "not recorded
29
+ // yet" state: its name, what it holds, no number. Real data for every floor
30
+ // is task 1004527, which fills these same descriptors (floorFor()).
31
+ // * Who holds a building and how it decides. The docket does not carry a
32
+ // holder yet (task 1004529 sets who decides what, per project); until it
33
+ // does the seat is drawn empty and says so, and the rule is the one the
34
+ // building's own open matters are decided by, else the design's default,
35
+ // named as the default. A docket building that carries `holders` / `rule`
36
+ // is read first (holderOf()).
37
+ // * On the table here: the docket's matters at this building. Each row opens
38
+ // the short card (GovStreet.cardFor(); task 1004528 makes it act).
39
+ // * The record: those same matters filed by the month they were opened, one
40
+ // drawer a month, newest first.
41
+ // * The annex lot stands when the building is full, as the prototype's
42
+ // "growth" lines say it would.
43
+ // * A district's work is the goal's tasks (GET /tasks?goal_id=N): shape from
44
+ // the task's discipline, stage from its status, label its kind; up to
45
+ // MAX_ITEMS stand on the road and the rest are counted, with a link to the
46
+ // goal. Heights come from the task's estimate, not from a random.
47
+ //
48
+ // PURE. No DOM, no fetch, no clock: street.js passes everything in.
49
+
50
+ (function (root) {
51
+ 'use strict';
52
+ const City = root.GovCity || (typeof require === 'function' ? require('./city-draw.js') : null);
53
+ const S = root.GovStreet || (typeof require === 'function' ? require('./street-draw.js') : null);
54
+ const push = S.push, rect = S.rect, ellipse = S.ellipse;
55
+
56
+ // ---------- the kit pieces (verbatim) ----------
57
+
58
+ // The crown caps the section with the building's own silhouette.
59
+ function crown(out, T, kind, x0, w, top) {
60
+ const C = T.civic, R = T.roof, Gl = T.glass;
61
+ const r = (x, y, ww, hh, tone) => push(out, rect(x, y, ww, hh), tone.f, tone.s);
62
+ if (kind === 'charter') { r(x0 + 60, top - 26, w - 120, 26, C); r(x0 + 130, top - 52, w - 260, 26, C); r(x0 + 175, top - 74, w - 350, 22, Gl); S.light(out, x0 + w / 2, top - 84, 3.5, T.beacon); }
63
+ else if (kind === 'rules') r(x0 - 12, top - 10, w + 24, 10, R);
64
+ else if (kind === 'treasury') { r(x0 - 10, top - 10, w + 20, 10, R); r(x0 + 120, top - 32, w - 240, 22, C); }
65
+ else if (kind === 'planning') S.sawtooth(out, x0, top, w, 6, 22, R, Gl);
66
+ else if (kind === 'review') { r(x0 + 16, top - 30, w - 32, 30, Gl); r(x0 + 10, top - 34, w - 20, 4, R); }
67
+ else if (kind === 'release') { r(x0 + 26, top - 64, 26, 64, C); r(x0 + w - 52, top - 64, 26, 64, C); r(x0 + 52, top - 50, w - 104, 12, C); S.light(out, x0 + 39, top - 72, 2.8, T.beacon); S.light(out, x0 + w - 39, top - 72, 2.8, T.beacon); }
68
+ else if (kind === 'automation') { r(x0 + 40, top - 70, 34, 70, C); r(x0 + w - 130, top - 28, 100, 28, R); S.light(out, x0 + 57, top - 80, 3, T.beacon); }
69
+ else if (kind === 'people' || kind === 'disputes') { const hr = kind === 'people' ? 54 : 38; push(out, 'M' + (x0 - 8) + ' ' + top + 'L' + (x0 + w / 2) + ' ' + (top - hr) + 'L' + (x0 + w + 8) + ' ' + top + 'Z', R.f, R.s); }
70
+ }
71
+
72
+ // A district work item: shape from the craft, stage from the status.
73
+ function workItem(out, T, craft, stage, x, w, h) {
74
+ if (stage === 'backlog') {
75
+ push(out, 'M' + x + ' 0L' + (x + 7) + ' -7L' + (x + w + 7) + ' -7L' + (x + w) + ' 0Z', 'none', T.ghost.s, '3 3');
76
+ push(out, 'M' + (x + 3) + ' 0V-16M' + (x + w - 3) + ' 0V-16', 'none', T.ghost.s);
77
+ return;
78
+ }
79
+ if (stage === 'ready') { S.box(out, T, x, 0, w, 6, T.work, { d: 6 }); return; }
80
+ if (stage === 'being built') { S.box(out, T, x, 0, w, h, T.scaffold, { d: 6, brace: true }); return; }
81
+ const lit = stage === 'shipped';
82
+ let top = h;
83
+ if (craft === 'artist') { const bh = Math.round(h * 0.8); S.box(out, T, x, 0, w, bh, T.work, { d: 6, win: true, lit: lit }); S.sawtooth(out, x, -bh, w, 3, 12, T.roof, T.glass); top = bh + 12; }
84
+ else if (craft === 'ui') { S.box(out, T, x, 0, w, h, T.glass, { d: 6, grid: 3 }); S.box(out, T, x - 2, 0, w + 4, 4, T.roof, { d: 6, z: h }); top = h + 4; }
85
+ else if (craft === 'ideator') { S.box(out, T, x, 0, w, 22, T.work, { d: 6, door: true }); S.gable(out, x, -22, w, 14, T.roof); top = 36; }
86
+ else S.box(out, T, x, 0, w, h, T.work, { d: 6, win: true, lit: lit });
87
+ if (!lit) {
88
+ const fx = x + w / 2;
89
+ push(out, 'M' + fx + ' ' + (-top) + 'V' + (-top - 18), 'none', T.line);
90
+ push(out, 'M' + fx + ' ' + (-top - 18) + 'h11l-3 4l3 4h-11Z', stage === 'awaiting a tester' ? '#fb9569' : T.line, 'none');
91
+ }
92
+ }
93
+
94
+ // Every floor carries a sign: one symbol, drawn on a 24-unit grid, that says
95
+ // what the floor is. PROTO_SYMBOLS is the prototype's map, verbatim; the
96
+ // floors PLAN names that the sample project did not are added after it, each
97
+ // to one of the forty symbols of the library (never a new one).
98
+ const PROTO_SYMBOLS = {
99
+ '§1 Purpose': 'compass', '§2 Membership': 'door', '§3 The council': 'council', '§4 Rules': 'gavel', '§5 Money': 'coins', '§6 Review': 'magnifier', '§7 Release': 'gate',
100
+ 'Bugs go to the maintenance goal': 'bug', 'Newcomers start on newcomer tasks': 'sprout', 'Payouts every Friday': 'calendar', 'Screenshots on UI work': 'frame', 'Small fixes ship without a vote': 'wrench',
101
+ 'Security report: sign-in limits': 'shield', 'Removal: the old search page': 'removal', 'Appeal: the refund flow grade': 'scales',
102
+ 'The council': 'council', 'Testers': 'badge', 'Builders': 'people', 'Newcomers': 'sprout', 'Waiting to join': 'hourglass',
103
+ 'Hall': 'hall', 'Payments': 'card', 'Onboarding': 'door', 'Modules': 'modules', 'Payouts v2': 'coinhand',
104
+ 'Being tested': 'magnifier', 'Signed off': 'seal', 'Waiting outside': 'hourglass',
105
+ 'Held back': 'pause', 'Core update': 'update', 'Live this month': 'live', 'Ready to go': 'crate',
106
+ 'Builder rewards': 'coinhand', 'AI grading': 'chip', 'Hosting': 'server', 'Reserve': 'safe', 'Second AI reviewer': 'chip',
107
+ 'Founding': 'flag', 'Ideas to ratify': 'bulb', 'Priorities': 'ordered', 'Versions': 'layers', 'Waiting on a decision': 'hourglass',
108
+ 'Ranks': 'ladder', 'Goal teams': 'team', 'Overrides': 'key',
109
+ 'The switch': 'power', 'Allowed goals': 'flag', 'Running now': 'chip', 'Spending limit': 'gauge', 'Reports': 'doc',
110
+ };
111
+ const PLAN_SYMBOLS = {
112
+ 'Rules in force': 'gavel', 'New rules': 'wrench', 'Security reports': 'shield', 'Removals': 'removal', 'Grade appeals': 'scales',
113
+ 'Goals': 'flag', 'Promises': 'seal', 'Spending matters': 'coins',
114
+ };
115
+ function floorSymbol(name) {
116
+ return PROTO_SYMBOLS[name] || PLAN_SYMBOLS[name] || 'crate';
117
+ }
118
+
119
+ const SYMBOLS = {
120
+ compass: ['M21 12a9 9 0 1 1-18 0a9 9 0 1 1 18 0Z M12 5L14.5 12L12 19L9.5 12Z', 'M12 5L14.5 12L9.5 12Z'],
121
+ door: ['M5 21V3h9v18 M14 5.5l5 2V21 M3 21h18 M11.5 12v1.5', ''],
122
+ council: ['M3 15h18 M5 15v5 M19 15v5 M9 15v5 M15 15v5', 'M3.4 10.5a1.6 1.6 0 1 0 3.2 0a1.6 1.6 0 1 0-3.2 0Z M6.9 8.5a1.6 1.6 0 1 0 3.2 0a1.6 1.6 0 1 0-3.2 0Z M10.4 7.5a1.6 1.6 0 1 0 3.2 0a1.6 1.6 0 1 0-3.2 0Z M13.9 8.5a1.6 1.6 0 1 0 3.2 0a1.6 1.6 0 1 0-3.2 0Z M17.4 10.5a1.6 1.6 0 1 0 3.2 0a1.6 1.6 0 1 0-3.2 0Z'],
123
+ gavel: ['M5.5 9.5l5-5 4 4-5 5Z M10.5 11.5L19 20 M3 21h9', ''],
124
+ coins: ['M3 9a4 1.6 0 1 0 8 0a4 1.6 0 1 0-8 0Z M3 9v8a4 1.6 0 0 0 8 0V9 M3 13a4 1.6 0 0 0 8 0 M13 13a4 1.6 0 1 0 8 0a4 1.6 0 1 0-8 0Z M13 13v4a4 1.6 0 0 0 8 0v-4', ''],
125
+ magnifier: ['M15.5 10a5.5 5.5 0 1 1-11 0a5.5 5.5 0 1 1 11 0Z M14.2 14.2L20 20 M7.5 10l1.8 1.8 3.2-3.3', ''],
126
+ gate: ['M4 21V8 M20 21V8 M4 8q8-6.5 16 0 M2 21h20 M8 15h7 M12.5 12l3 3-3 3', ''],
127
+ bug: ['M8 13.5a4 5 0 1 0 8 0a4 5 0 1 0-8 0Z M12 8.5v10 M8 11.5H4 M8 15.5H4 M16 11.5h4 M16 15.5h4 M10 8l-2-3.5 M14 8l2-3.5', ''],
128
+ sprout: ['M12 21v-9 M12 12c0-4.5 3-6.5 7.5-6.5 0 4.5-3 6.5-7.5 6.5Z M12 15c0-3.5-2.5-5.5-6.5-5.5 0 3.5 2.5 5.5 6.5 5.5Z M7 21h10', ''],
129
+ calendar: ['M4 6h16v15H4Z M4 10h16 M8 4v4 M16 4v4', 'M13 15.5a2 2 0 1 0 4 0a2 2 0 1 0-4 0Z'],
130
+ frame: ['M3 5h18v14H3Z M3 16l5-5 4 4 3-3 6 6', 'M14.5 9a1.5 1.5 0 1 0 3 0a1.5 1.5 0 1 0-3 0Z'],
131
+ wrench: ['M15 4a5 5 0 0 0-4.6 6.9L4 17.3 6.7 20l6.4-6.4A5 5 0 0 0 20 9l-3 3-3-1-1-3Z', ''],
132
+ shield: ['M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6Z M8.5 12l2.5 2.5 4.5-4.5', ''],
133
+ removal: ['M6 3h9l4 4v14H6Z M15 3v4h4 M9.5 12.5l5 5 M14.5 12.5l-5 5', ''],
134
+ scales: ['M12 4v16 M7 20h10 M4 7h16 M4 7l-2.5 6h5Z M20 7l-2.5 6h5Z', ''],
135
+ badge: ['M13 20c0-3.5-2.5-6-5-6s-5 2.5-5 6 M14.5 13l2 2 4.5-4.5', 'M5 8a3 3 0 1 0 6 0a3 3 0 1 0-6 0Z'],
136
+ people: ['M2.5 20c0-3 2-5 4.5-5s4.5 2 4.5 5 M8 20c0-3.5 1.8-6 4-6s4 2.5 4 6 M12.5 20c0-3 2-5 4.5-5s4.5 2 4.5 5', 'M4.8 10a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0-4.4 0Z M9.8 8.5a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0-4.4 0Z M14.8 10a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0-4.4 0Z'],
137
+ hourglass: ['M6 3h12 M6 21h12 M7 3h10l-5 9 5 9H7l5-9Z', 'M9 6h6l-3 5Z'],
138
+ hall: ['M3 10l9-6 9 6 M5 10v10 M19 10v10 M3 20h18 M9.5 20v-6h5v6 M8 12h.01 M16 12h.01', ''],
139
+ card: ['M3 6h18v12H3Z M3 10h18 M6 15h4', ''],
140
+ modules: ['M4 4h7v7H4Z M13 4h7v7h-7Z M4 13h7v7H4Z M13 13h7v7h-7Z', ''],
141
+ coinhand: ['M3 15h3.5l4 2h5a1.8 1.8 0 0 0 0-3.6h-4 M3 20h9l8.5-4.5a1.8 1.8 0 0 0-1.8-3L15 14 M15.5 7a3.5 3.5 0 1 1-7 0a3.5 3.5 0 1 1 7 0Z', ''],
142
+ seal: ['M21 12a9 9 0 1 1-18 0a9 9 0 1 1 18 0Z M8 12.5l3 3 5-6', ''],
143
+ pause: ['M21 12a9 9 0 1 1-18 0a9 9 0 1 1 18 0Z M10 8.5v7 M14 8.5v7', ''],
144
+ update: ['M4 8.5l8-4.5 8 4.5v9L12 22l-8-4.5Z M12 9v7.5 M9 13.5l3 3 3-3', ''],
145
+ live: ['M8.5 8.5a5 5 0 0 0 0 7 M15.5 8.5a5 5 0 0 1 0 7 M5.5 5.5a9.2 9.2 0 0 0 0 13 M18.5 5.5a9.2 9.2 0 0 1 0 13', 'M10.2 12a1.8 1.8 0 1 0 3.6 0a1.8 1.8 0 1 0-3.6 0Z'],
146
+ crate: ['M4 9h16v11H4Z M3 5.5h18V9H3Z M10 13h4', ''],
147
+ chip: ['M7 7h10v10H7Z M10 10h4v4h-4Z M9.5 4v3 M14.5 4v3 M9.5 17v3 M14.5 17v3 M4 9.5h3 M4 14.5h3 M17 9.5h3 M17 14.5h3', ''],
148
+ server: ['M4 4h16v6H4Z M4 14h16v6H4Z M11 7h6 M11 17h6', 'M6.3 7a1 1 0 1 0 2 0a1 1 0 1 0-2 0Z M6.3 17a1 1 0 1 0 2 0a1 1 0 1 0-2 0Z'],
149
+ safe: ['M4 4h16v15H4Z M15 11.5a3 3 0 1 1-6 0a3 3 0 1 1 6 0Z M12 8.5v1.2 M12 13.3v1.2 M6 19v2 M18 19v2', ''],
150
+ flag: ['M5 21V3.5 M5 4h12l-2.5 4.5L17 13H5', ''],
151
+ bulb: ['M9 17h6 M10 20.5h4 M12 3a6 6 0 0 0-3.6 10.8c.6.5 1 1.3 1 2.2h5.2c0-.9.4-1.7 1-2.2A6 6 0 0 0 12 3Z', ''],
152
+ ordered: ['M10 6h10 M10 12h10 M10 18h10 M4 5l1.5-1V8 M3.5 11.2a1.3 1.3 0 0 1 2.4.6c0 .9-2.4 2-2.4 2.7h2.6 M3.6 16.5h2.2l-1.2 1.3a1.1 1.1 0 1 1-1 1.7', ''],
153
+ layers: ['M12 3l9 4.5-9 4.5-9-4.5Z M3 12l9 4.5 9-4.5 M3 16.5L12 21l9-4.5', ''],
154
+ ladder: ['M7 3v18 M17 3v18 M7 7h10 M7 11.5h10 M7 16h10', 'M10.5 2.5h3l-1.5-2Z'],
155
+ team: ['M4 20c0-3 2-5 4.5-5s4.5 2 4.5 5 M14 14.5c2.6 0 4.5 2 4.5 5 M16.5 9.5', 'M6 9.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0-5 0Z M14 8.5a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0-4.4 0Z'],
156
+ key: ['M14.5 9.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 1 1 9 0Z M13 13l7.5 7.5 M17 17l2-2 M19 19.5l1.5-1.5', ''],
157
+ power: ['M12 3v8 M7 6.3a7.5 7.5 0 1 0 10 0', ''],
158
+ gauge: ['M3.5 17a8.5 8.5 0 1 1 17 0 M12 17l4.5-6 M3.5 17h17', ''],
159
+ doc: ['M6 3h9l4 4v14H6Z M15 3v4h4 M9 11h7 M9 14.5h7 M9 18h4', ''],
160
+ };
161
+ function symbolPath(name) {
162
+ const p = SYMBOLS[name] || SYMBOLS.crate;
163
+ return { d: p[0], f: p[1] };
164
+ }
165
+
166
+ // A floor that is not recorded yet, switched off, or could not be read is
167
+ // lettered faint, like the prototype's proposed floors, and says which on the
168
+ // wall where a count would stand.
169
+ const QUIET = Object.freeze({ proposed: '', coming: 'not recorded yet', off: 'switched off', unavailable: 'not read', not_yours: 'not yours' });
170
+
171
+ // ---------- the building in section (Street.dc.html buildingView) ----------
172
+ // T GovStreet.theme(mode)
173
+ // key one of the nine civic keys
174
+ // b the building, in civicInfo()'s shape: { group, name, holds, seats,
175
+ // who, rule, how, cap, growth, floors: [{ name, line, count, status,
176
+ // lines }], record: [{ label, items }], onFile, table: [rows], annex }
177
+ // st { floor, drawer, picks, vacant, reviewHeld, evening }
178
+ // Everything the prototype's template binds, as data, plus `selFloor` and
179
+ // `selDrawer`. The geometry is the prototype's to the pixel.
180
+ function buildingView(T, key, b, st) {
181
+ st = st || {};
182
+ const vacant = !!st.vacant;
183
+ const dark = T.bg === '#000000';
184
+ const X = 130, W = 460, FH = 52, GY = 676;
185
+ const occ = b.floors.length, cap = Math.max(b.cap, occ), top = GY - cap * FH;
186
+ const firstPending = b.floors.findIndex((f) => f.status === 'pending' || f.status === 'proposed');
187
+ const pick = st.floor === null || st.floor === undefined ? null : Number(st.floor);
188
+ const selFloor = pick === null || !isFinite(pick) ? (firstPending >= 0 ? firstPending : occ - 1) : Math.max(0, Math.min(pick, occ - 1));
189
+ const M2 = dark
190
+ ? { slab: '#4a4a52', wall: '#2c2c32', room: '#121215', roomLit: '#2b2219', lamp: '#55565a', lampLit: '#ffd9a8', glow: 'rgba(255,217,168,0.10)', pane: 'rgba(150,170,255,0.22)', paneLit: 'rgba(255,214,170,0.55)', kit: 'rgba(255,255,255,0.34)', kitFill: 'rgba(255,255,255,0.05)', name: '#ffffff', line: '#c0c0c0', faint: '#8e8e93' }
191
+ : { slab: '#b9b8be', wall: '#e2e1e5', room: '#f8f7f9', roomLit: '#fdf0e2', lamp: '#a9a8ad', lampLit: '#f0a86a', glow: 'rgba(240,168,106,0.12)', pane: 'rgba(91,111,208,0.20)', paneLit: 'rgba(240,168,106,0.45)', kit: 'rgba(23,24,26,0.40)', kitFill: 'rgba(23,24,26,0.05)', name: '#17181a', line: '#55565a', faint: '#65656a' };
192
+ if (key === 'review' && st.reviewHeld) { M2.room = dark ? '#121722' : '#f1f3fb'; M2.pane = dark ? 'rgba(150,170,255,0.35)' : 'rgba(91,111,208,0.30)'; }
193
+ const shapes = [], texts = [], syms = [];
194
+ const sans = "Manrope, 'Segoe UI', system-ui, sans-serif", mono = "'JetBrains Mono', ui-monospace, Consolas, monospace";
195
+ const txt = (x, y, v, c, s, w, a, f, ls) => texts.push({ key: 't' + texts.length, x: Math.round(x), y: Math.round(y), v: v, c: c, s: s, w: w || 400, a: a || 'start', f: f || sans, ls: ls || 'normal' });
196
+ const cut = (s, n) => s.length > n ? s.slice(0, n - 1) + '…' : s;
197
+ const ghost = vacant, dash = ghost ? '4 4' : 'none';
198
+ const fillOr = (c) => ghost ? 'none' : c, line = ghost ? T.ghost.s : 'none';
199
+ push(shapes, 'M0 ' + GY + 'H800', 'none', T.line);
200
+ push(shapes, rect(X - 10, GY, W + 20, 8), fillOr(M2.slab), line, dash);
201
+ if (!ghost) {
202
+ push(shapes, 'M' + (X + W) + ' ' + top + 'L' + (X + W + 16) + ' ' + (top - 16) + 'L' + (X + W + 16) + ' ' + (GY - 16) + 'L' + (X + W) + ' ' + GY + 'Z', T.civic.r, T.civic.s);
203
+ crown(shapes, T, key, X, W, top);
204
+ }
205
+ for (let i = 0; i < cap; i++) {
206
+ const y0 = GY - (i + 1) * FH, y1 = y0 + FH, fl = b.floors[i];
207
+ const used = !!fl, sel = used && i === selFloor;
208
+ const pend = used && fl.status === 'pending';
209
+ const prop = used && (fl.status === 'proposed' || QUIET[fl.status] !== undefined);
210
+ const lit = sel || (used && st.evening);
211
+ push(shapes, rect(X - 6, y1 - 5, W + 12, 5), fillOr(M2.slab), line, dash);
212
+ push(shapes, rect(X + 9, y0, W - 18, FH - 5), fillOr(lit ? M2.roomLit : M2.room), line, dash);
213
+ [X, X + W - 9].forEach((wx) => {
214
+ push(shapes, rect(wx, y0, 9, FH - 5), fillOr(M2.wall), line, dash);
215
+ if (!ghost) push(shapes, rect(wx + 3, y0 + 9, 3, FH - 23), lit ? M2.paneLit : M2.pane, 'none');
216
+ });
217
+ if (i === 0 && !ghost) push(shapes, rect(X, y1 - 33, 9, 28), lit ? M2.roomLit : M2.room, 'none');
218
+ txt(X - 50, y0 + 30, String(i + 1).padStart(2, '0'), sel ? T.accentText : M2.faint, 11, 400, 'end', mono, '0.08em');
219
+ if (!used) {
220
+ if (!ghost) txt(X + 24, y0 + 30, 'empty floor', M2.faint, 12.5, 400);
221
+ continue;
222
+ }
223
+ const px = Math.round(X + W * 0.62);
224
+ push(shapes, 'M' + px + ' ' + y0 + 'V' + (y1 - 25), 'none', ghost ? T.ghost.s : M2.kit, dash);
225
+ [0.2, 0.45, 0.8].forEach((f) => {
226
+ const lx = Math.round(X + W * f);
227
+ push(shapes, 'M' + lx + ' ' + y0 + 'V' + (y0 + 7), 'none', ghost ? T.ghost.s : M2.kit);
228
+ push(shapes, ellipse(lx, y0 + 9, 2.4, 2.4), lit ? M2.lampLit : M2.lamp, 'none');
229
+ if (lit) push(shapes, ellipse(lx, y0 + 12, 12, 7), M2.glow, 'none');
230
+ });
231
+ // the floor's sign: a plate on the wall carrying the one symbol that says what this floor is
232
+ const sx0 = px + 16, sy0 = y0 + 5;
233
+ push(shapes, rect(sx0 - 4, sy0 - 1, 44, 40), ghost ? 'none' : M2.wall, ghost ? T.ghost.s : M2.kit, dash);
234
+ const sp = symbolPath(floorSymbol(fl.name));
235
+ syms.push({ key: 's' + i, x: sx0 + 2, y: sy0 + 2, k: 1.333, d: sp.d, f: sp.f, c: sel ? '#fb9569' : pend ? T.accentText : (ghost || prop ? M2.faint : M2.line), w: sel ? 1.9 : 1.5 });
236
+ if (pend) push(shapes, rect(X + 11, y0 + 2, W - 22, FH - 9), 'none', '#fb9569', '4 4');
237
+ txt(X + 24, y0 + 24, cut(fl.name, 34), ghost || prop ? M2.faint : M2.name, 14.5, 500);
238
+ txt(X + 24, y0 + 40, cut(fl.line, 44), pend ? T.accentText : M2.line, 12, 400);
239
+ if (fl.count) txt(X + W - 22, y0 + 34, fl.count, M2.name, 20, 300, 'end', sans, '-0.02em');
240
+ else if (pend) txt(X + W - 22, y0 + 32, 'being decided', T.accentText, 11.5, 500, 'end');
241
+ else if (QUIET[fl.status]) txt(X + W - 22, y0 + 32, QUIET[fl.status], M2.faint, 11.5, 400, 'end');
242
+ }
243
+ // the chosen floor gets a frame, and a lift runs in its shaft beside the building to meet it
244
+ if (b.floors[selFloor]) push(shapes, rect(X - 3, GY - (selFloor + 1) * FH - 2, W + 6, FH - 1), 'none', '#fb9569', 'none', 1.5);
245
+ push(shapes, rect(X - 40, top, 22, cap * FH), 'none', ghost ? T.ghost.s : M2.kit, ghost ? '4 4' : 'none');
246
+ let rungs = '';
247
+ for (let i = 1; i < cap; i++) rungs += 'M' + (X - 40) + ' ' + (GY - i * FH) + 'h22';
248
+ push(shapes, rungs, 'none', M2.kit, '2 3');
249
+ const floors = b.floors.map((f, i) => ({ key: f.name, index: i, x: X, y: GY - (i + 1) * FH, w: W, h: FH - 5, aria: 'Open ' + f.name, selected: i === selFloor }));
250
+ const record = Array.isArray(b.record) ? b.record : [];
251
+ const selDrawer = record.length ? Math.max(0, Math.min(st.drawer === null || st.drawer === undefined ? 0 : Number(st.drawer) || 0, record.length - 1)) : -1;
252
+ const seatOf = (s) => s === 'you'
253
+ ? { key: s, init: 'you', bg: 'transparent', fg: T.accentText, border: '2px solid #fb9569' }
254
+ : s === '—' ? { key: s, init: '', bg: 'transparent', fg: T.faint, border: '1px dashed ' + T.seatOpen }
255
+ : { key: s, init: s, bg: T.seatFor.bg, fg: T.seatFor.fg, border: 'none' };
256
+ const g = City.glyph(b.rule, 9, 9, 16);
257
+ const pct = Math.round(occ / cap * 100);
258
+ const table = Array.isArray(b.table) ? b.table : [];
259
+ const f = b.floors[selFloor];
260
+ const quietWord = f && QUIET[f.status] !== undefined ? ' · ' + (f.status === 'proposed' ? 'proposed' : QUIET[f.status]) : '';
261
+ return {
262
+ kicker: b.group + ' · ' + b.name.toLowerCase(), name: b.name, holds: b.holds,
263
+ seats: b.seats.map(seatOf), whoText: b.who, gf: g.fill, gl: g.line, howText: b.how,
264
+ fill: pct, fillColor: pct >= 80 ? '#fb9569' : T.line,
265
+ growth: (vacant ? 'Not built yet. ' : occ + ' of ' + cap + ' floors in use. ') + b.growth,
266
+ hasTable: table.length > 0, table: table,
267
+ openKicker: 'floor ' + (selFloor + 1) + (f && f.status === 'pending' ? ' · being decided' : quietWord),
268
+ sym: f ? Object.assign(symbolPath(floorSymbol(f.name)), { c: '#fb9569' }) : { d: '', f: '', c: 'none' },
269
+ flipA: ((st.picks || 0) % 2) === 0, flipB: ((st.picks || 0) % 2) === 1,
270
+ openTitle: f ? f.name : '', openLines: f ? f.lines.map((l, i) => ({ key: 'l' + i, text: l[0], meta: l[1] })) : [],
271
+ drawers: record.map((r, i) => ({ key: r.label, index: i, label: r.label, count: r.items.length, aria: 'Open the ' + r.label + ' drawer', selected: i === selDrawer, bg: i === selDrawer ? T.wash : 'transparent', border: i === selDrawer ? T.ink : T.ruleStrong, handle: i === selDrawer ? T.ink : T.ruleStrong, color: T.ink })),
272
+ drawerItems: selDrawer >= 0 ? record[selDrawer].items.map((it, i) => ({ key: 'r' + i, text: it[0], meta: it[1] })) : [],
273
+ onFile: b.onFile, shapes: shapes, texts: texts, syms: syms, floors: floors,
274
+ lift: { x: X - 38, y: GY - FH + 6, ty: -(selFloor * FH), h: FH - 16 },
275
+ hasAnnex: !!b.annex, annex: b.annex ? { x: 628, y: GY - 156, w: 160, h: 156, text: b.annex } : { x: 0, y: 0, w: 0, h: 0, text: '' },
276
+ selFloor: selFloor, selDrawer: selDrawer, cap: cap, occ: occ,
277
+ };
278
+ }
279
+
280
+ // ---------- inside a district (Street.dc.html districtView) ----------
281
+ // d { name, line, items: [[name, kind, craft, stage, h, label?]] }
282
+ // `stage` is the drawing stage in the prototype's words ('backlog', 'ready',
283
+ // 'being built', 'shipped', or a check it waits on); `label`, when given, is
284
+ // the pill's words for a status the prototype did not name.
285
+ function districtView(T, key, d) {
286
+ const shapes = [];
287
+ const iw = 40, gap = 46, s = 1.6, GY = 520;
288
+ let x = 0;
289
+ const placed = d.items.map((it) => {
290
+ workItem(shapes, T, it[2], it[3], x, iw, it[4]);
291
+ const p = { lx: x, name: it[0], kind: it[1], craft: it[2], stage: it[5] || it[3], draw: it[3], href: it[6] || null };
292
+ x += iw + gap;
293
+ return p;
294
+ });
295
+ const tx = Math.round((1440 - (x - gap) * s) / 2);
296
+ return {
297
+ kicker: 'the work · ' + key, name: d.name, line: d.line,
298
+ road: rect(32, GY, 1376, 26), ground: 'M32 ' + GY + 'H1408', tx: tx, ty: GY, s: s, shapes: shapes,
299
+ items: placed.map((p) => ({
300
+ key: p.name, x: Math.round(tx + p.lx * s + iw * s / 2 - 62), y: GY + 36, w: 124, name: p.name, meta: p.kind + ' · ' + p.craft, stage: p.stage, href: p.href,
301
+ stageColor: p.draw === 'awaiting a tester' ? T.accentText : p.draw === 'shipped' ? T.ink : T.soft,
302
+ stageEdge: p.draw === 'awaiting a tester' ? 'rgba(251,149,105,0.6)' : T.ruleStrong,
303
+ })),
304
+ legend: [
305
+ { key: 'a', text: 'Shape is the craft: tower for engineer, studio roof for artist, glass front for UI, a small pavilion for ideator.' },
306
+ { key: 'b', text: 'Stage is the status: staked plot, slab, scaffold, built, then lit once shipped. A flag means it waits on a check.' },
307
+ ],
308
+ };
309
+ }
310
+
311
+ // ---------- the real buildings (see the header) ----------
312
+
313
+ // The prototype's words for each building that are about the government, not
314
+ // its sample project: what it holds, the rule the design gives it, its
315
+ // capacity, its growth line and its annex.
316
+ const GROUPS = Object.freeze({ charter: 'how we decide', rules: 'how we decide', disputes: 'how we decide', people: 'who and what', planning: 'who and what', automation: 'the work', review: 'checks and going live', release: 'checks and going live', treasury: 'money' });
317
+ const RULE_WORDS = Object.freeze({ decide: 'one person decides', first: 'first yes wins', consent: 'passes unless someone objects', majority: 'more than half must say yes', unanimous: 'everyone has to agree', signoff: 'signed off by someone who did not build it', rule: 'settled by a rule' });
318
+
319
+ // A floor: [name, what it holds, binding]. A binding is { source }, { reading }
320
+ // or { goals: true }; no binding is a floor not recorded yet.
321
+ const PLAN = Object.freeze({
322
+ charter: {
323
+ holds: 'The form of government. Seven sections say who holds each building and how it decides.', rule: 'unanimous', cap: 9,
324
+ growth: 'Room for more sections before the charter needs a second volume.', annex: 'room for a second volume',
325
+ floors: [['Founding', 'the starting government'], ['§1 Purpose', 'what this project is for'], ['§2 Membership', 'who belongs, and how they join'], ['§3 The council', 'who sits on the council'], ['§4 Rules', 'how a rule passes'], ['§5 Money', 'who may spend, and how much'], ['§6 Review', 'who signs off finished work'], ['§7 Release', 'who puts work live']],
326
+ },
327
+ rules: {
328
+ holds: 'Standing rules. Each one settles a kind of matter so the council never votes on it again.', rule: 'majority', cap: 8,
329
+ growth: 'When the hall fills, the council can open an annex for one area, such as money rules.', annex: 'room for an annex, such as money rules',
330
+ floors: [['Rules in force', 'the rules settling matters now', { reading: 'rules' }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
331
+ },
332
+ disputes: {
333
+ holds: 'Grade appeals, removal requests and security reports. Every ruling becomes precedent for the next one.', rule: 'decide', cap: 5,
334
+ growth: 'Busy weeks go to a panel of three once a second room is built.', annex: 'room for a second room, a panel of three',
335
+ floors: [['Security reports', 'flaws found in our own walls', { source: 'redteam_reports' }], ['Removals', 'work proposed for removal', { source: 'removal_proposals' }], ['Grade appeals', 'a grade someone asks to change', { source: 'grade_appeals' }]],
336
+ },
337
+ people: {
338
+ holds: 'Who belongs, the rank each person holds, and who sits in which seat.', rule: 'consent', cap: 8,
339
+ growth: 'A new floor opens whenever the charter creates a new seat.', annex: 'room for an annex of seats',
340
+ floors: [['The council', 'who sits, and for how long'], ['Testers', 'who signs off finished work'], ['Builders', 'who builds here', { reading: 'people' }], ['Newcomers', 'builders on newcomer tasks'], ['Waiting to join', 'asked to join the project', { source: 'access_requests' }], ['Ranks', 'the ranks, and who holds each'], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
341
+ },
342
+ planning: {
343
+ holds: 'Goals, and the promises that say when each one is done. Kept promises are what the project owes its users.', rule: 'majority', cap: 9,
344
+ growth: 'When it fills, the next new goal opens a second planning office.', annex: 'room for a second planning office',
345
+ floors: [['Goals', 'the open goals', { goals: true }], ['Promises', 'what the goals say they deliver', { reading: 'promises' }], ['Ideas to ratify', 'full ideas waiting for a vote'], ['Priorities', 'the order goals are worked in'], ['Versions', 'what each version holds'], ['Waiting on a decision', 'blockers holding work', { source: 'blockers' }]],
346
+ },
347
+ automation: {
348
+ holds: 'AI builders working on their own. You choose which goals they may build and how much they may spend, and you can stop them at any time.', rule: 'decide', cap: 6,
349
+ growth: 'A new floor opens for each goal you let it build.', annex: 'room for an annex',
350
+ floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence' }], ['Allowed goals', 'the goals it may build'], ['Running now', 'builds in progress'], ['Spending limit', 'what it may spend a day'], ['Reports', 'what it did, told without asking']],
351
+ },
352
+ review: {
353
+ holds: 'Testing finished work and signing it off. Work does not count until someone who did not build it says it works.', rule: 'signoff', cap: 5,
354
+ growth: 'When one queue of testers is not enough, an annex can take design reviews.', vacantGrowth: 'Nothing is built here until the charter says who holds the seat.', annex: 'room for an annex, such as design review',
355
+ floors: [['Testers', 'who signs off finished work'], ['Waiting outside', 'finished work waiting for a tester', { source: 'criteria_uat' }], ['Signed off', 'work a tester has signed off']],
356
+ },
357
+ release: {
358
+ holds: 'Putting finished work live, and taking updates to Bongos itself.', rule: 'decide', cap: 7,
359
+ growth: 'Room for a second gate if releases are ever split by area.', annex: 'room for a second gate',
360
+ floors: [['Overrides', 'past a failed check or grade', { source: 'override_requests' }], ['Modules', 'listed modules, and their re-checks', { source: 'module_rechecks' }], ['Held back', 'work held from going live'], ['Core update', 'a new version of Bongos', { source: 'core_update' }], ['Live this month', 'what went live'], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
361
+ },
362
+ treasury: {
363
+ holds: 'The budget, and who gets paid for what.', rule: 'majority', cap: 7,
364
+ growth: 'Each new spending line takes a floor.', annex: 'room for an annex of spending lines',
365
+ floors: [['Builder rewards', 'paid for shipped work'], ['AI grading', 'the reviewer that grades every ship'], ['Hosting', 'servers and storage'], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
366
+ },
367
+ });
368
+ const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
369
+ const STATE_WORDS = Object.freeze({ needs: 'needs you', moving: 'moving', settled: 'settled' });
370
+ const MAX_LINES = 4;
371
+
372
+ function stateOf(m) { return m && (m.state === 'needs' || m.state === 'moving' || m.state === 'settled') ? m.state : 'moving'; }
373
+ function matterMeta(m, now) {
374
+ const when = S.whenWord(m.deadline, now);
375
+ return STATE_WORDS[stateOf(m)] + (when ? ' · ' + when : '');
376
+ }
377
+ function countLine(ms) {
378
+ const n = { needs: 0, moving: 0, settled: 0 };
379
+ ms.forEach((m) => { n[stateOf(m)] += 1; });
380
+ if (n.needs) return n.needs + (n.needs === 1 ? ' needs you' : ' need you');
381
+ if (n.moving) return n.moving + ' moving';
382
+ if (n.settled) return n.settled + ' settled';
383
+ return 'nothing open';
384
+ }
385
+ // The short figure a reading stands on its floor: '12 builders' → '12'.
386
+ function shortFigure(figure) {
387
+ const m = /^[$]?[0-9][0-9,.]*/.exec(String(figure || ''));
388
+ return m ? m[0] : '';
389
+ }
390
+ function F(name, line, count, status, lines) { return { name: name, line: line, count: count, status: status || 'ok', lines: lines || [] }; }
391
+
392
+ // One floor of the PLAN, read against the docket. A floor nothing reads yet
393
+ // keeps its name and what it holds, and says it is not recorded.
394
+ function floorFor(spec, ctx) {
395
+ const name = spec[0], what = spec[1], bind = spec[2] || null;
396
+ const docket = ctx.docket, now = ctx.now;
397
+ if (!bind) return F(name, what, '', 'coming', [['What this floor holds is not recorded in Bongos yet.', 'coming']]);
398
+ if (!docket) return F(name, what, '', 'unavailable', [['The docket could not be read just now.', '']]);
399
+ if (bind.goals) {
400
+ const goals = ctx.goals || [];
401
+ const lines = goals.slice(0, MAX_LINES).map((g) => [String(g.title || 'Goal ' + g.id), 'open']);
402
+ if (goals.length > MAX_LINES) lines.push(['and ' + (goals.length - MAX_LINES) + ' more on the goals page', '']);
403
+ return F(name, goals.length ? goals.length + (goals.length === 1 ? ' goal open' : ' goals open') : 'no goal open', String(goals.length), 'ok', lines.length ? lines : [['No goal is open.', '']]);
404
+ }
405
+ if (bind.reading) {
406
+ const r = (Array.isArray(docket.readings) ? docket.readings : []).find((x) => x && x.key === bind.reading);
407
+ if (!r) return F(name, what, '', 'unavailable', [['This reading could not be read just now.', '']]);
408
+ if (r.status === 'live') return F(name, String(r.line || what), shortFigure(r.figure), r.hot ? 'pending' : 'ok', [[String(r.line || what), String(r.figure || '')]]);
409
+ return quietFloor(name, what, r.status, r.reason);
410
+ }
411
+ const src = (Array.isArray(docket.sources) ? docket.sources : []).find((x) => x && x.source === bind.source);
412
+ if (!src) return F(name, what, '', 'unavailable', [['This could not be read just now.', '']]);
413
+ if (src.status !== 'live') return quietFloor(name, what, src.status, src.reason);
414
+ const ms = (docket.matters || []).filter((m) => m && m.source === bind.source && (!ctx.key || m.building === ctx.key));
415
+ const lines = ms.slice(0, MAX_LINES).map((m) => [String(m.title || m.short || ''), matterMeta(m, now)]);
416
+ if (ms.length > MAX_LINES) lines.push(['and ' + (ms.length - MAX_LINES) + ' more', '']);
417
+ return F(name, countLine(ms), String(ms.length), ms.some((m) => stateOf(m) === 'needs') ? 'pending' : 'ok', lines.length ? lines : [['Nothing open here.', '']]);
418
+ }
419
+ function quietFloor(name, what, status, reason) {
420
+ if (status === 'not_yours') return F(name, what, '', 'not_yours', [['This is not yours to read here.', '']]);
421
+ if (status === 'off') return F(name, what, '', 'off', [[String(reason || 'The module that holds this is not enabled here.'), 'off']]);
422
+ if (status === 'coming') return F(name, what, '', 'coming', [[String(reason || 'This is not recorded in Bongos yet.'), 'coming']]);
423
+ return F(name, what, '', 'unavailable', [[String(reason || 'It could not be read just now.'), '']]);
424
+ }
425
+
426
+ // Who holds the building and how it decides. A docket building that names
427
+ // its holders (task 1004529) is read first; otherwise the seat is empty and
428
+ // says so, and the rule is its matters' own, else the design's default (and
429
+ // the words say 'by default', so a default is never read as a decision).
430
+ function holderOf(key, info, matters) {
431
+ const plan = PLAN[key];
432
+ const holders = info && Array.isArray(info.holders) ? info.holders.filter((h) => h && (h.name || h.login)) : [];
433
+ const seats = holders.length ? holders.slice(0, 5).map((h) => String(h.name || h.login).charAt(0).toUpperCase()) : ['—'];
434
+ const who = info && info.who ? String(info.who) : holders.length ? holders.map((h) => String(h.name || h.login)).join(', ') : 'who holds it is not set on this project yet';
435
+ const fromMatter = matters.find((m) => m && RULE_WORDS[m.rule]);
436
+ const rule = info && RULE_WORDS[info.rule] ? info.rule : fromMatter ? fromMatter.rule : plan.rule;
437
+ const how = RULE_WORDS[rule] + (info && RULE_WORDS[info.rule] ? '' : fromMatter ? '' : ' · by default');
438
+ return { seats: seats, who: who, rule: rule, how: how };
439
+ }
440
+
441
+ // The record: the building's matters filed by the month each was opened.
442
+ function recordOf(matters, now) {
443
+ const by = new Map();
444
+ matters.forEach((m) => {
445
+ const t = m.opened_at ? new Date(m.opened_at) : null;
446
+ const ok = t && !isNaN(t.getTime());
447
+ const k = ok ? t.getUTCFullYear() * 12 + t.getUTCMonth() : -1;
448
+ if (!by.has(k)) by.set(k, []);
449
+ by.get(k).push([String(m.title || m.short || ''), STATE_WORDS[stateOf(m)]]);
450
+ });
451
+ const thisYear = new Date(now || 0).getUTCFullYear();
452
+ return [...by.keys()].sort((a, b) => b - a).map((k) => ({
453
+ label: k < 0 ? 'undated' : MONTHS[k % 12] + (Math.floor(k / 12) === thisYear ? '' : ' ' + String(Math.floor(k / 12)).slice(2)),
454
+ items: by.get(k),
455
+ }));
456
+ }
457
+
458
+ // The whole building as civicInfo() shaped it, from the real feeds.
459
+ // ctx { docket, goals (open), now }
460
+ function civicInfo(key, ctx) {
461
+ const plan = PLAN[key];
462
+ if (!plan) return null;
463
+ const docket = ctx && ctx.docket && Array.isArray(ctx.docket.matters) ? ctx.docket : null;
464
+ const c = { key: key, docket: docket, goals: (ctx && ctx.goals) || [], now: (ctx && ctx.now) || 0 };
465
+ const matters = docket ? docket.matters.filter((m) => m && m.building === key) : [];
466
+ const info = docket && Array.isArray(docket.buildings) ? docket.buildings.find((b) => b && b.key === key) : null;
467
+ const h = holderOf(key, info, matters);
468
+ const vacant = key === 'review' && !S.reviewHeldOf(docket);
469
+ const floors = plan.floors.map((spec) => floorFor(spec, c));
470
+ const order = { needs: 0, moving: 1, settled: 2 };
471
+ const table = matters.slice().sort((a, b) => order[stateOf(a)] - order[stateOf(b)]).map((m) => {
472
+ const s = stateOf(m), when = S.whenWord(m.deadline, c.now);
473
+ return { key: String(m.id), matter: String(m.id), label: String(m.title || m.short || ''), status: s === 'needs' ? 'needs you' + (when ? ' · ' + when : '') : s === 'moving' ? 'moving' + (when ? ' · ' + when : '') : (when || 'settled'), state: s, color: s === 'needs' ? 'accent' : 'ink' };
474
+ });
475
+ const record = recordOf(matters, c.now);
476
+ return {
477
+ key: key, group: GROUPS[key], name: info && info.name ? String(info.name) : key.charAt(0).toUpperCase() + key.slice(1), holds: plan.holds,
478
+ seats: h.seats, who: h.who, rule: h.rule, how: h.how, cap: plan.cap,
479
+ growth: vacant ? plan.vacantGrowth : plan.growth,
480
+ floors: floors, record: record,
481
+ onFile: !docket ? 'the docket could not be read just now' : matters.length ? matters.length + (matters.length === 1 ? ' matter' : ' matters') + ' on the docket' : 'nothing on file yet',
482
+ table: table, annex: !vacant && floors.length >= plan.cap ? plan.annex : null, vacant: vacant,
483
+ };
484
+ }
485
+
486
+ // The building interior as data: civicInfo() drawn by buildingView().
487
+ // input { key, docket, goals, mode, now, floor, drawer, picks }
488
+ function buildingModel(input) {
489
+ const o = input || {};
490
+ if (S.CIVIC_KEYS.indexOf(o.key) < 0) return null;
491
+ const mode = o.mode === 'light' ? 'light' : 'dark';
492
+ const T = S.theme(mode);
493
+ const goals = (Array.isArray(o.goals) ? o.goals : []).filter((g) => g && g.status === 'open');
494
+ const b = civicInfo(o.key, { docket: o.docket, goals: goals, now: o.now });
495
+ const docket = o.docket && Array.isArray(o.docket.matters) ? o.docket : null;
496
+ const evening = !!docket && !(docket.counts && Number(docket.counts.needs) > 0);
497
+ const bv = buildingView(T, o.key, b, { floor: o.floor, drawer: o.drawer, picks: o.picks, vacant: b.vacant, reviewHeld: !b.vacant, evening: evening });
498
+ bv.table = bv.table.map((r) => Object.assign({}, r, { color: r.color === 'accent' ? T.accentText : T.ink }));
499
+ bv.key = o.key; bv.mode = mode; bv.vacant = b.vacant;
500
+ bv.label = b.name + ' in section: ' + b.floors.length + ' of ' + bv.cap + ' floors in use' + (bv.hasTable ? ', ' + bv.table.length + ' on the table' : '') + '.';
501
+ return bv;
502
+ }
503
+
504
+ // ---------- the real districts ----------
505
+ const CRAFTS = Object.freeze(['engineer', 'artist', 'ui', 'ideator']);
506
+ // A task's status as the prototype's construction stage: [drawing stage, pill words].
507
+ const STAGES = Object.freeze({
508
+ shipped: ['shipped', 'shipped'], confirmed: ['graded · landing', 'graded · landing'], completed: ['being graded', 'being graded'],
509
+ active: ['being built', 'being built'], ready: ['ready', 'ready'], blocked: ['backlog', 'blocked'], backlog: ['backlog', 'backlog'],
510
+ });
511
+ const PIPELINE = Object.freeze(['shipped', 'confirmed', 'completed', 'active', 'ready', 'blocked', 'backlog']);
512
+ const PRIORITY = Object.freeze(['active', 'completed', 'confirmed', 'ready', 'blocked', 'shipped', 'backlog']);
513
+ const MAX_ITEMS = 10;
514
+
515
+ function taskHeight(t) {
516
+ const est = Number(t && (t.est_minutes_calibrated || t.est_minutes)) || 60;
517
+ return Math.max(40, Math.min(120, 36 + Math.round(est / 6)));
518
+ }
519
+
520
+ // A goal's work as districtInfo() shaped it.
521
+ // goal { id, title } (or null when it is not one of the open goals)
522
+ // tasks GET /tasks?goal_id=N tasks[], null while reading, false when it failed
523
+ function districtInfo(goalId, goal, tasks) {
524
+ const id = String(goalId);
525
+ const name = goal && goal.title ? String(goal.title) : 'Goal ' + id;
526
+ if (tasks === null || tasks === undefined) return { name: name, line: 'reading the goal’s work', items: [], more: 0, total: 0 };
527
+ if (tasks === false) return { name: name, line: 'the goal’s work could not be read just now', items: [], more: 0, total: 0 };
528
+ const mine = tasks.filter((t) => t && String(t.goal_id) === id && STAGES[t.status]);
529
+ const rank = (t) => PRIORITY.indexOf(t.status);
530
+ const recent = (t) => -(Date.parse(t.shipped_at || t.updated_at || t.created_at || '') || 0);
531
+ const shown = mine.slice().sort((a, b) => rank(a) - rank(b) || recent(a) - recent(b) || Number(a.id) - Number(b.id)).slice(0, MAX_ITEMS)
532
+ .sort((a, b) => PIPELINE.indexOf(a.status) - PIPELINE.indexOf(b.status) || Number(a.id) - Number(b.id));
533
+ const items = shown.map((t) => {
534
+ const craft = CRAFTS.indexOf(t.discipline) >= 0 ? t.discipline : 'engineer';
535
+ const st = STAGES[t.status];
536
+ const built = ['ready', 'backlog'].indexOf(st[0]) < 0;
537
+ return [String(t.title || 'task ' + t.id), String(t.kind || 'task'), craft, st[0], built ? taskHeight(t) : 0, st[1], '/builders/task/' + encodeURIComponent(t.id)];
538
+ });
539
+ const n = (s) => mine.filter((t) => t.status === s).length;
540
+ const parts = [[n('shipped'), 'shipped'], [n('active'), 'being built'], [n('completed') + n('confirmed'), 'waiting on a check'], [n('ready') + n('blocked') + n('backlog'), 'not started']].filter((p) => p[0] > 0).map((p) => p[0] + ' ' + p[1]);
541
+ return { name: name, line: parts.length ? 'the goal’s work · ' + parts.join(' · ') : 'no work in this goal yet', items: items, more: mine.length - items.length, total: mine.length };
542
+ }
543
+
544
+ // input { goalId, goals, tasks, mode }
545
+ function districtModel(input) {
546
+ const o = input || {};
547
+ const mode = o.mode === 'light' ? 'light' : 'dark';
548
+ const T = S.theme(mode);
549
+ const goal = (Array.isArray(o.goals) ? o.goals : []).find((g) => g && String(g.id) === String(o.goalId)) || null;
550
+ const d = districtInfo(o.goalId, goal, o.tasks);
551
+ const kicker = String(d.name).toLowerCase();
552
+ const dv = districtView(T, kicker.length > 40 ? kicker.slice(0, 39) + '…' : kicker, d);
553
+ dv.goalId = String(o.goalId); dv.mode = mode; dv.open = !!goal && goal.status === 'open';
554
+ dv.more = d.more; dv.total = d.total;
555
+ dv.goalHref = '/builders/goals#/goal/' + encodeURIComponent(o.goalId);
556
+ dv.label = d.name + ': ' + d.items.length + ' pieces of work on the road' + (d.more ? ', ' + d.more + ' more on the goal page' : '') + '.';
557
+ return dv;
558
+ }
559
+
560
+ const api = {
561
+ crown: crown, workItem: workItem, floorSymbol: floorSymbol, symbolPath: symbolPath, buildingView: buildingView, districtView: districtView,
562
+ civicInfo: civicInfo, floorFor: floorFor, holderOf: holderOf, recordOf: recordOf, buildingModel: buildingModel,
563
+ districtInfo: districtInfo, districtModel: districtModel,
564
+ PLAN: PLAN, SYMBOL_NAMES: Object.freeze(Object.keys(SYMBOLS)), RULE_WORDS: RULE_WORDS, MAX_ITEMS: MAX_ITEMS, QUIET: QUIET,
565
+ };
566
+ root.GovInside = api;
567
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
568
+ }(typeof window !== 'undefined' ? window : globalThis));