@bongos/core 1.24.29 → 1.24.31

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 (113) hide show
  1. package/.bongos-core.json +198 -108
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +12 -0
  4. package/clients/bongos-client/index.cjs +12 -0
  5. package/clients/bongos-client/index.d.ts +12 -0
  6. package/clients/bongos-client/index.mjs +12 -0
  7. package/docs/api/openapi.json +174 -1
  8. package/docs/api-reference.md +12 -2
  9. package/docs/copy-inventory.md +226 -224
  10. package/docs/copy-registry.json +583 -549
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-inventory.json +18 -37
  13. package/docs/page-readings.json +306 -361
  14. package/modules/copy-desk/module.json +1 -1
  15. package/modules/copy-desk/routes/copy-desk-studio.js +103 -0
  16. package/modules/copy-desk/tests/copy_no_cms.mjs +2 -2
  17. package/modules/government/db.js +29 -0
  18. package/modules/government/docket.js +61 -4
  19. package/modules/government/routes/government.js +2 -0
  20. package/modules/hall-ui/public/ai-studio.html +1 -1
  21. package/modules/hall-ui/public/atlas.html +1 -1
  22. package/modules/hall-ui/public/blockers.html +1 -1
  23. package/modules/hall-ui/public/board-room.html +1 -1
  24. package/modules/hall-ui/public/builders.js +3 -79
  25. package/modules/hall-ui/public/city.html +1 -1
  26. package/modules/hall-ui/public/collab.html +1 -1
  27. package/modules/hall-ui/public/commands.html +1 -1
  28. package/modules/hall-ui/public/copy-desk.html +1 -1
  29. package/modules/hall-ui/public/craft-mode.js +1 -1
  30. package/modules/hall-ui/public/deploy.html +1 -1
  31. package/modules/hall-ui/public/diagrams.html +1 -1
  32. package/modules/hall-ui/public/drachmae.html +1 -1
  33. package/modules/hall-ui/public/fleet.html +1 -1
  34. package/modules/hall-ui/public/gate.html +1 -1
  35. package/modules/hall-ui/public/goal-map.html +1 -1
  36. package/modules/hall-ui/public/goals.html +1 -1
  37. package/modules/hall-ui/public/government.html +1 -1
  38. package/modules/hall-ui/public/hall-render.js +68 -167
  39. package/modules/hall-ui/public/hall-widgets.js +4 -45
  40. package/modules/hall-ui/public/home-city-draw.js +583 -0
  41. package/modules/hall-ui/public/home-city.css +102 -0
  42. package/modules/hall-ui/public/home-city.js +339 -0
  43. package/modules/hall-ui/public/home-gallery.css +129 -0
  44. package/modules/hall-ui/public/home-gallery.js +349 -0
  45. package/modules/hall-ui/public/home-studio.css +72 -0
  46. package/modules/hall-ui/public/home-studio.js +203 -0
  47. package/modules/hall-ui/public/idea.html +1 -1
  48. package/modules/hall-ui/public/ideas.html +1 -1
  49. package/modules/hall-ui/public/index.html +9 -7
  50. package/modules/hall-ui/public/index.states.json +73 -8
  51. package/modules/hall-ui/public/market-watch-item.html +1 -1
  52. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  53. package/modules/hall-ui/public/market-watch-settings.js +6 -1
  54. package/modules/hall-ui/public/market-watch.html +141 -54
  55. package/modules/hall-ui/public/market-watch.js +54 -0
  56. package/modules/hall-ui/public/market-watch.states.json +2 -1
  57. package/modules/hall-ui/public/modules.html +1 -1
  58. package/modules/hall-ui/public/my-agents.html +1 -1
  59. package/modules/hall-ui/public/primer.html +1 -1
  60. package/modules/hall-ui/public/profile.html +1 -1
  61. package/modules/hall-ui/public/project-settings.html +1 -1
  62. package/modules/hall-ui/public/ranks.html +1 -1
  63. package/modules/hall-ui/public/roadmap.html +1 -1
  64. package/modules/hall-ui/public/roster.html +1 -1
  65. package/modules/hall-ui/public/sessions.html +1 -1
  66. package/modules/hall-ui/public/settings.html +1 -1
  67. package/modules/hall-ui/public/shell.js +2 -3
  68. package/modules/hall-ui/public/street.html +1 -1
  69. package/modules/hall-ui/public/studio.html +9 -1
  70. package/modules/hall-ui/public/style.css +74 -27
  71. package/modules/hall-ui/public/task.html +1 -1
  72. package/modules/hall-ui/public/thinking.html +1 -1
  73. package/modules/hall-ui/public/tweak-editor.html +1 -1
  74. package/modules/hall-ui/public/watch.html +1 -1
  75. package/modules/hall-ui/public/work.html +1 -1
  76. package/modules/hall-ui/records/home-city.md +189 -0
  77. package/modules/hall-ui/records/loader.md +1 -1
  78. package/modules/hall-ui/records/market-watch-settings.md +16 -3
  79. package/modules/hall-ui/records/market-watch.md +3 -1
  80. package/modules/lifecycle/home-city.js +388 -0
  81. package/modules/lifecycle/home-gallery.js +320 -0
  82. package/modules/lifecycle/migrations/lifecycle_020_work_appreciations.sql +36 -0
  83. package/modules/lifecycle/module.json +2 -0
  84. package/modules/lifecycle/routes/home-city.js +29 -0
  85. package/modules/lifecycle/routes/home-gallery.js +37 -0
  86. package/modules/market-watch/routes/settings.js +1 -1
  87. package/package-lock.json +2 -2
  88. package/package.json +1 -1
  89. package/release-notes.json +38 -0
  90. package/scripts/bongos/leak-scan-allowlist.js +8 -0
  91. package/scripts/hall-preview/README.md +1 -0
  92. package/scripts/hall-preview/server.js +10 -0
  93. package/src/bongos/serve-internal.js +17 -3
  94. package/src/module-api.js +1 -1
  95. package/tests/claim_from_task_and_home.mjs +8 -43
  96. package/tests/hall_board_world.mjs +10 -12
  97. package/tests/hall_builders_page_pass.mjs +18 -27
  98. package/tests/hall_home_ships_bounded.mjs +4 -3
  99. package/tests/hall_loading.mjs +1 -1
  100. package/tests/hall_nav.mjs +11 -4
  101. package/tests/hall_page_gate_map.mjs +7 -4
  102. package/tests/home_bounded_reads.mjs +16 -31
  103. package/tests/home_city.mjs +580 -0
  104. package/tests/home_city_widget.mjs +260 -0
  105. package/tests/home_gallery.mjs +255 -0
  106. package/tests/home_gallery_widget.mjs +220 -0
  107. package/tests/home_studio.mjs +169 -0
  108. package/tests/home_studio_widget.mjs +157 -0
  109. package/tests/leaderboard_roles.mjs +3 -2
  110. package/tests/market_watch_pages.mjs +162 -0
  111. package/tests/nav_permission_atoms.mjs +2 -2
  112. package/modules/hall-ui/public/market-watch-settings.html +0 -115
  113. package/modules/hall-ui/public/market-watch-settings.states.json +0 -14
