@bongos/core 1.21.33 → 1.21.34
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 +35 -25
- package/docs/copy-inventory.md +19 -17
- package/docs/copy-registry.json +39 -21
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -1
- package/docs/page-readings.json +267 -198
- package/modules/hall-ui/public/street-desk.js +285 -0
- package/modules/hall-ui/public/street-inside.js +8 -4
- package/modules/hall-ui/public/street.css +59 -0
- package/modules/hall-ui/public/street.html +9 -3
- package/modules/hall-ui/public/street.js +219 -17
- package/modules/hall-ui/public/street.states.json +83 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/hall_matter_card.mjs +1 -1
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_desk.mjs +328 -0
- package/tests/hall_street_inside.mjs +1 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// modules/hall-ui/public/street-desk.js — THE FLOOR DESK AND THE LINE RULE
|
|
2
|
+
// (task 1004558, goal 1000125; round 2 of the Governor City).
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS. The desk from the reference board "Round 2 · floor desks"
|
|
5
|
+
// (docs/design/mocks/governor-city/Desks.dc.html, its Component class): the
|
|
6
|
+
// chosen floor of a building, as one panel you act from in place. A header
|
|
7
|
+
// (the floor's sign, its name, its status), up to three tiles, the to-do rows
|
|
8
|
+
// (a name, one fact, at most two buttons; tap the name for at most three facts
|
|
9
|
+
// in place; reasons as one-tap chips), the quiet state when nothing needs you,
|
|
10
|
+
// and the record line. The game layer around it (the today meter, rows
|
|
11
|
+
// clearing away, a cleared floor lighting up, the toast) is drawn by
|
|
12
|
+
// street.js; this file is pure.
|
|
13
|
+
//
|
|
14
|
+
// THE LINE RULE (Ground.dc.html, "the line rule and ground level"):
|
|
15
|
+
// a ROW is a name of at most 3 words, one fact, at most two buttons, and
|
|
16
|
+
// at most three facts that open in place;
|
|
17
|
+
// a TILE is one number and two words, and is hidden when it reads zero;
|
|
18
|
+
// a STATUS is count, noun and oldest age, orange only when it needs you;
|
|
19
|
+
// a QUIET floor shows one line and lights on, never an empty list;
|
|
20
|
+
// REASONS and options are one-tap chips, never free text.
|
|
21
|
+
// fit() is the ONE line formatter every desk string goes through, and breaks()
|
|
22
|
+
// is the rule as a checker: tests/hall_street_desk.mjs runs every desk the
|
|
23
|
+
// docket can produce through it.
|
|
24
|
+
//
|
|
25
|
+
// THE ACTS. A row's buttons are the matter card's own actions
|
|
26
|
+
// (GovMatterCard.model, street-card.js) under the board's shorter words, and an
|
|
27
|
+
// act is sent through GovMatterCard.request(): the same existing routes the
|
|
28
|
+
// card uses, with their own permission checks. Nothing here invents a route.
|
|
29
|
+
// Two of the board's touches have no route under them and are left out rather
|
|
30
|
+
// than faked: a reason for "not now" on a join request (the route takes none),
|
|
31
|
+
// and the abstain text action (it records nothing; a row has two buttons).
|
|
32
|
+
//
|
|
33
|
+
// API (what the next desks plug into):
|
|
34
|
+
// GovDesk.deskModel(input) → desk pure; input below
|
|
35
|
+
// GovDesk.rowsFor(key, floor, docket, ctx) the rows of one floor
|
|
36
|
+
// GovDesk.floorMatters(key, floor, docket) which open matters sit on a floor
|
|
37
|
+
// GovDesk.firstNeeds(key, docket) the floor that needs you first, or null
|
|
38
|
+
// GovDesk.fit(s, n, words) the line formatter
|
|
39
|
+
// GovDesk.breaks(desk) [] when the desk keeps the line rule
|
|
40
|
+
// GovDesk.todayPips(n) the street header's meter
|
|
41
|
+
// GovDesk.LIMITS, ACT_WORDS, CHIPS, NOUNS, CLEAR
|
|
42
|
+
// A desk row is { key, matter, ini, name, fact, facts[], actions[{ key, label,
|
|
43
|
+
// tone, go, ask }], chips { for, ask, options[{ key, label, input }] } | null,
|
|
44
|
+
// wait, needs, card }: `card` is the GovMatterCard model the act is sent with.
|
|
45
|
+
|
|
46
|
+
(function (root) {
|
|
47
|
+
'use strict';
|
|
48
|
+
const S = root.GovStreet || (typeof require === 'function' ? require('./street-draw.js') : null);
|
|
49
|
+
const I = root.GovInside || (typeof require === 'function' ? require('./street-inside.js') : null);
|
|
50
|
+
const C = root.GovMatterCard || (typeof require === 'function' ? require('./street-card.js') : null);
|
|
51
|
+
|
|
52
|
+
// Every limit of the line rule, in characters unless named otherwise.
|
|
53
|
+
const LIMITS = Object.freeze({
|
|
54
|
+
nameWords: 3, name: 20, fact: 26, facts: 3, factLen: 24, actions: 2,
|
|
55
|
+
tiles: 3, tileValue: 8, tileWords: 2, tileLabel: 14,
|
|
56
|
+
status: 44, clearLine: 30, clearSub: 40, record: 4, recordLen: 30,
|
|
57
|
+
chips: 5, chip: 16, todayTarget: 10,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// One line, never more: the house rule for every row, tile and status. A cap
|
|
61
|
+
// on words first (when given), then on characters; anything cut says so.
|
|
62
|
+
function fit(s, n, words) {
|
|
63
|
+
let t = String(s == null ? '' : s).replace(/\s+/g, ' ').trim();
|
|
64
|
+
let cut = false;
|
|
65
|
+
if (words) {
|
|
66
|
+
const w = t.split(' ');
|
|
67
|
+
if (w.length > words) { t = w.slice(0, words).join(' ').replace(/[,.;:·-]+$/, ''); cut = true; }
|
|
68
|
+
}
|
|
69
|
+
if (t.length > n) cut = true;
|
|
70
|
+
if (!cut) return t;
|
|
71
|
+
t = t.replace(/…$/, '');
|
|
72
|
+
if (t.length > n - 1) t = t.slice(0, n - 1).trimEnd();
|
|
73
|
+
return t + '…';
|
|
74
|
+
}
|
|
75
|
+
const wordCount = (s) => String(s).replace(/…$/, '').split(' ').filter(Boolean).length;
|
|
76
|
+
|
|
77
|
+
// An age in the board's words: '40 min', '5 hours', '2 days', '3 weeks'.
|
|
78
|
+
function ageWord(at, now) {
|
|
79
|
+
const t = at ? Date.parse(at) : NaN;
|
|
80
|
+
if (!isFinite(t) || !now) return '';
|
|
81
|
+
const m = Math.max(0, Math.round((now - t) / 60000));
|
|
82
|
+
if (m < 60) return (m || 1) + ' min';
|
|
83
|
+
const h = Math.round(m / 60);
|
|
84
|
+
if (h < 48) return h + (h === 1 ? ' hour' : ' hours');
|
|
85
|
+
const d = Math.round(h / 24);
|
|
86
|
+
if (d < 14) return d + ' days';
|
|
87
|
+
return Math.round(d / 7) + ' weeks';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The words a floor counts its rows in, and says when it is cleared.
|
|
91
|
+
const NOUNS = Object.freeze({
|
|
92
|
+
access_requests: 'waiting', goal_join_requests: 'asking', override_requests: 'asked', gate_approvals: 'ready',
|
|
93
|
+
core_update: 'update', blockers: 'waiting', board: 'open vote', idea: 'open vote',
|
|
94
|
+
});
|
|
95
|
+
const CLEAR = Object.freeze({
|
|
96
|
+
access_requests: 'Everyone is in', goal_join_requests: 'No one is asking', override_requests: 'No overrides asked',
|
|
97
|
+
gate_approvals: 'Everything ready is live', blockers: 'Nothing blocked on you', board: 'Every vote is in', idea: 'No ideas waiting',
|
|
98
|
+
});
|
|
99
|
+
// The card's acts under the desk's words (the board's button labels).
|
|
100
|
+
const ACT_WORDS = Object.freeze({ for: 'yes', object: 'no', fine: 'let in', refuse: 'not now', release: 'release', hold: 'hold' });
|
|
101
|
+
// An objection's reason as one tap: the five sections the server accepts
|
|
102
|
+
// (street-card.js SECTIONS), each in at most 16 characters.
|
|
103
|
+
const CHIPS = Object.freeze({ scope: 'what it changes', purpose: 'its purpose', why_matters: 'why it matters', fit: 'how it fits', success: 'how to measure' });
|
|
104
|
+
const STATE = Object.freeze({ needs: 'needs you', moving: 'moving', settled: 'settled' });
|
|
105
|
+
const PEOPLE = Object.freeze(['access_requests', 'goal_join_requests']);
|
|
106
|
+
|
|
107
|
+
function str(v) { return v == null ? '' : String(v); }
|
|
108
|
+
function personOf(m) { return m && m.proposer ? str(m.proposer.name || m.proposer.login) : ''; }
|
|
109
|
+
function initials(t) {
|
|
110
|
+
const w = str(t).replace(/[^A-Za-z0-9 ]+/g, ' ').trim().split(/\s+/).filter(Boolean);
|
|
111
|
+
return (w.length > 1 ? w[0].charAt(0) + w[1].charAt(0) : str(w[0]).slice(0, 2)).toUpperCase() || '·';
|
|
112
|
+
}
|
|
113
|
+
function stateOf(m) { return m && STATE[m.state] ? m.state : 'moving'; }
|
|
114
|
+
function isIdea(m) { return !!m && m.source === 'board' && m.short === 'idea to ratify'; }
|
|
115
|
+
|
|
116
|
+
// ---------- which matters sit on which floor ----------
|
|
117
|
+
// A floor bound to a source holds that source's matters in this building; the
|
|
118
|
+
// ideas floor holds the board's open full ideas; every other open matter of
|
|
119
|
+
// the building (a board sitting, a source no floor names) sits on the first
|
|
120
|
+
// floor, the building's front desk. Settled matters are the record, not rows.
|
|
121
|
+
function floorMatters(key, floor, docket) {
|
|
122
|
+
const plan = I && I.PLAN[key];
|
|
123
|
+
if (!plan || !docket || !Array.isArray(docket.matters)) return [];
|
|
124
|
+
const specs = plan.floors;
|
|
125
|
+
const spec = specs[floor];
|
|
126
|
+
if (!spec) return [];
|
|
127
|
+
const bind = spec[2] || {};
|
|
128
|
+
const open = docket.matters.filter((m) => m && stateOf(m) !== 'settled');
|
|
129
|
+
const own = open.filter((m) => m.building === key);
|
|
130
|
+
const sources = new Set(specs.map((s) => s[2] && s[2].source).filter(Boolean));
|
|
131
|
+
const ideasHere = specs.some((s) => s[2] && s[2].floor === 'planning.ideas');
|
|
132
|
+
if (bind.source) return own.filter((m) => m.source === bind.source);
|
|
133
|
+
if (bind.floor === 'planning.ideas') return open.filter(isIdea);
|
|
134
|
+
if (floor !== 0) return [];
|
|
135
|
+
return own.filter((m) => !sources.has(m.source) && !(ideasHere && isIdea(m)));
|
|
136
|
+
}
|
|
137
|
+
// The floor that needs you first (so a building opens on its work), or null.
|
|
138
|
+
function firstNeeds(key, docket) {
|
|
139
|
+
const plan = I && I.PLAN[key];
|
|
140
|
+
if (!plan) return null;
|
|
141
|
+
for (let i = 0; i < plan.floors.length; i++) if (floorMatters(key, i, docket).some((m) => stateOf(m) === 'needs')) return i;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------- one row ----------
|
|
146
|
+
// ctx { docket, room, constitution, now, viewerId }
|
|
147
|
+
function rowOf(m, ctx) {
|
|
148
|
+
const now = ctx.now || 0;
|
|
149
|
+
const person = personOf(m);
|
|
150
|
+
const people = PEOPLE.indexOf(m.source) >= 0;
|
|
151
|
+
const name = fit(people && person ? person : (m.title || m.short || 'a matter'), LIMITS.name, LIMITS.nameWords);
|
|
152
|
+
const age = ageWord(m.opened_at, now);
|
|
153
|
+
const lead = people ? (m.source === 'goal_join_requests' ? 'goal team' : 'to join') : person ? person.split(/\s+/)[0] : str(m.short || STATE[stateOf(m)]);
|
|
154
|
+
const fact = fit([lead, age].filter(Boolean).join(' · '), LIMITS.fact);
|
|
155
|
+
const when = S && S.whenWord ? S.whenWord(m.deadline, now) : '';
|
|
156
|
+
const facts = [];
|
|
157
|
+
const add = (t) => { const v = fit(t, LIMITS.factLen); if (v && facts.indexOf(v) < 0 && facts.length < LIMITS.facts) facts.push(v); };
|
|
158
|
+
if (m.title && name !== str(m.title)) add(m.title);
|
|
159
|
+
if (person && !people) add('by ' + person);
|
|
160
|
+
if (typeof m.tally === 'string') add(m.tally);
|
|
161
|
+
if (when) add('closes ' + when);
|
|
162
|
+
if (m.where) add(str(m.where).toLowerCase());
|
|
163
|
+
add(STATE[stateOf(m)]);
|
|
164
|
+
const cv = C && C.model ? C.model({ docket: ctx.docket, key: m.id, room: ctx.room, constitution: ctx.constitution, now: now, viewerId: ctx.viewerId }) : null;
|
|
165
|
+
const actions = [];
|
|
166
|
+
let chips = null;
|
|
167
|
+
if (cv && !cv.readOnly && !cv.loading) {
|
|
168
|
+
cv.actions.forEach((a) => {
|
|
169
|
+
if (actions.length >= LIMITS.actions || !ACT_WORDS[a.key]) return;
|
|
170
|
+
actions.push({ key: a.key, label: ACT_WORDS[a.key], tone: actions.length ? 'quiet' : 'primary', go: a.go || null, ask: a.key === 'object' });
|
|
171
|
+
});
|
|
172
|
+
if (actions.some((a) => a.ask)) {
|
|
173
|
+
chips = { for: 'object', ask: 'why', options: Object.keys(CHIPS).map((k) => ({ key: k, label: CHIPS[k], input: { section: k, reason: 'Objects to ' + CHIPS[k] + ' (one tap on the floor desk).' } })) };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const wait = actions.length ? '' : cv && cv.loading ? 'reading…' : stateOf(m) === 'needs' ? 'not from here' : 'moving';
|
|
177
|
+
return {
|
|
178
|
+
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
179
|
+
actions: actions, chips: chips, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function rowsFor(key, floor, docket, ctx) {
|
|
183
|
+
const c = Object.assign({ docket: docket }, ctx || {});
|
|
184
|
+
const done = c.done || {};
|
|
185
|
+
return floorMatters(key, floor, docket).filter((m) => !done[str(m.id)]).map((m) => rowOf(m, c))
|
|
186
|
+
.sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------- the whole desk ----------
|
|
190
|
+
// input { key, floor (index), docket, goals (open), now, room, constitution,
|
|
191
|
+
// viewerId, done {matterId: true}, mine [what you did here, newest
|
|
192
|
+
// first], cleared (you cleared it this visit), today (decisions
|
|
193
|
+
// today), sym {d, f} (the floor's sign, from buildingModel) }
|
|
194
|
+
function deskModel(input) {
|
|
195
|
+
const o = input || {};
|
|
196
|
+
const plan = I && I.PLAN[o.key];
|
|
197
|
+
if (!plan) return null;
|
|
198
|
+
const docket = o.docket && Array.isArray(o.docket.matters) ? o.docket : null;
|
|
199
|
+
const floor = Math.max(0, Math.min(Number(o.floor) || 0, plan.floors.length - 1));
|
|
200
|
+
const spec = plan.floors[floor];
|
|
201
|
+
const fl = I.floorFor(spec, { key: o.key, docket: docket, goals: o.goals || [], now: o.now || 0 });
|
|
202
|
+
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done };
|
|
203
|
+
const rows = docket ? rowsFor(o.key, floor, docket, ctx) : [];
|
|
204
|
+
const bind = spec[2] || {};
|
|
205
|
+
const src = bind.source ? bind.source : bind.floor === 'planning.ideas' ? 'idea' : floor === 0 ? 'board' : null;
|
|
206
|
+
const needs = rows.filter((r) => r.needs);
|
|
207
|
+
const noun = NOUNS[src] || 'open';
|
|
208
|
+
const oldestOf = (list) => list.reduce((a, r) => (r.opened && (!a || Date.parse(r.opened) < Date.parse(a)) ? r.opened : a), null);
|
|
209
|
+
const cleared = !!o.cleared && rows.length === 0;
|
|
210
|
+
const quiet = rows.length === 0;
|
|
211
|
+
let status, hot = false;
|
|
212
|
+
if (!docket) status = 'the docket could not be read';
|
|
213
|
+
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
|
|
214
|
+
else if (rows.length) { const age = ageWord(oldestOf(rows), o.now); status = rows.length + ' moving' + (age ? ' · oldest ' + age : ''); }
|
|
215
|
+
else if (cleared) status = 'floor clear';
|
|
216
|
+
else status = 'nothing needs you · ' + fl.line;
|
|
217
|
+
const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
|
|
218
|
+
// tiles: one number and two words, never a zero
|
|
219
|
+
const tiles = [];
|
|
220
|
+
const tile = (v, l) => {
|
|
221
|
+
const val = str(v).trim();
|
|
222
|
+
if (!/[0-9]/.test(val) || /^[$]?[0.,]+%?$/.test(val)) return;
|
|
223
|
+
// two words, read as a label: the rest of a longer line is left off, not marked cut
|
|
224
|
+
const words = str(l).replace(/\s+/g, ' ').trim().split(' ').slice(0, LIMITS.tileWords).join(' ').replace(/[,.;:·-]+$/, '');
|
|
225
|
+
tiles.push({ key: 't' + tiles.length, v: fit(val, LIMITS.tileValue), l: fit(words, LIMITS.tileLabel) });
|
|
226
|
+
};
|
|
227
|
+
if (rows.length) { tile(needs.length, noun); tile(rows.length - needs.length, 'moving'); }
|
|
228
|
+
else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
|
|
229
|
+
tile(history.length, 'on record');
|
|
230
|
+
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
|
|
231
|
+
.concat(history.map((h, i) => ({ key: 'h' + i, text: fit([h.title, h.outcome, ageWord(h.at, o.now)].filter(Boolean).join(' · '), LIMITS.recordLen), mine: false })))
|
|
232
|
+
.slice(0, LIMITS.record);
|
|
233
|
+
return {
|
|
234
|
+
key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
|
|
235
|
+
status: fit(status, LIMITS.status), hot: hot,
|
|
236
|
+
tiles: tiles.slice(0, LIMITS.tiles), rows: rows,
|
|
237
|
+
quiet: quiet, cleared: cleared,
|
|
238
|
+
clearLine: fit(cleared ? (CLEAR[src] || 'Floor clear') : 'Nothing needs you here', LIMITS.clearLine),
|
|
239
|
+
clearSub: fit(cleared ? 'lit up · ' + (o.today || 0) + ' decided today' : fl.line, LIMITS.clearSub),
|
|
240
|
+
record: record,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------- the rule as a checker ----------
|
|
245
|
+
function breaks(desk) {
|
|
246
|
+
const out = [];
|
|
247
|
+
if (!desk) return out;
|
|
248
|
+
const bad = (what, v) => out.push(desk.key + ' ' + what + ': ' + JSON.stringify(v));
|
|
249
|
+
const oneLine = (s) => typeof s === 'string' && !/[\r\n]/.test(s);
|
|
250
|
+
if (!oneLine(desk.status) || desk.status.length > LIMITS.status) bad('status too long', desk.status);
|
|
251
|
+
if (desk.hot !== desk.rows.some((r) => r.needs)) bad('status is orange only when something needs you', desk.status);
|
|
252
|
+
if (desk.tiles.length > LIMITS.tiles) bad('more than three tiles', desk.tiles.length);
|
|
253
|
+
desk.tiles.forEach((t) => {
|
|
254
|
+
if (!/[0-9]/.test(t.v) || t.v.length > LIMITS.tileValue || /^[$]?[0.,]+%?$/.test(t.v)) bad('tile value', t.v);
|
|
255
|
+
if (!t.l || wordCount(t.l) > LIMITS.tileWords || t.l.length > LIMITS.tileLabel) bad('tile label', t.l);
|
|
256
|
+
});
|
|
257
|
+
if (desk.quiet !== (desk.rows.length === 0)) bad('quiet and rows disagree', desk.rows.length);
|
|
258
|
+
if (desk.quiet && (!desk.clearLine || desk.clearLine.length > LIMITS.clearLine || !oneLine(desk.clearSub) || desk.clearSub.length > LIMITS.clearSub)) bad('quiet line', [desk.clearLine, desk.clearSub]);
|
|
259
|
+
desk.rows.forEach((r) => {
|
|
260
|
+
if (!r.name || !oneLine(r.name) || wordCount(r.name) > LIMITS.nameWords || r.name.length > LIMITS.name) bad('row name', r.name);
|
|
261
|
+
if (!oneLine(r.fact) || r.fact.length > LIMITS.fact) bad('row fact', r.fact);
|
|
262
|
+
if (r.actions.length > LIMITS.actions) bad('more than two buttons', r.actions.map((a) => a.label));
|
|
263
|
+
if (r.facts.length > LIMITS.facts || r.facts.some((f) => !oneLine(f) || f.length > LIMITS.factLen)) bad('row facts', r.facts);
|
|
264
|
+
if (r.chips && (r.chips.options.length > LIMITS.chips || r.chips.options.some((c) => c.label.length > LIMITS.chip))) bad('chips', r.chips.options.map((c) => c.label));
|
|
265
|
+
if (r.actions.some((a) => a.ask) && !r.chips) bad('a reason without chips', r.name);
|
|
266
|
+
});
|
|
267
|
+
if (desk.record.length > LIMITS.record || desk.record.some((x) => !oneLine(x.text) || x.text.length > LIMITS.recordLen)) bad('record line', desk.record.map((x) => x.text));
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// The street header's meter: one pip per decision today, ten to a row.
|
|
272
|
+
function todayPips(n) {
|
|
273
|
+
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
274
|
+
const pips = [];
|
|
275
|
+
for (let i = 0; i < LIMITS.todayTarget; i++) pips.push({ key: i, on: i < k, fresh: k > 0 && i === Math.min(k, LIMITS.todayTarget) - 1 });
|
|
276
|
+
return { pips: pips, text: k + ' decided' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const api = {
|
|
280
|
+
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
281
|
+
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips,
|
|
282
|
+
};
|
|
283
|
+
root.GovDesk = api;
|
|
284
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
285
|
+
}(typeof window !== 'undefined' ? window : globalThis));
|
|
@@ -208,7 +208,9 @@
|
|
|
208
208
|
const used = !!fl, sel = used && i === selFloor;
|
|
209
209
|
const pend = used && fl.status === 'pending';
|
|
210
210
|
const prop = used && (fl.status === 'proposed' || QUIET[fl.status] !== undefined);
|
|
211
|
-
|
|
211
|
+
// a floor you cleared this visit lights its windows and shows a tick (task 1004558)
|
|
212
|
+
const clr = used && Array.isArray(st.cleared) && st.cleared.indexOf(i) >= 0;
|
|
213
|
+
const lit = sel || clr || (used && st.evening);
|
|
212
214
|
push(shapes, rect(X - 6, y1 - 5, W + 12, 5), fillOr(M2.slab), line, dash);
|
|
213
215
|
push(shapes, rect(X + 9, y0, W - 18, FH - 5), fillOr(lit ? M2.roomLit : M2.room), line, dash);
|
|
214
216
|
[X, X + W - 9].forEach((wx) => {
|
|
@@ -237,7 +239,8 @@
|
|
|
237
239
|
if (pend) push(shapes, rect(X + 11, y0 + 2, W - 22, FH - 9), 'none', '#fb9569', '4 4');
|
|
238
240
|
txt(X + 24, y0 + 24, cut(fl.name, 34), ghost || prop ? M2.faint : M2.name, 14.5, 500);
|
|
239
241
|
txt(X + 24, y0 + 40, cut(fl.line, 44), pend ? T.accentText : M2.line, 12, 400);
|
|
240
|
-
if (
|
|
242
|
+
if (clr) push(shapes, 'M' + (X + W - 44) + ' ' + (y0 + 22) + 'l6 6 12-13', 'none', M2.name, 'none', 1.8);
|
|
243
|
+
else if (fl.count) txt(X + W - 22, y0 + 34, fl.count, M2.name, 20, 300, 'end', sans, '-0.02em');
|
|
241
244
|
else if (pend) txt(X + W - 22, y0 + 32, 'being decided', T.accentText, 11.5, 500, 'end');
|
|
242
245
|
else if (QUIET[fl.status]) txt(X + W - 22, y0 + 32, QUIET[fl.status], M2.faint, 11.5, 400, 'end');
|
|
243
246
|
}
|
|
@@ -540,7 +543,8 @@
|
|
|
540
543
|
}
|
|
541
544
|
|
|
542
545
|
// The building interior as data: civicInfo() drawn by buildingView().
|
|
543
|
-
// input { key, docket, goals, mode, now, floor, drawer, picks }
|
|
546
|
+
// input { key, docket, goals, mode, now, floor, drawer, picks, cleared }
|
|
547
|
+
// cleared: the floor indexes the viewer cleared at the desk this visit (street-desk.js)
|
|
544
548
|
function buildingModel(input) {
|
|
545
549
|
const o = input || {};
|
|
546
550
|
if (S.CIVIC_KEYS.indexOf(o.key) < 0) return null;
|
|
@@ -550,7 +554,7 @@
|
|
|
550
554
|
const b = civicInfo(o.key, { docket: o.docket, goals: goals, now: o.now });
|
|
551
555
|
const docket = o.docket && Array.isArray(o.docket.matters) ? o.docket : null;
|
|
552
556
|
const evening = !!docket && !(docket.counts && Number(docket.counts.needs) > 0);
|
|
553
|
-
const bv = buildingView(T, o.key, b, { floor: o.floor, drawer: o.drawer, picks: o.picks, vacant: b.vacant, reviewHeld: !b.vacant, evening: evening });
|
|
557
|
+
const bv = buildingView(T, o.key, b, { floor: o.floor, drawer: o.drawer, picks: o.picks, vacant: b.vacant, reviewHeld: !b.vacant, evening: evening, cleared: o.cleared });
|
|
554
558
|
bv.table = bv.table.map((r) => Object.assign({}, r, { color: r.color === 'accent' ? T.accentText : T.ink }));
|
|
555
559
|
bv.key = o.key; bv.mode = mode; bv.vacant = b.vacant;
|
|
556
560
|
bv.label = b.name + ' in section: ' + b.floors.length + ' of ' + bv.cap + ' floors in use' + (bv.hasTable ? ', ' + bv.table.length + ' on the table' : '') + '.';
|
|
@@ -283,6 +283,58 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
283
283
|
.inside-line { display: flex; justify-content: space-between; gap: 14px; padding: 9px 0; border-top: 1px solid var(--street-rule); font-size: 14.5px; line-height: 1.4; }
|
|
284
284
|
.inside-line__meta { color: var(--ink-faint); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
285
285
|
|
|
286
|
+
/* THE FLOOR DESK (task 1004558; Desks.dc.html): the chosen floor as one panel
|
|
287
|
+
you act from. Status orange only when it needs you; tiles are one number and
|
|
288
|
+
two words; rows are a name, one fact and at most two chips; a quiet floor is
|
|
289
|
+
one lit line. Tokens only, so both modes come from the pack. */
|
|
290
|
+
.desk { gap: 0; }
|
|
291
|
+
.desk .inside-open__head { padding-bottom: 14px; }
|
|
292
|
+
.desk-status { font-size: 14px; color: var(--ink-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
293
|
+
.desk-status--hot { color: var(--street-accent-text); }
|
|
294
|
+
.desk-tiles { display: flex; flex-wrap: wrap; border-top: 1px solid var(--street-rule); }
|
|
295
|
+
.desk-tile { min-width: 108px; padding: 10px 16px; border-left: 1px solid var(--street-rule); display: flex; flex-direction: column; gap: 2px; }
|
|
296
|
+
.desk-tile:first-child { border-left: 0; padding-left: 0; }
|
|
297
|
+
.desk-tile__v { font-size: 26px; font-weight: 300; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
|
|
298
|
+
.desk-tile__l { font-size: 12.5px; color: var(--ink-faint); white-space: nowrap; }
|
|
299
|
+
.desk-rows { display: flex; flex-direction: column; border-top: 1px solid var(--street-rule); }
|
|
300
|
+
.desk-row { border-bottom: 1px solid var(--street-rule); padding: 12px 0; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
|
|
301
|
+
.desk-row--gone { animation: desk-gone 320ms cubic-bezier(.4,0,.2,1) both; }
|
|
302
|
+
.desk-row__main { display: flex; align-items: center; gap: 14px; min-height: 44px; }
|
|
303
|
+
.desk-ini { width: 38px; height: 38px; border-radius: 999px; flex-shrink: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center; font-size: 12.5px; font-weight: 500; background: var(--street-wash); border: 1px solid var(--street-rule); }
|
|
304
|
+
.desk-row--needs .desk-ini { border-color: var(--accent); }
|
|
305
|
+
.desk-name { flex: 1 1 auto; min-width: 0; background: none; border: 0; padding: 4px 0; min-height: 24px; cursor: pointer; text-align: left; display: flex; flex-direction: column; gap: 3px; color: var(--ink); font-family: var(--font-body); }
|
|
306
|
+
.desk-name__t { font-size: 15.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
307
|
+
.desk-name__f { font-size: 13px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
308
|
+
.desk-acts { display: flex; align-items: center; gap: 8px; flex: none; margin-left: auto; }
|
|
309
|
+
.desk-chip { height: 34px; padding: 0 16px; border-radius: 999px; font-family: var(--font-body); font-size: 13.5px; font-weight: 500; cursor: pointer; white-space: nowrap; text-transform: lowercase; }
|
|
310
|
+
.desk-chip--primary { background: var(--ink); border: 0; color: var(--bg); }
|
|
311
|
+
.desk-chip--quiet, .desk-chip--choice { background: none; border: 1px solid var(--street-rule-strong); color: var(--ink); }
|
|
312
|
+
.desk-chip--choice { height: 30px; padding: 0 14px; }
|
|
313
|
+
.desk-chip:hover { filter: brightness(1.08); }
|
|
314
|
+
.desk-chip[disabled] { opacity: 0.55; cursor: progress; }
|
|
315
|
+
.desk-wait { font-size: 13px; color: var(--ink-faint); border: 1px dashed var(--street-rule-strong); border-radius: 999px; padding: 6px 12px; white-space: nowrap; }
|
|
316
|
+
.desk-facts, .desk-choices { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 10px 0 2px 52px; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
|
|
317
|
+
.desk-fact { font-size: 13px; color: var(--ink-soft); background: var(--street-wash); border-radius: 6px; padding: 6px 10px; white-space: nowrap; }
|
|
318
|
+
.desk-choices__ask { font-size: 12.5px; color: var(--ink-faint); margin-right: 4px; }
|
|
319
|
+
.desk-quiet { min-height: 168px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; border-top: 1px solid var(--street-rule); text-align: center; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
|
|
320
|
+
.desk-quiet__tick { display: block; fill: none; stroke: var(--ink); stroke-width: 1.4; stroke-linecap: round; animation: desk-pop 420ms cubic-bezier(.2,.7,.3,1) both; }
|
|
321
|
+
.desk-quiet--cleared .desk-quiet__tick { stroke: var(--street-accent-text); }
|
|
322
|
+
.desk-quiet__line { font-size: 20px; font-weight: 300; }
|
|
323
|
+
.desk-quiet__sub { font-size: 13.5px; color: var(--ink-faint); }
|
|
324
|
+
.desk-record { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 22px; border-top: 1px solid var(--street-rule); margin-top: 18px; padding-top: 14px; }
|
|
325
|
+
.desk-record__label { font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }
|
|
326
|
+
.desk-record__item { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
|
|
327
|
+
.desk-record__item--mine { color: var(--ink); }
|
|
328
|
+
.desk-pop { animation: desk-pop 420ms cubic-bezier(.2,.7,.3,1) both; }
|
|
329
|
+
.desk-name:focus-visible, .desk-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
330
|
+
@keyframes desk-gone { to { opacity: 0; transform: translateX(24px); } }
|
|
331
|
+
@keyframes desk-pop { 0% { transform: scale(.6); opacity: 0; } 60% { transform: scale(1.15); opacity: 1; } 100% { transform: scale(1); } }
|
|
332
|
+
/* the today meter in the street header: one pip per decision */
|
|
333
|
+
.street-today__pips { display: flex; gap: 5px; }
|
|
334
|
+
.street-today__pip { width: 10px; height: 10px; border-radius: 3px; box-sizing: border-box; border: 1px solid var(--street-rule-strong); }
|
|
335
|
+
.street-today__pip--on { background: var(--accent); border-color: var(--accent); }
|
|
336
|
+
.street-today__n { font-size: 14px; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
337
|
+
|
|
286
338
|
/* the record: one filing drawer a month */
|
|
287
339
|
.inside-record { display: flex; flex-direction: column; gap: 10px; }
|
|
288
340
|
.inside-record__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
@@ -318,6 +370,13 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
318
370
|
.inside-panel { order: 3; overflow: visible; }
|
|
319
371
|
.inside-name { font-size: 32px; }
|
|
320
372
|
.inside-gov__row { flex-wrap: wrap; row-gap: 4px; }
|
|
373
|
+
.desk-row__main { flex-wrap: wrap; row-gap: 8px; }
|
|
374
|
+
.desk-name { flex-basis: calc(100% - 52px); }
|
|
375
|
+
.desk-acts { margin-left: 52px; }
|
|
376
|
+
.desk-facts, .desk-choices { padding-left: 0; }
|
|
377
|
+
.desk-tile { min-width: 0; padding: 10px 14px; }
|
|
378
|
+
.street-today__pips { gap: 3px; }
|
|
379
|
+
.street-today__pip { width: 8px; height: 8px; }
|
|
321
380
|
}
|
|
322
381
|
|
|
323
382
|
/* the prototype's motion, verbatim */
|
|
@@ -11,7 +11,7 @@
|
|
|
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-
|
|
14
|
+
<link rel="stylesheet" href="/builders/street.css?v=2026-10-03-desk1">
|
|
15
15
|
<link rel="stylesheet" href="/builders/street-card.css?v=2026-10-02-card1">
|
|
16
16
|
</head>
|
|
17
17
|
<!-- THE STREET (task 1004525, goal 1000125, R03). The street view of
|
|
@@ -67,6 +67,11 @@
|
|
|
67
67
|
<h1 class="street-kicker">the street</h1>
|
|
68
68
|
</div>
|
|
69
69
|
<div class="street-hud">
|
|
70
|
+
<div class="street-hud__item street-today" title="decisions made today">
|
|
71
|
+
<span class="street-label">today</span>
|
|
72
|
+
<span class="street-today__pips" id="streetTodayPips" aria-hidden="true"></span>
|
|
73
|
+
<span class="street-today__n" id="streetTodayN">0 decided</span>
|
|
74
|
+
</div>
|
|
70
75
|
<div class="street-hud__item">
|
|
71
76
|
<span class="street-hud__dot" id="hudDot"></span>
|
|
72
77
|
<span class="street-label">needs you</span>
|
|
@@ -210,8 +215,9 @@
|
|
|
210
215
|
<script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
|
|
211
216
|
<script src="/builders/street-draw.js?v=2026-10-02-street1"></script>
|
|
212
217
|
<script src="/builders/street-card.js?v=2026-10-02-amend1"></script>
|
|
213
|
-
<script src="/builders/street-inside.js?v=2026-10-
|
|
218
|
+
<script src="/builders/street-inside.js?v=2026-10-03-desk1"></script>
|
|
219
|
+
<script src="/builders/street-desk.js?v=2026-10-03-desk1"></script>
|
|
214
220
|
<script src="/builders/street-amend.js?v=2026-10-02-amend1"></script>
|
|
215
|
-
<script src="/builders/street.js?v=2026-10-
|
|
221
|
+
<script src="/builders/street.js?v=2026-10-03-desk1"></script>
|
|
216
222
|
</body>
|
|
217
223
|
</html>
|