@bongos/core 1.19.458 → 1.19.460

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 (50) hide show
  1. package/.bongos-core.json +117 -42
  2. package/.claude/skills/design/SKILL.md +10 -0
  3. package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/branding-contract.md +1 -1
  6. package/docs/copy-inventory.md +52 -61
  7. package/docs/copy-registry.json +92 -203
  8. package/docs/file-map.md +1 -1
  9. package/docs/module-api-changelog.md +4 -0
  10. package/docs/recipes/ui-look-before-you-ship.md +2 -0
  11. package/modules/hall-ui/public/collab-lib.js +40 -4
  12. package/modules/hall-ui/public/collab.css +18 -34
  13. package/modules/hall-ui/public/collab.html +17 -17
  14. package/modules/hall-ui/public/collab.js +75 -39
  15. package/modules/hall-ui/public/collab.states.json +2 -2
  16. package/modules/hall-ui/public/copy-desk.css +64 -109
  17. package/modules/hall-ui/public/copy-desk.html +76 -69
  18. package/modules/hall-ui/public/copy-desk.js +175 -80
  19. package/modules/hall-ui/public/copy-desk.states.json +59 -2
  20. package/modules/hall-ui/public/oversight.css +11 -2
  21. package/modules/lifecycle/help-requests.js +18 -8
  22. package/modules/ui-design/kit/README.md +7 -5
  23. package/modules/ui-design/kit/check-mock.js +12 -5
  24. package/modules/ui-design/kit/contrast.js +13 -6
  25. package/modules/ui-design/kit/lib.js +39 -2
  26. package/modules/ui-design/kit/probe.js +4 -1
  27. package/modules/ui-design/kit/render.js +7 -4
  28. package/modules/ui-design/kit/serve.js +9 -2
  29. package/modules/ui-design/styles/README.md +54 -2
  30. package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
  31. package/modules/ui-design/styles/chrome-world/mock.html +275 -0
  32. package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
  33. package/modules/ui-design/styles/chrome-world/pack.json +33 -0
  34. package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
  35. package/modules/ui-design/styles/expedition/mock.html +285 -0
  36. package/modules/ui-design/styles/expedition/mock.states.json +11 -0
  37. package/modules/ui-design/styles/expedition/pack.json +33 -0
  38. package/modules/ui-design/styles/grove/DESIGN.md +148 -0
  39. package/modules/ui-design/styles/grove/mock.html +294 -0
  40. package/modules/ui-design/styles/grove/mock.states.json +11 -0
  41. package/modules/ui-design/styles/grove/pack.json +33 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/src/module-api.js +1 -1
  45. package/tests/collab_page.mjs +13 -7
  46. package/tests/cosmos_token_contrast.mjs +9 -2
  47. package/tests/hall_collab_world.mjs +214 -0
  48. package/tests/hall_tokens.mjs +15 -6
  49. package/tests/ui_design_styles.mjs +218 -0
  50. package/tests/world_tokens.mjs +9 -2
package/docs/file-map.md CHANGED
@@ -130,7 +130,7 @@
130
130
  │ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Three ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages) and `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend.
131
131
  │ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
132
132
  │ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
133
- │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. Reserved home styles/ (1003323) with a README. config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
133
+ │ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove; STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
134
134
  │ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
135
135
  │ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
136
136
  │ └── demo/ ← BV1.R52 acceptance-proof module: one rank-gated write route + one namespaced migration proving the loader mounts a scaffolded module with zero core edits. CLAUDE.md, module.json.
@@ -1353,5 +1353,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1353
1353
  landed since 1.19.456 with no explicit bump. run 33561330390. (task 1002620)
1354
1354
  1.19.458 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1355
1355
  landed since 1.19.457 with no explicit bump. run 33566876510. (task 1002620)
1356
+ 1.19.459 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1357
+ landed since 1.19.458 with no explicit bump. run 33568609735. (task 1002620)
1358
+ 1.19.460 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1359
+ landed since 1.19.459 with no explicit bump. run 33571137958. (task 1002620)
1356
1360
  ---------------------------------------------------------------------------
1357
1361
  ```
