@bongos/core 1.21.40 → 1.21.42

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.
Files changed (80) hide show
  1. package/.bongos-core.json +136 -76
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +25 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/api/openapi.json +492 -3
  8. package/docs/api-reference.md +13 -1
  9. package/docs/copy-inventory.md +29 -12
  10. package/docs/copy-registry.json +177 -14
  11. package/docs/file-map.md +1 -0
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +4 -2
  14. package/docs/page-readings.json +574 -547
  15. package/modules/autonomy/governor-docket.js +6 -1
  16. package/modules/hall-ui/public/atlas.html +3 -3
  17. package/modules/hall-ui/public/blockers.html +3 -3
  18. package/modules/hall-ui/public/board-room.html +3 -3
  19. package/modules/hall-ui/public/city.html +3 -3
  20. package/modules/hall-ui/public/collab.html +3 -3
  21. package/modules/hall-ui/public/commands.html +3 -3
  22. package/modules/hall-ui/public/copy-desk.html +3 -3
  23. package/modules/hall-ui/public/craft-mode.js +62 -0
  24. package/modules/hall-ui/public/deploy.html +3 -3
  25. package/modules/hall-ui/public/diagrams.html +3 -3
  26. package/modules/hall-ui/public/drachmae.html +3 -3
  27. package/modules/hall-ui/public/fleet.html +3 -3
  28. package/modules/hall-ui/public/gate.html +3 -3
  29. package/modules/hall-ui/public/goal-map.html +3 -3
  30. package/modules/hall-ui/public/goals.html +3 -3
  31. package/modules/hall-ui/public/government.html +3 -3
  32. package/modules/hall-ui/public/idea.html +3 -3
  33. package/modules/hall-ui/public/ideas.html +3 -3
  34. package/modules/hall-ui/public/index.html +3 -3
  35. package/modules/hall-ui/public/modules.html +3 -3
  36. package/modules/hall-ui/public/page-notes.js +320 -0
  37. package/modules/hall-ui/public/primer.html +3 -3
  38. package/modules/hall-ui/public/profile.html +3 -3
  39. package/modules/hall-ui/public/project-settings.html +3 -3
  40. package/modules/hall-ui/public/ranks.html +3 -3
  41. package/modules/hall-ui/public/roadmap.html +3 -3
  42. package/modules/hall-ui/public/roster.html +3 -3
  43. package/modules/hall-ui/public/sessions.html +3 -3
  44. package/modules/hall-ui/public/settings.html +3 -3
  45. package/modules/hall-ui/public/street-card.css +13 -0
  46. package/modules/hall-ui/public/street-card.js +61 -9
  47. package/modules/hall-ui/public/street-card.states.json +72 -0
  48. package/modules/hall-ui/public/street.html +3 -3
  49. package/modules/hall-ui/public/studio.html +3 -3
  50. package/modules/hall-ui/public/style.css +144 -0
  51. package/modules/hall-ui/public/task.html +3 -3
  52. package/modules/hall-ui/public/thinking.html +3 -3
  53. package/modules/hall-ui/public/tweak-editor.html +3 -3
  54. package/modules/hall-ui/public/watch.html +3 -3
  55. package/modules/hall-ui/public/work.html +3 -3
  56. package/modules/hall-ui/records/governor-street.md +1 -0
  57. package/modules/hall-ui/records/page-notes.md +16 -0
  58. package/modules/page-notes/CLAUDE.md +15 -0
  59. package/modules/page-notes/db.js +147 -0
  60. package/modules/page-notes/migrations/page_notes_001_notes.sql +68 -0
  61. package/modules/page-notes/module.json +13 -0
  62. package/modules/page-notes/notes.js +174 -0
  63. package/modules/page-notes/routes/page-notes.js +172 -0
  64. package/modules/page-notes/split.js +68 -0
  65. package/modules/provisioning/starter-bundles.js +1 -0
  66. package/modules/ui-design/kit/serve.js +1 -1
  67. package/package-lock.json +2 -2
  68. package/package.json +1 -1
  69. package/release-notes.json +12 -0
  70. package/scripts/gds/publish-manifest.js +1 -0
  71. package/src/bongos/module-scope-map.js +4 -0
  72. package/src/bongos/routes.js +4 -1
  73. package/src/module-api.js +1 -1
  74. package/tests/governor_docket.mjs +3 -1
  75. package/tests/hall_matter_card.mjs +71 -0
  76. package/tests/hall_tweak_editor.mjs +1 -1
  77. package/tests/module_loader.mjs +1 -1
  78. package/tests/page_notes.mjs +289 -0
  79. package/tests/page_notes_db.mjs +140 -0
  80. package/tests/page_notes_panel.mjs +145 -0
