@bongos/core 1.24.92 → 1.24.94
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 +53 -33
- package/docs/adr/0404-a-party-is-a-room-and-rooms-register-as-kinds.md +118 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +1 -1
- package/docs/copy-registry.json +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +2 -0
- package/docs/page-inventory.json +4 -2
- package/docs/page-readings.json +7 -7
- package/modules/hall-ui/public/city.js +1 -1
- package/modules/hall-ui/public/goal-map.html +2 -0
- package/modules/hall-ui/public/home-city.js +14 -3
- package/modules/hall-ui/public/home-gallery.css +6 -0
- package/modules/hall-ui/public/home-gallery.js +30 -0
- package/modules/hall-ui/public/sky-interaction.js +4 -9
- package/modules/hall-ui/public/sky-visor.js +96 -0
- package/modules/hall-ui/public/thinking.css +8 -0
- package/modules/hall-ui/public/thinking.html +2 -0
- package/modules/hall-ui/public/visor.js +2 -1
- package/modules/hall-ui/records/visor.md +17 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/src/module-api.js +1 -1
- package/src/module-seams.js +3 -0
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_sky_visor.mjs +150 -0
- package/tests/hall_visor.mjs +1 -1
- package/tests/home_gallery_widget.mjs +43 -0
- package/tests/room_kinds_seam.mjs +68 -0
|
@@ -122,6 +122,29 @@
|
|
|
122
122
|
var room = root.querySelector('.hg-room'), track = root.querySelector('.hg-track'), salon = root.querySelector('.hg-salon');
|
|
123
123
|
var prev = root.querySelector('.hg-prev'), next = root.querySelector('.hg-next'), count = root.querySelector('.hg-count');
|
|
124
124
|
var wayAll = root.querySelector('.hg-way-all'), empty = root.querySelector('.hg-empty');
|
|
125
|
+
var fsbtn = root.querySelector('.hg-fsbtn'), studio = root.querySelector('a.hg-way');
|
|
126
|
+
// task 1005211 (ADR 0403): in game mode the gallery's tabs, ways, arrows and full-screen
|
|
127
|
+
// button are the visor's (home-gallery.css hides them under html.visor-on). These are
|
|
128
|
+
// their rows, each pressing the gallery's own control, in the control's own words; Home
|
|
129
|
+
// (home-city.js) registers them as the scene's actions and asks again when they change.
|
|
130
|
+
var changed = function () {};
|
|
131
|
+
function visorActions() {
|
|
132
|
+
if (root.hidden) return [];
|
|
133
|
+
var all = room.classList.contains('is-all');
|
|
134
|
+
var out = [{ label: wayAll.firstChild.textContent.trim(), key: 'g', bind: true, lead: true, run: function () { wayAll.click(); room.focus(); } }];
|
|
135
|
+
root.querySelectorAll('.hg-sort button').forEach(function (b) {
|
|
136
|
+
out.push({ label: b.textContent + (b.getAttribute('aria-pressed') === 'true' ? ' · on' : ' · off'), run: function () { b.click(); } });
|
|
137
|
+
});
|
|
138
|
+
// the wall's own keys answer once the room has the keyboard, so a row hands it over
|
|
139
|
+
if (!all && list.length) {
|
|
140
|
+
out.push({ label: prev.getAttribute('aria-label'), key: '←', run: function () { prev.click(); room.focus(); } });
|
|
141
|
+
out.push({ label: next.getAttribute('aria-label'), key: '→', run: function () { next.click(); room.focus(); } });
|
|
142
|
+
out.push({ label: 'Full screen', key: 'F', run: function () { fsbtn.click(); room.focus(); } });
|
|
143
|
+
}
|
|
144
|
+
var href = studio.getAttribute('href');
|
|
145
|
+
out.push({ label: studio.firstChild.textContent.trim(), run: function () { window.location.assign(href); } });
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
125
148
|
room.setAttribute('aria-label', ROOM_LABEL);
|
|
126
149
|
|
|
127
150
|
function hang(w, i) {
|
|
@@ -239,6 +262,7 @@
|
|
|
239
262
|
wayAll.firstChild.textContent = on ? 'Back to the bench ' : 'The full gallery ';
|
|
240
263
|
room.setAttribute('aria-label', on ? ALL_LABEL : ROOM_LABEL);
|
|
241
264
|
if (on) renderSalon();
|
|
265
|
+
changed();
|
|
242
266
|
}
|
|
243
267
|
|
|
244
268
|
// ---- full screen: the art alone, with its signature and heart ----
|
|
@@ -264,6 +288,7 @@
|
|
|
264
288
|
if (order === b.dataset.order) return;
|
|
265
289
|
order = b.dataset.order;
|
|
266
290
|
root.querySelectorAll('.hg-sort button').forEach(function (x) { x.setAttribute('aria-pressed', String(x === b)); });
|
|
291
|
+
changed();
|
|
267
292
|
load();
|
|
268
293
|
});
|
|
269
294
|
});
|
|
@@ -327,6 +352,7 @@
|
|
|
327
352
|
if (mine !== asking) return;
|
|
328
353
|
list = res && res.ok && res.data && Array.isArray(res.data.works) ? res.data.works.filter(function (w) { return w && w.id && w.artist; }) : [];
|
|
329
354
|
render();
|
|
355
|
+
changed();
|
|
330
356
|
if (first) { first = false; arrive(); }
|
|
331
357
|
}, function () { if (mine === asking) { list = []; render(); } });
|
|
332
358
|
}
|
|
@@ -348,7 +374,11 @@
|
|
|
348
374
|
root.hidden = !on;
|
|
349
375
|
if (on && !loaded) load();
|
|
350
376
|
if (!on && isFs()) setFs(false);
|
|
377
|
+
changed();
|
|
351
378
|
},
|
|
379
|
+
// task 1005211: the rows the visor shows for the gallery, and a hook for when they change
|
|
380
|
+
visorActions: visorActions,
|
|
381
|
+
onChange: function (fn) { changed = typeof fn === 'function' ? fn : function () {}; },
|
|
352
382
|
};
|
|
353
383
|
}
|
|
354
384
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// `filed_via` exists on at least one thought; until then it is hidden and
|
|
14
14
|
// every thought shows (the README's "must not ship" row).
|
|
15
15
|
//
|
|
16
|
-
// Keys: L the list, ? the tour, 0 fit, / the composer, Escape closes, arrows
|
|
16
|
+
// Keys: L the list, ? the tour, 0 fit, / the composer (J jumps, from the visor in game mode), Escape closes, arrows
|
|
17
17
|
// pan, + and - zoom.
|
|
18
18
|
|
|
19
19
|
(() => {
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
// An Escape the sky USED (the tour, the menu, a pick) calls preventDefault, so
|
|
76
76
|
// the shell's full-screen switch leaves that press alone (task 1004327).
|
|
77
77
|
addEventListener('keydown', (e) => {
|
|
78
|
-
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { if (e.key === 'Escape') e.target.blur(); return; }
|
|
78
|
+
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { if (e.key === 'Escape') { e.target.blur(); if (e.target.id === 'thought' || e.target.id === 'jump') { e.preventDefault(); if (SKY.closeField) SKY.closeField(e.target.id, true); } } return; }
|
|
79
79
|
const { cam } = S; const step = 60 / cam.k;
|
|
80
80
|
if (e.key === 'ArrowLeft') { cam.x -= step; S.dirty = true; } else if (e.key === 'ArrowRight') { cam.x += step; S.dirty = true; } else if (e.key === 'ArrowUp') { cam.y -= step; S.dirty = true; } else if (e.key === 'ArrowDown') { cam.y += step; S.dirty = true; }
|
|
81
81
|
else if (e.key === '+' || e.key === '=') { cam.k = clamp(cam.k * 1.3, .015, 9); S.dirty = true; } else if (e.key === '-') { cam.k = clamp(cam.k / 1.3, .015, 9); S.dirty = true; }
|
|
82
|
-
else if (e.key === '0') SKY.fitAll(); else if (e.key === 'Escape') { if (!$('tour').hidden) { e.preventDefault(); SKY.endTour(); } else if (!menu.hidden) { e.preventDefault(); hideMenu(); } else { if (S.picked || !$('panel').hidden) e.preventDefault(); pick(null); } } else if (e.key === '/') { e.preventDefault(); $('thought').focus(); }
|
|
82
|
+
else if (e.key === '0') SKY.fitAll(); else if (e.key === 'Escape') { if (!$('tour').hidden) { e.preventDefault(); SKY.endTour(); } else if (!menu.hidden) { e.preventDefault(); hideMenu(); } else { if (S.picked || !$('panel').hidden) e.preventDefault(); pick(null); } } else if (e.key === '/') { e.preventDefault(); if (SKY.openField) SKY.openField('thought'); else $('thought').focus(); }
|
|
83
83
|
else if (e.key === '?') SKY.startTour(); else if (e.key.toLowerCase() === 'l') SKY.toggleRoom(); else return;
|
|
84
84
|
});
|
|
85
85
|
function pick(h, fly) { if (h && h.kind === 'ltask') { SKY.renderPanel({ kind: 'ltask', ref: h.ref }); S.dirty = true; return; } S.picked = h; SKY.renderPanel(S.picked); SKY.renderRoom(); S.dirty = true; if (fly && S.picked && S.lens !== 'simple') { if (S.picked.kind === 'goal') SKY.fitGoal(S.picked.ref); else if (S.picked.kind === 'idea') SKY.fitThought(S.picked.ref); } }
|
|
@@ -148,10 +148,5 @@
|
|
|
148
148
|
|
|
149
149
|
Object.assign(SKY, { card, menu, glyphHtml, ideaTitle, sizeWords, showCard, showMenu, hideMenu, pick, setLens, togglePill, updateStats, altText, TAU, hash });
|
|
150
150
|
|
|
151
|
-
// task 1005148: the sky as the visor's scene
|
|
152
|
-
// "Back to centre" is the camera's own fitAll, and the keys listed are the ones bound above.
|
|
153
|
-
window.__visorScene = { name: 'sky', place: '', // the sky's own heading names the place
|
|
154
|
-
actions: [{ label: 'Back to centre', key: '0', run: () => SKY.fitAll() }],
|
|
155
|
-
keys: [{ label: 'Look around', key: 'drag' }, { label: 'List view', key: 'L' }, { label: 'Search', key: '/' }, { label: 'Take the tour', key: '?' }] };
|
|
156
|
-
if (window.OTBVisor) window.OTBVisor.scene(window.__visorScene);
|
|
151
|
+
// task 1005148 / 1005210: the sky as the visor's scene lives in sky-visor.js, after this file.
|
|
157
152
|
})();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// modules/hall-ui/public/sky-visor.js — THE SKY as the visor's scene (task 1005148; task
|
|
2
|
+
// 1005210, ADR 0403). Loaded after sky-interaction.js on /goal-map and /thinking.
|
|
3
|
+
//
|
|
4
|
+
// The visor owns no camera (ADR 0399 §2.2): every action here presses one of the sky's own
|
|
5
|
+
// controls or opens one of its own fields, so a pill does exactly what it did in classic mode.
|
|
6
|
+
(() => {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const SKY = window.OTBSky; const S = SKY.S;
|
|
10
|
+
const { $, on } = SKY;
|
|
11
|
+
|
|
12
|
+
// task 1005210 (ADR 0403): in game mode the visor is the control and the sky is the view. Its
|
|
13
|
+
// top row (the lens switch and every pill), the jump field and the thought composer leave the
|
|
14
|
+
// scene (thinking.css, html.visor-on) and come back as visor actions: writing a thought is the
|
|
15
|
+
// lead action, on the sky's own "/"; jumping is J; each pill is a row in the Actions sheet
|
|
16
|
+
// that presses the pill itself, so a pill does exactly what it did. The rows are read off the
|
|
17
|
+
// page's own buttons (their words, their pressed state, whether this lens shows them), and the
|
|
18
|
+
// scene registers again whenever one of those changes. Panning, zooming and picking a star
|
|
19
|
+
// stay in the sky. In classic mode nothing here shows or moves.
|
|
20
|
+
const FIELDS = ['thought', 'jump'];
|
|
21
|
+
function fieldWrap(id) { const el = $(id); return el && el.closest ? el.closest('.composer, .jump') : null; }
|
|
22
|
+
function openField(id) {
|
|
23
|
+
for (const other of FIELDS) if (other !== id) closeField(other, true);
|
|
24
|
+
const wrap = fieldWrap(id); if (wrap) wrap.classList.add('is-open');
|
|
25
|
+
$(id).focus();
|
|
26
|
+
}
|
|
27
|
+
// A field closes when it is left with nothing in it, or on Escape (its words are kept).
|
|
28
|
+
// The composer stays while a full idea is being asked or a receipt waits to be read.
|
|
29
|
+
function closeField(id, force) {
|
|
30
|
+
const wrap = fieldWrap(id); if (!wrap || !wrap.classList.contains('is-open')) return;
|
|
31
|
+
if (!force) {
|
|
32
|
+
if (wrap.contains(document.activeElement)) return;
|
|
33
|
+
if ($(id).value.trim()) return;
|
|
34
|
+
if (id === 'thought' && (!$('fullSteps').hidden || !$('receipt').hidden)) return;
|
|
35
|
+
}
|
|
36
|
+
wrap.classList.remove('is-open');
|
|
37
|
+
}
|
|
38
|
+
for (const id of FIELDS) {
|
|
39
|
+
const wrap = fieldWrap(id); if (!wrap) continue;
|
|
40
|
+
wrap.addEventListener('focusout', () => setTimeout(() => closeField(id), 0));
|
|
41
|
+
}
|
|
42
|
+
on('receiptX', 'click', () => setTimeout(() => closeField('thought'), 0));
|
|
43
|
+
// the sky binds these keys itself (sky-interaction.js); the rest are rows in the sheet
|
|
44
|
+
const OWN_KEYS = { roomBtn: 'L', tourBtn: '?', fitBtn: '0' };
|
|
45
|
+
function visible(el) { return !!el && !el.hidden && !(el.closest && el.closest('[hidden]')); }
|
|
46
|
+
function rowFor(b) {
|
|
47
|
+
const words = (b.textContent || '').trim();
|
|
48
|
+
const name = words.length > 1 ? words : (b.getAttribute('aria-label') || b.getAttribute('title') || '');
|
|
49
|
+
const pressed = b.getAttribute('aria-pressed');
|
|
50
|
+
const label = name.charAt(0).toUpperCase() + name.slice(1) + (pressed == null ? '' : pressed === 'true' ? ' · on' : ' · off');
|
|
51
|
+
return { label, key: OWN_KEYS[b.id] || '', run: () => b.click() };
|
|
52
|
+
}
|
|
53
|
+
// The foot's clock and legend are the sky's too: in game mode each shows only while the visor
|
|
54
|
+
// has it open (thinking.css), so the scene carries none of their buttons until asked.
|
|
55
|
+
function footOpen(id) { const el = $(id); return !!el && el.classList.contains('is-open'); }
|
|
56
|
+
function footRow(id, name) {
|
|
57
|
+
return { label: name + (footOpen(id) ? ' · on' : ' · off'), key: '', run: () => {
|
|
58
|
+
const el = $(id); if (!el) return;
|
|
59
|
+
const open = !el.classList.contains('is-open');
|
|
60
|
+
el.classList.toggle('is-open', open);
|
|
61
|
+
// a clock classic mode was told to hide comes back through its own pill
|
|
62
|
+
if (open && el.hidden && id === 'time') $('clockBtn').click();
|
|
63
|
+
} };
|
|
64
|
+
}
|
|
65
|
+
function skyScene() {
|
|
66
|
+
const actions = [{ label: 'Write a thought', key: '/', lead: true, run: () => openField('thought') }];
|
|
67
|
+
if (visible(fieldWrap('jump'))) actions.push({ label: 'Jump to a goal or a thought', key: 'j', bind: true, run: () => openField('jump') });
|
|
68
|
+
document.querySelectorAll('#lensGroup button, #tools button').forEach((b) => {
|
|
69
|
+
if (!visible(b)) return;
|
|
70
|
+
// the clock pill keeps classic mode's remembered choice; in game mode the clock opens from here
|
|
71
|
+
if (b.id === 'clockBtn') { actions.push(footRow('time', 'Clock')); return; }
|
|
72
|
+
actions.push(rowFor(b));
|
|
73
|
+
});
|
|
74
|
+
if (visible($('legend'))) actions.push(footRow('legend', 'Legend'));
|
|
75
|
+
return { name: 'sky', place: '', // the sky's own heading names the place
|
|
76
|
+
actions, keys: [{ label: 'Look around', key: 'drag' }, { label: 'Zoom', key: '+ −' }] };
|
|
77
|
+
}
|
|
78
|
+
let sceneSig = null;
|
|
79
|
+
function registerScene() {
|
|
80
|
+
const next = skyScene();
|
|
81
|
+
const sig = next.actions.map((a) => a.label + '|' + a.key).join('\n');
|
|
82
|
+
if (sig === sceneSig) return;
|
|
83
|
+
sceneSig = sig;
|
|
84
|
+
window.__visorScene = next;
|
|
85
|
+
if (window.OTBVisor) window.OTBVisor.scene(next);
|
|
86
|
+
}
|
|
87
|
+
// a pill's pressed state or a lens's set of pills changed: the sheet follows
|
|
88
|
+
if (typeof MutationObserver === 'function') {
|
|
89
|
+
const watch = new MutationObserver(registerScene);
|
|
90
|
+
for (const id of ['tools', 'lensGroup']) { const el = $(id); if (el) watch.observe(el, { subtree: true, attributes: true, attributeFilter: ['aria-pressed', 'hidden'] }); }
|
|
91
|
+
const jump = fieldWrap('jump'); if (jump) watch.observe(jump, { attributes: true, attributeFilter: ['hidden'] });
|
|
92
|
+
for (const id of ['time', 'legend']) { const el = $(id); if (el) watch.observe(el, { attributes: true, attributeFilter: ['class', 'hidden'] }); }
|
|
93
|
+
}
|
|
94
|
+
Object.assign(SKY, { openField, closeField, skyScene });
|
|
95
|
+
registerScene();
|
|
96
|
+
})();
|
|
@@ -315,3 +315,11 @@ html.visor-on .composer, html.visor-on .jump { top: calc(140px + env(safe-area-i
|
|
|
315
315
|
html.visor-on .panel, html.visor-on .room { top: calc(204px + env(safe-area-inset-top, 0px)); bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
|
|
316
316
|
html.visor-on .foot { padding-bottom: calc(132px + env(safe-area-inset-bottom, 0px)); }
|
|
317
317
|
html.visor-on .tour, html.visor-on .start { bottom: calc(210px + env(safe-area-inset-bottom, 0px)); }
|
|
318
|
+
/* task 1005210 (ADR 0403): with the visor up the sky carries no control of its own. The top
|
|
319
|
+
row's lens switch and pills are rows in the visor's Actions sheet; the composer and the jump
|
|
320
|
+
field open from the visor ("Write a thought" on /, "Jump" on J) and close again when left
|
|
321
|
+
empty (sky-interaction.js openField / closeField). The brand and its figures stay, as words. */
|
|
322
|
+
html.visor-on .top .tools, html.visor-on .top .lens { display: none; }
|
|
323
|
+
html.visor-on .composer:not(.is-open), html.visor-on .jump:not(.is-open) { display: none; }
|
|
324
|
+
html.visor-on .jump.is-open:not([hidden]) { display: block; }
|
|
325
|
+
html.visor-on .time:not(.is-open), html.visor-on .legend:not(.is-open) { display: none; }
|
|
@@ -159,5 +159,7 @@
|
|
|
159
159
|
<script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
|
|
160
160
|
<script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
|
|
161
161
|
<script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
|
|
162
|
+
<!-- task 1005210: the sky as the visor's scene (ADR 0403), after every other sky file -->
|
|
163
|
+
<script src="/builders/sky-visor.js"></script>
|
|
162
164
|
</body>
|
|
163
165
|
</html>
|
|
@@ -274,7 +274,8 @@
|
|
|
274
274
|
sheet.setAttribute('role', 'dialog');
|
|
275
275
|
sheet.setAttribute('aria-labelledby', 'visor-sheet-title');
|
|
276
276
|
sheet.innerHTML = sheetHtml(id);
|
|
277
|
-
sheet
|
|
277
|
+
// no listener of its own: the sheet sits inside #visor, whose click listener already
|
|
278
|
+
// sees its rows (a second one ran every row twice, so a toggle undid itself; task 1005210)
|
|
278
279
|
root.appendChild(sheet);
|
|
279
280
|
var btn = root.querySelector('[data-visor-sheet]');
|
|
280
281
|
if (btn) { btn.setAttribute('aria-expanded', 'true'); btn.hidden = true; }
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
# The visor — the first-person HUD over every scene (task 1005148, BONGOS-V3 R08; ADR 0399)
|
|
2
2
|
|
|
3
|
-
Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thinking`, and the three scene registrations (`sky-
|
|
3
|
+
Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thinking`, and the three scene registrations (`sky-visor.js`, `city.js`, `home-city.js`). Spec: `docs/specs/bongos-v3-game-conversion.md` C2 `you-are-a-character-in-first-person`. Game mode itself is in `game-mode.md`.
|
|
4
4
|
|
|
5
5
|
## The rule every game page keeps (ADR 0403, owner 2026-11-11)
|
|
6
6
|
|
|
7
|
-
The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet; the Governor City keeps it since task 1005209 (below).
|
|
7
|
+
The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet; the Governor City keeps it since task 1005209 the sky since task 1005210, and Home's gallery since task 1005211 (below).
|
|
8
8
|
|
|
9
9
|
## A place's own buttons on the glass (task 1005209, the Governor City first)
|
|
10
10
|
|
|
11
11
|
- **The scene contract grew two flags.** A scene action's `key` is still a key the scene binds itself, shown beside it in the sheet. With `bind: true` the visor binds that key instead, after the character's keys (a clash goes to the character), for a scene button that had no key of its own. With `lead: true` the action is also the first quiet key hint on the glass (`.visor__hint--place`). Every scene action is still a row under "This place" in the sheet.
|
|
12
12
|
- **The glass binds keys in game mode only.** `onKey` now returns unless `html[data-game="on"]`: the glass was never drawn outside game mode, so its keys no longer act there either (before, a held character's letter keys still ran on a classic page).
|
|
13
|
-
- **The Governor City keeps ADR 0403.** In game mode (`html.visor-on`) the door's button (`#cityStart`) and the archived Street links leave the scene (`city.css`). The door is the lead action, on D, in the door's own words ("
|
|
13
|
+
- **The Governor City keeps ADR 0403.** In game mode (`html.visor-on`) the door's button (`#cityStart`) and the archived Street links leave the scene (`city.css`). The door is the lead action, on D, in the door's own words and case ("decide 37 in a row", or "start governing"; lowercase is the door's, task 1005210); it re-registers only when those words change. The archived links are rows in the sheet ("Archived: Street", "Archived: Building Level Views"), read off the page's own archived line so the words and the addresses cannot drift; reached by A and the keyboard. The city line stays on the scene as words, raised to `bottom: 224px` because the extra hint wraps the hints onto a second line at a desk width and the centred accent button rises with it. A building's card is a pop-up, not the scene, and keeps its own buttons and keys. Classic mode is as it was.
|
|
14
|
+
|
|
15
|
+
## The sky on the glass (task 1005210)
|
|
16
|
+
|
|
17
|
+
- **What left the sky in game mode** (`thinking.css`, under `html.visor-on`): the top row's lens switch and every pill, the jump field, the thought composer, and the foot's clock and legend. The brand line and its figures stay, as words. A building-like pop-up (the panel, the list, the start dialog, the tour) is not the scene and keeps its buttons.
|
|
18
|
+
- **Where it went** (`sky-visor.js`, loaded after every other sky file; the registration moved out of `sky-interaction.js`). "Write a thought" is the lead action, on the sky's own `/`; "Jump to a goal or a thought" is J (bound by the visor). Each visible pill is a row in the sheet, in its own words with "· on" / "· off" for a toggle, and pressing the row presses the pill, so it does exactly what it did; List, the tour and Fit keep the sky's own L, ? and 0. "Clock" and "Legend" open the foot's two parts. The rows are read off the page's buttons and the scene registers again when a pill's pressed state, a pill's presence (the lens decides it) or the foot changes.
|
|
19
|
+
- **The fields open and close.** A field opens with `is-open` (one at a time) and closes when focus leaves it empty, or on Escape, which keeps its words and is spent there (no options menu). The composer stays open while a full idea is being asked or a receipt waits.
|
|
20
|
+
- **A visor fix that came with it.** The sheet had a click listener of its own inside `#visor`'s, so every row ran twice; a toggle row undid itself. The sheet's listener is gone.
|
|
21
|
+
- **Not done:** the tour still spots the pills it explains, which are off the glass in game mode. Tests: `tests/hall_sky_visor.mjs` (both pages' own markup, the real visor and sheets, in a real browser).
|
|
22
|
+
|
|
23
|
+
## Home's gallery on the glass (task 1005211)
|
|
24
|
+
|
|
25
|
+
- **What left the gallery in game mode** (`home-gallery.css`, under `html.visor-on`): the order tabs (Most popular, Latest), the ways (The full gallery, Your studio), the two arrows and the full-screen button. A work's label, its heart and a piece in the full gallery stay: they are looking at the art, not leaving it.
|
|
26
|
+
- **Where they went.** `home-gallery.js` gives Home its rows (`visorActions()`, and `onChange()` for when they change), each pressing the gallery's own control in its own words: the full gallery (or "Back to the bench") is the lead on G; the two tabs with "· on/off"; Previous work ←, Next work →, Full screen F, which hand the wall the keyboard so its own keys answer next; Your studio. `home-city.js` registers them as Home's scene, and none in a craft whose Home is not the gallery.
|
|
27
|
+
- **Not done:** the attention cards and the Artist's idea box on Home are not the gallery's and still sit on the scene in game mode.
|
|
14
28
|
|
|
15
29
|
## The minimal visor (Helm look 3, task 1005195)
|
|
16
30
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.94",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.94",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.94",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11657,5 +11657,25 @@
|
|
|
11657
11657
|
"id": "1005206",
|
|
11658
11658
|
"text": "The screen you land on after signing in, where you pick Game or Classic, now looks like the V3 game start menu: your usual choice is the bright salmon bar with its Enter key, the other a quiet row."
|
|
11659
11659
|
}
|
|
11660
|
+
],
|
|
11661
|
+
"1.24.93": [
|
|
11662
|
+
{
|
|
11663
|
+
"id": "1005210",
|
|
11664
|
+
"text": "In game mode the sky (Goals and Thinking) no longer has buttons of its own: \"Write a thought\" is on the visor (press /), jumping is J, and the lens switch, the pills, the clock and the legend are rows in the visor's Actions li"
|
|
11665
|
+
}
|
|
11666
|
+
],
|
|
11667
|
+
"1.24.94": [
|
|
11668
|
+
{
|
|
11669
|
+
"id": "1005247",
|
|
11670
|
+
"text": "The V3 game plan now records the owner's eight answers from the task cleanup, so new work is written against them."
|
|
11671
|
+
},
|
|
11672
|
+
{
|
|
11673
|
+
"id": "1005211",
|
|
11674
|
+
"text": "In game mode the Artist's Home gallery no longer has tabs, links or arrows of its own: the full gallery is on the visor (press G), and the order tabs, previous/next work, full screen and Your studio are rows in the visor's Act"
|
|
11675
|
+
},
|
|
11676
|
+
{
|
|
11677
|
+
"id": "1005220",
|
|
11678
|
+
"text": "The game's parties now have one agreed design: any group that plays together gets the same voice channel, member list and chat, whether it's a party you form, a quest party or a meeting room."
|
|
11679
|
+
}
|
|
11660
11680
|
]
|
|
11661
11681
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.94'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/src/module-seams.js
CHANGED
|
@@ -190,6 +190,9 @@ function listEvents() {
|
|
|
190
190
|
// the slices merge into the GET /api/bongos/me payload.
|
|
191
191
|
// (The client settings-panel seam is its browser twin,
|
|
192
192
|
// window.OTB.contributeSettingsPanel in dom-utils.js.)
|
|
193
|
+
// • 'room.kinds' — one plain object per kind of room (party, chain, scoping,
|
|
194
|
+
// decision), contributed by the module that owns the group;
|
|
195
|
+
// read with contributions('room.kinds'). ADR 0404.
|
|
193
196
|
|
|
194
197
|
const contributors = new Map(); // point -> [contribution, ...] (registration order)
|
|
195
198
|
|
package/tests/hall_city.mjs
CHANGED
|
@@ -514,7 +514,7 @@ test("game mode: the door is the visor's lead action on D, and the archived link
|
|
|
514
514
|
const last = scenes[scenes.length - 1];
|
|
515
515
|
assert.equal(last.place, 'Governor city');
|
|
516
516
|
const [lead, street, views] = last.actions;
|
|
517
|
-
assert.equal(lead.label, '
|
|
517
|
+
assert.equal(lead.label, 'start governing', 'what the door says, before the living city reads who it waits on (hall_city_living pins the count)');
|
|
518
518
|
assert.deepEqual([lead.key, lead.bind, lead.lead], ['d', true, true], 'it is on the glass, and D (which nothing else binds) presses it');
|
|
519
519
|
assert.deepEqual([street.label, views.label], ['Archived: Street', 'Archived: Building Level Views'], "the line's own words, read off the page");
|
|
520
520
|
assert.ok(!street.lead && !views.lead && !street.bind && !views.bind, 'the archived links are rows in the sheet, reached by A then the keyboard');
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/hall_sky_visor.mjs — the sky as the visor's scene in game mode (task 1005210; ADR 0403).
|
|
2
|
+
//
|
|
3
|
+
// In game mode the visor is the control and the sky is the view: the sky's top row, its jump
|
|
4
|
+
// field, its thought composer and its foot's clock and legend leave the scene and come back as
|
|
5
|
+
// visor actions (sky-visor.js, thinking.css). Driven in a real browser over the page's OWN
|
|
6
|
+
// markup (goal-map.html's and thinking.html's <main>, word for word) with the real visor and
|
|
7
|
+
// the hall's real sheets. The pills are wired here the way sky-interaction.js wires them (a
|
|
8
|
+
// press flips aria-pressed), because what is under test is the visor's side: that each pill is
|
|
9
|
+
// a row, that a row presses its pill exactly once, and that the rows follow the pills.
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test, after } from 'node:test';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
20
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
21
|
+
|
|
22
|
+
test('source: both sky pages load sky-visor.js after every other sky file', () => {
|
|
23
|
+
for (const p of ['goal-map.html', 'thinking.html']) {
|
|
24
|
+
const html = read(p);
|
|
25
|
+
const at = html.indexOf('/builders/sky-visor.js');
|
|
26
|
+
assert.ok(at > 0, `${p} loads it`);
|
|
27
|
+
for (const f of ['sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']) assert.ok(html.indexOf('/builders/' + f) < at, `${p}: after ${f}`);
|
|
28
|
+
}
|
|
29
|
+
assert.doesNotMatch(read('sky-interaction.js'), /window\.__visorScene = /, 'one registration, in sky-visor.js');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('source: the sky keeps its controls in classic mode; in game mode they open from the visor', () => {
|
|
33
|
+
const css = read('thinking.css');
|
|
34
|
+
for (const rule of [
|
|
35
|
+
'html.visor-on .top .tools, html.visor-on .top .lens { display: none; }',
|
|
36
|
+
'html.visor-on .composer:not(.is-open), html.visor-on .jump:not(.is-open) { display: none; }',
|
|
37
|
+
'html.visor-on .time:not(.is-open), html.visor-on .legend:not(.is-open) { display: none; }',
|
|
38
|
+
]) assert.ok(css.includes('\n' + rule), rule);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---------- in a browser ----------
|
|
42
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
43
|
+
let browser = null;
|
|
44
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
45
|
+
after(() => browser && browser.close());
|
|
46
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
47
|
+
|
|
48
|
+
const ORIGIN = 'http://hall.test';
|
|
49
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png' };
|
|
50
|
+
const mainOf = (page) => { const html = read(page); return html.slice(html.indexOf('<main'), html.indexOf('</main>') + 7); };
|
|
51
|
+
// The sky's own wiring, as far as the visor needs it: SKY.$ / on / S, and a press that flips.
|
|
52
|
+
const STUB = `<script>
|
|
53
|
+
window.OTB = { escapeHtml: (s) => String(s) };
|
|
54
|
+
window.OTBCraftMode = { character() { return 'artist'; }, current() { return null; }, me() { return { builder: { total_credits: 10 } }; }, spriteSrc() { return ''; }, openTool() {} };
|
|
55
|
+
const $ = (id) => document.getElementById(id);
|
|
56
|
+
window.OTBSky = { $, on: (id, ev, fn) => { const el = $(id); if (el) el.addEventListener(ev, fn); }, S: { clockHidden: false } };
|
|
57
|
+
window.presses = {};
|
|
58
|
+
document.querySelectorAll('#tools button, #lensGroup button').forEach((b) => b.addEventListener('click', () => {
|
|
59
|
+
window.presses[b.id] = (window.presses[b.id] || 0) + 1;
|
|
60
|
+
if (b.hasAttribute('aria-pressed')) b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'));
|
|
61
|
+
}));
|
|
62
|
+
</script>`;
|
|
63
|
+
const PAGE = (page, game) => `<!doctype html><html data-theme="dark" data-faces="helm"${game ? ' data-game="on"' : ''}><head><meta charset="utf-8">
|
|
64
|
+
<link rel="stylesheet" href="/builders/style.css"><link rel="stylesheet" href="/builders/visor.css"><link rel="stylesheet" href="/builders/thinking.css"></head>
|
|
65
|
+
<body data-page="${page.replace('.html', '')}">${mainOf(page)}${STUB}
|
|
66
|
+
<script src="/builders/shell-game.js"></script><script src="/builders/visor.js"></script><script src="/builders/sky-visor.js"></script></body></html>`;
|
|
67
|
+
|
|
68
|
+
async function open(page, { game = true } = {}) {
|
|
69
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
70
|
+
await ctx.route(ORIGIN + '/**', (route) => {
|
|
71
|
+
const p = new URL(route.request().url()).pathname;
|
|
72
|
+
if (p === '/') return route.fulfill({ contentType: 'text/html', body: PAGE(page, game) });
|
|
73
|
+
const f = path.join(HALL, p.replace(/^\/builders\//, ''));
|
|
74
|
+
if (p.startsWith('/builders/') && TYPES[path.extname(p)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(p)], body: fs.readFileSync(f) });
|
|
75
|
+
return route.fulfill({ status: 404, body: '' });
|
|
76
|
+
});
|
|
77
|
+
const pg = await ctx.newPage();
|
|
78
|
+
const errors = [];
|
|
79
|
+
pg.on('pageerror', (e) => errors.push(String(e.message)));
|
|
80
|
+
await pg.addInitScript((g) => { try { localStorage.setItem('hall-game', g ? 'on' : 'off'); } catch (_) { /* none */ } }, game);
|
|
81
|
+
await pg.goto(ORIGIN + '/');
|
|
82
|
+
await pg.waitForFunction(() => !!window.OTBVisor && !!window.__visorScene);
|
|
83
|
+
return { ctx, page: pg, errors };
|
|
84
|
+
}
|
|
85
|
+
// every button and field on the sky a person can see (dialogs and side panels are pop-ups)
|
|
86
|
+
const sceneControls = () => [...document.querySelectorAll('.sky-stage button, .sky-stage input')]
|
|
87
|
+
.filter((e) => e.getClientRects().length && !e.closest('#start, #tour, #panel, #room, #menu')).map((e) => e.id || e.className);
|
|
88
|
+
const labels = () => window.__visorScene.actions.map((a) => a.label + (a.key ? ' [' + a.key + ']' : ''));
|
|
89
|
+
|
|
90
|
+
test('game mode, the project sky: no control of its own, and every one is a visor action', { skip }, async () => {
|
|
91
|
+
const { ctx, page, errors } = await open('goal-map.html');
|
|
92
|
+
assert.deepEqual(await page.evaluate(sceneControls), [], 'the sky shows no button or field');
|
|
93
|
+
assert.deepEqual(await page.evaluate(labels), [
|
|
94
|
+
'Write a thought [/]', 'Jump to a goal or a thought [j]', 'List · off [L]', 'Explore · on', 'My ideas · off',
|
|
95
|
+
'Clock · off', 'How to read the sky [?]', 'Fit the whole sky [0]', 'Legend · off',
|
|
96
|
+
]);
|
|
97
|
+
assert.equal(await page.textContent('.visor__hint--place'), '/Write a thought', 'writing a thought is the lead, on the glass');
|
|
98
|
+
assert.deepEqual(errors, []);
|
|
99
|
+
await ctx.close();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('game mode, your sky: the lens switch and its own pills are rows too', { skip }, async () => {
|
|
103
|
+
const { ctx, page } = await open('thinking.html');
|
|
104
|
+
assert.deepEqual(await page.evaluate(sceneControls), []);
|
|
105
|
+
const got = await page.evaluate(labels);
|
|
106
|
+
for (const want of ['Simple · off', 'Your thinking · on', 'My influence · off', 'Claude layer · off', 'What if · off']) assert.ok(got.includes(want), want);
|
|
107
|
+
await ctx.close();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a row presses its pill once, and the row follows the pill', { skip }, async () => {
|
|
111
|
+
const { ctx, page } = await open('goal-map.html');
|
|
112
|
+
await page.keyboard.press('a');
|
|
113
|
+
await page.click('.visor__sheet [data-visor-scene] >> text=My ideas · off');
|
|
114
|
+
assert.deepEqual(await page.evaluate(() => [window.presses.ideasBtn, document.getElementById('ideasBtn').getAttribute('aria-pressed')]), [1, 'true'], 'one press, not two');
|
|
115
|
+
await page.waitForFunction(() => window.__visorScene.actions.some((a) => a.label === 'My ideas · on'));
|
|
116
|
+
await ctx.close();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('J opens the jump field and / is the composer; each closes again when left empty', { skip }, async () => {
|
|
120
|
+
const { ctx, page } = await open('goal-map.html');
|
|
121
|
+
await page.keyboard.press('j');
|
|
122
|
+
assert.deepEqual(await page.evaluate(() => [getComputedStyle(document.querySelector('.jump')).display, document.activeElement.id]), ['block', 'jump']);
|
|
123
|
+
await page.evaluate(() => window.__visorScene.actions[0].run());
|
|
124
|
+
assert.deepEqual(await page.evaluate(() => [getComputedStyle(document.querySelector('.jump')).display, getComputedStyle(document.getElementById('composerWrap')).display, document.activeElement.id]), ['none', 'flex', 'thought'], 'one field at a time');
|
|
125
|
+
await page.keyboard.type('half a thought');
|
|
126
|
+
await page.evaluate(() => document.activeElement.blur());
|
|
127
|
+
await page.waitForTimeout(20);
|
|
128
|
+
assert.equal(await page.evaluate(() => getComputedStyle(document.getElementById('composerWrap')).display), 'flex', 'words in it: it stays');
|
|
129
|
+
await page.evaluate(() => { document.getElementById('thought').value = ''; document.getElementById('thought').focus(); document.activeElement.blur(); });
|
|
130
|
+
await page.waitForTimeout(20);
|
|
131
|
+
assert.equal(await page.evaluate(() => getComputedStyle(document.getElementById('composerWrap')).display), 'none', 'left empty: it closes');
|
|
132
|
+
await ctx.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('the clock and the legend open from the visor', { skip }, async () => {
|
|
136
|
+
const { ctx, page } = await open('goal-map.html');
|
|
137
|
+
const shown = () => page.evaluate(() => ['time', 'legend'].map((id) => getComputedStyle(document.getElementById(id)).display));
|
|
138
|
+
assert.deepEqual(await shown(), ['none', 'none']);
|
|
139
|
+
await page.evaluate(() => { for (const a of window.__visorScene.actions) if (/^(Clock|Legend)/.test(a.label)) a.run(); });
|
|
140
|
+
assert.deepEqual(await shown(), ['flex', 'flex']);
|
|
141
|
+
await page.waitForFunction(() => window.__visorScene.actions.some((a) => a.label === 'Clock · on'));
|
|
142
|
+
await ctx.close();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('classic mode: the sky keeps its own top row, fields and foot', { skip }, async () => {
|
|
146
|
+
const { ctx, page } = await open('goal-map.html', { game: false });
|
|
147
|
+
const ids = await page.evaluate(sceneControls);
|
|
148
|
+
for (const id of ['roomBtn', 'exploreBtn', 'ideasBtn', 'jump', 'thought', 'fileGo', 'play', 'clock']) assert.ok(ids.includes(id), id);
|
|
149
|
+
await ctx.close();
|
|
150
|
+
});
|
package/tests/hall_visor.mjs
CHANGED
|
@@ -120,7 +120,7 @@ test('source: the visor loads after the shell on every scene page, with its shee
|
|
|
120
120
|
assert.ok(at('/builders/visor.js') > at('/builders/shell.js?') && at('/builders/shell.js?') > at('/builders/shell-game.js'), `${f}: shell-game, shell, then the visor`);
|
|
121
121
|
assert.match(html, /<link rel="stylesheet" href="\/builders\/visor\.css/, `${f}: links visor.css`);
|
|
122
122
|
}
|
|
123
|
-
for (const f of ['sky-
|
|
123
|
+
for (const f of ['sky-visor.js', 'city.js', 'home-city.js']) assert.match(read(f), /window\.__visorScene = /, `${f} registers its scene`);
|
|
124
124
|
assert.doesNotMatch(VISOR, /cam\.(x|y|k)\s*=/, 'the visor writes no camera (ADR 0399 §2.2)');
|
|
125
125
|
assert.doesNotMatch(VISOR, /setAttribute\('hidden'|\.hidden = true;[^\n]*nav/, 'it hides nothing but itself');
|
|
126
126
|
assert.match(read('visor.css'), /html\[data-game="on"\] \.visor/, 'game mode only');
|
|
@@ -218,3 +218,46 @@ test('Home is the gallery only in Artist: the city\'s widget switches on the cra
|
|
|
218
218
|
assert.equal(art.hidden, false);
|
|
219
219
|
assert.notEqual(back, 'none', 'and comes back in another craft');
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
// ---- task 1005211 (ADR 0403): in game mode the gallery's controls are the visor's ----
|
|
223
|
+
test("the gallery's rows for the visor: each in its control's own words, each pressing that control", { skip }, async () => {
|
|
224
|
+
const { ctx, page } = await boot();
|
|
225
|
+
const rows = () => page.evaluate(() => window.__g.visorActions().map((a) => [a.label, a.key || '', !!a.lead, !!a.bind]));
|
|
226
|
+
assert.deepEqual(await rows(), [
|
|
227
|
+
['The full gallery', 'g', true, true], ['Most popular · on', '', false, false], ['Latest · off', '', false, false],
|
|
228
|
+
['Previous work', '←', false, false], ['Next work', '→', false, false], ['Full screen', 'F', false, false], ['Your studio', '', false, false],
|
|
229
|
+
], 'the full gallery is the lead, on G; the wall keeps its own arrow keys and F');
|
|
230
|
+
await page.evaluate(() => { window.__changes = 0; window.__g.onChange(() => { window.__changes += 1; }); });
|
|
231
|
+
await page.evaluate(() => window.__g.visorActions().find((a) => a.label === 'Next work').run());
|
|
232
|
+
assert.equal((await state(page)).count, `2 of ${N}`);
|
|
233
|
+
assert.equal(await page.evaluate(() => document.activeElement.className), 'hg-room', 'a row hands the wall the keyboard, so the arrows answer next');
|
|
234
|
+
await page.keyboard.press('ArrowRight');
|
|
235
|
+
assert.equal((await state(page)).count, `3 of ${N}`);
|
|
236
|
+
await page.evaluate(() => window.__g.visorActions()[0].run());
|
|
237
|
+
assert.deepEqual((await rows()).map((r) => r[0]), ['Back to the bench', 'Most popular · on', 'Latest · off', 'Your studio'], 'in the full gallery there is no wall to walk');
|
|
238
|
+
assert.ok(await page.evaluate(() => window.__changes) >= 1, 'Home is told the rows changed');
|
|
239
|
+
await page.evaluate(() => window.__g.visorActions().find((a) => a.label === 'Latest · off').run());
|
|
240
|
+
await page.waitForFunction(() => window.__g.visorActions().some((a) => a.label === 'Latest · on'));
|
|
241
|
+
await ctx.close();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('game mode: the tabs, the ways, the arrows and full screen leave the wall; classic mode keeps them', { skip }, async () => {
|
|
245
|
+
const { ctx, page } = await boot();
|
|
246
|
+
const shown = () => page.evaluate(() => ['.hg-sort', '.hg-ways', '.hg-prev', '.hg-next', '.hg-fsbtn'].map((s) => getComputedStyle(document.querySelector(s)).display !== 'none'));
|
|
247
|
+
assert.deepEqual(await shown(), [true, true, true, true, true], 'classic');
|
|
248
|
+
await page.evaluate(() => document.documentElement.classList.add('visor-on'));
|
|
249
|
+
assert.deepEqual(await shown(), [false, false, false, false, false], 'with the visor up');
|
|
250
|
+
assert.ok(await page.evaluate(() => getComputedStyle(document.querySelector('.hg-heart')).display !== 'none'), 'a heart is the work\'s, and stays');
|
|
251
|
+
await ctx.close();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('Home registers the gallery\'s rows as its scene in Artist, and none in another craft', { skip }, async () => {
|
|
255
|
+
const { ctx, page } = await boot({ city: true });
|
|
256
|
+
await page.evaluate(() => { window.__scenes = []; window.OTBVisor = { scene: (s) => window.__scenes.push(s) }; window.__widget.mount(document.getElementById('home-city')); });
|
|
257
|
+
await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
|
|
258
|
+
assert.deepEqual(await page.evaluate(() => window.__visorScene.actions.length), 0, 'the builder\'s Home: no gallery rows');
|
|
259
|
+
await page.evaluate(() => { document.documentElement.dataset.craft = 'artist'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'artist' } })); });
|
|
260
|
+
await page.waitForFunction(() => window.__visorScene.actions.length > 0 && window.__visorScene.actions[0].label === 'The full gallery');
|
|
261
|
+
assert.equal(await page.evaluate(() => window.__visorScene.place), 'Home');
|
|
262
|
+
await ctx.close();
|
|
263
|
+
});
|