@@ -21,6 +21,7 @@ SURFACE=hall-ui PREFIX=/builders/ node modules/ui-design/kit/serve.js
21
21
  CHILD=1 node modules/ui-design/kit/serve.js # the child-instance shape (apex-only regions stripped)
22
22
  LIVE=1 node modules/ui-design/kit/serve.js # public feeds proxied to the pack's public origin
23
23
  FAIL=featured,progress node modules/ui-design/kit/serve.js # those feeds answer 500
24
+ STYLE=grove node modules/ui-design/kit/serve.js # under a look from the style library (modules/ui-design/styles/; task 1003323)
24
25
  ```
25
26
 
26
27
  The stub runs the page through the **instance's real transforms** — the apex-only strip, the brand-token stamp, the pack's theme injection, run by the core-side command `scripts/gds/ui-kit-transform.js` (a module may not require core, so the stub spawns the command and memoises the page) — then the content-hash `?v=` stamp, with the instance's own branding pack, and answers the public feeds from `kit/fixtures/`. It also serves `docs/` raw, so a mock under `docs/design/mocks/` renders with its relative links: `http://127.0.0.1:3412/docs/design/mocks/hub/v2.html`.
@@ -34,6 +35,7 @@ node modules/ui-design/kit/render.js --page /projects
34
35
  node modules/ui-design/kit/render.js --page / # MSYS_NO_PATHCONV=1 in Git Bash, or the kit reads the mangled root back
35
36
  node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
36
37
  node modules/ui-design/kit/render.js --page /projects --only in-map,card --modes dark --sizes 390
