@bongos/core 1.19.781 → 1.19.783

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.
@@ -110,12 +110,18 @@ function helpRequestsNeed(ctx) {
110
110
  const verb = n === 1 ? 'has' : 'have';
111
111
  return {
112
112
  id: 'help_requests',
113
+ // The number, machine-readable (task 1003942) — the field boardVotesNeed
114
+ // already carries, and the whole badge contract: shell.js names this need's
115
+ // id on the Collab nav item and reads this count into the badge.
116
+ count: n,
113
117
  title: n === 1 ? 'Someone asked for your help' : 'People asked for your help',
114
118
  what: 'A builder who is stuck addressed a request to your craft, or to you by name.',
115
119
  state: 'action_needed',
116
120
  severity: 'info',
117
121
  message: `${people} ${verb} asked for help you can give. Answering is optional and declining costs nothing.`,
118
- action: { label: n === 1 ? 'See the request' : 'See the requests', href: '/#/collab' },
122
+ // Collab is its own page since task 1003172; the retired '/#/collab' hash
123
+ // form pointed at a view the index no longer routes.
124
+ action: { label: n === 1 ? 'See the request' : 'See the requests', href: '/collab' },
119
125
  };
120
126
  }
121
127
 
@@ -152,7 +158,7 @@ function taskRecommendationsNeed(ctx) {
152
158
  state: 'action_needed',
153
159
  severity: 'info',
154
160
  message: `${subject} ${verb} recommended to you. Nothing has been assigned — you would still claim it yourself, and declining costs nothing.`,
155
- action: { label: n === 1 ? 'See the recommendation' : 'See the recommendations', href: '/#/collab' },
161
+ action: { label: n === 1 ? 'See the recommendation' : 'See the recommendations', href: '/collab' },
156
162
  };
157
163
  }
158
164
 
@@ -230,7 +236,7 @@ function mingleIntroductionsNeed(ctx) {
230
236
  severity: 'info',
231
237
  message: `${subject} waiting on you. Saying no costs nothing at all — no rank, no karma, no streak, `
232
238
  + 'and it does not change how often you are paired again.',
233
- action: { label: n === 1 ? 'See the introduction' : 'See the introductions', href: '/#/collab' },
239
+ action: { label: n === 1 ? 'See the introduction' : 'See the introductions', href: '/collab' },
234
240
  };
235
241
  }
236
242
 
@@ -468,6 +468,8 @@ function createClient(opts = {}) {
468
468
  getHealthz: (args) => request("GET", "/healthz", { hasBody: false }, args),
469
469
  },
470
470
  "helpRequests": {
471
+ // GET /help-requests — rank: any-builder — GET /help-requests
472
+ getHelpRequests: (args) => request("GET", "/help-requests", { hasBody: false }, args),
471
473
  // POST /help-requests — rank: any-builder — POST /help-requests
472
474
  postHelpRequests: (args) => request("POST", "/help-requests", { hasBody: true }, args),
473
475
  // PATCH /help-requests/{id} — rank: any-builder — PATCH /help-requests/:id
@@ -128,10 +128,102 @@
128
128
  return { id: 'other', label: 'asked' };
129
129
  }
130
130
 
