@bongos/core 1.21.18 → 1.21.19
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 +39 -24
- package/docs/copy-inventory.md +5 -5
- package/docs/copy-registry.json +6 -6
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +38 -38
- package/modules/hall-ui/public/street-amend.js +240 -0
- package/modules/hall-ui/public/street-card.js +4 -1
- package/modules/hall-ui/public/street.css +8 -0
- package/modules/hall-ui/public/street.html +5 -3
- package/modules/hall-ui/public/street.js +164 -11
- package/modules/hall-ui/public/street.states.json +12 -5
- package/modules/ui-design/kit/fixtures/government-constitution-rules.json +184 -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_street.mjs +1 -1
- package/tests/hall_street_amend.mjs +336 -0
- package/tests/hall_street_inside.mjs +4 -2
- package/tests/rank_tier_single_source.mjs +5 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// modules/hall-ui/public/street-amend.js — CHANGE HOW THIS WORKS: the drafted
|
|
2
|
+
// amendment a building's interior opens (task 1004530, goal 1000125).
|
|
3
|
+
//
|
|
4
|
+
// From any building, "change how this works" drafts an amendment to that
|
|
5
|
+
// building's row of the who-decides-what table (GET /government/constitution →
|
|
6
|
+
// decision_rules.areas[]), prefilled with the rule in force and a proposed one,
|
|
7
|
+
// shown as struck and inserted text. The governor picks who and which rule —
|
|
8
|
+
// from the vocabulary only (the server's rules list and the rank ladder, never
|
|
9
|
+
// free text) — adds a reason, and files it. A filed amendment is a sitting on
|
|
10
|
+
// the Charter, voted through the matter card (street-card.js). There is no
|
|
11
|
+
// settings page: this draft is the only way in, and the board is the only way
|
|
12
|
+
// it takes effect.
|
|
13
|
+
//
|
|
14
|
+
// WHERE IT GOES. A building's row → POST /government/board/decision-rules
|
|
15
|
+
// { rules: [{ area, decided_by, rule }], rationale_md }. The Charter's own row
|
|
16
|
+
// is the board itself, so the Charter's draft is a constitution amendment of
|
|
17
|
+
// the board's members and pass rule → POST /government/board/items (the live
|
|
18
|
+
// board block, those two fields changed). The server's permission checks
|
|
19
|
+
// decide who may file; a refusal is said in plain words (refusal()).
|
|
20
|
+
//
|
|
21
|
+
// THE WORDS. The struck and inserted text is street-card.js's own change() run
|
|
22
|
+
// over the amendment exactly as the server will store it, so the card the
|
|
23
|
+
// council votes on shows the words the governor drafted, by construction.
|
|
24
|
+
//
|
|
25
|
+
// PURE: no DOM, no fetch, no clock. street.js draws what this returns.
|
|
26
|
+
|
|
27
|
+
(function (root) {
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
// The standard rank ladder, lowest first (held equal to the server's
|
|
31
|
+
// membership-predicate RANK_ORDER by test).
|
|
32
|
+
var RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
33
|
+
// The table's words for the board's pass rules, and back (decision-rules.js
|
|
34
|
+
// PASS_RULE_TO_DECISION_RULE).
|
|
35
|
+
var TO_PASS = { first: 'first_ratifier', consent: 'consent', majority: 'majority', unanimous: 'unanimous' };
|
|
36
|
+
var FROM_PASS = { first_ratifier: 'first', consent: 'consent', majority: 'majority', unanimous: 'unanimous' };
|
|
37
|
+
var BOARD_FIELDS = ['membership', 'pass_rule', 'window_minutes', 'close_early_on_full_turnout'];
|
|
38
|
+
var REASON_MAX = 5000;
|
|
39
|
+
|
|
40
|
+
function str(v) { return v == null ? '' : String(v); }
|
|
41
|
+
function card() { return root.GovMatterCard || null; }
|
|
42
|
+
function memberWords(p) { var C = card(); return C ? C.memberWords(p) : str(p); }
|
|
43
|
+
function isPredicate(p) { return /^rank:[a-z0-9,+-]+$/.test(str(p)); }
|
|
44
|
+
|
|
45
|
+
function areasOf(constitution) {
|
|
46
|
+
var d = constitution && constitution.decision_rules;
|
|
47
|
+
return d && Array.isArray(d.areas) ? d.areas : null;
|
|
48
|
+
}
|
|
49
|
+
function vocabOf(constitution) {
|
|
50
|
+
var d = constitution && constitution.decision_rules;
|
|
51
|
+
return d && Array.isArray(d.rules) ? d.rules.filter(function (r) { return r && r.key; }) : [];
|
|
52
|
+
}
|
|
53
|
+
function rowOf(constitution, area) {
|
|
54
|
+
var as = areasOf(constitution) || [];
|
|
55
|
+
for (var i = 0; i < as.length; i++) if (as[i] && as[i].area === area) return as[i];
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Who may decide here: as today, the board's own members, a step of the
|
|
60
|
+
// rank ladder, whoever decides now, or no one. Values only, never free text.
|
|
61
|
+
function whoOptions(area, row, board) {
|
|
62
|
+
var out = [], seen = {};
|
|
63
|
+
function add(value, words) { if (!value || seen[value]) return; seen[value] = true; out.push({ value: value, words: words }); }
|
|
64
|
+
if (area !== 'charter') add('gate', 'as today: ' + str(row && row.today));
|
|
65
|
+
if (board && isPredicate(board.membership)) add(board.membership, 'the board: ' + memberWords(board.membership));
|
|
66
|
+
// the top of the ladder alone is exactly that rank (rank:archon+ is rank:archon)
|
|
67
|
+
RANKS.forEach(function (k, i) { var v = 'rank:' + k + (i === RANKS.length - 1 ? '' : '+'); add(v, memberWords(v)); });
|
|
68
|
+
var cur = area === 'charter' ? board && board.membership : row && row.decided_by;
|
|
69
|
+
if (isPredicate(cur)) add(cur, memberWords(cur));
|
|
70
|
+
if (area !== 'charter') add('nobody', 'no one: the building stands empty');
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Which rules fit who decides: as today decides alone (no rule to pick), no
|
|
75
|
+
// one is vacant, the charter takes the board's four pass rules, a sitting any
|
|
76
|
+
// rule but vacant. Drawn from the server's vocabulary only.
|
|
77
|
+
function ruleOptions(area, decidedBy, vocab) {
|
|
78
|
+
if (decidedBy === 'gate' || decidedBy === 'nobody') return [];
|
|
79
|
+
return (vocab || []).filter(function (r) {
|
|
80
|
+
if (area === 'charter') return Object.prototype.hasOwnProperty.call(TO_PASS, r.key);
|
|
81
|
+
return r.key !== 'vacant';
|
|
82
|
+
}).map(function (r) { return { value: r.key, words: str(r.words) }; });
|
|
83
|
+
}
|
|
84
|
+
function fixedRule(decidedBy) { return decidedBy === 'gate' ? null : decidedBy === 'nobody' ? 'vacant' : undefined; }
|
|
85
|
+
function hasValue(list, v) { for (var i = 0; i < list.length; i++) if (list[i].value === v) return true; return false; }
|
|
86
|
+
|
|
87
|
+
// The draft for one building, or { ok: false, reason } when the table could
|
|
88
|
+
// not be read. The proposal starts as the most likely change: a building
|
|
89
|
+
// decided as today goes to the board's members by consent; one already
|
|
90
|
+
// changed (or empty) goes back to as today; the charter changes its rule.
|
|
91
|
+
function draftFor(area, constitution) {
|
|
92
|
+
if (!areasOf(constitution)) return { ok: false, reason: 'unread' };
|
|
93
|
+
var row = rowOf(constitution, area);
|
|
94
|
+
if (!row) return { ok: false, reason: 'unknown_area' };
|
|
95
|
+
var vocab = vocabOf(constitution);
|
|
96
|
+
if (!vocab.length) return { ok: false, reason: 'unread' };
|
|
97
|
+
var board = constitution.board || null;
|
|
98
|
+
var charter = area === 'charter';
|
|
99
|
+
if (charter && (!board || !isPredicate(board.membership))) return { ok: false, reason: 'unread' };
|
|
100
|
+
var d = {
|
|
101
|
+
ok: true, area: area, name: str(row.name), charter: charter, row: row, board: board, vocab: vocab,
|
|
102
|
+
current: charter
|
|
103
|
+
? { decided_by: board.membership, rule: FROM_PASS[board.pass_rule] || null }
|
|
104
|
+
: { decided_by: row.decided_by, rule: row.decided_by === 'gate' ? null : row.rule },
|
|
105
|
+
decided_by: null, rule: null, reason: '',
|
|
106
|
+
};
|
|
107
|
+
if (charter) {
|
|
108
|
+
d.decided_by = board.membership;
|
|
109
|
+
d.rule = d.current.rule === 'consent' ? 'majority' : 'consent';
|
|
110
|
+
} else if (row.as_today) {
|
|
111
|
+
d.decided_by = board && isPredicate(board.membership) ? board.membership : 'rank:metic+';
|
|
112
|
+
d.rule = 'consent';
|
|
113
|
+
} else {
|
|
114
|
+
d.decided_by = 'gate';
|
|
115
|
+
d.rule = null;
|
|
116
|
+
}
|
|
117
|
+
return settle(d);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Keep a draft inside the vocabulary after a pick: the rule follows who.
|
|
121
|
+
function settle(d) {
|
|
122
|
+
var who = whoOptions(d.area, d.row, d.board);
|
|
123
|
+
if (!hasValue(who, d.decided_by)) d.decided_by = who.length ? who[0].value : null;
|
|
124
|
+
var fixed = fixedRule(d.decided_by);
|
|
125
|
+
var rules = ruleOptions(d.area, d.decided_by, d.vocab);
|
|
126
|
+
if (fixed !== undefined) d.rule = fixed;
|
|
127
|
+
else if (!hasValue(rules, d.rule)) d.rule = hasValue(rules, d.current.rule) ? d.current.rule : rules.length ? rules[0].value : null;
|
|
128
|
+
d.whoOptions = who;
|
|
129
|
+
d.ruleOptions = rules;
|
|
130
|
+
return d;
|
|
131
|
+
}
|
|
132
|
+
function copy(d) { var o = {}; for (var k in d) if (Object.prototype.hasOwnProperty.call(d, k)) o[k] = d[k]; return o; }
|
|
133
|
+
|
|
134
|
+
// One pick: field 'who' | 'rule' | 'reason'. A value outside the vocabulary
|
|
135
|
+
// is refused (the draft comes back unchanged).
|
|
136
|
+
function pick(d, field, value) {
|
|
137
|
+
if (!d || !d.ok) return d;
|
|
138
|
+
var n = copy(d);
|
|
139
|
+
if (field === 'who') { if (!hasValue(d.whoOptions, value)) return d; n.decided_by = value; }
|
|
140
|
+
else if (field === 'rule') { if (!hasValue(d.ruleOptions, value)) return d; n.rule = value; }
|
|
141
|
+
else if (field === 'reason') { n.reason = str(value).slice(0, REASON_MAX); return n; }
|
|
142
|
+
else return d;
|
|
143
|
+
return settle(n);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function changed(d) {
|
|
147
|
+
return !!(d && d.ok && (d.decided_by !== d.current.decided_by || (d.rule || null) !== (d.current.rule || null)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The amendment as the server will store it, shaped as a board item, so
|
|
151
|
+
// street-card.js's change() reads it exactly as the card will.
|
|
152
|
+
function ruleRow(d) {
|
|
153
|
+
var r = { area: d.area, decided_by: d.decided_by };
|
|
154
|
+
if (d.decided_by !== 'gate') r.rule = d.rule;
|
|
155
|
+
return r;
|
|
156
|
+
}
|
|
157
|
+
function proposal(d) {
|
|
158
|
+
var p = {};
|
|
159
|
+
BOARD_FIELDS.forEach(function (f) { p[f] = d.board[f] === undefined ? null : d.board[f]; });
|
|
160
|
+
p.membership = d.decided_by;
|
|
161
|
+
p.pass_rule = TO_PASS[d.rule] || d.board.pass_rule;
|
|
162
|
+
if (typeof p.close_early_on_full_turnout !== 'boolean') p.close_early_on_full_turnout = false;
|
|
163
|
+
return p;
|
|
164
|
+
}
|
|
165
|
+
function asItem(d) {
|
|
166
|
+
if (d.charter) return { subject_type: 'constitutional_amendment', subject: { kind: 'constitution', proposed: proposal(d) } };
|
|
167
|
+
return { subject_type: 'constitutional_amendment', subject: { kind: 'decision_rules', proposed: { decision_rules: [ruleRow(d)] } } };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The struck and inserted rows: [{ label, struck, inserted }].
|
|
171
|
+
function change(d) {
|
|
172
|
+
if (!changed(d)) return [];
|
|
173
|
+
var C = card();
|
|
174
|
+
if (!C || !C.change) return [];
|
|
175
|
+
var docket = { buildings: [{ key: d.area, decision: d.row }] };
|
|
176
|
+
var ch = C.change(asItem(d), docket, { board: d.board });
|
|
177
|
+
return ch && Array.isArray(ch.rows) ? ch.rows : [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Why the draft cannot be filed yet, in words, or '' when it can.
|
|
181
|
+
function check(d) {
|
|
182
|
+
if (!d || !d.ok) return 'How this building decides could not be read just now.';
|
|
183
|
+
if (!changed(d)) return 'That is how it works today. Pick a different way to change it.';
|
|
184
|
+
if (!str(d.reason).trim()) return 'Say why, so the council knows what it is voting on.';
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The request that files it (the server's permission checks decide).
|
|
189
|
+
function request(d) {
|
|
190
|
+
if (check(d)) return null;
|
|
191
|
+
var api = (card() && card().API) || '/api/bongos';
|
|
192
|
+
var why = str(d.reason).trim();
|
|
193
|
+
if (d.charter) return { method: 'POST', url: api + '/government/board/items', body: { subject_type: 'constitutional_amendment', proposal: proposal(d), rationale_md: why } };
|
|
194
|
+
return { method: 'POST', url: api + '/government/board/decision-rules', body: { rules: [ruleRow(d)], rationale_md: why } };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The server's refusal, plainly.
|
|
198
|
+
var REASONS = {
|
|
199
|
+
unknown_rank: 'There is no such rank yet, so that would seat no one.',
|
|
200
|
+
charter_is_the_board: 'The charter is decided by the board itself. Change it from the Charter.',
|
|
201
|
+
only_the_charter_is_the_board: 'Only the charter is decided by the board.',
|
|
202
|
+
bad_rule: 'That is not a way this project decides.',
|
|
203
|
+
bad_decided_by: 'That is not a seat this project knows.',
|
|
204
|
+
gate_takes_no_rule: 'As today, the building decides alone: there is no rule to pick.',
|
|
205
|
+
nobody_is_vacant: 'An empty building decides nothing.',
|
|
206
|
+
bad_membership: 'That is not a seat this project knows.',
|
|
207
|
+
bad_pass_rule: 'That is not a way the board can decide.',
|
|
208
|
+
};
|
|
209
|
+
function refusal(status, body) {
|
|
210
|
+
var e = body && body.error ? body.error : {};
|
|
211
|
+
var det = e.details || {};
|
|
212
|
+
if (e.code === 'proposer_would_lose_their_seat' && det.what_this_means) return str(det.what_this_means);
|
|
213
|
+
if (e.code === 'no_change') return 'That already is how it works. Nothing was filed.';
|
|
214
|
+
if (status === 401) return 'Sign in to file an amendment.';
|
|
215
|
+
if (status === 403) return 'Filing an amendment is not yours to do here. The server refused it.';
|
|
216
|
+
if ((e.code === 'invalid_decision_rules' || e.code === 'invalid_proposal') && det.reason) {
|
|
217
|
+
return REASONS[det.reason] || 'The server refused it: ' + str(det.reason).replace(/_/g, ' ') + '.';
|
|
218
|
+
}
|
|
219
|
+
var C = card();
|
|
220
|
+
return C ? C.refusal(status, body) : 'It could not be filed just now.';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// What the street says once it is filed, and which sitting to mark.
|
|
224
|
+
function answer(d, body) {
|
|
225
|
+
var item = body && body.item;
|
|
226
|
+
return {
|
|
227
|
+
toast: 'Filed. Your amendment to ' + (d && d.name ? d.name : 'this building') + ' is on the Charter for the council to vote on.',
|
|
228
|
+
building: 'charter',
|
|
229
|
+
matter: item && item.id != null ? 'board:' + item.id : null,
|
|
230
|
+
kind: 'light',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
var api = {
|
|
235
|
+
RANKS: RANKS, draftFor: draftFor, pick: pick, changed: changed, change: change,
|
|
236
|
+
check: check, request: request, refusal: refusal, answer: answer, whoOptions: whoOptions, ruleOptions: ruleOptions,
|
|
237
|
+
};
|
|
238
|
+
root.GovAmend = api;
|
|
239
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
240
|
+
}(typeof window !== 'undefined' ? window : globalThis));
|
|
@@ -195,7 +195,8 @@
|
|
|
195
195
|
return {
|
|
196
196
|
label: BUILDING_NAMES[r.area] || str(r.area),
|
|
197
197
|
struck: today ? [today.who, today.rule_words].filter(Boolean).join(', ') : '',
|
|
198
|
-
|
|
198
|
+
// as today and no one say it all in their seat's words (task 1004530)
|
|
199
|
+
inserted: [r.who || memberWords(r.decided_by), r.decided_by === 'gate' || r.decided_by === 'nobody' ? '' : r.rule ? RULE_WORDS[r.rule] || r.rule : ''].filter(Boolean).join(', '),
|
|
199
200
|
};
|
|
200
201
|
});
|
|
201
202
|
return rows.length ? { rows: rows } : null;
|
|
@@ -691,6 +692,8 @@
|
|
|
691
692
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
692
693
|
model: model, request: request, check: check, answer: answer, refusal: refusal, render: render,
|
|
693
694
|
memberWords: memberWords, windowWords: windowWords,
|
|
695
|
+
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
696
|
+
change: changeOf,
|
|
694
697
|
};
|
|
695
698
|
root.GovMatterCard = api;
|
|
696
699
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
@@ -240,6 +240,13 @@ html:has(body[data-page="street"]) { height: 100%; }
|
|
|
240
240
|
.inside-glyph path + path { fill: none; stroke: var(--ink); stroke-width: 1.4; }
|
|
241
241
|
.inside-textbtn { background: none; border: 0; padding: 6px 0; min-height: 24px; text-decoration: underline; text-underline-offset: 4px; font-family: var(--font-body); font-size: 14px; cursor: pointer; text-align: left; color: var(--street-accent-text); flex: none; }
|
|
242
242
|
.inside-note { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
|
|
243
|
+
/* change how this works: the drafted amendment (task 1004530) — the struck and
|
|
244
|
+
inserted words, then who, how and why, on the card's own form pieces */
|
|
245
|
+
.inside-draft { display: flex; flex-direction: column; gap: 12px; padding: 14px 0 4px; border-top: 1px solid var(--street-rule); }
|
|
246
|
+
.inside-draft[hidden] { display: none; }
|
|
247
|
+
.inside-draft__change { gap: 6px; }
|
|
248
|
+
.inside-draft__picks { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
249
|
+
.inside-draft__fixed { font-size: 14px; line-height: 1.45; color: var(--ink-soft); min-height: 36px; display: flex; align-items: center; }
|
|
243
250
|
.inside-meter { display: flex; flex-direction: column; gap: 6px; }
|
|
244
251
|
.inside-meter__track { height: 6px; border-radius: 999px; background: var(--street-wash); overflow: hidden; }
|
|
245
252
|
.inside-meter__fill { height: 6px; width: 0; background: color-mix(in srgb, var(--ink) 30%, var(--bg)); }
|
|
@@ -284,6 +291,7 @@ html:has(body[data-page="street"]) { height: 100%; }
|
|
|
284
291
|
|
|
285
292
|
/* narrow: the drawing in its own sideways strip, the panel below it */
|
|
286
293
|
@media (max-width: 899px) {
|
|
294
|
+
.inside-draft__picks { grid-template-columns: 1fr; }
|
|
287
295
|
.inside { display: block; overflow-x: hidden; overflow-y: auto; }
|
|
288
296
|
.inside-sizer { width: auto !important; height: auto !important; margin: 0; overflow: visible; }
|
|
289
297
|
.inside-board { position: static; width: auto; height: auto; transform: none; display: flex; flex-direction: column; gap: 16px; padding: 8px 16px 24px; }
|
|
@@ -138,9 +138,10 @@
|
|
|
138
138
|
<svg class="inside-glyph" width="18" height="18" aria-hidden="true" focusable="false"><path id="insideGlyphF" d=""></path><path id="insideGlyphL" d=""></path></svg>
|
|
139
139
|
<span class="inside-gov__text" id="insideHow"></span>
|
|
140
140
|
<span class="inside-gov__fill"></span>
|
|
141
|
-
<button type="button" class="inside-textbtn" id="insideAmend" aria-
|
|
141
|
+
<button type="button" class="inside-textbtn" id="insideAmend" aria-expanded="false" aria-controls="insideDraft">change how this works</button>
|
|
142
142
|
</div>
|
|
143
143
|
<p class="inside-note" id="insideAmendNote" role="status" hidden></p>
|
|
144
|
+
<div class="inside-draft" id="insideDraft" hidden></div>
|
|
144
145
|
<div class="inside-meter">
|
|
145
146
|
<div class="inside-meter__track"><div class="inside-meter__fill" id="insideFill"></div></div>
|
|
146
147
|
<span class="inside-meter__line" id="insideGrowth"></span>
|
|
@@ -188,8 +189,9 @@
|
|
|
188
189
|
|
|
189
190
|
<script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
|
|
190
191
|
<script src="/builders/street-draw.js?v=2026-10-02-street1"></script>
|
|
191
|
-
<script src="/builders/street-card.js?v=2026-10-02-
|
|
192
|
+
<script src="/builders/street-card.js?v=2026-10-02-amend1"></script>
|
|
192
193
|
<script src="/builders/street-inside.js?v=2026-10-02-inside1"></script>
|
|
193
|
-
<script src="/builders/street.js?v=2026-10-02-
|
|
194
|
+
<script src="/builders/street-amend.js?v=2026-10-02-amend1"></script>
|
|
195
|
+
<script src="/builders/street.js?v=2026-10-02-amend1"></script>
|
|
194
196
|
</body>
|
|
195
197
|
</html>
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
function drawInside(h, mode) {
|
|
363
363
|
var box = $('streetInside');
|
|
364
364
|
var same = view.inside && view.inside.kind === h.kind && view.inside.key === h.key;
|
|
365
|
-
if (!same) view.inside = { kind: h.kind, key: h.key, floor: null, drawer: null, picks: 0,
|
|
365
|
+
if (!same) view.inside = { kind: h.kind, key: h.key, floor: null, drawer: null, picks: 0, draft: null };
|
|
366
366
|
var arriving = box.hidden;
|
|
367
367
|
box.hidden = false;
|
|
368
368
|
box.setAttribute('data-kind', h.kind);
|
|
@@ -448,8 +448,7 @@
|
|
|
448
448
|
if (gf) gf.setAttribute('d', bv.gf || '');
|
|
449
449
|
if (gl) gl.setAttribute('d', bv.gl || '');
|
|
450
450
|
setText('insideHow', bv.howText);
|
|
451
|
-
|
|
452
|
-
if (note) { note.textContent = st.note ? AMEND_NOTE : ''; note.hidden = !st.note; }
|
|
451
|
+
drawDraft(false);
|
|
453
452
|
var fill = $('insideFill');
|
|
454
453
|
if (fill) { fill.style.width = bv.fill + '%'; fill.className = 'inside-meter__fill' + (bv.fill >= 80 ? ' inside-meter__fill--full' : ''); }
|
|
455
454
|
setText('insideGrowth', bv.growth);
|
|
@@ -528,14 +527,167 @@
|
|
|
528
527
|
for (var k = 0; k < kids.length; k++) if (kids[k].getAttribute('data-floor') === String(i)) { if (kids[k].focus) kids[k].focus(); break; }
|
|
529
528
|
}
|
|
530
529
|
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
530
|
+
// ---------- change how this works (task 1004530) ----------
|
|
531
|
+
// The button opens a drafted amendment to this building's row of the
|
|
532
|
+
// who-decides-what table: GovAmend (street-amend.js) builds the draft from the
|
|
533
|
+
// constitution as it stands (read fresh, so the struck words are the rule in
|
|
534
|
+
// force), this draws it — the struck and inserted words, who, which rule, why
|
|
535
|
+
// — and files it. The server's permission checks decide; a refusal is said in
|
|
536
|
+
// the draft, plainly. Filed, the street answers: a toast, back to the street,
|
|
537
|
+
// the Charter lights and its new pin settles. No settings page anywhere.
|
|
534
538
|
function amend() {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
+
var st = view.inside;
|
|
540
|
+
if (!st || st.kind !== 'building') return Promise.resolve(false);
|
|
541
|
+
if (st.draft) { closeDraft(); return Promise.resolve(false); }
|
|
542
|
+
var dr = st.draft = { d: null, error: '', busy: false, reading: true, rev: 1, drawn: 0 };
|
|
543
|
+
drawDraft(true);
|
|
544
|
+
return getJson(FEEDS.constitution).then(function (c) {
|
|
545
|
+
if (view.inside !== st || st.draft !== dr) return false;
|
|
546
|
+
if (c && c.board) data.constitution = c;
|
|
547
|
+
var A = window.GovAmend;
|
|
548
|
+
dr.reading = false;
|
|
549
|
+
dr.d = A ? A.draftFor(st.key, c && c.board ? c : null) : { ok: false, reason: 'unread' };
|
|
550
|
+
dr.rev++;
|
|
551
|
+
drawDraft(true);
|
|
552
|
+
var who = $('insideDraftWho');
|
|
553
|
+
if (who && who.focus) who.focus();
|
|
554
|
+
return !!(dr.d && dr.d.ok);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function closeDraft() {
|
|
558
|
+
var st = view.inside;
|
|
559
|
+
if (!st || !st.draft) return;
|
|
560
|
+
st.draft = null;
|
|
561
|
+
drawDraft(true);
|
|
562
|
+
var am = $('insideAmend');
|
|
563
|
+
if (am && am.focus) am.focus();
|
|
564
|
+
}
|
|
565
|
+
function bumpDraft(focusId) {
|
|
566
|
+
var dr = view.inside && view.inside.draft;
|
|
567
|
+
if (!dr) return;
|
|
568
|
+
dr.rev++;
|
|
569
|
+
drawDraft(true);
|
|
570
|
+
var el = focusId ? $(focusId) : null;
|
|
571
|
+
if (el && el.focus) el.focus();
|
|
572
|
+
}
|
|
573
|
+
function draftSelect(id, list, value, onPick) {
|
|
574
|
+
var sel = node('select', 'mcard-form__select');
|
|
575
|
+
sel.setAttribute('id', id);
|
|
576
|
+
list.forEach(function (o) {
|
|
577
|
+
var op = node('option', '', o.words);
|
|
578
|
+
op.setAttribute('value', o.value);
|
|
579
|
+
if (o.value === value) op.setAttribute('selected', '');
|
|
580
|
+
sel.appendChild(op);
|
|
581
|
+
});
|
|
582
|
+
sel.value = value == null ? '' : value;
|
|
583
|
+
sel.addEventListener('change', function () { onPick(sel.value); });
|
|
584
|
+
return sel;
|
|
585
|
+
}
|
|
586
|
+
function draftField(labelText, forId, control) {
|
|
587
|
+
var f = node('div', 'mcard-form');
|
|
588
|
+
var l = node(forId ? 'label' : 'span', 'mcard-form__label', labelText);
|
|
589
|
+
if (forId) l.setAttribute('for', forId);
|
|
590
|
+
f.appendChild(l);
|
|
591
|
+
f.appendChild(control);
|
|
592
|
+
return f;
|
|
593
|
+
}
|
|
594
|
+
// Draw the draft into #insideDraft. A redraw of the building leaves an
|
|
595
|
+
// unchanged draft alone (force false), so a feed landing mid-sentence never
|
|
596
|
+
// takes the governor's place in the form.
|
|
597
|
+
function drawDraft(force) {
|
|
598
|
+
var st = view.inside, box = $('insideDraft'), note = $('insideAmendNote'), am = $('insideAmend');
|
|
599
|
+
var dr = st && st.draft;
|
|
600
|
+
if (am) am.setAttribute('aria-expanded', dr ? 'true' : 'false');
|
|
601
|
+
if (note) {
|
|
602
|
+
var said = !dr ? '' : dr.reading ? 'Reading how this building decides…' : dr.d && dr.d.ok ? '' : 'How this building decides could not be read just now.';
|
|
603
|
+
note.textContent = said;
|
|
604
|
+
note.hidden = !said;
|
|
605
|
+
}
|
|
606
|
+
if (!box) return;
|
|
607
|
+
if (!dr || !dr.d || !dr.d.ok) { box.hidden = true; clear(box); return; }
|
|
608
|
+
if (!force && dr.drawn === dr.rev) return;
|
|
609
|
+
dr.drawn = dr.rev;
|
|
610
|
+
var A = window.GovAmend, d = dr.d;
|
|
611
|
+
clear(box);
|
|
612
|
+
box.hidden = false;
|
|
613
|
+
box.appendChild(node('span', 'street-label', 'drafted amendment · ' + d.name));
|
|
614
|
+
var rows = A.change(d);
|
|
615
|
+
var ch = node('div', 'mcard-change inside-draft__change');
|
|
616
|
+
if (rows.length) {
|
|
617
|
+
rows.forEach(function (r) {
|
|
618
|
+
var p = node('p', 'mcard-change__row');
|
|
619
|
+
p.appendChild(node('span', 'mcard-change__label', r.label));
|
|
620
|
+
if (r.struck) p.appendChild(node('del', 'mcard-del', r.struck));
|
|
621
|
+
p.appendChild(node('ins', 'mcard-ins', r.inserted));
|
|
622
|
+
ch.appendChild(p);
|
|
623
|
+
});
|
|
624
|
+
} else ch.appendChild(node('p', 'inside-note', 'This is how it works today. Pick who decides, or how, to change it.'));
|
|
625
|
+
box.appendChild(ch);
|
|
626
|
+
var picks = node('div', 'inside-draft__picks');
|
|
627
|
+
picks.appendChild(draftField('who decides', 'insideDraftWho', draftSelect('insideDraftWho', d.whoOptions, d.decided_by, function (v) {
|
|
628
|
+
dr.d = A.pick(dr.d, 'who', v); dr.error = ''; bumpDraft('insideDraftWho');
|
|
629
|
+
})));
|
|
630
|
+
if (d.ruleOptions.length) {
|
|
631
|
+
picks.appendChild(draftField('how they decide', 'insideDraftRule', draftSelect('insideDraftRule', d.ruleOptions, d.rule, function (v) {
|
|
632
|
+
dr.d = A.pick(dr.d, 'rule', v); dr.error = ''; bumpDraft('insideDraftRule');
|
|
633
|
+
})));
|
|
634
|
+
} else {
|
|
635
|
+
picks.appendChild(draftField('how they decide', null, node('span', 'inside-draft__fixed', d.decided_by === 'nobody' ? 'nothing is decided until someone holds it' : 'decides alone, as today')));
|
|
636
|
+
}
|
|
637
|
+
box.appendChild(picks);
|
|
638
|
+
var why = node('textarea', 'mcard-form__text');
|
|
639
|
+
why.setAttribute('id', 'insideDraftWhy');
|
|
640
|
+
why.setAttribute('rows', '3');
|
|
641
|
+
why.setAttribute('maxlength', '5000');
|
|
642
|
+
why.value = d.reason;
|
|
643
|
+
why.addEventListener('input', function () { dr.d = A.pick(dr.d, 'reason', why.value); });
|
|
644
|
+
box.appendChild(draftField('why', 'insideDraftWhy', why));
|
|
645
|
+
box.appendChild(node('p', 'inside-note', 'Filed, it goes on the Charter and the council votes on it there.'));
|
|
646
|
+
if (dr.error) {
|
|
647
|
+
var err = node('p', 'mcard-error', dr.error);
|
|
648
|
+
err.setAttribute('role', 'alert');
|
|
649
|
+
err.setAttribute('id', 'insideDraftError');
|
|
650
|
+
box.appendChild(err);
|
|
651
|
+
}
|
|
652
|
+
var acts = node('div', 'mcard-actions');
|
|
653
|
+
var file = node('button', 'mcard-btn mcard-btn--primary', dr.busy ? 'filing…' : 'file the amendment');
|
|
654
|
+
file.setAttribute('type', 'button');
|
|
655
|
+
file.setAttribute('id', 'insideDraftFile');
|
|
656
|
+
if (dr.busy) file.setAttribute('disabled', '');
|
|
657
|
+
file.addEventListener('click', fileDraft);
|
|
658
|
+
acts.appendChild(file);
|
|
659
|
+
var not = node('button', 'mcard-btn mcard-btn--quiet', 'not now');
|
|
660
|
+
not.setAttribute('type', 'button');
|
|
661
|
+
not.addEventListener('click', closeDraft);
|
|
662
|
+
acts.appendChild(not);
|
|
663
|
+
box.appendChild(acts);
|
|
664
|
+
}
|
|
665
|
+
function fileDraft() {
|
|
666
|
+
var A = window.GovAmend, st = view.inside, dr = st && st.draft;
|
|
667
|
+
if (!A || !dr || !dr.d || dr.busy) return Promise.resolve(false);
|
|
668
|
+
var why = $('insideDraftWhy');
|
|
669
|
+
if (why && why.value != null) dr.d = A.pick(dr.d, 'reason', why.value);
|
|
670
|
+
var refused = A.check(dr.d);
|
|
671
|
+
if (refused) { dr.error = refused; bumpDraft(refused.indexOf('Say why') === 0 ? 'insideDraftWhy' : 'insideDraftWho'); return Promise.resolve(false); }
|
|
672
|
+
dr.error = '';
|
|
673
|
+
dr.busy = true;
|
|
674
|
+
bumpDraft(null);
|
|
675
|
+
return send(A.request(dr.d)).then(function (out) {
|
|
676
|
+
dr.busy = false;
|
|
677
|
+
if (!out.ok) {
|
|
678
|
+
dr.error = A.refusal(out.status, out.body);
|
|
679
|
+
if (view.inside === st && st.draft === dr) bumpDraft('insideDraftFile');
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
var a = A.answer(dr.d, out.body);
|
|
683
|
+
say(a.toast);
|
|
684
|
+
if (view.inside === st) st.draft = null;
|
|
685
|
+
card.answer = { building: a.building, matter: a.matter, kind: a.kind, at: Date.now() };
|
|
686
|
+
data.room = null;
|
|
687
|
+
data.constitution = null;
|
|
688
|
+
goStreet();
|
|
689
|
+
return load().then(function () { return true; });
|
|
690
|
+
});
|
|
539
691
|
}
|
|
540
692
|
|
|
541
693
|
function drawDistrict(mode) {
|
|
@@ -744,6 +896,7 @@
|
|
|
744
896
|
|
|
745
897
|
function onKey(e) {
|
|
746
898
|
if (e.key === 'Escape' && view.open) { e.preventDefault(); closeCard(true); }
|
|
899
|
+
else if (e.key === 'Escape' && view.inside && view.inside.draft) { e.preventDefault(); closeDraft(); }
|
|
747
900
|
}
|
|
748
901
|
|
|
749
902
|
function boot() {
|
|
@@ -767,6 +920,6 @@
|
|
|
767
920
|
return load();
|
|
768
921
|
}
|
|
769
922
|
|
|
770
|
-
window.GovStreetPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, actOn: actOn, pickFloor: pickFloor, goStreet: goStreet, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
|
|
923
|
+
window.GovStreetPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, actOn: actOn, pickFloor: pickFloor, goStreet: goStreet, amend: amend, fileDraft: fileDraft, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
|
|
771
924
|
window.GovStreetPage.ready = boot();
|
|
772
925
|
}());
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
"page": "/builders/street",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
5
|
"stub": {
|
|
6
|
-
"prefix": "/builders/"
|
|
6
|
+
"prefix": "/builders/",
|
|
7
|
+
"fixtures": {
|
|
8
|
+
"government-constitution": "government-constitution-rules"
|
|
9
|
+
}
|
|
7
10
|
},
|
|
8
11
|
"modeQuery": false,
|
|
9
12
|
"ignoreRequests": [
|
|
@@ -311,8 +314,8 @@
|
|
|
311
314
|
},
|
|
312
315
|
"amend": {
|
|
313
316
|
"auth": true,
|
|
314
|
-
"_": "Change how this works pressed:
|
|
315
|
-
"url": "/builders/street#building=
|
|
317
|
+
"_": "Change how this works pressed inside Review: the drafted amendment to its row, the rule in force struck and the proposed one inserted, who and how to pick, why, and file (task 1004530).",
|
|
318
|
+
"url": "/builders/street#building=review",
|
|
316
319
|
"actions": [
|
|
317
320
|
[
|
|
318
321
|
"wait",
|
|
@@ -324,12 +327,16 @@
|
|
|
324
327
|
],
|
|
325
328
|
[
|
|
326
329
|
"wait",
|
|
327
|
-
|
|
330
|
+
600
|
|
328
331
|
]
|
|
329
332
|
],
|
|
330
333
|
"expect": {
|
|
331
334
|
"visible": [
|
|
332
|
-
"#
|
|
335
|
+
"#insideDraft",
|
|
336
|
+
"#insideDraftWho",
|
|
337
|
+
"#insideDraftRule",
|
|
338
|
+
"#insideDraftWhy",
|
|
339
|
+
"#insideDraftFile"
|
|
333
340
|
]
|
|
334
341
|
}
|
|
335
342
|
},
|