@bongos/core 1.21.26 → 1.21.27
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 -53
- package/docs/copy-inventory.md +8 -8
- package/docs/copy-registry.json +9 -9
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +87 -87
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +97 -60
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +46 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/hall-navigation.md +7 -2
- 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_fullscreen.mjs +122 -5
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
165
|
+
<script src="/builders/shell.js?v=2026-10-02-trail"></script>
|
|
166
166
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
167
167
|
<script src="/builders/palette.js" defer></script>
|
|
168
168
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
174
|
+
<script src="/builders/shell.js?v=2026-10-02-trail"></script>
|
|
175
175
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
176
176
|
<script src="/builders/palette.js" defer></script>
|
|
177
177
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Hall navigation — the Back link and the standard full-screen switch (tasks 1004430, 1004477)
|
|
1
|
+
# Hall navigation — the Back link and its trail, and the standard full-screen switch (tasks 1004430, 1004477, 1004476)
|
|
2
2
|
|
|
3
3
|
Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html` joining the chrome.
|
|
4
4
|
|
|
@@ -11,6 +11,11 @@ Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html`
|
|
|
11
11
|
- `<body data-back-fallback="/work">` names where Back goes when there is no origin (a deep-linked task falls back to Tasks, an idea to Your thinking, the tweak editor to Studio). It is honoured only as a path on this host; a sidebar hop still shows nothing.
|
|
12
12
|
- A link marked `[data-shell-exit]` is a second exit a page places: the task record's actions row and its two failure renders, the primer and drachmae error notes, and the tweak editor's bar. The shell fills its `href` and its words ("Back to goal 12", "Back to tasks"), and with no way back at all it goes home. A page that renders one after boot calls `OTBShell.fillExits(root)`.
|
|
13
13
|
- The origin's own `?from=` is dropped so back links never chain, and a `?from=` naming the page itself is refused.
|
|
14
|
+
- **The trail (task 1004476).** Two or more clicks deep the Back link grows into a breadcrumb: "‹ Goals › goal 12 › task 340", each crumb a link and the last one `#topbar-back` itself. One step back renders exactly the old Back link.
|
|
15
|
+
- It lives per tab in `sessionStorage['hall-nav-trail']`, a list of `{href,label}` crumbs ending with the current page. A drill-down (origin found in the trail) cuts after the origin and appends; returning to a crumb (this page is in the trail and the origin sits after it, or there is none) trims after it; an origin the trail does not know starts fresh, so `?from=`, referrer and `data-back-fallback` still decide the first crumb; a sidebar hop starts over with no trail.
|
|
16
|
+
- A referrer has no `#hash`, so a referrer origin matches the latest crumb on the same address: a task opened from `/goals#/goal/12` finds "goal 12", not "Goals". Hash routes (a goal opened on `/goals`, the hall's overlays) update the trail on `hashchange`: a record hash appends, a held crumb trims, any other view only renames the page's own crumb.
|
|
17
|
+
- Capped at four crumbs, oldest dropped. Stored crumbs pass the same host-path check as the fallback and are escaped when drawn. Storage that throws keeps the one-step Back.
|
|
18
|
+
- Width folds it: at 640px the middle becomes "…"; at 480px the first crumb joins it, the "…" becomes a link to the trail's start (titled with what it hides), and the bar's page name hides while a trail shows; at 360px only the Back arrow is left. Measured with no sideways scroll at 320, 390, 600 and 1280.
|
|
14
19
|
- `/profile?id=N` highlights **Builders**, not your own Profile, unless `/me` says the id is yours.
|
|
15
20
|
|
|
16
21
|
## Full screen is standard
|
|
@@ -22,4 +27,4 @@ Covers: every shelled hall page (`shell.js`, `style.css`), plus `goal-map.html`
|
|
|
22
27
|
|
|
23
28
|
`goal-map.html` now uses thinking.html's structure: sidebar and top-bar mounts, `style.css`, `shell.js`, the sky inside `<main id="hall-main" class="sky-stage">`, and a `<div class="brand">` because the shell's bar is the page's one `<header>`. In `thinking.css` both pages share one stage rule, `body[data-page] .sky-stage`. No hall page is standalone any more (`tests/hall_audit.mjs` `STANDALONE_EXEMPT` is empty).
|
|
24
29
|
|
|
25
|
-
Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link, fallback and exit cases), `tests/hall_record_world.mjs` §10 (the record places its exit), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
|
|
30
|
+
Tests: `tests/hall_fullscreen.mjs` (the standard switch, `plain`, Back link, trail, fallback and exit cases), `tests/hall_record_world.mjs` §10 (the record places its exit), `tests/goal_map_page.mjs`, `tests/hall_audit.mjs` §8.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.27",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.27",
|
|
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.21.
|
|
3
|
+
"version": "1.21.27",
|
|
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
|
@@ -8912,5 +8912,11 @@
|
|
|
8912
8912
|
"id": "1003623",
|
|
8913
8913
|
"text": "Fixed idea-triage cluster handling: 'keep #N' and 'merge #N' now parse; skipped clusters count toward the summary; spark ideas excluded from batch clustering; stale Step 1.5 refs now point at 1.5b. Tests added in tests/idea_tr"
|
|
8914
8914
|
}
|
|
8915
|
+
],
|
|
8916
|
+
"1.21.27": [
|
|
8917
|
+
{
|
|
8918
|
+
"id": "1004476",
|
|
8919
|
+
"text": "In the builders' hall, a page you reach through several clicks now shows the whole way back at the top, like Goals > goal 12 > task 340, and each step is a link. Clicking an earlier step takes you straight there; the sidebar s"
|
|
8920
|
+
}
|
|
8915
8921
|
]
|
|
8916
8922
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.27'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -33,7 +33,8 @@ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
|
33
33
|
// <body data-back-fallback> and `exits` how many [data-shell-exit] links the
|
|
34
34
|
// page placed for the shell to fill (task 1004477).
|
|
35
35
|
function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = false,
|
|
36
|
-
referrer = '', search = '', session = {}, fallback = null, exits = 0, path = null, meId = null
|
|
36
|
+
referrer = '', search = '', session = {}, fallback = null, exits = 0, path = null, meId = null,
|
|
37
|
+
hash = '', sessionThrows = false } = {}) {
|
|
37
38
|
const els = new Map();
|
|
38
39
|
const windowListeners = {};
|
|
39
40
|
const timers = [];
|
|
@@ -87,11 +88,13 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
87
88
|
addEventListener() {}, dispatchEvent() {},
|
|
88
89
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
89
90
|
};
|
|
90
|
-
const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash
|
|
91
|
+
const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign() {}, replace() {} };
|
|
92
|
+
// `session` is the TAB's sessionStorage: pass one object to several boots and
|
|
93
|
+
// they are page loads in one tab (the trail, task 1004476).
|
|
91
94
|
const sessionStorage = {
|
|
92
|
-
getItem: (k) => (k in session ? session[k] : null
|
|
93
|
-
setItem: (k, v) => { session[k] = String(v); },
|
|
94
|
-
removeItem: (k) => { delete session[k]; },
|
|
95
|
+
getItem: (k) => { if (sessionThrows) throw new Error('SecurityError'); return k in session ? session[k] : null; },
|
|
96
|
+
setItem: (k, v) => { if (sessionThrows) throw new Error('SecurityError'); session[k] = String(v); },
|
|
97
|
+
removeItem: (k) => { if (sessionThrows) throw new Error('SecurityError'); delete session[k]; },
|
|
95
98
|
};
|
|
96
99
|
const localStorage = {
|
|
97
100
|
getItem: (k) => { if (storageThrows) throw new Error('SecurityError'); return k in store ? store[k] : null; },
|
|
@@ -131,6 +134,11 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
131
134
|
shell: () => windowObj.OTBShell,
|
|
132
135
|
session,
|
|
133
136
|
resizes: () => resizes,
|
|
137
|
+
// A hash route moving without a page load (a goal opened on /goals).
|
|
138
|
+
go(h) { location.hash = h; for (const fn of windowListeners.hashchange || []) fn({ type: 'hashchange' }); },
|
|
139
|
+
trail: () => JSON.parse(session['hall-nav-trail'] || '[]').map((c) => c.label),
|
|
140
|
+
crumbs: () => [...topbar._html.matchAll(/class="topbar-crumb" href="([^"]*)">([^<]*)</g)].map((m) => `${m[2]} ${m[1]}`),
|
|
141
|
+
back: () => { const m = /id="topbar-back" href="([^"]*)"[^>]*>(?:<svg[\s\S]*?<\/svg>)?<span class="topbar-back__label">([^<]*)</.exec(topbar._html); return m ? `${m[2]} ${m[1]}` : null; },
|
|
134
142
|
// Press a key the way the browser dispatches it: every window listener,
|
|
135
143
|
// then the page may mark the press used, then the queued timers run.
|
|
136
144
|
press(key, { target = { tagName: 'CANVAS' }, pageUses = false } = {}) {
|
|
@@ -277,6 +285,115 @@ test('Back: an origin\'s own ?from= is dropped, so one page\'s way back never ch
|
|
|
277
285
|
assert.match(p.topbar(), /href="\/roster\?q=a"/);
|
|
278
286
|
});
|
|
279
287
|
|
|
288
|
+
// THE TRAIL (task 1004476). The owner's case: Goals -> goal 12 -> task 340 ->
|
|
289
|
+
// an idea shows "Goals › goal 12 › task 340", each a link, and Back is the last.
|
|
290
|
+
// One `session` object across boots is one browser tab.
|
|
291
|
+
function walkToIdea() {
|
|
292
|
+
const tab = {};
|
|
293
|
+
const goals = boot({ page: 'goals', optIn: false, path: '/goals', session: Object.assign(tab, { 'hall-nav-hop': '/goals' }) });
|
|
294
|
+
goals.go('#/goal/12');
|
|
295
|
+
boot({ page: 'task', optIn: false, path: '/task/340', search: '?from=' + encodeURIComponent('/goals#/goal/12'), fallback: '/work', session: tab });
|
|
296
|
+
const idea = boot({ page: 'idea', optIn: false, path: '/idea/4', referrer: 'https://h/task/340', fallback: '/thinking', session: tab });
|
|
297
|
+
return { tab, idea };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
test('trail: a drill-down two layers deep shows every step, each a link, and Back is the last', () => {
|
|
301
|
+
const { tab, idea } = walkToIdea();
|
|
302
|
+
assert.deepEqual(idea.crumbs(), ['Goals /goals', 'goal 12 /goals#/goal/12']);
|
|
303
|
+
assert.equal(idea.back(), 'task 340 /task/340', 'the step before this page is still the Back link');
|
|
304
|
+
assert.match(idea.topbar(), /<nav class="topbar-trail topbar-trail--long" aria-label="Breadcrumb">/);
|
|
305
|
+
assert.deepEqual(JSON.parse(tab['hall-nav-trail']).map((c) => c.label), ['Goals', 'goal 12', 'task 340', 'idea 4']);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('trail: one step back is exactly the Back link it always was', () => {
|
|
309
|
+
const tab = {};
|
|
310
|
+
boot({ page: 'roster', optIn: false, path: '/roster', session: Object.assign(tab, { 'hall-nav-hop': '/roster' }) });
|
|
311
|
+
const p = boot({ page: 'profile', optIn: false, search: '?id=7', referrer: 'https://h/roster', session: tab });
|
|
312
|
+
assert.doesNotMatch(p.topbar(), /topbar-trail|topbar-crumb/);
|
|
313
|
+
assert.match(p.topbar(), /<a class="topbar-back" id="topbar-back" href="\/roster" title="Back to Builders"><svg/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('trail: returning to a crumb trims everything after it, and a reload keeps it', () => {
|
|
317
|
+
const { tab } = walkToIdea();
|
|
318
|
+
const goal = boot({ page: 'goals', optIn: false, path: '/goals', hash: '#/goal/12', referrer: 'https://h/idea/4', session: tab });
|
|
319
|
+
assert.deepEqual(goal.crumbs(), [], 'one step left, so just Back');
|
|
320
|
+
assert.equal(goal.back(), 'Goals /goals');
|
|
321
|
+
assert.deepEqual(goal.trail(), ['Goals', 'goal 12']);
|
|
322
|
+
// A reload keeps its referrer: the task opened from the goal again.
|
|
323
|
+
const task = boot({ page: 'task', optIn: false, path: '/task/340', referrer: 'https://h/goals', fallback: '/work', session: tab });
|
|
324
|
+
const again = boot({ page: 'task', optIn: false, path: '/task/340', referrer: 'https://h/goals', fallback: '/work', session: tab });
|
|
325
|
+
for (const p of [task, again]) {
|
|
326
|
+
assert.deepEqual(p.crumbs(), ['Goals /goals'], 'a referrer has no #hash, and still finds goal 12');
|
|
327
|
+
assert.equal(p.back(), 'goal 12 /goals#/goal/12');
|
|
328
|
+
}
|
|
329
|
+
assert.deepEqual(again.trail(), ['Goals', 'goal 12', 'task 340']);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('trail: a hash route back to a crumb trims, and a view switch only renames the page', () => {
|
|
333
|
+
const tab = {};
|
|
334
|
+
const goals = boot({ page: 'goals', optIn: false, path: '/goals', session: Object.assign(tab, { 'hall-nav-hop': '/goals' }) });
|
|
335
|
+
goals.go('#/goal/12');
|
|
336
|
+
assert.deepEqual(goals.trail(), ['Goals', 'goal 12']);
|
|
337
|
+
goals.go('');
|
|
338
|
+
assert.deepEqual(goals.trail(), ['Goals'], 'back to the list is a return, not a step');
|
|
339
|
+
goals.go('#archived');
|
|
340
|
+
assert.deepEqual(goals.trail(), ['Goals'], 'a view of the same page is not a drill-down');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('trail: a sidebar hop starts over', () => {
|
|
344
|
+
const { tab } = walkToIdea();
|
|
345
|
+
const work = boot({ page: 'work', optIn: false, path: '/work', referrer: 'https://h/idea/4', session: Object.assign(tab, { 'hall-nav-hop': '/work' }) });
|
|
346
|
+
assert.doesNotMatch(work.topbar(), /topbar-back/);
|
|
347
|
+
assert.deepEqual(work.trail(), ['Tasks']);
|
|
348
|
+
const task = boot({ page: 'task', optIn: false, path: '/task/9', referrer: 'https://h/work', fallback: '/work', session: tab });
|
|
349
|
+
assert.deepEqual(task.crumbs(), []);
|
|
350
|
+
assert.equal(task.back(), 'Tasks /work');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('trail: an origin the trail does not know starts a fresh one by the ?from= rules', () => {
|
|
354
|
+
const { tab } = walkToIdea();
|
|
355
|
+
const p = boot({ page: 'profile', optIn: false, search: '?id=7&from=%2Froster', referrer: 'https://h/idea/4', session: tab });
|
|
356
|
+
assert.deepEqual(p.crumbs(), [], 'the old trail is not this page\'s way here');
|
|
357
|
+
assert.equal(p.back(), 'Builders /roster');
|
|
358
|
+
assert.deepEqual(p.trail(), ['Builders', 'Profile']);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('trail: capped at four crumbs, and on a phone the middle collapses', () => {
|
|
362
|
+
const tab = {};
|
|
363
|
+
boot({ page: 'goals', optIn: false, path: '/goals', session: Object.assign(tab, { 'hall-nav-hop': '/goals' }) });
|
|
364
|
+
let prev = '/goals';
|
|
365
|
+
let last;
|
|
366
|
+
for (const id of [1, 2, 3, 4, 5]) {
|
|
367
|
+
last = boot({ page: 'task', optIn: false, path: `/task/${id}`, referrer: `https://h${prev}`, fallback: '/work', session: tab });
|
|
368
|
+
prev = `/task/${id}`;
|
|
369
|
+
}
|
|
370
|
+
assert.deepEqual(last.crumbs(), ['task 1 /task/1', 'task 2 /task/2', 'task 3 /task/3'], 'Goals fell off the front');
|
|
371
|
+
assert.equal(last.back(), 'task 4 /task/4');
|
|
372
|
+
assert.equal(last.trail().length, 5, 'four crumbs plus this page');
|
|
373
|
+
const html = last.topbar();
|
|
374
|
+
assert.equal((html.match(/topbar-trail__step--mid/g) || []).length, 2, 'every crumb between the first and Back is the middle');
|
|
375
|
+
assert.match(html, /<nav class="topbar-trail topbar-trail--long"/);
|
|
376
|
+
assert.match(html, /<span class="topbar-trail__more"><a class="topbar-crumb topbar-crumb--more" href="\/task\/1" title="task 1 › task 2 › task 3" aria-label="task 1">…<\/a>/,
|
|
377
|
+
'the fold is a link to the start of the trail, naming what it hides');
|
|
378
|
+
const css = read('style.css');
|
|
379
|
+
assert.match(css, /@media \(max-width: 640px\) \{\s*\.topbar-trail__step--mid \{ display: none; \}\s*\.topbar-trail--long \.topbar-trail__more \{ display: inline-flex; \}/,
|
|
380
|
+
'source: on a phone the middle hides and "…" stands in');
|
|
381
|
+
assert.match(css, /@media \(max-width: 480px\) \{\s*\.topbar-trail__step--first \{ display: none; \}/, 'source: narrower, the first crumb folds too');
|
|
382
|
+
assert.match(css, /@media \(max-width: 360px\) \{\s*\.topbar-trail__mark, \.topbar-trail \.topbar-trail__more, \.topbar-trail \.topbar-back__label \{ display: none; \}/,
|
|
383
|
+
'source: narrowest, only the Back arrow is left');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('trail: a hostile stored crumb is dropped, and storage that throws keeps the one-step Back', () => {
|
|
387
|
+
const tab = { 'hall-nav-trail': JSON.stringify([{ href: '//evil.example', label: 'x' }, { href: '/api/gds/x', label: 'y' },
|
|
388
|
+
{ href: 'javascript:alert(1)', label: 'z' }, { href: '/goals', label: '<b>Goals</b>' }]) };
|
|
389
|
+
const p = boot({ page: 'task', optIn: false, path: '/task/9', referrer: 'https://h/goals', fallback: '/work', session: tab });
|
|
390
|
+
assert.equal(p.back(), '<b>Goals</b> /goals', 'a stored label is escaped');
|
|
391
|
+
assert.deepEqual(JSON.parse(tab['hall-nav-trail']).map((c) => c.href), ['/goals', '/task/9']);
|
|
392
|
+
const locked = boot({ page: 'task', optIn: false, path: '/task/9', referrer: 'https://h/goals', fallback: '/work', sessionThrows: true });
|
|
393
|
+
assert.equal(locked.back(), 'Goals /goals');
|
|
394
|
+
assert.deepEqual(locked.crumbs(), []);
|
|
395
|
+
});
|
|
396
|
+
|
|
280
397
|
test('an opted-in page renders ONE switch in the top bar, off by default', () => {
|
|
281
398
|
const p = boot();
|
|
282
399
|
const btn = p.button();
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-02-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-02-trail/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '901fc3252b096ce6';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|