131
+ // ---- the FACES (task 1003942, owner report: "doesn't feel collaborative") ----
132
+ //
133
+ // A board of builders reaching builders should read as people before it reads
134
+ // as prose, so every row now leads with a face and the top strip stacks the
135
+ // faces of everyone waiting. These are the pure halves: which person a row
136
+ // leads with, what to draw when there is no picture, and the figure strip's
137
+ // numbers. collab.js only turns the result into markup.
138
+
139
+ // Defence in depth on an avatar src (roster.js's rule): escaping alone cannot
140
+ // stop a non-http scheme reaching an attribute the browser resolves, so a src
141
+ // this page did not vouch for renders as the initial instead.
142
+ function httpUrl(u) {
143
+ return /^https?:\/\//i.test(String(u || '')) ? String(u) : null;
144
+ }
145
+
146
+ // The letter a faceless person shows: the first character of their name,
147
+ // uppercased, with a leading @ or # dropped. Never empty — '?' stands in.
148
+ function initialOf(name) {
149
+ var n = String(name == null ? '' : name).replace(/^[@#]/, '').trim();
150
+ return (n.charAt(0) || '?').toUpperCase();
151
+ }
152
+
153
+ // The person a row LEADS with. Incoming ('in') is always the asker — the
154
+ // human waiting on an answer. Outgoing ('out') is who you asked: the one
155
+ // person on a by-name ask, else the craft, drawn as a lettered disc so a craft
156
+ // ask never borrows a person's face. Same dir contract as directionLine.
157
+ function faceOf(req, dir) {
158
+ if (!req) return null;
159
+ if (dir === 'out') {
160
+ if (req.needs_builder_id != null) {
161
+ var to = req.needs_builder_name || req.needs_builder_login || '';
162
+ return {
163
+ kind: 'person', id: req.needs_builder_id, name: to,
164
+ avatar: httpUrl(req.needs_builder_avatar_url), initial: initialOf(to),
165
+ };
166
+ }
167
+ var craft = req.needs_craft ? String(req.needs_craft) : '';
168
+ return { kind: 'craft', id: null, name: craft ? craft + 's' : 'anyone', avatar: null, initial: craft ? initialOf(craft) : '?' };
169
+ }
170
+ var who = askerOf(req);
171
+ return {
172
+ kind: 'person', id: req.requested_by != null ? req.requested_by : null, name: who,
173
+ avatar: httpUrl(req.requested_by_avatar_url), initial: initialOf(who),
174
+ };
175
+ }
176
+
177
+ // The DISTINCT people asking, in list order — the face stack in the top strip.
178
+ // Two asks from one person are one face; keyed by id when the server sent one,
179
+ // by name otherwise, so an older server still dedupes.
180
+ function askers(rows) {
181
+ var seen = {};
182
+ var out = [];
183
+ for (var i = 0; i < (rows || []).length; i += 1) {
184
+ var f = faceOf(rows[i], 'in');
185
+ if (!f) continue;
186
+ var key = f.id != null ? 'id:' + f.id : 'name:' + f.name;
187
+ if (seen[key]) continue;
188
+ seen[key] = true;
189
+ out.push(f);
190
+ }
191
+ return out;
192
+ }
193
+
194
+ // Newest first — the order the three-block board reads in (owner direction,
195
+ // task 1003942: "only show the three most recent, then expand"). The ageing
196
+ // criterion is still met by the band on every row; this is a display order,
197
+ // and the oldest-first comparator stays for the callers that want risk first.
198
+ function byNewestFirst(a, b) {
199
+ return byAgeOldestFirst(b, a);
200
+ }
201
+
202
+ // The first N rows are shown, the rest wait behind "show all": a pure split so
203
+ // the count on the button and the rows it reveals come from one place.
204
+ function splitShown(rows, n) {
205
+ var list = rows || [];
206
+ var cap = typeof n === 'number' && n >= 0 ? n : list.length;
207
+ return { shown: list.slice(0, cap), hidden: list.slice(cap) };
208
+ }
209
+
210
+ // The THIRD-PERSON line, for the board of everyone's asks: neither "you" nor
211
+ // "asked you". "Gamma asked builders" / "Alpha asked Beta by name".
212
+ function boardLine(req) {
213
+ if (!req) return '';
214
+ var who = askerOf(req) || 'Someone';
215
+ if (req.needs_builder_id != null) {
216
+ var to = req.needs_builder_name || req.needs_builder_login;
217
+ return to ? who + ' asked ' + to + ' by name' : who + ' asked one person';
218
+ }
219
+ return req.needs_craft ? who + ' asked ' + req.needs_craft + 's' : who + ' asked';
220
+ }
221
+
131
222
  window.OTBCollabLib = {
132
- BANDS: BANDS, ageBand: ageBand, byAgeOldestFirst: byAgeOldestFirst,
223
+ BANDS: BANDS, ageBand: ageBand, byAgeOldestFirst: byAgeOldestFirst, byNewestFirst: byNewestFirst,
133
224
  askerOf: askerOf, contextOf: contextOf,
134
- directionLine: directionLine,
225
+ directionLine: directionLine, boardLine: boardLine,
135
226
  titleOf: titleOf, groupOf: groupOf,
227
+ httpUrl: httpUrl, initialOf: initialOf, faceOf: faceOf, askers: askers, splitShown: splitShown,
136
228
  };
137
229
  })();
