@bongos/core 1.24.29 → 1.24.30
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 +78 -38
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +36 -1
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +86 -89
- package/docs/copy-registry.json +137 -175
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +3 -2
- package/docs/page-readings.json +89 -107
- package/modules/government/db.js +29 -0
- package/modules/government/docket.js +61 -4
- package/modules/government/routes/government.js +2 -0
- package/modules/hall-ui/public/builders.js +3 -79
- package/modules/hall-ui/public/hall-render.js +6 -81
- package/modules/hall-ui/public/hall-widgets.js +4 -45
- package/modules/hall-ui/public/home-city-draw.js +275 -0
- package/modules/hall-ui/public/home-city.css +66 -0
- package/modules/hall-ui/public/home-city.js +223 -0
- package/modules/hall-ui/public/index.html +5 -6
- package/modules/hall-ui/public/index.states.json +73 -8
- package/modules/hall-ui/records/home-city.md +66 -0
- package/modules/lifecycle/home-city.js +340 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/home-city.js +29 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/src/module-api.js +1 -1
- package/tests/claim_from_task_and_home.mjs +8 -43
- package/tests/hall_board_world.mjs +10 -12
- package/tests/hall_home_ships_bounded.mjs +4 -3
- package/tests/home_bounded_reads.mjs +16 -31
- package/tests/home_city.mjs +385 -0
- package/tests/home_city_widget.mjs +120 -0
- package/tests/leaderboard_roles.mjs +3 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* modules/hall-ui/public/home-city.css — Home's project city (task 1005054).
|
|
2
|
+
The scene card, the ladder of builders beside it and the time slider under it
|
|
3
|
+
(home-city.js; record modules/hall-ui/records/home-city.md). Every colour is a
|
|
4
|
+
hall token or a mix of one; the tower faces are drawing data in
|
|
5
|
+
home-city-draw.js. One sky for both the city and the ladder, so the two read
|
|
6
|
+
as one scene. No :root here; motion is CSS and the hall's reduced-motion rule (style.css, `prefers-reduced-motion: reduce`)
|
|
7
|
+
stops it. */
|
|
8
|
+
|
|
9
|
+
.home-city { padding: 0; overflow: hidden; background: linear-gradient(var(--bg-card), var(--bg)); }
|
|
10
|
+
.hc-stage { display: flex; align-items: center; gap: 4px; }
|
|
11
|
+
.hc-svg { display: block; flex: 1; min-width: 0; height: auto; }
|
|
12
|
+
.hc-svg .hc-wait__n { font: 700 11px var(--font-display); fill: var(--accent-on); }
|
|
13
|
+
.hc-svg .hc-av__i { font: 600 13px var(--font-display); }
|
|
14
|
+
.hc-svg [data-tip] { cursor: default; }
|
|
15
|
+
.hc-svg .hc-tower:hover .hc-face { filter: brightness(1.3); }
|
|
16
|
+
|
|
17
|
+
/* the ladder: a small face and one thin line split by craft */
|
|
18
|
+
.hc-ladder { position: relative; flex: none; width: 230px; margin: 24px 28px 24px 0; }
|
|
19
|
+
.hc-rung { position: absolute; left: 0; right: 0; height: 34px; display: grid; grid-template-columns: 28px minmax(0, 1fr); align-items: center; gap: 12px; transition: top .55s cubic-bezier(.2, .7, .2, 1); }
|
|
20
|
+
.hc-av { position: relative; width: 28px; height: 28px; border-radius: 50%; background: var(--rule-soft); display: grid; place-items: center; overflow: hidden; font: 600 11px var(--font-display); color: var(--ink-soft); }
|
|
21
|
+
.hc-av img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
22
|
+
.hc-av--agent { background: color-mix(in srgb, var(--sea) 26%, var(--bg-card)); }
|
|
23
|
+
.hc-av svg { width: 15px; height: 15px; }
|
|
24
|
+
.hc-rung--me .hc-av { color: var(--ink); box-shadow: 0 0 0 1.5px var(--ink-soft); }
|
|
25
|
+
.hc-line { display: flex; gap: 3px; align-items: flex-end; height: 22px; transition: width .55s cubic-bezier(.2, .7, .2, 1); }
|
|
26
|
+
.hc-seg { display: flex; flex-direction: column; justify-content: flex-end; gap: 3px; min-width: 26px; color: var(--ink-faint); }
|
|
27
|
+
.hc-seg b { display: flex; align-items: center; gap: 3px; font: 600 10.5px/1 var(--font-display); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
28
|
+
.hc-seg .hc-ico { width: 11px; height: 11px; flex: none; opacity: .8; }
|
|
29
|
+
.hc-seg i { display: block; height: 3px; border-radius: 2px; background: var(--rule); }
|
|
30
|
+
.hc-rung--me .hc-seg { color: var(--ink-soft); }
|
|
31
|
+
.hc-rung--me .hc-seg i { background: var(--ink-soft); }
|
|
32
|
+
.hc-seg:hover { color: var(--ink); }
|
|
33
|
+
.hc-seg:hover i { background: var(--ink); }
|
|
34
|
+
|
|
35
|
+
/* the time slider */
|
|
36
|
+
.hc-time { display: flex; align-items: center; gap: 16px; padding: 6px 24px 18px; background: var(--bg-card); }
|
|
37
|
+
.hc-play { flex: none; width: 46px; height: 46px; border-radius: 50%; border: 1px solid var(--rule); background: transparent; color: var(--ink); cursor: pointer; display: grid; place-items: center; }
|
|
38
|
+
.hc-play svg { width: 16px; height: 16px; }
|
|
39
|
+
.hc-play .hc-play__off, .hc-play.is-playing .hc-play__on { display: none; }
|
|
40
|
+
.hc-play.is-playing .hc-play__off { display: block; }
|
|
41
|
+
.hc-play:focus-visible, .hc-slider:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
|
|
42
|
+
.hc-track { flex: 1; min-width: 0; }
|
|
43
|
+
.hc-slider { width: 100%; margin: 0; appearance: none; -webkit-appearance: none; background: transparent; height: 24px; cursor: pointer; }
|
|
44
|
+
.hc-slider::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--rule-soft); }
|
|
45
|
+
.hc-slider::-moz-range-track { height: 4px; border-radius: 2px; background: var(--rule-soft); }
|
|
46
|
+
.hc-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: var(--ink); margin-top: -8px; border: 3px solid var(--bg-card); }
|
|
47
|
+
.hc-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--ink); border: 3px solid var(--bg-card); }
|
|
48
|
+
.hc-ticks { display: flex; justify-content: space-between; font-size: 11px; color: var(--ink-faint); margin-top: 2px; }
|
|
49
|
+
|
|
50
|
+
.hc-tip { position: fixed; pointer-events: none; z-index: var(--z-modal); background: var(--ink); color: var(--bg-card); font-size: 12px; line-height: 1.35; padding: 8px 11px; border-radius: 9px; max-width: 260px; opacity: 0; transform: translate(-50%, calc(-100% - 12px)); }
|
|
51
|
+
.hc-tip.is-on { opacity: 1; }
|
|
52
|
+
.hc-tip b { display: block; font-size: 13px; }
|
|
53
|
+
|
|
54
|
+
/* live, and nothing more: a crane's slab climbs, this week's ideas glow (20s+) */
|
|
55
|
+
@keyframes hc-lift { 0%, 12% { transform: translateY(0); } 55%, 70% { transform: translateY(-24px); } 100% { transform: translateY(0); } }
|
|
56
|
+
@keyframes hc-glow { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
|
|
57
|
+
.hc-slab { animation: hc-lift 20s ease-in-out infinite; }
|
|
58
|
+
.hc-orb { animation: hc-glow 24s ease-in-out infinite; }
|
|
59
|
+
|
|
60
|
+
/* a phone: the ladder runs full width under the city */
|
|
61
|
+
@media (max-width: 700px) {
|
|
62
|
+
.hc-stage { flex-direction: column; align-items: stretch; }
|
|
63
|
+
.hc-svg { width: 100%; }
|
|
64
|
+
.hc-ladder { width: auto; margin: 12px 18px 4px; }
|
|
65
|
+
.hc-time { padding: 6px 16px 16px; }
|
|
66
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// modules/hall-ui/public/home-city.js — Home's project city (task 1005054).
|
|
2
|
+
//
|
|
3
|
+
// The page half of the owner's Home redesign (docs/design/mocks/hall/home/v14.html,
|
|
4
|
+
// record modules/hall-ui/records/home-city.md): the city scene, the ladder of
|
|
5
|
+
// builders beside it, and the time slider under it. It asks the server for ONE
|
|
6
|
+
// bounded weekly summary (GET /home/city) and never a feed it would slice — the
|
|
7
|
+
// rule tests/home_bounded_reads.mjs holds Home to. The drawing is
|
|
8
|
+
// home-city-draw.js (pure); this file owns the read, the DOM, the slider and hover.
|
|
9
|
+
//
|
|
10
|
+
// Motion stays CSS: play steps the slider a week at a time on a timer (state, not
|
|
11
|
+
// animation); the ladder's rows slide and a crane's slab lifts in CSS, which the
|
|
12
|
+
// hall's reduced-motion rule stops.
|
|
13
|
+
(function () {
|
|
14
|
+
'use strict';
|
|
15
|
+
if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
|
|
16
|
+
const Draw = window.OTBHomeCityDraw;
|
|
17
|
+
if (!Draw) return;
|
|
18
|
+
const API = '/api/bongos';
|
|
19
|
+
const WEEKS = 10;
|
|
20
|
+
const STEP_MS = 800;
|
|
21
|
+
const RUNG = 40;
|
|
22
|
+
const CRAFTS = [
|
|
23
|
+
['art', 'artistry', 'Art', 'pages designed'],
|
|
24
|
+
['build', 'building', 'Build', 'shipped'],
|
|
25
|
+
['idea', 'ideating', 'Ideas', 'ideas'],
|
|
26
|
+
['gov', 'governing', 'Govern', 'votes and decisions'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let host = null, model = null, viewerId = null, week = 0, timer = null;
|
|
30
|
+
const rungs = new Map();
|
|
31
|
+
|
|
32
|
+
function mode() {
|
|
33
|
+
const t = document.documentElement.dataset.theme;
|
|
34
|
+
if (t === 'light' || t === 'dark') return t;
|
|
35
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function weekName(i) {
|
|
39
|
+
if (!model || i === model.W - 1) return 'this week';
|
|
40
|
+
const w = model.weeks[i];
|
|
41
|
+
const d = w && w.start ? new Date(w.start + 'T00:00:00Z') : null;
|
|
42
|
+
return d && !isNaN(d) ? 'week of ' + d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }) : 'that week';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function craftIcon(key) {
|
|
46
|
+
const CM = window.OTBCraftMode;
|
|
47
|
+
return CM && typeof CM.iconHtml === 'function' ? CM.iconHtml(key, 'hc-ico') : '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- the scene: the drawing's shapes built as SVG nodes (city.js's split),
|
|
51
|
+
// so every name and title lands as an attribute value or text, never markup ----
|
|
52
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
53
|
+
function build(n) {
|
|
54
|
+
const e = document.createElementNS(NS, n.tag);
|
|
55
|
+
Object.keys(n.attrs).forEach((k) => e.setAttribute(k, String(n.attrs[k])));
|
|
56
|
+
if (n.text != null) e.textContent = n.text;
|
|
57
|
+
n.kids.forEach((c) => e.appendChild(build(c)));
|
|
58
|
+
return e;
|
|
59
|
+
}
|
|
60
|
+
function drawScene() {
|
|
61
|
+
const svg = host.querySelector('.hc-svg');
|
|
62
|
+
const out = Draw.citySvg(model, week, { mode: mode() });
|
|
63
|
+
svg.setAttribute('viewBox', out.viewBox);
|
|
64
|
+
svg.replaceChildren(...out.nodes.map(build));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- the ladder: a small face and one line split by craft, ranked ----
|
|
68
|
+
function rungFor(row) {
|
|
69
|
+
let r = rungs.get(row.id);
|
|
70
|
+
if (r) return r;
|
|
71
|
+
r = document.createElement('div');
|
|
72
|
+
r.className = 'hc-rung';
|
|
73
|
+
const av = document.createElement('span');
|
|
74
|
+
av.className = 'hc-av' + (row.agent ? ' hc-av--agent' : '');
|
|
75
|
+
if (row.agent) av.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="8" width="14" height="11" rx="3" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="9.5" cy="13.5" r="1.5" fill="currentColor"/><circle cx="14.5" cy="13.5" r="1.5" fill="currentColor"/><path d="M12 8V5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
76
|
+
else {
|
|
77
|
+
av.textContent = Draw.initial(row.name);
|
|
78
|
+
if (row.avatar && /^https:\/\//.test(row.avatar)) {
|
|
79
|
+
const img = document.createElement('img');
|
|
80
|
+
img.src = row.avatar; img.alt = ''; img.loading = 'lazy';
|
|
81
|
+
img.addEventListener('error', () => img.remove());
|
|
82
|
+
av.appendChild(img);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const line = document.createElement('span');
|
|
86
|
+
line.className = 'hc-line';
|
|
87
|
+
r.append(av, line);
|
|
88
|
+
host.querySelector('.hc-ladder').appendChild(r);
|
|
89
|
+
rungs.set(row.id, r);
|
|
90
|
+
return r;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function drawLadder() {
|
|
94
|
+
const rows = Draw.ladderFor(model, week, viewerId);
|
|
95
|
+
const ladder = host.querySelector('.hc-ladder');
|
|
96
|
+
ladder.style.height = (Math.max(1, rows.length) * RUNG) + 'px';
|
|
97
|
+
const top = Math.max(1, ...rows.map((r) => r.total));
|
|
98
|
+
const room = Math.max(60, ladder.clientWidth - 40);
|
|
99
|
+
const shown = new Set();
|
|
100
|
+
rows.forEach((row, n) => {
|
|
101
|
+
const r = rungFor(row);
|
|
102
|
+
shown.add(row.id);
|
|
103
|
+
r.hidden = false;
|
|
104
|
+
r.classList.toggle('hc-rung--me', row.me);
|
|
105
|
+
r.style.top = (n * RUNG) + 'px';
|
|
106
|
+
r.dataset.tip = row.name + (row.me ? ' (you)' : '') + ' · ' + row.total;
|
|
107
|
+
r.dataset.tipBody = weekName(week);
|
|
108
|
+
const line = r.querySelector('.hc-line');
|
|
109
|
+
line.textContent = '';
|
|
110
|
+
const crafts = CRAFTS.filter((c) => row.v[c[0]] > 0);
|
|
111
|
+
line.style.width = Math.max(crafts.length * 29, room * row.total / top).toFixed(1) + 'px';
|
|
112
|
+
crafts.forEach((c) => {
|
|
113
|
+
const seg = document.createElement('span');
|
|
114
|
+
seg.className = 'hc-seg';
|
|
115
|
+
seg.style.flex = row.v[c[0]] + ' 1 0';
|
|
116
|
+
seg.dataset.tip = row.name + ' · ' + c[2];
|
|
117
|
+
seg.dataset.tipBody = row.v[c[0]] + ' ' + c[3] + ' ' + weekName(week);
|
|
118
|
+
const lab = document.createElement('b');
|
|
119
|
+
lab.innerHTML = craftIcon(c[1]);
|
|
120
|
+
lab.appendChild(document.createTextNode(String(row.v[c[0]])));
|
|
121
|
+
seg.append(lab, document.createElement('i'));
|
|
122
|
+
line.appendChild(seg);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
rungs.forEach((r, id) => { if (!shown.has(id)) r.hidden = true; });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setWeek(i) {
|
|
129
|
+
week = Math.max(0, Math.min(model.W - 1, i));
|
|
130
|
+
const s = host.querySelector('.hc-slider');
|
|
131
|
+
s.value = String(week);
|
|
132
|
+
s.setAttribute('aria-valuetext', weekName(week));
|
|
133
|
+
drawScene();
|
|
134
|
+
drawLadder();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- play: a week at a time, then stop on now ----
|
|
138
|
+
function stop() {
|
|
139
|
+
if (timer) clearTimeout(timer);
|
|
140
|
+
timer = null;
|
|
141
|
+
const b = host && host.querySelector('.hc-play');
|
|
142
|
+
if (b) { b.classList.remove('is-playing'); b.setAttribute('aria-label', 'Play the weeks from the start'); }
|
|
143
|
+
}
|
|
144
|
+
function play() {
|
|
145
|
+
if (timer) { stop(); return; }
|
|
146
|
+
const b = host.querySelector('.hc-play');
|
|
147
|
+
b.classList.add('is-playing'); b.setAttribute('aria-label', 'Pause');
|
|
148
|
+
let i = 0;
|
|
149
|
+
const step = () => { setWeek(i); if (i++ < model.W - 1) timer = setTimeout(step, STEP_MS); else stop(); };
|
|
150
|
+
step();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- one tooltip for the whole widget; labels are ledger data, so text only ----
|
|
154
|
+
function wireTip() {
|
|
155
|
+
let tip = document.getElementById('hc-tip');
|
|
156
|
+
if (!tip) { tip = document.createElement('div'); tip.id = 'hc-tip'; tip.className = 'hc-tip'; tip.setAttribute('role', 'tooltip'); document.body.appendChild(tip); }
|
|
157
|
+
host.addEventListener('pointerover', (e) => {
|
|
158
|
+
const n = e.target.closest('[data-tip]');
|
|
159
|
+
if (!n || !host.contains(n)) return;
|
|
160
|
+
const r = n.getBoundingClientRect();
|
|
161
|
+
tip.textContent = '';
|
|
162
|
+
const b = document.createElement('b'); b.textContent = n.dataset.tip; tip.appendChild(b);
|
|
163
|
+
if (n.dataset.tipBody) tip.appendChild(document.createTextNode(n.dataset.tipBody));
|
|
164
|
+
tip.style.left = (r.left + r.width / 2) + 'px';
|
|
165
|
+
tip.style.top = r.top + 'px';
|
|
166
|
+
tip.classList.add('is-on');
|
|
167
|
+
});
|
|
168
|
+
host.addEventListener('pointerout', (e) => { if (e.target.closest('[data-tip]')) tip.classList.remove('is-on'); });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ticks() {
|
|
172
|
+
const t = host.querySelector('.hc-ticks');
|
|
173
|
+
const first = model.weeks[0] && model.weeks[0].start ? new Date(model.weeks[0].start + 'T00:00:00Z') : null;
|
|
174
|
+
const mid = model.weeks[Math.floor(model.W / 2)] && model.weeks[Math.floor(model.W / 2)].start ? new Date(model.weeks[Math.floor(model.W / 2)].start + 'T00:00:00Z') : null;
|
|
175
|
+
const mon = (d) => (d && !isNaN(d) ? d.toLocaleDateString('en-GB', { month: 'short', timeZone: 'UTC' }) : '');
|
|
176
|
+
t.innerHTML = '';
|
|
177
|
+
[mon(first), mon(mid), 'Now'].forEach((s) => { const sp = document.createElement('span'); sp.textContent = s; t.appendChild(sp); });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function load() {
|
|
181
|
+
const api = window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
182
|
+
if (!api) return;
|
|
183
|
+
let res;
|
|
184
|
+
try { res = await api.request('GET', `${API}/home/city?weeks=${WEEKS}`); } catch (_) { return; }
|
|
185
|
+
if (!res || !res.ok || !res.data) return; // signed out, gated or failed: Home keeps its other cards, never an empty plate
|
|
186
|
+
model = Draw.cityModel(res.data);
|
|
187
|
+
viewerId = res.data.viewer && res.data.viewer.id != null ? String(res.data.viewer.id) : null;
|
|
188
|
+
if (!model.W || !model.areas.length) return;
|
|
189
|
+
const s = host.querySelector('.hc-slider');
|
|
190
|
+
s.max = String(model.W - 1);
|
|
191
|
+
ticks();
|
|
192
|
+
host.hidden = false;
|
|
193
|
+
setWeek(model.W - 1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mount(hostEl) {
|
|
197
|
+
host = hostEl;
|
|
198
|
+
host.innerHTML =
|
|
199
|
+
'<h2 id="home-city-h" class="sr-only">The project this week</h2>' +
|
|
200
|
+
'<div class="hc-stage">' +
|
|
201
|
+
'<svg class="hc-svg" role="img" aria-label="The project as a city: one tower per working area, as tall as what has shipped there, with the builders of the week beside it"></svg>' +
|
|
202
|
+
'<div class="hc-ladder" aria-label="This week\'s builders, ranked by what they did"></div>' +
|
|
203
|
+
'</div>' +
|
|
204
|
+
'<div class="hc-time">' +
|
|
205
|
+
'<button type="button" class="hc-play" aria-label="Play the weeks from the start">' +
|
|
206
|
+
'<svg class="hc-play__on" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2.5v11l9-5.5z" fill="currentColor"/></svg>' +
|
|
207
|
+
'<svg class="hc-play__off" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h3v10H4zM9 3h3v10H9z" fill="currentColor"/></svg>' +
|
|
208
|
+
'</button>' +
|
|
209
|
+
'<div class="hc-track"><input type="range" class="hc-slider" min="0" max="0" step="1" value="0" aria-label="Week"><div class="hc-ticks"></div></div>' +
|
|
210
|
+
'</div>';
|
|
211
|
+
host.querySelector('.hc-slider').addEventListener('input', (e) => { stop(); setWeek(Number(e.target.value)); });
|
|
212
|
+
host.querySelector('.hc-play').addEventListener('click', play);
|
|
213
|
+
wireTip();
|
|
214
|
+
new MutationObserver(() => { if (model) drawScene(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
215
|
+
window.addEventListener('resize', () => { if (model) drawLadder(); });
|
|
216
|
+
load();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
window.OTB.contributeHallWidget({
|
|
220
|
+
id: 'home-city', label: 'The project', order: 15, core: true, mount,
|
|
221
|
+
view: 'home', zone: 'home-stats', sectionClass: 'scroll home-city', labelledby: 'home-city-h', hiddenByDefault: true,
|
|
222
|
+
});
|
|
223
|
+
})();
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
22
22
|
<link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-10-23-city1">
|
|
23
23
|
<link rel="stylesheet" href="/builders/hall-ask.css">
|
|
24
|
+
<link rel="stylesheet" href="/builders/home-city.css">
|
|
24
25
|
</head>
|
|
25
26
|
<body data-page="home">
|
|
26
27
|
|
|
@@ -138,8 +139,10 @@
|
|
|
138
139
|
<script src="/builders/hall-widgets.js?v=2026-09-30-founding" defer></script>
|
|
139
140
|
<!-- The genesis home (task 1004423): a Home banner card that renders only in founding mode. -->
|
|
140
141
|
<script src="/builders/genesis-home.js?v=2026-10-01-genesis" defer></script>
|
|
141
|
-
<!--
|
|
142
|
-
|
|
142
|
+
<!-- task 1005054: the project city — the drawing (pure) and the widget — in the
|
|
143
|
+
home-stats zone; the Goals card (goals.js) left Home with it. -->
|
|
144
|
+
<script src="/builders/home-city-draw.js" defer></script>
|
|
145
|
+
<script src="/builders/home-city.js" defer></script>
|
|
143
146
|
<!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
|
|
144
147
|
both goal-inbox.js's attention-inbox slice below and the /blockers page.
|
|
145
148
|
Loaded BEFORE goal-inbox.js, which calls it while rendering. -->
|
|
@@ -159,10 +162,6 @@
|
|
|
159
162
|
window.OTBProfileNudge; goal-inbox.js's mount() calls it once it has
|
|
160
163
|
created the #attention-profile div, so load order is not load-bearing. -->
|
|
161
164
|
<script src="/builders/profile-nudge.js?v=2026-08-25-profile-nudge" defer></script>
|
|
162
|
-
<!-- task 1003459: the hall's ONE claim/release action (task 1003458) — the
|
|
163
|
-
Home "Next up" row's Claim button consumes it rather than a second
|
|
164
|
-
hand-rolled POST /claims. Loaded BEFORE builders.js, which wires it. -->
|
|
165
|
-
<script src="/builders/claim-action.js?v=2026-09-26-claimaction1" defer></script>
|
|
166
165
|
<script src="/builders/builders.js?v=2026-09-28-nextupclaim" defer></script>
|
|
167
166
|
<!-- task 1004727: the ONE karma vote control every task/idea list renders -->
|
|
168
167
|
<script src="/builders/vote-control.js"></script>
|
|
@@ -2,17 +2,82 @@
|
|
|
2
2
|
"_": "The hall's HOME page through modules/ui-design/kit/render.js (task 1003304). Home is served at the surface root, so its page path has no last segment to name the file — run it with `--states modules/hall-ui/public/index.states.json`, not `--page`.",
|
|
3
3
|
"page": "/builders/",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
|
-
"stub": {
|
|
5
|
+
"stub": {
|
|
6
|
+
"prefix": "/builders/"
|
|
7
|
+
},
|
|
6
8
|
"modeQuery": false,
|
|
7
9
|
"_feeds": "As work.states.json: the kit's stub is not the hall's feed set, so Home renders its skeletons and empty states here. The populated dashboard is the hall-preview harness.",
|
|
8
|
-
"ignoreRequests": [
|
|
9
|
-
|
|
10
|
+
"ignoreRequests": [
|
|
11
|
+
"/api/(gds|bongos)/me$",
|
|
12
|
+
"/api/(gds|bongos)/me/",
|
|
13
|
+
"/api/(gds|bongos)/tasks",
|
|
14
|
+
"/api/(gds|bongos)/claims",
|
|
15
|
+
"/api/(gds|bongos)/goals",
|
|
16
|
+
"/api/(gds|bongos)/inbox",
|
|
17
|
+
"/api/(gds|bongos)/live",
|
|
18
|
+
"/api/(gds|bongos)/analytics/",
|
|
19
|
+
"/api/(gds|bongos)/achievements",
|
|
20
|
+
"/api/(gds|bongos)/versions",
|
|
21
|
+
"/api/(gds|bongos)/leaderboard",
|
|
22
|
+
"/api/(gds|bongos)/home/"
|
|
23
|
+
],
|
|
24
|
+
"ignoreConsole": [
|
|
25
|
+
"/api/(gds|bongos)/",
|
|
26
|
+
"\\b401\\b",
|
|
27
|
+
"\\b404\\b",
|
|
28
|
+
"Unauthorized",
|
|
29
|
+
"Not Found",
|
|
30
|
+
"load failed"
|
|
31
|
+
],
|
|
10
32
|
"_1003441": "The `leaderboard` state left with the view: the Roll is the /roster page now and #leaderboard REDIRECTS there, so there is no #view-leaderboard on this page to make visible. The redirect is proven in tests/hall_nav.mjs (it asserts location.replace('/roster') fires on the index and nowhere else) rather than by a render state, because a state whose page navigates away measures the destination, not this page. `standing-karma` replaces the coverage: task 2178 made each Standing rail section addressable and none had a measured state.",
|
|
11
33
|
"states": {
|
|
12
|
-
"home": {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
"home": {
|
|
35
|
+
"auth": true,
|
|
36
|
+
"expect": {
|
|
37
|
+
"visible": [
|
|
38
|
+
"#view-home"
|
|
39
|
+
],
|
|
40
|
+
"hidden": [
|
|
41
|
+
"#view-standing"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"standing": {
|
|
46
|
+
"auth": true,
|
|
47
|
+
"url": "/builders/#accomplishments",
|
|
48
|
+
"actions": [
|
|
49
|
+
[
|
|
50
|
+
"wait",
|
|
51
|
+
400
|
|
52
|
+
]
|
|
53
|
+
],
|
|
54
|
+
"expect": {
|
|
55
|
+
"visible": [
|
|
56
|
+
"#view-standing"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"standing-karma": {
|
|
61
|
+
"auth": true,
|
|
62
|
+
"url": "/builders/#standing-karma",
|
|
63
|
+
"actions": [
|
|
64
|
+
[
|
|
65
|
+
"wait",
|
|
66
|
+
400
|
|
67
|
+
]
|
|
68
|
+
],
|
|
69
|
+
"expect": {
|
|
70
|
+
"visible": [
|
|
71
|
+
"#view-standing"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"out": {
|
|
76
|
+
"auth": false
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"reducedMotion": {
|
|
80
|
+
"auth": true
|
|
16
81
|
},
|
|
17
|
-
"
|
|
82
|
+
"_1005054": "The project city (home-city.js, task 1005054) reads GET /home/city, which the kit stub does not serve: here the city stays hidden, as it does for a signed-out visitor or a failed read, and the inbox below renders as before. The populated city is measured in the hall-preview harness (scripts/hall-preview/fixtures/home__city.json) and by tests/home_city_widget.mjs."
|
|
18
83
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# The project city — Home's body becomes one scene: towers, a ladder of builders, a time slider (task 1005054)
|
|
2
|
+
|
|
3
|
+
The owner's Home redesign of 2026-11-03, agreed over fourteen mock rounds; the approved mock is
|
|
4
|
+
[`docs/design/mocks/hall/home/v14.html`](../../../docs/design/mocks/hall/home/v14.html) (v2 to v13 beside it
|
|
5
|
+
are the rounds, each with its own header comment on what changed and why). The brief in the owner's words:
|
|
6
|
+
"I don't want to reach, I want to see and feel", and the UI "subtle and free from overuse of text".
|
|
7
|
+
|
|
8
|
+
**What people see** (agreed before drawing, round 6): who is involved, the speed of builds as a time slider,
|
|
9
|
+
what is waiting on them, what is being built now, then spend, health and just shipped as small things. Not on
|
|
10
|
+
Home: how close the version is to done, what is ready to go live, work to pick up. The same page for everyone.
|
|
11
|
+
|
|
12
|
+
- **The scene** (`home-city-draw.js`, pure; the city's own iso palette from `city-draw.js`): one tower per
|
|
13
|
+
working area (the open, non-maintenance goals of the version being built), as tall as what has shipped
|
|
14
|
+
there; artist and ui work clads it in glass from the ground up; one hairline per floor. The sites worked in
|
|
15
|
+
the selected week carry a crane lifting a slab, scaffolding round the floor going up, that floor lit, and the
|
|
16
|
+
face of who built there (a robot face when an Autobongos runner is working it). That week's ideas are lights
|
|
17
|
+
in the sky, the week before's dimmer; one that became work drops a dotted line to its tower. A salmon dot
|
|
18
|
+
with a count sits on each tower where something waits on the viewer, on the latest week only.
|
|
19
|
+
- **The ladder** beside the city: a small face and one thin line per builder, length = their week's total,
|
|
20
|
+
split into art · build · ideas · govern, each piece marked with the hall's own craft icon (`craft-mode.js`
|
|
21
|
+
`iconHtml`, never a second copy) and its count. Ranked by the total, the top seven, and the viewer appended
|
|
22
|
+
when they fall below. No rank numbers, no names, no tabs: the words are on hover. One colour, not four: four
|
|
23
|
+
craft colours from this pack fail the dataviz validator's normal-vision floor (v11's note).
|
|
24
|
+
- **The slider** under the scene, with play: every part redraws for the week it is on. Play steps a week at a
|
|
25
|
+
time on a timer and stops on now — state, never animation, so no motion lives in JavaScript. The ladder's
|
|
26
|
+
rows slide and a crane's slab lifts in CSS; the hall's reduced-motion rule stops both.
|
|
27
|
+
- **Placement.** The widget is `home-city` in the `home-stats` zone (budget 1), which the stats strip held.
|
|
28
|
+
|
|
29
|
+
**Left Home:** the stats strip and version tab (`home-stats`, and the `/public/progress` read that only fed the
|
|
30
|
+
tab), the Goals card (`goals.js` is no longer loaded by `index.html`), and Your work — Next up with its claim
|
|
31
|
+
button and `GET /tasks/claimable?limit=5`, and the Recently shipped table (`home-recent`). `claim-action.js` left
|
|
32
|
+
`index.html` with the claim button. **Kept:** the conditional welcome, setup, genesis and founding banners, the
|
|
33
|
+
Standing chip (its Shipped figure now comes straight from the ships payload, `shipState`, not a DOM tile), and
|
|
34
|
+
the Needs-your-attention inbox below the city, because it holds actions with no other home (join requests,
|
|
35
|
+
invitations, triage). The `/me/recent-ships?limit=5` read stays: it feeds the chip and Accomplishments.
|
|
36
|
+
|
|
37
|
+
**The data** is ONE bounded read, `GET /home/city?weeks=10` (`modules/lifecycle/home-city.js`, any signed-in
|
|
38
|
+
builder): counts grouped by week in SQL, so the payload grows with areas, builders and weeks, never with the
|
|
39
|
+
corpus; `tests/home_city.mjs` proves every query is bounded and selects no prose. Four things it is honest about:
|
|
40
|
+
|
|
41
|
+
- **Art** is economy's credits-by-craft rule: `artist` and `ui` disciplines. Page tweaks file under each version's
|
|
42
|
+
artist-review goal, so that tower is mostly glass.
|
|
43
|
+
- **An agent is a site fact, never a person.** Autobongos runs under its builder's account; a site whose task an
|
|
44
|
+
`autonomy_runner_heartbeat` row is working (seen in the last 15 minutes) is drawn with the robot face and named
|
|
45
|
+
"<builder>'s agent". `people[].agent` is always false from the server.
|
|
46
|
+
- **"Waits on you" is the docket's own routing (task 1005056):** the government port's `yoursByGoal(viewer)`
|
|
47
|
+
routes the docket's goal-shaped matters (blockers, criteria awaiting sign-off, goal join requests) with
|
|
48
|
+
`modules/government/routing.js` and counts, per area, those routed to the viewer — so a seated Lead, a board
|
|
49
|
+
member or an access holder sees their own dots, not only an Archon. With government off, an older government
|
|
50
|
+
without the method, or seats it could not read, the server falls back to open blockers whose stored ask waits
|
|
51
|
+
on `owner` (or carry none), shown only to an Archon.
|
|
52
|
+
- Government and autonomy are optional modules: their tables are probed with `to_regclass` first and left out
|
|
53
|
+
when absent, so a project with either switched off still gets its city.
|
|
54
|
+
|
|
55
|
+
**Verifying:** the hall-preview harness serves `fixtures/home__city.json` (hand-shaped from the approved mock, ids
|
|
56
|
+
from the other fixtures); `tests/home_city_widget.mjs` runs the real widget in a browser (one read, the slider
|
|
57
|
+
redraws, play stops on now, a refused read leaves no empty plate, no sideways scroll at 390); the kit stub does
|
|
58
|
+
not serve the read, so `index.states.json` ignores it and its renders show the city hidden, as for a visitor.
|
|
59
|
+
|
|
60
|
+
## Private areas
|
|
61
|
+
|
|
62
|
+
A private goal the viewer is neither a member of nor an Archon over still stands as a tower (its counts are structure), but the server sends no title for it or for the ideas tied to it; the drawing labels them "A private area" and "A private idea" (ADR 0112, as the goal map and the sky apply it). Pinned in `tests/home_city.mjs`.
|
|
63
|
+
|
|
64
|
+
## Sign-off
|
|
65
|
+
|
|
66
|
+
The owner chose v14 from fourteen mock rounds, approved the before and after review sheet (1280 dark and light, 390 dark) on 2026-11-02, and said "Yes, ship it".
|