@bongos/core 1.17.3 → 1.18.0

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 (95) hide show
  1. package/.bongos-core.json +161 -91
  2. package/.devcontainer/healthcheck.sh +0 -0
  3. package/.devcontainer/init-firewall.sh +0 -0
  4. package/.devcontainer/post-create.sh +0 -0
  5. package/GOVERNANCE.md +1 -1
  6. package/README.md +26 -22
  7. package/bin/bongos.js +0 -0
  8. package/docker-entrypoint.sh +0 -0
  9. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +94 -0
  10. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +43 -0
  11. package/docs/adr/README.md +3 -0
  12. package/docs/api/openapi.json +4 -345
  13. package/docs/api-reference.md +5 -17
  14. package/docs/file-map.md +7 -25
  15. package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
  16. package/docs/onboarding/diagrams/assertions.json +5 -9
  17. package/modules/hall-ui/public/builders.js +37 -0
  18. package/modules/hall-ui/public/goals-page.js +98 -176
  19. package/modules/hall-ui/public/goals.css +183 -0
  20. package/modules/hall-ui/public/goals.html +35 -261
  21. package/modules/hall-ui/public/goals.js +7 -2
  22. package/modules/hall-ui/public/hall-widgets.js +12 -4
  23. package/modules/hall-ui/public/settings.css +355 -0
  24. package/modules/hall-ui/public/settings.html +86 -414
  25. package/modules/hall-ui/public/settings.js +96 -35
  26. package/modules/hall-ui/public/style.css +31 -0
  27. package/modules/hall-ui/public/watch.css +184 -0
  28. package/modules/hall-ui/public/watch.html +111 -285
  29. package/modules/hall-ui/public/watch.js +59 -21
  30. package/package-lock.json +2 -2
  31. package/package.json +1 -1
  32. package/public-landing/create.html +83 -7
  33. package/public-landing/index.html +11 -76
  34. package/scripts/cron/style_guide_audit.sh +0 -0
  35. package/scripts/gds/api.js +0 -0
  36. package/scripts/gds/architect-audit.js +0 -0
  37. package/scripts/gds/audit-authorship.sh +0 -0
  38. package/scripts/gds/audit-replay.sh +0 -0
  39. package/scripts/gds/box.js +0 -0
  40. package/scripts/gds/capture.js +0 -0
  41. package/scripts/gds/conductor-dispatch.js +0 -0
  42. package/scripts/gds/coreB-carve-proof.sh +0 -0
  43. package/scripts/gds/db-backup-nightly.sh +0 -0
  44. package/scripts/gds/db-backup-offsite.sh +0 -0
  45. package/scripts/gds/doctor.js +0 -0
  46. package/scripts/gds/grade-smoke.js +0 -0
  47. package/scripts/gds/hooks/pre-push.js +0 -0
  48. package/scripts/gds/install-git-hooks.js +0 -0
  49. package/scripts/gds/main-audit.js +0 -0
  50. package/scripts/gds/oauth-secret.js +0 -0
  51. package/scripts/gds/onboarding-analyze.js +0 -0
  52. package/scripts/gds/onboarding-ship.js +0 -0
  53. package/scripts/gds/paste-token.js +0 -0
  54. package/scripts/gds/portability-smoke-art.sh +0 -0
  55. package/scripts/gds/portability-smoke-memory.sh +0 -0
  56. package/scripts/gds/provision.js +125 -4
  57. package/scripts/gds/push-main.js +0 -0
  58. package/scripts/gds/render-ideas.js +0 -0
  59. package/scripts/gds/security-baseline-check.sh +0 -0
  60. package/scripts/gds/session.js +0 -0
  61. package/scripts/gds/ship.js +11 -1
  62. package/scripts/gds/smoke-batch-claim.sh +0 -0
  63. package/scripts/gds/smoke-batch-release.sh +0 -0
  64. package/scripts/gds/smoke-box.sh +0 -0
  65. package/scripts/gds/smoke-claim-consistency.sh +0 -0
  66. package/scripts/gds/smoke-dependencies.sh +0 -0
  67. package/scripts/gds/smoke-write.sh +0 -0
  68. package/scripts/hall-preview/fixtures/access-requests.json +20 -1
  69. package/scripts/hall-preview/fixtures/audit-log.json +68 -1
  70. package/scripts/hall-preview/fixtures/blockers__detail.json +12 -0
  71. package/scripts/hall-preview/fixtures/box__me.json +5 -0
  72. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +69 -1
  73. package/scripts/hall-preview/fixtures/builders__roster.json +48 -1
  74. package/scripts/hall-preview/fixtures/grades__by-builder.json +32 -1
  75. package/scripts/hall-preview/fixtures/inbox__detail.json +15 -0
  76. package/scripts/hall-preview/fixtures/me__art-key__own.json +6 -0
  77. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +209 -0
  78. package/scripts/hall-preview/fixtures/me__render.json +5 -0
  79. package/scripts/hall-preview/fixtures/me__skill-prefs.json +143 -0
  80. package/scripts/hall-preview/fixtures/me__sound-prefs.json +67 -0
  81. package/scripts/hall-preview/fixtures/me__wandering.json +43 -0
  82. package/scripts/hall-preview/fixtures/override-requests.json +15 -1
  83. package/scripts/hall-preview/fixtures/security__docs.json +10 -1
  84. package/scripts/hall-preview/fixtures/security__reports.json +31 -1
  85. package/scripts/hall-preview/fixtures/sessions__search.json +9 -1
  86. package/scripts/hall-preview/server.js +18 -1
  87. package/scripts/migrate.sh +0 -0
  88. package/scripts/migrate_cost_ledger.sh +0 -0
  89. package/scripts/render-diagrams.sh +0 -0
  90. package/scripts/security/dep-audit.js +0 -0
  91. package/scripts/setup-builder.js +0 -0
  92. package/scripts/setup-builder.sh +0 -0
  93. package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
  94. package/src/module-api.js +20 -1
  95. package/tests/provision.mjs +69 -0