@@ -1,11 +1,18 @@
1
1
  /* modules/hall-ui/public/collab.css — the Collab page (task 1003172 / C02),
2
- rebuilt on the hall's row archetype by task 1003471: the panel is the shell's
3
- .scroll, the row is oversight.css's .ov-row, the age is a .fact-pill, the
4
- context a .fact-chip. This sheet holds only what Collab has and no other
5
- page does — the colour of each age band.
2
+ rebuilt on the hall's row archetype by task 1003471, and made a THREE-BLOCK
3
+ room of people by task 1003942 (owner direction: "way too text heavy, doesn't
4
+ feel collaborative" — asked of you left, your asks right, every open ask
5
+ below, each condensed to the newest three with a show-all). The panel is the
6
+ shell's .scroll, the two-up is the shell's .view-zone--band, the row is
7
+ oversight.css's .ov-row--lead (a face, then the words, then the state), the
8
+ age is a .fact-pill, the context a .fact-chip, the expander the shell's
9
+ .btn-ghost. This sheet holds only what Collab has and no other page does: the
10
+ colour of each age band, the faces, the block head, the lit personal call, and
11
+ the foot.
6
12
  Tokens only from style.css's real set: inventing one renders as nothing,
7
13
  which is how copy-desk.css once ended up with invisible fallbacks (idea
8
- 1000821). */
14
+ 1000821). Every colour here is a var() of the fifteen or the hall's measured
15
+ status vocabulary, so the dark tier needs no twin in this file. */
9
16
 
