@bongos/core 1.19.462 → 1.19.463
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.
- package/.bongos-core.json +39 -34
- package/docs/copy-inventory.md +122 -125
- package/docs/copy-registry.json +306 -322
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/goals-lib.js +10 -6
- package/modules/hall-ui/public/goals-page.js +27 -12
- package/modules/hall-ui/public/goals-render.js +83 -52
- package/modules/hall-ui/public/goals.css +129 -120
- package/modules/hall-ui/public/goals.html +29 -11
- package/modules/hall-ui/public/goals.states.json +71 -3
- package/modules/hall-ui/public/people.css +27 -0
- package/modules/hall-ui/public/people.html +15 -7
- package/modules/hall-ui/public/people.js +111 -22
- package/modules/hall-ui/public/people.states.json +31 -1
- package/modules/hall-ui/public/scouting.html +30 -17
- package/modules/hall-ui/public/scouting.states.json +7 -2
- package/modules/hall-ui/public/sessions.css +18 -15
- package/modules/hall-ui/public/sessions.html +27 -22
- package/modules/hall-ui/public/sessions.js +32 -13
- package/modules/hall-ui/public/sessions.states.json +3 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_roster_world.mjs +287 -0
- package/modules/hall-ui/public/scouting.js +0 -73
package/docs/file-map.md
CHANGED
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
│ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
|
|
128
128
|
│ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
|
|
129
129
|
│ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
|
|
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.
|
|
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, `people.css` the directory's identity cell (task 1003470: `/scouting` is the LEGACY DOOR to `/people` and ships the same body and script — `scouting.html` loads `people.js`; there is no `scouting.js`).
|
|
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
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. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. 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).
|
|
@@ -1361,5 +1361,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1361
1361
|
landed since 1.19.460 with no explicit bump. run 33571982271. (task 1002620)
|
|
1362
1362
|
1.19.462 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1363
1363
|
landed since 1.19.461 with no explicit bump. run 33576366095. (task 1002620)
|
|
1364
|
+
1.19.463 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1365
|
+
landed since 1.19.462 with no explicit bump. run 33577660347. (task 1002620)
|
|
1364
1366
|
---------------------------------------------------------------------------
|
|
1365
1367
|
```
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
const circ = 2 * Math.PI * r;
|
|
27
27
|
const dash = (pct * circ).toFixed(1);
|
|
28
28
|
const gap = (circ - pct * circ).toFixed(1);
|
|
29
|
-
|
|
29
|
+
// The arc is ink, not the accent: the accent lights what is pressable or
|
|
30
|
+
// live, and a progress arc is neither (task 1003470; the mock's .ring__arc).
|
|
31
|
+
const stroke = achieved ? 'var(--ok)' : 'var(--ink)';
|
|
30
32
|
return `<svg class="goal-ring" width="${size}" height="${size}" viewBox="0 0 42 42" role="img" aria-label="${satisfied} of ${total} criteria satisfied">
|
|
31
33
|
<circle cx="21" cy="21" r="${r}" fill="none" stroke="var(--rule-soft)" stroke-width="4"></circle>
|
|
32
34
|
<circle cx="21" cy="21" r="${r}" fill="none" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-dasharray="${dash} ${gap}" transform="rotate(-90 21 21)"></circle>
|
|
@@ -60,15 +62,17 @@
|
|
|
60
62
|
const EMPTY_ROLLUP = { done: 0, active: 0, ready: 0, blocked: 0, backlog: 0, cut: 0, total: 0, nextReady: null };
|
|
61
63
|
|
|
62
64
|
// task 1002439 — a goal row's readiness rollup line (only the non-zero buckets).
|
|
65
|
+
// The dot colours are classes in goals.css (.gr-mark--*), shared with the
|
|
66
|
+
// readiness band above the list, so the two cannot drift (task 1003470).
|
|
63
67
|
const ROLL_DOTS = [
|
|
64
|
-
['ready', 'ready'
|
|
65
|
-
['active', 'in progress'
|
|
66
|
-
['blocked', 'blocked'
|
|
67
|
-
['backlog', 'backlog'
|
|
68
|
+
['ready', 'ready'],
|
|
69
|
+
['active', 'in progress'],
|
|
70
|
+
['blocked', 'blocked'],
|
|
71
|
+
['backlog', 'backlog'],
|
|
68
72
|
];
|
|
69
73
|
function goalRollLine(r) {
|
|
70
74
|
const parts = ROLL_DOTS.filter(([k]) => r[k] > 0)
|
|
71
|
-
.map(([k, label
|
|
75
|
+
.map(([k, label]) => `<span class="gr-d"><i class="gr-mark gr-mark--${k}" aria-hidden="true"></i><b>${r[k]}</b> ${label}</span>`);
|
|
72
76
|
return parts.length ? `<div class="gr-roll">${parts.join('')}</div>` : '';
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -856,8 +856,9 @@
|
|
|
856
856
|
// Kit shimmer placeholders while the goal list loads (was one "…" div).
|
|
857
857
|
const grid = $('#goal-grid');
|
|
858
858
|
if (grid) {
|
|
859
|
-
|
|
860
|
-
|
|
859
|
+
// task 1003470 — row-shaped, since the list opens on rows.
|
|
860
|
+
grid.innerHTML = `<div class="gr-list">${Array.from({ length: 6 }, () =>
|
|
861
|
+
'<div class="gr-row gr-row--skel" aria-hidden="true"><span class="gr-skel__ring"></span><div><span class="kit-skel"></span><span class="kit-skel"></span></div><span></span></div>').join('')}</div>`;
|
|
861
862
|
}
|
|
862
863
|
try {
|
|
863
864
|
state.me = await getJSON(`${API}/me`).catch(() => null);
|
|
@@ -927,12 +928,23 @@
|
|
|
927
928
|
// options are filled after boot's fetch (populateOwnerFilter), the
|
|
928
929
|
// populateVersionFilter pattern. No URL riding (url defaults off here and this
|
|
929
930
|
// page is deliberately url:false — it owns the #/goal/N hash route).
|
|
931
|
+
// task 1003470 — three lenses stay in the open: the version, the state and
|
|
932
|
+
// the order. The other five are real but rarely turned, and twelve controls in
|
|
933
|
+
// two rows were the loudest thing on the page; they sit behind the kit's
|
|
934
|
+
// Filters disclosure, which counts and chips whatever is active so a hidden
|
|
935
|
+
// filter can never narrow the board silently. Control ids are unchanged.
|
|
930
936
|
makeFilterBar({
|
|
931
937
|
mount: $('#goals-filters'),
|
|
932
938
|
search: { id: 'f-goals-search', label: 'Search', placeholder: 'goal # or name…' },
|
|
933
939
|
primary: [
|
|
934
940
|
{ id: 'f-goal-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
935
941
|
{ id: 'f-goal-status', label: 'Status', options: [{ value: '', label: '— any —' }, 'open', 'achieved', 'archived'] },
|
|
942
|
+
{ id: 'f-goal-order',
|
|
943
|
+
label: 'Sort',
|
|
944
|
+
value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
|
|
945
|
+
options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
|
|
946
|
+
],
|
|
947
|
+
overflow: [
|
|
936
948
|
{ id: 'f-goal-mine', label: 'Membership', options: [{ value: '', label: 'All goals' }, { value: 'mine', label: 'My goals' }] },
|
|
937
949
|
{ id: 'f-goal-owner', label: 'Owner', options: [{ value: '', label: '— any —' }] },
|
|
938
950
|
// task 1003276 — filled from GET /work-categories after boot
|
|
@@ -946,19 +958,22 @@
|
|
|
946
958
|
{ id: 'f-goal-role', label: 'Craft', options: [ // ADR 0174: "role" means a craft
|
|
947
959
|
{ value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui'] },
|
|
948
960
|
{ id: 'f-goal-stale', label: 'Age', options: window.OTBTaskLib.STALE_CHOICES },
|
|
949
|
-
{ id: 'f-goal-order',
|
|
950
|
-
label: 'Sort',
|
|
951
|
-
value: GOAL_SORTS[savedGoalSort] ? savedGoalSort : undefined,
|
|
952
|
-
options: Object.entries(GOAL_SORTS).map(([value, { label }]) => ({ value, label })) },
|
|
953
|
-
// task 1003136 — list|cards toggle; renderList forks its group renderer
|
|
954
|
-
// on this. Persisted via GOAL_VIEW_PREF, never the URL.
|
|
955
|
-
{ id: 'f-goal-view',
|
|
956
|
-
label: 'View',
|
|
957
|
-
value: savedGoalView === 'cards' ? 'cards' : undefined,
|
|
958
|
-
options: [{ value: 'list', label: 'list' }, { value: 'cards', label: 'cards' }] },
|
|
959
961
|
],
|
|
960
962
|
onChange: onListFilterChange,
|
|
961
963
|
});
|
|
964
|
+
// task 1003470 — rows|cards is a VIEW, not a filter: it left the filter bar
|
|
965
|
+
// for the archetype's segmented toggle on the count line. renderList still
|
|
966
|
+
// reads #f-goal-view (task 1003136's fork), now a hidden input the two buttons
|
|
967
|
+
// write, so the fork and the otb.goals.view pref are unchanged.
|
|
968
|
+
const viewInput = $('#f-goal-view');
|
|
969
|
+
const viewBtns = Array.from(document.querySelectorAll('#goals-view .viewtoggle__btn'));
|
|
970
|
+
function setGoalView(view, opts) {
|
|
971
|
+
if (viewInput) viewInput.value = view;
|
|
972
|
+
viewBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.view === view)));
|
|
973
|
+
if (!opts || opts.fire !== false) onListFilterChange();
|
|
974
|
+
}
|
|
975
|
+
viewBtns.forEach((b) => b.addEventListener('click', () => setGoalView(b.dataset.view)));
|
|
976
|
+
if (savedGoalView === 'cards') setGoalView('cards', { fire: false });
|
|
962
977
|
// task 2171 — stamp + wire the kit pager bars (grid + both Work tables); the
|
|
963
978
|
// bar ids and localStorage prefs are exactly what the bespoke pagers used.
|
|
964
979
|
const pagerMounts = [
|
|
@@ -127,6 +127,15 @@
|
|
|
127
127
|
const { label, ms } = stamp(g);
|
|
128
128
|
return ` · ${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}`;
|
|
129
129
|
}
|
|
130
|
+
// The check drawn, not typed: the world's icons are strokes, never glyphs.
|
|
131
|
+
const CHECK_PATH = '<path d="M3.4 8.4 6.6 11.5 12.6 4.9"/>';
|
|
132
|
+
const checkSvg = (cls) => `<svg class="${cls}" viewBox="0 0 16 16" aria-hidden="true">${CHECK_PATH}</svg>`;
|
|
133
|
+
// task 1003470 — a goal's facts, computed ONCE for the row and the card so the
|
|
134
|
+
// two views cannot drift (pinned by tests/goals_card_view.mjs). Three things a
|
|
135
|
+
// row says, each in one language: the ring (the goal's own done-when,
|
|
136
|
+
// satisfied over total), the facts line (state pill, scope + version chips,
|
|
137
|
+
// the category), and the right column — ONE next action, or a one-line state.
|
|
138
|
+
// The task readiness counts live on the roll line and nowhere else.
|
|
130
139
|
function goalRenderFacts(g, ringSize) {
|
|
131
140
|
const d = state.details.get(String(g.id));
|
|
132
141
|
const crit = d ? (d.criteria || []) : [];
|
|
@@ -134,90 +143,83 @@
|
|
|
134
143
|
const r = rollupOf(g.id);
|
|
135
144
|
const achieved = g.status === 'achieved';
|
|
136
145
|
const ringHtml = crit.length ? ring(sat, crit.length, achieved, ringSize)
|
|
137
|
-
: `<span class="gr-crit-none" aria-
|
|
146
|
+
: `<span class="gr-crit-none" role="img" aria-label="No done-when criteria yet" title="No done-when criteria yet"></span>`;
|
|
138
147
|
const goalHref = `#/goal/${escapeHtml(String(g.id))}`;
|
|
139
148
|
let right;
|
|
140
149
|
if (achieved) {
|
|
141
|
-
right = `<span class="gr-
|
|
150
|
+
right = `<span class="gr-state gr-state--ok">${checkSvg('gr-state__mark')}achieved</span>`;
|
|
142
151
|
} else if (crit.length && sat >= crit.length) {
|
|
143
|
-
right = `<span class="gr-
|
|
152
|
+
right = `<span class="gr-state gr-state--ok">${checkSvg('gr-state__mark')}criteria met · awaiting review</span>`;
|
|
144
153
|
} else if (r.nextReady) {
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
// The one place the accent lands on the list: the row's live door.
|
|
155
|
+
right = `<div class="gr-next"><span class="gr-next__t" title="${escapeHtml(r.nextReady.title || '')}">${escapeHtml(r.nextReady.title || 'Next task')}</span>`
|
|
156
|
+
+ `<a class="btn-accent gr-claim" href="#/task/${escapeHtml(String(r.nextReady.id))}">claim next</a></div>`;
|
|
147
157
|
} else if (r.blocked > 0) {
|
|
148
|
-
right = `<a class="gr-drill" href="${goalHref}">
|
|
158
|
+
right = `<a class="btn-ghost gr-drill" href="${goalHref}">what's blocking</a>`;
|
|
149
159
|
} else {
|
|
150
|
-
right = `<span class="gr-quiet">
|
|
160
|
+
right = `<span class="gr-quiet">no tasks yet</span>`;
|
|
151
161
|
}
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
// one chip per scope module and one for the version. The mock argues rows
|
|
155
|
-
// over cards precisely because a goal carries this many facts; the row was
|
|
156
|
-
// showing two of them as one run of grey text.
|
|
162
|
+
// The sort's date, only while a date sort is on (task 1002965) — it rides
|
|
163
|
+
// under the right column's state, not in a meta run under the title.
|
|
157
164
|
const stampHtml = goalStampHtml(g);
|
|
165
|
+
const whenHtml = stampHtml ? `<span class="gr-when">${stampHtml.replace(/^ · /, '')}</span>` : '';
|
|
158
166
|
const idHtml = `<span class="gr-title__id">#${escapeHtml(String(g.id))}</span>`;
|
|
159
167
|
const chips = [];
|
|
160
168
|
for (const m of (Array.isArray(g.scope_modules) ? g.scope_modules : []).slice(0, 3)) {
|
|
161
169
|
chips.push(`<span class="fact-chip">${escapeHtml(String(m))}</span>`);
|
|
162
170
|
}
|
|
163
171
|
if (g.version_id) chips.push(`<span class="fact-chip">${escapeHtml(g.version_id)}</span>`);
|
|
172
|
+
// task 1003276 — the category chip is a fact like the others, so it sits in
|
|
173
|
+
// the facts line (a label a person scans, never a door — ADR 0192).
|
|
174
|
+
const catHtml = categoryChip(g.category);
|
|
164
175
|
const factsHtml = `<div class="gr-facts">`
|
|
165
176
|
+ `<span class="fact-pill fact-pill--${escapeHtml(g.status || 'open')}">${escapeHtml(g.status || 'open')}</span>`
|
|
166
177
|
+ chips.join('')
|
|
178
|
+
+ catHtml
|
|
167
179
|
+ `</div>`;
|
|
168
|
-
|
|
169
|
-
// so the row and the card (task 1003136) cannot drift on it.
|
|
170
|
-
const catHtml = categoryChip(g.category);
|
|
171
|
-
return { r, achieved, ringHtml, goalHref, right, metaHtml, catHtml, idHtml, stampHtml, factsHtml };
|
|
180
|
+
return { r, achieved, ringHtml, goalHref, right, catHtml, idHtml, stampHtml, whenHtml, factsHtml };
|
|
172
181
|
}
|
|
173
182
|
function goalRowHtml(g) {
|
|
174
|
-
const { r, achieved, ringHtml, goalHref, right,
|
|
175
|
-
// NOTE: a rollup row nests real links (title → goal,
|
|
183
|
+
const { r, achieved, ringHtml, goalHref, right, idHtml, whenHtml, factsHtml } = goalRenderFacts(g, 44);
|
|
184
|
+
// NOTE: a rollup row nests real links (title → goal, claim → the task page,
|
|
176
185
|
// drill → goal), so the row itself is a <div>, not an <a> — nested anchors
|
|
177
186
|
// are invalid. Only the explicit links navigate.
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
// rather than a grey run-on. The sort stamp is all that is left of .gr-meta,
|
|
182
|
-
// and it renders only while a date sort is active.
|
|
187
|
+
// The archetype's row (mock .goal): the id rides INSIDE the title, mono and
|
|
188
|
+
// quiet; the subtitle is a real line; the facts are pills and chips; the
|
|
189
|
+
// right column is one action or one state, with the sort date under it.
|
|
183
190
|
return `<div class="gr-row" data-goal-id="${escapeHtml(String(g.id))}">
|
|
184
191
|
<div class="gr-ringcol">${ringHtml}</div>
|
|
185
192
|
<div class="gr-main">
|
|
186
193
|
<a class="gr-title" href="${goalHref}">${idHtml}${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
187
194
|
${subtitleLine(g)}
|
|
188
195
|
${factsHtml}
|
|
189
|
-
${stampHtml ? `<div class="gr-meta">${stampHtml.replace(/^ · /, '')}${catHtml}</div>` : (catHtml ? `<div class="gr-meta">${catHtml}</div>` : '')}
|
|
190
196
|
${achieved ? '' : goalRollLine(r)}
|
|
191
197
|
</div>
|
|
192
|
-
<div class="gr-right">${right}</div>
|
|
198
|
+
<div class="gr-right">${right}${whenHtml}</div>
|
|
193
199
|
</div>`;
|
|
194
200
|
}
|
|
201
|
+
// The card is the row folded (goals.css says why it draws no box). It keeps
|
|
202
|
+
// the subtitle as a tooltip — task 1003224's shape — because a 280px column
|
|
203
|
+
// has no line to give it.
|
|
195
204
|
function goalCardHtml(g) {
|
|
196
|
-
const { r, achieved, ringHtml, goalHref, right,
|
|
205
|
+
const { r, achieved, ringHtml, goalHref, right, idHtml, whenHtml, factsHtml } = goalRenderFacts(g, 36);
|
|
197
206
|
return `<div class="goal-card" data-goal-id="${escapeHtml(String(g.id))}">
|
|
198
207
|
<div class="goal-card__head">
|
|
199
208
|
<div class="gr-ringcol">${ringHtml}</div>
|
|
200
|
-
<a class="goal-card__title gr-title" href="${goalHref}"${subtitleAttr(g)}>${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
209
|
+
<a class="goal-card__title gr-title" href="${goalHref}"${subtitleAttr(g)}>${idHtml}${escapeHtml(g.title || 'Untitled goal')}</a>
|
|
201
210
|
</div>
|
|
202
|
-
|
|
211
|
+
${factsHtml}
|
|
203
212
|
${achieved ? '' : goalRollLine(r)}
|
|
204
|
-
<div class="goal-card__foot">${right}</div>
|
|
213
|
+
<div class="goal-card__foot">${right}${whenHtml}</div>
|
|
205
214
|
</div>`;
|
|
206
215
|
}
|
|
207
216
|
function renderList() {
|
|
208
217
|
const grid = $('#goal-grid');
|
|
209
218
|
if (!grid) return;
|
|
210
219
|
const rows = filteredGoals();
|
|
211
|
-
const count = $('#goals-count');
|
|
212
|
-
if (count) count.textContent = `${rows.length} goal${rows.length === 1 ? '' : 's'}`;
|
|
213
220
|
const pagerMount = $('#goals-pager-mount');
|
|
214
221
|
if (pagerMount) pagerMount.hidden = true;
|
|
215
222
|
|
|
216
|
-
if (!rows.length) {
|
|
217
|
-
grid.innerHTML = emptyStateHtml('No goals match these filters.');
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
223
|
const isBlocked = (g) => {
|
|
222
224
|
const r = rollupOf(g.id);
|
|
223
225
|
return g.status === 'open' && r.ready === 0 && r.active === 0 && r.blocked > 0;
|
|
@@ -231,26 +233,55 @@
|
|
|
231
233
|
const blocked = sortGoals(openRows.filter(isBlocked));
|
|
232
234
|
const active = sortGoals(openRows.filter((g) => !isBlocked(g)));
|
|
233
235
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
236
|
+
// task 1003470 — the count line reads the groups (the mock's count-line).
|
|
237
|
+
const count = $('#goals-count');
|
|
238
|
+
if (count) {
|
|
239
|
+
const parts = [`${rows.length} goal${rows.length === 1 ? '' : 's'}`];
|
|
240
|
+
if (rows.length) parts.push(`${openRows.length} open`, `${achieved.length} achieved`, `${archived.length} archived`);
|
|
241
|
+
count.textContent = parts.join(' · ');
|
|
242
|
+
}
|
|
243
|
+
if (!rows.length) {
|
|
244
|
+
grid.innerHTML = emptyStateHtml('No goals match these filters.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The readiness band: the four figures the mock puts on the panel, in the
|
|
249
|
+
// family's figure band, over the OPEN goals in view. The dots are the roll
|
|
250
|
+
// line's own, so the band and the rows read as one language.
|
|
251
|
+
let R = 0, A = 0, B = 0, sat = 0, crit = 0;
|
|
252
|
+
openRows.forEach((g) => {
|
|
253
|
+
const r = rollupOf(g.id); R += r.ready; A += r.active; B += r.blocked;
|
|
254
|
+
const d = state.details.get(String(g.id));
|
|
255
|
+
const c = d ? (d.criteria || []) : [];
|
|
256
|
+
crit += c.length; sat += c.filter((x) => x.satisfied).length;
|
|
257
|
+
});
|
|
258
|
+
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
|
|
259
|
+
const fig = (label, value, foot, mark) => `<div class="ov-fig">`
|
|
260
|
+
+ `<span class="ov-fig__l">${mark ? `<i class="gr-mark gr-mark--${mark}" aria-hidden="true"></i>` : ''}${label}</span>`
|
|
261
|
+
+ `<span class="ov-fig__v">${value}</span>`
|
|
262
|
+
+ (foot ? `<span class="ov-fig__s">${foot}</span>` : '')
|
|
263
|
+
+ `</div>`;
|
|
264
|
+
const summary = `<div class="ov-figs gr-figs">`
|
|
265
|
+
+ fig('Ready to claim', R, `across ${plural(active.length, 'active goal')}`, 'ready')
|
|
266
|
+
+ fig('In progress', A, 'claimed right now', 'active')
|
|
267
|
+
+ fig('Blocked', B, 'waiting on upstream tasks', 'blocked')
|
|
268
|
+
+ fig('Criteria met', `${sat} / ${crit}`, `on ${plural(openRows.length, 'open goal')}`)
|
|
269
|
+
+ `</div>`;
|
|
241
270
|
|
|
242
|
-
|
|
243
|
-
//
|
|
271
|
+
// The groups: the family head (name in the label voice, count in mono, one
|
|
272
|
+
// rule under) over the rows, the cards, or the pills.
|
|
273
|
+
const group = (label, arr, body) => `<div class="ov-group"><div class="ov-group__head"><span class="ov-group__name">${label}</span><span class="ov-group__n">${arr.length}</span></div>${body}</div>`;
|
|
274
|
+
// task 1003136 — the rows|cards toggle swaps how the open groups render;
|
|
244
275
|
// grouping, filters, and sort run above the fork so both views share them.
|
|
245
276
|
// Achieved/archived pills stay pills (already the compact form). The inner
|
|
246
277
|
// div is class-only .goal-grid on purpose: task 1002439's block override
|
|
247
|
-
// targets #goal-grid.goal-grid, so the
|
|
278
|
+
// targets #goal-grid.goal-grid, so the card-grid CSS applies here.
|
|
248
279
|
const cardsView = ($('#f-goal-view')?.value || 'list') === 'cards';
|
|
249
280
|
const listOf = cardsView
|
|
250
281
|
? (arr) => `<div class="goal-grid">${arr.map(goalCardHtml).join('')}</div>`
|
|
251
282
|
: (arr) => `<div class="gr-list">${arr.map(goalRowHtml).join('')}</div>`;
|
|
252
283
|
// task 1002965 — an achieved/archived goal is a compact pill, so its sort
|
|
253
|
-
// date rides as its own muted span (the row's
|
|
284
|
+
// date rides as its own muted span (the row's gr-when equivalent). The
|
|
254
285
|
// pills row already wraps, so the extra text can't widen the page.
|
|
255
286
|
const pillWhen = (g) => {
|
|
256
287
|
const { stamp } = currentGoalSort();
|
|
@@ -258,13 +289,13 @@
|
|
|
258
289
|
const { label, ms } = stamp(g);
|
|
259
290
|
return `<span class="gr-pill__when">${escapeHtml(label)} ${escapeHtml(ms ? fmtDate(new Date(ms).toISOString()) : '—')}</span>`;
|
|
260
291
|
};
|
|
261
|
-
const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}"
|
|
292
|
+
const pill = (g) => `<a class="gr-pill" href="#/goal/${escapeHtml(String(g.id))}">${checkSvg('gr-pill__mark')}<b>${escapeHtml(g.title || 'Untitled')}</b><span class="gr-pill__id">#${escapeHtml(String(g.id))}</span>${pillWhen(g)}</a>`;
|
|
262
293
|
|
|
263
294
|
grid.innerHTML = summary
|
|
264
|
-
+ (active.length ?
|
|
265
|
-
+ (blocked.length ?
|
|
266
|
-
+ (achieved.length ?
|
|
267
|
-
+ (archived.length ?
|
|
295
|
+
+ (active.length ? group('Active', active, listOf(active)) : '')
|
|
296
|
+
+ (blocked.length ? group('Blocked', blocked, listOf(blocked)) : '')
|
|
297
|
+
+ (achieved.length ? group('Achieved', achieved, `<div class="gr-pills">${achieved.map(pill).join('')}</div>`) : '')
|
|
298
|
+
+ (archived.length ? group('Archived', archived, `<div class="gr-pills gr-pills--muted">${archived.map(pill).join('')}</div>`) : '');
|
|
268
299
|
}
|
|
269
300
|
function todoSearchQuery() {
|
|
270
301
|
const el = $('#f-goal-search');
|