@bongos/core 1.24.52 → 1.24.54

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 (46) hide show
  1. package/.bongos-core.json +88 -43
  2. package/docs/copy-inventory.md +85 -86
  3. package/docs/copy-registry.json +117 -126
  4. package/docs/file-map.md +1 -1
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-readings.json +258 -293
  7. package/modules/discord/webhook.js +28 -6
  8. package/modules/hall-ui/public/controls.css +96 -0
  9. package/modules/hall-ui/public/dom-utils.js +64 -3
  10. package/modules/hall-ui/public/genesis-home.js +1 -1
  11. package/modules/hall-ui/public/goal-forest-render.js +1 -1
  12. package/modules/hall-ui/public/goals.css +2 -8
  13. package/modules/hall-ui/public/idea.js +1 -1
  14. package/modules/hall-ui/public/record.css +3 -20
  15. package/modules/hall-ui/public/roster.css +0 -2
  16. package/modules/hall-ui/public/style.css +14 -26
  17. package/modules/hall-ui/public/task.js +8 -10
  18. package/modules/hall-ui/public/watch.css +1 -5
  19. package/modules/hall-ui/specimens/components.css +26 -0
  20. package/modules/hall-ui/specimens/components.html +115 -0
  21. package/modules/hall-ui/specimens/components.js +27 -0
  22. package/modules/hall-ui/specimens/components.states.json +35 -0
  23. package/modules/public-landing/public/index.html +11 -8
  24. package/modules/ui-design/public/brand/og-card.png +0 -0
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/public-docs/index.html +107 -12
  28. package/release-notes.json +12 -0
  29. package/scripts/bongos/gen-brand.js +40 -6
  30. package/scripts/discord/lib.js +17 -3
  31. package/scripts/status-mirror-sync.js +11 -7
  32. package/src/bongos/serve-internal.js +3 -2
  33. package/src/error-page.js +120 -0
  34. package/src/link-preview.js +71 -0
  35. package/src/module-api.js +1 -1
  36. package/src/platform-server.js +22 -8
  37. package/tests/brand_surfaces.mjs +213 -0
  38. package/tests/discord_board_broadcast.mjs +23 -10
  39. package/tests/docs_openapi_route.mjs +2 -1
  40. package/tests/goal_forest_page.mjs +2 -1
  41. package/tests/hall_audit.mjs +2 -1
  42. package/tests/hall_record_world.mjs +10 -6
  43. package/tests/hall_roster_world.mjs +1 -1
  44. package/tests/hall_status_pill.mjs +251 -0
  45. package/tests/helpers.mjs +2 -0
  46. package/tests/idea_descent_surface.mjs +2 -1
@@ -71,16 +71,36 @@ function capContent(text) {
71
71
  return String(text).slice(0, 2000);
72
72
  }
73
73
 
74
+ // THE BRAND ON A POST (task 1005091). A broadcast goes out as an EMBED, not as bare
75
+ // message text: the same words as the embed's description, with the pack's accent as
76
+ // the embed's colour bar, so every post in a channel reads as the product's. The
77
+ // description cap is 4096 (an embed's own limit). Mentions inside an embed never ring,
78
+ // which is #871's rule anyway; allowed_mentions stays parse:[] belt-and-braces.
79
+ // The pack's accent as Discord's integer colour, or null (no bar) when it is not a hex.
80
+ function brandColor(brand) {
81
+ let accent = '';
82
+ try { accent = ((brand || api.clientBranding()).theme || {}).colors.accent || ''; } catch { accent = ''; }
83
+ const m = /^#([0-9a-f]{6})$/i.exec(String(accent).trim());
84
+ return m ? parseInt(m[1], 16) : null;
85
+ }
86
+ // The body of a broadcast post: one embed carrying the text, coloured with the accent.
87
+ function embedBody(text, brand) {
88
+ const embed = { description: String(text).slice(0, 4096) };
89
+ const color = brandColor(brand);
90
+ if (color !== null) embed.color = color;
91
+ return { embeds: [embed], allowed_mentions: { parse: [] } };
92
+ }
93
+
74
94
  // R22 (the board mirror) needs the id of the message it just posted so the close
75
95
  // can be edited onto it, so the POST asks for the created message back
76
96
  // (`?wait=true` — Discord then answers 200 + the message JSON instead of 204 with
77
97
  // no body). The extra field is additive: every existing caller ignores the return.