10
17
  /* THE AGEING SIGNAL. The criterion asks that a request nobody takes be visibly
11
18
  aging rather than silently dead — so the age is a state pill in the row's
@@ -18,3 +25,101 @@
18
25
  .cb-age--waiting { color: var(--ink-soft); }
19
26
  .cb-age--aging { color: var(--warn); }
20
27
  .cb-age--stale { color: var(--danger); font-weight: 600; }
28
+
29
+ /* ---- the blocks ------------------------------------------------------------------
30
+ The top band is the shell's two-up grid (it stacks to one column at the
31
+ shell's phone breakpoint). A block is the shell's panel with a head row: the
32
+ heading, its count in mono, and the faces of whoever is asking on the right. */
33
+ .cb-band { margin: 0 0 var(--sp-md); }
34
+ .cb-block__head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 0 0 6px; }
35
+ .cb-block__head .scroll__h { margin: 0; }
36
+ .cb-block__n {
37
+ font-family: var(--font-mono);
38
+ font-size: 13px;
39
+ font-weight: 400;
40
+ letter-spacing: 0;
41
+ color: var(--ink-faint);
42
+ font-variant-numeric: tabular-nums;
43
+ margin-left: 6px;
44
+ }
45
+ .cb-block__head .cb-faces { margin-left: auto; }
46
+
47
+ /* "Show all N" / "Show fewer": the shell's ghost pill, on its own line under
48
+ the rows, so the condensed block ends on its door to the rest. */
49
+ .cb-more { margin-top: 12px; }
50
+
51
+ /* ---- the face -----------------------------------------------------------------
52
+ A row leads with WHO: a 36px disc, the picture when the server sent one, the
53
+ initial on the glass when it did not (people.css's .dir-avatar, at the row's
54
+ size). A craft ask wears the craft's letter in the label face, so it never
55
+ passes for a person. Decorative — the name beside it is the door. */
56
+ .cb-face {
57
+ width: 36px; height: 36px; flex: none;
58
+ border-radius: 50%;
59
+ object-fit: cover;
60
+ background: var(--skeleton);
61
+ border: 1px solid var(--rule);
62
+ margin-top: 1px;
63
+ }
64
+ .cb-face--initial {
65
+ display: inline-grid; place-items: center;
66
+ font-size: 14px; font-weight: 500; line-height: 1;
67
+ color: var(--ink-soft);
68
+ background: var(--glass-strong);
69
+ }
70
+ .cb-face--craft {
71
+ font-family: var(--label-face);
72
+ font-size: 12px;
73
+ letter-spacing: 0;
74
+ color: var(--ink-faint);
75
+ background: none;
76
+ border-style: dashed;
77
+ }
78
+
79
+ /* The stack of everyone asking: 24px faces at -7px overlap, each ringed in the
80
+ panel ground (the poster card's face-row grammar). */
81
+ .cb-faces { display: inline-flex; align-items: center; }
82
+ .cb-faces .cb-face {
83
+ width: 24px; height: 24px;
84
+ margin: 0 0 0 -7px;
85
+ border: 2px solid var(--bg-card);
86
+ font-size: 10px;
87
+ }
88
+ .cb-faces .cb-face:first-child { margin-left: 0; }
89
+ .cb-faces .cb-face--initial { background: var(--glass-strong); }
90
+
91
+ /* ---- the personal call ------------------------------------------------------------
92
+ An ask addressed to you BY NAME is the one live thing on the board — the
93
+ personal call this reader owes an answer — so its direction line takes the
94
+ accent ink. Accent goes only on what is live, and this is. */
95
+ .cb-row--live .ov-row__facts > .ov-fact { color: var(--accent-ink); }
96
+
97
+ /* ---- the rows, as people -------------------------------------------------------
98
+ The archetype's --lead grid puts the face in its own column. A row lights on
99
+ hover with the glass the family's button rows use, so the board feels awake
100
+ without any motion; the Kill Switch has nothing to kill here. */
101
+ .cb-row { padding-left: 10px; padding-right: 10px; margin: 0 -10px; border-radius: var(--radius-sm); }
102
+ .cb-row:hover { background: var(--glass); }
103
+ .cb-row .ov-row__facts a { color: var(--accent-ink); }
104
+ /* Inside the two condensed blocks a row has half the page: the state and the
105
+ actions drop UNDER the words as one line (the archetype's own phone shape),
106
+ the date yields to the age pill, and the two-up itself becomes one column
107
+ before the shell's phone breakpoint, at the width where two half-rows stop
108
+ holding a sentence. The wide block below keeps the archetype's two columns. */
109
+ .cb-band .ov-row--lead { grid-template-columns: auto minmax(0, 1fr); row-gap: 8px; }
110
+ .cb-band .ov-row--lead > .ov-row__right {
111
+ grid-column: 2;
112
+ flex-direction: row;
113
+ flex-wrap: wrap;
114
+ align-items: center;
115
+ text-align: left;
116
+ }
117
+ .cb-band .ov-row__actions { justify-content: flex-start; }
118
+ .cb-band .ov-row__when { display: none; }
119
+ @media (max-width: 1100px) {
120
+ .cb-band { grid-template-columns: 1fr; }
121
+ }
122
+
123
+ /* ---- empties and the foot -------------------------------------------------- */
124
+ .cb-empty p { font-size: 14px; }
125
+ .cb-foot { margin: var(--sp-sm) 0 0; font-size: 12.5px; color: var(--ink-faint); max-width: var(--measure); }
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Collab — {{worldName}}</title>
7
- <meta name="description" content="Requests for help addressed to your craft or to you by name, and the asks you have open — with how long each has been waiting.">
7
+ <meta name="description" content="Builders asking builders for help: what is waiting on you, what you asked, and every open ask across the project.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -20,30 +20,53 @@
20
20
  <div class="app-body">