@@ -0,0 +1,349 @@
1
+ // modules/hall-ui/public/home-gallery.js — the artist's Home: a bench in the museum
2
+ // (task 1005060, Home v16 step 3; mock docs/design/mocks/hall/home/v16-artistry.html,
3
+ // record modules/hall-ui/records/home-city.md § "The gallery").
4
+ //
5
+ // In Artistry, Home is not the city: it is a seat on a bench in the city's art museum,
6
+ // facing a quiet wall. One signed work hangs at a time; swipe, drag, the arrows or the
7
+ // arrow keys move along the wall. Most popular first (the default), or Latest. Each
8
+ // work is labelled the way a museum labels a wall — small, at the work's foot, opening
9
+ // closer on a click — with the artist's signature in their hand on the wall under it
10
+ // and one heart that never changes size. Full screen (the button or F) is the art
11
+ // alone, its signature and heart. "The full gallery" hangs every work at once.
12
+ //
13
+ // The owner's rule is the whole design: "not about quality but about appreciation".
14
+ // So a work shows how many builders appreciated it only in its credit line, and
15
+ // nothing here ranks a person.
16
+ //
17
+ // It is not a hall widget of its own (Home's stats zone holds one): home-city.js
18
+ // attaches it beside the city and shows it when the craft is Artistry. It asks for the
19
+ // wall only then — one bounded read (GET /home/gallery, at most 60 works), again only
20
+ // when the order changes — and writes one thing, the viewer's own heart
21
+ // (PUT /home/gallery/:id/heart). Ledger text lands as text, never markup; a picture
22
+ // is shown only from the hall's own task-visuals route.
23
+ //
24
+ // Motion is CSS (the track's slide, the label's zoom), which the hall's reduced-motion
25
+ // rule stops; this file only sets where things are.
26
+ (function () {
27
+ 'use strict';
28
+
29
+ var API = '/api/bongos';
30
+ var ART_URL = /^\/api\/(bongos|gds)\/task-visuals\/task-\d{1,12}-[0-9a-f]{16}\.(png|jpg|gif|webp)$/;
31
+ var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
32
+ var HEART = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13.6S2 10 2 6a3 3 0 0 1 6-1 3 3 0 0 1 6 1c0 4-6 7.6-6 7.6Z" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></svg>';
33
+
34
+ // ---- pure: the label's words (node-testable) ----
35
+ function day(iso) {
36
+ var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''));
37
+ return m ? { y: Number(m[1]), m: Number(m[2]) - 1, d: Number(m[3]) } : null;
38
+ }
39
+ function longDate(iso) { var x = day(iso); return x ? x.d + ' ' + MONTHS[x.m] + ' ' + x.y : ''; }
40
+ function year(iso) { var x = day(iso); return x ? x.y : ''; }
41
+ function bio(since) { var x = day(since); return x ? 'On this project since ' + MONTHS[x.m] + ' ' + x.y : 'On this project'; }
42
+ // how an artist signs: a first name and an initial, and the year — "Sana O. ’26"
43
+ function signature(name, shipped) {
44
+ var parts = String(name || '').trim().split(/\s+/).filter(Boolean);
45
+ var sig = parts.length > 1 ? parts[0] + ' ' + parts[parts.length - 1].charAt(0) + '.' : (parts[0] || '');
46
+ var y = year(shipped);
47
+ return y ? sig + ' ’' + String(y).slice(2) : sig;
48
+ }
49
+ // a page tweak's title can carry the branding pack's holes ({{productName}})
50
+ function fillHoles(s, brand) {
51
+ return String(s || '').replace(/\{\{\s*([A-Za-z]+)\s*\}\}/g, function (_, k) { return brand && typeof brand[k] === 'string' ? brand[k] : ''; })
52
+ .replace(/^\s*[—–:-]\s*|\s*[—–:-]\s*$/g, '').trim();
53
+ }
54
+ function artFor(work, mode) {
55
+ var a = work && work.art ? work.art : {};
56
+ var u = mode === 'dark' ? (a.dark || a.light) : (a.light || a.dark);
57
+ return typeof u === 'string' && ART_URL.test(u) ? u : null;
58
+ }
59
+ // the studio's links back into the gallery: ?work=<id> or ?gallery=all
60
+ function landing(search) {
61
+ var q = String(search || '');
62
+ var w = /[?&]work=(\d{1,12})(?:&|$)/.exec(q);
63
+ return { work: w ? w[1] : null, all: /[?&]gallery=all(?:&|$)/.test(q) };
64
+ }
65
+ function credit(work) {
66
+ var n = Math.max(0, Number(work.hearts) || 0);
67
+ return 'Gift of the artist to the project. Shipped ' + longDate(work.shipped) + '. Appreciated by ' + n + (n === 1 ? ' builder.' : ' builders.');
68
+ }
69
+
70
+ var Lib = { landing: landing, longDate: longDate, bio: bio, signature: signature, fillHoles: fillHoles, artFor: artFor, credit: credit, ART_URL: ART_URL };
71
+ if (typeof module !== 'undefined' && module.exports) module.exports = Lib;
72
+ if (typeof window === 'undefined') return;
73
+
74
+ // ---- the page ----
75
+ function el(tag, cls, attrs, text) {
76
+ var e = document.createElement(tag);
77
+ if (cls) e.className = cls;
78
+ if (attrs) Object.keys(attrs).forEach(function (k) { e.setAttribute(k, attrs[k]); });
79
+ if (text != null) e.textContent = text;
80
+ return e;
81
+ }
82
+ function mode() {
83
+ var t = document.documentElement.dataset.theme;
84
+ if (t === 'light' || t === 'dark') return t;
85
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
86
+ }
87
+ function brand() {
88
+ var b = window.__BRANDING__ || {};
89
+ return { productName: b.productName || (b.identity && b.identity.productName) || '', worldName: b.worldName || (b.identity && b.identity.worldName) || '', tagline: b.tagline || '' };
90
+ }
91
+
92
+ function attach(host) {
93
+ var api = null, list = [], at = 0, order = 'popular', loaded = false, asking = 0, first = true;
94
+ var root = el('div', 'hg');
95
+ root.innerHTML =
96
+ '<div class="hg-room" tabindex="0" aria-roledescription="gallery">' +
97
+ '<div class="hg-vinyl">' +
98
+ '<h2 class="hg-h">The collection</h2>' +
99
+ '<p class="hg-sub">Works by the people of this project</p>' +
100
+ '<div class="hg-sort" role="group" aria-label="Order the works">' +
101
+ '<button type="button" data-order="popular" aria-pressed="true">Most popular</button>' +
102
+ '<button type="button" data-order="latest" aria-pressed="false">Latest</button>' +
103
+ '</div>' +
104
+ '<nav class="hg-ways" aria-label="Elsewhere in the museum">' +
105
+ '<button type="button" class="hg-way hg-way-all" aria-pressed="false">The full gallery <span aria-hidden="true">→</span></button>' +
106
+ '<a class="hg-way" href="/studio">Your studio <span aria-hidden="true">→</span></a>' +
107
+ '</nav>' +
108
+ '</div>' +
109
+ '<div class="hg-wall"><div class="hg-track"></div></div>' +
110
+ '<div class="hg-salon" role="list" aria-label="The full gallery" hidden></div>' +
111
+ '<p class="hg-empty" hidden>No signed works hang here yet. A page an artist tweaks, or a design that ships with its picture, hangs here once it ships.</p>' +
112
+ '<button type="button" class="hg-edge hg-prev" aria-label="Previous work"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 4 7 12l8 8"/></svg></button>' +
113
+ '<button type="button" class="hg-edge hg-next" aria-label="Next work"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4l8 8-8 8"/></svg></button>' +
114
+ '<div class="hg-count" aria-live="polite"></div>' +
115
+ '<div class="hg-cap" aria-hidden="true"></div>' +
116
+ '<button type="button" class="hg-fsbtn" aria-label="Full screen: only the art (F)"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10"/></svg></button>' +
117
+ '<div class="hg-bench" aria-hidden="true"></div>' +
118
+ '</div>';
119
+ host.appendChild(root);
120
+ var ROOM_LABEL = 'The gallery: one work at a time. Use the left and right arrow keys, or swipe. F for full screen.';
121
+ var ALL_LABEL = 'The full gallery: every work at once. Choose one to sit in front of it.';
122
+ var room = root.querySelector('.hg-room'), track = root.querySelector('.hg-track'), salon = root.querySelector('.hg-salon');
123
+ var prev = root.querySelector('.hg-prev'), next = root.querySelector('.hg-next'), count = root.querySelector('.hg-count');
124
+ var wayAll = root.querySelector('.hg-way-all'), empty = root.querySelector('.hg-empty');
125
+ room.setAttribute('aria-label', ROOM_LABEL);
126
+
127
+ function hang(w, i) {
128
+ var b = brand(), title = fillHoles(w.title, b) || 'Untitled';
129
+ var sec = el('section', 'hg-hang', { 'aria-label': title + ', by ' + w.artist.name });
130
+ var work = el('div', 'hg-work');
131
+ var canvas = el('div', 'hg-canvas');
132
+ canvas.dataset.i = String(i);
133
+ work.appendChild(canvas);
134
+ var label = el('div', 'hg-label');
135
+ var text = el('div', 'hg-text', { role: 'button', tabindex: '0', 'aria-expanded': 'false', 'aria-label': 'Wall label for ' + title + '. Click to read it closer.' });
136
+ text.appendChild(el('b', 'hg-who', null, w.artist.name));
137
+ text.appendChild(el('span', 'hg-bio', null, bio(w.artist.since)));
138
+ var t = el('span', 'hg-title');
139
+ t.appendChild(el('i', null, null, title));
140
+ t.appendChild(document.createTextNode(', ' + year(w.shipped)));
141
+ text.appendChild(t);
142
+ text.appendChild(el('span', 'hg-medium', null, w.medium || ''));
143
+ if (w.place) text.appendChild(el('span', 'hg-place', null, fillHoles(w.place, b)));
144
+ if (w.words) text.appendChild(el('p', 'hg-words', null, w.words));
145
+ text.appendChild(el('p', 'hg-credit', null, credit(w)));
146
+ var sig = el('div', 'hg-sigline');
147
+ sig.appendChild(el('span', 'hg-sig', null, signature(w.artist.name, w.shipped)));
148
+ var heart = el('button', 'hg-heart', { type: 'button', 'data-id': w.id, 'aria-pressed': String(!!w.mine), 'aria-label': 'Appreciate ' + title });
149
+ heart.innerHTML = HEART;
150
+ sig.appendChild(heart);
151
+ label.append(text, sig);
152
+ work.appendChild(label);
153
+ sec.appendChild(work);
154
+ return sec;
155
+ }
156
+ // only the work in view and its neighbours load their picture
157
+ function hangNear() {
158
+ var m = mode();
159
+ track.querySelectorAll('.hg-canvas').forEach(function (c) {
160
+ var i = Number(c.dataset.i), w = list[i];
161
+ if (Math.abs(i - at) > 1 || !w) return;
162
+ var src = artFor(w, m), img = c.querySelector('img');
163
+ if (!src) return;
164
+ if (!img) { img = el('img', null, { alt: w.art && w.art.alt ? w.art.alt : '', loading: 'lazy', decoding: 'async' }); c.appendChild(img); }
165
+ if (img.getAttribute('src') !== src) img.setAttribute('src', src);
166
+ });
167
+ }
168
+ function render() {
169
+ track.textContent = '';
170
+ list.forEach(function (w, i) { track.appendChild(hang(w, i)); });
171
+ empty.hidden = list.length > 0;
172
+ room.classList.toggle('is-empty', list.length === 0);
173
+ go(0, true);
174
+ if (room.classList.contains('is-all')) renderSalon();
175
+ }
176
+ function go(i, instant) {
177
+ at = Math.max(0, Math.min(list.length - 1, i));
178
+ if (at < 0) at = 0;
179
+ if (instant) track.classList.add('is-drag');
180
+ track.style.transform = 'translateX(' + (-at * 100) + '%)';
181
+ if (instant) { void track.offsetWidth; track.classList.remove('is-drag'); }
182
+ prev.disabled = at <= 0;
183
+ next.disabled = at >= list.length - 1;
184
+ count.textContent = list.length ? (at + 1) + ' of ' + list.length : '';
185
+ track.querySelectorAll('.hg-hang.is-reading').forEach(function (h) { read(h, false); });
186
+ var w = list[at];
187
+ root.querySelector('.hg-cap').textContent = w ? fillHoles(w.title, brand()) + ' · ' + w.artist.name : '';
188
+ track.querySelectorAll('.hg-hang').forEach(function (h, n) { h.setAttribute('aria-hidden', String(n !== at)); h.inert = n !== at; });
189
+ hangNear();
190
+ }
191
+ function read(h, on) {
192
+ h.classList.toggle('is-reading', on);
193
+ h.querySelector('.hg-text').setAttribute('aria-expanded', String(on));
194
+ }
195
+
196
+ // ---- appreciation: one heart, the viewer's own; the count lives in the credit line ----
197
+ function paintHeart(w) {
198
+ root.querySelectorAll('.hg-heart[data-id="' + w.id + '"]').forEach(function (b) { b.setAttribute('aria-pressed', String(!!w.mine)); });
199
+ track.querySelectorAll('.hg-hang').forEach(function (h, n) { if (list[n] === w) h.querySelector('.hg-credit').textContent = credit(w); });
200
+ salon.querySelectorAll('.hg-piece[data-id="' + w.id + '"] .hg-pn').forEach(function (n) { n.textContent = String(w.hearts); });
201
+ }
202
+ function toggleHeart(id) {
203
+ var w = list.filter(function (x) { return x.id === id; })[0];
204
+ if (!w || !api || w.busy) return;
205
+ var on = !w.mine, was = { mine: w.mine, hearts: w.hearts };
206
+ w.busy = true; w.mine = on; w.hearts = Math.max(0, w.hearts + (on ? 1 : -1));
207
+ paintHeart(w);
208
+ api.request('PUT', API + '/home/gallery/' + encodeURIComponent(id) + '/heart', { body: { on: on } }).then(function (res) {
209
+ if (res && res.ok && res.data) { w.mine = !!res.data.mine; w.hearts = Number(res.data.hearts) || 0; } else { w.mine = was.mine; w.hearts = was.hearts; }
210
+ }, function () { w.mine = was.mine; w.hearts = was.hearts; }).then(function () { w.busy = false; paintHeart(w); });
211
+ }
212
+
213
+ // ---- the full gallery: every work at once, salon-style; click one to sit in front of it ----
214
+ function renderSalon() {
215
+ salon.textContent = '';
216
+ var m = mode(), b = brand();
217
+ list.forEach(function (w, i) {
218
+ var title = fillHoles(w.title, b) || 'Untitled';
219
+ var item = el('div', null, { role: 'listitem' });
220
+ var p = el('button', 'hg-piece', { type: 'button', 'data-i': String(i), 'data-id': w.id, 'aria-label': title + ', by ' + w.artist.name + '. Sit in front of it.' });
221
+ var pc = el('span', 'hg-pc');
222
+ var src = artFor(w, m);
223
+ if (src) pc.appendChild(el('img', null, { src: src, alt: '', loading: 'lazy', decoding: 'async' }));
224
+ var pt = el('span', 'hg-pt');
225
+ pt.appendChild(el('i', null, null, title));
226
+ pt.appendChild(document.createTextNode(' · ' + w.artist.name));
227
+ var pl = el('span', 'hg-pl');
228
+ pl.innerHTML = HEART;
229
+ pl.appendChild(el('span', 'hg-pn', null, String(w.hearts)));
230
+ p.append(pc, pt, pl);
231
+ item.appendChild(p);
232
+ salon.appendChild(item);
233
+ });
234
+ }
235
+ function setAll(on) {
236
+ room.classList.toggle('is-all', on);
237
+ salon.hidden = !on;
238
+ wayAll.setAttribute('aria-pressed', String(on));
239
+ wayAll.firstChild.textContent = on ? 'Back to the bench ' : 'The full gallery ';
240
+ room.setAttribute('aria-label', on ? ALL_LABEL : ROOM_LABEL);
241
+ if (on) renderSalon();
242
+ }
243
+
244
+ // ---- full screen: the art alone, with its signature and heart ----
245
+ function isFs() { return document.fullscreenElement === room || room.classList.contains('is-fs'); }
246
+ function setFs(on) {
247
+ if (on) {
248
+ if (room.requestFullscreen) room.requestFullscreen().catch(function () { room.classList.add('is-fs'); });
249
+ else room.classList.add('is-fs');
250
+ } else {
251
+ if (document.fullscreenElement && document.exitFullscreen) document.exitFullscreen();
252
+ room.classList.remove('is-fs');
253
+ }
254
+ }
255
+ document.addEventListener('fullscreenchange', function () { if (document.fullscreenElement !== room) room.classList.remove('is-fs'); });
256
+
257
+ // ---- wiring ----
258
+ prev.addEventListener('click', function () { go(at - 1); });
259
+ next.addEventListener('click', function () { go(at + 1); });
260
+ root.querySelector('.hg-fsbtn').addEventListener('click', function () { setFs(!isFs()); });
261
+ wayAll.addEventListener('click', function () { setAll(!room.classList.contains('is-all')); });
262
+ root.querySelectorAll('.hg-sort button').forEach(function (b) {
263
+ b.addEventListener('click', function () {
264
+ if (order === b.dataset.order) return;
265
+ order = b.dataset.order;
266
+ root.querySelectorAll('.hg-sort button').forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });
267
+ load();
268
+ });
269
+ });
270
+ room.addEventListener('keydown', function (e) {
271
+ if (e.target.closest('input, textarea')) return;
272
+ if (e.key === 'ArrowLeft') { go(at - 1); e.preventDefault(); }
273
+ else if (e.key === 'ArrowRight') { go(at + 1); e.preventDefault(); }
274
+ else if ((e.key === 'f' || e.key === 'F') && !e.metaKey && !e.ctrlKey && !e.altKey) { setFs(!isFs()); e.preventDefault(); }
275
+ else if (e.key === 'Escape') { if (isFs()) setFs(false); track.querySelectorAll('.hg-hang.is-reading').forEach(function (h) { read(h, false); }); }
276
+ });
277
+ track.addEventListener('click', function (e) {
278
+ var h = e.target.closest('.hg-heart');
279
+ if (h) { toggleHeart(h.dataset.id); return; }
280
+ var t = e.target.closest('.hg-text');
281
+ if (t) { var hang = t.closest('.hg-hang'); read(hang, !hang.classList.contains('is-reading')); }
282
+ });
283
+ track.addEventListener('keydown', function (e) {
284
+ var t = e.target.closest('.hg-text');
285
+ if (t && (e.key === 'Enter' || e.key === ' ')) { var hang = t.closest('.hg-hang'); read(hang, !hang.classList.contains('is-reading')); e.preventDefault(); }
286
+ });
287
+ salon.addEventListener('click', function (e) {
288
+ var p = e.target.closest('.hg-piece'); if (!p) return;
289
+ setAll(false); go(Number(p.dataset.i), true); room.focus();
290
+ });
291
+ // swipe or drag along the wall
292
+ var x0 = null, dx = 0, W = 1;
293
+ room.addEventListener('pointerdown', function (e) {
294
+ if (room.classList.contains('is-all') || e.target.closest('button, a, .hg-text') || !list.length) return;
295
+ x0 = e.clientX; dx = 0; W = room.clientWidth || 1; track.classList.add('is-drag');
296
+ if (room.setPointerCapture) try { room.setPointerCapture(e.pointerId); } catch (_) { /* a synthetic pointer */ }
297
+ });
298
+ room.addEventListener('pointermove', function (e) {
299
+ if (x0 === null) return;
300
+ dx = e.clientX - x0;
301
+ track.style.transform = 'translateX(calc(' + (-at * 100) + '% + ' + dx + 'px))';
302
+ });
303
+ function end() {
304
+ if (x0 === null) return;
305
+ track.classList.remove('is-drag');
306
+ var step = Math.abs(dx) > W * 0.12 ? (dx < 0 ? 1 : -1) : 0;
307
+ x0 = null; go(at + step);
308
+ }
309
+ room.addEventListener('pointerup', end);
310
+ room.addEventListener('pointercancel', end);
311
+ new MutationObserver(function () { hangNear(); if (room.classList.contains('is-all')) renderSalon(); })
312
+ .observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
313
+
314
+ function load() {
315
+ api = api || (window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false }));
316
+ if (!api) return;
317
+ var mine = ++asking;
318
+ loaded = true;
319
+ api.request('GET', API + '/home/gallery?order=' + order).then(function (res) {
320
+ if (mine !== asking) return;
321
+ list = res && res.ok && res.data && Array.isArray(res.data.works) ? res.data.works.filter(function (w) { return w && w.id && w.artist; }) : [];
322
+ render();
323
+ if (first) { first = false; arrive(); }
324
+ }, function () { if (mine === asking) { list = []; render(); } });
325
+ }
326
+
327
+ // ---- arriving from the studio (/studio, task 1005061): `?work=<id>` sits you in
328
+ // front of that work, `?gallery=all` opens the full gallery; read once, then the
329
+ // address is put back so a reload is an ordinary Home ----
330
+ function arrive() {
331
+ var want = landing(window.location.search);
332
+ if (!want.work && !want.all) return;
333
+ if (want.all) setAll(true);
334
+ else { var i = -1; list.forEach(function (w, n) { if (i < 0 && w.id === want.work) i = n; }); if (i >= 0) go(i, true); }
335
+ try { window.history.replaceState(null, '', window.location.pathname + window.location.hash); } catch (_) { /* an opaque page */ }
336
+ }
337
+
338
+ return {
339
+ // shown in Artistry; the wall is asked for the first time it is shown
340
+ show: function (on) {
341
+ root.hidden = !on;
342
+ if (on && !loaded) load();
343
+ if (!on && isFs()) setFs(false);
344
+ },
345
+ };
346
+ }
347
+
348
+ window.OTBHomeGallery = { attach: attach, lib: Lib };
349
+ })();
@@ -0,0 +1,72 @@
1
+ /* modules/hall-ui/public/home-studio.css — the artist's studio, behind the gallery
2
+ (task 1005061; mock docs/design/mocks/hall/home/v16-studio.html, record
3
+ modules/hall-ui/records/home-city.md § "The studio"). The gallery's wall, lit from a
4
+ north window, nothing loud. Every colour is a hall token or a mix of one; the frame
5
+ and shadows are black with alpha. The tokens carry both modes. No :root; no motion. */
6
+
7
+ .hs {
8
+ --hs-wall: color-mix(in srgb, var(--bg-card) 94%, var(--accent));
9
+ --hs-wall-low: color-mix(in srgb, var(--bg) 90%, var(--accent-deep));
10
+ --hs-window: color-mix(in srgb, var(--sea) 9%, transparent);
11
+ --hs-line: color-mix(in srgb, var(--ink) 11%, transparent);
12
+ --hg-frame: rgba(0, 0, 0, .82);
13
+ --hg-shadow: rgba(0, 0, 0, .45);
14
+ position: relative; padding: 40px 44px 44px; color: var(--ink);
15
+ background: radial-gradient(ellipse 60% 70% at 0% 0%, var(--hs-window), transparent 70%), linear-gradient(var(--hs-wall), var(--hs-wall-low));
16
+ }
17
+ .hs[hidden] { display: none; }
18
+ .hs-top { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 12px 24px; margin-bottom: 30px; }
19
+ .hs-ways { margin-left: auto; display: flex; flex-wrap: wrap; gap: 4px 22px; }
20
+ .hs-h { margin: 0 0 14px; font-size: 11px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-soft); }
21
+ .hs-grid { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 44px; }
22
+ .hs-below { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; margin-top: 40px; }
23
+
24
+ /* the easel */
25
+ .hs-art { border-width: 3px; }
26
+ .hs-blank { position: absolute; inset: 0; display: grid; place-items: center; font-size: 12px; color: var(--ink-faint); }
27
+ .hs-what { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 16px; margin-top: 16px; }
28
+ .hs-what__t { min-width: 0; flex: 1; }
29
+ .hs-what b { display: block; font-size: 15px; font-weight: 600; }
30
+ .hs-line { display: block; font-size: 12.5px; color: var(--ink-soft); }
31
+ .hs-what .hg-sig { display: inline-block; margin-top: 6px; }
32
+ .hs-pill { display: inline-flex; align-items: center; min-height: 46px; padding: 0 22px; border-radius: 999px; background: var(--accent); color: var(--accent-on, var(--ink)); font-weight: 600; font-size: 14px; text-decoration: none; white-space: nowrap; }
33
+ .hs-pill[hidden] { display: none; }
34
+ .hs-pill:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 3px; }
35
+
36
+ /* the quiet lists beside it */
37
+ .hs-shelf { margin-bottom: 34px; }
38
+ .hs-row { display: flex; align-items: center; gap: 12px; min-height: 24px; padding: 11px 0; border-top: 1px solid var(--hs-line); color: inherit; text-decoration: none; }
39
+ .hs-row:first-child { border-top: 0; padding-top: 0; }
40
+ .hs-row .t { flex: 1; min-width: 0; }
41
+ .hs-row .t b { display: block; font-size: 13.5px; font-weight: 600; }
42
+ .hs-row .t span { display: block; font-size: 12px; color: var(--ink-soft); }
43
+ .hs-row .t q { display: block; font-size: 13px; font-style: italic; quotes: "“" "”"; }
44
+ a.hs-row:hover .t b, a.hs-row:hover .t q { text-decoration: underline; text-underline-offset: 3px; }
45
+ a.hs-row:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 3px; }
46
+ .hs-dot { flex: none; min-width: 24px; height: 24px; border-radius: 12px; display: grid; place-items: center; padding: 0 7px; box-sizing: border-box; font-size: 11px; font-weight: 700; background: var(--accent); color: var(--accent-on, var(--ink)); }
47
+ .hs-quiet, .hs-more { margin: 0; font-size: 12.5px; color: var(--ink-soft); }
48
+ .hs-more { margin-top: 10px; }
49
+ .hs-desk { margin-top: 14px; }
50
+
51
+ /* small hung works */
52
+ .hs-works { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; align-items: start; }
53
+ .hs-work { all: unset; cursor: pointer; display: block; min-width: 0; }
54
+ .hs-work:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 6px; }
55
+ .hs-work:hover .hg-pc { transform: translateY(-3px); }
56
+ .hs-cap { display: block; margin-top: 7px; font-size: 11px; }
57
+ .hs-sub { display: flex; align-items: center; gap: 5px; margin-top: 1px; font-size: 10.5px; color: var(--ink-soft); }
58
+ .hs-sub svg { width: 11px; height: 11px; }
59
+ .hs-works .hs-quiet { grid-column: 1 / -1; }
60
+
61
+ @media (max-width: 900px) {
62
+ .hs { padding: 24px 20px; }
63
+ .hs-grid, .hs-below { grid-template-columns: 1fr; gap: 30px; }
64
+ .hs-ways { margin-left: 0; }
65
+ .hs-what__t { flex-basis: 100%; }
66
+ .hs-works { grid-template-columns: repeat(2, minmax(0, 1fr)); }
67
+ }
68
+
69
+ /* on the /studio page, under its illustrated room */
70
+ .hs-page { padding: 24px; }
71
+ .hs-page .hs { border-radius: var(--radius); }
72
+ @media (max-width: 900px) { .hs-page { padding: 12px; } }
@@ -0,0 +1,203 @@
1
+ // modules/hall-ui/public/home-studio.js — the artist's studio, behind the gallery
2
+ // (task 1005061, Home v16 step 4; mock docs/design/mocks/hall/home/v16-studio.html,
3
+ // record modules/hall-ui/records/home-city.md § "The studio").
4
+ //
5
+ // "As an artist you look at other works to get inspiration, and then you are in your
6
+ // studio, where you are at peace and can think best." The studio is the hall's /studio
7
+ // page — the sidebar's Studio, and the gallery's "Your studio" door — and these five
8
+ // shelves sit there under its illustrated room, in the gallery's colours: nothing loud,
9
+ // nothing due. Its doors lead back into Home's gallery (`/?gallery=all`, `/?work=<id>`,
10
+ // read by home-gallery.js).
11
+ // · On your easel — the page round you hold, and the one way back into it (the tweak
12
+ // editor); with none, the page the Tweak/CopyWrite Flow would hand you next.
13
+ // · Waiting for new inspiration — pages whose words changed since an artist last
14
+ // designed them, a quiet count of changed lines each.
15
+ // · Words to tune — the newest lines the copy desk has flagged.
16
+ // · Pinned from the gallery — the works you appreciated on the bench.
17
+ // · Your works in the collection — what you signed, and how often it was appreciated.
18
+ // No ranks and no leaderboard.
19
+ //
20
+ // Two bounded reads: GET /copy-desk/studio (the easel and the two copy-desk shelves;
21
+ // copy-desk is a module, and without it those shelves say so), then
22
+ // GET /home/studio/works?page=<the easel's page> (the two shelves of works, and the
23
+ // easel's page as it hangs on the wall). It writes nothing. Ledger text
24
+ // lands as text, never markup; a picture comes only through the gallery's own rule.
25
+ (function () {
26
+ 'use strict';
27
+
28
+ var API = '/api/bongos';
29
+ var HEART = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13.6S2 10 2 6a3 3 0 0 1 6-1 3 3 0 0 1 6 1c0 4-6 7.6-6 7.6Z" fill="currentColor"/></svg>';
30
+
31
+ // ---- pure: what the easel says (node-testable) ----
32
+ function easelWords(easel) {
33
+ if (!easel) return { title: 'Nothing on your easel', line: 'No page is waiting for an artist at the moment.', act: null };
34
+ if (easel.held) {
35
+ return {
36
+ title: easel.title + (easel.round > 1 ? ' · round ' + easel.round : ''),
37
+ line: 'Your page, waiting for your words' + (easel.place ? ' · ' + easel.place : '') + '.',
38
+ act: 'Pick up where you left off',
39
+ };
40
+ }
41
+ return {
42
+ title: 'Nothing on your easel',
43
+ line: 'Next up: ' + easel.title + (easel.why ? ', ' + easel.why : '') + '.',
44
+ act: 'Begin ' + easel.title,
45
+ };
46
+ }
47
+ function linesChanged(n) { return n === 1 ? '1 line changed' : n + ' lines changed'; }
48
+ // only a path inside the hall: the editor and the copy desk
49
+ function hallPath(p) { return typeof p === 'string' && /^\/(tweak-editor|copy-desk)(\?[A-Za-z0-9_=:&%.-]*)?$/.test(p) ? p : null; }
50
+ // where a work hangs in Home's gallery
51
+ function benchPath(id) { return /^\d{1,12}$/.test(String(id)) ? '/?work=' + id : '/'; }
52
+
53
+ var Lib = { easelWords: easelWords, linesChanged: linesChanged, hallPath: hallPath, benchPath: benchPath };
54
+ if (typeof module !== 'undefined' && module.exports) module.exports = Lib;
55
+ if (typeof window === 'undefined') return;
56
+
57
+ function el(tag, cls, attrs, text) {
58
+ var e = document.createElement(tag);
59
+ if (cls) e.className = cls;
60
+ if (attrs) Object.keys(attrs).forEach(function (k) { e.setAttribute(k, attrs[k]); });
61
+ if (text != null) e.textContent = text;
62
+ return e;
63
+ }
64
+
65
+ function mode() {
66
+ var t = document.documentElement.dataset.theme;
67
+ if (t === 'light' || t === 'dark') return t;
68
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
69
+ }
70
+ // the gallery's label words and picture rule (home-gallery.js, loaded on this page too)
71
+ function G() { return (window.OTBHomeGallery && window.OTBHomeGallery.lib) || null; }
72
+ function art(w) { var g = G(); return g ? g.artFor(w, mode()) : null; }
73
+ function words(s) { var g = G(); var b = window.__BRANDING__ || {}; return g ? g.fillHoles(s, { productName: b.productName || '', tagline: b.tagline || '' }) : String(s || ''); }
74
+
75
+ function attach(parent) {
76
+ var api = window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
77
+ var root = el('div', 'hs');
78
+ root.innerHTML =
79
+ '<div class="hs-top">' +
80
+ '<div><h2 class="hg-h" id="hs-h">Your studio</h2><p class="hg-sub">Quiet. Nothing here is due.</p></div>' +
81
+ '<nav class="hs-ways" aria-label="Elsewhere in the museum">' +
82
+ '<a class="hg-way hs-back" href="/"><span aria-hidden="true">←</span> The bench</a>' +
83
+ '<a class="hg-way hs-all" href="/?gallery=all">The full gallery</a>' +
84
+ '</nav>' +
85
+ '</div>' +
86
+ '<div class="hs-grid">' +
87
+ '<section class="hs-easel" aria-labelledby="hs-easel-h"><h3 id="hs-easel-h" class="hs-h">On your easel</h3>' +
88
+ '<div class="hs-art hg-canvas"></div>' +
89
+ '<div class="hs-what"><div class="hs-what__t"><b></b><span class="hs-line"></span><span class="hg-sig"></span></div><a class="hs-pill" hidden></a></div>' +
90
+ '</section>' +
91
+ '<div class="hs-side">' +
92
+ '<section class="hs-shelf" aria-labelledby="hs-wait-h"><h3 id="hs-wait-h" class="hs-h">Waiting for new inspiration</h3><div class="hs-rows hs-waiting"></div></section>' +
93
+ '<section class="hs-shelf" aria-labelledby="hs-words-h"><h3 id="hs-words-h" class="hs-h">Words to tune</h3><div class="hs-rows hs-words"></div>' +
94
+ '<a class="hg-way hs-desk" href="/copy-desk">The copy desk <span aria-hidden="true">→</span></a></section>' +
95
+ '</div>' +
96
+ '</div>' +
97
+ '<div class="hs-below">' +
98
+ '<section aria-labelledby="hs-pin-h"><h3 id="hs-pin-h" class="hs-h">Pinned from the gallery</h3><div class="hs-works hs-pinned"></div></section>' +
99
+ '<section aria-labelledby="hs-yours-h"><h3 id="hs-yours-h" class="hs-h">Your works in the collection</h3><div class="hs-works hs-yours"></div></section>' +
100
+ '</div>';
101
+ parent.appendChild(root);
102
+ var asking = 0;
103
+
104
+ function quiet(box, text) { box.textContent = ''; box.appendChild(el('p', 'hs-quiet', null, text)); }
105
+
106
+ function paintEasel(easel, works) {
107
+ var w = easelWords(easel);
108
+ root.querySelector('.hs-what b').textContent = w.title;
109
+ root.querySelector('.hs-line').textContent = w.line;
110
+ var pill = root.querySelector('.hs-pill'), to = easel && hallPath(easel.open);
111
+ pill.hidden = !(w.act && to);
112
+ if (w.act && to) { pill.textContent = w.act; pill.setAttribute('href', to); }
113
+ // the page as it hangs now: its newest work on the wall, if it has one
114
+ var frame = root.querySelector('.hs-art'), hung = easel && works && works.easel;
115
+ frame.textContent = '';
116
+ var src = hung && art(hung);
117
+ if (src) frame.appendChild(el('img', null, { src: src, alt: '', loading: 'lazy', decoding: 'async' }));
118
+ else frame.appendChild(el('span', 'hs-blank', null, easel ? 'No picture of this page hangs yet' : ''));
119
+ var me = works && works.yours && works.yours.works[0], g = G();
120
+ root.querySelector('.hs-what .hg-sig').textContent = me && g ? g.signature(me.artist.name, '') : '';
121
+ }
122
+ function paintWaiting(waiting) {
123
+ var box = root.querySelector('.hs-waiting');
124
+ if (!waiting) return quiet(box, 'The copy desk is not switched on in this project.');
125
+ if (!waiting.pages.length) return quiet(box, 'Nothing is waiting. Every page’s words match its last design.');
126
+ box.textContent = '';
127
+ waiting.pages.forEach(function (p) {
128
+ var to = hallPath(p.open);
129
+ var row = el(to ? 'a' : 'div', 'hs-row', to ? { href: to } : null);
130
+ var t = el('span', 't');
131
+ t.appendChild(el('b', null, null, p.title));
132
+ t.appendChild(el('span', null, null, (p.place ? p.place + ' · ' : '') + linesChanged(p.changed_lines)));
133
+ row.append(t, el('span', 'hs-dot', { 'aria-hidden': 'true' }, String(p.changed_lines)));
134
+ box.appendChild(row);
135
+ });
136
+ var more = waiting.total - waiting.pages.length;
137
+ if (more > 0) box.appendChild(el('p', 'hs-more', null, more === 1 ? 'And 1 more page changed since its last design' : 'And ' + more + ' more pages changed since their last design'));
138
+ }
139
+ function paintWords(words) {
140
+ var box = root.querySelector('.hs-words');
141
+ root.querySelector('.hs-desk').hidden = !words;
142
+ if (!words) return quiet(box, 'The copy desk is not switched on in this project.');
143
+ if (!words.length) return quiet(box, 'No words are flagged. The copy desk is quiet.');
144
+ box.textContent = '';
145
+ words.forEach(function (f) {
146
+ var row = el('a', 'hs-row', { href: '/copy-desk' });
147
+ var t = el('span', 't');
148
+ if (f.text) t.appendChild(el('q', null, null, f.text));
149
+ t.appendChild(el(f.text ? 'span' : 'b', null, null, f.text ? f.reason + (f.place ? ' · ' + f.place : '') : f.reason));
150
+ row.appendChild(t);
151
+ box.appendChild(row);
152
+ });
153
+ }
154
+ function paintShelf(box, shelf, mine) {
155
+ if (!shelf) return quiet(box, 'The collection could not be read. Try again in a moment.');
156
+ if (!shelf.works.length) return quiet(box, mine ? 'Nothing of yours hangs in the collection yet.' : 'Nothing pinned yet. A heart on the bench pins a work here.');
157
+ box.textContent = '';
158
+ shelf.works.forEach(function (w) {
159
+ var title = words(w.title) || 'Untitled';
160
+ var b = el('a', 'hs-work', { href: benchPath(w.id), 'data-id': w.id, 'aria-label': title + (mine ? '' : ', by ' + w.artist.name) + '. See it on the bench.' });
161
+ var pc = el('span', 'hg-pc');
162
+ var src = art(w);
163
+ if (src) pc.appendChild(el('img', null, { src: src, alt: '', loading: 'lazy', decoding: 'async' }));
164
+ var cap = el('span', 'hs-cap');
165
+ cap.appendChild(el('i', null, null, title));
166
+ if (!mine) cap.appendChild(document.createTextNode(' · ' + w.artist.name));
167
+ var sub = el('span', 'hs-sub');
168
+ if (mine) { sub.innerHTML = HEART; sub.appendChild(document.createTextNode(String(w.hearts))); } else sub.textContent = 'You appreciated this';
169
+ cap.appendChild(sub);
170
+ b.append(pc, cap);
171
+ box.appendChild(b);
172
+ });
173
+ }
174
+
175
+ function load() {
176
+ if (!api) return;
177
+ var mine = ++asking;
178
+ var get = function (p) { return api.request('GET', API + p).then(function (r) { return r && r.ok ? r.data : null; }, function () { return null; }); };
179
+ get('/copy-desk/studio').then(function (desk) {
180
+ var page = desk && desk.easel && desk.easel.page_id;
181
+ return get('/home/studio/works' + (page ? '?page=' + encodeURIComponent(page) : '')).then(function (works) { return [desk, works]; });
182
+ }).then(function (res) {
183
+ if (mine !== asking) return;
184
+ var desk = res[0], works = res[1];
185
+ paintEasel(desk && desk.easel, works);
186
+ paintWaiting(desk ? desk.waiting : null);
187
+ paintWords(desk ? desk.words : null);
188
+ paintShelf(root.querySelector('.hs-pinned'), works && works.pinned, false);
189
+ paintShelf(root.querySelector('.hs-yours'), works && works.yours, true);
190
+ });
191
+ }
192
+
193
+ // a picture follows the hall's light or dark
194
+ new MutationObserver(load).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
195
+ load();
196
+ return { reload: load };
197
+ }
198
+
199
+ window.OTBHomeStudio = { attach: attach, lib: Lib };
200
+ // the /studio page carries the host; mount once the page has parsed
201
+ function boot() { var host = document.getElementById('studio-shelves'); if (host && !host.firstChild) attach(host); }
202
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();
203
+ })();
@@ -53,7 +53,7 @@
53
53
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
54
54
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
55
55
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
56
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
56
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
57
57
  <script src="/builders/palette.js" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
59
59
  written inside an idea's own prose keeps resolving from this page. -->
@@ -228,7 +228,7 @@
228
228
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
229
229
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
230
230
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
231
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
231
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
232
232
  <script src="/builders/palette.js" defer></script>
233
233
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
234
234
  <script src="/builders/vote-control.js"></script>