@@ -0,0 +1,320 @@
1
+ // page-notes.js — the Artistry palette's feedback doc (task 1004496, goal
2
+ // 1000095 WA6). Loaded on demand by craft-mode.js: when the palette button is
3
+ // pressed (open) or a page arrives with ?note=N (openNote). Never on page load.
4
+ //
5
+ // A side sheet over the page: write a doc about THIS page (as long as it needs
6
+ // to be), add reference images, and "Turn into ideas" — the server's AI split
7
+ // files each point as its own idea under your name, and the doc stays as the
8
+ // page's reference. Below the writing area, the page's earlier docs; each opens
9
+ // in place. The draft saves as you type, so closing loses nothing.
10
+ //
11
+ // The walls are the server's (modules/page-notes): a draft is its author's, a
12
+ // submitted doc is any signed-in builder's. This file only draws.
13
+ (function () {
14
+ 'use strict';
15
+
16
+ var API = '/api/bongos/page-notes';
17
+ var IMAGE_RE = /!\[([^\]\n]{0,120})\]\(\/api\/bongos\/page-notes\/images\/(\d{1,18})\)/g;
18
+ var MAX_IMAGE_BYTES = 4 * 1024 * 1024;
19
+ var MAX_IMAGES = 8;
20
+ var TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
21
+
22
+ var sheet = null, returnFocus = null, state = null, saveTimer = null, pollTimer = null;
23
+
24
+ function esc(s) {
25
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
26
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
27
+ });
28
+ }
29
+ function page() {
30
+ var b = document.body || { dataset: {} };
31
+ var slug = String((b.dataset && b.dataset.page) || 'home').toLowerCase().replace(/[^a-z0-9_-]/g, '-').slice(0, 64) || 'home';
32
+ var title = (b.dataset && b.dataset.pageTitle) || String(document.title || '').split(/\s[—-]\s/)[0] || slug;
33
+ var path = String((window.location && window.location.pathname) || '/').replace(/[^A-Za-z0-9/_.-]/g, '').slice(0, 200) || '/';
34
+ return { id: 'builders:' + slug, path: path, title: title.slice(0, 120) };
35
+ }
36
+ function api(method, path, body) {
37
+ var opts = { method: method, credentials: 'same-origin', headers: {} };
38
+ if (body !== undefined) { opts.headers['content-type'] = 'application/json'; opts.body = JSON.stringify(body); }
39
+ return fetch(API + path, opts).then(function (r) {
40
+ return r.json().catch(function () { return {}; }).then(function (j) {
41
+ if (!r.ok) {
42
+ var e = new Error((j && j.error && j.error.message) || (r.status === 401 ? 'Sign in to write page feedback.' : 'That did not work. Try again in a moment.'));
43
+ e.status = r.status;
44
+ throw e;
45
+ }
46
+ return j;
47
+ });
48
+ });
49
+ }
50
+ function day(iso) {
51
+ var d = iso ? new Date(iso) : null;
52
+ return d && !isNaN(d) ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
53
+ }
54
+ function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many); }
55
+
56
+ // The doc, read: headings, lists, paragraphs, and only this module's own images.
57
+ function renderBody(md) {
58
+ var out = [], para = [], list = [];
59
+ function flushPara() { if (para.length) { out.push('<p>' + para.join('<br>') + '</p>'); para = []; } }
60
+ function flushList() { if (list.length) { out.push('<ul>' + list.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul>'); list = []; } }
61
+ function inline(line) {
62
+ return esc(line).replace(IMAGE_RE, function (_m, alt, id) {
63
+ return '<img class="pn-doc__img" src="' + API + '/images/' + id + '" alt="' + alt + '" loading="lazy">';
64
+ });
65
+ }
66
+ String(md || '').split('\n').forEach(function (raw) {
67
+ var line = raw.replace(/\s+$/, '');
68
+ var h = /^(#{1,3})\s+(.*)$/.exec(line);
69
+ var li = /^\s*[-*]\s+(.*)$/.exec(line);
70
+ if (!line.trim()) { flushPara(); flushList(); return; }
71
+ if (h) { flushPara(); flushList(); out.push('<h3 class="pn-doc__h">' + inline(h[2]) + '</h3>'); return; }
72
+ if (li) { flushPara(); list.push(inline(li[1])); return; }
73
+ flushList();
74
+ para.push(inline(line));
75
+ });
76
+ flushPara(); flushList();
77
+ return out.join('');
78
+ }
79
+
80
+ // ---------- the sheet ----------
81
+ function shellHtml(p) {
82
+ return '<div class="pn__panel" role="dialog" aria-modal="true" aria-labelledby="pn-title">' +
83
+ '<div class="pn__head">' +
84
+ '<p class="pn__eyebrow">Page feedback · ' + esc(p.title) + '</p>' +
85
+ '<h2 class="pn__title" id="pn-title">What should this page be?</h2>' +
86
+ '<button class="pn__close" type="button" data-pn-close aria-label="Close">' +
87
+ '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M5 5l8 8M13 5l-8 8"/></svg></button>' +
88
+ '</div>' +
89
+ '<div class="pn__body" id="pn-body"><p class="pn__quiet">Opening your doc…</p></div>' +
90
+ '</div>';
91
+ }
92
+ function writeHtml() {
93
+ var n = state.note, imgs = state.images;
94
+ return '<label class="pn__label" for="pn-text">Your feedback</label>' +
95
+ '<textarea class="pn__text" id="pn-text" rows="14" placeholder="Write what you see on this page and what it should be. Long is fine: each separate point becomes its own idea.">' + esc(n.body_md) + '</textarea>' +
96
+ '<div class="pn__tools">' +
97
+ '<label class="pn__add"><input type="file" id="pn-file" accept="' + TYPES.join(',') + '" multiple>Add images</label>' +
98
+ '<span class="pn__quiet" id="pn-count">' + imgs.length + ' of ' + MAX_IMAGES + ' images</span>' +
99
+ '<span class="pn__quiet pn__saved" id="pn-saved" aria-live="polite"></span>' +
100
+ '</div>' +
101
+ (imgs.length ? '<div class="pn__thumbs">' + imgs.map(function (i) {
102
+ return '<img class="pn__thumb" src="' + esc(i.url) + '" alt="' + esc(i.name || 'reference image') + '">';
103
+ }).join('') + '</div>' : '') +
104
+ '<p class="pn__error" id="pn-error" role="alert"></p>' +
105
+ '<div class="pn__foot">' +
106
+ '<p class="pn__hint">When you submit, an AI splits this into separate ideas, filed under your name. The doc stays here as this page\'s reference.</p>' +
107
+ '<button class="pn__submit" type="button" data-pn-submit>Turn into ideas</button>' +
108
+ '</div>' +
109
+ listHtml();
110
+ }
111
+ function listHtml() {
112
+ var rows = state.list || [];
113
+ return '<section class="pn__list" aria-labelledby="pn-list-h"><h3 class="pn__list-h" id="pn-list-h">This page\'s reference docs</h3>' +
114
+ (rows.length ? '<ul class="pn__rows">' + rows.map(function (r) {
115
+ return '<li><button class="pn__row" type="button" data-pn-note="' + esc(r.id) + '">' +
116
+ '<span class="pn__row-meta">' + esc(r.author_name) + ' · ' + esc(day(r.submitted_at)) + ' · ' +
117
+ (r.status === 'filed' ? esc(plural(r.idea_count, 'idea', 'ideas')) : 'splitting…') + '</span>' +
118
+ '<span class="pn__row-text">' + esc(String(r.excerpt || '').replace(IMAGE_RE, '[image]').replace(/\s+/g, ' ').slice(0, 160)) + '</span>' +
119
+ '</button></li>';
120
+ }).join('') + '</ul>' : '<p class="pn__quiet">None yet. Yours will be the first.</p>') +
121
+ '</section>';
122
+ }
123
+ function ideasHtml(ideas) {
124
+ if (!ideas || !ideas.length) return '';
125
+ return '<ul class="pn__ideas">' + ideas.map(function (i) {
126
+ return '<li><a href="/#/idea/' + esc(i.id) + '">' + esc(i.title) + '</a></li>';
127
+ }).join('') + '</ul>';
128
+ }
129
+ function readHtml(doc) {
130
+ var n = doc.note;
131
+ var filed = n.status === 'filed';
132
+ return '<button class="pn__back" type="button" data-pn-back>← ' + (state && state.note ? 'Back to your doc' : 'This page\'s docs') + '</button>' +
133
+ '<p class="pn__row-meta">' + esc(n.author_name) + ' · ' + esc(day(n.submitted_at)) + ' · ' + esc(n.page_title || n.page_id) + '</p>' +
134
+ '<div class="pn-doc">' + renderBody(n.body_md) + '</div>' +
135
+ (filed
136
+ ? '<h3 class="pn__list-h">It became ' + esc(plural(doc.ideas.length, 'idea', 'ideas')) + '</h3>' + ideasHtml(doc.ideas)
137
+ : '<p class="pn__quiet">Being split into ideas…</p>');
138
+ }
139
+ function doneHtml(doc) {
140
+ return '<p class="pn__done">Filed as ' + esc(plural(doc.ideas.length, 'idea', 'ideas')) + ' under your name.</p>' +
141
+ ideasHtml(doc.ideas) +
142
+ '<p class="pn__hint">The doc is now this page\'s reference. Every idea links back to it.</p>' +
143
+ '<button class="pn__back" type="button" data-pn-new>Write another</button>';
144
+ }
145
+
146
+ function body() { return sheet && sheet.querySelector('#pn-body'); }
147
+ function setError(msg) { var e = sheet && sheet.querySelector('#pn-error'); if (e) e.textContent = msg || ''; }
148
+ function setSaved(msg) { var e = sheet && sheet.querySelector('#pn-saved'); if (e) e.textContent = msg || ''; }
149
+
150
+ function setTitle(text) { var h = sheet && sheet.querySelector('#pn-title'); if (h) h.textContent = text; }
151
+ function showWrite() {
152
+ var b = body();
153
+ if (!b) return;
154
+ setTitle('What should this page be?');
155
+ b.innerHTML = writeHtml();
156
+ var t = b.querySelector('#pn-text');
157
+ if (t) { t.addEventListener('input', queueSave); t.focus(); }
158
+ var f = b.querySelector('#pn-file');
159
+ if (f) f.addEventListener('change', function () { addImages(Array.prototype.slice.call(f.files || [])); f.value = ''; });
160
+ }
161
+ function currentText() { var t = sheet && sheet.querySelector('#pn-text'); return t ? t.value : (state && state.note ? state.note.body_md : ''); }
162
+ function queueSave() {
163
+ setSaved('');
164
+ if (saveTimer) clearTimeout(saveTimer);
165
+ saveTimer = setTimeout(save, 700);
166
+ }
167
+ function save() {
168
+ if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }
169
+ if (!state || !state.note || state.note.status !== 'draft') return Promise.resolve();
170
+ var text = currentText();
171
+ if (text === state.note.body_md) return Promise.resolve();
172
+ return api('PATCH', '/' + state.note.id, { body_md: text }).then(function () {
173
+ state.note.body_md = text;
174
+ setSaved('Saved');
175
+ }, function (e) { setError(e.message); });
176
+ }
177
+ function readFile(file) {
178
+ return new Promise(function (resolve, reject) {
179
+ var r = new FileReader();
180
+ r.onload = function () { resolve(String(r.result || '')); };
181
+ r.onerror = function () { reject(new Error('That file could not be read.')); };
182
+ r.readAsDataURL(file);
183
+ });
184
+ }
185
+ function addImages(files) {
186
+ setError('');
187
+ var chain = Promise.resolve();
188
+ files.forEach(function (file) {
189
+ chain = chain.then(function () {
190
+ if (TYPES.indexOf(file.type) === -1) throw new Error('Images only: PNG, JPG, GIF or WebP.');
191
+ if (file.size > MAX_IMAGE_BYTES) throw new Error('Each image can be up to 4 MB.');
192
+ if (state.images.length >= MAX_IMAGES) throw new Error('A doc can carry up to ' + MAX_IMAGES + ' images.');
193
+ return readFile(file).then(function (data) {
194
+ return api('POST', '/' + state.note.id + '/images', { content_type: file.type, data: data, name: String(file.name || '').slice(0, 120) });
195
+ }).then(function (j) {
196
+ state.images.push(j.image);
197
+ var name = String(j.image.name || 'image').replace(/[\]\[()]/g, '');
198
+ var t = sheet.querySelector('#pn-text');
199
+ var text = t ? t.value : '';
200
+ var at = t && typeof t.selectionStart === 'number' ? t.selectionStart : text.length;
201
+ var md = (at && text[at - 1] !== '\n' ? '\n' : '') + '![' + name + '](' + j.image.url + ')\n';
202
+ state.note.body_md = text;
203
+ var next = text.slice(0, at) + md + text.slice(at);
204
+ return api('PATCH', '/' + state.note.id, { body_md: next }).then(function () {
205
+ state.note.body_md = next;
206
+ showWrite();
207
+ setSaved('Saved');
208
+ });
209
+ });
210
+ });
211
+ });
212
+ chain.catch(function (e) { setError(e.message); });
213
+ }
214
+ function submit() {
215
+ setError('');
216
+ var btn = sheet.querySelector('[data-pn-submit]');
217
+ if (btn) btn.disabled = true;
218
+ save().then(function () {
219
+ return api('POST', '/' + state.note.id + '/submit');
220
+ }).then(function () {
221
+ var b = body();
222
+ if (b) b.innerHTML = '<p class="pn__done" aria-live="polite">Splitting your feedback into ideas…</p>';
223
+ poll(state.note.id, 0);
224
+ }, function (e) { if (btn) btn.disabled = false; setError(e.message); });
225
+ }
226
+ function poll(id, tries) {
227
+ pollTimer = setTimeout(function () {
228
+ api('GET', '/' + id).then(function (doc) {
229
+ if (!sheet) return;
230
+ if (doc.note.status === 'filed') { state.note = null; body().innerHTML = doneHtml(doc); refreshList(); return; }
231
+ if (tries < 60) poll(id, tries + 1);
232
+ else body().innerHTML = '<p class="pn__quiet">Still splitting. It will appear in this page\'s docs when it is done.</p>';
233
+ }, function () { if (tries < 60) poll(id, tries + 1); });
234
+ }, tries ? 2000 : 1200);
235
+ }
236
+ function refreshList() {
237
+ return api('GET', '?page_id=' + encodeURIComponent(page().id)).then(function (j) { if (state) state.list = j.notes || []; }, function () { /* the list is a convenience */ });
238
+ }
239
+ function openDraft() {
240
+ var p = page();
241
+ return Promise.all([
242
+ api('POST', '', { page_id: p.id, page_path: p.path, page_title: p.title }),
243
+ refreshList(),
244
+ ]).then(function (r) {
245
+ state.note = r[0].note;
246
+ state.images = r[0].images || [];
247
+ showWrite();
248
+ }, function (e) {
249
+ var b = body();
250
+ if (b) b.innerHTML = '<p class="pn__error" role="alert">' + esc(e.message) + '</p>';
251
+ });
252
+ }
253
+ function showNote(id) {
254
+ var b = body();
255
+ setTitle('Feedback on this page');
256
+ if (b) b.innerHTML = '<p class="pn__quiet">Opening the doc…</p>';
257
+ return save().then(function () { return api('GET', '/' + id); }).then(function (doc) {
258
+ if (b) { b.innerHTML = readHtml(doc); var back = b.querySelector('[data-pn-back]'); if (back) back.focus(); }
259
+ }, function (e) {
260
+ if (b) b.innerHTML = '<p class="pn__error" role="alert">' + esc(e.message) + '</p><button class="pn__back" type="button" data-pn-back>← Back</button>';
261
+ });
262
+ }
263
+
264
+ function onClick(e) {
265
+ if (e.target === sheet) { close(); return; }
266
+ var btn = e.target.closest && e.target.closest('button');
267
+ if (!btn) return;
268
+ if (btn.hasAttribute('data-pn-close')) close();
269
+ else if (btn.hasAttribute('data-pn-submit')) submit();
270
+ else if (btn.hasAttribute('data-pn-note')) showNote(btn.getAttribute('data-pn-note'));
271
+ else if (btn.hasAttribute('data-pn-back') || btn.hasAttribute('data-pn-new')) {
272
+ if (state.note && !btn.hasAttribute('data-pn-new')) showWrite();
273
+ else { body().innerHTML = '<p class="pn__quiet">Opening your doc…</p>'; openDraft(); }
274
+ }
275
+ }
276
+ function onKey(e) {
277
+ if (!sheet) return;
278
+ if (e.key === 'Escape') { e.preventDefault(); close(); return; }
279
+ if (e.key !== 'Tab') return;
280
+ var f = Array.prototype.slice.call(sheet.querySelectorAll('button, textarea, input, a[href]')).filter(function (x) { return !x.disabled; });
281
+ if (!f.length) return;
282
+ if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
283
+ else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
284
+ }
285
+ function mountSheet() {
286
+ if (sheet) return false;
287
+ returnFocus = document.activeElement;
288
+ state = { note: null, images: [], list: [] };
289
+ sheet = document.createElement('div');
290
+ sheet.className = 'pn';
291
+ sheet.id = 'page-notes';
292
+ sheet.innerHTML = shellHtml(page());
293
+ sheet.addEventListener('click', onClick);
294
+ document.addEventListener('keydown', onKey, true);
295
+ document.body.appendChild(sheet);
296
+ document.documentElement.classList.add('pn-open');
297
+ var c = sheet.querySelector('[data-pn-close]');
298
+ if (c) c.focus();
299
+ return true;
300
+ }
301
+ function close() {
302
+ if (!sheet) return;
303
+ save();
304
+ if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
305
+ document.removeEventListener('keydown', onKey, true);
306
+ if (sheet.parentNode) sheet.parentNode.removeChild(sheet);
307
+ sheet = null;
308
+ document.documentElement.classList.remove('pn-open');
309
+ if (returnFocus && returnFocus.focus) { try { returnFocus.focus(); } catch (_) { /* gone */ } }
310
+ returnFocus = null;
311
+ }
312
+
313
+ window.OTBPageNotes = {
314
+ open: function () { if (mountSheet()) openDraft(); },
315
+ openNote: function (id) { if (mountSheet()) refreshList().then(function () { return showNote(id); }); },
316
+ close: close,
317
+ renderBody: renderBody,
318
+ page: page,
319
+ };
320
+ })();
@@ -115,9 +115,9 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
119
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
120
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
118
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
119
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
120
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
121
121
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
122
122
  <script src="/builders/palette.js" defer></script>
123
123
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -206,9 +206,9 @@
206
206
 
207
207
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
208
208
  chrome exists before the page script runs. -->
209
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
210
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
211
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
209
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
210
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
211
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
212
212
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
213
213
  <script src="/builders/palette.js" defer></script>
214
214
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,9 +58,9 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
62
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
63
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
61
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
62
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
63
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
64
64
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
65
65
  <script src="/builders/palette.js" defer></script>
66
66
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -77,9 +77,9 @@
77
77
 
78
78
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
79
79
  chrome exists before the page script runs. -->
80
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
81
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
82
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
80
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
81
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
82
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
83
83
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
84
84
  <script src="/builders/palette.js" defer></script>
85
85
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,9 +41,9 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
45
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
46
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
44
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
45
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
46
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
47
47
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
48
  <script src="/builders/palette.js" defer></script>
49
49
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -53,9 +53,9 @@
53
53
 
54
54
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
55
55
  chrome exists before the page script runs. -->
56
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
57
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
58
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
56
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
57
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
58
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
59
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -111,9 +111,9 @@
111
111
 
112
112
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
113
113
  chrome exists before the page script runs. -->
114
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
115
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
116
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
114
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
115
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
116
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -435,9 +435,9 @@
435
435
 
436
436
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
437
437
  chrome exists before the page scripts run. -->
438
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
439
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
440
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
438
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
439
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
440
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
441
441
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
442
442
  <script src="/builders/palette.js" defer></script>
443
443
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -22,6 +22,19 @@ body[data-page="street"] {
22
22
 
23
23
  .street-card__body { display: flex; flex-direction: column; gap: 11px; }
24
24
 
25
+ /* sized to its words (task 1004568): no min-height anywhere, and every part
26
+ of the compact card is held to its lines. The model cuts at a word boundary
27
+ (street-card.js LINE); these are the net under that, so a wide run of glyphs
28
+ can never push a part onto another line and stretch the card. The kicker
29
+ gives way to "when" and the close. The title is cut to about two lines;
30
+ its clamp at three is only the net, for a run of wide glyphs. */
31
+ .street-card__head .street-label { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
32
+ .street-card__head #streetCardWhen { flex: none; }
33
+ .street-card__title { overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; flex: none; }
34
+ .mcard-fact { margin: 0; font-size: 14px; line-height: 1.4; color: var(--ink-soft); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
35
+ .street-card__lines li { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
36
+ .mcard-voice__words { overflow-wrap: anywhere; }
37
+
25
38
  .mcard-quote { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ink-soft); }
26
39
 
27
40
  /* the wording change: struck, then inserted */
@@ -101,10 +101,25 @@
101
101
  if (names.length <= 1) return names.join('');
102
102
  return names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
103
103
  }
104
+ // a text as one run of words: every whitespace run is one space
105
+ function norm(s) { return str(s).replace(/\s+/g, ' ').trim(); }
104
106
  function clip(s, n) {
105
- var t = str(s).replace(/\s+/g, ' ').trim();
107
+ var t = norm(s);
106
108
  return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : t;
107
109
  }
110
+ // THE COMPACT CARD'S LINE RULE (task 1004568). The card is only as tall as
111
+ // its words need: every part of it has a cap, and anything longer is cut at
112
+ // a word boundary with an ellipsis (clip(): the desk's line rule, GovDesk.fit(),
113
+ // but never mid-word), with the whole text under "more". The caps are
114
+ // characters at the card's 390px width: the kicker, the fact and a listed
115
+ // line are one line; the title two; a voice's reason two; the proposer's
116
+ // words three.
117
+ var LINE = { where: 40, title: 64, fact: 44, line: 44, reason: 96, quote: 132 };
118
+ // the first line of a text, as words (a fact is one line of it)
119
+ function firstLine(s) {
120
+ var lines = str(s).split(/\r?\n/).map(function (l) { return l.replace(/^[#>*\-\s]+/, '').replace(/\s+/g, ' ').trim(); }).filter(Boolean);
121
+ return lines[0] || '';
122
+ }
108
123
  // the first paragraph of a markdown text, as words
109
124
  function firstPara(md) {
110
125
  var paras = str(md).split(/\r?\n\s*\r?\n/).map(function (p) { return p.replace(/^[#>*\-\s]+/gm, '').replace(/\s+/g, ' ').trim(); }).filter(Boolean);
@@ -273,7 +288,7 @@
273
288
  var votes = (item && item.votes) || [];
274
289
  var out = [];
275
290
  votes.filter(function (v) { return v.direction === 'no'; }).forEach(function (v) {
276
- out.push({ side: 'against', by: firstName(personName(v)) + ' · objects to ' + sectionWords(v.section_key), words: str(v.reason_md) });
291
+ out.push({ side: 'against', by: firstName(personName(v)) + ' · objects to ' + sectionWords(v.section_key), words: norm(v.reason_md) });
277
292
  });
278
293
  var yes = votes.filter(function (v) { return v.direction === 'yes'; }).map(function (v) { return firstName(personName(v)); });
279
294
  if (yes.length && out.length) out.unshift({ side: 'for', by: 'for', words: list(yes) });
@@ -319,7 +334,7 @@
319
334
  var kind = subjectKind(item);
320
335
  var proposer = m.proposer ? firstName(m.proposer.name || m.proposer.login) : '';
321
336
  var words = proposerWords(item);
322
- if (words) cv.quote = { by: proposer, words: clip(words, 200) };
337
+ if (words) cv.quote = { by: proposer, words: words };
323
338
  cv.change = changeOf(item, ctx.docket, ctx.constitution);
324
339
  cv.revision = revisionOf(item, ctx.room);
325
340
  var b = ballotOf(item, m, ctx.viewerId);
@@ -399,6 +414,9 @@
399
414
  var who = m.proposer ? str(m.proposer.name || m.proposer.login) : '';
400
415
  cv.subject = { kind: m.source, id: id, who: who, goalId: m.source === 'goal_join_requests' ? joinGoalId(m) : null };
401
416
  var mine = m.state === 'needs';
417
+ // its one fact (task 1004568): the first line of the matter's detail (the
418
+ // switch's reason, a blocker's body), cut by compact(); "more" has it whole
419
+ if (m.detail) cv.fact = firstLine(m.detail);
402
420
  if (m.source === 'access_requests') {
403
421
  if (who) cv.asked = who + ' asked to join as a builder' + (/vouched/.test(str(m.where)) ? ', and someone vouched for them.' : '.');
404
422
  if (!mine) { cv.readOnly = true; cv.result = 'Not yours to decide.'; return cv; }
@@ -470,7 +488,7 @@
470
488
  if (cv.revision) history.push('an earlier sitting sent it back to be revised');
471
489
  (item.votes || []).slice().sort(function (a, b) { return (Date.parse(a.cast_at) || 0) - (Date.parse(b.cast_at) || 0); }).forEach(function (v) {
472
490
  var by = personName(v) || 'someone';
473
- var said = v.direction === 'yes' ? by + ' voted for' : by + ' objected to ' + sectionWords(v.section_key) + (v.reason_md ? ': ' + clip(str(v.reason_md).replace(/\s+/g, ' ').trim(), 160) : '');
491
+ var said = v.direction === 'yes' ? by + ' voted for' : by + ' objected to ' + sectionWords(v.section_key) + (v.reason_md ? ': ' + bound(str(v.reason_md).replace(/\s+/g, ' ').trim()) : '');
474
492
  history.push([dayOf(v.cast_at), said].filter(Boolean).join(' · '));
475
493
  });
476
494
  } else if (m) {
@@ -497,9 +515,9 @@
497
515
  key: short.key, building: short.building, where: short.where, when: short.when, title: short.title, href: short.href,
498
516
  source: null, quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
499
517
  result: short.result, actions: [], textActions: [], reviseForm: null, readOnly: false, loading: false, lines: short.lines || [], subject: null,
500
- more: null, row: null,
518
+ more: null, row: null, fact: '',
501
519
  };
502
- if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return cv; }
520
+ if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return compact(cv); }
503
521
  var m = findMatter(c.docket, c.key);
504
522
  cv.source = m.source;
505
523
  var viewerId = c.viewerId != null ? c.viewerId : (c.docket && c.docket.viewer ? c.docket.viewer.id : null);
@@ -511,14 +529,47 @@
511
529
  cv.readOnly = true;
512
530
  cv.tally = short.tally;
513
531
  cv.result = c.room ? 'This sitting could not be read just now.' : 'Reading the sitting…';
514
- return cv;
532
+ return compact(cv);
515
533
  }
516
534
  boardCard(cv, m, item, { docket: c.docket, room: c.room, constitution: c.constitution, viewerId: viewerId });
517
535
  cv.more = moreOf(cv, m, item);
518
- return cv;
536
+ return compact(cv);
519
537
  }
520
538
  sourceCard(cv, m);
521
539
  cv.more = moreOf(cv, m, null);
540
+ return compact(cv);
541
+ }
542
+
543
+ // Every part of the compact card under the line rule (task 1004568): the
544
+ // kicker, the title, the fact, the proposer's words, the listed lines and a
545
+ // voice's reason. What
546
+ // was cut and is not already in "more" is added there whole, so nothing a
547
+ // cut hides is lost.
548
+ function compact(cv) {
549
+ var whole = [];
550
+ function cut(s, n) {
551
+ var t = norm(s);
552
+ var c = clip(t, n);
553
+ if (c !== t) whole.push(t);
554
+ return c;
555
+ }
556
+ cv.where = cut(cv.where, LINE.where);
557
+ cv.title = cut(cv.title, LINE.title);
558
+ cv.fact = cut(cv.fact, LINE.fact);
559
+ if (cv.quote && cv.quote.words) cv.quote = { by: cv.quote.by, words: cut(cv.quote.words, LINE.quote) };
560
+ cv.lines = (cv.lines || []).map(function (l) { return cut(l, LINE.line); });
561
+ cv.voices = (cv.voices || []).map(function (v) {
562
+ var w = cut(v.words, LINE.reason);
563
+ return w === v.words ? v : { side: v.side, by: v.by, words: w };
564
+ });
565
+ if (whole.length) {
566
+ var mo = cv.more || { text: [], who: '', when: '', history: [] };
567
+ var have = mo.text.map(function (x) { return x.words; }).concat(mo.history).join('\n');
568
+ // compared as "more" holds it: bound() may have shortened a long text there
569
+ var add = whole.filter(function (t) { return have.indexOf(bound(t).replace(/…$/, '')) < 0; }).map(function (t) { return { label: '', words: bound(t) }; });
570
+ mo.text = add.concat(mo.text);
571
+ cv.more = mo;
572
+ }
522
573
  return cv;
523
574
  }
524
575
 
@@ -807,6 +858,7 @@
807
858
  function render(doc, host, cv, on, ui) {
808
859
  var u = ui || {};
809
860
  while (host.firstChild) host.removeChild(host.firstChild);
861
+ if (cv.fact) host.appendChild(el(doc, 'p', 'mcard-fact', cv.fact));
810
862
  if (cv.quote && cv.quote.words) host.appendChild(el(doc, 'p', 'mcard-quote', (cv.quote.by ? cv.quote.by + ': ' : '') + cv.quote.words));
811
863
  if (cv.lines && cv.lines.length) {
812
864
  var ul = el(doc, 'ul', 'street-card__lines');
@@ -997,7 +1049,7 @@
997
1049
  var api = {
998
1050
  API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
999
1051
  model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
1000
- memberWords: memberWords, windowWords: windowWords,
1052
+ memberWords: memberWords, windowWords: windowWords, LINE: LINE, compact: compact,
1001
1053
  // the wording change, for a drafted amendment's struck and inserted text (task 1004530)
1002
1054
  change: changeOf,
1003
1055
  };