@bongos/core 1.19.488 → 1.19.489
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 +27 -17
- package/docs/copy-inventory.md +43 -33
- package/docs/copy-registry.json +167 -57
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/profile-charts.js +461 -0
- package/modules/hall-ui/public/profile.css +90 -1
- package/modules/hall-ui/public/profile.html +146 -97
- package/modules/hall-ui/public/profile.js +164 -31
- package/modules/hall-ui/public/profile.states.json +109 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_profile_world.mjs +342 -0
|
@@ -35,103 +35,147 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
37
|
<main id="hall-main">
|
|
38
|
-
<!--
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
<!-- Three screens behind a section rail (task 1003489): the settings
|
|
39
|
+
idiom, OTBKit.makeSectionRail in switch mode. profile.js wires the
|
|
40
|
+
rail and the #key deep links; profile.css shows one group at a
|
|
41
|
+
time (.profile-groups[data-active]) and a section's own [hidden]
|
|
42
|
+
still wins, so the hub-only reveals below are untouched. -->
|
|
43
|
+
<div class="profile-layout" id="profile-layout">
|
|
44
|
+
<nav class="section-rail" id="profile-rail" aria-label="Profile sections"></nav>
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
46
|
+
<div class="profile-groups" id="profile-groups" data-active="overview">
|
|
47
|
+
|
|
48
|
+
<!-- ============ overview: the portfolio, one screen ============ -->
|
|
49
|
+
|
|
50
|
+
<!-- Standing — avatar, name, rank badge, member-since. Filled by profile.js. -->
|
|
51
|
+
<section class="scroll" id="standing-scroll" aria-labelledby="standing-h" data-group="overview">
|
|
52
|
+
<h2 id="standing-h" class="sr-only">Standing</h2>
|
|
53
|
+
<div class="profile profile--ident" id="profile-head">
|
|
54
|
+
<div class="profile__placeholder">…</div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<!-- By the numbers — credits, karma, works shipped. -->
|
|
59
|
+
<section class="scroll" id="numbers-scroll" aria-labelledby="numbers-h" data-group="overview">
|
|
60
|
+
<h2 id="numbers-h" class="scroll__h">By the numbers</h2>
|
|
61
|
+
<div class="stat-row" id="numbers-body">
|
|
62
|
+
<div class="stat-tile"><span class="stat-tile__label">Loading…</span></div>
|
|
63
|
+
</div>
|
|
64
|
+
</section>
|
|
65
|
+
|
|
66
|
+
<!-- Over time — the figures above, seen as a record (task 1003489).
|
|
67
|
+
profile-charts.js fills the grid: works shipped and drachmae
|
|
68
|
+
earned as cumulative step-lines, the craft mix as bars, and on
|
|
69
|
+
the viewer's own profile their pace by week. Every chart has a
|
|
70
|
+
table twin; signed out, the grid is one line saying why. -->
|
|
71
|
+
<section class="scroll" id="charts-scroll" aria-labelledby="charts-h" data-group="overview">
|
|
72
|
+
<h2 id="charts-h" class="scroll__h">Over time</h2>
|
|
73
|
+
<!-- The lookback control (task 1003489, owner request) — one filter
|
|
74
|
+
row above the grid, scoping every chart (dataviz "date range
|
|
75
|
+
first"). profile-charts.js fills it and re-draws on change. -->
|
|
76
|
+
<div id="charts-controls"></div>
|
|
77
|
+
<div class="pcharts" id="charts-grid">
|
|
78
|
+
<p class="pchart__note">…</p>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<!-- Achievements — the builder's earned shelf. -->
|
|
83
|
+
<section class="scroll scroll--bare" id="ach-scroll" aria-labelledby="ach-h" data-group="overview">
|
|
84
|
+
<h2 id="ach-h" class="scroll__h">Achievements</h2>
|
|
85
|
+
<div class="ach-grid" id="ach-grid">
|
|
86
|
+
<div class="profile__placeholder">…</div>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<!-- Cross-project record — hub-only (ADR 0171). profile.js reveals this
|
|
91
|
+
only when the profile response carries a cross_project key; on a
|
|
92
|
+
vanilla instance the key is absent and the section never renders. -->
|
|
93
|
+
<section class="scroll" id="xp-scroll" aria-labelledby="xp-h" data-group="overview" hidden>
|
|
94
|
+
<h2 id="xp-h" class="scroll__h">Cross-project record</h2>
|
|
95
|
+
<div id="xp-body"></div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<!-- ============ works: the full shipped-works ledger ============ -->
|
|
99
|
+
|
|
100
|
+
<!-- Works — the full, server-paged ledger of everything this builder
|
|
101
|
+
shipped (the owner's confirmed key; the old Overview "recent
|
|
102
|
+
works" strip folded in here). A signed-in viewer gets the paged
|
|
103
|
+
record; signed out, the page's public recent_ships stand in with
|
|
104
|
+
a sign-in line. Activity (next key) is the SAME works seen one
|
|
105
|
+
craft at a time, with goals and the idea trail. -->
|
|
106
|
+
<section class="scroll" id="works-scroll" aria-labelledby="works-h" data-group="works">
|
|
107
|
+
<h2 id="works-h" class="scroll__h">Works</h2>
|
|
108
|
+
<p class="scroll__lede" id="works-lede">Everything this builder has shipped. Seen one craft at a time, with goals and ideas, under <a href="#activity">activity</a>.</p>
|
|
109
|
+
<table class="ledger" id="works-table">
|
|
110
|
+
<thead>
|
|
111
|
+
<tr>
|
|
112
|
+
<th class="ledger__num">#</th>
|
|
113
|
+
<th>Work</th>
|
|
114
|
+
<th class="ledger__drachmae">Reward</th>
|
|
115
|
+
<th class="ledger__date">Shipped</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>
|
|
119
|
+
<tr><td colspan="4">…</td></tr>
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
<div id="works-pager"></div>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<!-- ============ activity: the record, by craft ============ -->
|
|
126
|
+
|
|
127
|
+
<!-- Activity, by craft — task 1003281, goal 1000072 / criterion
|
|
128
|
+
legibility-activity-by-role. A view per role over the same record:
|
|
129
|
+
the tasks this builder completed, the goals they created, and (in
|
|
130
|
+
the ideator view) every idea they filed with what became of it.
|
|
131
|
+
All three ledgers page server-side; profile.js fills them. -->
|
|
132
|
+
<section class="scroll" id="activity-scroll" aria-labelledby="activity-h" data-group="activity">
|
|
133
|
+
<h2 id="activity-h" class="scroll__h">Activity, by craft</h2>
|
|
134
|
+
<p class="scroll__lede" id="activity-lede">What this builder has done, seen one craft at a time.</p>
|
|
135
|
+
<div id="activity-tabs"></div>
|
|
136
|
+
<div id="activity-body">
|
|
137
|
+
<div class="profile__placeholder" id="activity-placeholder">…</div>
|
|
138
|
+
|
|
139
|
+
<div id="activity-tasks-panel" hidden>
|
|
140
|
+
<h3 class="profile-act__h">Works completed</h3>
|
|
141
|
+
<div id="activity-tasks"></div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div id="activity-goals-panel" hidden>
|
|
145
|
+
<h3 class="profile-act__h">Goals created</h3>
|
|
146
|
+
<div id="activity-goals"></div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- The idea trail. Revealed only in the craft views where filing an
|
|
150
|
+
idea is the work being shown (all + ideator) — see profile.js. -->
|
|
151
|
+
<div id="activity-ideas-panel" hidden>
|
|
152
|
+
<h3 class="profile-act__h">Ideas filed, and what became of them</h3>
|
|
153
|
+
<p class="scroll__lede">Every idea this builder captured — promoted, discarded, merged, or still open.</p>
|
|
154
|
+
<div id="activity-ideas"></div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
|
|
159
|
+
<!-- ============ connections: hub-only ============ -->
|
|
160
|
+
|
|
161
|
+
<!-- Connections — section 6 of the profile-page spec (R20, task
|
|
162
|
+
1002296; ADR 0202). Hub-only, like the cross-project record:
|
|
163
|
+
profile-connections.js reveals it only when the hub's connections
|
|
164
|
+
API answers, so on an instance without platform-identity the
|
|
165
|
+
routes are absent and the section never renders — and its rail
|
|
166
|
+
key with it (profile.js watches this section's hidden attribute). -->
|
|
167
|
+
<section class="scroll" id="conn-scroll" aria-labelledby="conn-h" data-group="connections" hidden>
|
|
168
|
+
<h2 id="conn-h" class="scroll__h">Connections</h2>
|
|
169
|
+
<div id="conn-body"></div>
|
|
170
|
+
</section>
|
|
171
|
+
|
|
172
|
+
<!-- Reserved slots — connections, guilds, and actions (profile-page
|
|
173
|
+
spec sections 6–8) mount here in later phases; keeping the order
|
|
174
|
+
fixed now means the page is not re-laid-out twice (Decision 5).
|
|
175
|
+
Section 6 landed above (task 1002296) as the connections key;
|
|
176
|
+
7 and 8 are still to come, each as its own rail key. -->
|
|
118
177
|
</div>
|
|
119
|
-
</
|
|
120
|
-
|
|
121
|
-
<!-- Connections — section 6 of the profile-page spec (R20, task
|
|
122
|
-
1002296; ADR 0202). Hub-only, like the cross-project record:
|
|
123
|
-
profile-connections.js reveals it only when the hub's connections
|
|
124
|
-
API answers, so on an instance without platform-identity the
|
|
125
|
-
routes are absent and the section never renders. -->
|
|
126
|
-
<section class="scroll" id="conn-scroll" aria-labelledby="conn-h" hidden>
|
|
127
|
-
<h2 id="conn-h" class="scroll__h">Connections</h2>
|
|
128
|
-
<div id="conn-body"></div>
|
|
129
|
-
</section>
|
|
130
|
-
|
|
131
|
-
<!-- Reserved slots — connections, guilds, and actions (profile-page
|
|
132
|
-
spec sections 6–8) mount here in later phases; keeping the order
|
|
133
|
-
fixed now means the page is not re-laid-out twice (Decision 5).
|
|
134
|
-
Section 6 landed above (task 1002296); 7 and 8 are still to come. -->
|
|
178
|
+
</div>
|
|
135
179
|
</main>
|
|
136
180
|
|
|
137
181
|
<footer class="app-foot" id="app-foot"></footer>
|
|
@@ -150,7 +194,8 @@
|
|
|
150
194
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
151
195
|
<script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
|
|
152
196
|
<!-- hall-kit (window.OTBKit) — profile.js renders ship-time task visuals through
|
|
153
|
-
its shared thumbnail helper (task 1003109)
|
|
197
|
+
its shared thumbnail helper (task 1003109) and builds the section rail with
|
|
198
|
+
it (task 1003489), so it must load first. -->
|
|
154
199
|
<script src="/builders/hall-kit.js?v=2026-08-27-activity"></script>
|
|
155
200
|
<!-- Connections (section 6). Loaded BEFORE profile.js so window.OTBConnections
|
|
156
201
|
exists by the time renderProfile runs; profile.js guards on it anyway, so a
|
|
@@ -160,6 +205,10 @@
|
|
|
160
205
|
window.OTBCanonical exists when renderProfile runs; profile.js guards on it,
|
|
161
206
|
so a page that omits this script simply declares no canonical. -->
|
|
162
207
|
<script src="/builders/profile-canonical.js?v=2026-08-29-p08"></script>
|
|
163
|
-
|
|
208
|
+
<!-- The charts (task 1003489). Loaded BEFORE profile.js so window.OTBProfileCharts
|
|
209
|
+
exists when renderProfile runs; profile.js guards on it, so a page that
|
|
210
|
+
omits this script simply has no charts section content. -->
|
|
211
|
+
<script src="/builders/profile-charts.js?v=2026-09-02-charts"></script>
|
|
212
|
+
<script src="/builders/profile.js?v=2026-09-02-rail"></script>
|
|
164
213
|
</body>
|
|
165
214
|
</html>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// modules/hall-ui/public/profile.js — the viewable builder profile page
|
|
2
2
|
// (/builders/profile, task 1467, goal 25 "bx-identity").
|
|
3
3
|
//
|
|
4
|
+
// Since task 1003489 the page is three screens behind a section rail (the
|
|
5
|
+
// settings idiom, OTBKit.makeSectionRail in switch mode): overview — identity,
|
|
6
|
+
// figures, the charts (profile-charts.js), shelf, latest works, cross-project;
|
|
7
|
+
// activity — the craft ledgers and the idea trail; connections — hub-only,
|
|
8
|
+
// its rail key following the section's own reveal. Deep links are the key
|
|
9
|
+
// (/profile?id=95#activity); the "#/…" overlay namespace passes through.
|
|
10
|
+
//
|
|
4
11
|
// A PUBLIC, shareable portfolio for one builder: rank, works shipped, standing
|
|
5
12
|
// (drachmae + karma), achievements, and recent works. Reads the public route
|
|
6
13
|
// GET /api/bongos/builders/:id/profile — every field it shows is already
|
|
@@ -125,45 +132,93 @@
|
|
|
125
132
|
}).join('');
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
// ---- render:
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
// ---- render: the Works ledger -------------------------------------------
|
|
136
|
+
//
|
|
137
|
+
// The owner's confirmed composition (task 1003489) made Works its own rail
|
|
138
|
+
// key: the full, server-paged ledger of everything this builder shipped, with
|
|
139
|
+
// the old Overview "recent works" strip folded in. A signed-in viewer pages
|
|
140
|
+
// the whole record from the activity route (any viewer, any builder); signed
|
|
141
|
+
// out, the page's PUBLIC recent_ships stand in with a sign-in line, so a
|
|
142
|
+
// stranger still sees the last few. Activity (the next key) is the SAME works
|
|
143
|
+
// seen one craft at a time, with goals and the idea trail — a different
|
|
144
|
+
// question, which is why it is a second key and not a duplicate.
|
|
145
|
+
const worksReward = (s) => window.OTBBranding?.taskCreditValue?.(s) ?? s.credits_reward;
|
|
146
|
+
function worksRowHtml(s) {
|
|
147
|
+
const summary = s.value_summary
|
|
148
|
+
? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(s.value_summary)}</span>`
|
|
149
|
+
: '';
|
|
150
|
+
const visual = window.OTBKit && window.OTBKit.shipVisualThumbHtml ? window.OTBKit.shipVisualThumbHtml(s) : '';
|
|
151
|
+
return `<tr>
|
|
152
|
+
<td class="ledger__num">#${escapeHtml(String(s.id))}</td>
|
|
153
|
+
<td><a href="${taskHref(s.id)}">${escapeHtml(s.title || `Task ${s.id}`)}</a>${summary}${visual ? `<span style="display:block;margin-top:6px">${visual}</span>` : ''}</td>
|
|
154
|
+
<td class="ledger__drachmae">${fmtNum(worksReward(s))}</td>
|
|
155
|
+
<td class="ledger__date">${fmtDate(s.shipped_at)}</td>
|
|
156
|
+
</tr>`;
|
|
157
|
+
}
|
|
158
|
+
let worksBuilt = false;
|
|
159
|
+
function buildWorksLedger(builderId, signedIn, recentShips) {
|
|
131
160
|
const tbody = $('#works-table tbody');
|
|
132
|
-
const lede = $('#works-lede');
|
|
133
161
|
if (!tbody) return;
|
|
134
|
-
//
|
|
135
|
-
// cost-plus
|
|
136
|
-
// label must keep its "Est." qualifier (ADR 0146).
|
|
162
|
+
// One header serves every row, but the figure stays an estimate on a
|
|
163
|
+
// cost-plus instance, so the label keeps its "Est." qualifier (ADR 0146).
|
|
137
164
|
const rewardTh = $('#works-table thead .ledger__drachmae');
|
|
138
165
|
if (rewardTh) {
|
|
139
166
|
rewardTh.textContent = window.OTBBranding?.taskCreditLabel?.({ status: 'shipped' }, { caps: true })
|
|
140
167
|
?? (curLabel(true) + ' Est.');
|
|
141
168
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
169
|
+
const empty = (msg) => { tbody.innerHTML = `<tr><td colspan="4" class="ledger__empty">${escapeHtml(msg)}</td></tr>`; };
|
|
170
|
+
const pagerMount = $('#works-pager');
|
|
171
|
+
|
|
172
|
+
// Signed out: no activity route, so the public recent_ships ARE the ledger,
|
|
173
|
+
// with a line pointing at the full record behind sign-in.
|
|
174
|
+
if (!signedIn) {
|
|
175
|
+
if (pagerMount) pagerMount.innerHTML = '';
|
|
176
|
+
const list = Array.isArray(recentShips) ? recentShips : [];
|
|
177
|
+
if (!list.length) { empty('Nothing shipped yet.'); return; }
|
|
178
|
+
tbody.innerHTML = list.map(worksRowHtml).join('')
|
|
179
|
+
+ `<tr><td colspan="4" class="ledger__empty"><a href="${API}/auth/web/start">Sign in</a> to see the full record.</td></tr>`;
|
|
146
180
|
return;
|
|
147
181
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
182
|
+
|
|
183
|
+
if (worksBuilt) return;
|
|
184
|
+
worksBuilt = true;
|
|
185
|
+
const kit = window.OTBKit;
|
|
186
|
+
// Without the kit's pager helpers a single unpaged page beats nothing (the
|
|
187
|
+
// page still works if hall-kit failed to load, or an older kit lacks the
|
|
188
|
+
// server-pager helpers), the same graceful-degrade the charts take.
|
|
189
|
+
if (!kit || typeof kit.pagerBarHtml !== 'function' || !pagerMount) {
|
|
190
|
+
(async () => {
|
|
191
|
+
try {
|
|
192
|
+
const res = await api.request('GET', `${API}/builders/${encodeURIComponent(builderId)}/activity?section=tasks&limit=20&offset=0`);
|
|
193
|
+
const rows = (res.ok && res.data.rows) || [];
|
|
194
|
+
if (!rows.length) { empty('Nothing shipped yet.'); return; }
|
|
195
|
+
tbody.innerHTML = rows.map(worksRowHtml).join('');
|
|
196
|
+
} catch (err) { console.error('[profile] works ledger failed', err); empty('Could not load the works.'); }
|
|
197
|
+
})();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
pagerMount.innerHTML = kit.pagerBarHtml('works', { label: 'works' });
|
|
201
|
+
const pager = kit.makePager('works', 'otb.profile.worksPerPage', { defaultPerPage: 10, minPerPage: 10 });
|
|
202
|
+
let inFlight = 0;
|
|
203
|
+
async function load() {
|
|
204
|
+
const seq = ++inFlight;
|
|
205
|
+
try {
|
|
206
|
+
const res = await api.request('GET', `${API}/builders/${encodeURIComponent(builderId)}/activity`
|
|
207
|
+
+ `?section=tasks&limit=${pager.perPage}&offset=${kit.pageOffset(pager)}`);
|
|
208
|
+
if (seq !== inFlight) return;
|
|
209
|
+
if (!res.ok) throw new Error(`works → ${res.status}`);
|
|
210
|
+
const rows = res.data.rows || [];
|
|
211
|
+
const total = Number(res.data.total) || 0;
|
|
212
|
+
if (kit.renderServerPage(pager, total, rows.length)) { load(); return; }
|
|
213
|
+
tbody.innerHTML = rows.length ? rows.map(worksRowHtml).join('') : `<tr><td colspan="4" class="ledger__empty">Nothing shipped yet.</td></tr>`;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (seq !== inFlight) return;
|
|
216
|
+
console.error('[profile] works ledger failed', err);
|
|
217
|
+
empty('Could not load the works.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
kit.wirePager(pager, load);
|
|
221
|
+
load();
|
|
167
222
|
}
|
|
168
223
|
|
|
169
224
|
// ---- render: cross-project record (hub-only, ADR 0171 / task 1002280) ----
|
|
@@ -514,6 +569,76 @@
|
|
|
514
569
|
activity.show(activity.active());
|
|
515
570
|
}
|
|
516
571
|
|
|
572
|
+
// ---- the section rail (task 1003489) ------------------------------------------
|
|
573
|
+
//
|
|
574
|
+
// Settings' idiom, borrowed whole (settings.js wireRail): one group of
|
|
575
|
+
// sections shows at a time behind a left rail in switch mode; the sheet does
|
|
576
|
+
// the showing (.profile-groups[data-active]) and a section's own [hidden]
|
|
577
|
+
// still wins, so the cross-project and connections reveal logic is
|
|
578
|
+
// untouched. The connections KEY follows the section: the rail lists it only
|
|
579
|
+
// while #conn-scroll is revealed, so an instance without the hub never shows
|
|
580
|
+
// a key that opens an empty screen.
|
|
581
|
+
const RAIL_ITEMS = [
|
|
582
|
+
{ id: 'overview', label: 'Overview' },
|
|
583
|
+
{ id: 'works', label: 'Works' },
|
|
584
|
+
{ id: 'activity', label: 'Activity' },
|
|
585
|
+
{ id: 'connections', label: 'Connections' },
|
|
586
|
+
];
|
|
587
|
+
const RAIL_GROUPS = RAIL_ITEMS.map((i) => i.id);
|
|
588
|
+
let rail = null;
|
|
589
|
+
|
|
590
|
+
function railItems() {
|
|
591
|
+
const conn = $('#conn-scroll');
|
|
592
|
+
return RAIL_ITEMS.filter((i) => i.id !== 'connections' || (conn && !conn.hidden));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function activateGroup(group, opts) {
|
|
596
|
+
if (!RAIL_GROUPS.includes(group)) return;
|
|
597
|
+
const groups = $('#profile-groups');
|
|
598
|
+
if (!groups) return;
|
|
599
|
+
groups.dataset.active = group;
|
|
600
|
+
rail?.activate(group);
|
|
601
|
+
if (!opts || opts.scroll !== false) window.scrollTo({ top: 0 });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// The hash is a key (#activity) or nothing of ours; "#/…" is the overlay
|
|
605
|
+
// namespace and passes through. A key whose section is hidden (connections
|
|
606
|
+
// without the hub) lands on the overview rather than an empty screen.
|
|
607
|
+
function applyHash() {
|
|
608
|
+
const id = String(window.location.hash || '').replace(/^#/, '');
|
|
609
|
+
if (!id || id.startsWith('/')) return;
|
|
610
|
+
const open = railItems().some((i) => i.id === id);
|
|
611
|
+
activateGroup(open ? id : 'overview', { scroll: false });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function wireRail() {
|
|
615
|
+
const mount = $('#profile-rail');
|
|
616
|
+
if (!mount || !window.OTBKit || !window.OTBKit.makeSectionRail) return;
|
|
617
|
+
rail = window.OTBKit.makeSectionRail({
|
|
618
|
+
mount,
|
|
619
|
+
items: railItems(),
|
|
620
|
+
mode: 'switch',
|
|
621
|
+
onSwitch: (key) => {
|
|
622
|
+
activateGroup(key);
|
|
623
|
+
// Reflect the key in the URL so the screen is shareable; replaceState
|
|
624
|
+
// keeps Back for leaving the page, not retracing rail clicks.
|
|
625
|
+
history.replaceState(null, '', window.location.pathname + window.location.search + `#${key}`);
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
rail.activate('overview');
|
|
629
|
+
applyHash();
|
|
630
|
+
window.addEventListener('hashchange', applyHash);
|
|
631
|
+
// profile-connections.js reveals #conn-scroll on its own schedule (a hub
|
|
632
|
+
// read); the key appears when the section does.
|
|
633
|
+
const conn = $('#conn-scroll');
|
|
634
|
+
if (conn && typeof MutationObserver === 'function') {
|
|
635
|
+
new MutationObserver(() => {
|
|
636
|
+
rail.refresh(railItems());
|
|
637
|
+
rail.activate($('#profile-groups')?.dataset.active || 'overview');
|
|
638
|
+
}).observe(conn, { attributes: true, attributeFilter: ['hidden'] });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
517
642
|
// ---- states -------------------------------------------------------------
|
|
518
643
|
|
|
519
644
|
function showMessage(text) {
|
|
@@ -528,9 +653,12 @@
|
|
|
528
653
|
showMessage(`${name} — ${rankLabel(normalizeRank(builder.rank))} · ${fmtNum(shipped)} work${shipped === 1 ? '' : 's'} shipped`);
|
|
529
654
|
renderHead(builder);
|
|
530
655
|
renderNumbers(data.shipped_count, data.achievements);
|
|
656
|
+
// The charts (task 1003489) read the activity route, so they need the same
|
|
657
|
+
// session the ledgers do; on the viewer's own profile they add the pace.
|
|
658
|
+
if (window.OTBProfileCharts) window.OTBProfileCharts.render({ api, API, builderId: builder.id, selfId, signedIn: selfId != null });
|
|
531
659
|
renderCrossProject(data, selfId);
|
|
532
660
|
renderAchievements(data.achievements);
|
|
533
|
-
|
|
661
|
+
buildWorksLedger(builder.id, selfId != null, data.recent_ships);
|
|
534
662
|
// The activity log needs a session (private goals + the idea trail), so it
|
|
535
663
|
// keys off whether /me resolved — the same signal the self-detect uses.
|
|
536
664
|
renderActivity(builder.id, selfId != null);
|
|
@@ -567,6 +695,9 @@
|
|
|
567
695
|
const xp = $('#xp-scroll'); if (xp) xp.hidden = true;
|
|
568
696
|
const act = $('#activity-scroll'); if (act) act.hidden = true;
|
|
569
697
|
const conn = $('#conn-scroll'); if (conn) conn.hidden = true;
|
|
698
|
+
const charts = $('#charts-scroll'); if (charts) charts.hidden = true;
|
|
699
|
+
const railEl = $('#profile-rail'); if (railEl) railEl.hidden = true;
|
|
700
|
+
activateGroup('overview', { scroll: false });
|
|
570
701
|
}
|
|
571
702
|
|
|
572
703
|
async function fetchProfile(id, selfId) {
|
|
@@ -576,6 +707,7 @@
|
|
|
576
707
|
// There is no builder, so there is no activity to log — hide the section
|
|
577
708
|
// rather than leaving its loading dots on a page about nobody.
|
|
578
709
|
const act = $('#activity-scroll'); if (act) act.hidden = true;
|
|
710
|
+
const charts = $('#charts-scroll'); if (charts) charts.hidden = true;
|
|
579
711
|
return;
|
|
580
712
|
}
|
|
581
713
|
if (!res.ok) throw new Error(`profile → ${res.status}`);
|
|
@@ -598,6 +730,7 @@
|
|
|
598
730
|
}
|
|
599
731
|
|
|
600
732
|
async function load() {
|
|
733
|
+
wireRail();
|
|
601
734
|
const idParam = new URLSearchParams(location.search).get('id');
|
|
602
735
|
try {
|
|
603
736
|
if (idParam) {
|