@bongos/core 1.21.174 → 1.21.175

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.
@@ -87,6 +87,7 @@
87
87
  "files": [
88
88
  "modules/hall-ui/public/blockers.html",
89
89
  "modules/hall-ui/public/style.css",
90
+ "modules/hall-ui/public/hall-ask.css",
90
91
  "modules/hall-ui/public/oversight.css",
91
92
  "modules/hall-ui/public/shell-trail.js",
92
93
  "modules/hall-ui/public/craft-mode.js",
@@ -97,6 +98,7 @@
97
98
  "modules/hall-ui/public/hall-kit.js",
98
99
  "modules/hall-ui/public/task-detail.js",
99
100
  "modules/hall-ui/public/blockers-lib.js",
101
+ "modules/hall-ui/public/hall-ask.js",
100
102
  "modules/hall-ui/public/blockers.js"
101
103
  ],
102
104
  "states_files": [],
@@ -115,6 +117,7 @@
115
117
  "files": [
116
118
  "modules/hall-ui/public/board-room.html",
117
119
  "modules/hall-ui/public/style.css",
120
+ "modules/hall-ui/public/hall-ask.css",
118
121
  "modules/hall-ui/public/oversight.css",
119
122
  "modules/hall-ui/public/board-room.css",
120
123
  "modules/hall-ui/public/shell-trail.js",
@@ -578,6 +581,7 @@
578
581
  "files": [
579
582
  "modules/hall-ui/public/index.html",
580
583
  "modules/hall-ui/public/style.css",
584
+ "modules/hall-ui/public/hall-ask.css",
581
585
  "modules/hall-ui/public/dom-utils.js",
582
586
  "modules/hall-ui/public/task-detail.js",
583
587
  "modules/hall-ui/public/brand-apply.js",
@@ -591,6 +595,7 @@
591
595
  "modules/hall-ui/public/hall-widgets.js",
592
596
  "modules/hall-ui/public/genesis-home.js",
593
597
  "modules/hall-ui/public/goals.js",
598
+ "modules/hall-ui/public/hall-ask.js",
594
599
  "modules/hall-ui/public/blockers-lib.js",
595
600
  "modules/hall-ui/public/goal-inbox.js",
596
601
  "modules/hall-ui/public/hall-render.js",
@@ -5,7 +5,7 @@
5
5
  "adr": "0341 D3",
6
6
  "reread": "node scripts/gds/page-reader.js --stale",
7
7
  "how": ["Generated by node scripts/gds/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
8
- "counts": {"pages":46,"lines":5405,"placed":1698,"shared":399,"unplaced":3308,"omitted_data":3498},
8
+ "counts": {"pages":46,"lines":5407,"placed":1698,"shared":399,"unplaced":3310,"omitted_data":3498},
9
9
  "pages": [
10
10
  {
11
11
  "id": "builders:atlas",
@@ -50,9 +50,9 @@
50
50
  "surface": "builders",
51
51
  "title": "Blockers",
52
52
  "states_read": ["default"],
53
- "files_hash": "f5bf3c2244e3fbdd",
54
- "reading_hash": "2b3fd98b7ba1bff6",
55
- "counts": {"lines":28,"placed":12,"shared":8,"unplaced":8},
53
+ "files_hash": "6bbb72d61775d17b",
54
+ "reading_hash": "4fcc4d52c13cf804",
55
+ "counts": {"lines":30,"placed":12,"shared":8,"unplaced":10},
56
56
  "omitted": {"data":9},
57
57
  "notes": [],
58
58
  "lines": [
@@ -64,26 +64,28 @@
64
64
  {"key":"L0006","section":"top bar","text":"Artistry","placement":"shared"},
65
65
  {"key":"L0007","section":"top bar","text":"Ideating","placement":"shared"},
66
66
  {"key":"L0008","section":"Blockers","text":"Blockers","placement":"unplaced"},
67
- {"key":"L0009","section":"Blockers","text":"A credential, a purchase, a product call, etc. gated at the owner level. Any tasks awaiting the same block will unblock automatically on resolution.","placement":"placed","file":"modules/hall-ui/public/blockers.html","line":26,"string_id":"715edae2bf8d"},
68
- {"key":"L0010","section":"Open {…}","text":"Open blockers","placement":"placed","file":"modules/hall-ui/public/blockers.html","line":36,"string_id":"ade0e39c65e9"},
69
- {"key":"L0011","section":"Open {…}","text":"+ File blocker","placement":"placed","file":"modules/hall-ui/public/blockers.js","line":303,"string_id":"32e5b4833f55"},
70
- {"key":"L0012","section":"Open {…}","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
71
- {"key":"L0013","section":"Open {…}","text":"Filed {…} · Blocking nothing yet","placement":"unplaced","occurrences":3},
72
- {"key":"L0014","section":"Open {…}","text":"Resolve","placement":"unplaced","occurrences":9},
73
- {"key":"L0015","section":"Open {…}","text":"Link a task","placement":"placed","file":"modules/hall-ui/public/blockers.js","line":148,"string_id":"b8ba1fa4457c","occurrences":9},
74
- {"key":"L0016","section":"Open {…}","text":"Filed {…} · Blocking 1 {…}:","placement":"unplaced","occurrences":6},
75
- {"key":"L0017","section":"Open {…}","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232"},
76
- {"key":"L0018","section":"Open {…}","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5"},
77
- {"key":"L0019","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc"},
78
- {"key":"L0020","section":"Open {…}","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":674,"string_id":"fd1903586e8e"},
79
- {"key":"L0021","section":"What are you doing now?","text":"What are you doing now?","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":675,"string_id":"60853319f290"},
80
- {"key":"L0022","section":"What are you doing now?","text":"Pick one. You can switch any time from the top bar.","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":676,"string_id":"4f87d5b17b54"},
81
- {"key":"L0023","section":"What are you doing now?","text":"Building","placement":"unplaced"},
82
- {"key":"L0024","section":"What are you doing now?","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":3},
83
- {"key":"L0025","section":"What are you doing now?","text":"Artistry","placement":"unplaced"},
84
- {"key":"L0026","section":"What are you doing now?","text":"Ideating","placement":"unplaced"},
85
- {"key":"L0027","section":"What are you doing now?","text":"Turn thoughts into ideas.","placement":"unplaced"},
86
- {"key":"L0028","section":"What are you doing now?","text":"Ask me again next session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":679,"string_id":"fa86bf403775"}
67
+ {"key":"L0009","section":"Blockers","text":"A credential, a purchase, a product call, etc. gated at the owner level. Any tasks awaiting the same block will unblock automatically on resolution.","placement":"placed","file":"modules/hall-ui/public/blockers.html","line":27,"string_id":"715edae2bf8d"},
68
+ {"key":"L0010","section":"Open {…}","text":"Open blockers","placement":"placed","file":"modules/hall-ui/public/blockers.html","line":37,"string_id":"ade0e39c65e9"},
69
+ {"key":"L0011","section":"Open {…}","text":"+ File blocker","placement":"placed","file":"modules/hall-ui/public/blockers.js","line":316,"string_id":"32e5b4833f55"},
70
+ {"key":"L0012","section":"Open {…}","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5","occurrences":10},
71
+ {"key":"L0013","section":"Open {…}","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
72
+ {"key":"L0014","section":"Open {…}","text":"Filed {…} · Blocking nothing yet","placement":"unplaced","occurrences":3},
73
+ {"key":"L0015","section":"Open {…}","text":"Resolve it","placement":"unplaced","occurrences":9},
74
+ {"key":"L0016","section":"Open {…}","text":"Say what settled it","placement":"unplaced","occurrences":9},
75
+ {"key":"L0017","section":"Open {…}","text":"Link a task","placement":"placed","file":"modules/hall-ui/public/blockers.js","line":161,"string_id":"b8ba1fa4457c","occurrences":9},
76
+ {"key":"L0018","section":"Open {…}","text":"Name the {…} it holds up","placement":"unplaced","occurrences":9},
77
+ {"key":"L0019","section":"Open {…}","text":"Filed {…} · Blocking 1 {…}:","placement":"unplaced","occurrences":6},
78
+ {"key":"L0020","section":"Open {…}","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232"},
79
+ {"key":"L0021","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc"},
80
+ {"key":"L0022","section":"Open {…}","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":674,"string_id":"fd1903586e8e"},
81
+ {"key":"L0023","section":"What are you doing now?","text":"What are you doing now?","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":675,"string_id":"60853319f290"},
82
+ {"key":"L0024","section":"What are you doing now?","text":"Pick one. You can switch any time from the top bar.","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":676,"string_id":"4f87d5b17b54"},
83
+ {"key":"L0025","section":"What are you doing now?","text":"Building","placement":"unplaced"},
84
+ {"key":"L0026","section":"What are you doing now?","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":3},
85
+ {"key":"L0027","section":"What are you doing now?","text":"Artistry","placement":"unplaced"},
86
+ {"key":"L0028","section":"What are you doing now?","text":"Ideating","placement":"unplaced"},
87
+ {"key":"L0029","section":"What are you doing now?","text":"Turn thoughts into ideas.","placement":"unplaced"},
88
+ {"key":"L0030","section":"What are you doing now?","text":"Ask me again next session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":679,"string_id":"fa86bf403775"}
87
89
  ]
88
90
  },
89
91
  {
@@ -91,22 +93,22 @@
91
93
  "surface": "builders",
92
94
  "title": "Board Room",
93
95
  "states_read": ["room","out","sitting-meta","objection-form"],
94
- "files_hash": "33d6388010313047",
96
+ "files_hash": "8c30d46056493f66",
95
97
  "reading_hash": "bb827082275d1b73",
96
98
  "counts": {"lines":41,"placed":19,"shared":8,"unplaced":14},
97
99
  "omitted": {"data":11},
98
100
  "notes": ["sitting-meta: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood","objection-form: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood"],
99
101
  "lines": [
100
102
  {"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":872,"string_id":"87cee78ca134"},
101
- {"key":"L0002","section":"top bar","text":"Board Room","placement":"shared","file":"modules/hall-ui/public/board-room.html","line":19,"string_id":"118026595c16"},
103
+ {"key":"L0002","section":"top bar","text":"Board Room","placement":"shared","file":"modules/hall-ui/public/board-room.html","line":20,"string_id":"118026595c16"},
102
104
  {"key":"L0003","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":544,"string_id":"eba610d25523"},
103
105
  {"key":"L0004","section":"top bar","text":"Ctrl K","placement":"shared"},
104
106
  {"key":"L0005","section":"top bar","text":"Building","placement":"shared"},
105
107
  {"key":"L0006","section":"top bar","text":"Artistry","placement":"shared"},
106
108
  {"key":"L0007","section":"top bar","text":"Ideating","placement":"shared"},
107
- {"key":"L0008","section":"Board Room","text":"Board Room","placement":"placed","file":"modules/hall-ui/public/board-room.html","line":19,"string_id":"118026595c16"},
109
+ {"key":"L0008","section":"Board Room","text":"Board Room","placement":"placed","file":"modules/hall-ui/public/board-room.html","line":20,"string_id":"118026595c16"},
108
110
  {"key":"L0009","section":"Board Room","text":"Nothing is before the {…}.","placement":"unplaced"},
109
- {"key":"L0010","section":"Board Room","text":"Read an idea, then decide. An objection needs a reason and the section it faults.","placement":"placed","file":"modules/hall-ui/public/board-room.html","line":51,"string_id":"fae2f90927bd"},
111
+ {"key":"L0010","section":"Board Room","text":"Read an idea, then decide. An objection needs a reason and the section it faults.","placement":"placed","file":"modules/hall-ui/public/board-room.html","line":52,"string_id":"fae2f90927bd"},
110
112
  {"key":"L0011","section":"Board Room","text":"Nothing is before the {…}. When a Full Idea clears the completeness grade, its window opens here.","placement":"unplaced"},
111
113
  {"key":"L0012","section":"Board Room","text":"Closed sittings","placement":"unplaced"},
112
114
  {"key":"L0013","section":"{…} project{…}","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5","occurrences":2},
@@ -1440,7 +1442,7 @@
1440
1442
  "surface": "builders",
1441
1443
  "title": "Builders' Hall",
1442
1444
  "states_read": ["home","standing","standing-karma","out","founding","genesis","decide","demo"],
1443
- "files_hash": "e32eb071e46bec38",
1445
+ "files_hash": "59b17bf6a34ffe16",
1444
1446
  "reading_hash": "42dbab4f3948fe66",
1445
1447
  "counts": {"lines":117,"placed":63,"shared":10,"unplaced":44},
1446
1448
  "omitted": {"data":101},
@@ -1475,7 +1477,7 @@
1475
1477
  {"key":"L0027","section":"Goals","text":"Goals","placement":"unplaced"},
1476
1478
  {"key":"L0028","section":"Goals","text":"You haven’t joined any open goals.","placement":"placed","file":"modules/hall-ui/public/goals.js","line":100,"string_id":"044387a6f90c"},
1477
1479
  {"key":"L0029","section":"Goals","text":"Browse goals →","placement":"placed","file":"modules/hall-ui/public/goals.js","line":100,"string_id":"da910fee01d7"},
1478
- {"key":"L0030","section":"Needs your attention13","text":"Needs your attention","placement":"placed","file":"modules/hall-ui/public/goal-inbox.js","line":597,"string_id":"7def635421ea"},
1480
+ {"key":"L0030","section":"Needs your attention13","text":"Needs your attention","placement":"placed","file":"modules/hall-ui/public/goal-inbox.js","line":580,"string_id":"7def635421ea"},
1479
1481
  {"key":"L0031","section":"Needs your attention13","text":"From the system","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":721,"string_id":"d32c5e058d1e"},
1480
1482
  {"key":"L0032","section":"Needs your attention13","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":895,"string_id":"e181637c5a5e"},
1481
1483
  {"key":"L0033","section":"Needs your attention13","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5"},
@@ -9,6 +9,7 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
+ <link rel="stylesheet" href="/builders/hall-ask.css">
12
13
  <!-- task 1003455: the row shape (title + facts) is the oversight family's,
13
14
  the same archetype collab/copy-desk/watch/gate already ride. -->
14
15
  <link rel="stylesheet" href="/builders/oversight.css">
@@ -65,6 +66,7 @@
65
66
  for the "blocking N tasks" facts line, but not the overlay's own render. -->
66
67
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
67
68
  <script src="/builders/blockers-lib.js"></script>
69
+ <script src="/builders/hall-ask.js"></script>
68
70
  <script src="/builders/blockers.js"></script>
69
71
  </body>
70
72
  </html>
@@ -142,6 +142,19 @@
142
142
  + '<button type="submit" class="td-btn td-btn--primary" data-submit>Link</button>'
143
143
  + '<button type="button" class="td-btn" data-row-cancel>Cancel</button>'
144
144
  + '</div><p class="td-action-note" data-err></p></form></div>';
145
+ } else if (window.OTBAsk) {
146
+ // task 1004820: a closed row a reviewer can act on is the city's ask card
147
+ return window.OTBAsk.html({
148
+ attrs: { 'data-id': id },
149
+ where: 'Waiting on the project owner',
150
+ q: b.title || ('Blocker ' + id), qHref: blockerHref(id),
151
+ saidHtml: facts.noteHtml + '<div class="ov-row__facts">' + facts.factsHtml + '</div>',
152
+ row: true,
153
+ opts: [
154
+ { label: 'Resolve it', result: 'Say what settled it', rec: true, attrs: { 'data-resolve-open': id } },
155
+ { label: 'Link a task', result: 'Name the task it holds up', attrs: { 'data-link-open': id } },
156
+ ],
157
+ });
145
158
  } else {
146
159
  right = '<div class="ov-row__right"><div class="ov-row__actions">'
147
160
  + '<button type="button" class="btn-ghost" data-resolve-open="' + kit.esc(id) + '">Resolve</button>'
@@ -76,3 +76,8 @@
76
76
  .gov-vote__sections { display: flex; flex-wrap: wrap; gap: 6px 16px; }
77
77
  .gov-vote__section { display: inline-flex; align-items: center; gap: 8px; min-height: 24px; font-size: 13px; color: var(--ink); cursor: pointer; }
78
78
  .gov-vote__section input { width: 16px; height: 16px; margin: 0; }
79
+ /* task 1004820: the vote's two answers are the city's ask-card chips */
80
+ .gov-vote > .hask__opt { flex: 1 1 220px; width: auto; }
81
+ .gov-vote__no { flex: 1 1 220px; }
82
+ .gov-vote__no > summary.hask__opt { list-style: none; }
83
+ .gov-vote__no > summary.hask__opt::-webkit-details-marker { display: none; }
@@ -9,6 +9,7 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
+ <link rel="stylesheet" href="/builders/hall-ask.css">
12
13
  <link rel="stylesheet" href="/builders/oversight.css">
13
14
  <!-- The room's own sheet: the sitting card and the ballot controls moved here
14
15
  out of government.css with the room (ADR 0266), so neither page carries the
@@ -378,9 +378,9 @@
378
378
  : '';
379
379
  return `<div class="gov-vote" data-item="${escapeHtml(item.id)}">
380
380
  ${ownNote}
381
- <button type="button" class="btn-accent" data-vote="yes">Vote yes</button>
381
+ <button type="button" class="hask__opt hask__opt--rec" data-vote="yes"><span class="hask__label">Vote yes</span><span class="hask__result">It counts toward passing</span></button>
382
382
  <details class="gov-vote__no">
383
- <summary class="btn-ghost">Object…</summary>
383
+ <summary class="hask__opt"><span class="hask__label">Object</span><span class="hask__result">It goes back to its author with your reason</span></summary>
384
384
  <p class="gov-grid-note">This sends the idea back to its author. Say why, and which section.</p>
385
385
  <textarea data-vote-reason placeholder="Why — the reason the author will act on" maxlength="5000" rows="3"></textarea>
386
386
  <div class="gov-vote__sections">${sections}</div>
@@ -46,37 +46,36 @@
46
46
 
47
47
  // The goal-detail hash route on the Goals page (matches goals.js's row link).
48
48
  const goalHref = (goalId) => `/goals#/goal/${encodeURIComponent(goalId)}`;
49
- const noteHtml = (note) => (note ? `<p class="ginbox__note">“${escapeHtml(note)}”</p>` : '');
50
49
 
51
50
  function invitationRow(inv) {
52
51
  const who = inv.invited_by_name || inv.invited_by_login || 'someone';
53
- return `<li class="ginbox__item" data-goal="${escapeHtml(String(inv.goal_id))}">
54
- <div class="ginbox__body">
55
- <a class="ginbox__goal" href="${goalHref(inv.goal_id)}" title="Open ${escapeHtml(inv.goal_title || 'goal')}">${escapeHtml(inv.goal_title || 'a goal')}</a>
56
- <span class="ginbox__sub">invited by ${escapeHtml(who)}</span>
57
- ${noteHtml(inv.note)}
58
- </div>
59
- <div class="ginbox__actions">
60
- <button class="btn-ghost" type="button" data-accept-inv="${escapeHtml(String(inv.id))}">Accept</button>
61
- <button class="btn-ghost" type="button" data-decline-inv="${escapeHtml(String(inv.id))}">Decline</button>
62
- </div>
63
- </li>`;
52
+ const id = String(inv.id);
53
+ return window.OTBAsk.html({
54
+ cls: 'ginbox__item', attrs: { 'data-goal': String(inv.goal_id) },
55
+ where: `Invited by ${who}`,
56
+ q: `Join ${inv.goal_title || 'a goal'}?`, qHref: goalHref(inv.goal_id),
57
+ said: inv.note ? `“${inv.note}”` : '', row: true,
58
+ opts: [
59
+ { label: 'Join', result: 'You can claim its work', rec: true, attrs: { 'data-accept-inv': id } },
60
+ { label: 'No thanks', result: 'The invitation closes', attrs: { 'data-decline-inv': id } },
61
+ ],
62
+ });
64
63
  }
65
64
 
66
65
  function requestRow(r) {
67
66
  const who = r.display_name || r.github_login || 'a builder';
68
- const rank = r.rank ? ` · ${escapeHtml(rankLabel(r.rank, { empty: '' }))}` : '';
69
- return `<li class="ginbox__item" data-goal="${escapeHtml(String(r.goal_id))}">
70
- <div class="ginbox__body">
71
- <a class="ginbox__goal" href="${goalHref(r.goal_id)}" title="Open ${escapeHtml(r.goal_title || 'goal')}">${escapeHtml(r.goal_title || 'a goal')}</a>
72
- <span class="ginbox__sub">${escapeHtml(who)}${rank} asks to join</span>
73
- ${noteHtml(r.note)}
74
- </div>
75
- <div class="ginbox__actions">
76
- <button class="btn-ghost" type="button" data-approve-req="${escapeHtml(String(r.id))}">Approve</button>
77
- <button class="btn-ghost" type="button" data-deny-req="${escapeHtml(String(r.id))}">Deny</button>
78
- </div>
79
- </li>`;
67
+ const rank = r.rank ? ` · ${rankLabel(r.rank, { empty: '' })}` : '';
68
+ const id = String(r.id);
69
+ return window.OTBAsk.html({
70
+ cls: 'ginbox__item', attrs: { 'data-goal': String(r.goal_id) },
71
+ where: `${r.goal_title || 'A goal'}${rank}`,
72
+ q: `Let ${who} join?`, qHref: goalHref(r.goal_id),
73
+ said: r.note ? `“${r.note}”` : '', row: true,
74
+ opts: [
75
+ { label: 'Let them in', result: 'They can claim its work', rec: true, attrs: { 'data-approve-req': id } },
76
+ { label: 'Not now', result: 'The request closes', attrs: { 'data-deny-req': id } },
77
+ ],
78
+ });
80
79
  }
81
80
 
82
81
  function pendingLine(myPending) {
@@ -289,20 +288,16 @@
289
288
  // non-triager's fetch 403s and the slice renders nothing. One gate, on the
290
289
  // server, beats a client check that can disagree with it.
291
290
  function nodRowHtml(r) {
292
- const title = escapeHtml(r.title || `task #${r.task_id}`);
293
- const kind = r.kind ? `<span class="ginbox__sub">${escapeHtml(r.kind)}</span>` : '';
294
- const who = r.filed_by_login ? ` · filed by ${escapeHtml(r.filed_by_login)}` : '';
295
- const goal = r.goal_title ? escapeHtml(r.goal_title) : `goal ${escapeHtml(String(r.goal_id))}`;
296
- return `<li class="ginbox__item" data-goal="${escapeHtml(String(r.goal_id))}">
297
- <div class="ginbox__body">
298
- <a class="ginbox__goal" href="/task/${encodeURIComponent(r.task_id)}">${title}</a>
299
- <span class="ginbox__sub">${goal}${who}</span>
300
- </div>
301
- ${kind}
302
- <div class="ginbox__actions">
303
- <button class="btn-ghost" type="button" data-nod-task="${escapeHtml(String(r.task_id))}">Give the nod</button>
304
- </div>
305
- </li>`;
291
+ const goal = r.goal_title || `goal ${r.goal_id}`;
292
+ const who = r.filed_by_login ? ` · filed by ${r.filed_by_login}` : '';
293
+ return window.OTBAsk.html({
294
+ cls: 'ginbox__item', attrs: { 'data-goal': String(r.goal_id) },
295
+ where: `${goal}${who}`,
296
+ q: r.title || `Task ${r.task_id}`, qHref: `/task/${encodeURIComponent(r.task_id)}`,
297
+ opts: [
298
+ { label: 'Give the nod', result: 'It opens for anyone to claim', rec: true, attrs: { 'data-nod-task': String(r.task_id) } },
299
+ ],
300
+ });
306
301
  }
307
302
 
308
303
  function renderNodQueue(data) {
@@ -366,23 +361,18 @@
366
361
  // One word each brings it back into that band; the sentence each answer means
367
362
  // rides the `title`, and the head already says what the question is.
368
363
  function removalRowHtml(r) {
369
- const title = escapeHtml(r.title || `task #${r.proposal_task_id}`);
370
- const from = r.ship_task_id
371
- ? `deleted by task #${escapeHtml(String(r.ship_task_id))}`
372
- : 'from a ship whose id could not be read';
373
- const goal = r.goal_title ? ` · ${escapeHtml(r.goal_title)}` : '';
374
- return `<li class="ginbox__item ginbox__item--stacked">
375
- <div class="ginbox__body">
376
- <a class="ginbox__goal" href="/task/${encodeURIComponent(r.proposal_task_id)}">${title}</a>
377
- <span class="ginbox__sub">${from}${goal} — open it to see what went</span>
378
- </div>
379
- <div class="ginbox__actions">
380
- <button class="btn-ghost" type="button" data-removal-task="${escapeHtml(String(r.proposal_task_id))}"
381
- data-removal-verdict="confirmed" title="That work is genuinely gone">Confirm</button>
382
- <button class="btn-ghost" type="button" data-removal-task="${escapeHtml(String(r.proposal_task_id))}"
383
- data-removal-verdict="dismissed" title="A refactor, a move, or work already superseded">Dismiss</button>
384
- </div>
385
- </li>`;
364
+ const from = r.ship_task_id ? `Deleted by task ${r.ship_task_id}` : 'Deleted by a ship';
365
+ const id = String(r.proposal_task_id);
366
+ return window.OTBAsk.html({
367
+ cls: 'ginbox__item',
368
+ where: `${from}${r.goal_title ? ' · ' + r.goal_title : ''}`,
369
+ q: r.title || `Task ${r.proposal_task_id}`, qHref: `/task/${encodeURIComponent(r.proposal_task_id)}`,
370
+ row: true,
371
+ opts: [
372
+ { label: 'It is gone', result: 'That work is really removed', attrs: { 'data-removal-task': id, 'data-removal-verdict': 'confirmed' } },
373
+ { label: 'Not removed', result: 'It moved or was replaced', attrs: { 'data-removal-task': id, 'data-removal-verdict': 'dismissed' } },
374
+ ],
375
+ });
386
376
  }
387
377
 
388
378
  function renderRemovals(data) {
@@ -437,29 +427,23 @@
437
427
  }
438
428
 
439
429
  function rotRowHtml(r) {
440
- const href = r.tier === 'goal'
441
- ? goalHref(r.id)
442
- : `/task/${encodeURIComponent(r.id)}`;
443
- const where = r.tier === 'goal'
444
- ? 'goal'
445
- : (r.goal_title ? escapeHtml(r.goal_title) : `goal ${escapeHtml(String(r.goal_id || '—'))}`);
446
- // `watered` is worth showing: a row that is back DESPITE having been watered is
447
- // a different message from one nobody has ever looked at, and hiding that would
448
- // make the second watering feel like the first one did nothing.
430
+ const href = r.tier === 'goal' ? goalHref(r.id) : `/task/${encodeURIComponent(r.id)}`;
431
+ const where = r.tier === 'goal' ? 'Goal' : (r.goal_title || `goal ${r.goal_id || '—'}`);
432
+ // a row back DESPITE a watering says something a first-timer does not
449
433
  const again = r.ever_watered ? ' · watered before' : '';
450
- const buttons = (Array.isArray(r.verbs) ? r.verbs : []).map((v) => {
451
- const path = String(v.path || '').replace(':id', encodeURIComponent(r.id));
452
- return `<button class="btn-ghost" type="button" data-rot-path="${escapeHtml(path)}"`
453
- + ` data-rot-verb="${escapeHtml(String(v.verb || ''))}"`
454
- + ` title="${escapeHtml(String(v.hint || ''))}">${escapeHtml(String(v.label || v.verb || '?'))}</button>`;
455
- }).join('');
456
- return `<li class="ginbox__item" data-rot-tier="${escapeHtml(String(r.tier))}">
457
- <div class="ginbox__body">
458
- <a class="ginbox__goal" href="${escapeHtml(href)}">${escapeHtml(r.title || `${r.tier} #${r.id}`)}</a>
459
- <span class="ginbox__sub">${escapeHtml(String(r.status || ''))} · ${where} · ${escapeHtml(untouchedFor(r.last_touched_at))}${again}</span>
460
- </div>
461
- <div class="ginbox__actions">${buttons}</div>
462
- </li>`;
434
+ const opts = (Array.isArray(r.verbs) ? r.verbs : []).map((v) => ({
435
+ label: String(v.label || v.verb || '?'),
436
+ result: String(v.hint || ''),
437
+ attrs: {
438
+ 'data-rot-path': String(v.path || '').replace(':id', encodeURIComponent(r.id)),
439
+ 'data-rot-verb': String(v.verb || ''),
440
+ },
441
+ }));
442
+ return window.OTBAsk.html({
443
+ cls: 'ginbox__item', attrs: { 'data-rot-tier': String(r.tier) },
444
+ where: `${where} · ${untouchedFor(r.last_touched_at)}${again}`,
445
+ q: r.title || `${r.tier} ${r.id}`, qHref: href, opts,
446
+ });
463
447
  }
464
448
 
465
449
  // What each verb says when it lands. Keyed by the SERVER's verb name so a new verb
@@ -531,20 +515,19 @@
531
515
  titleClass: 'ginbox__goal',
532
516
  noteClass: 'ginbox__note',
533
517
  whenClass: 'ginbox__sub',
534
- // The SAME overlay task-detail.js has rendered since task 1003066's
535
- // predecessor — this slice is what gives it its first door in.
536
518
  esc: window.OTBKit.esc,
537
519
  fmtDate: window.OTBKit.fmtAbsDate,
538
520
  blockerHref: (id) => `#/blocker/${encodeURIComponent(id)}`,
539
521
  taskHref: (id) => (window.OTBTaskDetail ? window.OTBTaskDetail.href(id) : `/task/${encodeURIComponent(id)}`),
540
522
  });
541
- return `<li class="ginbox__item">
542
- <div class="ginbox__body">
543
- ${facts.titleHtml}
544
- ${facts.factsHtml}
545
- ${facts.noteHtml}
546
- </div>
547
- </li>`;
523
+ // Was a link only; now it answers in place: the overlay is where it resolves.
524
+ return window.OTBAsk.html({
525
+ cls: 'ginbox__item',
526
+ where: 'Waiting on the project owner',
527
+ q: b.title || `Blocker ${b.id}`, qHref: `#/blocker/${encodeURIComponent(b.id)}`,
528
+ saidHtml: facts.factsHtml + facts.noteHtml,
529
+ opts: [{ label: 'Resolve it', result: 'Say what settled it', rec: true, href: `#/blocker/${encodeURIComponent(b.id)}` }],
530
+ });
548
531
  }
549
532
 
550
533
  function renderBlockers(data) {
@@ -0,0 +1,57 @@
1
+ /* modules/hall-ui/public/hall-ask.css — THE CITY'S ASK CARD, IN THE HALL
2
+ (task 1004820, criterion wa6-hall-reads-like-home).
3
+ The hall's actions are drawn as the city's ask card (street-deck.css `.ask`):
4
+ where it sits, the question large and light, what was said, then each answer
5
+ as a one-tap option chip carrying its result line. The hall keeps its own
6
+ prefix (`hask`) so a page that loads both sheets never cascades one into the
7
+ other; the tokens are the street's glass chip recipe on the hall's own
8
+ ground, light first and then twice for dark (the hall's switch). */
9
+
10
+ body {
11
+ --hask-chip: color-mix(in srgb, var(--ink) 4%, transparent);
12
+ --hask-chip-pick: color-mix(in srgb, var(--bg-card) 80%, var(--bg));
13
+ --hask-edge: color-mix(in srgb, var(--ink) 9%, transparent);
14
+ --hask-edge-pick: color-mix(in srgb, var(--ink) 34%, transparent);
15
+ }
16
+ html[data-theme="dark"] body {
17
+ --hask-chip: color-mix(in srgb, var(--ink) 5%, transparent);
18
+ --hask-chip-pick: color-mix(in srgb, var(--ink) 11%, transparent);
19
+ --hask-edge: color-mix(in srgb, var(--ink) 11%, transparent);
20
+ --hask-edge-pick: color-mix(in srgb, var(--ink) 42%, transparent);
21
+ }
22
+ @media (prefers-color-scheme: dark) {
23
+ html:not([data-theme]) body {
24
+ --hask-chip: color-mix(in srgb, var(--ink) 5%, transparent);
25
+ --hask-chip-pick: color-mix(in srgb, var(--ink) 11%, transparent);
26
+ --hask-edge: color-mix(in srgb, var(--ink) 11%, transparent);
27
+ --hask-edge-pick: color-mix(in srgb, var(--ink) 42%, transparent);
28
+ }
29
+ }
30
+
31
+ /* the card: one item that waits on the reader */
32
+ .hask { list-style: none; display: flex; flex-direction: column; gap: 12px; padding: 16px 16px 14px; margin: 0 0 10px; box-sizing: border-box; border-radius: 22px; background: var(--hask-chip); border: 1px solid var(--hask-edge); color: var(--ink); }
33
+ .hask:last-child { margin-bottom: 0; }
34
+ .hask__where { display: flex; align-items: center; gap: 8px; min-height: 24px; font-size: 13px; color: var(--ink-soft); min-width: 0; }
35
+ .hask__dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
36
+ .hask__where-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
37
+ .hask__q { margin: 0; font-size: 21px; font-weight: 300; letter-spacing: -0.02em; line-height: 1.2; overflow-wrap: anywhere; }
38
+ .hask__q a { color: inherit; text-decoration: none; }
39
+ .hask__q a:hover { text-decoration: underline; text-underline-offset: 4px; text-decoration-color: var(--hask-edge-pick); }
40
+ .hask__said { margin: -4px 0 0; font-size: 14px; line-height: 1.45; color: var(--ink-soft); }
41
+ .hask__opts { display: flex; flex-direction: column; gap: 8px; }
42
+ .hask__opt { display: grid; grid-template-columns: minmax(0, 1fr); row-gap: 2px; align-items: center; min-height: 52px; width: 100%; padding: 9px 16px; box-sizing: border-box; border-radius: 15px; border: 1px solid var(--hask-edge); background: transparent; color: var(--ink); font-family: var(--font-body); text-align: left; text-decoration: none; cursor: pointer; }
43
+ .hask__opt:hover { background: var(--hask-chip-pick); }
44
+ .hask__opt--rec { background: var(--hask-chip-pick); border-color: var(--hask-edge-pick); }
45
+ .hask__opt[disabled] { cursor: progress; opacity: .6; }
46
+ .hask__opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
47
+ .hask__label { font-size: 15px; font-weight: 500; min-width: 0; }
48
+ .hask__result { font-size: 13px; line-height: 1.35; color: var(--ink-soft); min-width: 0; }
49
+ @media (min-width: 761px) {
50
+ .hask__opts--row { flex-direction: row; }
51
+ .hask__opts--row .hask__opt { flex: 1 1 0; }
52
+ }
53
+
54
+ /* an inbox row drawn as the card keeps .ginbox__item (the count and the
55
+ card's visibility read it); the card's own box wins over the ledger row */
56
+ .ginbox > .hask.ginbox__item { display: flex; flex-direction: column; align-items: stretch; gap: 12px; padding: 16px 16px 14px; border: 1px solid var(--hask-edge); margin: 0 0 10px; }
57
+ .ginbox > .hask.ginbox__item:first-child { border-top: 1px solid var(--hask-edge); }
@@ -0,0 +1,50 @@
1
+ // modules/hall-ui/public/hall-ask.js — THE CITY'S ASK CARD, IN THE HALL
2
+ // (task 1004820, criterion wa6-hall-reads-like-home).
3
+ //
4
+ // One builder for every hall action that waits on the reader: where it sits,
5
+ // the question, what was said, and each answer as a one-tap option chip with
6
+ // its result line (the city's ask card, street-deck.js, in hall-ask.css).
7
+ //
8
+ // It draws markup only. An option carries the SAME data-* attributes the
9
+ // page's delegated click handler already reads, so no action gains a second
10
+ // path: switching a row to the card changes how it looks, never what it calls.
11
+ // An option with `href` is a link (an act that needs its own panel).
12
+ //
13
+ // OTBAsk.html({ where, q, qHref, said, saidHtml, tag, cls, attrs,
14
+ // row: bool, opts: [{ label, result, rec, href, attrs }] })
15
+ //
16
+ // Every string is escaped here; `saidHtml` is the one pre-built slot, for a
17
+ // caller whose facts are already escaped markup (blockerFacts).
18
+ (function () {
19
+ const esc = (s) => String(s == null ? '' : s)
20
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
22
+ const attrsOf = (a) => Object.keys(a || {})
23
+ .filter((k) => a[k] != null && a[k] !== false)
24
+ .map((k) => ` ${k}="${esc(a[k])}"`).join('');
25
+
26
+ function optHtml(o) {
27
+ const cls = 'hask__opt' + (o.rec ? ' hask__opt--rec' : '');
28
+ const inner = `<span class="hask__label">${esc(o.label)}</span>`
29
+ + (o.result ? `<span class="hask__result">${esc(o.result)}</span>` : '');
30
+ return o.href
31
+ ? `<a class="${cls}" href="${esc(o.href)}"${attrsOf(o.attrs)}>${inner}</a>`
32
+ : `<button class="${cls}" type="button"${attrsOf(o.attrs)}>${inner}</button>`;
33
+ }
34
+
35
+ function html(c) {
36
+ const tag = c.tag || 'li';
37
+ const opts = (c.opts || []).filter(Boolean);
38
+ const q = c.qHref ? `<a href="${esc(c.qHref)}">${esc(c.q)}</a>` : esc(c.q);
39
+ return `<${tag} class="hask${c.cls ? ' ' + esc(c.cls) : ''}"${attrsOf(c.attrs)}>`
40
+ + (c.where ? `<div class="hask__where"><span class="hask__dot" aria-hidden="true"></span><span class="hask__where-t">${esc(c.where)}</span></div>` : '')
41
+ + `<h4 class="hask__q">${q}</h4>`
42
+ + (c.saidHtml ? `<div class="hask__said">${c.saidHtml}</div>` : (c.said ? `<p class="hask__said">${esc(c.said)}</p>` : ''))
43
+ + (opts.length ? `<div class="hask__opts${c.row ? ' hask__opts--row' : ''}">${opts.map(optHtml).join('')}</div>` : '')
44
+ + `</${tag}>`;
45
+ }
46
+
47
+ const api = { html, esc };
48
+ if (typeof window !== 'undefined') window.OTBAsk = api;
49
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
50
+ })();
@@ -19,6 +19,7 @@
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
22
+ <link rel="stylesheet" href="/builders/hall-ask.css">
22
23
  </head>
23
24
  <body data-page="home">
24
25
 
@@ -136,6 +137,8 @@
136
137
  <!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
137
138
  both goal-inbox.js's attention-inbox slice below and the /blockers page.
138
139
  Loaded BEFORE goal-inbox.js, which calls it while rendering. -->
140
+ <!-- task 1004820: the city ask card, drawn for every inbox action -->
141
+ <script src="/builders/hall-ask.js" defer></script>
139
142
  <script src="/builders/blockers-lib.js" defer></script>
140
143
  <!-- ADR 0112 (task 1954): the home-page goal-membership inbox — invitations to
141
144
  accept + join-requests to approve. Self-registers; hidden when empty. -->