38
+ node modules/ui-design/kit/render.js --page / --style expedition # the same page under a look from the style library
37
39
  ```
38
40
 
39
41
  `render.js` starts its own two stubs (signed out, signed in), renders each state of the page's **states file** in dark + light at 1440×900 / 390×844 / 320×700, drives the state's actions, and audits every render against the **platform floors**:
@@ -1,5 +1,6 @@
1
1
  // modules/hall-ui/public/collab-lib.js — the Collab page's PURE helpers, exposed
2
- // as window.OTBCollabLib (task 1003172 / C02, goal 1000075).
2
+ // as window.OTBCollabLib (task 1003172 / C02, goal 1000075; the row's first
3
+ // line, the asker and the groups added by the redesign, task 1003471).
3
4
  //
4
5
  // Same split as goals-lib.js: everything here is a pure function of its
5
6
  // arguments, so the rules can be executed by a test instead of grepped out of a
@@ -52,15 +53,29 @@
52
53
 
53
54
  // Who a request is addressed to, in words. A craft request and a by-name
54
55
  // request read differently on purpose: "asked of builders" is a call anyone of
55
- // that craft may take, "asked of you" is personal and should feel it.
56
+ // that craft may take, "asked of you" is personal and should feel it. A
57
+ // by-name ask that is not yours names the person when the server joined a
58
+ // name, and says "one person" when it did not.
56
59
  function addressedTo(req, viewerId) {
57
60
  if (req && req.needs_builder_id != null) {
58
- return String(req.needs_builder_id) === String(viewerId) ? 'asked of you by name' : 'asked of one person';
61
+ if (String(req.needs_builder_id) === String(viewerId)) return 'asked of you by name';
62
+ var name = req.needs_builder_name || req.needs_builder_login;
63
+ return name ? 'asked of ' + name + ' by name' : 'asked of one person';
59
64
  }
60
65
  if (req && req.needs_craft) return 'asked of ' + req.needs_craft + 's';
61
66
  return 'asked';
62
67
  }
63
68
 
69
+ // Who is ASKING, in words — the fact a helper needs to answer. The name the
70
+ // server joined, else the login, else the bare id an older server sends;
71
+ // never nothing.
72
+ function askerOf(req) {
73
+ if (!req) return '';
74
+ if (req.requested_by_name) return String(req.requested_by_name);
75
+ if (req.requested_by_login) return String(req.requested_by_login);
76
+ return req.requested_by != null ? 'builder ' + req.requested_by : '';
77
+ }
78
+
64
79
  // What the request is about, if anything. Returns null rather than an empty
65
80
  // string so a caller can skip the element entirely.
66
81
  function contextOf(req) {
@@ -70,8 +85,29 @@
70
85
  return null;
71
86
  }
72
87
 
88
+ // The row's first line. A help request has no title field, so the ask's
89
+ // first sentence is the title and the rest is the sub line — both the asker's
90
+ // own words, nothing clipped. A first sentence too long to be a line, or an
91
+ // ask with no sentence break, is the whole ask.
92
+ var TITLE_MAX = 140;
93
+ function titleOf(text) {
94
+ var s = String(text == null ? '' : text).trim();
95
+ var m = /^([\s\S]*?[.?!])\s+([\s\S]+)$/.exec(s);
96
+ if (!m || m[1].length > TITLE_MAX) return { title: s, rest: '' };
97
+ return { title: m[1], rest: m[2].trim() };
98
+ }
99
+
100
+ // The "asked of you" list reads in runs: what is asked of you BY NAME first
101
+ // (a personal ask is the one you owe an answer), then one run per craft.
102
+ function groupOf(req) {
103
+ if (req && req.needs_builder_id != null) return { id: 'you', label: 'asked of you by name' };
104
+ if (req && req.needs_craft) return { id: req.needs_craft, label: 'asked of ' + req.needs_craft + 's' };
105
+ return { id: 'other', label: 'asked' };
106
+ }
107
+
73
108
  window.OTBCollabLib = {
74
109
  BANDS: BANDS, ageBand: ageBand, byAgeOldestFirst: byAgeOldestFirst,
75
- addressedTo: addressedTo, contextOf: contextOf,
110
+ addressedTo: addressedTo, askerOf: askerOf, contextOf: contextOf,
111
+ titleOf: titleOf, groupOf: groupOf,
76
112
  };
77
113
  })();
@@ -1,36 +1,20 @@
1
- /* modules/hall-ui/public/collab.css — the Collab page (task 1003172 / C02).
2
- Tokens only from style.css's real set: inventing one renders as nothing, which
3
- is how copy-desk.css ended up with invisible fallbacks (idea 1000821). */
4
-
5
- .cb-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
6
-
7
- .cb-item {
8
- border: var(--line) solid var(--rule);
9
- border-left: 3px solid var(--rule);
10
- border-radius: var(--radius-sm);
11
- background: var(--bg-card);
12
- padding: 10px 12px;
13
- }
14
-
15
- .cb-item__stuck { margin: 0; color: var(--ink); font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
16
- .cb-item__meta { margin: 6px 0 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 12px; }
17
- .cb-item__actions { margin: 8px 0 0; display: flex; gap: 8px; }
18
- .cb-who { color: var(--ink-soft); }
19
- .cb-ctx { color: var(--accent); text-decoration: none; }
20
- .cb-ctx:hover { text-decoration: underline; }
21
- .cb-empty { color: var(--ink-faint); font-size: 13px; margin: 10px 0 0; }
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.
6
+ Tokens only from style.css's real set: inventing one renders as nothing,
7
+ which is how copy-desk.css once ended up with invisible fallbacks (idea
8
+ 1000821). */
22
9
 
23
10
  /* THE AGEING SIGNAL. The criterion asks that a request nobody takes be visibly
24
- aging rather than silently dead — so age changes how a row LOOKS, not only
25
- where it sits. The left border carries it (a band of colour that reads at a
26
- glance down a list) and the chip says it in words, because colour alone is not
27
- a message and not everyone sees it. */
28
- .cb-age { color: var(--ink-faint); font-size: 11.5px; letter-spacing: 0.01em; }
29
- .cb-item--fresh { border-left-color: var(--rule); }
30
- .cb-item--waiting { border-left-color: var(--ink-faint); }
31
- .cb-item--aging { border-left-color: var(--warn); }
32
- .cb-item--stale { border-left-color: var(--danger); }
33
- .cb-item--aging .cb-age { color: var(--warn); }
34
- .cb-item--stale .cb-age { color: var(--danger); font-weight: 600; }
35
-
36
- .btn--quiet { background: transparent; color: var(--ink-soft); border-color: var(--rule); }
11
+ aging rather than silently dead — so the age is a state pill in the row's
12
+ right column: read down the list and the column says it in words AND in the
13
+ verification colours, because colour alone is not a message and not everyone
14
+ sees it. Four bands, four treatments: faint, soft, warn, danger. The 3px
15
+ coloured band the first cut painted down the row's left edge is gone — this
16
+ world marks state with a pill, never with a bar. */
17
+ .cb-age--fresh { color: var(--ink-faint); }
18
+ .cb-age--waiting { color: var(--ink-soft); }
19
+ .cb-age--aging { color: var(--warn); }
20
+ .cb-age--stale { color: var(--danger); font-weight: 600; }
@@ -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/oversight.css">
12
13
  <link rel="stylesheet" href="/builders/collab.css">
13
14
  </head>
14
15
  <body data-page="collab" data-page-title="Collab">
@@ -19,11 +20,9 @@
19
20
  <div class="app-body">
20
21
  <header class="app-topbar" id="app-topbar"></header>
21
22
  <div class="app-content">
22
- <!-- task 1003307: the board page head. Collab shipped with NO <h1> at all
23
- (one-header-main-h1 measured 0 on every render), so the re-skin
24
- carries the structural fix rather than porting the hole. The heading
25
- is the LABEL voice, not a display line: on a working surface the
26
- page's own name is the least interesting thing on it. -->
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. -->
27
26
  <div class="page-head page-head--board">
28
27
  <h1 class="page-head__h">Collab</h1>
29
28
  <p class="page-head__sub" id="cb-sub">Loading…</p>
@@ -32,28 +31,29 @@
32
31
  <main id="hall-main">
33
32
  <!-- ASKED OF YOU comes first: it is the half where someone is waiting on a
34
33
  human. Your own asks are below, because they are a status check, not a
35
- call to act. -->
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
36
  <section class="scroll" aria-labelledby="cb-for-me-h">
37
37
  <h2 class="scroll__h" id="cb-for-me-h">Asked of you</h2>
38
- <p class="scroll__note" id="cb-for-me-note">
38
+ <p class="scroll__lede" id="cb-for-me-note">
39
39
  Requests addressed to your craft, or to you by name. Answering is optional
40
40
  and declining costs nothing — nobody is assigned by being asked.
41
41
  </p>
42
- <ul class="cb-list" id="cb-for-me"></ul>
43
- <p class="cb-empty" id="cb-for-me-empty" hidden>
44
- 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.
46
- </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>
46
+ </div>
47
47
  </section>
48
48
 
49
49
  <section class="scroll" aria-labelledby="cb-mine-h">
50
50
  <h2 class="scroll__h" id="cb-mine-h">Your asks</h2>
51
- <p class="scroll__note">
51
+ <p class="scroll__lede">
52
52
  Only you can settle your own request — mark it answered when someone
53
53
  helped, or withdraw it when you no longer need it.
54
54
  </p>
55
- <ul class="cb-list" id="cb-mine"></ul>
56
- <p class="cb-empty" id="cb-mine-empty" hidden>You have no open asks.</p>
55
+ <ul class="ov-rows" id="cb-mine"></ul>
56
+ <div class="empty-state" id="cb-mine-empty" hidden><p>You have no open asks.</p></div>
57
57
  </section>
58
58
  </main>
59
59
  </div>
@@ -65,7 +65,7 @@
65
65
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
66
66
  <script src="/builders/palette.js" defer></script>
67
67
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
68
- <script src="/builders/collab-lib.js?v=2026-08-24-c02"></script>
69
- <script src="/builders/collab.js?v=2026-08-24-c02"></script>
68
+ <script src="/builders/collab-lib.js?v=2026-09-01-redesign"></script>
69
+ <script src="/builders/collab.js?v=2026-09-01-redesign"></script>
70
70
  </body>
71
71
  </html>
@@ -1,15 +1,19 @@
1
1
  // modules/hall-ui/public/collab.js — the Collab page (/builders/collab).
2
- // Task 1003172 / C02 of goal 1000075, criterion collab-page-matches-my-craft.
2
+ // Task 1003172 / C02 of goal 1000075, criterion collab-page-matches-my-craft;
3
+ // rebuilt on the hall's row archetype by task 1003471 (goal 1000081).
3
4
  //
4
- // The hall surface for C01's help requests. Two sections, in this order on
5
+ // The hall surface for C01's help requests. Two panels, in this order on
5
6
  // purpose: what is ASKED OF YOU first, because someone is waiting on a human
6
7
  // answer, then YOUR OWN ASKS, which are a status check rather than a call to act.
8
+ // "Asked of you" reads in runs — by name first, then each craft — because a
9
+ // personal ask is the one you owe an answer.
7
10
  //
8
- // THE PART THAT IS EASY TO SKIP, and the criterion names it explicitly: a request
9
- // nobody takes must be VISIBLY AGING, not silently dead. Sorting the oldest to the
10
- // top does not satisfy that — the oldest then looks exactly like the newest, only
11
- // lower. So every row carries an age BAND with its own label and styling, and the
12
- // rules for it live in collab-lib.js where a test can execute them.
11
+ // A row is the family's .ov-row: the ask's first sentence as the title and the
12
+ // rest as the sub (collab-lib titleOf), WHO IS ASKING as a fact with a door to
13
+ // their profile, the task or goal as a chip, and the AGE as a state pill in the
14
+ // right column, so a glance down the list reads the ages in words and in the
15
+ // verification colours. The band rules live in collab-lib.js where a test can
16
+ // execute them.
13
17
  //
14
18
  // MATCHING IS THE SERVER'S JOB, not this file's. GET /help-requests/for-me derives
15
19
  // the viewer's crafts from their own profile; sending a craft from here would be
@@ -31,15 +35,38 @@
31
35
 
32
36
  let settling = false;
33
37
 
38
+ // The age pill: the band's words, in the band's colour (collab.css .cb-age--*).
34
39
  function ageChip(req, nowMs) {
35
40
  const band = L.ageBand(req.created_at, nowMs);
36
- return `<span class="cb-age cb-age--${band.id}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(band.label)}</span>`;
41
+ return `<span class="fact-pill cb-age cb-age--${band.id}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(band.label)}</span>`;
42
+ }
43
+
44
+ function whenLine(iso) {
45
+ const d = iso ? new Date(iso) : null;
46
+ if (!d || Number.isNaN(d.getTime())) return '';
47
+ return `<span class="ov-row__when">${escapeHtml(d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }))}</span>`;
48
+ }
49
+
50
+ function titleHtml(req) {
51
+ const t = L.titleOf(req.what_is_stuck);
52
+ return `<span class="ov-row__title">${escapeHtml(t.title)}</span>`
53
+ + (t.rest ? `<p class="ov-row__sub">${escapeHtml(t.rest)}</p>` : '');
37
54
  }
