@bongos/core 1.21.33 → 1.21.35
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 +51 -41
- package/docs/copy-inventory.md +19 -17
- package/docs/copy-registry.json +39 -21
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -1
- package/docs/page-readings.json +322 -194
- package/modules/autonomy/governor-docket.js +68 -1
- package/modules/government/docket-floors.js +20 -0
- package/modules/government/docket-vocab.js +14 -0
- package/modules/government/docket.js +103 -2
- package/modules/government/routes/government.js +13 -0
- package/modules/hall-ui/public/street-card.js +97 -1
- package/modules/hall-ui/public/street-desk.js +386 -0
- package/modules/hall-ui/public/street-inside.js +14 -10
- package/modules/hall-ui/public/street.css +60 -0
- package/modules/hall-ui/public/street.html +9 -3
- package/modules/hall-ui/public/street.js +238 -17
- package/modules/hall-ui/public/street.states.json +314 -6
- package/modules/lifecycle/governor-docket.js +24 -2
- package/modules/lifecycle/routes/project-settings.js +24 -0
- package/modules/onboarding/routes/access-requests.js +7 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +36 -1
- package/modules/security/routes/reports.js +23 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/routes/backup.js +26 -0
- package/src/bongos/routes/builders.js +40 -0
- package/src/bongos/routes/modules.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +28 -1
- package/tests/hall_matter_card.mjs +12 -3
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_desk.mjs +557 -0
- package/tests/hall_street_inside.mjs +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
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({
|
|
101
|
+
for: 'yes', object: 'no', fine: 'let in', refuse: 'not now', release: 'release', hold: 'hold',
|
|
102
|
+
// the acts the server hands a row (task 1004559), each its old page's route
|
|
103
|
+
confirm: 'confirm', dispute: 'dispute', fix: 'fixed', remove: 'remove', keep: 'keep',
|
|
104
|
+
signoff: 'sign off', record: 'record it', force: 'force',
|
|
105
|
+
switch_on: 'switch on', switch_off: 'switch off', allow: 'allow', disallow: 'disallow', first: 'first',
|
|
106
|
+
pause: 'pause', resume: 'resume', tier: 'tier', deactivate: 'deactivate', give: 'give', take: 'take back',
|
|
107
|
+
check: 'check', take_core: 'take it', rot: 'set', module_on: 'turn on', module_off: 'turn off', backup: 'back up', invite: 'invite', open: 'open',
|
|
108
|
+
});
|
|
109
|
+
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
110
|
+
const ASKS = Object.freeze({ check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
111
|
+
// How a floor counts rows nobody is waiting on (desk rows), by its desk.
|
|
112
|
+
const DESK_NOUNS = Object.freeze({
|
|
113
|
+
'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
|
|
114
|
+
'automation.switch': 'switch', 'automation.goals': 'allowed', 'automation.runners': 'runners',
|
|
115
|
+
'people.invite': 'invites', 'release.core': 'update', 'rules.settings': 'settings', 'rules.modules': 'modules', 'treasury.backup': 'backups',
|
|
116
|
+
});
|
|
117
|
+
// An objection's reason as one tap: the five sections the server accepts
|
|
118
|
+
// (street-card.js SECTIONS), each in at most 16 characters.
|
|
119
|
+
const CHIPS = Object.freeze({ scope: 'what it changes', purpose: 'its purpose', why_matters: 'why it matters', fit: 'how it fits', success: 'how to measure' });
|
|
120
|
+
const STATE = Object.freeze({ needs: 'needs you', moving: 'moving', settled: 'settled' });
|
|
121
|
+
const PEOPLE = Object.freeze(['access_requests', 'goal_join_requests']);
|
|
122
|
+
|
|
123
|
+
function str(v) { return v == null ? '' : String(v); }
|
|
124
|
+
function personOf(m) { return m && m.proposer ? str(m.proposer.name || m.proposer.login) : ''; }
|
|
125
|
+
function initials(t) {
|
|
126
|
+
const w = str(t).replace(/[^A-Za-z0-9 ]+/g, ' ').trim().split(/\s+/).filter(Boolean);
|
|
127
|
+
return (w.length > 1 ? w[0].charAt(0) + w[1].charAt(0) : str(w[0]).slice(0, 2)).toUpperCase() || '·';
|
|
128
|
+
}
|
|
129
|
+
function stateOf(m) { return m && STATE[m.state] ? m.state : 'moving'; }
|
|
130
|
+
function isIdea(m) { return !!m && m.source === 'board' && m.short === 'idea to ratify'; }
|
|
131
|
+
|
|
132
|
+
// ---------- which matters sit on which floor ----------
|
|
133
|
+
// A floor bound to a source holds that source's matters in this building; the
|
|
134
|
+
// ideas floor holds the board's open full ideas; every other open matter of
|
|
135
|
+
// the building (a board sitting, a source no floor names) sits on the first
|
|
136
|
+
// floor, the building's front desk. Settled matters are the record, not rows.
|
|
137
|
+
function floorMatters(key, floor, docket) {
|
|
138
|
+
const plan = I && I.PLAN[key];
|
|
139
|
+
if (!plan || !docket || !Array.isArray(docket.matters)) return [];
|
|
140
|
+
const specs = plan.floors;
|
|
141
|
+
const spec = specs[floor];
|
|
142
|
+
if (!spec) return [];
|
|
143
|
+
const bind = spec[2] || {};
|
|
144
|
+
const open = docket.matters.filter((m) => m && stateOf(m) !== 'settled');
|
|
145
|
+
const own = open.filter((m) => m.building === key);
|
|
146
|
+
const sources = new Set(specs.map((s) => s[2] && s[2].source).filter(Boolean));
|
|
147
|
+
const ideasHere = specs.some((s) => s[2] && s[2].floor === 'planning.ideas');
|
|
148
|
+
if (bind.source) return own.filter((m) => m.source === bind.source);
|
|
149
|
+
if (bind.floor === 'planning.ideas') return open.filter(isIdea);
|
|
150
|
+
if (floor !== 0) return [];
|
|
151
|
+
return own.filter((m) => !sources.has(m.source) && !(ideasHere && isIdea(m)));
|
|
152
|
+
}
|
|
153
|
+
// The floor that needs you first (so a building opens on its work), or null.
|
|
154
|
+
function firstNeeds(key, docket) {
|
|
155
|
+
const plan = I && I.PLAN[key];
|
|
156
|
+
if (!plan) return null;
|
|
157
|
+
for (let i = 0; i < plan.floors.length; i++) if (floorMatters(key, i, docket).some((m) => stateOf(m) === 'needs')) return i;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------- one row ----------
|
|
162
|
+
// ctx { docket, room, constitution, now, viewerId }
|
|
163
|
+
function rowOf(m, ctx) {
|
|
164
|
+
const now = ctx.now || 0;
|
|
165
|
+
const person = personOf(m);
|
|
166
|
+
const people = PEOPLE.indexOf(m.source) >= 0;
|
|
167
|
+
const name = fit(people && person ? person : (m.title || m.short || 'a matter'), LIMITS.name, LIMITS.nameWords);
|
|
168
|
+
const age = ageWord(m.opened_at, now);
|
|
169
|
+
const lead = people ? (m.source === 'goal_join_requests' ? 'goal team' : 'to join') : person ? person.split(/\s+/)[0] : str(m.short || STATE[stateOf(m)]);
|
|
170
|
+
const fact = fit([lead, age].filter(Boolean).join(' · '), LIMITS.fact);
|
|
171
|
+
const when = S && S.whenWord ? S.whenWord(m.deadline, now) : '';
|
|
172
|
+
const facts = [];
|
|
173
|
+
const add = (t) => { const v = fit(t, LIMITS.factLen); if (v && facts.indexOf(v) < 0 && facts.length < LIMITS.facts) facts.push(v); };
|
|
174
|
+
if (m.title && name !== str(m.title)) add(m.title);
|
|
175
|
+
if (person && !people) add('by ' + person);
|
|
176
|
+
if (typeof m.tally === 'string') add(m.tally);
|
|
177
|
+
if (when) add('closes ' + when);
|
|
178
|
+
if (m.where) add(str(m.where).toLowerCase());
|
|
179
|
+
add(STATE[stateOf(m)]);
|
|
180
|
+
if (Array.isArray(m.acts) && m.acts.length && C && C.actsCard) {
|
|
181
|
+
const ac = C.actsCard({ key: m.id, kind: m.source, id: m.ref && m.ref.id != null ? m.ref.id : str(m.id).replace(/^[^:]*:/, ''), who: person || name, title: m.title, building: m.building, href: m.href });
|
|
182
|
+
return Object.assign(actedRow(m.acts, ac, ctx), {
|
|
183
|
+
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
184
|
+
needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
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;
|
|
188
|
+
const actions = [];
|
|
189
|
+
let chips = null;
|
|
190
|
+
if (cv && !cv.readOnly && !cv.loading) {
|
|
191
|
+
cv.actions.forEach((a) => {
|
|
192
|
+
if (actions.length >= LIMITS.actions || !ACT_WORDS[a.key]) return;
|
|
193
|
+
actions.push({ key: a.key, label: ACT_WORDS[a.key], tone: actions.length ? 'quiet' : 'primary', go: a.go || null, ask: a.key === 'object' });
|
|
194
|
+
});
|
|
195
|
+
if (actions.some((a) => a.ask)) {
|
|
196
|
+
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).' } })) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const wait = actions.length ? '' : cv && cv.loading ? 'reading…' : stateOf(m) === 'needs' ? 'not from here' : 'moving';
|
|
200
|
+
return {
|
|
201
|
+
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
202
|
+
actions: actions, chips: chips, choices: chips ? { [chips.for]: chips } : {}, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The buttons and chips of a row whose acts the server handed it (task
|
|
207
|
+
// 1004559). An act with options asks first, with its options as one-tap chips;
|
|
208
|
+
// `pick: 'open_goals'` offers the open goals the street already reads; an act
|
|
209
|
+
// whose chips come out empty is left off rather than drawn dead. 'record'
|
|
210
|
+
// goes to the matter's own page (a sign-off that needs a recording).
|
|
211
|
+
function actedRow(acts, card, ctx) {
|
|
212
|
+
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || 'goal ' + g.id)]));
|
|
213
|
+
const actions = [];
|
|
214
|
+
const choices = {};
|
|
215
|
+
acts.forEach((a) => {
|
|
216
|
+
// the core's 'take' is its own word ('take it'), not the rank's 'take back'
|
|
217
|
+
const word = a.key === 'take' && card.subject && card.subject.kind === 'core_pin' ? 'take_core' : a.key;
|
|
218
|
+
if (actions.length >= LIMITS.actions || !ACT_WORDS[word]) return;
|
|
219
|
+
let options = (a.options || []).map((o) => ({ key: str(o.key), label: fit(o.label, LIMITS.chip), input: { option: str(o.key), label: str(o.label) } }));
|
|
220
|
+
if (a.pick === 'open_goals') {
|
|
221
|
+
const except = new Set((a.except || []).map(str));
|
|
222
|
+
options = (ctx.goals || []).filter((g) => g && !except.has(str(g.id))).slice(0, LIMITS.chips)
|
|
223
|
+
.map((g) => ({ key: str(g.id), label: fit(titles.get(str(g.id)), LIMITS.chip), input: { option: str(g.id), label: titles.get(str(g.id)) } }));
|
|
224
|
+
if (!options.length) return;
|
|
225
|
+
}
|
|
226
|
+
options = options.slice(0, LIMITS.chips);
|
|
227
|
+
const goes = a.key === 'record' || a.key === 'open';
|
|
228
|
+
if (goes && !card.href) return;
|
|
229
|
+
const field = a.field || null;
|
|
230
|
+
const ask = options.length > 0 || !!field;
|
|
231
|
+
if (ask) choices[a.key] = field ? { for: a.key, ask: ASKS[word] || 'which', field: field, options: [] } : { for: a.key, ask: ASKS[word] || 'which', options: options };
|
|
232
|
+
actions.push({ key: a.key, label: ACT_WORDS[word], tone: actions.length ? 'quiet' : 'primary', go: goes ? card.href : null, ask: ask });
|
|
233
|
+
});
|
|
234
|
+
const first = actions.find((a) => a.ask);
|
|
235
|
+
return { actions: actions, chips: first ? choices[first.key] : null, choices: choices, wait: actions.length ? '' : 'not from here', card: card };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A desk row (task 1004559): something on this floor the viewer may act on that
|
|
239
|
+
// no one is waiting on, read by the module that owns its route.
|
|
240
|
+
function deskRowOf(d, ctx) {
|
|
241
|
+
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || '')]));
|
|
242
|
+
const hit = /^goal (\d+)$/.exec(str(d.name));
|
|
243
|
+
const full = hit && titles.get(hit[1]) ? titles.get(hit[1]) : str(d.name);
|
|
244
|
+
const name = fit(full, LIMITS.name, LIMITS.nameWords);
|
|
245
|
+
const age = ageWord(d.opened_at, ctx.now || 0);
|
|
246
|
+
const facts = [];
|
|
247
|
+
const add = (t) => { const v = fit(t, LIMITS.factLen); if (v && facts.indexOf(v) < 0 && facts.length < LIMITS.facts) facts.push(v); };
|
|
248
|
+
if (full !== name) add(full);
|
|
249
|
+
(d.facts || []).forEach(add);
|
|
250
|
+
if (age) add('since ' + age);
|
|
251
|
+
const ref = d.ref || {};
|
|
252
|
+
const card = C && C.actsCard ? C.actsCard({ key: d.id, kind: ref.type, id: ref.id, who: full, title: full, building: d.building, href: d.href }) : null;
|
|
253
|
+
return Object.assign(actedRow(d.acts || [], card || { href: null }, ctx), {
|
|
254
|
+
key: str(d.id), matter: str(d.id), ini: initials(name), name: name, fact: fit(d.fact, LIMITS.fact), facts: facts,
|
|
255
|
+
needs: d.needs === true, source: d.desk, opened: d.opened_at || null, desk: true, tool: d.tool === true, over: d.over || null,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// The desk rows of one floor: those of the desk its PLAN entry names.
|
|
259
|
+
function deskRows(key, floor, docket, ctx) {
|
|
260
|
+
const plan = I && I.PLAN[key];
|
|
261
|
+
const spec = plan && plan.floors[floor];
|
|
262
|
+
const bind = (spec && spec[2]) || {};
|
|
263
|
+
if (!bind.desk || !docket || !Array.isArray(docket.desks)) return [];
|
|
264
|
+
// a floor may hold more than one desk (Rules in force: the settings and the modules)
|
|
265
|
+
const keys = [].concat(bind.desk);
|
|
266
|
+
return docket.desks.filter((x) => x && keys.indexOf(x.key) >= 0 && x.status === 'live' && Array.isArray(x.rows))
|
|
267
|
+
.reduce((all, x) => all.concat(x.rows.filter((r) => r && r.building === key)), []);
|
|
268
|
+
}
|
|
269
|
+
function rowsFor(key, floor, docket, ctx) {
|
|
270
|
+
const c = Object.assign({ docket: docket }, ctx || {});
|
|
271
|
+
const done = c.done || {};
|
|
272
|
+
const desked = deskRows(key, floor, docket).filter((d) => !done[str(d.id)]).map((d) => deskRowOf(d, c)).filter((r) => r.actions.length);
|
|
273
|
+
// a desk row that stands in for a source (the core update's check-then-take)
|
|
274
|
+
// replaces that source's matter rows on this floor
|
|
275
|
+
const over = desked.map((r) => r.over).filter(Boolean);
|
|
276
|
+
return floorMatters(key, floor, docket).filter((m) => !done[str(m.id)] && over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
|
|
277
|
+
.concat(desked)
|
|
278
|
+
.sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------- the whole desk ----------
|
|
282
|
+
// input { key, floor (index), docket, goals (open), now, room, constitution,
|
|
283
|
+
// viewerId, done {matterId: true}, mine [what you did here, newest
|
|
284
|
+
// first], cleared (you cleared it this visit), today (decisions
|
|
285
|
+
// today), sym {d, f} (the floor's sign, from buildingModel) }
|
|
286
|
+
function deskModel(input) {
|
|
287
|
+
const o = input || {};
|
|
288
|
+
const plan = I && I.PLAN[o.key];
|
|
289
|
+
if (!plan) return null;
|
|
290
|
+
const docket = o.docket && Array.isArray(o.docket.matters) ? o.docket : null;
|
|
291
|
+
const floor = Math.max(0, Math.min(Number(o.floor) || 0, plan.floors.length - 1));
|
|
292
|
+
const spec = plan.floors[floor];
|
|
293
|
+
const fl = I.floorFor(spec, { key: o.key, docket: docket, goals: o.goals || [], now: o.now || 0 });
|
|
294
|
+
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done, goals: o.goals || [] };
|
|
295
|
+
const rows = docket ? rowsFor(o.key, floor, docket, ctx) : [];
|
|
296
|
+
const bind = spec[2] || {};
|
|
297
|
+
const src = bind.source ? bind.source : bind.floor === 'planning.ideas' ? 'idea' : floor === 0 ? 'board' : null;
|
|
298
|
+
const needs = rows.filter((r) => r.needs);
|
|
299
|
+
const noun = NOUNS[src] || 'open';
|
|
300
|
+
// rows nobody waits on: 'moving' for matters, the desk's own noun for desk rows
|
|
301
|
+
const moving = rows.length && rows.every((r) => r.desk) ? (DESK_NOUNS[rows[0].source] || 'here') : 'moving';
|
|
302
|
+
const counted = rows.filter((r) => !r.tool);
|
|
303
|
+
const oldestOf = (list) => list.reduce((a, r) => (r.opened && (!a || Date.parse(r.opened) < Date.parse(a)) ? r.opened : a), null);
|
|
304
|
+
const cleared = !!o.cleared && rows.length === 0;
|
|
305
|
+
const quiet = rows.length === 0;
|
|
306
|
+
let status, hot = false;
|
|
307
|
+
if (!docket) status = 'the docket could not be read';
|
|
308
|
+
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
|
|
309
|
+
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + moving + (age ? ' · oldest ' + age : ''); }
|
|
310
|
+
else if (rows.length) status = fl.line;
|
|
311
|
+
else if (cleared) status = 'floor clear';
|
|
312
|
+
else status = 'nothing needs you · ' + fl.line;
|
|
313
|
+
const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
|
|
314
|
+
// tiles: one number and two words, never a zero
|
|
315
|
+
const tiles = [];
|
|
316
|
+
const tile = (v, l) => {
|
|
317
|
+
const val = str(v).trim();
|
|
318
|
+
if (!/[0-9]/.test(val) || /^[$]?[0.,]+%?$/.test(val)) return;
|
|
319
|
+
// two words, read as a label: the rest of a longer line is left off, not marked cut
|
|
320
|
+
const words = str(l).replace(/\s+/g, ' ').trim().split(' ').slice(0, LIMITS.tileWords).join(' ').replace(/[,.;:·-]+$/, '');
|
|
321
|
+
tiles.push({ key: 't' + tiles.length, v: fit(val, LIMITS.tileValue), l: fit(words, LIMITS.tileLabel) });
|
|
322
|
+
};
|
|
323
|
+
if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
|
|
324
|
+
else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
|
|
325
|
+
tile(history.length, 'on record');
|
|
326
|
+
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
|
|
327
|
+
.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 })))
|
|
328
|
+
.slice(0, LIMITS.record);
|
|
329
|
+
return {
|
|
330
|
+
key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
|
|
331
|
+
status: fit(status, LIMITS.status), hot: hot,
|
|
332
|
+
tiles: tiles.slice(0, LIMITS.tiles), rows: rows,
|
|
333
|
+
quiet: quiet, cleared: cleared,
|
|
334
|
+
clearLine: fit(cleared ? (CLEAR[src] || 'Floor clear') : 'Nothing needs you here', LIMITS.clearLine),
|
|
335
|
+
clearSub: fit(cleared ? 'lit up · ' + (o.today || 0) + ' decided today' : fl.line, LIMITS.clearSub),
|
|
336
|
+
record: record,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------- the rule as a checker ----------
|
|
341
|
+
function breaks(desk) {
|
|
342
|
+
const out = [];
|
|
343
|
+
if (!desk) return out;
|
|
344
|
+
const bad = (what, v) => out.push(desk.key + ' ' + what + ': ' + JSON.stringify(v));
|
|
345
|
+
const oneLine = (s) => typeof s === 'string' && !/[\r\n]/.test(s);
|
|
346
|
+
if (!oneLine(desk.status) || desk.status.length > LIMITS.status) bad('status too long', desk.status);
|
|
347
|
+
if (desk.hot !== desk.rows.some((r) => r.needs)) bad('status is orange only when something needs you', desk.status);
|
|
348
|
+
if (desk.tiles.length > LIMITS.tiles) bad('more than three tiles', desk.tiles.length);
|
|
349
|
+
desk.tiles.forEach((t) => {
|
|
350
|
+
if (!/[0-9]/.test(t.v) || t.v.length > LIMITS.tileValue || /^[$]?[0.,]+%?$/.test(t.v)) bad('tile value', t.v);
|
|
351
|
+
if (!t.l || wordCount(t.l) > LIMITS.tileWords || t.l.length > LIMITS.tileLabel) bad('tile label', t.l);
|
|
352
|
+
});
|
|
353
|
+
if (desk.quiet !== (desk.rows.length === 0)) bad('quiet and rows disagree', desk.rows.length);
|
|
354
|
+
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]);
|
|
355
|
+
desk.rows.forEach((r) => {
|
|
356
|
+
if (!r.name || !oneLine(r.name) || wordCount(r.name) > LIMITS.nameWords || r.name.length > LIMITS.name) bad('row name', r.name);
|
|
357
|
+
if (!oneLine(r.fact) || r.fact.length > LIMITS.fact) bad('row fact', r.fact);
|
|
358
|
+
if (r.actions.length > LIMITS.actions) bad('more than two buttons', r.actions.map((a) => a.label));
|
|
359
|
+
if (r.facts.length > LIMITS.facts || r.facts.some((f) => !oneLine(f) || f.length > LIMITS.factLen)) bad('row facts', r.facts);
|
|
360
|
+
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));
|
|
361
|
+
if (r.actions.some((a) => a.ask) && !r.chips) bad('a reason without chips', r.name);
|
|
362
|
+
Object.keys(r.choices || {}).forEach((k) => {
|
|
363
|
+
const c = r.choices[k];
|
|
364
|
+
if ((!c.field && !c.options.length) || c.options.length > LIMITS.chips || c.options.some((x) => !x.label || x.label.length > LIMITS.chip)) bad('chips', c.options.map((x) => x.label));
|
|
365
|
+
});
|
|
366
|
+
if (r.actions.some((a) => a.ask && !(r.choices || {})[a.key])) bad('an ask without its own chips', r.name);
|
|
367
|
+
});
|
|
368
|
+
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));
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The street header's meter: one pip per decision today, ten to a row.
|
|
373
|
+
function todayPips(n) {
|
|
374
|
+
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
375
|
+
const pips = [];
|
|
376
|
+
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 });
|
|
377
|
+
return { pips: pips, text: k + ' decided' };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const api = {
|
|
381
|
+
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, ASKS: ASKS, DESK_NOUNS: DESK_NOUNS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
382
|
+
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips,
|
|
383
|
+
};
|
|
384
|
+
root.GovDesk = api;
|
|
385
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
386
|
+
}(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
|
}
|
|
@@ -328,17 +331,17 @@
|
|
|
328
331
|
rules: {
|
|
329
332
|
holds: 'Standing rules. Each one settles a kind of matter so the council never votes on it again.', rule: 'majority', cap: 8,
|
|
330
333
|
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',
|
|
331
|
-
floors: [['Rules in force', 'the rules settling matters now', { floor: 'rules.in_force', reading: 'rules' }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
|
|
334
|
+
floors: [['Rules in force', 'the rules settling matters now', { floor: 'rules.in_force', reading: 'rules', desk: ['rules.settings', 'rules.modules'] }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
|
|
332
335
|
},
|
|
333
336
|
disputes: {
|
|
334
337
|
holds: 'Grade appeals, removal requests and security reports. Every ruling becomes precedent for the next one.', rule: 'decide', cap: 5,
|
|
335
338
|
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',
|
|
336
|
-
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' }]],
|
|
339
|
+
floors: [['Security reports', 'flaws found in our own walls', { source: 'redteam_reports', desk: 'disputes.confirmed' }], ['Removals', 'work proposed for removal', { source: 'removal_proposals' }], ['Grade appeals', 'a grade someone asks to change', { source: 'grade_appeals' }]],
|
|
337
340
|
},
|
|
338
341
|
people: {
|
|
339
342
|
holds: 'Who belongs, the rank each person holds, and who sits in which seat.', rule: 'consent', cap: 8,
|
|
340
343
|
growth: 'A new floor opens whenever the charter creates a new seat.', annex: 'room for an annex of seats',
|
|
341
|
-
floors: [['The council', 'who sits, and for how long', { floor: 'people.council' }], ['Testers', 'who signs off finished work', { floor: 'people.testers' }], ['Builders', 'who builds here', { reading: 'people' }], ['Newcomers', 'builders on newcomer tasks', { floor: 'people.newcomers' }], ['Waiting to join', 'asked to join the project', { source: 'access_requests' }], ['Ranks', 'the ranks, and who holds each', { floor: 'people.ranks' }], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
|
|
344
|
+
floors: [['The council', 'who sits, and for how long', { floor: 'people.council' }], ['Testers', 'who signs off finished work', { floor: 'people.testers' }], ['Builders', 'who builds here', { reading: 'people', desk: 'people.builders' }], ['Newcomers', 'builders on newcomer tasks', { floor: 'people.newcomers' }], ['Waiting to join', 'asked to join the project', { source: 'access_requests', desk: 'people.invite' }], ['Ranks', 'the ranks, and who holds each', { floor: 'people.ranks', desk: 'people.ranks' }], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
|
|
342
345
|
},
|
|
343
346
|
planning: {
|
|
344
347
|
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,
|
|
@@ -348,7 +351,7 @@
|
|
|
348
351
|
automation: {
|
|
349
352
|
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,
|
|
350
353
|
growth: 'A new floor opens for each goal you let it build.', annex: 'room for an annex',
|
|
351
|
-
floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence' }], ['Allowed goals', 'the goals it may build', { floor: 'automation.allowed_goals' }], ['Running now', 'builds in progress', { floor: 'automation.running' }], ['Spending limit', 'what it may spend', { floor: 'automation.spending_limit' }], ['Reports', 'what it did, told without asking', { floor: 'automation.reports' }]],
|
|
354
|
+
floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence', desk: 'automation.switch' }], ['Allowed goals', 'the goals it may build', { floor: 'automation.allowed_goals', desk: 'automation.goals' }], ['Running now', 'builds in progress', { floor: 'automation.running', desk: 'automation.runners' }], ['Spending limit', 'what it may spend', { floor: 'automation.spending_limit' }], ['Reports', 'what it did, told without asking', { floor: 'automation.reports' }]],
|
|
352
355
|
},
|
|
353
356
|
review: {
|
|
354
357
|
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,
|
|
@@ -358,12 +361,12 @@
|
|
|
358
361
|
release: {
|
|
359
362
|
holds: 'Putting finished work live, and taking updates to Bongos itself.', rule: 'decide', cap: 7,
|
|
360
363
|
growth: 'Room for a second gate if releases are ever split by area.', annex: 'room for a second gate',
|
|
361
|
-
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', { floor: 'release.held_back' }], ['Core update', 'a new version of Bongos', { source: 'core_update' }], ['Live this month', 'what went live', { floor: 'release.live_month' }], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
|
|
364
|
+
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', { floor: 'release.held_back' }], ['Core update', 'a new version of Bongos', { source: 'core_update', desk: 'release.core' }], ['Live this month', 'what went live', { floor: 'release.live_month' }], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
|
|
362
365
|
},
|
|
363
366
|
treasury: {
|
|
364
367
|
holds: 'The budget, and who gets paid for what.', rule: 'majority', cap: 7,
|
|
365
368
|
growth: 'Each new spending line takes a floor.', annex: 'room for an annex of spending lines',
|
|
366
|
-
floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting' }], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
369
|
+
floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting', desk: 'treasury.backup' }], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
367
370
|
},
|
|
368
371
|
});
|
|
369
372
|
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
@@ -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,59 @@ 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-field { height: 30px; min-width: 0; flex: 1 1 140px; max-width: 220px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--street-rule-strong); background: none; color: var(--ink); font-family: var(--font-body); font-size: 13.5px; }
|
|
314
|
+
.desk-chip:hover { filter: brightness(1.08); }
|
|
315
|
+
.desk-chip[disabled] { opacity: 0.55; cursor: progress; }
|
|
316
|
+
.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; }
|
|
317
|
+
.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; }
|
|
318
|
+
.desk-fact { font-size: 13px; color: var(--ink-soft); background: var(--street-wash); border-radius: 6px; padding: 6px 10px; white-space: nowrap; }
|
|
319
|
+
.desk-choices__ask { font-size: 12.5px; color: var(--ink-faint); margin-right: 4px; }
|
|
320
|
+
.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; }
|
|
321
|
+
.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; }
|
|
322
|
+
.desk-quiet--cleared .desk-quiet__tick { stroke: var(--street-accent-text); }
|
|
323
|
+
.desk-quiet__line { font-size: 20px; font-weight: 300; }
|
|
324
|
+
.desk-quiet__sub { font-size: 13.5px; color: var(--ink-faint); }
|
|
325
|
+
.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; }
|
|
326
|
+
.desk-record__label { font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }
|
|
327
|
+
.desk-record__item { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
|
|
328
|
+
.desk-record__item--mine { color: var(--ink); }
|
|
329
|
+
.desk-pop { animation: desk-pop 420ms cubic-bezier(.2,.7,.3,1) both; }
|
|
330
|
+
.desk-name:focus-visible, .desk-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
331
|
+
@keyframes desk-gone { to { opacity: 0; transform: translateX(24px); } }
|
|
332
|
+
@keyframes desk-pop { 0% { transform: scale(.6); opacity: 0; } 60% { transform: scale(1.15); opacity: 1; } 100% { transform: scale(1); } }
|
|
333
|
+
/* the today meter in the street header: one pip per decision */
|
|
334
|
+
.street-today__pips { display: flex; gap: 5px; }
|
|
335
|
+
.street-today__pip { width: 10px; height: 10px; border-radius: 3px; box-sizing: border-box; border: 1px solid var(--street-rule-strong); }
|
|
336
|
+
.street-today__pip--on { background: var(--accent); border-color: var(--accent); }
|
|
337
|
+
.street-today__n { font-size: 14px; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
338
|
+
|
|
286
339
|
/* the record: one filing drawer a month */
|
|
287
340
|
.inside-record { display: flex; flex-direction: column; gap: 10px; }
|
|
288
341
|
.inside-record__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
@@ -318,6 +371,13 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
318
371
|
.inside-panel { order: 3; overflow: visible; }
|
|
319
372
|
.inside-name { font-size: 32px; }
|
|
320
373
|
.inside-gov__row { flex-wrap: wrap; row-gap: 4px; }
|
|
374
|
+
.desk-row__main { flex-wrap: wrap; row-gap: 8px; }
|
|
375
|
+
.desk-name { flex-basis: calc(100% - 52px); }
|
|
376
|
+
.desk-acts { margin-left: 52px; }
|
|
377
|
+
.desk-facts, .desk-choices { padding-left: 0; }
|
|
378
|
+
.desk-tile { min-width: 0; padding: 10px 14px; }
|
|
379
|
+
.street-today__pips { gap: 3px; }
|
|
380
|
+
.street-today__pip { width: 8px; height: 8px; }
|
|
321
381
|
}
|
|
322
382
|
|
|
323
383
|
/* 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>
|