@bongos/core 1.21.27 → 1.21.29
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 +89 -69
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +399 -391
- package/docs/copy-registry.json +532 -460
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +68 -0
- package/docs/page-readings.json +3737 -3587
- package/modules/hall-ui/public/atlas.html +3 -1
- package/modules/hall-ui/public/blockers.html +3 -1
- package/modules/hall-ui/public/board-room.html +3 -1
- package/modules/hall-ui/public/city.html +3 -1
- package/modules/hall-ui/public/collab.html +3 -1
- package/modules/hall-ui/public/commands.html +3 -1
- package/modules/hall-ui/public/copy-desk.html +3 -1
- package/modules/hall-ui/public/craft-mode.js +297 -0
- package/modules/hall-ui/public/deploy.html +3 -1
- package/modules/hall-ui/public/diagrams.html +3 -1
- package/modules/hall-ui/public/drachmae.html +3 -1
- package/modules/hall-ui/public/fleet.html +3 -1
- package/modules/hall-ui/public/gate.html +3 -1
- package/modules/hall-ui/public/goal-map.html +3 -1
- package/modules/hall-ui/public/goals.html +3 -1
- package/modules/hall-ui/public/government.html +3 -1
- package/modules/hall-ui/public/idea.html +3 -1
- package/modules/hall-ui/public/ideas.html +3 -1
- package/modules/hall-ui/public/index.html +3 -1
- package/modules/hall-ui/public/modules.html +3 -1
- package/modules/hall-ui/public/primer.html +3 -1
- package/modules/hall-ui/public/profile.html +3 -1
- package/modules/hall-ui/public/project-settings.html +3 -1
- package/modules/hall-ui/public/ranks.html +3 -1
- package/modules/hall-ui/public/roadmap.html +3 -1
- package/modules/hall-ui/public/roster.html +3 -1
- package/modules/hall-ui/public/sessions.html +3 -1
- package/modules/hall-ui/public/settings.html +3 -1
- package/modules/hall-ui/public/shell-trail.js +186 -0
- package/modules/hall-ui/public/shell.js +17 -172
- package/modules/hall-ui/public/street.html +3 -1
- package/modules/hall-ui/public/studio.html +3 -1
- package/modules/hall-ui/public/style.css +228 -0
- package/modules/hall-ui/public/task.html +3 -1
- package/modules/hall-ui/public/thinking.html +3 -1
- package/modules/hall-ui/public/tweak-editor.html +3 -1
- package/modules/hall-ui/public/watch.html +3 -1
- package/modules/hall-ui/public/work.html +3 -1
- package/modules/hall-ui/records/craft-mode.md +22 -0
- package/modules/hall-ui/records/hall-navigation.md +1 -1
- package/modules/provisioning/routes/callback-page.js +28 -65
- package/modules/provisioning/routes/provisioning.js +6 -6
- package/modules/public-landing/public/projects.html +68 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/craft_mode.mjs +267 -112
- package/tests/goal_map_page.mjs +2 -2
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_fullscreen.mjs +3 -0
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provisioning_callback_page.mjs +5 -5
- package/tests/provisioning_github_app_routes.mjs +24 -25
- package/tests/wizard_auto_open.mjs +94 -0
- package/tests/wizard_intent_resume.mjs +17 -2
|
@@ -435,7 +435,9 @@
|
|
|
435
435
|
|
|
436
436
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
437
437
|
chrome exists before the page scripts run. -->
|
|
438
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
438
|
+
<script src="/builders/shell-trail.js?v=2026-10-02-craft"></script>
|
|
439
|
+
<script src="/builders/craft-mode.js?v=2026-10-02-craft"></script>
|
|
440
|
+
<script src="/builders/shell.js?v=2026-10-02-craft"></script>
|
|
439
441
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
440
442
|
<script src="/builders/palette.js" defer></script>
|
|
441
443
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// shell-trail.js — the hall top bar's way back: the Back link, the breadcrumb
|
|
2
|
+
// trail, and the [data-shell-exit] links (tasks 1004430, 1004476, 1004477).
|
|
3
|
+
// Carved out of shell.js in task 1004492 (shell.js sat on the 1,500-line
|
|
4
|
+
// ratchet); the behaviour and its rules are unchanged, and tests/hall_fullscreen.mjs
|
|
5
|
+
// still drives it through the real shell. Loaded BEFORE shell.js on every hall
|
|
6
|
+
// page; shell.js calls make() once with its own esc() and nav model, and a page
|
|
7
|
+
// without this file simply shows no Back link.
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
function make(ctx) {
|
|
11
|
+
var esc = ctx.esc;
|
|
12
|
+
var NAV = ctx.nav;
|
|
13
|
+
// ---------- the Back link (task 1004430) ----------
|
|
14
|
+
// A page reached from INSIDE another hall page (`?from=`, else a same-origin
|
|
15
|
+
// referrer) shows "← <that page>" in the top bar. A sidebar hop is not a
|
|
16
|
+
// drill-down: its click leaves a one-shot sessionStorage note, and the page it
|
|
17
|
+
// lands on shows none. task 1004477: the ONLY place a page's way back is
|
|
18
|
+
// decided. <body data-back-fallback="/work"> names where Back goes with no
|
|
19
|
+
// origin; a [data-shell-exit] link the page places is filled by the shell.
|
|
20
|
+
var NAV_HOP_KEY = 'hall-nav-hop';
|
|
21
|
+
function hallPath(url) {
|
|
22
|
+
return String(url.pathname).replace(/^\/builders(?=\/|$)/, '').replace(/\.html$/, '').replace(/\/index$/, '') || '/';
|
|
23
|
+
}
|
|
24
|
+
// The page this one was opened from: ?from=, then a same-origin referrer.
|
|
25
|
+
function linkOrigin() {
|
|
26
|
+
var from = null;
|
|
27
|
+
var viaReferrer = false;
|
|
28
|
+
try {
|
|
29
|
+
var q = new URLSearchParams(location.search || '').get('from');
|
|
30
|
+
if (q) from = new URL(q, location.origin);
|
|
31
|
+
else if (document.referrer) { from = new URL(document.referrer); viaReferrer = true; }
|
|
32
|
+
} catch (_) { return null; }
|
|
33
|
+
if (!from || from.origin !== location.origin) return null;
|
|
34
|
+
var path = hallPath(from);
|
|
35
|
+
if (/^\/(api|auth|lib)(\/|$)/.test(path)) return null;
|
|
36
|
+
// Keep the origin's query (a filtered list) but never its own ?from=, which
|
|
37
|
+
// would chain; and compare without OUR ?from=, so a self-link is refused.
|
|
38
|
+
var params = new URLSearchParams(from.search);
|
|
39
|
+
params.delete('from');
|
|
40
|
+
var search = params.toString() ? '?' + params.toString() : '';
|
|
41
|
+
var ownParams = new URLSearchParams(location.search || '');
|
|
42
|
+
ownParams.delete('from');
|
|
43
|
+
var here = hallPath(location) + (ownParams.toString() ? '?' + ownParams.toString() : '');
|
|
44
|
+
if (path + search === here) return null;
|
|
45
|
+
return { href: path + search + (from.hash || ''), path: path, label: backLabel(path, from.hash), viaReferrer: viaReferrer };
|
|
46
|
+
}
|
|
47
|
+
// <body data-back-fallback>, honoured only as a path on this host (never the API).
|
|
48
|
+
function fallbackOrigin() {
|
|
49
|
+
var f = document.body && document.body.dataset && document.body.dataset.backFallback;
|
|
50
|
+
if (!f || !/^\/(?!\/)/.test(f) || /[\\\s]/.test(f)) return null;
|
|
51
|
+
var u;
|
|
52
|
+
try { u = new URL(f, location.origin); } catch (_) { return null; }
|
|
53
|
+
if (u.origin !== location.origin) return null;
|
|
54
|
+
var path = hallPath(u);
|
|
55
|
+
if (/^\/(api|auth|lib)(\/|$)/.test(path) || path === hallPath(location)) return null;
|
|
56
|
+
return { href: path + (u.search || '') + (u.hash || ''), path: path, label: backLabel(path, u.hash), viaReferrer: false };
|
|
57
|
+
}
|
|
58
|
+
// "Back to tasks": a nav label is lowercased mid-sentence unless it has an acronym.
|
|
59
|
+
function exitWords(label) {
|
|
60
|
+
return 'Back to ' + (/[A-Z]{2}/.test(label) ? label : label.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
// Fill every [data-shell-exit] under `root` with the way back (else home); a
|
|
63
|
+
// page that renders one later (task.js's failure states) calls this again.
|
|
64
|
+
function fillExits(root) {
|
|
65
|
+
var scope = root || document;
|
|
66
|
+
if (!scope.querySelectorAll) return;
|
|
67
|
+
var target = BACK || { href: '/', label: 'Home', viaReferrer: false };
|
|
68
|
+
var links = scope.querySelectorAll('[data-shell-exit]');
|
|
69
|
+
for (var i = 0; i < links.length; i++) {
|
|
70
|
+
links[i].setAttribute('href', target.href);
|
|
71
|
+
links[i].textContent = exitWords(target.label);
|
|
72
|
+
if (!links[i].dataset.shellExitWired) {
|
|
73
|
+
links[i].dataset.shellExitWired = '1';
|
|
74
|
+
links[i].addEventListener('click', onBackClick);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function backLabel(path, hash) {
|
|
79
|
+
if (path === '/') {
|
|
80
|
+
var m = /^#\/(task|idea|blocker)\/(\d+)/.exec(hash || '');
|
|
81
|
+
return m ? m[1] + ' ' + m[2] : (/^#standing/.test(hash || '') ? 'Standing' : 'Home');
|
|
82
|
+
}
|
|
83
|
+
var rec = /^\/(task|idea)\/(\d+)$/.exec(path);
|
|
84
|
+
if (rec) return rec[1] + ' ' + rec[2];
|
|
85
|
+
if (path === '/goals' && /^#\/goal\/\d+/.test(hash || '')) return 'goal ' + hash.split('/')[2];
|
|
86
|
+
for (var g = 0; g < NAV.length; g++) {
|
|
87
|
+
for (var i = 0; i < NAV[g].items.length; i++) {
|
|
88
|
+
if (NAV[g].items[i].href === path) return NAV[g].items[i].label;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return 'Back';
|
|
92
|
+
}
|
|
93
|
+
// ---------- the trail (task 1004476) ----------
|
|
94
|
+
// Two clicks deep the bar shows the run of pages that led here, "Goals › goal
|
|
95
|
+
// 12 › task 340", the last crumb being Back. Kept per tab (sessionStorage) as
|
|
96
|
+
// crumbs ending with THIS page: a drill-down cuts after its origin and appends;
|
|
97
|
+
// returning to a crumb (the origin, if any, sits after it) trims; an unknown
|
|
98
|
+
// origin starts fresh by the ?from= / referrer / fallback rules; a sidebar hop
|
|
99
|
+
// starts over. Storage that throws, or no URL API, keeps the one-step Back.
|
|
100
|
+
var TRAIL_KEY = 'hall-nav-trail', TRAIL_MAX = 4, BACK = null;
|
|
101
|
+
function hereCrumb() {
|
|
102
|
+
var own = new URLSearchParams(location.search || ''); own.delete('from');
|
|
103
|
+
var path = hallPath(location), hash = location.hash || '';
|
|
104
|
+
return { href: path + (own.toString() ? '?' + own.toString() : '') + hash, label: backLabel(path, hash) };
|
|
105
|
+
}
|
|
106
|
+
// A stored crumb is honoured only as a path on this host with a string label.
|
|
107
|
+
function crumbOk(c) {
|
|
108
|
+
return !!c && typeof c.href === 'string' && typeof c.label === 'string' &&
|
|
109
|
+
/^\/(?!\/)/.test(c.href) && !/[\\\s]/.test(c.href) && !/^\/(api|auth|lib)(\/|[?#]|$)/.test(c.href);
|
|
110
|
+
}
|
|
111
|
+
function readTrail() {
|
|
112
|
+
try { var t = JSON.parse(window.sessionStorage.getItem(TRAIL_KEY) || '[]'); return Array.isArray(t) ? t.filter(crumbOk) : []; } catch (_) { return null; }
|
|
113
|
+
}
|
|
114
|
+
function writeTrail(t) {
|
|
115
|
+
try { window.sessionStorage.setItem(TRAIL_KEY, JSON.stringify(t.slice(-(TRAIL_MAX + 1)))); } catch (_) { /* private mode */ }
|
|
116
|
+
}
|
|
117
|
+
// Latest crumb at `href`; a referrer has no #hash, so it matches on the address alone.
|
|
118
|
+
function crumbAt(t, href, anyHash) {
|
|
119
|
+
for (var i = t.length - 1; i >= 0; i--) if (anyHash ? t[i].href.split('#')[0] === href.split('#')[0] : t[i].href === href) return i;
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
// The crumbs before this page; the last is where Back goes. Sets BACK.
|
|
123
|
+
function trailCrumbs() {
|
|
124
|
+
var hop = null, here = null;
|
|
125
|
+
try { hop = window.sessionStorage.getItem(NAV_HOP_KEY); window.sessionStorage.removeItem(NAV_HOP_KEY); } catch (_) { /* private mode */ }
|
|
126
|
+
try { here = hereCrumb(); } catch (_) { /* no URL API */ }
|
|
127
|
+
var stored = here ? readTrail() : null, t = stored || [], trail = null, via = false;
|
|
128
|
+
here = here || { href: '', label: '' };
|
|
129
|
+
if (hop && hop === hallPath(location)) trail = [here];
|
|
130
|
+
var origin = trail ? null : linkOrigin();
|
|
131
|
+
var at = crumbAt(t, here.href), from = origin ? crumbAt(t, origin.href, origin.viaReferrer) : -1;
|
|
132
|
+
var f = trail || origin || at >= 0 ? null : fallbackOrigin();
|
|
133
|
+
if (!trail && at >= 0 && (!origin || from > at)) trail = t.slice(0, at + 1);
|
|
134
|
+
else if (!trail && origin) { trail = (from >= 0 ? t.slice(0, from + 1) : [origin]).concat([here]); via = origin.viaReferrer; }
|
|
135
|
+
else if (!trail) trail = f ? [f, here] : [here];
|
|
136
|
+
trail = trail.slice(-(TRAIL_MAX + 1)).map(function (c) { return { href: c.href, label: c.label }; });
|
|
137
|
+
if (stored) writeTrail(trail);
|
|
138
|
+
var crumbs = trail.slice(0, -1), last = crumbs[crumbs.length - 1];
|
|
139
|
+
BACK = last ? { href: last.href, label: last.label, viaReferrer: via } : null;
|
|
140
|
+
return crumbs;
|
|
141
|
+
}
|
|
142
|
+
// A hash route moves without a page load: a record view is a drill-down, a
|
|
143
|
+
// crumb already held is a return, any other view just renames this page's crumb.
|
|
144
|
+
function onTrailHash() {
|
|
145
|
+
var t = readTrail(), here;
|
|
146
|
+
if (!t || !t.length || t[t.length - 1].href.split(/[?#]/)[0] !== hallPath(location)) return;
|
|
147
|
+
try { here = hereCrumb(); } catch (_) { return; }
|
|
148
|
+
var at = crumbAt(t, here.href);
|
|
149
|
+
if (at >= 0) t = t.slice(0, at + 1);
|
|
150
|
+
else if (/^#\/(task|idea|blocker|goal)\/\d+/.test(location.hash || '')) t = t.concat([here]);
|
|
151
|
+
else t[t.length - 1] = here;
|
|
152
|
+
writeTrail(t);
|
|
153
|
+
}
|
|
154
|
+
var BACK_ICON = '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4 6 9l5 5"/></svg>';
|
|
155
|
+
// Narrower windows fold the trail (style.css): the middle goes to "…", then
|
|
156
|
+
// the first crumb joins it and "…" links to the start, then only Back is left.
|
|
157
|
+
function backLinkHtml() {
|
|
158
|
+
var crumbs = trailCrumbs(), sep = '<span class="topbar-trail__sep" aria-hidden="true">›</span>';
|
|
159
|
+
if (!BACK) return '';
|
|
160
|
+
var back = '<a class="topbar-back" id="topbar-back" href="' + esc(BACK.href) + '" title="Back to ' + esc(BACK.label) + '">' +
|
|
161
|
+
BACK_ICON + '<span class="topbar-back__label">' + esc(BACK.label) + '</span></a>';
|
|
162
|
+
if (crumbs.length < 2) return back;
|
|
163
|
+
var older = crumbs.slice(0, -1), steps = older.map(function (c, i) {
|
|
164
|
+
return '<span class="topbar-trail__step topbar-trail__step--' + (i ? 'mid' : 'first') + '"><a class="topbar-crumb" href="' +
|
|
165
|
+
esc(c.href) + '">' + esc(c.label) + '</a>' + sep + '</span>';
|
|
166
|
+
});
|
|
167
|
+
steps.splice(1, 0, '<span class="topbar-trail__more"><a class="topbar-crumb topbar-crumb--more" href="' + esc(older[0].href) + '" title="' +
|
|
168
|
+
esc(older.map(function (c) { return c.label; }).join(' › ')) + '" aria-label="' + esc(older[0].label) + '">…</a>' + sep + '</span>');
|
|
169
|
+
return '<nav class="topbar-trail' + (older.length > 1 ? ' topbar-trail--long' : '') + '" aria-label="Breadcrumb">' +
|
|
170
|
+
BACK_ICON.replace('<svg ', '<svg class="topbar-trail__mark" ') + steps.join('') + back + '</nav>';
|
|
171
|
+
}
|
|
172
|
+
// Back through history when the origin IS the previous entry (the list keeps its
|
|
173
|
+
// scroll and filters); the href stays the real address for a new tab.
|
|
174
|
+
function onBackClick(e) {
|
|
175
|
+
if (!BACK || !BACK.viaReferrer || e.metaKey || e.ctrlKey || e.shiftKey || e.button > 0) return;
|
|
176
|
+
if (window.history && window.history.length > 1) { e.preventDefault(); window.history.back(); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// A sidebar hop is not a drill-down: the page it opens shows no Back link.
|
|
180
|
+
function noteHop(href) {
|
|
181
|
+
try { window.sessionStorage.setItem(NAV_HOP_KEY, hallPath(new URL(href, location.href))); } catch (_) { /* private mode */ }
|
|
182
|
+
}
|
|
183
|
+
return { backLinkHtml: backLinkHtml, fillExits: fillExits, onBackClick: onBackClick, onTrailHash: onTrailHash, noteHop: noteHop };
|
|
184
|
+
}
|
|
185
|
+
window.OTBShellTrail = { make: make };
|
|
186
|
+
})();
|
|
@@ -483,7 +483,7 @@
|
|
|
483
483
|
'<button class="topbar-menu" id="topbar-menu" type="button" aria-label="Open navigation" aria-expanded="false">' +
|
|
484
484
|
'<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3 5h12M3 9h12M3 13h12"/></svg>' +
|
|
485
485
|
'</button>' +
|
|
486
|
-
backLinkHtml() +
|
|
486
|
+
TRAIL.backLinkHtml() +
|
|
487
487
|
'<h2 class="topbar-title" id="topbar-title"></h2>' +
|
|
488
488
|
'<div class="topbar-side"><span class="topbar-note" id="welcome-line"></span>' +
|
|
489
489
|
// task 2178: the jump-palette slot. Rendered inert + hidden — palette.js
|
|
@@ -495,6 +495,7 @@
|
|
|
495
495
|
'<span class="topbar-jump__label">Jump to…</span>' +
|
|
496
496
|
'<kbd class="topbar-jump__kbd" id="topbar-jump-kbd"></kbd>' +
|
|
497
497
|
'</button>' +
|
|
498
|
+
(CRAFT ? CRAFT.switchHtml() : '') +
|
|
498
499
|
'<button class="zoom-toggle" id="zoom-toggle" type="button" aria-label="Text size"></button>' +
|
|
499
500
|
'<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Switch theme"></button>' +
|
|
500
501
|
// task 1004327: the full-screen switch, LAST so it sits in the corner the
|
|
@@ -506,171 +507,13 @@
|
|
|
506
507
|
if (t) t.textContent = title;
|
|
507
508
|
}
|
|
508
509
|
|
|
509
|
-
// ---------- the
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
var NAV_HOP_KEY = 'hall-nav-hop';
|
|
517
|
-
function hallPath(url) {
|
|
518
|
-
return String(url.pathname).replace(/^\/builders(?=\/|$)/, '').replace(/\.html$/, '').replace(/\/index$/, '') || '/';
|
|
519
|
-
}
|
|
520
|
-
// The page this one was opened from: ?from=, then a same-origin referrer.
|
|
521
|
-
function linkOrigin() {
|
|
522
|
-
var from = null;
|
|
523
|
-
var viaReferrer = false;
|
|
524
|
-
try {
|
|
525
|
-
var q = new URLSearchParams(location.search || '').get('from');
|
|
526
|
-
if (q) from = new URL(q, location.origin);
|
|
527
|
-
else if (document.referrer) { from = new URL(document.referrer); viaReferrer = true; }
|
|
528
|
-
} catch (_) { return null; }
|
|
529
|
-
if (!from || from.origin !== location.origin) return null;
|
|
530
|
-
var path = hallPath(from);
|
|
531
|
-
if (/^\/(api|auth|lib)(\/|$)/.test(path)) return null;
|
|
532
|
-
// Keep the origin's query (a filtered list) but never its own ?from=, which
|
|
533
|
-
// would chain; and compare without OUR ?from=, so a self-link is refused.
|
|
534
|
-
var params = new URLSearchParams(from.search);
|
|
535
|
-
params.delete('from');
|
|
536
|
-
var search = params.toString() ? '?' + params.toString() : '';
|
|
537
|
-
var ownParams = new URLSearchParams(location.search || '');
|
|
538
|
-
ownParams.delete('from');
|
|
539
|
-
var here = hallPath(location) + (ownParams.toString() ? '?' + ownParams.toString() : '');
|
|
540
|
-
if (path + search === here) return null;
|
|
541
|
-
return { href: path + search + (from.hash || ''), path: path, label: backLabel(path, from.hash), viaReferrer: viaReferrer };
|
|
542
|
-
}
|
|
543
|
-
// <body data-back-fallback>, honoured only as a path on this host (never the API).
|
|
544
|
-
function fallbackOrigin() {
|
|
545
|
-
var f = document.body && document.body.dataset && document.body.dataset.backFallback;
|
|
546
|
-
if (!f || !/^\/(?!\/)/.test(f) || /[\\\s]/.test(f)) return null;
|
|
547
|
-
var u;
|
|
548
|
-
try { u = new URL(f, location.origin); } catch (_) { return null; }
|
|
549
|
-
if (u.origin !== location.origin) return null;
|
|
550
|
-
var path = hallPath(u);
|
|
551
|
-
if (/^\/(api|auth|lib)(\/|$)/.test(path) || path === hallPath(location)) return null;
|
|
552
|
-
return { href: path + (u.search || '') + (u.hash || ''), path: path, label: backLabel(path, u.hash), viaReferrer: false };
|
|
553
|
-
}
|
|
554
|
-
// "Back to tasks": a nav label is lowercased mid-sentence unless it has an acronym.
|
|
555
|
-
function exitWords(label) {
|
|
556
|
-
return 'Back to ' + (/[A-Z]{2}/.test(label) ? label : label.toLowerCase());
|
|
557
|
-
}
|
|
558
|
-
// Fill every [data-shell-exit] under `root` with the way back (else home); a
|
|
559
|
-
// page that renders one later (task.js's failure states) calls this again.
|
|
560
|
-
function fillExits(root) {
|
|
561
|
-
var scope = root || document;
|
|
562
|
-
if (!scope.querySelectorAll) return;
|
|
563
|
-
var target = BACK || { href: '/', label: 'Home', viaReferrer: false };
|
|
564
|
-
var links = scope.querySelectorAll('[data-shell-exit]');
|
|
565
|
-
for (var i = 0; i < links.length; i++) {
|
|
566
|
-
links[i].setAttribute('href', target.href);
|
|
567
|
-
links[i].textContent = exitWords(target.label);
|
|
568
|
-
if (!links[i].dataset.shellExitWired) {
|
|
569
|
-
links[i].dataset.shellExitWired = '1';
|
|
570
|
-
links[i].addEventListener('click', onBackClick);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
function backLabel(path, hash) {
|
|
575
|
-
if (path === '/') {
|
|
576
|
-
var m = /^#\/(task|idea|blocker)\/(\d+)/.exec(hash || '');
|
|
577
|
-
return m ? m[1] + ' ' + m[2] : (/^#standing/.test(hash || '') ? 'Standing' : 'Home');
|
|
578
|
-
}
|
|
579
|
-
var rec = /^\/(task|idea)\/(\d+)$/.exec(path);
|
|
580
|
-
if (rec) return rec[1] + ' ' + rec[2];
|
|
581
|
-
if (path === '/goals' && /^#\/goal\/\d+/.test(hash || '')) return 'goal ' + hash.split('/')[2];
|
|
582
|
-
for (var g = 0; g < NAV.length; g++) {
|
|
583
|
-
for (var i = 0; i < NAV[g].items.length; i++) {
|
|
584
|
-
if (NAV[g].items[i].href === path) return NAV[g].items[i].label;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return 'Back';
|
|
588
|
-
}
|
|
589
|
-
// ---------- the trail (task 1004476) ----------
|
|
590
|
-
// Two clicks deep the bar shows the run of pages that led here, "Goals › goal
|
|
591
|
-
// 12 › task 340", the last crumb being Back. Kept per tab (sessionStorage) as
|
|
592
|
-
// crumbs ending with THIS page: a drill-down cuts after its origin and appends;
|
|
593
|
-
// returning to a crumb (the origin, if any, sits after it) trims; an unknown
|
|
594
|
-
// origin starts fresh by the ?from= / referrer / fallback rules; a sidebar hop
|
|
595
|
-
// starts over. Storage that throws, or no URL API, keeps the one-step Back.
|
|
596
|
-
var TRAIL_KEY = 'hall-nav-trail', TRAIL_MAX = 4, BACK = null;
|
|
597
|
-
function hereCrumb() {
|
|
598
|
-
var own = new URLSearchParams(location.search || ''); own.delete('from');
|
|
599
|
-
var path = hallPath(location), hash = location.hash || '';
|
|
600
|
-
return { href: path + (own.toString() ? '?' + own.toString() : '') + hash, label: backLabel(path, hash) };
|
|
601
|
-
}
|
|
602
|
-
// A stored crumb is honoured only as a path on this host with a string label.
|
|
603
|
-
function crumbOk(c) {
|
|
604
|
-
return !!c && typeof c.href === 'string' && typeof c.label === 'string' &&
|
|
605
|
-
/^\/(?!\/)/.test(c.href) && !/[\\\s]/.test(c.href) && !/^\/(api|auth|lib)(\/|[?#]|$)/.test(c.href);
|
|
606
|
-
}
|
|
607
|
-
function readTrail() {
|
|
608
|
-
try { var t = JSON.parse(window.sessionStorage.getItem(TRAIL_KEY) || '[]'); return Array.isArray(t) ? t.filter(crumbOk) : []; } catch (_) { return null; }
|
|
609
|
-
}
|
|
610
|
-
function writeTrail(t) {
|
|
611
|
-
try { window.sessionStorage.setItem(TRAIL_KEY, JSON.stringify(t.slice(-(TRAIL_MAX + 1)))); } catch (_) { /* private mode */ }
|
|
612
|
-
}
|
|
613
|
-
// Latest crumb at `href`; a referrer has no #hash, so it matches on the address alone.
|
|
614
|
-
function crumbAt(t, href, anyHash) {
|
|
615
|
-
for (var i = t.length - 1; i >= 0; i--) if (anyHash ? t[i].href.split('#')[0] === href.split('#')[0] : t[i].href === href) return i;
|
|
616
|
-
return -1;
|
|
617
|
-
}
|
|
618
|
-
// The crumbs before this page; the last is where Back goes. Sets BACK.
|
|
619
|
-
function trailCrumbs() {
|
|
620
|
-
var hop = null, here = null;
|
|
621
|
-
try { hop = window.sessionStorage.getItem(NAV_HOP_KEY); window.sessionStorage.removeItem(NAV_HOP_KEY); } catch (_) { /* private mode */ }
|
|
622
|
-
try { here = hereCrumb(); } catch (_) { /* no URL API */ }
|
|
623
|
-
var stored = here ? readTrail() : null, t = stored || [], trail = null, via = false;
|
|
624
|
-
here = here || { href: '', label: '' };
|
|
625
|
-
if (hop && hop === hallPath(location)) trail = [here];
|
|
626
|
-
var origin = trail ? null : linkOrigin();
|
|
627
|
-
var at = crumbAt(t, here.href), from = origin ? crumbAt(t, origin.href, origin.viaReferrer) : -1;
|
|
628
|
-
var f = trail || origin || at >= 0 ? null : fallbackOrigin();
|
|
629
|
-
if (!trail && at >= 0 && (!origin || from > at)) trail = t.slice(0, at + 1);
|
|
630
|
-
else if (!trail && origin) { trail = (from >= 0 ? t.slice(0, from + 1) : [origin]).concat([here]); via = origin.viaReferrer; }
|
|
631
|
-
else if (!trail) trail = f ? [f, here] : [here];
|
|
632
|
-
trail = trail.slice(-(TRAIL_MAX + 1)).map(function (c) { return { href: c.href, label: c.label }; });
|
|
633
|
-
if (stored) writeTrail(trail);
|
|
634
|
-
var crumbs = trail.slice(0, -1), last = crumbs[crumbs.length - 1];
|
|
635
|
-
BACK = last ? { href: last.href, label: last.label, viaReferrer: via } : null;
|
|
636
|
-
return crumbs;
|
|
637
|
-
}
|
|
638
|
-
// A hash route moves without a page load: a record view is a drill-down, a
|
|
639
|
-
// crumb already held is a return, any other view just renames this page's crumb.
|
|
640
|
-
function onTrailHash() {
|
|
641
|
-
var t = readTrail(), here;
|
|
642
|
-
if (!t || !t.length || t[t.length - 1].href.split(/[?#]/)[0] !== hallPath(location)) return;
|
|
643
|
-
try { here = hereCrumb(); } catch (_) { return; }
|
|
644
|
-
var at = crumbAt(t, here.href);
|
|
645
|
-
if (at >= 0) t = t.slice(0, at + 1);
|
|
646
|
-
else if (/^#\/(task|idea|blocker|goal)\/\d+/.test(location.hash || '')) t = t.concat([here]);
|
|
647
|
-
else t[t.length - 1] = here;
|
|
648
|
-
writeTrail(t);
|
|
649
|
-
}
|
|
650
|
-
var BACK_ICON = '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4 6 9l5 5"/></svg>';
|
|
651
|
-
// Narrower windows fold the trail (style.css): the middle goes to "…", then
|
|
652
|
-
// the first crumb joins it and "…" links to the start, then only Back is left.
|
|
653
|
-
function backLinkHtml() {
|
|
654
|
-
var crumbs = trailCrumbs(), sep = '<span class="topbar-trail__sep" aria-hidden="true">›</span>';
|
|
655
|
-
if (!BACK) return '';
|
|
656
|
-
var back = '<a class="topbar-back" id="topbar-back" href="' + esc(BACK.href) + '" title="Back to ' + esc(BACK.label) + '">' +
|
|
657
|
-
BACK_ICON + '<span class="topbar-back__label">' + esc(BACK.label) + '</span></a>';
|
|
658
|
-
if (crumbs.length < 2) return back;
|
|
659
|
-
var older = crumbs.slice(0, -1), steps = older.map(function (c, i) {
|
|
660
|
-
return '<span class="topbar-trail__step topbar-trail__step--' + (i ? 'mid' : 'first') + '"><a class="topbar-crumb" href="' +
|
|
661
|
-
esc(c.href) + '">' + esc(c.label) + '</a>' + sep + '</span>';
|
|
662
|
-
});
|
|
663
|
-
steps.splice(1, 0, '<span class="topbar-trail__more"><a class="topbar-crumb topbar-crumb--more" href="' + esc(older[0].href) + '" title="' +
|
|
664
|
-
esc(older.map(function (c) { return c.label; }).join(' › ')) + '" aria-label="' + esc(older[0].label) + '">…</a>' + sep + '</span>');
|
|
665
|
-
return '<nav class="topbar-trail' + (older.length > 1 ? ' topbar-trail--long' : '') + '" aria-label="Breadcrumb">' +
|
|
666
|
-
BACK_ICON.replace('<svg ', '<svg class="topbar-trail__mark" ') + steps.join('') + back + '</nav>';
|
|
667
|
-
}
|
|
668
|
-
// Back through history when the origin IS the previous entry (the list keeps its
|
|
669
|
-
// scroll and filters); the href stays the real address for a new tab.
|
|
670
|
-
function onBackClick(e) {
|
|
671
|
-
if (!BACK || !BACK.viaReferrer || e.metaKey || e.ctrlKey || e.shiftKey || e.button > 0) return;
|
|
672
|
-
if (window.history && window.history.length > 1) { e.preventDefault(); window.history.back(); }
|
|
673
|
-
}
|
|
510
|
+
// ---------- the way back (task 1004430/1004476/1004477) — shell-trail.js ----------
|
|
511
|
+
// task 1004492: the craft mode (craft-mode.js) — the top bar's mode switch and the picker.
|
|
512
|
+
var CRAFT = window.OTBCraftMode || null;
|
|
513
|
+
var TRAIL = window.OTBShellTrail ? window.OTBShellTrail.make({ esc: esc, nav: NAV }) : {
|
|
514
|
+
backLinkHtml: function () { return ''; }, fillExits: function () {}, onBackClick: function () {},
|
|
515
|
+
onTrailHash: function () {}, noteHop: function () {},
|
|
516
|
+
};
|
|
674
517
|
|
|
675
518
|
// ---------- full screen (task 1004327; standard since task 1004430) ----------
|
|
676
519
|
// One switch on EVERY shelled page hides the sidebar, top bar and foot; the same
|
|
@@ -1210,6 +1053,8 @@
|
|
|
1210
1053
|
// AFTER the gate loop for the same reason applyCounts is: it reads the settled
|
|
1211
1054
|
// permission map, which the loop above is what the shell has just been told.
|
|
1212
1055
|
maybeRenderCoreUpdate();
|
|
1056
|
+
// task 1004492: a signed-in builder with no mode yet this session is asked once.
|
|
1057
|
+
if (CRAFT) CRAFT.onAccess(access, state && state.builder);
|
|
1213
1058
|
var signin = document.getElementById('shell-signin');
|
|
1214
1059
|
if (signin) signin.hidden = access.authed !== false;
|
|
1215
1060
|
var user = document.getElementById('shell-user');
|
|
@@ -1383,14 +1228,13 @@
|
|
|
1383
1228
|
}
|
|
1384
1229
|
var a = e.target.closest && e.target.closest('a');
|
|
1385
1230
|
if (a) {
|
|
1386
|
-
|
|
1387
|
-
try { window.sessionStorage.setItem(NAV_HOP_KEY, hallPath(new URL(a.href, location.href))); } catch (_) { /* private mode */ }
|
|
1231
|
+
TRAIL.noteHop(a.href);
|
|
1388
1232
|
closeDrawer();
|
|
1389
1233
|
}
|
|
1390
1234
|
});
|
|
1391
1235
|
var back = document.getElementById('topbar-back');
|
|
1392
|
-
if (back) back.addEventListener('click', onBackClick);
|
|
1393
|
-
window.addEventListener('hashchange', onTrailHash);
|
|
1236
|
+
if (back) back.addEventListener('click', TRAIL.onBackClick);
|
|
1237
|
+
window.addEventListener('hashchange', TRAIL.onTrailHash);
|
|
1394
1238
|
if (IS_INDEX) {
|
|
1395
1239
|
window.addEventListener('hashchange', function () { route(); });
|
|
1396
1240
|
}
|
|
@@ -1463,10 +1307,11 @@
|
|
|
1463
1307
|
renderFoot();
|
|
1464
1308
|
applyTheme();
|
|
1465
1309
|
applyZoom();
|
|
1310
|
+
if (CRAFT) CRAFT.mount();
|
|
1466
1311
|
applyFullscreen(storedFullscreen());
|
|
1467
1312
|
applyAccess(null); // default: everything gated until access is known
|
|
1468
1313
|
wire();
|
|
1469
|
-
fillExits();
|
|
1314
|
+
TRAIL.fillExits();
|
|
1470
1315
|
if (IS_INDEX) {
|
|
1471
1316
|
route({ scroll: false });
|
|
1472
1317
|
} else {
|
|
@@ -1494,6 +1339,6 @@
|
|
|
1494
1339
|
// and street) repaints the top bar's after writing 'hall-theme'.
|
|
1495
1340
|
applyTheme: applyTheme,
|
|
1496
1341
|
// task 1004477: a page that renders an exit after boot asks the shell to fill it.
|
|
1497
|
-
fillExits: fillExits,
|
|
1342
|
+
fillExits: TRAIL.fillExits,
|
|
1498
1343
|
};
|
|
1499
1344
|
})();
|
|
@@ -203,7 +203,9 @@
|
|
|
203
203
|
</div>
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
206
|
+
<script src="/builders/shell-trail.js?v=2026-10-02-craft"></script>
|
|
207
|
+
<script src="/builders/craft-mode.js?v=2026-10-02-craft"></script>
|
|
208
|
+
<script src="/builders/shell.js?v=2026-10-02-craft"></script>
|
|
207
209
|
<script src="/builders/palette.js" defer></script>
|
|
208
210
|
<script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
|
|
209
211
|
<script src="/builders/street-draw.js?v=2026-10-02-street1"></script>
|
|
@@ -176,7 +176,9 @@
|
|
|
176
176
|
|
|
177
177
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
178
178
|
|
|
179
|
-
<script src="/builders/shell.js?v=2026-10-02-
|
|
179
|
+
<script src="/builders/shell-trail.js?v=2026-10-02-craft"></script>
|
|
180
|
+
<script src="/builders/craft-mode.js?v=2026-10-02-craft"></script>
|
|
181
|
+
<script src="/builders/shell.js?v=2026-10-02-craft"></script>
|
|
180
182
|
<script src="/builders/palette.js" defer></script>
|
|
181
183
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
182
184
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|