38
55
 
39
56
  function contextChip(req) {
40
57
  const ctx = L.contextOf(req);
41
58
  if (!ctx) return '';
42
- return `<a class="cb-ctx" href="${escapeHtml(ctx.href)}">${escapeHtml(ctx.kind)} #${escapeHtml(String(ctx.id))}</a>`;
59
+ return `<a class="fact-chip" href="${escapeHtml(ctx.href)}">${escapeHtml(ctx.kind)} #${escapeHtml(String(ctx.id))}</a>`;
60
+ }
61
+
62
+ // Who to answer: the asker's name, as a door to their profile.
63
+ function askerFact(req) {
64
+ const name = L.askerOf(req);
65
+ if (!name) return '';
66
+ const who = req.requested_by != null
67
+ ? `<a href="/builders/profile?id=${escapeHtml(String(req.requested_by))}">${escapeHtml(name)}</a>`
68
+ : escapeHtml(name);
69
+ return `<span class="ov-fact">asked by ${who}</span>`;
43
70
  }
44
71
 
45
72
  // A row for the "asked of you" list. There is deliberately NO "claim this"
@@ -47,58 +74,67 @@
47
74
  // be reimplementing the Agora, which this goal explicitly forbids (C06 owns the
48
75
  // chat mirror). What the page owes a helper is the request, its age, its