@@ -0,0 +1,183 @@
1
+ /* goals.css — page-local styles for the Goals page (task 2171; extracted from
2
+ goals.html's old inline <style> per the hall placement rules). Only what the
3
+ shared kit doesn't carry lives here: goal cards, the detail band, criteria /
4
+ member / conflict lists, the invite + pending panels. Shared pieces —
5
+ .pager--paged, .count-pill, .kit-tabs, .filters, .empty-state, .kit-skel —
6
+ are in style.css's hall-kit section. Everything derives from the shared
7
+ design tokens so instance packs reskin this page along with the rest. */
8
+
9
+ /* ---- goal-card grid (list view) ----
10
+ task 2171 density pass: minmax 300→230 (four across at desktop width),
11
+ tighter gaps/padding/type — the grid holds ~26 goals, so card height is
12
+ what sets the page height. */
13
+ .goal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
14
+ .goal-card {
15
+ display: flex; flex-direction: column; gap: 7px;
16
+ background: var(--surface); border: 1px solid var(--border);
17
+ border-radius: var(--radius); padding: 10px 12px;
18
+ color: var(--ink);
19
+ }
20
+ a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); background: var(--surface-2); }
21
+ .goal-card__head { display: flex; align-items: flex-start; gap: 9px; }
22
+ /* task 1760 — priority (visible once narrowed to one version) + Archon
23
+ drag-to-reorder handle (server enforces the write; this is the affordance). */
24
+ .goal-card__handle { flex: none; cursor: grab; color: var(--ink-faint); font-size: 14px; line-height: 1.2; touch-action: none; }
25
+ .goal-card__handle:active { cursor: grabbing; }
26
+ .goal-card__priority {
27
+ flex: none; min-width: 1.4em; text-align: center;
28
+ font-size: 11.5px; font-weight: 600; color: var(--ink-faint);
29
+ font-variant-numeric: tabular-nums;
30
+ }
31
+ .goal-card--dragging { opacity: 0.45; }
32
+ /* task 1761: a goal blocked by an unachieved prerequisite is dimmed + hue-shifted. */
33
+ .goal-card--blocked { opacity: 0.62; border-style: dashed; }
34
+ .goal-card--blocked:hover { opacity: 0.85; }
35
+ .goal-deps { color: var(--ink-faint); }
36
+ .goal-deps--blocked { color: var(--warn); font-weight: 600; }
37
+ .goal-card__title { font-weight: 650; font-size: 13px; line-height: 1.35; flex: 1; min-width: 0; }
38
+ .goal-card__meta { display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: center; font-size: 11.5px; color: var(--ink-faint); }
39
+ /* task 2163 — the real goal id, shown as a monospace #NNN token so it reads as
40
+ an identifier and never gets confused with the priority ordinal on the left. */
41
+ .goal-card__id, .goal-detail__id {
42
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
43
+ font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink-faint);
44
+ }
45
+ .goal-card__id { padding: 0 5px; border: 1px solid var(--rule-soft); border-radius: 5px; }
46
+ .goal-detail__id { font-size: 0.72em; vertical-align: 0.12em; margin-right: 4px; }
47
+ .goal-card__foot { display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 11.5px; color: var(--ink-soft); }
48
+ .goal-card__foot strong { font-variant-numeric: tabular-nums; }
49
+ /* boot skeleton — goal-card-shaped shimmer placeholders (kit .kit-skel bars)
50
+ so the grid doesn't jump when data lands. */
51
+ .goal-card--skel { pointer-events: none; }
52
+ .goal-card--skel .kit-skel { width: 80%; }
53
+ .goal-card--skel .kit-skel + .kit-skel { width: 55%; }
54
+
55
+ /* ---- detail view ---- */
56
+ .goal-detail__head { display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap; }
57
+ .goal-detail__title { font-size: 19px; font-weight: 650; letter-spacing: -0.01em; margin: 0; }
58
+ .goal-detail__desc { color: var(--ink-soft); font-size: 13.5px; max-width: 72ch; margin: 6px 0 0; }
59
+ .goal-detail__actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
60
+ /* task 1888/1828 — Completion | Members | Conflicts share ONE card. Wide:
61
+ three columns split by vertical rules. Narrow: they stack, split by
62
+ horizontal rules. The .scroll gives the outer frame; columns own padding
63
+ (the .scroll padding is zeroed here). Conflicts live up here so a long
64
+ work list can no longer push them off the bottom of the page. */
65
+ .goal-band { padding: 0; margin-top: 20px; display: grid; grid-template-columns: 1fr; }
66
+ .goal-band__col { padding: 14px 18px; min-width: 0; }
67
+ .goal-band__col + .goal-band__col { border-top: 1px solid var(--rule-soft); }
68
+ @media (min-width: 900px) {
69
+ .goal-band { grid-template-columns: repeat(3, minmax(0, 1fr)); }
70
+ .goal-band__col + .goal-band__col { border-top: none; border-left: 1px solid var(--rule-soft); }
71
+ }
72
+
73
+ /* Live conflicts: the band COLUMN can't own the card border, so it gets a
74
+ subtle amber wash; the standalone full-view card keeps the amber border.
75
+ (Count pills on these headers are the shared .count-pill.) */
76
+ .goal-conflicts-card--live { background: color-mix(in srgb, var(--warn) 6%, transparent); }
77
+ .goal-conflicts-card--live .count-pill { background: var(--warn-weak); color: var(--warn); }
78
+ #goal-conflicts-view-card.goal-conflicts-card--live { background: var(--surface); border-color: color-mix(in srgb, var(--warn) 40%, var(--border)); }
79
+
80
+ /* task 1828 — inline Claim on a ready To-do row + the conflicts "view all". */
81
+ .goal-claim-btn {
82
+ display: inline-flex; align-items: center; justify-content: center;
83
+ height: 24px; padding: 0 12px; border-radius: var(--radius-sm);
84
+ border: 1px solid var(--accent); background: var(--accent); color: #fff;
85
+ font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
86
+ }
87
+ .goal-claim-btn:hover:not(:disabled) { background: var(--accent-deep); }
88
+ .goal-claim-btn:disabled { opacity: 0.55; cursor: default; }
89
+ /* task 1828 — Release on the viewer's OWN active claim (ghost, not accent —
90
+ it's the undo of a claim); a muted label for another builder's claim. */
91
+ .goal-release-btn {
92
+ display: inline-flex; align-items: center; justify-content: center;
93
+ height: 24px; padding: 0 12px; border-radius: var(--radius-sm);
94
+ border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink-soft);
95
+ font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
96
+ }
97
+ .goal-release-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
98
+ .goal-release-btn:disabled { opacity: 0.55; cursor: default; }
99
+ .ledger__claimed { font-size: 12px; color: var(--ink-faint); white-space: nowrap; }
100
+ .ledger__action { text-align: right; white-space: nowrap; }
101
+ /* task 1828 — shared "View all N →" foot under a capped band column. */
102
+ .goal-col__foot { margin-top: 10px; }
103
+
104
+ /* task 2171 density pass — the Work tables were the airiest surface on the
105
+ page (12px cells): tighter cells + type lift rows-per-screen ~40%. */
106
+ #goal-todo th, #goal-todo td, #goal-done th, #goal-done td { padding: 7px 10px; }
107
+ #goal-todo td, #goal-done td { font-size: 12.5px; }
108
+
109
+ /* task 1828 — full completion view: each criterion's detail shown inline. */
110
+ .crit-list--full .crit { align-items: start; }
111
+ .crit__col { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
112
+ .crit__detail { font-size: 12px; color: var(--ink-faint); line-height: 1.45; }
113
+
114
+ /* task 1828 — full members view: goal-level aggregate + per-member stats. */
115
+ .goal-mstats { display: flex; flex-wrap: wrap; gap: 8px 20px; margin: 0 0 16px; font-size: 13px; color: var(--ink-soft); }
116
+ .mstat strong { color: var(--ink); font-variant-numeric: tabular-nums; font-weight: 650; }
117
+ .member--stat { gap: 12px; }
118
+ .member__stats { font-size: 12.5px; color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
119
+ .member__stats strong { color: var(--ink); font-weight: 600; }
120
+ .member__stats--pending { color: var(--ink-faint); }
121
+
122
+ /* task 1888 — the single Work card: header row with the to-do/completed tab
123
+ toggle (kit tabs), paginated tables underneath. */
124
+ .goal-work__head { display: flex; align-items: center; justify-content: space-between; gap: 10px 14px; flex-wrap: wrap; margin: 0 0 12px; }
125
+ .goal-work__head .scroll__h { margin: 0; }
126
+ .goal-work__tabs .kit-tabs { margin: 0; }
127
+ .conflict__sev {
128
+ display: inline-flex; align-items: center; height: 18px; padding: 0 7px; margin-left: 8px;
129
+ border-radius: 999px; font-size: 10.5px; font-weight: 600; vertical-align: 1px;
130
+ background: var(--surface-2); color: var(--ink-soft);
131
+ }
132
+ .conflict__sev--high { background: var(--danger-weak); color: var(--danger); }
133
+ .conflict__sev--medium { background: var(--warn-weak); color: var(--warn); }
134
+
135
+ .crit-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; }
136
+ .crit { display: grid; grid-template-columns: 22px minmax(0,1fr); gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; }
137
+ .crit:last-child { border-bottom: none; }
138
+ .crit__mark { width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--border-strong); display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--ink-faint); }
139
+ .crit--done .crit__mark { background: var(--ok); border-color: var(--ok); color: #fff; }
140
+ .crit--done .crit__text { color: var(--ink-soft); }
141
+ /* BV1.R79: per-criterion expand in the capped band — short label, full text on
142
+ click (matches the Roadmap page's .vbar__crit-d pattern). */
143
+ .crit-d { min-width: 0; }
144
+ .crit-d > summary { cursor: pointer; list-style: none; text-decoration: underline dotted var(--border-strong); text-underline-offset: 3px; }
145
+ .crit-d > summary::-webkit-details-marker { display: none; }
146
+ .crit-d[open] > summary { text-decoration: none; font-weight: 500; }
147
+ .crit-d .crit__detail { margin-top: 4px; }
148
+
149
+ .member-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
150
+ .member { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; }
151
+ .member:last-child { border-bottom: none; }
152
+ .member__avatar { width: 24px; height: 24px; border-radius: 50%; background: var(--accent-weak); color: var(--accent-text); font-size: 10px; font-weight: 600; display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0; }
153
+ .member__avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
154
+ .member__name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
155
+ .member__name small { color: var(--ink-faint); }
156
+ .role-pill { display: inline-flex; align-items: center; height: 20px; padding: 0 9px; border-radius: 999px; font-size: 11px; font-weight: 600; background: var(--surface-2); color: var(--ink-soft); }
157
+ .role-pill--lead { background: var(--accent-weak); color: var(--accent-text); }
158
+
159
+ /* task 1762/1877 — owner/manager invite control (goal member admission),
160
+ collapsed behind "+ Invite" (disclosure, closed by default — task 2171). */
161
+ .goal-invite { margin: 4px 0 10px; }
162
+ .goal-invite__form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
163
+ .goal-invite__form select#goal-invite-builder { flex: 1; min-width: 180px; }
164
+
165
+ /* task 1953 — private goals: the lock badge (meta line) + the goal's own
166
+ pending invite/join-request queue (manager view, next to the invite box). */
167
+ .goal-badge--locked { background: var(--warn-weak); color: var(--warn); }
168
+ .goal-pending { margin: 4px 0 10px; display: flex; flex-direction: column; gap: 10px; }
169
+ .pending-group__h { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-faint); margin: 0 0 4px; display: flex; align-items: center; gap: 6px; }
170
+ .pending-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
171
+ .pending-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; flex-wrap: wrap; }
172
+ .pending-row:last-child { border-bottom: none; }
173
+ .pending-row__who { font-weight: 600; }
174
+ .pending-row__note { color: var(--ink-faint); font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
175
+ .pending-row__actions { display: flex; gap: 6px; margin-left: auto; }
176
+
177
+ .conflict { border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); background: var(--warn-weak); border-radius: var(--radius-sm); padding: 8px 12px; font-size: 12.5px; margin: 8px 0 0; }
178
+ .conflict__pair { font-weight: 600; }
179
+ .conflict__paths { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11.5px; color: var(--ink-soft); margin-top: 4px; word-break: break-all; }
180
+ .conflict-note { font-size: 12px; color: var(--ink-faint); margin: 10px 0 0; }
181
+
182
+ /* Filtered-count line under the kit filter bar (list view). */
183
+ .goals-count-line { margin: -4px 0 12px; font-size: 12.5px; color: var(--ink-faint); }
@@ -10,183 +10,10 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
12
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-03-goals1">
13
- <style>
14
- /* page-local styles — goals list + detail (task 1734); tokens only */
15
- .goal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
16
- .goal-card {
17
- display: flex; flex-direction: column; gap: 10px;
18
- background: var(--surface); border: 1px solid var(--border);
19
- border-radius: var(--radius); padding: 16px 18px;
20
- color: var(--ink);
21
- }
22
- a.goal-card:hover { text-decoration: none; border-color: var(--border-strong); background: var(--surface-2); }
23
- .goal-card__head { display: flex; align-items: flex-start; gap: 12px; }
24
- /* task 1760 — priority (visible once narrowed to one version) + Archon
25
- drag-to-reorder handle (server enforces the write; this is the affordance). */
26
- .goal-card__handle { flex: none; cursor: grab; color: var(--ink-faint); font-size: 14px; line-height: 1.2; touch-action: none; }
27
- .goal-card__handle:active { cursor: grabbing; }
28
- .goal-card__priority {
29
- flex: none; min-width: 1.4em; text-align: center;
30
- font-size: 11.5px; font-weight: 600; color: var(--ink-faint);
31
- font-variant-numeric: tabular-nums;
32
- }
33
- .goal-card--dragging { opacity: 0.45; }
34
- /* task 1761: a goal blocked by an unachieved prerequisite is dimmed + hue-shifted. */
35
- .goal-card--blocked { opacity: 0.62; border-style: dashed; }
36
- .goal-card--blocked:hover { opacity: 0.85; }
37
- .goal-deps { color: var(--ink-faint); }
38
- .goal-deps--blocked { color: var(--warn); font-weight: 600; }
39
- .goal-card__title { font-weight: 650; font-size: 14px; flex: 1; min-width: 0; }
40
- .goal-card__meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 12px; color: var(--ink-faint); }
41
- /* task 2163 — the real goal id, shown as a monospace #NNN token so it reads as
42
- an identifier and never gets confused with the priority ordinal on the left. */
43
- .goal-card__id, .goal-detail__id {
44
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
45
- font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink-faint);
46
- }
47
- .goal-card__id { padding: 0 5px; border: 1px solid var(--rule-soft); border-radius: 5px; }
48
- .goal-detail__id { font-size: 0.72em; vertical-align: 0.12em; margin-right: 4px; }
49
- .goal-card__foot { display: flex; gap: 14px; font-size: 12.5px; color: var(--ink-soft); }
50
- .goal-card__foot strong { font-variant-numeric: tabular-nums; }
51
-
52
- .goal-detail__head { display: flex; align-items: flex-start; gap: 20px; flex-wrap: wrap; }
53
- .goal-detail__title { font-size: 19px; font-weight: 650; letter-spacing: -0.01em; margin: 0; }
54
- .goal-detail__desc { color: var(--ink-soft); font-size: 13.5px; max-width: 72ch; margin: 6px 0 0; }
55
- .goal-detail__actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
56
- /* task 1888/1828 — Completion | Members | Conflicts share ONE card. Wide:
57
- three columns split by vertical rules. Narrow: they stack, split by
58
- horizontal rules. The .scroll gives the outer frame; columns own padding
59
- (the .scroll padding is zeroed here). Conflicts live up here so a long
60
- work list can no longer push them off the bottom of the page. */
61
- .goal-band { padding: 0; margin-top: 20px; display: grid; grid-template-columns: 1fr; }
62
- .goal-band__col { padding: 18px 22px; min-width: 0; }
63
- .goal-band__col + .goal-band__col { border-top: 1px solid var(--rule-soft); }
64
- @media (min-width: 900px) {
65
- .goal-band { grid-template-columns: repeat(3, minmax(0, 1fr)); }
66
- .goal-band__col + .goal-band__col { border-top: none; border-left: 1px solid var(--rule-soft); }
67
- }
68
-
69
- /* task 1888 — count pill on card/tab headers + the live-conflicts accent. */
70
- .goal-count {
71
- display: inline-flex; align-items: center; justify-content: center;
72
- min-width: 20px; height: 18px; padding: 0 6px; margin-left: 8px;
73
- border-radius: 999px; background: var(--surface-2); color: var(--ink-soft);
74
- font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
75
- }
76
- /* Live conflicts: the band COLUMN can't own the card border, so it gets a
77
- subtle amber wash; the standalone full-view card keeps the amber border. */
78
- .goal-conflicts-card--live { background: color-mix(in srgb, var(--warn) 6%, transparent); }
79
- .goal-conflicts-card--live .goal-count { background: var(--warn-weak); color: var(--warn); }
80
- #goal-conflicts-view-card.goal-conflicts-card--live { background: var(--surface); border-color: color-mix(in srgb, var(--warn) 40%, var(--border)); }
81
-
82
- /* task 1828 — inline Claim on a ready To-do row + the conflicts "view all". */
83
- .goal-claim-btn {
84
- display: inline-flex; align-items: center; justify-content: center;
85
- height: 26px; padding: 0 12px; border-radius: var(--radius-sm);
86
- border: 1px solid var(--accent); background: var(--accent); color: #fff;
87
- font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
88
- }
89
- .goal-claim-btn:hover:not(:disabled) { background: var(--accent-deep); }
90
- .goal-claim-btn:disabled { opacity: 0.55; cursor: default; }
91
- /* task 1828 — Release on the viewer's OWN active claim (ghost, not accent —
92
- it's the undo of a claim); a muted label for another builder's claim. */
93
- .goal-release-btn {
94
- display: inline-flex; align-items: center; justify-content: center;
95
- height: 26px; padding: 0 12px; border-radius: var(--radius-sm);
96
- border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink-soft);
97
- font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
98
- }
99
- .goal-release-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }
100
- .goal-release-btn:disabled { opacity: 0.55; cursor: default; }
101
- .ledger__claimed { font-size: 12px; color: var(--ink-faint); white-space: nowrap; }
102
- .ledger__action { text-align: right; white-space: nowrap; }
103
- /* task 1828 — shared "View all N →" foot under a capped band column. */
104
- .goal-col__foot { margin-top: 12px; }
105
-
106
- /* task 1828 — full completion view: each criterion's detail shown inline. */
107
- .crit-list--full .crit { align-items: start; }
108
- .crit__col { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
109
- .crit__detail { font-size: 12px; color: var(--ink-faint); line-height: 1.45; }
110
-
111
- /* task 1828 — full members view: goal-level aggregate + per-member stats. */
112
- .goal-mstats { display: flex; flex-wrap: wrap; gap: 8px 20px; margin: 0 0 16px; font-size: 13px; color: var(--ink-soft); }
113
- .mstat strong { color: var(--ink); font-variant-numeric: tabular-nums; font-weight: 650; }
114
- .member--stat { gap: 12px; }
115
- .member__stats { font-size: 12.5px; color: var(--ink-soft); white-space: nowrap; font-variant-numeric: tabular-nums; }
116
- .member__stats strong { color: var(--ink); font-weight: 600; }
117
- .member__stats--pending { color: var(--ink-faint); }
118
-
119
- /* task 1888 — the single Work card: header row with the to-do/completed tab
120
- toggle (house .setup__tab pattern), capped tables + pager underneath. */
121
- .goal-work__head { display: flex; align-items: center; justify-content: space-between; gap: 10px 14px; flex-wrap: wrap; margin: 0 0 12px; }
122
- .goal-work__head .scroll__h { margin: 0; }
123
- .goal-work__tabs { margin: 0; }
124
- .conflict__sev {
125
- display: inline-flex; align-items: center; height: 18px; padding: 0 7px; margin-left: 8px;
126
- border-radius: 999px; font-size: 10.5px; font-weight: 600; vertical-align: 1px;
127
- background: var(--surface-2); color: var(--ink-soft);
128
- }
129
- .conflict__sev--high { background: var(--danger-weak); color: var(--danger); }
130
- .conflict__sev--medium { background: var(--warn-weak); color: var(--warn); }
131
-
132
- .crit-list { list-style: none; margin: 10px 0 0; padding: 0; display: flex; flex-direction: column; }
133
- .crit { display: grid; grid-template-columns: 22px minmax(0,1fr); gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; }
134
- .crit:last-child { border-bottom: none; }
135
- .crit__mark { width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--border-strong); display: flex; align-items: center; justify-content: center; font-size: 11px; color: var(--ink-faint); }
136
- .crit--done .crit__mark { background: var(--ok); border-color: var(--ok); color: #fff; }
137
- .crit--done .crit__text { color: var(--ink-soft); }
138
- /* BV1.R79: per-criterion expand in the capped band — short label, full text on
139
- click (matches the Roadmap page's .vbar__crit-d pattern). */
140
- .crit-d { min-width: 0; }
141
- .crit-d > summary { cursor: pointer; list-style: none; text-decoration: underline dotted var(--border-strong); text-underline-offset: 3px; }
142
- .crit-d > summary::-webkit-details-marker { display: none; }
143
- .crit-d[open] > summary { text-decoration: none; font-weight: 500; }
144
- .crit-d .crit__detail { margin-top: 4px; }
145
-
146
- .member-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
147
- .member { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13.5px; }
148
- .member:last-child { border-bottom: none; }
149
- .member__avatar { width: 26px; height: 26px; border-radius: 50%; background: var(--accent-weak); color: var(--accent-text); font-size: 10.5px; font-weight: 600; display: flex; align-items: center; justify-content: center; overflow: hidden; flex-shrink: 0; }
150
- .member__avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
151
- .member__name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
152
- .member__name small { color: var(--ink-faint); }
153
- .role-pill { display: inline-flex; align-items: center; height: 20px; padding: 0 9px; border-radius: 999px; font-size: 11px; font-weight: 600; background: var(--surface-2); color: var(--ink-soft); }
154
- .role-pill--lead { background: var(--accent-weak); color: var(--accent-text); }
155
-
156
- /* task 1762/1877 — owner/manager invite control (goal member admission). */
157
- .goal-invite { margin: 4px 0 10px; }
158
- .goal-invite__form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
159
- .goal-invite__form select#goal-invite-builder { flex: 1; min-width: 180px; }
160
-
161
- /* task 1953 — private goals: the lock badge (meta line) + the goal's own
162
- pending invite/join-request queue (manager view, next to the invite box). */
163
- .goal-badge--locked { background: var(--warn-weak); color: var(--warn); }
164
- .goal-pending { margin: 4px 0 10px; display: flex; flex-direction: column; gap: 10px; }
165
- .pending-group__h { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; color: var(--ink-faint); margin: 0 0 4px; display: flex; align-items: center; gap: 6px; }
166
- .pending-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
167
- .pending-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13px; flex-wrap: wrap; }
168
- .pending-row:last-child { border-bottom: none; }
169
- .pending-row__who { font-weight: 600; }
170
- .pending-row__note { color: var(--ink-faint); font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
171
- .pending-row__actions { display: flex; gap: 6px; margin-left: auto; }
172
-
173
- .conflict { border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); background: var(--warn-weak); border-radius: var(--radius-sm); padding: 10px 14px; font-size: 13px; margin: 10px 0 0; }
174
- .conflict__pair { font-weight: 600; }
175
- .conflict__paths { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11.5px; color: var(--ink-soft); margin-top: 4px; word-break: break-all; }
176
- .conflict-note { font-size: 12px; color: var(--ink-faint); margin: 10px 0 0; }
177
-
178
- .goal-filters { display: flex; gap: 10px 14px; flex-wrap: wrap; align-items: flex-end; margin: 0 0 16px; }
179
-
180
- /* task 1932: paginated goal grid (Per-page dropdown + Prev/Next). .pager /
181
- .pager__range / .btn-ghost come from the shared style.css; these extend
182
- that pager into a two-sided bar (range left, controls right). */
183
- .pager--paged { justify-content: space-between; flex-wrap: wrap; gap: 10px 16px; }
184
- .pager__nav { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
185
- .pager__perpage { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-faint); }
186
- .pager__perpage select { height: 30px; padding: 2px 8px; font-size: 13px; }
187
- .pager__page { font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; min-width: 6.5em; text-align: center; }
188
- .pager__nav .pager__btn:disabled { opacity: 0.4; cursor: default; }
189
- </style>
13
+ <!-- Page-local styles (task 2171) — extracted from the old inline <style>
14
+ block per the hall placement rules; shared pager/filter/tab/count-pill
15
+ styles now live in style.css's hall-kit section. -->
16
+ <link rel="stylesheet" href="/builders/goals.css">
190
17
  </head>
