@bongos/core 1.19.1051 → 1.19.1053
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 +66 -61
- package/docs/copy-inventory.md +138 -146
- package/docs/copy-registry.json +294 -458
- package/docs/module-api-changelog.md +4 -0
- 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/collab.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 +18 -24
- package/modules/hall-ui/public/goal-map.states.json +2 -2
- 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 +77 -1
- package/modules/hall-ui/public/sky-data.js +34 -19
- package/modules/hall-ui/public/sky-interaction.js +36 -30
- package/modules/hall-ui/public/sky-layout.js +11 -6
- package/modules/hall-ui/public/sky-panel.js +3 -2
- package/modules/hall-ui/public/sky-tour.js +37 -26
- package/modules/hall-ui/public/studio.html +10 -4
- package/modules/hall-ui/public/studio.states.json +76 -1
- package/modules/hall-ui/public/style.css +53 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +56 -16
- package/modules/hall-ui/public/thinking.html +52 -41
- package/modules/hall-ui/public/thinking.states.json +20 -3
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/sky.md +20 -0
- 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/goal_map_page.mjs +54 -28
- package/tests/hall_audit.mjs +2 -1
- package/tests/hall_fullscreen.mjs +196 -0
- package/tests/hall_sky.mjs +49 -12
- package/tests/helpers.mjs +2 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The prototype's "hover card + shuttle menu", "interaction" and "lenses and
|
|
7
7
|
// toggles" sections, verbatim but for the shared state on `SKY.S`. The one
|
|
8
|
-
// behaviour this part CHANGES on purpose: the
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// walks all three. And the "claude layer" pill only appears once part 5's
|
|
8
|
+
// behaviour this part CHANGES on purpose: the two skies are separate (task
|
|
9
|
+
// 1004328, owner decision 2026-09-27). /thinking's switcher is "simple" and
|
|
10
|
+
// "your thinking" and nothing else; /goal-map has no switcher, only "the
|
|
11
|
+
// whole project". setLens stays inside the page's own `S.lenses`, and no
|
|
12
|
+
// button leaves for the other page. And the "claude layer" pill only appears once part 5's
|
|
14
13
|
// `filed_via` exists on at least one thought; until then it is hidden and
|
|
15
14
|
// every thought shows (the README's "must not ship" row).
|
|
16
15
|
//
|
|
@@ -21,7 +20,7 @@
|
|
|
21
20
|
'use strict';
|
|
22
21
|
|
|
23
22
|
const SKY = window.OTBSky; const S = SKY.S;
|
|
24
|
-
const { $, TAU, hash, clamp, esc, fmtDay, short,
|
|
23
|
+
const { $, hide, press, on, TAU, hash, clamp, esc, fmtDay, short, OBJ, diameter, reach, became, inLayer, shown, seen, present, toWorld, bandAt, cv, hit, edgesOf } = SKY;
|
|
25
24
|
|
|
26
25
|
// ---------------------------------------------------------------- hover card + shuttle menu
|
|
27
26
|
const card = $('card'); const menu = $('menu');
|
|
@@ -57,57 +56,63 @@
|
|
|
57
56
|
menu.addEventListener('pointerleave', () => { S.menuTimer = setTimeout(hideMenu, 300); });
|
|
58
57
|
|
|
59
58
|
// ---------------------------------------------------------------- interaction
|
|
60
|
-
|
|
59
|
+
// A pointer's position on the canvas: its client position less the offset of
|
|
60
|
+
// the box the sky is drawn in (zero on /goal-map; the stage's on /thinking).
|
|
61
|
+
const px = (e) => e.clientX - (S.OX || 0); const py = (e) => e.clientY - (S.OY || 0);
|
|
62
|
+
cv.addEventListener('pointerdown', (e) => { S.drag = { x: px(e), y: py(e), cx: S.cam.x, cy: S.cam.y, moved: false }; if (cv.setPointerCapture) cv.setPointerCapture(e.pointerId); cv.classList.add('dragging'); S.flight = null; showCard(null); });
|
|
61
63
|
cv.addEventListener('pointermove', (e) => {
|
|
62
64
|
const { cam } = S;
|
|
63
|
-
if (S.drag) { if (S.lens === 'simple') return; const dx = e
|
|
64
|
-
const h = hit(e
|
|
65
|
+
if (S.drag) { if (S.lens === 'simple') return; const dx = px(e) - S.drag.x, dy = py(e) - S.drag.y; if (Math.hypot(dx, dy) > 3) S.drag.moved = true; cam.x = S.drag.cx - dx / cam.k; cam.y = S.drag.cy - dy / cam.k; S.dirty = true; return; }
|
|
66
|
+
const h = hit(px(e), py(e)); const hover = S.hover; const same = h && hover && h.kind === hover.kind && h.ref === hover.ref && h.index === hover.index && h.goal === hover.goal;
|
|
65
67
|
if (!same) { S.hover = h; S.dirty = true; cv.style.cursor = h && h.kind !== 'band' ? 'pointer' : 'grab'; }
|
|
66
|
-
showCard(h, e
|
|
68
|
+
showCard(h, px(e), py(e));
|
|
67
69
|
if (h && h.kind === 'shuttle') { if (S.menuEdge !== h.ref) showMenu(h.ref, h.x, h.y); } else if (S.menuEdge && menu.hidden === false) { clearTimeout(S.menuTimer); S.menuTimer = setTimeout(hideMenu, 350); }
|
|
68
70
|
});
|
|
69
71
|
cv.addEventListener('pointerleave', () => { S.hover = null; showCard(null); S.dirty = true; });
|
|
70
|
-
cv.addEventListener('pointerup', (e) => { cv.classList.remove('dragging'); if (S.drag && !S.drag.moved) { const h = hit(e
|
|
71
|
-
cv.addEventListener('dblclick', (e) => { const h = hit(e
|
|
72
|
-
cv.addEventListener('wheel', (e) => { e.preventDefault(); if (S.lens === 'simple') return; const { cam } = S; const f = Math.exp(-e.deltaY * 0.0016); const nk = clamp(cam.k * f, 0.015, 9); const [wx, wy] = toWorld(e
|
|
72
|
+
cv.addEventListener('pointerup', (e) => { cv.classList.remove('dragging'); if (S.drag && !S.drag.moved) { const h = hit(px(e), py(e)); if (h && h.kind === 'stub') SKY.chartRoute(h.edge, h.goal); else if (h && h.kind === 'shuttle') showMenu(h.ref, h.x, h.y); else if (h && h.kind === 'ring') pick({ kind: 'goal', ref: h.goal }, false); else if (h && h.kind === 'goal' && !seen(h.ref)) { /* not reached yet */ } else if (h && h.kind === 'band') { pick(null); SKY.fitBand(h.ref); } else pick(h, false); } S.drag = null; });
|
|
73
|
+
cv.addEventListener('dblclick', (e) => { const h = hit(px(e), py(e)); if (h && h.kind === 'stub') SKY.chartAllOf(h.goal); else if (h && (h.kind === 'goal' || h.kind === 'ring') && seen(h.kind === 'goal' ? h.ref : h.goal)) SKY.fitGoal(h.kind === 'goal' ? h.ref : h.goal); else { const [wx, wy] = toWorld(px(e), py(e)); SKY.flyTo(wx, wy, S.cam.k * 2.2, 700); } });
|
|
74
|
+
cv.addEventListener('wheel', (e) => { e.preventDefault(); if (S.lens === 'simple') return; const { cam } = S; const f = Math.exp(-e.deltaY * 0.0016); const nk = clamp(cam.k * f, 0.015, 9); const [wx, wy] = toWorld(px(e), py(e)); cam.x = wx - (px(e) - S.W / 2) / nk; cam.y = wy - (py(e) - S.H / 2) / nk; cam.k = nk; S.flight = null; S.dirty = true; showCard(null); }, { passive: false });
|
|
75
|
+
// An Escape the sky USED (the tour, the menu, a pick) calls preventDefault, so
|
|
76
|
+
// the shell's full-screen switch leaves that press alone (task 1004327).
|
|
73
77
|
addEventListener('keydown', (e) => {
|
|
74
78
|
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) { if (e.key === 'Escape') e.target.blur(); return; }
|
|
75
79
|
const { cam } = S; const step = 60 / cam.k;
|
|
76
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; }
|
|
77
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; }
|
|
78
|
-
else if (e.key === '0') SKY.fitAll(); else if (e.key === 'Escape') { if (!$('tour').hidden) SKY.endTour(); else if (!menu.hidden) hideMenu(); else 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(); $('thought').focus(); }
|
|
79
83
|
else if (e.key === '?') SKY.startTour(); else if (e.key.toLowerCase() === 'l') SKY.toggleRoom(); else return;
|
|
80
84
|
});
|
|
81
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); } }
|
|
82
86
|
|
|
83
87
|
// ---------------------------------------------------------------- lenses and toggles
|
|
84
88
|
function setLens(l, noFly) {
|
|
89
|
+
// A lens this page does not have is never entered (task 1004328): the page's own home stands in.
|
|
90
|
+
if (!S.lenses.includes(l)) l = S.home;
|
|
85
91
|
S.lens = l;
|
|
86
|
-
for (const [id, v] of [['lensSimple', 'simple'], ['lensMine', 'mine']
|
|
92
|
+
for (const [id, v] of [['lensSimple', 'simple'], ['lensMine', 'mine']]) press(id, l === v);
|
|
87
93
|
$('lensWord').textContent = l === 'simple' ? '— simply' : l === 'mine' ? '— your thinking' : '— the whole project'; document.querySelector('.brand h1').firstChild.textContent = l === 'all' ? 'the sky ' : 'your sky ';
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
hide('claudeBtn', l !== 'mine' || !S.hasFiledVia); hide('influenceBtn', l !== 'mine'); hide('ideasBtn', l !== 'all'); hide('exploreBtn', l !== 'all'); $('rehearse').hidden = !(l === 'mine' || (l === 'all' && S.ideasOnMap));
|
|
95
|
+
hide('recentBtn', l !== 'simple'); $('roomBtn').hidden = l === 'simple'; $('clockBtn').hidden = l === 'simple'; $('time').hidden = S.clockHidden || l === 'simple'; document.querySelector('.jump').hidden = l === 'simple'; $('legend').hidden = l === 'simple';
|
|
90
96
|
if (l !== 'all') SKY.clearCharts();
|
|
91
97
|
if (l === 'simple') { SKY.toggleRoom(false); pick(null); }
|
|
92
98
|
if (!noFly) { if (l === 'all') SKY.fitAll(); else SKY.fitPresent(); }
|
|
93
99
|
SKY.maybeStart(); SKY.renderRoom(); updateStats(); S.dirty = true;
|
|
94
100
|
}
|
|
95
|
-
// The switcher is
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
$('lensSimple').addEventListener('click', () => (S.home === 'all' ? leave('/thinking') : setLens('simple')));
|
|
101
|
-
$('lensMine').addEventListener('click', () => (S.home === 'all' ? leave('/thinking') : setLens('mine')));
|
|
102
|
-
$('lensAll').addEventListener('click', () => (S.home === 'all' ? setLens('all') : leave('/goal-map')));
|
|
101
|
+
// The switcher is /thinking's alone (task 1004328): "simple" and "your
|
|
102
|
+
// thinking", both switching in place. /goal-map carries no switcher, and no
|
|
103
|
+
// button on either page leaves for the other.
|
|
104
|
+
on('lensSimple', 'click', () => setLens('simple'));
|
|
105
|
+
on('lensMine', 'click', () => setLens('mine'));
|
|
103
106
|
$('fitBtn').addEventListener('click', () => (S.lens === 'all' ? SKY.fitAll() : SKY.fitPresent()));
|
|
104
|
-
|
|
107
|
+
// Each pill is one page's: recent, claude layer and my influence are
|
|
108
|
+
// /thinking's; my ideas and explore are /goal-map's (task 1004328).
|
|
109
|
+
function togglePill(id, get, set) { on(id, 'click', () => { set(!get()); press(id, get()); SKY.renderRoom(); updateStats(); S.dirty = true; }); }
|
|
105
110
|
togglePill('recentBtn', () => S.recentOnly, (v) => { S.recentOnly = v; });
|
|
106
111
|
togglePill('claudeBtn', () => S.claudeLayer, (v) => { S.claudeLayer = v; });
|
|
107
112
|
togglePill('influenceBtn', () => S.influence, (v) => { S.influence = v; });
|
|
108
113
|
togglePill('ideasBtn', () => S.ideasOnMap, (v) => { S.ideasOnMap = v; $('rehearse').hidden = !(S.lens === 'mine' || S.ideasOnMap); });
|
|
109
114
|
togglePill('exploreBtn', () => S.explore.on, (v) => { S.explore.on = v; SKY.saveExplore(); SKY.maybeStart(); });
|
|
110
|
-
|
|
115
|
+
press('exploreBtn', S.explore.on);
|
|
111
116
|
$('rehearse').addEventListener('click', () => { S.rehearsal = !S.rehearsal; $('rehearse').setAttribute('aria-pressed', String(S.rehearsal)); SKY.applyRehearsal(); SKY.placeIdeas(); if (S.picked && S.picked.kind === 'idea') { S.picked = { kind: 'idea', ref: S.ideas.find((i) => i.id === S.picked.ref.id) }; SKY.renderPanel(S.picked); } SKY.renderRoom(); updateStats(); S.dirty = true; });
|
|
112
117
|
|
|
113
118
|
function updateStats() {
|
|
@@ -131,14 +136,15 @@
|
|
|
131
136
|
const is = S.ideas.filter((i) => inLayer(i) && i.title.toLowerCase().includes(q)).slice(0, 8).map((i) => ({ kind: 'idea', ref: i, w: (OBJ[i.st] || { word: i.st }).word }));
|
|
132
137
|
const all = [...gs, ...is]; ul.hidden = false;
|
|
133
138
|
ul.innerHTML = all.length ? all.map((h, n) => `<li><button type="button" data-n="${n}"><span class="lbl">${esc(h.w)}</span><span>${esc(h.ref.title)}</span></button></li>`).join('') : '<li><button type="button" disabled>nothing by that name — or not explored yet</button></li>';
|
|
134
|
-
ul.querySelectorAll('button[data-n]').forEach((b) => b.addEventListener('click', () => { const h = all[+b.dataset.n]; if (h.kind === '
|
|
139
|
+
ul.querySelectorAll('button[data-n]').forEach((b) => b.addEventListener('click', () => { const h = all[+b.dataset.n]; if (h.kind === 'idea' && S.lens === 'all') { S.ideasOnMap = true; press('ideasBtn', true); } pick({ kind: h.kind, ref: h.ref }, true); ul.hidden = true; $('jump').value = ''; }));
|
|
135
140
|
});
|
|
136
141
|
document.addEventListener('click', (e) => { if (!(e.target && e.target.closest && e.target.closest('.jump'))) $('jumpList').hidden = true; });
|
|
137
142
|
|
|
138
143
|
$('legend').innerHTML = [
|
|
139
144
|
['star', 'star', 'burning'], ['protostar', 'protostar', 'waiting'], ['satellite', 'satellite', 'folded in'], ['remnant', 'remnant', 'burned out'], ['cold', 'cold body', 'declined'],
|
|
140
145
|
['well', 'well', 'an open goal'], ['alive', 'alive', 'a well with work in motion'], ['settled', 'settled', 'achieved'], ['collapsed', 'collapsed', 'archived'], ['unseen', 'unexplored', 'not yet reached'], ['task live', 'body', 'a task'], ['born', 'born of a thought', 'a goal or ring an idea became'], ['route', 'route', 'a dependency, charted'],
|
|
141
|
-
|
|
146
|
+
// "unexplored" is explore's word, and explore is the project map's (task 1004328).
|
|
147
|
+
].filter(([cls]) => cls !== 'unseen' || S.lenses.includes('all')).map(([cls, name, what]) => `<button type="button" class="obj" title="${esc(what)}"><span class="g"><span class="mk ${cls}"></span></span>${esc(name)}</button>`).join('');
|
|
142
148
|
|
|
143
149
|
Object.assign(SKY, { card, menu, glyphHtml, ideaTitle, sizeWords, showCard, showMenu, hideMenu, pick, setLens, togglePill, updateStats, altText, TAU, hash });
|
|
144
150
|
})();
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
// sky-data.js for the split). The clock draws the sky as it stood on a day —
|
|
7
7
|
// nothing moves position, only what exists changes. Routes are the goal
|
|
8
8
|
// graph's own edges, charted by a shuttle when asked. Explore is the project
|
|
9
|
-
// sky's rule
|
|
10
|
-
//
|
|
9
|
+
// sky's rule, and only /goal-map carries it (task 1004328: its pill and its
|
|
10
|
+
// start dialog are that page's markup alone). The camera is a
|
|
11
11
|
// pan/zoom over world coordinates laid out in sky-data.js.
|
|
12
12
|
|
|
13
13
|
(() => {
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
16
|
const SKY = window.OTBSky; const S = SKY.S;
|
|
17
|
-
const { $, TAU, reduce, NOW, clamp, smooth, ease, esc, fmtDay, short, store } = SKY;
|
|
17
|
+
const { $, hide, press, TAU, reduce, NOW, clamp, smooth, ease, esc, fmtDay, short, store } = SKY;
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------- the clock
|
|
20
20
|
const slider = $('clock');
|
|
@@ -50,18 +50,23 @@
|
|
|
50
50
|
el.innerHTML = `<button type="button" class="door primary" data-start="mine">where my thoughts are</button>` + wells.map((g) => `<button type="button" class="door" data-start="${esc(g.id)}">${esc(short(g.title))}</button>`).join('') + `<button type="button" class="door" data-start="all">show me everything</button>`;
|
|
51
51
|
el.querySelectorAll('button').forEach((b) => b.addEventListener('click', () => {
|
|
52
52
|
const v = b.dataset.start; $('start').hidden = true;
|
|
53
|
-
if (v === 'all') { explore.on = false;
|
|
53
|
+
if (v === 'all') { explore.on = false; press('exploreBtn', false); saveExplore(); fitAll(); }
|
|
54
54
|
else if (v === 'mine') { const t = S.goals.filter(SKY.touched); (t.length ? t : wells.slice(0, 1)).forEach(reveal); fitPresent(); }
|
|
55
55
|
else { const g = S.goalById.get(v); reveal(g); SKY.pick({ kind: 'goal', ref: g }, true); }
|
|
56
56
|
SKY.renderRoom(); SKY.updateStats(); S.dirty = true;
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
|
-
function maybeStart() { const need = S.lens === 'all' && explore.on && explore.seen.size === 0 && $('tour').hidden
|
|
59
|
+
function maybeStart() { const need = S.lens === 'all' && explore.on && explore.seen.size === 0 && $('tour').hidden && !!$('start'); hide('start', !need); if (need) startOptions(); }
|
|
60
60
|
|
|
61
61
|
// ---------------------------------------------------------------- the camera
|
|
62
62
|
const cv = $('sky'); const cam = S.cam;
|
|
63
|
-
|
|
63
|
+
// The canvas fills the sky's box (SKY.viewport: the hall's stage, or the
|
|
64
|
+
// window on /goal-map), and S.OX/S.OY keep where that box sits so a pointer's
|
|
65
|
+
// client coordinates land on the canvas (task 1004327). The stage can change
|
|
66
|
+
// size with no window resize — the full-screen switch — so it is observed too.
|
|
67
|
+
function resize() { const vp = SKY.viewport(); S.OX = vp.x; S.OY = vp.y; S.DPR = Math.min(2, devicePixelRatio || 1); S.W = vp.w; S.H = vp.h; cv.width = Math.round(S.W * S.DPR); cv.height = Math.round(S.H * S.DPR); cv.style.width = S.W + 'px'; cv.style.height = S.H + 'px'; S.dirty = true; }
|
|
64
68
|
addEventListener('resize', resize);
|
|
69
|
+
{ const st = document.querySelector('.sky-stage'); if (st && typeof ResizeObserver === 'function') new ResizeObserver(resize).observe(st); }
|
|
65
70
|
const toScreen = (x, y) => [(x - cam.x) * cam.k + S.W / 2, (y - cam.y) * cam.k + S.H / 2];
|
|
66
71
|
const toWorld = (sx, sy) => [(sx - S.W / 2) / cam.k + cam.x, (sy - S.H / 2) / cam.k + cam.y];
|
|
67
72
|
function flyTo(x, y, k, dur) { if (reduce) { cam.x = x; cam.y = y; cam.k = k; S.dirty = true; return; } S.flight = { x0: cam.x, y0: cam.y, k0: cam.k, x, y, k, t0: performance.now(), ms: dur || 900 }; }
|
|
@@ -85,10 +85,11 @@
|
|
|
85
85
|
<a class="door" href="${HALL}/goals#/goal/${esc(g.id)}" target="_blank" rel="noopener">the goal</a>
|
|
86
86
|
</div>
|
|
87
87
|
<p class="note">${g.status === 'open' ? 'A well holds work. Its rings are the done-when criteria, thick with the credits they carry, solid once met. It glows while it is alive: a builder holding a task, or work landing this week. Held and claimable tasks drift along their rings; landed work holds still.' : g.status === 'achieved' ? 'Every ring closed. It keeps the full mass of the work; nothing in it is live any more, so it holds no accent — a steady light, not a burning one.' : 'Archived. The mass of what was done stays on the record, drawn at half the radius; nothing here can ignite.'}</p>`;
|
|
88
|
-
|
|
88
|
+
// Descending stays in this page's sky (task 1004328): "simple" does not zoom, so it steps up to "your thinking" — never to the project map.
|
|
89
|
+
$('descendBtn').addEventListener('click', () => { if (S.lens === 'simple') SKY.setLens('mine', true); SKY.fitGoal(g); });
|
|
89
90
|
const cb = $('chartBtn'); if (cb) cb.addEventListener('click', () => SKY.chartAllOf(g));
|
|
90
91
|
el.querySelectorAll('[data-goal]').forEach((a) => a.addEventListener('click', (e) => { e.preventDefault(); SKY.pick({ kind: 'goal', ref: goalById.get(a.dataset.goal) }, true); }));
|
|
91
|
-
el.querySelectorAll('[data-idea]').forEach((a) => a.addEventListener('click', (e) => { e.preventDefault(); const i = S.ideas.find((x) => x.id === a.dataset.idea); if (i) { if (S.lens === 'all') { S.ideasOnMap = true;
|
|
92
|
+
el.querySelectorAll('[data-idea]').forEach((a) => a.addEventListener('click', (e) => { e.preventDefault(); const i = S.ideas.find((x) => x.id === a.dataset.idea); if (i) { if (S.lens === 'all') { S.ideasOnMap = true; SKY.press('ideasBtn', true); } SKY.pick({ kind: 'idea', ref: i }, true); } }));
|
|
92
93
|
} else if (p.kind === 'task' || p.kind === 'ltask') {
|
|
93
94
|
const t = p.ref; const g = t.goal || (t.goalId && goalById.get(t.goalId)); const crit = g && t.ci >= 0 ? g.crit[t.ci] : null;
|
|
94
95
|
el.innerHTML = `
|
|
@@ -5,20 +5,27 @@
|
|
|
5
5
|
// shared state on `SKY.S`, plus the boot the prototype did not need (its data
|
|
6
6
|
// was baked into the page): the five reads (sky-data.js `load`), then the
|
|
7
7
|
// prototype's own start-up line. The thirteen steps and their words are the
|
|
8
|
-
// design of record's; the tour opens once per browser (`
|
|
8
|
+
// design of record's; the tour opens once per browser, per page (`S.tourKey`:
|
|
9
|
+
// `sky-tour-seen` on /thinking, `sky-map-tour-seen` on /goal-map — the two
|
|
10
|
+
// tours differ since task 1004328, so seeing one does not mark the other seen).
|
|
9
11
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// TWO TOURS FROM ONE LIST (task 1004328, owner decision 2026-09-27). The two
|
|
13
|
+
// skies no longer share lenses, so each page walks only its own: every step
|
|
14
|
+
// says which sky it belongs to (`sky`: 'mine', 'all' or 'both'), and STEPS is
|
|
15
|
+
// this page's slice of the list. /thinking keeps the ideator's steps, the
|
|
16
|
+
// wells and rings they sit in, the clock, the composer and "simple" — nine of
|
|
17
|
+
// the thirteen, in the template's order and words. /goal-map keeps the wells,
|
|
18
|
+
// the rings, life, routes, settled systems, the clock and explore, opened by
|
|
19
|
+
// one step of its own ("This is the whole project.", the one new sentence the
|
|
20
|
+
// split needed — the template's first step is "This is your sky." and
|
|
21
|
+
// belongs to /thinking). When a tour ends it hands back to the page's home
|
|
22
|
+
// lens (`S.home`), and the boot opens in that lens too.
|
|
16
23
|
|
|
17
24
|
(() => {
|
|
18
25
|
'use strict';
|
|
19
26
|
|
|
20
27
|
const SKY = window.OTBSky; const S = SKY.S;
|
|
21
|
-
const { $, NOW, store, edgesOf, inLayer, clamp, lerp, ease } = SKY;
|
|
28
|
+
const { $, hide, press, NOW, store, edgesOf, inLayer, clamp, lerp, ease } = SKY;
|
|
22
29
|
|
|
23
30
|
// ---------------------------------------------------------------- the tour
|
|
24
31
|
const bigWell = () => S.goals.filter((g) => g.status === 'open').sort((a, b) => b.crit.length - a.crit.length || b.mass - a.mass)[0];
|
|
@@ -28,25 +35,29 @@
|
|
|
28
35
|
const aShaped = () => S.ideas.find((i) => i.shaped && inLayer(i)) || S.ideas.find((i) => i.shaped);
|
|
29
36
|
const aRouted = () => S.goals.filter((g) => g.status === 'open' && edgesOf(g).length).sort((a, b) => edgesOf(b).length - edgesOf(a).length)[0];
|
|
30
37
|
const aLive = () => S.goals.filter((g) => g.live > 0).sort((a, b) => b.live - a.live)[0];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{ h: '
|
|
35
|
-
{
|
|
36
|
-
{
|
|
37
|
-
{ h: '
|
|
38
|
-
{ h: '
|
|
39
|
-
{
|
|
40
|
-
{
|
|
41
|
-
{ h: '
|
|
42
|
-
{ h: '
|
|
43
|
-
{ h: '
|
|
44
|
-
{
|
|
38
|
+
// Explore off for the tour's length (startTour kept what it was; endTour gives it back).
|
|
39
|
+
const unexplore = () => { S.explore.on = false; hide('start', true); };
|
|
40
|
+
const ALL_STEPS = [
|
|
41
|
+
{ sky: 'all', h: 'This is the whole project.', p: 'Everything in it is a fact from the ledger, drawn instead of listed. Every goal is a gravity well, sized by the credits of its work; the present sits at the centre and shipped versions ring outward as darker ground. Your own thoughts live on Your thinking, and "my ideas" lays them over this map.', go: () => { unexplore(); SKY.setLens('all', true); SKY.fitAll(); spot($('ideasBtn')); } },
|
|
42
|
+
{ sky: 'mine', h: 'This is your sky.', p: 'Everything in it is a fact from the ledger, drawn instead of listed. The present sits at the centre; shipped versions ring outward as darker ground — hover the space between goals to be told which version you are in. Only what a person filed is shown; a session’s filings wait behind "claude layer".', go: () => { SKY.setLens('mine', true); SKY.fitPresent(); spot($('claudeBtn')); } },
|
|
43
|
+
{ sky: 'both', h: 'A goal is a gravity well.', p: 'It holds the work. The rings inside it are its done-when criteria: dashed while open, solid once met, thicker the more credits ride on them. Thoughts caught by the goal sit inside the well.', go: () => { const w = bigWell(); if (w) { S.tourSpot = { kind: 'goal', ref: w }; SKY.flyTo(w.x, w.y, Math.min(S.W, S.H) / (w.r * 2.9), 1100); } } },
|
|
44
|
+
{ sky: 'both', h: 'A ring is a criterion.', p: 'Hover any ring to read it. Come closer and the tasks that gate it resolve out of the dust onto the ring itself. Tasks born of the same thought share an angle, so kin line up radially. Held and claimable tasks drift slowly along their ring; landed work holds still.', go: () => { const w = bigWell(); if (w && w.crit.length) S.tourSpot = { kind: 'ring', goal: w, index: 0 }; } },
|
|
45
|
+
{ sky: 'mine', h: 'A burning thought is a star.', p: 'It caused work that is still alive. Its size is real: the credits the ledger paid this thought’s descent, a slice of every task that fell out of it. It grows as the work grows and never shrinks. Brightness is how much of that work still stands, and that can go out.', go: () => { const s = aStar(); if (s) { S.tourSpot = { kind: 'idea', ref: s }; SKY.flyTo(s.x, s.y, 2.6, 1100); } } },
|
|
46
|
+
{ sky: 'mine', h: 'Some thoughts became the structure.', p: 'A thought ratified into a goal sits at that well’s core with a small accent ring. One ratified into a criterion sits on its ring, and the ring wears an accent notch. When a thought’s descent reaches into other goals, hover it and its reach is drawn: dashed accent lines to the work it caused elsewhere.', go: () => { const s = aShaped(); const w = bigWell(); if (s) { S.tourSpot = { kind: 'idea', ref: s }; SKY.flyTo(s.x, s.y, Math.max(1.4, S.cam.k), 1100); } else if (w) S.tourSpot = { kind: 'goal', ref: w }; } },
|
|
47
|
+
{ sky: 'mine', h: 'Unclaimed thoughts drift.', p: 'A thought nothing caught floats in open space. The longer it stays unclaimed, the further from the centre it drifts and the cooler its light — warm white when fresh, blue-white by half a year.', go: () => { const d = aDrifter(); if (d) { S.tourSpot = { kind: 'idea', ref: d }; SKY.flyTo(d.x, d.y, 1.2, 1100); } } },
|
|
48
|
+
{ sky: 'all', h: 'An alive well glows.', p: 'On the project map the one accent a goal can wear is life: a builder holding a task right now, or work landing this week. Quiet wells stay white. The ledger’s "alive" filter lists them.', go: () => { unexplore(); SKY.setLens('all', true); const g = aLive() || aRouted(); if (g) { S.tourSpot = { kind: 'goal', ref: g }; SKY.flyTo(g.x, g.y, Math.min(S.W, S.H) / (g.r * 4), 1100); } } },
|
|
49
|
+
{ sky: 'all', h: 'Routes run between goals that wait on each other.', p: 'Almost invisible until you ask. Each well wears a small arrow at its rim per route. Click one and the camera pulls back while a shuttle charts the route; double-click charts every route from that goal. Hover the parked shuttle for more: every route from this goal, every route in the sky, only the latest, hide the unrelated.', go: () => { const g = aRouted(); if (g) { S.tourSpot = { kind: 'goal', ref: g }; setTimeout(() => { const e = edgesOf(g)[0]; if (e) SKY.chartRoute(e, g); }, 300); } } },
|
|
50
|
+
{ sky: 'all', h: 'A settled system is a goal that finished.', p: 'It keeps the full mass of its work, measured in credits, and every ring is closed. Nothing in it is live any more, so it carries no accent — a steady white. Archived goals collapse to half their radius and go dark.', go: () => { const g = aSettled(); if (g) { S.tourSpot = { kind: 'goal', ref: g }; SKY.flyTo(g.x, g.y, Math.min(S.W, S.H) / (g.r * 3.2), 1100); } } },
|
|
51
|
+
{ sky: 'both', h: 'The sky has a clock.', p: 'Drag the slider at the bottom and the sky is drawn as it stood that day: goals, tasks and thoughts appear when they were born. Press play to watch it grow from the first day. Nothing moves position — a thought stays where it was caught — only what exists changes.', go: () => { SKY.fitAll(); spot($('time')); } },
|
|
52
|
+
{ sky: 'all', h: 'By default, you explore.', p: 'The project map does not hand you everything. You pick a well to begin in; every route you chart brings the goal at its far end into view, and the ledger, the filters and the search only know what you have reached. Turn "explore" off to see the whole sky at once.', go: () => { spot($('exploreBtn')); } },
|
|
53
|
+
{ sky: 'mine', h: 'Release a thought here.', p: 'One line, and the well that would catch it lights up while you type. Letting it float is a real answer. Switch "full idea" on before the line and five questions follow it, one at a time; skip any. "My influence" fades every goal your thoughts never touched.', go: () => { S.tourSpot = null; SKY.setLens('mine', true); SKY.fitPresent(); spot($('composerWrap')); spot($('influenceBtn'), true); } },
|
|
54
|
+
{ sky: 'mine', h: 'When it is too much, go simple.', p: 'The "simple" view is the first sky: just your thoughts in their wells, recent or all, nothing else to press. Click a band anywhere else to fill the screen with that version. Press L for the list, ? to walk this again.', go: () => { SKY.setLens('simple', true); SKY.fitPresent(); spot($('lensSimple')); spot($('recentBtn'), true); } },
|
|
45
55
|
];
|
|
56
|
+
const STEPS = ALL_STEPS.filter((s) => s.sky === 'both' || s.sky === S.home);
|
|
46
57
|
function spot(el, add) { if (!add) { S.spotted.forEach((e) => e.classList.remove('spot')); S.spotted = []; } if (el) { el.classList.add('spot'); S.spotted.push(el); } }
|
|
47
58
|
function showStep() { const s = STEPS[S.tourI]; if (!s) return endTour(); spot(null); S.tourSpot = null; s.go(); $('tourN').textContent = `how to read the sky · ${S.tourI + 1} of ${STEPS.length}`; $('tourDots').innerHTML = STEPS.map((_, n) => `<i class="${n === S.tourI ? 'on' : ''}"></i>`).join(''); $('tourH').textContent = s.h; $('tourP').textContent = s.p; $('tourNext').textContent = S.tourI === STEPS.length - 1 ? 'start reading' : 'next'; $('tourBack').hidden = S.tourI === 0; $('tour').hidden = false; S.dirty = true; }
|
|
48
|
-
function startTour() { SKY.pick(null); SKY.toggleRoom(false); SKY.clearCharts();
|
|
49
|
-
function endTour() { $('tour').hidden = true; spot(null); S.tourSpot = null; S.tourI = -1; if (S.exploreWas != null) { S.explore.on = S.exploreWas;
|
|
59
|
+
function startTour() { SKY.pick(null); SKY.toggleRoom(false); SKY.clearCharts(); hide('start', true); S.exploreWas = S.explore.on; S.tourI = 0; showStep(); }
|
|
60
|
+
function endTour() { $('tour').hidden = true; spot(null); S.tourSpot = null; S.tourI = -1; if (S.exploreWas != null) { S.explore.on = S.exploreWas; press('exploreBtn', S.explore.on); S.exploreWas = null; } store.set(S.tourKey, '4'); SKY.setLens(S.home, true); SKY.clearCharts(); SKY.maybeStart(); S.dirty = true; }
|
|
50
61
|
$('tourNext').addEventListener('click', () => { S.tourI++; showStep(); }); $('tourBack').addEventListener('click', () => { S.tourI = Math.max(0, S.tourI - 1); showStep(); }); $('tourSkip').addEventListener('click', endTour); $('tourBtn').addEventListener('click', startTour);
|
|
51
62
|
|
|
52
63
|
// ---------------------------------------------------------------- the loop
|
|
@@ -76,9 +87,9 @@
|
|
|
76
87
|
S.cam.k = S.home === 'all' ? SKY.fitRad(S.BANDS[S.BANDS.length - 1].r1 + 100) : SKY.fitRad(S.BANDS[0].r1 + 120);
|
|
77
88
|
document.body.dataset.skyReady = '1';
|
|
78
89
|
requestAnimationFrame(frame);
|
|
79
|
-
if (store.get(
|
|
90
|
+
if (store.get(S.tourKey) !== '4') setTimeout(startTour, 600);
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
Object.assign(SKY, { STEPS, spot, showStep, startTour, endTour, frame, boot });
|
|
93
|
+
Object.assign(SKY, { ALL_STEPS, STEPS, spot, showStep, startTour, endTour, frame, boot });
|
|
83
94
|
boot();
|
|
84
95
|
})();
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
16
16
|
<link rel="stylesheet" href="/builders/studio.css">
|
|
17
17
|
</head>
|
|
18
|
-
|
|
18
|
+
<!-- data-fullscreen-switch opts into the shell's full-screen switch (task
|
|
19
|
+
1004327): the studio in the hall chrome by default, and one button in the top
|
|
20
|
+
bar that hides it so the room fills the window, remembered for this page. -->
|
|
21
|
+
<body data-page="studio" data-page-title="Studio" data-fullscreen-switch>
|
|
19
22
|
|
|
20
23
|
<div class="app">
|
|
21
24
|
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
@@ -32,11 +35,14 @@
|
|
|
32
35
|
<main id="hall-main" class="studio">
|
|
33
36
|
<div class="studio-atmos" aria-hidden="true"></div>
|
|
34
37
|
|
|
35
|
-
<header
|
|
38
|
+
<!-- A <div>, not a <header>: the shell's top bar is the page's one
|
|
39
|
+
<header> (the kit's one-header-main-h1 floor), and in full screen it
|
|
40
|
+
stays in the document as the way back (task 1004327). -->
|
|
41
|
+
<div class="studio-head">
|
|
36
42
|
<p class="studio-eyebrow">artist mode</p>
|
|
37
43
|
<h1 class="studio-greet" id="studio-greet">The studio</h1>
|
|
38
44
|
<p class="studio-sub" id="studio-sub">A quiet place to tend the words and art the project shows people.</p>
|
|
39
|
-
</
|
|
45
|
+
</div>
|
|
40
46
|
|
|
41
47
|
<!-- The one thing to tend now. Rendered by studio.js; the markup here is
|
|
42
48
|
the loading state (and the no-JS fallback), never an empty panel. -->
|
|
@@ -66,7 +72,7 @@
|
|
|
66
72
|
|
|
67
73
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
68
74
|
|
|
69
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
75
|
+
<script src="/builders/shell.js?v=2026-09-27-fullscreen"></script>
|
|
70
76
|
<script src="/builders/palette.js" defer></script>
|
|
71
77
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
72
78
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"/api/(gds|bongos)/me$",
|
|
11
11
|
"/api/(gds|bongos)/me/",
|
|
12
12
|
"/api/(gds|bongos)/tasks",
|
|
13
|
-
"/api/(gds|bongos)/copy-desk/"
|
|
13
|
+
"/api/(gds|bongos)/copy-desk/",
|
|
14
|
+
"/api/(gds|bongos)/core-update"
|
|
14
15
|
],
|
|
15
16
|
"ignoreConsole": [
|
|
16
17
|
"/api/(gds|bongos)/",
|
|
@@ -40,6 +41,80 @@
|
|
|
40
41
|
},
|
|
41
42
|
"out": {
|
|
42
43
|
"auth": false
|
|
44
|
+
},
|
|
45
|
+
"full": {
|
|
46
|
+
"auth": true,
|
|
47
|
+
"_": "Full screen (task 1004327): the shell switch pressed — the room fills the window, one button brings the chrome back.",
|
|
48
|
+
"actions": [
|
|
49
|
+
[
|
|
50
|
+
"wait",
|
|
51
|
+
700
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"click",
|
|
55
|
+
"#fullscreen-toggle"
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"wait",
|
|
59
|
+
300
|
|
60
|
+
]
|
|
61
|
+
],
|
|
62
|
+
"expect": {
|
|
63
|
+
"visible": [
|
|
64
|
+
"#studio-focus",
|
|
65
|
+
"#fullscreen-toggle"
|
|
66
|
+
],
|
|
67
|
+
"hidden": [
|
|
68
|
+
"#app-sidebar",
|
|
69
|
+
"#topbar-title"
|
|
70
|
+
],
|
|
71
|
+
"attr": [
|
|
72
|
+
[
|
|
73
|
+
"#fullscreen-toggle",
|
|
74
|
+
"aria-pressed",
|
|
75
|
+
"true"
|
|
76
|
+
]
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"full-back": {
|
|
81
|
+
"auth": true,
|
|
82
|
+
"_": "Escape leaves full screen.",
|
|
83
|
+
"actions": [
|
|
84
|
+
[
|
|
85
|
+
"wait",
|
|
86
|
+
700
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
"click",
|
|
90
|
+
"#fullscreen-toggle"
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"wait",
|
|
94
|
+
300
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"press",
|
|
98
|
+
"Escape"
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
"wait",
|
|
102
|
+
300
|
|
103
|
+
]
|
|
104
|
+
],
|
|
105
|
+
"expect": {
|
|
106
|
+
"visible": [
|
|
107
|
+
"#app-sidebar",
|
|
108
|
+
"#topbar-title"
|
|
109
|
+
],
|
|
110
|
+
"attr": [
|
|
111
|
+
[
|
|
112
|
+
"#fullscreen-toggle",
|
|
113
|
+
"aria-pressed",
|
|
114
|
+
"false"
|
|
115
|
+
]
|
|
116
|
+
]
|
|
117
|
+
}
|
|
43
118
|
}
|
|
44
119
|
},
|
|
45
120
|
"reducedMotion": {
|
|
@@ -971,6 +971,59 @@ main:focus { outline: none; }
|
|
|
971
971
|
background: var(--accent-tint);
|
|
972
972
|
color: var(--accent-ink);
|
|
973
973
|
}
|
|
974
|
+
/* task 1004327: the full-screen switch — the theme toggle's sibling, on the
|
|
975
|
+
pages that opt in (body[data-fullscreen-switch]). */
|
|
976
|
+
.fullscreen-toggle {
|
|
977
|
+
display: inline-flex;
|
|
978
|
+
align-items: center;
|
|
979
|
+
justify-content: center;
|
|
980
|
+
width: 32px; height: 32px;
|
|
981
|
+
border: 0;
|
|
982
|
+
border-radius: var(--radius-pill);
|
|
983
|
+
background: none;
|
|
984
|
+
color: var(--ink-soft);
|
|
985
|
+
cursor: pointer;
|
|
986
|
+
padding: 0;
|
|
987
|
+
flex-shrink: 0;
|
|
988
|
+
}
|
|
989
|
+
.fullscreen-toggle:hover { color: var(--ink); }
|
|
990
|
+
.fullscreen-toggle svg { width: 16px; height: 16px; }
|
|
991
|
+
|
|
992
|
+
/* FULL SCREEN (html[data-fullscreen="on"], shell.js). The sidebar, the scrim and
|
|
993
|
+
the foot leave the layout; the top bar does NOT leave the document — it
|
|
994
|
+
collapses to the one button that brings the chrome back, floating in the
|
|
995
|
+
corner it already held, so the page keeps exactly one <header> (the kit's
|
|
996
|
+
one-header-main-h1 floor) and the way back is where the way in was. The bar
|
|
997
|
+
stops catching the pointer; only its button does. */
|
|
998
|
+
html[data-fullscreen="on"] .app-sidebar,
|
|
999
|
+
html[data-fullscreen="on"] .app-scrim,
|
|
1000
|
+
html[data-fullscreen="on"] .app-foot { display: none; }
|
|
1001
|
+
html[data-fullscreen="on"] .app-body { margin-left: 0; }
|
|
1002
|
+
html[data-fullscreen="on"] .app-topbar {
|
|
1003
|
+
position: fixed;
|
|
1004
|
+
top: 12px;
|
|
1005
|
+
right: 12px;
|
|
1006
|
+
left: auto;
|
|
1007
|
+
height: auto;
|
|
1008
|
+
padding: 0;
|
|
1009
|
+
gap: 0;
|
|
1010
|
+
background: none;
|
|
1011
|
+
border: 0;
|
|
1012
|
+
backdrop-filter: none;
|
|
1013
|
+
-webkit-backdrop-filter: none;
|
|
1014
|
+
pointer-events: none;
|
|
1015
|
+
}
|
|
1016
|
+
html[data-fullscreen="on"] .app-topbar > :not(.topbar-side),
|
|
1017
|
+
html[data-fullscreen="on"] .topbar-side > :not(.fullscreen-toggle) { display: none; }
|
|
1018
|
+
html[data-fullscreen="on"] .fullscreen-toggle {
|
|
1019
|
+
pointer-events: auto;
|
|
1020
|
+
width: 36px; height: 36px;
|
|
1021
|
+
color: var(--ink);
|
|
1022
|
+
background: var(--glass);
|
|
1023
|
+
box-shadow: inset 0 0 0 1px var(--glass-edge);
|
|
1024
|
+
backdrop-filter: blur(var(--glass-blur));
|
|
1025
|
+
-webkit-backdrop-filter: blur(var(--glass-blur));
|
|
1026
|
+
}
|
|
974
1027
|
.zoom-toggle__pct {
|
|
975
1028
|
font-size: 12px;
|
|
976
1029
|
font-weight: 500;
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
59
59
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
60
|
-
<script src="/builders/shell.js?v=2026-09-
|
|
60
|
+
<script src="/builders/shell.js?v=2026-09-27-fullscreen"></script>
|
|
61
61
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
62
62
|
<script src="/builders/palette.js" defer></script>
|
|
63
63
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|