49
76
  // context, and who to answer.
50
- function forMeRow(req, viewerId, nowMs) {
51
- const who = L.addressedTo(req, viewerId);
52
- return `<li class="cb-item cb-item--${L.ageBand(req.created_at, nowMs).id}">
53
- <p class="cb-item__stuck">${escapeHtml(req.what_is_stuck || '')}</p>
54
- <p class="cb-item__meta">
55
- <span class="cb-who">${escapeHtml(who)}</span>
56
- ${contextChip(req)}
57
- ${ageChip(req, nowMs)}
58
- </p>
77
+ function forMeRow(req, nowMs) {
78
+ return `<li class="ov-row" data-id="${escapeHtml(String(req.id))}">
79
+ <div class="ov-row__main">${titleHtml(req)}
80
+ <div class="ov-row__facts">${askerFact(req)}${contextChip(req)}</div>
81
+ </div>
82
+ <div class="ov-row__right">${ageChip(req, nowMs)}${whenLine(req.created_at)}</div>
59
83
  </li>`;
60
84
  }
61
85
 
62
86
  // Your own ask, with the two settle actions. Only the asker can settle — the
63
87
  // server enforces it in the UPDATE's WHERE, this is just the affordance.
64
88
  function mineRow(req, nowMs) {
65
- return `<li class="cb-item cb-item--${L.ageBand(req.created_at, nowMs).id}" data-id="${escapeHtml(String(req.id))}">
66
- <p class="cb-item__stuck">${escapeHtml(req.what_is_stuck || '')}</p>
67
- <p class="cb-item__meta">
68
- <span class="cb-who">${escapeHtml(L.addressedTo(req, null))}</span>
69
- ${contextChip(req)}
70
- ${ageChip(req, nowMs)}
71
- </p>
72
- <p class="cb-item__actions">
73
- <button class="btn btn--sm" data-settle="answered" data-id="${escapeHtml(String(req.id))}">Someone helped</button>
74
- <button class="btn btn--sm btn--quiet" data-settle="withdrawn" data-id="${escapeHtml(String(req.id))}">Withdraw</button>
75
- </p>
89
+ const id = escapeHtml(String(req.id));
90
+ return `<li class="ov-row" data-id="${id}">
91
+ <div class="ov-row__main">${titleHtml(req)}
92
+ <div class="ov-row__facts"><span class="ov-fact">${escapeHtml(L.addressedTo(req, null))}</span>${contextChip(req)}</div>
93
+ </div>
94
+ <div class="ov-row__right">${ageChip(req, nowMs)}
95
+ <div class="ov-row__actions">
96
+ <button class="btn-accent" data-settle="answered" data-id="${id}">Someone helped</button>
97
+ <button class="btn-ghost" data-settle="withdrawn" data-id="${id}">Withdraw</button>
98
+ </div>
99
+ </div>
76
100
  </li>`;
77
101
  }