21
21
  <header class="app-topbar" id="app-topbar"></header>
22
22
  <div class="app-content">
23
- <!-- The board page head: the page's own name in the LABEL voice, not a
24
- display line — on a working surface it is the least interesting
25
- thing on it. -->
23
+ <!-- The board page head: the page's own name in the LABEL voice. The sub
24
+ line is the one sentence of orientation the page keeps: who reaches
25
+ you, written by collab.js. -->
26
26
  <div class="page-head page-head--board">
27
27
  <h1 class="page-head__h">Collab</h1>
28
28
  <p class="page-head__sub" id="cb-sub">Loading…</p>
29
29
  </div>
30
30
 
31
+ <!-- THREE BLOCKS (owner direction, task 1003942). Top: ASKED OF YOU on the
32
+ left and YOUR ASKS on the right, each condensed to the three newest
33
+ with a "show all" that reveals the rest. Below, one wide block: every
34
+ open ask in the project. Each block's head carries its count and,
35
+ where people are asking, their faces. The rows are the hall's row
36
+ archetype (oversight.css) led by a face; collab.js fills them. -->
31
37
  <main id="hall-main">
32
- <!-- ASKED OF YOU comes first: it is the half where someone is waiting on a
33
- human. Your own asks are below, because they are a status check, not a
34
- call to act. The rows are the hall's row archetype (oversight.css);
35
- collab.js fills the mount with one run per addressee. -->
36
- <section class="scroll" aria-labelledby="cb-for-me-h">
37
- <h2 class="scroll__h" id="cb-for-me-h">Asked of you</h2>
38
- <p class="scroll__lede" id="cb-for-me-note">
39
- Requests addressed to your craft, or to you by name. Answering is optional
40
- and declining costs nothing — nobody is assigned by being asked.
41
- </p>
42
- <div id="cb-for-me"></div>
43
- <div class="empty-state" id="cb-for-me-empty" hidden>
44
- <p>Nothing is waiting on you. Requests reach you by your craft — set yours in
45
- <a href="/builders/settings">Settings</a> if this stays empty.</p>
38
+ <div class="view-zone view-zone--band cb-band">
39
+ <section class="scroll cb-block" aria-labelledby="cb-for-me-h">
40
+ <div class="cb-block__head">
41
+ <h2 class="scroll__h" id="cb-for-me-h">Asked of you <span class="cb-block__n" id="cb-for-me-n"></span></h2>
42
+ <span class="cb-faces" id="cb-for-me-faces" aria-hidden="true"></span>
43
+ </div>
44
+ <ul class="ov-rows" id="cb-for-me"></ul>
45
+ <div class="empty-state cb-empty" id="cb-for-me-empty" hidden>
46
+ <p>Nobody is waiting on you. Requests reach you by craft —
47
+ <a href="/builders/settings">set yours in Settings</a>.</p>
48
+ </div>
49
+ <button class="btn-ghost cb-more" id="cb-for-me-more" type="button" aria-expanded="false" aria-controls="cb-for-me" hidden></button>
50
+ </section>
51
+
52
+ <section class="scroll cb-block" aria-labelledby="cb-mine-h">
53
+ <div class="cb-block__head">
54
+ <h2 class="scroll__h" id="cb-mine-h">Your asks <span class="cb-block__n" id="cb-mine-n"></span></h2>
55
+ </div>
56
+ <ul class="ov-rows" id="cb-mine"></ul>
57
+ <div class="empty-state cb-empty" id="cb-mine-empty" hidden><p>You have no open asks.</p></div>
58
+ <button class="btn-ghost cb-more" id="cb-mine-more" type="button" aria-expanded="false" aria-controls="cb-mine" hidden></button>
59
+ </section>
60
+ </div>
61
+
62
+ <section class="scroll cb-block cb-block--all" aria-labelledby="cb-all-h">
63
+ <div class="cb-block__head">
64
+ <h2 class="scroll__h" id="cb-all-h">All open asks <span class="cb-block__n" id="cb-all-n"></span></h2>
65
+ <span class="cb-faces" id="cb-all-faces" aria-hidden="true"></span>
46
66
  </div>