191
18
  <body data-page="goals" data-page-title="Goals">
192
19
 
@@ -204,39 +31,16 @@
204
31
  <!-- LIST view -->
205
32
  <section id="goals-list-view" aria-labelledby="goals-list-h">
206
33
  <h2 id="goals-list-h" class="sr-only">All goals</h2>
207
- <div class="goal-filters filters">
208
- <label class="filter"><span>Version</span><select id="f-goal-version"><option value="">— any —</option></select></label>
209
- <label class="filter"><span>Status</span><select id="f-goal-status">
210
- <option value="">— any —</option>
211
- <option value="open" selected>open</option>
212
- <option value="achieved">achieved</option>
213
- <option value="archived">archived</option>
214
- </select></label>
215
- <!-- task 1828 — filter to the goals you're a member of. -->
216
- <label class="filter"><span>Membership</span><select id="f-goal-mine">
217
- <option value="">All goals</option>
218
- <option value="mine">My goals</option>
219
- </select></label>
220
- <span class="filter__count" id="goals-count" aria-live="polite"></span>
221
- </div>
222
- <div class="goal-grid" id="goal-grid"><div class="profile__placeholder">…</div></div>
223
- <div class="pager pager--paged" id="goals-pager" hidden>
224
- <span class="pager__range" id="goals-range"></span>
225
- <div class="pager__nav">
226
- <label class="pager__perpage">
227
- <span>Per page</span>
228
- <select id="goals-per-page" aria-label="Goals per page">
229
- <option value="10">10</option>
230
- <option value="25">25</option>
231
- <option value="50">50</option>
232
- <option value="100">100</option>
233
- </select>
234
- </label>
235
- <button class="btn-ghost pager__btn" type="button" id="goals-prev" aria-label="Previous page">‹ Prev</button>
236
- <span class="pager__page" id="goals-page-ind" aria-live="polite"></span>
237
- <button class="btn-ghost pager__btn" type="button" id="goals-next" aria-label="Next page">Next ›</button>
238
- </div>
239
- </div>
34
+ <!-- task 2171: the filter bar is built by OTBKit.makeFilterBar —
35
+ Version / Status / Membership, all primary (no overflow). Control
36
+ ids are unchanged (f-goal-version, f-goal-status, f-goal-mine)
37
+ so goals-page.js reads stay stable. -->
38
+ <div id="goals-filters"></div>
39
+ <p class="goals-count-line" id="goals-count" aria-live="polite"></p>
40
+ <div class="goal-grid" id="goal-grid"></div>
41
+ <!-- task 2171: the grid's pager bar is stamped here by OTBKit
42
+ (pagerBarHtml('goals') — same ids, same otb.goals.perPage pref). -->
43
+ <div id="goals-pager-mount"></div>
240
44
  </section>