78
102
 
79
- function renderList(hostSel, emptySel, rows, html) {
103
+ // "Asked of you" as runs under a group head: by name first, then each craft.
104
+ function groupedHtml(rows, nowMs) {
105
+ const groups = [];
106
+ for (const r of rows) {
107
+ const g = L.groupOf(r);
108
+ let hit = groups.find((x) => x.id === g.id);
109
+ if (!hit) { hit = { id: g.id, label: g.label, rows: [] }; groups.push(hit); }
110
+ hit.rows.push(r);
111
+ }
112
+ groups.sort((a, b) => Number(b.id === 'you') - Number(a.id === 'you'));
113
+ return groups.map((g) => `<section class="ov-group">
114
+ <div class="ov-group__head"><span class="ov-group__name">${escapeHtml(g.label)}</span><span class="ov-group__n">${g.rows.length}</span></div>
115
+ <ul class="ov-rows">${g.rows.map((r) => forMeRow(r, nowMs)).join('')}</ul>
116
+ </section>`).join('');
117
+ }
118
+
119
+ function paint(hostSel, emptySel, html, n) {
80
120
  const host = $(hostSel);
81
121
  const empty = $(emptySel);
82
- if (!host) return;
83
- host.innerHTML = rows.map(html).join('');
84
- if (empty) empty.hidden = rows.length > 0;
122
+ if (host) host.innerHTML = html;
123
+ if (empty) empty.hidden = n > 0;
85
124
  }
86
125
 
87
126
  async function load() {
88
127
  const nowMs = Date.now();
89
- const [forMe, mine, me] = await Promise.all([
128
+ const [forMe, mine] = await Promise.all([
90
129
  api.helpRequests.getHelpRequestsForMe({}),
91
130
  api.helpRequests.getHelpRequestsMine({ query: { status: 'open' } }),
92
- api.me.getMe ? api.me.getMe({}) : Promise.resolve({ ok: false }),
93
131
  ]);
94
132
 
95
- const viewerId = me && me.ok && me.data && me.data.builder ? me.data.builder.id : null;
96
-
97
133
  const forMeRows = ((forMe.ok && forMe.data && forMe.data.help_requests) || []).slice().sort(L.byAgeOldestFirst);
98
134
  const mineRows = ((mine.ok && mine.data && mine.data.help_requests) || []).slice().sort(L.byAgeOldestFirst);
99
135
 
100
- renderList('#cb-for-me', '#cb-for-me-empty', forMeRows, (r) => forMeRow(r, viewerId, nowMs));
101
- renderList('#cb-mine', '#cb-mine-empty', mineRows, (r) => mineRow(r, nowMs));
136
+ paint('#cb-for-me', '#cb-for-me-empty', groupedHtml(forMeRows, nowMs), forMeRows.length);
137
+ paint('#cb-mine', '#cb-mine-empty', mineRows.map((r) => mineRow(r, nowMs)).join(''), mineRows.length);
102
138
 
103
139
  const sub = $('#cb-sub');
104
140
  if (sub) {
@@ -1,12 +1,12 @@
1
1
  {
2
- "_": "Collab — the two-sided ask board. ASKED OF YOU comes first because it is the half where someone is waiting, and the empty state of that half is the page's most common render. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
2
+ "_": "Collab — the two-sided ask board. ASKED OF YOU comes first because it is the half where someone is waiting, and the empty state of that half is the page's most common render. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once; the redesign (task 1003471) put four asks and two of the viewer's own in the harness so the rows can be looked at.",
3
3
  "page": "/builders/collab",
4
4
  "surface": "hall-ui",
5
5
  "stub": {
6
6
  "prefix": "/builders/"
7
7
  },
8
8
  "modeQuery": false,
9
- "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
9
+ "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY state — which is the point: that is a state the floors must hold in. Against the hall-preview harness the same state renders populated: help-requests__for-me.json (four asks across the four age bands, one by name) and help-requests__mine.json (two of the viewer's own). Every declared 404 below is a feed the kit's stub does not carry, named rather than hidden.",
10
10
  "ignoreRequests": [
11
11
  "/api/(gds|bongos)/me$",
12
12
  "/api/(gds|bongos)/me/",