78
- async function postCore(channel, text, allowedMentions) {
98
+ async function postCore(channel, body) {
79
99
  const url = resolveWebhookUrl(channel);
80
100
  const res = await fetch(`${url}${url.includes('?') ? '&' : '?'}wait=true`, {
81
101
  method: 'POST',
82
102
  headers: { 'Content-Type': 'application/json' },
83
- body: JSON.stringify({ content: capContent(text), allowed_mentions: allowedMentions }),
103
+ body: JSON.stringify(body),
84
104
  });
85
105
  if (!res.ok) {
86
106
  const body = await res.text().catch(() => '');
@@ -96,7 +116,7 @@ async function postCore(channel, text, allowedMentions) {
96
116
  // attacker-influenceable; allowed_mentions:{parse:[]} renders text verbatim but
97
117
  // fires zero notifications. (Discord defaults to parsing mentions when omitted.)
98
118
  async function postWebhook(channel, text) {
99
- return postCore(channel, text, { parse: [] });
119
+ return postCore(channel, embedBody(text));
100
120
  }
101
121
 
102
122
  // postWebhookMentions — the ONE sanctioned exception to #871's blanket mention
@@ -104,10 +124,12 @@ async function postWebhook(channel, text) {
104
124
  // notice"). Still parse:[] — nothing typed INTO the content can ever ping — but
105
125
  // an explicit `users` allowlist lets exactly the named ids ring. The ids must
106
126
  // come from the board-membership resolve (server data), NEVER from message
107
- // text; the cap of 20 is a sanity bound far above any real board.
127
+ // text; the cap of 20 is a sanity bound far above any real board. It stays
128
+ // MESSAGE TEXT, not an embed: a mention inside an embed never rings, and ringing
129
+ // is this post's whole job (R22's edit rewrites the same text in place).
108
130
  async function postWebhookMentions(channel, text, userIds = []) {
109
131
  const users = [...new Set(userIds.filter((u) => /^\d+$/.test(String(u))))].slice(0, 20).map(String);
110
- return postCore(channel, text, { parse: [], users });
132
+ return postCore(channel, { content: capContent(text), allowed_mentions: { parse: [], users } });
111
133
  }
112
134
 
113
135
  // editWebhook — rewrite a message this webhook already posted, in place (R22 of
@@ -149,5 +171,5 @@ function hasWebhook(channel) {
149
171
 
150
172
  module.exports = {
151
173
  CHANNELS, WEBHOOKS_PATH, loadWebhooks, resolveWebhookUrl, hasWebhook,
152
- postWebhook, postWebhookMentions, editWebhook,
174
+ postWebhook, postWebhookMentions, editWebhook, embedBody,
153
175
  };
@@ -321,3 +321,99 @@ select option { background: var(--bg-card); color: var(--ink); }
321
321
  gap: var(--ds-space-xs);
322
322
  margin-top: var(--ds-space-md);
323
323
  }
324
+
325
+ /* ---------- the status pill (task 1005086) ----------
326
+ THE HALL'S ONE STATUS PILL. DESIGN.md "The hall": a status pill is the chip's
327
+ shape (a 26px full pill, lowercase) on a status -weak fill with the status
328
+ colour as its text. Six tones, from the hall's fixed verification vocabulary
329
+ and the one warm accent:
330
+ --ok done, passed, live --ok on --ok-weak
331
+ --warn waiting, paused, at risk --warn on --warn-weak
332
+ --danger failed, refused, removed --danger on --danger-weak
333
+ --info moving, open, in hand --info on --info-weak
334
+ --neutral not started, closed, quiet (the base, no modifier needed)
335
+ --accent yours, the current thing --accent-ink on --accent-tint
336
+ A task's status picks its tone in ONE place, dom-utils.js STATUS_TONE
337
+ (OTB.statusPill writes the markup), so it reads the same on every page.
338
+ A status is always a word as well as a colour.
339
+
340
+ THE NAMES PAGES ALREADY WRITE join the one rule, as the buttons did in task
341
+ 1005084: .rec-pill (the records), .fact-pill (the rows), .gov-pill,
342
+ .gate-pill and .badge (its aliases), with their tone modifiers mapped below.
343
+ Every colour is a status token, a -weak fill mixed into --bg-card, or the
344
+ accent pair; tests/hall_status_pill.mjs measures every tone in both modes. */
345
+ .status-pill, .rec-pill, .fact-pill, .gov-pill, .gate-pill, .badge {
346
+ display: inline-flex;
347
+ align-items: center;
348
+ box-sizing: border-box;
349
+ min-height: 26px;
350
+ padding: 0 var(--ds-space-xs);
351
+ font-family: var(--font-body);
352
+ font-size: var(--ds-text-xs-plus);
353
+ font-weight: 500;
354
+ line-height: 1.2;
355
+ letter-spacing: 0;
356
+ text-transform: lowercase;
357
+ white-space: nowrap;
358
+ color: var(--ink-soft);
359
+ background: color-mix(in srgb, var(--ink) 6%, var(--bg-card));
360
+ border: var(--line) solid transparent;
361
+ border-radius: var(--radius-pill);
362
+ }
363
+ .status-pill--ok, .rec-pill--ok, .fact-pill--ok, .fact-pill--achieved {
364
+ color: var(--ok); background: var(--ok-weak);
365
+ }
366
+ .status-pill--warn, .rec-pill--warn, .fact-pill--warn, .fact-pill--blocked, .badge--sev-high {
367
+ color: var(--warn); background: var(--warn-weak);
368
+ }
369
+ .status-pill--danger, .rec-pill--danger, .fact-pill--danger, .badge--sev-critical {
370
+ color: var(--danger); background: var(--danger-weak);
371
+ }
372
+ .status-pill--info, .rec-pill--info, .fact-pill--info, .fact-pill--open, .badge--sev-medium {
373
+ color: var(--info); background: var(--info-weak);
374
+ }
375
+ .status-pill--neutral, .fact-pill--quiet, .fact-pill--archived, .badge--sev-low, .badge--status {
376
+ color: var(--ink-soft);
377
+ background: color-mix(in srgb, var(--ink) 6%, var(--bg-card));
378
+ }
379
+ .status-pill--accent {
380
+ color: var(--accent-ink); background: var(--accent-tint);
381
+ }
382
+ /* the row size, for a pill inside a dense line (a grade axis, a table cell) */
383
+ .status-pill--sm, .rec-pill--sm { min-height: 22px; padding: 0 9px; font-size: var(--ds-text-2xs-plus); }
384
+ /* a pill that is also a link keeps the pill, and says it is pressable on hover */
385
+ a.status-pill, a.fact-pill { text-decoration: none; }
386
+ a.status-pill:hover, a.fact-pill:hover { text-decoration: underline; text-underline-offset: 3px; }
387
+
388
+ /* ---------- the view toggle (task 1005086) ----------
389
+ ONE segmented control for a VIEW (rows | cards, list | map): not a filter,
390
+ so the pressed side takes the tint the world gives a current thing, never
391
+ the accent fill (a view is not a live door). Moved here from goals.css, its
392
+ first home (task 1003470), so every page that switches a view writes it. */
393
+ .viewtoggle {
394
+ display: inline-flex;
395
+ align-items: center;
396
+ gap: 3px;
397
+ padding: 3px;
398
+ border: var(--line) solid var(--rule-soft);
399
+ border-radius: var(--radius-pill);
400
+ }
401
+ .viewtoggle__btn {
402
+ min-height: 26px;
403
+ min-width: 60px;
404
+ padding: 0 var(--ds-space-sm);
405
+ font: inherit;
406
+ font-size: var(--ds-text-sm);
407
+ text-transform: lowercase;
408
+ color: var(--ink-faint);
409
+ background: none;
410
+ border: 0;
411
+ border-radius: var(--radius-pill);
412
+ cursor: pointer;
413
+ }
414
+ .viewtoggle__btn:hover { color: var(--ink-soft); }
415
+ .viewtoggle__btn[aria-pressed="true"] {
416
+ color: var(--accent-ink);
417
+ background: var(--accent-tint);
418
+ }
419
+ .viewtoggle__btn:focus-visible { border-radius: var(--radius-pill); }
@@ -93,14 +93,73 @@
93
93
  // (2200/2600/3200) and whether the prior hide-timer is cleared. A
94
94
  // module-level timer backs the clear path; each hall page loads one consuming
95
95
  // script, so it is effectively per-page.
96
+ //
97
+ // ONE TOAST, a duration per KIND (task 1005086): a confirmation reads in a
98
+ // glance, an error needs time to be read and acted on. `kind` is info | ok |
99
+ // warn | error; a message that opens like a failure ("Could not …", "Failed …")
100
+ // is an error even when the caller did not say so, so the forty call sites that
101
+ // predate the kinds get the error's longer hold without each being edited. An
102
+ // explicit `duration` still wins. Every message goes through plainMessage(), so
103
+ // a server address (a URL, a host, an IP and port) never reaches the screen.
104
+ const TOAST_MS = { info: 3200, ok: 2600, warn: 5000, error: 6500 };
105
+ const FAILURE_OPENING = /^\s*(could ?n[o']t|cannot|can't|failed|error\b|that did not|did not|not (saved|filed|proposed|sent))/i;
106
+ function toastKind(msg, kind) {
107
+ if (kind && TOAST_MS[kind]) return kind;
108
+ return FAILURE_OPENING.test(String(msg || '')) ? 'error' : 'info';
109
+ }
110
+
111
+ // plainMessage — a sentence safe to show: URLs, host names with a dot-separated
112
+ // domain, IPv4/IPv6 addresses and host:port pairs are replaced by "the server",
113
+ // so a fetch error ("request to https://10.0.0.5:8443/api failed") reads as a
114
+ // plain message. Collapses the doubled words the replacement can leave.
115
+ function plainMessage(msg) {
116
+ return String(msg ?? '')
117
+ .replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>)]*[^\s"'<>).,:;!?]/gi, 'the server')
118
+ .replace(/\[[0-9a-f:.]{2,}\](?::\d{2,5})?/gi, 'the server')
119
+ // an unbracketed IPv6: two or more colons, and a '::' or a hex letter (so a clock time is not one)
120
+ .replace(/(?<![\w:])(?=[0-9a-f:]*(?:::|[a-f]))[0-9a-f]{0,4}(?::[0-9a-f]{0,4}){2,7}(?![\w:])/gi, 'the server')
121
+ .replace(/\b\d{1,3}(?:\.\d{1,3}){3}(?::\d{2,5})?\b/g, 'the server')
122
+ .replace(/\b(?:localhost|[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.(?:com|net|org|io|dev|app|cloud|internal|local|lan|localhost|test|example|co))(?::\d{2,5})?(?:\/[^\s"'<>)]*)?/gi, 'the server')
123
+ .replace(/\bthe server(?:\s+the server)+/g, 'the server')
124
+ .replace(/\s{2,}/g, ' ')
125
+ .trim();
126
+ }
127
+
96
128
  let _toastTimer = null;
97
- function toast(msg, { duration = 3200, clear = false } = {}) {
129
+ function toast(msg, { duration, clear = false, kind } = {}) {
98
130
  const el = $('#toast');
99
131
  if (!el) return;
100
- el.textContent = msg;
132
+ const k = toastKind(msg, kind);
133
+ el.textContent = plainMessage(msg);
134
+ el.dataset.kind = k;
101
135
  el.dataset.visible = '1';
102
136
  if (clear) clearTimeout(_toastTimer);
103
- _toastTimer = setTimeout(() => { el.dataset.visible = '0'; }, duration);
137
+ _toastTimer = setTimeout(() => { el.dataset.visible = '0'; }, duration || TOAST_MS[k]);
138
+ }
139
+
140
+ // ONE STATUS → ONE TONE (task 1005086). A task's lifecycle status is coloured
141
+ // here and nowhere else, so it reads the same on the task page, a goal's
142
+ // table, Home's genesis stages and an idea's descent. The tones are the
143
+ // hall's verification vocabulary (DESIGN.md "The hall"): shipped and
144
+ // confirmed are done (ok); ready, active and completed are moving (info);
145
+ // blocked is waiting on something (warn); backlog and abandoned are neither
146
+ // (neutral). `backlog` is deliberately NOT amber: not scheduled is not a
147
+ // problem (task.js's note, kept). A status nobody mapped is neutral.
148
+ const STATUS_TONE = Object.freeze({
149
+ backlog: 'neutral', ready: 'info', active: 'info', blocked: 'warn',
150
+ completed: 'info', confirmed: 'ok', shipped: 'ok', abandoned: 'neutral',
151
+ });
152
+ const STATUS_TONES = Object.freeze(['ok', 'warn', 'danger', 'info', 'neutral', 'accent']);
153
+ function statusTone(status) {
154
+ return STATUS_TONE[String(status ?? '').trim().toLowerCase()] || 'neutral';
155
+ }
156
+ // The pill markup every page writes for a status: `.status-pill` + its tone
157
+ // (controls.css). `label` overrides the shown word (the status stays the key);
158
+ // `tone` names a tone directly for a status outside the task lifecycle.
159
+ function statusPill(status, { label, tone, small = false, extraClass = '' } = {}) {
160
+ const t = STATUS_TONES.includes(tone) ? tone : statusTone(status);
161
+ const cls = `status-pill status-pill--${t}${small ? ' status-pill--sm' : ''}${extraClass ? ` ${extraClass}` : ''}`;
162
+ return `<span class="${cls}">${escapeHtml(label ?? status ?? 'unknown')}</span>`;
104
163
  }
105
164
 
106
165
  // Feature-module gate (R60, task 1199 / ADR 0062 §4). serve-internal.js injects
@@ -472,6 +531,8 @@
472
531
 
473
532
  const OTB = {
474
533
  escapeHtml, sleep, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
534
+ plainMessage, toastKind, TOAST_MS, // task 1005086 — one toast, a duration per kind, no server address
535
+ STATUS_TONE, STATUS_TONES, statusTone, statusPill, // task 1005086 — one status, one tone, one pill
475
536
  // task 1003936 — ONE word for the role axis, all six surfaces
476
537
  roleAxisLabel, roleLabel, roleWord, roleOptions, roleIsUnset, ROLE_UNSET,
477
538
  fillKindFilter, kindLabel, kindIsUnset, KIND_UNSET, // task 1003453 — one Kind filler, all three surfaces
@@ -187,7 +187,7 @@
187
187
  ? '<ul class="genesis__tasks">' + s.tasks.map(function (t) {
188
188
  return '<li class="genesis__task' + (t.finished ? ' is-finished' : '') + '">'
189
189
  + '<a class="genesis__task-t" href="/task/' + encodeURIComponent(t.id) + '">' + esc(t.title) + '</a>'
190
- + '<span class="genesis__task-st">' + esc(t.status) + '</span></li>';
190
+ + window.OTB.statusPill(t.status, { small: true, extraClass: 'genesis__task-st' }) + '</li>';
191
191
  }).join('') + '</ul>'
192
192
  : '<p class="genesis__note">No tasks for this stage yet.</p>';
193
193
  return '<section class="genesis__now" aria-labelledby="genesis-now-h">'
@@ -177,7 +177,7 @@
177
177
  : '<span class="gf-none">—</span>');
178
178
  const rowHtml = (r) => `<tr>
179
179
  <td><a class="lb-name" href="${taskHref(r.id)}">#${escapeHtml(r.id)} ${escapeHtml(r.title)}</a></td>
180
- <td>${escapeHtml(r.status)}</td>
180
+ <td>${window.OTB.statusPill(r.status, { small: true })}</td>
181
181
  <td>${escapeHtml(r.priority)}</td>
182
182
  <td>${depList(r.waits)}</td>
183
183
  <td>${depList(r.needed)}</td>
@@ -283,14 +283,8 @@
283
283
  oversight select row), never the accent fill: a view is not a live door. */
284
284
  .goals-line { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px 20px; margin: 0 0 6px; }
285
285
  .goals-count-line { margin: 0; font-family: var(--font-mono); font-size: 12.5px; font-variant-numeric: tabular-nums; color: var(--ink-faint); }
286
- .viewtoggle { display: inline-flex; align-items: center; gap: 3px; padding: 3px; border: var(--line) solid var(--rule-soft); border-radius: var(--radius-pill); }
287
- .viewtoggle__btn {
288
- min-height: 26px; min-width: 60px; padding: 0 14px;
289
- font: inherit; font-size: 13px; text-transform: lowercase; color: var(--ink-faint);
290
- background: none; border: 0; border-radius: var(--radius-pill); cursor: pointer;
291
- }
292
- .viewtoggle__btn:hover { color: var(--ink-soft); }
293
- .viewtoggle__btn[aria-pressed="true"] { color: var(--accent-ink); background: var(--accent-tint); }
286
+ /* .viewtoggle itself is the hall's shared view toggle since task 1005086
287
+ (controls.css). */
294
288
 
295
289
  /* ============================================================
296
290
  The list — task 1002439's grouped rollup, redesigned in task 1003470 (goal
@@ -338,7 +338,7 @@
338
338
  const bits = [];
339
339
  if (row.relation) bits.push(escapeHtml(RELATION_WORD[row.relation] || row.relation));
340
340
  else if (row.depth === 0) bits.push('became work');
341
- if (row.status) bits.push(escapeHtml(row.status));
341
+ if (row.status) bits.push(window.OTB.statusPill(row.status, { small: true }));
342
342
  // A version that is not the idea's own is the interesting case, not an
343
343
  // anomaly to hide: the descent crosses version boundaries deliberately
344
344
  // (ADR 0312) and the read carries version_id so a surface can SAY so.
@@ -64,26 +64,9 @@
64
64
  sanctioned exception to the Fifteen Rule (DESIGN.md §Reuse) — a failed grade
65
65
  must not be salmon on a page whose ship button is also salmon — and its values
66
66
  are measured by tests/hall_tokens.mjs, never hand-tuned. */
67
- .rec-pill {
68
- display: inline-flex;
69
- align-items: center;
70
- min-height: 26px;
71
- padding: 0 13px;
72
- border: var(--line) solid var(--rule);
73
- border-radius: var(--radius-pill);
74
- background: none;
75
- font-family: var(--font-body);
76
- font-size: 12.5px;
77
- font-weight: 500;
78
- letter-spacing: 0;
79
- text-transform: lowercase;
80
- color: var(--ink-soft);
81
- }
82
- .rec-pill--ok { color: var(--ok); background: var(--ok-weak); border-color: color-mix(in srgb, var(--ok) 42%, transparent); }
83
- .rec-pill--warn { color: var(--warn); background: var(--warn-weak); border-color: color-mix(in srgb, var(--warn) 42%, transparent); }
84
- .rec-pill--danger { color: var(--danger); background: var(--danger-weak); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
85
- .rec-pill--info { color: var(--info); background: var(--info-weak); border-color: color-mix(in srgb, var(--info) 42%, transparent); }
86
- .rec-pill--sm { min-height: 22px; padding: 0 9px; font-size: 11.5px; }
67
+ /* .rec-pill is the hall's ONE status pill since task 1005086 (controls.css
68
+ .status-pill, with .rec-pill and its --ok/--warn/--danger/--info/--sm joined
69
+ to it); the tone a lifecycle status takes is dom-utils.js STATUS_TONE. */
87
70
 
88
71
  .rec-title {
89
72
  margin: 0 0 18px;
@@ -102,6 +102,4 @@
102
102
  others carry a control; the foot line under the table says why. */
103
103
  .roster-warn { color: var(--danger); font-weight: 500; }
104
104
  .roster-self { color: var(--ink-faint); }
105
- .badge--sev-medium { color: var(--info); }
106
- .badge--status { color: var(--ink-faint); }
107
105
  .roster-foot { margin: 14px 0 0; font-size: 12.5px; color: var(--ink-faint); max-width: var(--measure); }
@@ -797,7 +797,8 @@ html[data-theme="dark"] .gplate__light { display: none; }
797
797
  }
798
798
  .gplate--sm { width: 56px; height: 56px; }
799
799
  .genesis__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 24px; }
800
- .genesis__stage-st, .genesis__task-st { flex: none; font-size: 11.5px; letter-spacing: 0.04em; color: var(--ink-faint); }
800
+ .genesis__stage-st { flex: none; font-size: 11.5px; letter-spacing: 0.04em; color: var(--ink-faint); }
801
+ .genesis__task-st { flex: none; } /* the shared status pill (task 1005086) */
801
802
  .genesis__stage.is-now .genesis__stage-st { color: var(--accent-ink); }
802
803
  .genesis__stage.is-later { opacity: 0.6; }
803
804
  .genesis__now-h { margin: 0 0 4px; font-size: 15px; font-weight: 500; }
@@ -2102,10 +2103,13 @@ html.pb-editing main { padding-bottom: 180px; }
2102
2103
  .scroll primary — the default surface for a section
2103
2104
  .scroll--quiet secondary — a card INSIDE a card, or supporting detail
2104
2105
  .scroll--bare none — layout-only, for stat rows and grids */
2105
- .scroll {
2106
+ /* THE PANEL (DESIGN.md "The hall"): the card ground, the panel corner, no
2107
+ shadow, never nested in another. .panel is its plain name (task 1005086);
2108
+ .scroll is the name every page already writes. */
2109
+ .scroll, .panel {
2106
2110
  background: var(--bg-card);
2107
2111
  border: 1px solid transparent;
2108
- border-radius: var(--radius);
2112
+ border-radius: var(--ds-radius-panel);
2109
2113
  padding: var(--sp-md);
2110
2114
  margin: 0 0 var(--sp-md);
2111
2115
  /* anchor jumps must clear the 56px sticky top bar */
@@ -3751,6 +3755,10 @@ html[data-door="out"] #hall-main { width: 100%; }
3751
3755
  max-width: min(90vw, 480px);
3752
3756
  }
3753
3757
  .toast[data-visible="1"] { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
3758
+ /* An error toast (task 1005086: OTB.toast's kind, set on data-kind) is the status
3759
+ red with the card's ink, the pair the danger button measures; it also holds
3760
+ longest (dom-utils.js TOAST_MS), so there is time to read what went wrong. */
3761
+ .toast[data-kind="error"] { background: var(--danger); color: var(--bg-card); }
3754
3762
 
3755
3763
  #copy-toast {
3756
3764
  position: fixed;
@@ -4489,29 +4497,9 @@ html.palette-open { overflow: hidden; }
4489
4497
  mean editing the JS row builders, and a class name is not what was wrong —
4490
4498
  the duplicated, hand-rolled label voice was. Their own state modifiers stay
4491
4499
  in their own sheets. */
4492
- .fact-pill,
4493
- .gov-pill,
4494
- .gate-pill,
4495
- .badge {
4496
- display: inline-flex;
4497
- align-items: center;
4498
- min-height: 24px;
4499
- padding: 0 11px;
4500
- border: var(--line) solid currentColor;
4501
- border-radius: var(--radius-pill);
4502
- background: none;
4503
- font-family: var(--label-face);
4504
- font-size: 10.5px;
4505
- letter-spacing: var(--label-space);
4506
- text-transform: var(--label-case);
4507
- color: var(--ink-faint);
4508
- white-space: nowrap;
4509
- }
4510
- .fact-pill--open, .fact-pill--info { color: var(--info); }
4511
- .fact-pill--blocked, .fact-pill--warn { color: var(--warn); }
4512
- .fact-pill--achieved, .fact-pill--ok { color: var(--ok); }
4513
- .fact-pill--danger { color: var(--danger); }
4514
- .fact-pill--archived, .fact-pill--quiet { color: var(--ink-faint); }
4500
+ /* Since task 1005086 they are not only one another but the hall's ONE status
4501
+ pill: controls.css .status-pill, with these names and their tone modifiers
4502
+ joined to it. */
4515
4503
 
4516
4504
  /* A scope, not a state: what the record is filed under. Mono because it is a
4517
4505
  machine's word (a module key, a version id), --radius-sm because a pill would
@@ -254,15 +254,13 @@
254
254
  // conflating them made every unscheduled task read as a problem. Backlog takes
255
255
  // the neutral pill; the lifecycle rail's first beat carries the distinction in
256
256
  // words ("not ready to be claimed yet" vs "waiting on another task").
257
- const PILL_TONE = {
258
- shipped: 'ok', confirmed: 'ok',
259
- active: 'info', completed: 'info', ready: 'info',
260
- blocked: 'warn',
261
- };
262
- function statusPill(status, extraClass) {
263
- const s = String(status || 'unknown');
264
- const tone = PILL_TONE[s];
265
- return `<span class="rec-pill${tone ? ` rec-pill--${tone}` : ''}${extraClass ? ` ${extraClass}` : ''}">${escapeHtml(s)}</span>`;
257
+ //
258
+ // The map itself moved to dom-utils.js STATUS_TONE (task 1005086), so every page
259
+ // that shows a task's status colours it the same; the reasoning above is why it
260
+ // reads as it does. A word that is not a lifecycle status (the not-found and
261
+ // feed-down records) names its tone directly.
262
+ function statusPill(status, { tone, small } = {}) {
263
+ return window.OTB.statusPill(String(status || 'unknown'), { tone, small });
266
264
  }
267
265
 
268
266
  // `removable` is passed ONLY for "Blocked by" (task 1002858), and the asymmetry
@@ -274,7 +272,7 @@
274
272
  // task and remove it there.
275
273
  function depList(kindWord, items, { removable = false } = {}) {
276
274
  const rows = items.map((d) => {
277
- const status = d.status ? statusPill(d.status, 'rec-pill--sm') : '';
275
+ const status = d.status ? statusPill(d.status, { small: true }) : '';
278
276
  const cut = removable
279
277
  ? `<button class="rec-dep__cut" type="button" data-cut-dep="${escapeHtml(String(d.id))}" title="Stop waiting on task ${escapeHtml(String(d.id))}" aria-label="Stop waiting on task ${escapeHtml(String(d.id))}">×</button>`
280
278
  : '';
@@ -8,11 +8,7 @@
8
8
  /* Severity / status pills. Base: the shell's .fact-pill (task 1003307). A
9
9
  severity is a state, so it is a hairline in its own colour rather than a
10
10
  filled chip — four filled severities in one row was the loudest thing here. */
11
- .badge--sev-critical { color: var(--danger); }
12
- .badge--sev-high { color: var(--warn); }
13
- .badge--sev-medium { color: var(--info); }
14
- .badge--sev-low { color: var(--ink-faint); }
15
- .badge--status { color: var(--ink-faint); }
11
+ /* the severity and status badges take the shared pill's tones (controls.css, task 1005086) */
16
12
 
17
13
  /* Numeric cells in a kit ledger read right-aligned and tabular; a cell that
18
14
  warns takes the danger ink. */
@@ -0,0 +1,26 @@
1
+ /* modules/hall-ui/specimens/components.css — the specimen's own LAYOUT only
2
+ (task 1005086). Every component on the page is drawn by the hall's shared
3
+ sheets; this lays them out in sections and stops the live toast and the
4
+ dialog from floating, so each sits at rest where it can be looked at. */
5
+ body[data-page="components"] { margin: 0; color: var(--ink); background: var(--bg); font-family: var(--font-body); }
6
+ .spec-head, .spec-main { box-sizing: border-box; max-width: 980px; margin: 0 auto; padding: 0 var(--ds-space-md); }
7
+ .spec-head { padding-top: var(--ds-space-sm); padding-bottom: var(--ds-space-hair); }
8
+ .spec-kicker { display: inline-block; margin: 0 var(--ds-space-xs) 0 0; vertical-align: middle; font-family: var(--label-face); font-size: var(--label-size); font-weight: 400; letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-faint); }
9
+ .spec-title { margin: 0; font-family: var(--font-display); font-size: var(--ds-text-title); font-weight: 300; }
10
+ .spec-lede { margin: 0 0 var(--ds-space-md); max-width: 60ch; font-size: var(--ds-text-md); color: var(--ink-soft); }
11
+ .spec-sub { margin: var(--ds-space-md) 0 var(--ds-space-xs); font-size: var(--ds-text-sm); font-weight: 500; color: var(--ink-soft); }
12
+ .spec-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ds-space-xs); margin: var(--ds-space-xs) 0; }
13
+ .spec-note { margin: 0; font-size: var(--ds-text-sm); color: var(--ink-soft); }
14
+ .spec-tile { margin: var(--ds-space-sm) 0 0; }
15
+ .spec-field { display: flex; flex-direction: column; gap: var(--ds-space-hair); max-width: 360px; margin-top: var(--ds-space-sm); font-size: var(--ds-text-sm); color: var(--ink-soft); }
16
+ .spec-field input { width: 100%; }
17
+ .spec-toasts { display: flex; flex-direction: column; align-items: flex-start; gap: var(--ds-space-xs); }
18
+ .spec-toast.toast { position: static; transform: none; max-width: 100%; box-sizing: border-box; }
19
+ .spec-toast .spec-note { color: inherit; }
20
+ .spec-dialog { padding: var(--ds-space-sm); border-radius: var(--ds-radius-tile); background: var(--scrim); }
21
+ .spec-dialog .hall-dialog { position: static; width: 100%; max-width: 480px; margin: 0 auto; }
22
+ @media (max-width: 760px) {
23
+ .spec-head, .spec-main { padding: 0 var(--ds-space-xs); }
24
+ .spec-head { padding-top: var(--ds-space-sm); }
25
+ .spec-main .scroll { padding: var(--ds-space-sm); }
26
+ }
@@ -0,0 +1,115 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Hall components</title>
7
+ <!-- THE HALL'S COMPONENTS, EACH IN EACH STATE (task 1005086, goal 1000138).
8
+ A specimen, not a hall page: the real server never serves this folder. It
9
+ loads the hall's own sheets and scripts by their module path, so what it
10
+ shows is what every hall page draws, and the ui-design kit renders it in
11
+ both modes at every width:
12
+ node modules/ui-design/kit/render.js --states modules/hall-ui/specimens/components.states.json
13
+ The public design-system reference (task 1005083) is meant to pick this up.
14
+ Nothing here is styled for itself except the specimen's own layout
15
+ (components.css): every component is the shared rule. -->
16
+ <link rel="stylesheet" href="/modules/hall-ui/public/style.css">
17
+ <link rel="stylesheet" href="components.css">
18
+ </head>
19
+ <body data-page="components">
20
+ <header class="spec-head">
21
+ <h1 class="spec-title"><span class="spec-kicker">The hall</span> Components</h1>
22
+ </header>
23
+ <main class="spec-main">
24
+ <p class="spec-lede">Each shared component in each of its tones and states. The page is drawn by the same sheets every hall page loads.</p>
25
+
26
+ <section class="scroll spec-sec" aria-labelledby="sp-pill">
27
+ <h2 class="scroll__h" id="sp-pill">Status pill</h2>
28
+ <p class="scroll__lede">Six tones. A status is always a word as well as a colour.</p>
29
+ <div class="spec-row" data-spec="tones">
30
+ <span class="status-pill status-pill--ok">ok</span>
31
+ <span class="status-pill status-pill--warn">warn</span>
32
+ <span class="status-pill status-pill--danger">danger</span>
33
+ <span class="status-pill status-pill--info">info</span>
34
+ <span class="status-pill status-pill--neutral">neutral</span>
35
+ <span class="status-pill status-pill--accent">accent</span>
36
+ </div>
37
+ <div class="spec-row" data-spec="small">
38
+ <span class="status-pill status-pill--ok status-pill--sm">ok</span>
39
+ <span class="status-pill status-pill--warn status-pill--sm">warn</span>
40
+ <span class="status-pill status-pill--danger status-pill--sm">danger</span>
41
+ <span class="status-pill status-pill--info status-pill--sm">info</span>
42
+ <span class="status-pill status-pill--sm">neutral</span>
43
+ </div>
44
+ <h3 class="spec-sub">A task's status, from the one map (OTB.statusPill)</h3>
45
+ <div class="spec-row" id="spec-task-statuses" data-spec="task-statuses"></div>
46
+ </section>
47
+
48
+ <section class="scroll spec-sec" aria-labelledby="sp-panel">
49
+ <h2 class="scroll__h" id="sp-panel">Panel</h2>
50
+ <p class="scroll__lede">The card ground, the panel corner, no shadow. This section is one.</p>
51
+ <div class="scroll scroll--quiet spec-tile"><p class="spec-note">A quiet tile inside a panel recedes by tone.</p></div>
52
+ </section>
53
+
54
+ <section class="scroll spec-sec" aria-labelledby="sp-empty">
55
+ <h2 class="scroll__h" id="sp-empty">Empty state</h2>
56
+ <div class="empty-state"><p>Nothing is waiting for you.</p><a class="empty-state__action" href="#sp-empty">See the whole queue</a></div>
57
+ </section>
58
+
59
+ <section class="scroll spec-sec" aria-labelledby="sp-loading">
60
+ <h2 class="scroll__h" id="sp-loading">Loading</h2>
61
+ <p class="hall-loading" role="status">Reading the queue…</p>
62
+ <p class="spec-row"><span class="hall-loading hall-loading--inline" role="status">Saving…</span></p>
63
+ </section>
64
+
65
+ <section class="scroll spec-sec" aria-labelledby="sp-toast">
66
+ <h2 class="scroll__h" id="sp-toast">Toast</h2>
67
+ <p class="scroll__lede">One toast; how long it stays depends on its kind, and an error never shows a server address.</p>
68
+ <div class="spec-toasts" id="spec-toasts"></div>
69
+ </section>
70
+
71
+ <section class="scroll spec-sec" aria-labelledby="sp-tabs">
72
+ <h2 class="scroll__h" id="sp-tabs">Tabs and the view toggle</h2>
73
+ <div class="kit-tabs" role="tablist" aria-label="Specimen tabs">
74
+ <button type="button" class="setup__tab is-active" role="tab" aria-selected="true">ready <span class="count-pill">4</span></button>
75
+ <button type="button" class="setup__tab" role="tab" aria-selected="false">active</button>
76
+ <button type="button" class="setup__tab" role="tab" aria-selected="false">done</button>
77
+ </div>
78
+ <div class="viewtoggle" role="group" aria-label="View">
79
+ <button type="button" class="viewtoggle__btn" aria-pressed="true">rows</button>
80
+ <button type="button" class="viewtoggle__btn" aria-pressed="false">cards</button>
81
+ </div>
82
+ </section>
83
+
84
+ <section class="scroll spec-sec" aria-labelledby="sp-controls">
85
+ <h2 class="scroll__h" id="sp-controls">Buttons and the field</h2>
86
+ <div class="spec-row">
87
+ <button type="button" class="btn">claim it</button>
88
+ <button type="button" class="btn btn--ghost">later</button>
89
+ <button type="button" class="btn btn--danger">release</button>
90
+ <button type="button" class="btn" disabled>claimed</button>
91
+ </div>
92
+ <div class="spec-row">
93
+ <button type="button" class="btn btn--sm">claim</button>
94
+ <button type="button" class="btn btn--ghost btn--sm">release</button>
95
+ <button type="button" class="btn btn--text">read the brief</button>
96
+ </div>
97
+ <label class="spec-field">A field <input type="text" value="A sentence someone typed"></label>
98
+ </section>
99
+
100
+ <section class="scroll spec-sec" aria-labelledby="sp-dialog">
101
+ <h2 class="scroll__h" id="sp-dialog">Dialog</h2>
102
+ <div class="spec-dialog">
103
+ <div class="hall-dialog" role="dialog" aria-modal="false" aria-labelledby="sp-dialog-t">
104
+ <div class="hall-dialog__head"><h3 class="hall-dialog__title" id="sp-dialog-t">Release task 1005086?</h3></div>
105
+ <div class="hall-dialog__body"><p>It returns to the queue and no credits are earned.</p></div>
106
+ <div class="hall-dialog__actions"><button type="button" class="btn btn--ghost">cancel</button><button type="button" class="btn btn--danger">release</button></div>
107
+ </div>
108
+ </div>
109
+ </section>
110
+
111
+ </main>
112
+ <script src="/modules/hall-ui/public/dom-utils.js"></script>
113
+ <script src="components.js"></script>
114
+ </body>
115
+ </html>
@@ -0,0 +1,27 @@
1
+ // modules/hall-ui/specimens/components.js — fills the parts of the components
2
+ // specimen (task 1005086) that come from the hall's shared JavaScript, so the
3
+ // specimen shows what the code draws rather than a hand copy of it:
4
+ // - every task status through OTB.statusPill (dom-utils.js, the one map);
5
+ // - one toast per kind, each through OTB.plainMessage, at rest on the page
6
+ // (the live toast is fixed to the bottom of the window and leaves).
7
+ (function () {
8
+ 'use strict';
9
+ var OTB = window.OTB;
10
+ var statuses = Object.keys(OTB.STATUS_TONE);
11
+ var row = document.getElementById('spec-task-statuses');
12
+ if (row) row.innerHTML = statuses.map(function (s) { return OTB.statusPill(s); }).join(' ');
13
+
14
+ var toasts = document.getElementById('spec-toasts');
15
+ if (toasts) {
16
+ var said = [
17
+ ['info', 'Copied the prompt.'],
18
+ ['ok', 'Task 1005086 claimed.'],
19
+ ['warn', 'The board is paused while the owner reviews it.'],
20
+ ['error', 'Could not reach https://10.0.0.5:8443/api/bongos/claims: try again in a moment.'],
21
+ ];
22
+ toasts.innerHTML = said.map(function (k) {
23
+ return '<p class="toast spec-toast" data-visible="1" data-kind="' + k[0] + '">'
24
+ + OTB.escapeHtml(OTB.plainMessage(k[1])) + ' <span class="spec-note">(' + k[0] + ', ' + (OTB.TOAST_MS[k[0]] / 1000) + 's)</span></p>';
25
+ }).join('');
26
+ }
27
+ })();