241
45
 
242
46
  <!-- DETAIL view (hash-routed: #/goal/N) -->
@@ -257,13 +61,13 @@
257
61
  (three columns; stacks with horizontal dividers when narrow). -->
258
62
  <div class="scroll goal-band">
259
63
  <div class="goal-band__col">
260
- <h2 class="scroll__h" id="goal-completion-h">Completion<span class="goal-count" id="goal-completion-count" hidden></span></h2>
261
- <div id="goal-completion-body"><div class="profile__placeholder">…</div></div>
64
+ <h2 class="scroll__h" id="goal-completion-h">Completion<span class="count-pill" id="goal-completion-count" hidden></span></h2>
65
+ <div id="goal-completion-body"></div>
262
66
  <!-- task 1828 — each column caps its (growing) list + clicks into a full view. -->
263
67
  <div class="goal-col__foot" id="goal-completion-foot" hidden></div>
264
68
  </div>
265
69
  <div class="goal-band__col">
266
- <h2 class="scroll__h">Members<span class="goal-count" id="goal-members-count" hidden></span></h2>
70
+ <h2 class="scroll__h">Members<span class="count-pill" id="goal-members-count" hidden></span></h2>
267
71
  <p class="scroll__lede" id="goal-members-lede"></p>
268
72
  <div class="goal-invite" id="goal-invite" hidden></div>
269
73
  <!-- task 1953 — the goal's own pending invite/join-request queue
@@ -274,9 +78,9 @@
274
78
  <div class="goal-col__foot" id="goal-members-foot" hidden></div>
275
79
  </div>
276
80
  <div class="goal-band__col" id="goal-conflicts-card">
277
- <h2 class="scroll__h">Conflicts<span class="goal-count" id="goal-conflicts-count" hidden></span></h2>
81
+ <h2 class="scroll__h">Conflicts<span class="count-pill" id="goal-conflicts-count" hidden></span></h2>
278
82
  <p class="scroll__lede">Open tasks whose declared files overlap — sequence these before two builders collide.</p>
279
- <div id="goal-conflicts"><div class="profile__placeholder">…</div></div>
83
+ <div id="goal-conflicts"></div>
280
84
  <!-- task 1828 — replaces the show-all pager: click into a dedicated
281
85
  full-width conflicts view instead of expanding this column. -->
282
86
  <div class="goal-col__foot" id="goal-conflicts-foot" hidden></div>
@@ -286,12 +90,12 @@
286
90
  <div class="scroll" style="margin-top:20px">
287
91
  <div class="goal-work__head">
288
92
  <h2 class="scroll__h">Work</h2>
289
- <div class="setup__toggle goal-work__tabs" role="tablist" aria-label="Goal tasks">
290
- <button class="setup__tab" type="button" role="tab" id="goal-tab-todo" aria-selected="true" aria-controls="goal-todo-pane">To do<span class="goal-count" id="goal-todo-count" hidden></span></button>
291
- <button class="setup__tab" type="button" role="tab" id="goal-tab-done" aria-selected="false" aria-controls="goal-done-pane">Completed<span class="goal-count" id="goal-done-count" hidden></span></button>
292
- </div>
93
+ <!-- task 2171: the To do / Completed toggle is built by
94
+ OTBKit.makeTabs (hash:false — the #/goal/N sub-route
95
+ namespace stays untouched; tabs key off in-page state). -->
96
+ <div class="goal-work__tabs" id="goal-work-tabs" aria-label="Goal tasks"></div>
293
97
  </div>
294
- <div id="goal-todo-pane" role="tabpanel" aria-labelledby="goal-tab-todo">
98
+ <div id="goal-todo-pane" role="tabpanel" aria-labelledby="tab-todo">
295
99
  <!-- task 1828 — Kind / Status / Credits headers sort the list (click
296
100
  cycles desc → asc → natural); a ready row carries an inline Claim. -->
297
101
  <table class="ledger" id="goal-todo"><thead><tr>
@@ -302,25 +106,9 @@
302
106
  <th aria-label="Actions"></th>
303
107
  </tr></thead>
304
108
  <tbody><tr><td colspan="6" class="ledger__empty">…</td></tr></tbody></table>
305
- <div class="pager pager--paged" id="goal-todo-pager" hidden>
306
- <span class="pager__range" id="goal-todo-range"></span>
307
- <div class="pager__nav">
308
- <label class="pager__perpage">
309
- <span>Per page</span>
310
- <select id="goal-todo-per-page" aria-label="To-do tasks per page">
311
- <option value="10">10</option>
312
- <option value="25">25</option>
313
- <option value="50">50</option>
314
- <option value="100">100</option>
315
- </select>
316
- </label>
317
- <button class="btn-ghost pager__btn" type="button" id="goal-todo-prev" aria-label="Previous page">‹ Prev</button>
318
- <span class="pager__page" id="goal-todo-page-ind" aria-live="polite"></span>
319
- <button class="btn-ghost pager__btn" type="button" id="goal-todo-next" aria-label="Next page">Next ›</button>
320
- </div>
321
- </div>
109
+ <div id="goal-todo-pager-mount"></div>
322
110
  </div>
323
- <div id="goal-done-pane" role="tabpanel" aria-labelledby="goal-tab-done" hidden>
111
+ <div id="goal-done-pane" role="tabpanel" aria-labelledby="tab-done" hidden>
324
112
  <table class="ledger" id="goal-done"><thead><tr>
325
113
  <th>№</th><th>Task</th>
326
114
  <th class="ledger__sortable" id="goal-done-sort-kind" role="button" tabindex="0" aria-sort="none">Kind</th>
@@ -328,23 +116,7 @@
328
116
  <th class="ledger__sortable" id="goal-done-sort-shipped" role="button" tabindex="0" aria-sort="none">Shipped</th>
329
117
  </tr></thead>
330
118
  <tbody><tr><td colspan="5" class="ledger__empty">…</td></tr></tbody></table>
331
- <div class="pager pager--paged" id="goal-done-pager" hidden>
332
- <span class="pager__range" id="goal-done-range"></span>
333
- <div class="pager__nav">
334
- <label class="pager__perpage">
335
- <span>Per page</span>
336
- <select id="goal-done-per-page" aria-label="Completed tasks per page">
337
- <option value="10">10</option>
338
- <option value="25">25</option>
339
- <option value="50">50</option>
340
- <option value="100">100</option>
341
- </select>
342
- </label>
343
- <button class="btn-ghost pager__btn" type="button" id="goal-done-prev" aria-label="Previous page">‹ Prev</button>
344
- <span class="pager__page" id="goal-done-page-ind" aria-live="polite"></span>
345
- <button class="btn-ghost pager__btn" type="button" id="goal-done-next" aria-label="Next page">Next ›</button>
346
- </div>
347
- </div>
119
+ <div id="goal-done-pager-mount"></div>
348
120
  </div>
349
121
  </div>
350
122
  </section>
@@ -354,9 +126,9 @@
354
126
  <section id="goal-conflicts-view" hidden aria-labelledby="goal-conflicts-view-h">
355
127
  <p class="card-foot" style="margin:0 0 14px"><a href="#" id="goal-conflicts-back">← Back to goal</a></p>
356
128
  <div class="scroll" style="margin:0" id="goal-conflicts-view-card">
357
- <h2 class="scroll__h" id="goal-conflicts-view-h">Conflicts<span class="goal-count" id="goal-conflicts-view-count" hidden></span></h2>
129
+ <h2 class="scroll__h" id="goal-conflicts-view-h">Conflicts<span class="count-pill" id="goal-conflicts-view-count" hidden></span></h2>
358
130
  <p class="scroll__lede">Open tasks whose declared files overlap — sequence these before two builders collide.</p>
359
- <div id="goal-conflicts-full"><div class="profile__placeholder">…</div></div>
131
+ <div id="goal-conflicts-full"></div>
360
132
  </div>
361
133
  </section>
362
134
 
@@ -365,10 +137,10 @@
365
137
  <section id="goal-members-view" hidden aria-labelledby="goal-members-view-h">
366
138
  <p class="card-foot" style="margin:0 0 14px"><a href="#" id="goal-members-back">← Back to goal</a></p>
367
139
  <div class="scroll" style="margin:0">
368
- <h2 class="scroll__h" id="goal-members-view-h">Members<span class="goal-count" id="goal-members-view-count" hidden></span></h2>
140
+ <h2 class="scroll__h" id="goal-members-view-h">Members<span class="count-pill" id="goal-members-view-count" hidden></span></h2>
369
141
  <p class="scroll__lede">Who’s on this goal, and what they’ve contributed to it.</p>
370
142
  <div class="goal-mstats" id="goal-members-stats"></div>
371
- <div id="goal-members-full"><div class="profile__placeholder">…</div></div>
143
+ <div id="goal-members-full"></div>
372
144
  </div>
373
145
  </section>
374
146
 
@@ -379,7 +151,7 @@
379
151
  <div class="scroll" style="margin:0">
380
152
  <h2 class="scroll__h" id="goal-completion-view-h">Completion</h2>
381
153
  <p class="scroll__lede">Every done-when criterion for this goal, with its full detail.</p>
382
- <div id="goal-completion-full"><div class="profile__placeholder">…</div></div>
154
+ <div id="goal-completion-full"></div>
383
155
  </div>
384
156
  </section>
385
157
  </main>
@@ -393,8 +165,10 @@
393
165
 
394
166
  <script src="/builders/shell.js?v=2026-07-03-goals1"></script>
395
167
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
168
+ <!-- shared component kit (window.OTBKit) — pager, filter bar, tabs, empty/skeleton (task 2166). -->
169
+ <script src="/builders/hall-kit.js"></script>
396
170
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
397
171
  <script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
398
- <script src="/builders/goals-page.js?v=2026-07-03-goals4"></script>
172
+ <script src="/builders/goals-page.js?v=2026-07-10-goals5"></script>
399
173
  </body>
400
174
  </html>
@@ -74,14 +74,19 @@
74
74
  </li>`;
75
75
  }
76
76
 
77
+ // task 2184 (Home rule: a summary card never lists more than 5 rows): cap the
78
+ // joined-goals list; the overflow count rides the "All goals" foot link.
79
+ const MAX_HOME_ROWS = 5;
77
80
  function render(joined) {
78
81
  const body = host.querySelector('#goals-body');
79
82
  if (!joined.length) {
80
83
  body.innerHTML = '<p class="goal-empty">You haven’t joined any goals yet. <a href="/goals">Browse goals →</a></p>';
81
84
  return;
82
85
  }
83
- body.innerHTML = `<ul class="goal-mine-list">${joined.map(row).join('')}</ul>` +
84
- '<p class="goal-mine__foot card-foot"><a href="/goals">All goals →</a></p>';
86
+ const shown = joined.slice(0, MAX_HOME_ROWS);
87
+ const more = joined.length - shown.length;
88
+ body.innerHTML = `<ul class="goal-mine-list">${shown.map(row).join('')}</ul>` +
89
+ `<p class="goal-mine__foot card-foot"><a href="/goals">${more > 0 ? `All ${joined.length} goals →` : 'All goals →'}</a></p>`;
85
90
  }
86
91
 
87
92
  const SKELETON =
@@ -81,10 +81,18 @@
81
81
  // goal-inbox.js owns (#attention-ideas), the single landing place for every
82
82
  // actionable item. builders.js still gates the /inbox fetch to Metic+.)
83
83
 
84
- // 0e. Recent work — compact 5-row table (task 1723). Filled by builders.js
85
- // renderRecentShips from the same feed as the full Your-work table.
86
- w('home-recent', 'Recent work', 28,
87
- '<h2 id="home-recent-h" class="scroll__h">Recent work</h2>' +
84
+ // 0e. Your work — "what's my next action" + the compact recent table
85
+ // (task 1723; grown by task 2184). The Next-up slice surfaces the top of the
86
+ // claimable queue (builders.js loadNextUp fills it; hidden when the feed is
87
+ // gated or empty), then the 5-row recent-ships table below it.
88
+ w('home-recent', 'Your work', 28,
89
+ '<h2 id="home-recent-h" class="scroll__h">Your work</h2>' +
90
+ '<div id="home-next" hidden>' +
91
+ '<h3 class="ginbox__sec-h">Next up — ready to claim</h3>' +
92
+ '<ul class="next-up" id="home-next-list"></ul>' +
93
+ '<p class="next-up__foot"><a href="/work">See all ready work →</a></p>' +
94
+ '</div>' +
95
+ '<h3 class="ginbox__sec-h" id="home-recent-subh" hidden>Recently shipped</h3>' +
88
96
  '<table class="ledger" id="home-recent-table"><thead><tr><th>№</th><th>Work</th><th data-currency-label="caps">Credits</th><th>Shipped</th></tr></thead>' +
89
97
  '<tbody><tr><td colspan="4" class="ledger__empty">…</td></tr></tbody></table>' +
90
98
  '<p class="card-foot"><a href="#standing">All your work →</a></p>',