67
+ <ul class="ov-rows" id="cb-all"></ul>
68
+ <div class="empty-state cb-empty" id="cb-all-empty" hidden><p>Nobody has an open ask right now.</p></div>
69
+ <button class="btn-ghost cb-more" id="cb-all-more" type="button" aria-expanded="false" aria-controls="cb-all" hidden></button>
47
70
  </section>
48
71
 
49
72
  <!-- SOCIAL MODE ONLY (task 1003178 / C08). Not a page and not a nav item:
@@ -52,28 +75,19 @@
52
75
  from /me, and collab.js reveals this section from that. With social
53
76
  off the page is exactly what it was, which is the point: the hall
54
77
  changes, it does not gain a destination. -->
55
- <section class="scroll" id="cb-mingle-section" aria-labelledby="cb-mingle-h" hidden>
78
+ <section class="scroll cb-block" id="cb-mingle-section" aria-labelledby="cb-mingle-h" hidden>
56
79
  <h2 class="scroll__h" id="cb-mingle-h">Introductions</h2>
57
- <p class="scroll__lede" id="cb-mingle-note">
58
- Mingle pairs you with someone from a different craft. Saying no costs nothing
59
- at all — no rank, no karma, no streak, and it does not change how often you
60
- are paired again.
61
- </p>
80
+ <p class="scroll__lede" id="cb-mingle-note">Mingle pairs you with someone from another craft. Saying no costs nothing.</p>
62
81
  <ul class="ov-rows" id="cb-mingle"></ul>
63
- <div class="empty-state" id="cb-mingle-empty" hidden>
64
- <p>No introductions right now. You will see one here when a pairing round runs.</p>
82
+ <div class="empty-state cb-empty" id="cb-mingle-empty" hidden>
83
+ <p>No introductions right now. One appears here when a pairing round runs.</p>
65
84
  </div>
66
85
  </section>
67
86
 
68
- <section class="scroll" aria-labelledby="cb-mine-h">
69
- <h2 class="scroll__h" id="cb-mine-h">Your asks</h2>
70
- <p class="scroll__lede">
71
- Only you can settle your own request — mark it answered when someone
72
- helped, or withdraw it when you no longer need it.
73
- </p>
74
- <ul class="ov-rows" id="cb-mine"></ul>
75
- <div class="empty-state" id="cb-mine-empty" hidden><p>You have no open asks.</p></div>
76
- </section>
87
+ <!-- The one sentence of rules, said ONCE, at the foot. Answering is
88
+ optional by design (ADR 0187 §3), and the page still says so; it
89
+ just stops saying it first. -->
90
+ <p class="cb-foot">Answering is optional and declining costs nothing. Nobody is assigned by being asked.</p>
77
91
  </main>
78
92
  </div>
79
93
  </div>
@@ -84,7 +98,7 @@
84
98
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
85
99
  <script src="/builders/palette.js" defer></script>
86
100
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
87
- <script src="/builders/collab-lib.js?v=2026-09-01-redesign"></script>
88
- <script src="/builders/collab.js?v=2026-09-01-redesign"></script>
101
+ <script src="/builders/collab-lib.js?v=2026-09-16-blocks"></script>
102
+ <script src="/builders/collab.js?v=2026-09-16-blocks"></script>
89
103
  </body>
90
104
  </html>