@bongos/core 1.24.85 → 1.24.86
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 +21 -21
- package/docs/copy-inventory.md +17 -16
- package/docs/copy-registry.json +26 -17
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +194 -194
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/options-menu.js +4 -0
- package/modules/hall-ui/public/settings.css +15 -0
- package/modules/hall-ui/public/shell-game.js +82 -2
- package/modules/hall-ui/public/style.css +32 -0
- package/modules/hall-ui/records/options-menu.md +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/hall_options_menu.mjs +63 -4
- package/tests/page_notes_panel.mjs +9 -1
|
@@ -472,8 +472,11 @@
|
|
|
472
472
|
function toolNode(t) { return document.getElementById('craft-tool-' + t.id); }
|
|
473
473
|
// A docked tool has no button: in its mode the panel is docked at the edge,
|
|
474
474
|
// out of it the panel goes. Only a tool already loaded needs closing.
|
|
475
|
+
// A screen inside the options menu's frame docks nothing: the menu page around
|
|
476
|
+
// it already docks its own, and a second panel at the frame's edge is a double.
|
|
475
477
|
function applyDocked(mode) {
|
|
476
|
-
var
|
|
478
|
+
var framed = !!(window.OTBShellGame && window.OTBShellGame.embedded && window.OTBShellGame.embedded());
|
|
479
|
+
var want = framed ? [] : toolsFor(mode);
|
|
477
480
|
for (var m in TOOLS) TOOLS[m].forEach(function (t) {
|
|
478
481
|
if (!t.docked) return;
|
|
479
482
|
if (want.indexOf(t) !== -1) loadTool(t).then(function (api) { if (api.dock) api.dock(); }, function () { /* the page still works */ });
|
|
@@ -363,6 +363,10 @@
|
|
|
363
363
|
|
|
364
364
|
// Close the menu (game mode's Escape): back where it was opened from.
|
|
365
365
|
function close() {
|
|
366
|
+
// Opened as the game's options window over a place (shell-game.js): shut the window.
|
|
367
|
+
var holder = null;
|
|
368
|
+
try { holder = window.parent !== window ? window.parent.OTBOptionsOverlay : null; } catch (_) { holder = null; }
|
|
369
|
+
if (holder && holder.isOpen && holder.isOpen()) { holder.close(); return; }
|
|
366
370
|
var to = state.from || closeTo(window.location.search);
|
|
367
371
|
if (to) { window.location.assign(to); return; }
|
|
368
372
|
var ref = '';
|
|
@@ -129,6 +129,21 @@ html[data-fullscreen="on"] .opt-title { display: block; }
|
|
|
129
129
|
.opt-embed__frame { display: block; width: 100%; height: calc(100vh - 96px - var(--opt-screen-h)); min-height: 480px; border: 0; border-radius: var(--press-radius); background: transparent; }
|
|
130
130
|
html[data-fullscreen="on"] .opt-embed__frame { height: calc(100vh - 48px - var(--opt-screen-h)); }
|
|
131
131
|
|
|
132
|
+
/* Inside the game's options window (shell-game.js): the window is the chrome, so the
|
|
133
|
+
menu sits flush at its top and the open screen fills the rest of it. The page is
|
|
134
|
+
drawn at the embed zoom (style.css), so a window-high length divides by it. */
|
|
135
|
+
html[data-embed="on"] .settings-layout { --opt-vh: calc(100vh / 0.82); padding: var(--ds-space-md) var(--ds-space-lg) 0 0; }
|
|
136
|
+
html[data-embed="on"] .opt-rail { top: 0; max-height: calc(var(--opt-vh) - var(--ds-space-md)); margin-top: calc(-1 * var(--ds-space-md)); padding-top: var(--ds-space-lg); }
|
|
137
|
+
html[data-embed="on"] .opt-title { display: block; font-size: var(--ds-text-xl); }
|
|
138
|
+
/* the window's glass is the ground, so the menu adds none; and window-sized words */
|
|
139
|
+
html[data-embed="on"] .opt-rail { background: none; }
|
|
140
|
+
html[data-embed="on"] .opt-search input { min-height: 34px; font-size: var(--ds-text-sm-plus); }
|
|
141
|
+
html[data-embed="on"] .opt-link { min-height: 30px; font-size: var(--ds-text-md); }
|
|
142
|
+
html[data-embed="on"] .opt-screen { font-size: var(--ds-text-xl); }
|
|
143
|
+
html[data-embed="on"] .settings-groups { --opt-screen-h: calc(var(--ds-space-lg) + var(--ds-space-md)); }
|
|
144
|
+
html[data-embed="on"] .settings-groups .scroll__h { font-size: var(--ds-text-md); }
|
|
145
|
+
html[data-embed="on"] .opt-embed__frame { height: calc(var(--opt-vh) - var(--opt-screen-h) - var(--ds-space-xl)); min-height: 0; }
|
|
146
|
+
|
|
132
147
|
@media (max-width: 860px) {
|
|
133
148
|
/* Collapse to one column: the menu sits above the open screen, no longer sticky. */
|
|
134
149
|
.settings-layout { grid-template-columns: minmax(0, 1fr); gap: var(--sp-md); }
|
|
@@ -406,11 +406,91 @@
|
|
|
406
406
|
try { e.preventDefault(); } catch (_) { /* already finished */ }
|
|
407
407
|
var host = embedded ? window.parent : window;
|
|
408
408
|
if (host && host.OTBOptions && typeof host.OTBOptions.escape === 'function') { host.OTBOptions.escape(); return; }
|
|
409
|
+
if (host && host.OTBOptionsOverlay && host.OTBOptionsOverlay.isOpen()) { host.OTBOptionsOverlay.close(); return; }
|
|
409
410
|
if (embedded) return;
|
|
410
|
-
|
|
411
|
-
window.location.assign(menuPath() + '?from=' + encodeURIComponent(here));
|
|
411
|
+
openOverlay();
|
|
412
412
|
}, 0);
|
|
413
413
|
}
|
|
414
|
+
|
|
415
|
+
// ---------- the options window (game mode) ----------
|
|
416
|
+
// In game mode the options menu opens as a window OVER the place you are in, not
|
|
417
|
+
// as a page of its own, so the place stays drawn behind it and its colours show
|
|
418
|
+
// through (the owner's call, 2026-11-11: "WoW sizing, an overlay"). The window
|
|
419
|
+
// is a frame of the menu with ?embed=1; the menu page itself (a plain visit to
|
|
420
|
+
// /settings) is unchanged. It is the hall's shared dialog (OTBDialog.bind,
|
|
421
|
+
// hall-dialog.js): Escape, the scrim, the close button, the focus trap and the
|
|
422
|
+
// focus return are its. The page behind is made inert while it is up, because Tab
|
|
423
|
+
// leaving the frame's last control lands in the page, past the dialog's own trap.
|
|
424
|
+
var overlay = null, dialog = null;
|
|
425
|
+
function overlayOpen() { return !!(dialog && dialog.isOpen()); }
|
|
426
|
+
function closeOverlay() { if (overlayOpen()) dialog.close(); }
|
|
427
|
+
function setInert(on) {
|
|
428
|
+
var kids = document.body && document.body.children ? document.body.children : [];
|
|
429
|
+
for (var i = 0; i < kids.length; i += 1) {
|
|
430
|
+
var k = kids[i];
|
|
431
|
+
if (k === overlay || !k.setAttribute) continue;
|
|
432
|
+
if (on) { if (!k.hasAttribute('inert')) { k.setAttribute('inert', ''); k.setAttribute('data-opt-inert', ''); } }
|
|
433
|
+
else if (k.hasAttribute('data-opt-inert')) { k.removeAttribute('inert'); k.removeAttribute('data-opt-inert'); }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// `rest` is what follows the menu's path: '?open=x#y', '#access', or ''.
|
|
437
|
+
function openOverlay(rest) {
|
|
438
|
+
try { buildOverlay(rest || ''); } catch (_) {
|
|
439
|
+
// No window can be drawn here: the menu's own page, as before the window existed.
|
|
440
|
+
overlay = null; dialog = null;
|
|
441
|
+
var here = String(window.location.pathname || '') + String(window.location.search || '') + String(window.location.hash || '');
|
|
442
|
+
window.location.assign(menuPath() + (rest || '?from=' + encodeURIComponent(here)));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function buildOverlay(rest) {
|
|
446
|
+
var src = menuPath() + withEmbed(rest);
|
|
447
|
+
if (!overlay) {
|
|
448
|
+
if (!window.OTBDialog || typeof window.OTBDialog.bind !== 'function') throw new Error('no dialog');
|
|
449
|
+
overlay = document.createElement('div');
|
|
450
|
+
overlay.className = 'opt-window';
|
|
451
|
+
overlay.hidden = true;
|
|
452
|
+
overlay.innerHTML = '<div class="opt-window__box" aria-label="Options">' +
|
|
453
|
+
'<button type="button" class="opt-window__x" data-dialog-close aria-label="Close options">×</button>' +
|
|
454
|
+
'<iframe class="opt-window__frame" title="Options"></iframe></div>';
|
|
455
|
+
document.body.appendChild(overlay);
|
|
456
|
+
var frame0 = overlay.querySelector('.opt-window__frame');
|
|
457
|
+
dialog = window.OTBDialog.bind(overlay, {
|
|
458
|
+
panel: overlay.querySelector('.opt-window__box'),
|
|
459
|
+
lockClass: 'opt-window-open',
|
|
460
|
+
initialFocus: function () { return frame0; },
|
|
461
|
+
onOpen: function () { setInert(true); },
|
|
462
|
+
onClose: function () { setInert(false); },
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
var frame = overlay.querySelector('.opt-window__frame');
|
|
466
|
+
// A fresh address reloads the menu; the same one keeps it as it was left.
|
|
467
|
+
if (frame.getAttribute('src') !== src) frame.setAttribute('src', src);
|
|
468
|
+
dialog.open();
|
|
469
|
+
}
|
|
470
|
+
// '?open=x#y' → '?embed=1&open=x#y'; '#y' → '?embed=1#y'.
|
|
471
|
+
function withEmbed(rest) {
|
|
472
|
+
var h = rest.indexOf('#'), q = h === -1 ? rest : rest.slice(0, h), frag = h === -1 ? '' : rest.slice(h);
|
|
473
|
+
q = q.replace(/^\?/, '');
|
|
474
|
+
return '?embed=1' + (q ? '&' + q : '') + frag;
|
|
475
|
+
}
|
|
476
|
+
// The menu's own page never opens the window (nor a page in a frame: see the if below).
|
|
477
|
+
function overlayHere() { return on() && !(document.body && document.body.getAttribute('data-page') === 'settings'); }
|
|
478
|
+
if (!embedded) {
|
|
479
|
+
window.OTBOptionsOverlay = { open: openOverlay, close: closeOverlay, isOpen: overlayOpen };
|
|
480
|
+
// A link to the menu (the visor's Options row, the profile chip, the start
|
|
481
|
+
// screen's Options) opens the window in game mode instead of leaving the place.
|
|
482
|
+
if (document.addEventListener) document.addEventListener('click', function (e) {
|
|
483
|
+
if (e.defaultPrevented || e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || !overlayHere()) return;
|
|
484
|
+
var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
|
|
485
|
+
if (!a || a.getAttribute('target')) return;
|
|
486
|
+
var to;
|
|
487
|
+
try { to = new URL(a.getAttribute('href'), window.location.href); } catch (_) { return; }
|
|
488
|
+
if (to.origin !== window.location.origin || to.pathname.replace(/\.html$/, '') !== menuPath()) return;
|
|
489
|
+
e.preventDefault();
|
|
490
|
+
var q = to.search.replace(/([?&])from=[^&#]*&?/, '$1').replace(/[?&]$/, '');
|
|
491
|
+
openOverlay(q + to.hash);
|
|
492
|
+
}, true);
|
|
493
|
+
}
|
|
414
494
|
if (window.addEventListener) window.addEventListener('keydown', onEscape);
|
|
415
495
|
// Inside the menu, a link to ANOTHER page leaves the menu for it; a link within the page stays.
|
|
416
496
|
if (embedded && document.addEventListener) {
|
|
@@ -1398,6 +1398,38 @@ html[data-embed="on"] .app-foot,
|
|
|
1398
1398
|
html[data-embed="on"] .app-topbar { display: none; }
|
|
1399
1399
|
html[data-embed="on"] .app-body { margin-left: 0; }
|
|
1400
1400
|
html[data-embed="on"] .app-content { padding-top: 0; }
|
|
1401
|
+
/* An embedded screen is a sub-window, so it reads at window size, not page size
|
|
1402
|
+
(the owner, 2026-11-11: "WoW sizing"): everything in it is drawn smaller, and it
|
|
1403
|
+
has no ground of its own, so the options window's glass and the place behind
|
|
1404
|
+
show through. */
|
|
1405
|
+
html[data-embed="on"] { zoom: 0.82; }
|
|
1406
|
+
html[data-embed="on"], html[data-embed="on"] body { background: transparent; }
|
|
1407
|
+
/* THE OPTIONS WINDOW (shell-game.js openOverlay): in game mode the menu opens over
|
|
1408
|
+
the place you are in. The scrim only darkens a little, so the place stays legible;
|
|
1409
|
+
the window is night glass, so its colours carry through. */
|
|
1410
|
+
html.opt-window-open { overflow: hidden; }
|
|
1411
|
+
.opt-window { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; }
|
|
1412
|
+
.opt-window[hidden] { display: none; }
|
|
1413
|
+
/* the scrim is the overlay's own ground, so a click on it lands on the dialog root (OTBDialog closes) */
|
|
1414
|
+
.opt-window::before { content: ''; position: absolute; inset: 0; background: var(--scrim); opacity: .33; pointer-events: none; }
|
|
1415
|
+
.opt-window__box {
|
|
1416
|
+
position: relative; box-sizing: border-box;
|
|
1417
|
+
width: min(1040px, calc(100vw - 32px)); height: min(720px, calc(100vh - 48px));
|
|
1418
|
+
background: color-mix(in srgb, var(--bg) 48%, transparent);
|
|
1419
|
+
-webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
|
|
1420
|
+
border: var(--line-strong) solid var(--rule);
|
|
1421
|
+
border-radius: var(--press-radius);
|
|
1422
|
+
box-shadow: var(--ds-shadow-pop);
|
|
1423
|
+
}
|
|
1424
|
+
.opt-window__frame { display: block; width: 100%; height: 100%; border: 0; background: transparent; color-scheme: dark; }
|
|
1425
|
+
.opt-window__x {
|
|
1426
|
+
position: absolute; top: var(--ds-space-xs); right: var(--ds-space-xs); z-index: 1;
|
|
1427
|
+
width: 28px; height: 28px; padding: 0; display: grid; place-items: center;
|
|
1428
|
+
font-size: var(--ds-text-2xl); line-height: 1; color: var(--ink-soft);
|
|
1429
|
+
background: transparent; border: var(--line) solid var(--rule-soft); border-radius: var(--press-radius); cursor: pointer;
|
|
1430
|
+
}
|
|
1431
|
+
.opt-window__x:hover { color: var(--ink); background: var(--glass); }
|
|
1432
|
+
.opt-window__x:focus-visible { outline: 0; box-shadow: var(--press-focus); }
|
|
1401
1433
|
/* Said once on entering, since the choice is remembered and a page can open in it. */
|
|
1402
1434
|
.fullscreen-hint {
|
|
1403
1435
|
position: fixed;
|
|
@@ -24,3 +24,10 @@ The menu is a Helm screen, written from the hall tier's tokens alone (`settings.
|
|
|
24
24
|
- **Widths:** the panel is `min(--helm-panel-w, 32%)` of the content column; below 860px it stacks above the screen as before. A long label wraps inside its row.
|
|
25
25
|
- **Not built** (the mock shows them on a Controls screen the hall does not have): the key-binding rows, "Reset all keys", and a visible "Back to the city" button. Escape still closes the menu in game mode.
|
|
26
26
|
- **Tests:** `tests/hall_helm.mjs` ("the options menu is a Helm screen"); the kit states in `settings.states.json` render clean at 1440, 390 and 320.
|
|
27
|
+
|
|
28
|
+
## The options window (owner, 2026-11-11: "WoW sizing, an overlay")
|
|
29
|
+
|
|
30
|
+
- **In game mode the menu opens over the place, not as a page.** Escape away from the menu, or a click on any link to `/settings` (the visor's Options row, the profile chip), draws one window (`shell-game.js` `openOverlay`, `window.OTBOptionsOverlay`), built on the shared dialog (`OTBDialog.bind`: Escape, scrim, close button, focus trap and return), with the page behind made `inert` while it is up, holding the menu framed with `?embed=1`; the place stays drawn behind it. Escape, the scrim or the × shuts it; `options-menu.js` `close()` shuts the window when it is in one. A page that cannot draw it falls back to the menu page. A plain visit to `/settings` is the page, as before.
|
|
31
|
+
- **Window sized, see-through.** The window is `min(1040px, …) × min(720px, …)` of glass at 48% of the ground with a light blur, over the shared `--scrim` at a third. Every embedded screen is drawn at `zoom: 0.82` with no ground of its own (`style.css`), and the menu inside it takes smaller words (`settings.css`, `html[data-embed="on"]`); a window-high length divides by the zoom (`--opt-vh`).
|
|
32
|
+
- **One review palette.** A framed screen docks no artist palette of its own (`craft-mode.js` `applyDocked`); the page around it has one.
|
|
33
|
+
- **Tests:** `tests/hall_options_menu.mjs` ("away from the menu, Escape opens the options as a window…"), `tests/page_notes_panel.mjs` (the frame docks no panel).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.86",
|
|
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.86",
|
|
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.86",
|
|
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
|
@@ -11613,5 +11613,11 @@
|
|
|
11613
11613
|
"id": "1005096",
|
|
11614
11614
|
"text": "Autobongos now runs on a Mac for real: the setup command, start, stop and remove all proven on one, and the Mac-only test failures are gone."
|
|
11615
11615
|
}
|
|
11616
|
+
],
|
|
11617
|
+
"1.24.86": [
|
|
11618
|
+
{
|
|
11619
|
+
"id": "1005208",
|
|
11620
|
+
"text": "In game mode, Options now opens as a smaller see-through window over where you are, with smaller text, and the artist palette no longer shows twice."
|
|
11621
|
+
}
|
|
11616
11622
|
]
|
|
11617
11623
|
}
|
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.86'; // 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');
|
|
@@ -138,7 +138,7 @@ test('the Settings page IS the menu: one search over the groups, loaded before s
|
|
|
138
138
|
|
|
139
139
|
// ---- shell-game.js: the embedded page and game mode's Escape --------------------
|
|
140
140
|
|
|
141
|
-
function bootGame({ search = '', framed = true, game = false, path: p = '/my-agents' } = {}) {
|
|
141
|
+
function bootGame({ search = '', framed = true, game = false, path: p = '/my-agents', dom = false } = {}) {
|
|
142
142
|
const docListeners = {};
|
|
143
143
|
const winListeners = {};
|
|
144
144
|
const timers = [];
|
|
@@ -156,13 +156,40 @@ function bootGame({ search = '', framed = true, game = false, path: p = '/my-age
|
|
|
156
156
|
win.parent = framed ? parent : win;
|
|
157
157
|
const document = {
|
|
158
158
|
documentElement: root,
|
|
159
|
-
body: { setAttribute(k, v) { bodyAttrs[k] = v; } },
|
|
159
|
+
body: { setAttribute(k, v) { bodyAttrs[k] = v; }, getAttribute: (k) => (k in bodyAttrs ? bodyAttrs[k] : null) },
|
|
160
160
|
addEventListener(t, fn) { (docListeners[t] ||= []).push(fn); },
|
|
161
161
|
};
|
|
162
|
+
// dom: enough of a page for the game's options window to be drawn (createElement,
|
|
163
|
+
// a sibling the window makes inert, a frame that keeps its src), with OTBDialog.bind
|
|
164
|
+
// standing in for hall-dialog.js (its trap, Escape and focus return are tested there).
|
|
165
|
+
const windows = [];
|
|
166
|
+
const bound = [];
|
|
167
|
+
const sibling = { attrs: {}, setAttribute(k, v) { this.attrs[k] = v; }, hasAttribute(k) { return k in this.attrs; }, removeAttribute(k) { delete this.attrs[k]; } };
|
|
168
|
+
if (dom) {
|
|
169
|
+
document.body.children = [sibling];
|
|
170
|
+
document.body.appendChild = (el) => { windows.push(el); document.body.children.push(el); return el; };
|
|
171
|
+
document.createElement = () => {
|
|
172
|
+
const frameAttrs = {};
|
|
173
|
+
const frame = { getAttribute: (k) => (k in frameAttrs ? frameAttrs[k] : null), setAttribute: (k, v) => { frameAttrs[k] = v; }, focus() {} };
|
|
174
|
+
const box = { box: true };
|
|
175
|
+
return { hidden: false, innerHTML: '', querySelector: (sel) => (sel === '.opt-window__box' ? box : frame), frame, box };
|
|
176
|
+
};
|
|
177
|
+
win.OTBDialog = {
|
|
178
|
+
bind(root, opts) {
|
|
179
|
+
let open = false;
|
|
180
|
+
bound.push({ root, opts });
|
|
181
|
+
return {
|
|
182
|
+
open() { if (open) return; open = true; root.hidden = false; root.classes = [opts.lockClass]; opts.onOpen(); },
|
|
183
|
+
close() { if (!open) return; open = false; root.hidden = true; root.classes = []; opts.onClose(); },
|
|
184
|
+
isOpen: () => open,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
162
189
|
const ctx = vm.createContext({ window: win, document, URLSearchParams, URL });
|
|
163
190
|
vm.runInContext(read('shell-game.js'), ctx);
|
|
164
191
|
return {
|
|
165
|
-
root, bodyAttrs, parent, assigned, game: win.OTBShellGame,
|
|
192
|
+
root, bodyAttrs, parent, assigned, windows, bound, sibling, win, game: win.OTBShellGame,
|
|
166
193
|
press(key, target = { tagName: 'DIV' }) {
|
|
167
194
|
const ev = { key, target, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
|
|
168
195
|
for (const fn of winListeners.keydown || []) fn(ev);
|
|
@@ -173,7 +200,7 @@ function bootGame({ search = '', framed = true, game = false, path: p = '/my-age
|
|
|
173
200
|
const attrs = { href };
|
|
174
201
|
if (target) attrs.target = target;
|
|
175
202
|
const a = { getAttribute: (k) => (k in attrs ? attrs[k] : null), setAttribute: (k, v) => { attrs[k] = v; } };
|
|
176
|
-
for (const fn of docListeners.click || []) fn({ target: { closest: () => a } });
|
|
203
|
+
for (const fn of docListeners.click || []) fn({ target: { closest: () => a }, preventDefault() { this.defaultPrevented = true; } });
|
|
177
204
|
return attrs.target || null;
|
|
178
205
|
},
|
|
179
206
|
};
|
|
@@ -214,6 +241,38 @@ test('GAME MODE: on the menu itself the last Escape closes it; outside game mode
|
|
|
214
241
|
assert.deepEqual(off.assigned, [], 'outside game mode the press stays the shell\'s');
|
|
215
242
|
});
|
|
216
243
|
|
|
244
|
+
test('GAME MODE: away from the menu, Escape opens the options as a window over the place, and Escape again shuts it', () => {
|
|
245
|
+
const city = bootGame({ game: true, framed: false, path: '/city', dom: true });
|
|
246
|
+
city.press('Escape');
|
|
247
|
+
assert.deepEqual(city.assigned, [], 'the place stays: no page change');
|
|
248
|
+
assert.equal(city.windows.length, 1, 'one window drawn over it');
|
|
249
|
+
assert.equal(city.windows[0].frame.getAttribute('src'), '/settings?embed=1', 'the menu, framed and chromeless');
|
|
250
|
+
assert.equal(city.bound.length, 1, 'the window is the shared dialog: OTBDialog.bind owns Escape, the scrim, the focus trap and return');
|
|
251
|
+
assert.equal(city.bound[0].opts.panel, city.windows[0].box, 'the glass box is the dialog panel');
|
|
252
|
+
assert.equal(city.bound[0].opts.lockClass, 'opt-window-open', 'the page behind stops scrolling');
|
|
253
|
+
assert.equal(city.bound[0].opts.initialFocus(), city.windows[0].frame, 'focus goes into the menu');
|
|
254
|
+
assert.equal(city.sibling.hasAttribute('inert'), true, 'the page behind is inert, so Tab out of the frame cannot reach it');
|
|
255
|
+
assert.equal(city.win.OTBOptionsOverlay.isOpen(), true);
|
|
256
|
+
city.press('Escape');
|
|
257
|
+
assert.equal(city.win.OTBOptionsOverlay.isOpen(), false, 'the second Escape shuts it');
|
|
258
|
+
assert.equal(city.sibling.hasAttribute('inert'), false, 'and the page behind is live again');
|
|
259
|
+
city.press('Escape');
|
|
260
|
+
assert.equal(city.windows.length, 1, 'reopening reuses the one window');
|
|
261
|
+
assert.equal(city.bound.length, 1, 'bound once');
|
|
262
|
+
// a link to the menu (the visor's Options row) opens the window at that screen
|
|
263
|
+
city.win.OTBOptionsOverlay.close();
|
|
264
|
+
city.click('/settings?from=%2Fcity#access');
|
|
265
|
+
assert.equal(city.windows[0].frame.getAttribute('src'), '/settings?embed=1#access', 'the screen it named, without ?from=');
|
|
266
|
+
assert.equal(city.win.OTBOptionsOverlay.isOpen(), true);
|
|
267
|
+
// outside game mode the link is a link
|
|
268
|
+
const off = bootGame({ game: false, framed: false, path: '/city', dom: true });
|
|
269
|
+
off.click('/settings');
|
|
270
|
+
assert.equal(off.windows.length, 0);
|
|
271
|
+
// and the menu, closing inside the window, shuts the window rather than leaving the page
|
|
272
|
+
const om = read('options-menu.js');
|
|
273
|
+
assert.ok(om.includes('window.parent.OTBOptionsOverlay') && om.includes('holder.close(); return;'), 'close() shuts the window first');
|
|
274
|
+
});
|
|
275
|
+
|
|
217
276
|
test('options-menu.js gives the menu its Escape: close in game mode, nothing outside it', () => {
|
|
218
277
|
const src = read('options-menu.js');
|
|
219
278
|
assert.match(src, /function escape\(\) \{\s*var game = window\.OTBShellGame;\s*if \(game && game\.on && game\.on\(\)\) close\(\);/);
|
|
@@ -41,7 +41,7 @@ function node(tag, id, cls) {
|
|
|
41
41
|
}
|
|
42
42
|
function all(root) { return root.children.flatMap((c) => [c, ...all(c)]); }
|
|
43
43
|
|
|
44
|
-
function boot({ session = {}, search = '' } = {}) {
|
|
44
|
+
function boot({ session = {}, search = '', embedded = false } = {}) {
|
|
45
45
|
const sidebar = node('NAV', 'app-sidebar');
|
|
46
46
|
const topbar = node('DIV', 'app-topbar');
|
|
47
47
|
const side = topbar.appendChild(node('DIV', '', 'topbar-side'));
|
|
@@ -61,6 +61,7 @@ function boot({ session = {}, search = '' } = {}) {
|
|
|
61
61
|
const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
|
|
62
62
|
const opened = [];
|
|
63
63
|
const window = { OTB: realOTB(), localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }), location: { search } };
|
|
64
|
+
if (embedded) window.OTBShellGame = { embedded: () => true };
|
|
64
65
|
// "Loading" page-notes.js: record the tag, then define the API it would define.
|
|
65
66
|
head.onAppend = (tag) => {
|
|
66
67
|
loaded.push(tag.src);
|
|
@@ -94,6 +95,13 @@ test('THE PANEL: docked at the edge in Artist alone, with no top-bar button, and
|
|
|
94
95
|
assert.deepEqual(none.loaded, [], 'and nothing loads');
|
|
95
96
|
});
|
|
96
97
|
|
|
98
|
+
test('a screen in the options menu frame docks no second panel: the menu around it has one', async () => {
|
|
99
|
+
const p = boot({ session: { 'hall-craft': 'artist' }, embedded: true });
|
|
100
|
+
await flush();
|
|
101
|
+
assert.deepEqual(p.opened, [], 'no dock inside the frame');
|
|
102
|
+
assert.deepEqual(p.loaded, [], 'and the panel script never loads there');
|
|
103
|
+
});
|
|
104
|
+
|
|
97
105
|
test('the panel script loads once, at the same version as craft-mode.js', async () => {
|
|
98
106
|
const p = boot({ session: { 'hall-craft': 'artist' } });
|
|
99
107
|
await flush();
|