@bongos/core 1.19.1012 → 1.19.1013
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 +51 -41
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
- package/docs/copy-inventory.md +99 -56
- package/docs/copy-registry.json +830 -465
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/idea-objects.js +4 -1
- package/modules/hall-ui/public/sky-composer.js +155 -0
- package/modules/hall-ui/public/sky-data.js +287 -0
- package/modules/hall-ui/public/sky-draw.js +244 -0
- package/modules/hall-ui/public/sky-interaction.js +137 -0
- package/modules/hall-ui/public/sky-layout.js +81 -0
- package/modules/hall-ui/public/sky-panel.js +141 -0
- package/modules/hall-ui/public/sky-tour.js +80 -0
- package/modules/hall-ui/public/thinking.css +203 -267
- package/modules/hall-ui/public/thinking.html +119 -58
- package/modules/hall-ui/public/thinking.states.json +33 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/me.js +4 -1
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +27 -0
- package/tests/hall_page_gate_map.mjs +11 -5
- package/tests/hall_sky.mjs +393 -405
- package/tests/hall_thinking_room.mjs +274 -296
- package/tests/helpers.mjs +185 -217
- package/tests/ideator_large_dead_thought_proof.mjs +33 -25
- package/tests/ideator_traceable_proof.mjs +61 -39
- package/modules/hall-ui/public/sky.css +0 -365
- package/modules/hall-ui/public/sky.html +0 -79
- package/modules/hall-ui/public/sky.js +0 -580
- package/modules/hall-ui/public/sky.states.json +0 -20
- package/modules/hall-ui/public/thinking.js +0 -418
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// modules/hall-ui/public/sky-panel.js — THE SKY, part 5 of 7: the panel and
|
|
2
|
+
// the list (task 1004231 / sky part 3 of 5; ADR 0331).
|
|
3
|
+
//
|
|
4
|
+
// The prototype's "the panel" and "the list" sections, verbatim but for the
|
|
5
|
+
// shared state on `SKY.S`. THE LIST IS THE ROOM: spec decision 11's surviving
|
|
6
|
+
// rule is that the sky is never the ONLY path to anything it shows, so every
|
|
7
|
+
// drawn thought is a row here with its whole sentence — the object's word,
|
|
8
|
+
// what it became, where it sits, its size — and the drawer is one key away
|
|
9
|
+
// (L). Every title, criterion and goal name is escaped on its way into the
|
|
10
|
+
// markup (sky-data.js `esc`); nothing a person typed becomes an element.
|
|
11
|
+
//
|
|
12
|
+
// The nod: a protostar in a well, awaiting a nod, offers "nod — make it
|
|
13
|
+
// claimable" to a builder who holds `idea.triage` (GET /me says so). Pressing
|
|
14
|
+
// it runs the existing promote path (sky-composer.js `nod`); the button is
|
|
15
|
+
// never drawn for anyone else.
|
|
16
|
+
|
|
17
|
+
(() => {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const SKY = window.OTBSky; const S = SKY.S;
|
|
21
|
+
const { $, esc, fmtDate, fmtDay, plain, short, hallBase, OBJ, reach, became, inLayer, seen, edgesOf, glyphHtml, ideaTitle, sizeWords, positionWord, brightnessWord } = SKY;
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------- the panel
|
|
24
|
+
function fact(k, v, num) { return `<div class="fact"><span class="k">${k}</span><span class="v${num ? ' num' : ''}">${v}</span></div>`; }
|
|
25
|
+
function renderPanel(p) {
|
|
26
|
+
const el = $('panel'); if (!p) { el.hidden = true; $('alt').innerHTML = SKY.altText(); return; } el.hidden = false;
|
|
27
|
+
const { goalById } = S; const HALL = hallBase();
|
|
28
|
+
if (p.kind === 'idea') {
|
|
29
|
+
const i = p.ref; const o = OBJ[i.st] || { noun: i.st, blurb: '' }; const wellG = i.well && goalById.get(i.well); const bright = brightnessWord(i); const pos = positionWord(i); const rch = reach(i);
|
|
30
|
+
const FIELDS = SKY.fields();
|
|
31
|
+
const canNod = S.canNod && i.st === 'protostar' && i.well && (i.awaiting || (i.state && i.state.awaitingNod));
|
|
32
|
+
el.innerHTML = `
|
|
33
|
+
<div class="kind"><span class="glyph">${glyphHtml(i)}</span><span class="lbl">${o.noun}${i.origin === 'claude' ? ' · filed by a session' : ''}${i.invented ? ' · what if' : ''}${i.landed || i.justFiled ? ' · just filed' : ''}</span></div>
|
|
34
|
+
<h2>${esc(i.title)}</h2>
|
|
35
|
+
<div class="facts">
|
|
36
|
+
${i.shaped ? fact('shaped the space', i.shaped.kind === 'goal' ? `became the goal <a href="#" data-goal="${esc(i.shaped.goal.id)}">${esc(short(i.shaped.goal.title))}</a>` : `became ring ${i.shaped.crit.index + 1} of <a href="#" data-goal="${esc(i.shaped.goal.id)}">${esc(short(i.shaped.goal.title))}</a>`) : ''}
|
|
37
|
+
${fact('gravity', wellG ? `<a href="#" data-goal="${esc(wellG.id)}">${esc(wellG.title)}</a>` : `open space · ${Math.round(i.age)} days adrift`)}
|
|
38
|
+
${fact('became', esc(became(i)))}
|
|
39
|
+
${rch.length ? fact('lives on in', rch.map((g) => `<a href="#" data-goal="${esc(g.id)}">${esc(short(g.title))}</a>`).join(', ')) : ''}
|
|
40
|
+
${fact('size — credits', i.size ? `${i.size.credits}${i.size.historical ? ' <span class="lbl">hist.</span>' : ''}` : 'unknown', true)}
|
|
41
|
+
${i.size ? fact('of which', esc(sizeWords(i))) : ''}
|
|
42
|
+
${bright ? fact('brightness — still alive', `<span class="bar" style="width:${Math.max(4, Math.round(i.lit * 70))}px;${i.lit > 0 ? '' : 'background:rgba(255,255,255,.2)'}"></span>${esc(bright)}`) : ''}
|
|
43
|
+
${pos ? fact('position', esc(pos)) : ''}
|
|
44
|
+
${fact('filed', esc(fmtDate(i.capturedAt)) + (i.kind ? ` · ${esc(i.kind)}` : '') + (i.grade === 'full' ? ' · full idea' : '') + (i.origin === 'claude' ? ' · by a session, under your name' : ''))}
|
|
45
|
+
</div>
|
|
46
|
+
${i.fields && FIELDS.length ? `<div><span class="lbl">the five answers · ${SKY.completeness(i.fields)} of ${FIELDS.length} above the floor</span><ul class="orbit-list">${FIELDS.map((f) => `<li><span class="st">${esc(f.key.replace('_', ' '))}</span><span>${esc(i.fields[f.key] || '— skipped')}</span></li>`).join('')}</ul></div>` : ''}
|
|
47
|
+
${i.lineage.length ? `<div><span class="lbl">the descent — everything that fell out of it</span><ul class="orbit-list">${[...i.lineage, ...i.descentTasks].map((t) => `<li><span class="st${t.status === 'active' ? ' live' : ''}">${esc(t.status)}</span><span>${esc(t.title)}${t.goalId && t.goalId !== i.well && goalById.get(t.goalId) ? ` <span class="lbl">in ${esc(short(goalById.get(t.goalId).title))}</span>` : ''}</span></li>`).join('')}</ul></div>` : ''}
|
|
48
|
+
<div class="doors">
|
|
49
|
+
${canNod ? `<button type="button" class="door primary" id="nodBtn">${i.nodded ? 'nodded — a builder can claim it now' : 'nod — make it claimable'}</button>` : ''}
|
|
50
|
+
${i.st === 'protostar' && !i.well ? `<button type="button" class="door primary" id="aimBtn">aim it at a goal</button>` : ''}
|
|
51
|
+
${i.lineage.length ? `<button type="button" class="door" id="descBtn">read the descent</button>` : ''}
|
|
52
|
+
<a class="door" href="${HALL}/idea/${esc(i.id)}" target="_blank" rel="noopener">the record</a>
|
|
53
|
+
</div>
|
|
54
|
+
<p class="note">${esc(o.blurb)}. Size is what the ledger paid this thought's descent — a slice of every task that fell out of it, thinning by the hop — so it grows as the work it caused grows.</p>`;
|
|
55
|
+
el.querySelectorAll('[data-goal]').forEach((a) => a.addEventListener('click', (e) => { e.preventDefault(); SKY.pick({ kind: 'goal', ref: goalById.get(a.dataset.goal) }, true); }));
|
|
56
|
+
const nod = $('nodBtn'); if (nod) { if (i.nodded) nod.disabled = true; else nod.addEventListener('click', () => SKY.nod(i, nod)); }
|
|
57
|
+
const aim = $('aimBtn'); if (aim) aim.addEventListener('click', () => { $('thought').value = i.title; $('thought').focus(); $('thought').dispatchEvent(new Event('input')); });
|
|
58
|
+
const desc = $('descBtn'); if (desc) desc.addEventListener('click', () => SKY.fitThought(i));
|
|
59
|
+
} else if (p.kind === 'goal') {
|
|
60
|
+
const g = p.ref; const met = g.crit.filter((c) => c.satisfied).length; const here = g.thoughts.filter(inLayer);
|
|
61
|
+
const state = g.status === 'open' ? (g.live > 0 ? 'gravity well · alive' : 'gravity well') : g.status === 'achieved' ? 'settled system' : 'collapsed';
|
|
62
|
+
const counts = g.tasks.reduce((a, t) => { a[t.status] = (a[t.status] || 0) + 1; return a; }, {});
|
|
63
|
+
const waiting = S.edges.filter((e) => e.a === g).map((e) => e.b); const waitedOn = S.edges.filter((e) => e.b === g).map((e) => e.a);
|
|
64
|
+
const name = (x) => seen(x) ? `<a href="#" data-goal="${esc(x.id)}">${esc(short(x.title))}</a>` : '<span class="lbl">unexplored</span>';
|
|
65
|
+
el.innerHTML = `
|
|
66
|
+
<div class="kind"><span class="glyph"><span class="mk ${g.status === 'open' ? (g.live > 0 ? 'alive' : 'well') : g.status === 'achieved' ? 'settled' : 'collapsed'}"></span></span><span class="lbl${g.live > 0 ? ' hot' : ''}">${state} · ${esc(g.band.label)}</span></div>
|
|
67
|
+
<h2>${esc(g.title)}</h2>
|
|
68
|
+
${g.subtitle ? `<p class="note" style="margin:0">${esc(g.subtitle)}</p>` : ''}
|
|
69
|
+
<div class="facts">
|
|
70
|
+
${g.bornOf ? fact('born of a thought', `<a href="#" data-idea="${esc(g.bornOf)}">${esc(ideaTitle(g.bornOf))}</a>`) : ''}
|
|
71
|
+
${fact('tasks', `${g.taskDone} of ${g.taskTotal} done`, true)}
|
|
72
|
+
${g.credits ? fact('credits — the mass', String(g.credits), true) : ''}
|
|
73
|
+
${g.status === 'open' ? fact('alive', g.live > 0 ? `${g.active} held now · ${g.recent} landed this week` : 'quiet — nothing held, nothing landed this week') : ''}
|
|
74
|
+
${g.status === 'open' && g.tasks.length ? fact('in the well', ['active', 'ready', 'blocked', 'backlog', 'abandoned'].filter((s) => counts[s]).map((s) => `${counts[s]} ${s}`).join(' · ') || 'nothing open') : ''}
|
|
75
|
+
${fact('rings — criteria', g.crit.length ? `${met} of ${g.crit.length} closed` : 'none written', true)}
|
|
76
|
+
${fact('your thoughts here', String(here.length), true)}
|
|
77
|
+
${fact('born', esc(fmtDay(g.born)))}
|
|
78
|
+
${waiting.length ? fact('waits on', waiting.map(name).join(', ')) : ''}
|
|
79
|
+
${waitedOn.length ? fact('waited on by', waitedOn.map(name).join(', ')) : ''}
|
|
80
|
+
</div>
|
|
81
|
+
${g.crit.length ? `<ul class="crit">${g.crit.slice(0, 10).map((c) => `<li><span class="ring${c.satisfied ? ' on' : ''}"></span><span>${esc(plain(c.text).slice(0, 120))}${plain(c.text).length > 120 ? '…' : ''}${c.bornOf ? ` <span class="born">· born of a thought</span>` : ''}${c.credits ? ` <span class="lbl">${c.credits} cr</span>` : ''}</span></li>`).join('')}</ul>` : ''}
|
|
82
|
+
<div class="doors">
|
|
83
|
+
<button type="button" class="door primary" id="descendBtn">${g.status === 'open' ? 'descend into the well' : 'look closer'}</button>
|
|
84
|
+
${edgesOf(g).length ? `<button type="button" class="door" id="chartBtn">chart its routes</button>` : ''}
|
|
85
|
+
<a class="door" href="${HALL}/goals#/goal/${esc(g.id)}" target="_blank" rel="noopener">the goal</a>
|
|
86
|
+
</div>
|
|
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
|
+
$('descendBtn').addEventListener('click', () => { if (S.lens !== 'all') SKY.setLens('all', true); SKY.fitGoal(g); });
|
|
89
|
+
const cb = $('chartBtn'); if (cb) cb.addEventListener('click', () => SKY.chartAllOf(g));
|
|
90
|
+
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; $('ideasBtn').setAttribute('aria-pressed', 'true'); } SKY.pick({ kind: 'idea', ref: i }, true); } }));
|
|
92
|
+
} else if (p.kind === 'task' || p.kind === 'ltask') {
|
|
93
|
+
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
|
+
el.innerHTML = `
|
|
95
|
+
<div class="kind"><span class="glyph"><span class="mk task${t.status === 'active' ? ' live' : ''}"></span></span><span class="lbl">task · ${esc(t.status)}</span></div>
|
|
96
|
+
<h2>${esc(t.title)}</h2>
|
|
97
|
+
<div class="facts">
|
|
98
|
+
${g ? fact('well', esc(g.title)) : ''}
|
|
99
|
+
${crit ? fact('gates the ring', esc(plain(crit.text).slice(0, 90))) : g ? fact('gates the ring', 'none — outer orbit') : ''}
|
|
100
|
+
${t.credits != null ? fact('credits', String(t.credits), true) : ''}
|
|
101
|
+
${t.createdAt ? fact('born', esc(t.createdAt)) : ''}
|
|
102
|
+
${t.sourceRef ? fact('born of a thought', `<a href="${HALL}/idea/${esc(t.sourceRef)}" target="_blank" rel="noopener">${esc(ideaTitle(t.sourceRef))}</a>`) : ''}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="doors"><a class="door" href="${HALL}/task/${esc(t.id)}" target="_blank" rel="noopener">the task</a></div>
|
|
105
|
+
<p class="note">A body on a ring. Lit and drifting while a builder holds it; a hairline, drifting slower, while it waits to be claimed; solid and still once shipped; a dash when it was abandoned. Bodies born of the same thought share an angle across the rings.</p>`;
|
|
106
|
+
}
|
|
107
|
+
$('alt').innerHTML = SKY.altText();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------- the list
|
|
111
|
+
const FILTERS = [['all', 'everything'], ['star', 'burning'], ['protostar', 'waiting'], ['satellite', 'folded in'], ['remnant', 'burned out'], ['cold-body', 'declined']];
|
|
112
|
+
const GFILTERS = [['all', 'every goal'], ['alive', 'alive'], ['open', 'wells'], ['achieved', 'settled'], ['archived', 'collapsed'], ['routes', 'with routes']];
|
|
113
|
+
function renderRoom() {
|
|
114
|
+
const room = $('room'); if (room.hidden) return;
|
|
115
|
+
const { goalById, picked } = S;
|
|
116
|
+
if (S.lens === 'mine') {
|
|
117
|
+
$('roomTitle').textContent = 'the room'; const pool = S.ideas.filter(inLayer);
|
|
118
|
+
const list = pool.filter((i) => S.filter === 'all' || i.st === S.filter).slice().sort((a, b) => String(b.capturedAt).localeCompare(String(a.capturedAt)));
|
|
119
|
+
$('roomCount').textContent = `${list.length} of ${pool.length}`;
|
|
120
|
+
$('filters').innerHTML = FILTERS.map(([k, w]) => `<button type="button" class="pill" data-f="${k}" aria-pressed="${S.filter === k}">${w} <span class="mono" style="opacity:.6">${k === 'all' ? pool.length : pool.filter((i) => i.st === k).length}</span></button>`).join('');
|
|
121
|
+
$('filters').querySelectorAll('button').forEach((b) => b.addEventListener('click', () => { S.filter = b.dataset.f; renderRoom(); S.dirty = true; }));
|
|
122
|
+
$('rows').innerHTML = list.length ? list.map((i) => { const wellG = i.well && goalById.get(i.well); return `<button type="button" class="row" data-id="${esc(i.id)}" aria-current="${!!(picked && picked.kind === 'idea' && picked.ref === i)}"><span class="t">${esc(i.title)}</span><span class="m">${esc(fmtDate(i.capturedAt))} · ${esc(became(i))}${wellG ? ` · in ${esc(short(wellG.title))}` : ' · open space'}${i.size && i.size.credits ? ` · size ${i.size.credits}` : ''}</span><span class="g">${glyphHtml(i)}</span></button>`; }).join('') : '<p class="empty">Nothing of that kind in your sky.</p>';
|
|
123
|
+
$('rows').querySelectorAll('.row').forEach((b) => b.addEventListener('click', () => { const i = S.ideas.find((x) => x.id === b.dataset.id); SKY.pick({ kind: 'idea', ref: i }, true); }));
|
|
124
|
+
} else {
|
|
125
|
+
$('roomTitle').textContent = 'the ledger';
|
|
126
|
+
const pool = S.goals.filter((g) => seen(g));
|
|
127
|
+
const rank = (g) => g.status === 'open' ? 0 : g.status === 'achieved' ? 1 : 2;
|
|
128
|
+
const gf = S.gfilter;
|
|
129
|
+
const list = pool.filter((g) => gf === 'all' || (gf === 'routes' ? edgesOf(g).length > 0 : gf === 'alive' ? g.live > 0 : g.status === gf)).slice().sort((a, b) => (b.live - a.live) || (rank(a) - rank(b)) || (b.mass - a.mass));
|
|
130
|
+
$('roomCount').textContent = `${list.length} of ${pool.length}${pool.length < S.goals.length ? ` · ${S.goals.length - pool.length} unexplored` : ''}`;
|
|
131
|
+
$('filters').innerHTML = GFILTERS.map(([k, w]) => `<button type="button" class="pill" data-f="${k}" aria-pressed="${gf === k}">${w} <span class="mono" style="opacity:.6">${k === 'all' ? pool.length : k === 'routes' ? pool.filter((g) => edgesOf(g).length).length : k === 'alive' ? pool.filter((g) => g.live > 0).length : pool.filter((g) => g.status === k).length}</span></button>`).join('');
|
|
132
|
+
$('filters').querySelectorAll('button').forEach((b) => b.addEventListener('click', () => { S.gfilter = b.dataset.f; renderRoom(); }));
|
|
133
|
+
$('rows').innerHTML = list.length ? list.map((g) => { const w = S.edges.filter((e) => e.a === g).length, wb = S.edges.filter((e) => e.b === g).length; const cls = g.status === 'open' ? (g.live > 0 ? 'alive' : 'well') : g.status === 'achieved' ? 'settled' : 'collapsed'; return `<button type="button" class="row" data-id="${esc(g.id)}" aria-current="${!!(picked && picked.kind === 'goal' && picked.ref === g)}"><span class="t">${esc(g.title)}</span><span class="m">${cls} · ${g.taskDone}/${g.taskTotal} tasks${g.credits ? ` · ${g.credits} cr` : ''} · ${g.crit.filter((c) => c.satisfied).length}/${g.crit.length} rings${w ? ` · waits on ${w}` : ''}${wb ? ` · waited on by ${wb}` : ''}${g.bornOf ? ' · born of a thought' : ''}</span><span class="g"><span class="mk ${cls}"></span></span></button>`; }).join('') : '<p class="empty">Nothing explored yet. Pick a starting point in the sky.</p>';
|
|
134
|
+
$('rows').querySelectorAll('.row').forEach((b) => b.addEventListener('click', () => SKY.pick({ kind: 'goal', ref: goalById.get(b.dataset.id) }, true)));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function toggleRoom(force) { const room = $('room'); room.hidden = force == null ? !room.hidden : !force; $('roomBtn').setAttribute('aria-pressed', String(!room.hidden)); renderRoom(); }
|
|
138
|
+
$('roomBtn').addEventListener('click', () => toggleRoom());
|
|
139
|
+
|
|
140
|
+
Object.assign(SKY, { fact, renderPanel, FILTERS, GFILTERS, renderRoom, toggleRoom });
|
|
141
|
+
})();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// modules/hall-ui/public/sky-tour.js — THE SKY, part 7 of 7: the tour, the
|
|
2
|
+
// frame loop and the boot (task 1004231 / sky part 3 of 5; ADR 0331).
|
|
3
|
+
//
|
|
4
|
+
// The prototype's "the tour" and "the loop" sections, verbatim but for the
|
|
5
|
+
// shared state on `SKY.S`, plus the boot the prototype did not need (its data
|
|
6
|
+
// was baked into the page): the five reads (sky-data.js `load`), then the
|
|
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 (`sky-tour-seen`).
|
|
9
|
+
//
|
|
10
|
+
// Steps 7, 8 and 10 walk the project sky's lens ('all') inside this page —
|
|
11
|
+
// which is why that lens survives here although its switcher button leaves
|
|
12
|
+
// for /goal-map (part 4).
|
|
13
|
+
|
|
14
|
+
(() => {
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const SKY = window.OTBSky; const S = SKY.S;
|
|
18
|
+
const { $, NOW, store, edgesOf, inLayer, clamp, lerp, ease } = SKY;
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------- the tour
|
|
21
|
+
const bigWell = () => S.goals.filter((g) => g.status === 'open').sort((a, b) => b.crit.length - a.crit.length || b.mass - a.mass)[0];
|
|
22
|
+
const aStar = () => S.ideas.filter((i) => i.st === 'star' && i.x != null && inLayer(i)).sort((a, b) => (b.size?.credits || 0) - (a.size?.credits || 0))[0];
|
|
23
|
+
const aDrifter = () => S.ideas.filter((i) => i.st === 'protostar' && !i.well && i.x != null && inLayer(i)).sort((a, b) => b.age - a.age)[0];
|
|
24
|
+
const aSettled = () => S.goals.filter((g) => g.status === 'achieved').sort((a, b) => b.mass - a.mass)[0];
|
|
25
|
+
const aShaped = () => S.ideas.find((i) => i.shaped && inLayer(i)) || S.ideas.find((i) => i.shaped);
|
|
26
|
+
const aRouted = () => S.goals.filter((g) => g.status === 'open' && edgesOf(g).length).sort((a, b) => edgesOf(b).length - edgesOf(a).length)[0];
|
|
27
|
+
const aLive = () => S.goals.filter((g) => g.live > 0).sort((a, b) => b.live - a.live)[0];
|
|
28
|
+
const STEPS = [
|
|
29
|
+
{ 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')); } },
|
|
30
|
+
{ 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); } } },
|
|
31
|
+
{ 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 }; } },
|
|
32
|
+
{ 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); } } },
|
|
33
|
+
{ 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 }; } },
|
|
34
|
+
{ 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); } } },
|
|
35
|
+
{ 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: () => { S.exploreWas = S.explore.on; S.explore.on = false; SKY.setLens('all', true); $('start').hidden = 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); } } },
|
|
36
|
+
{ 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); } } },
|
|
37
|
+
{ 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); } } },
|
|
38
|
+
{ 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')); } },
|
|
39
|
+
{ 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')); } },
|
|
40
|
+
{ 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); } },
|
|
41
|
+
{ 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); } },
|
|
42
|
+
];
|
|
43
|
+
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); } }
|
|
44
|
+
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; }
|
|
45
|
+
function startTour() { SKY.pick(null); SKY.toggleRoom(false); SKY.clearCharts(); $('start').hidden = true; S.exploreWas = S.explore.on; S.tourI = 0; showStep(); }
|
|
46
|
+
function endTour() { $('tour').hidden = true; spot(null); S.tourSpot = null; S.tourI = -1; if (S.exploreWas != null) { S.explore.on = S.exploreWas; $('exploreBtn').setAttribute('aria-pressed', String(S.explore.on)); S.exploreWas = null; } store.set('sky-tour-seen', '4'); SKY.setLens('mine', true); SKY.clearCharts(); SKY.maybeStart(); S.dirty = true; }
|
|
47
|
+
$('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);
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------- the loop
|
|
50
|
+
function frame(now) {
|
|
51
|
+
const { cam } = S;
|
|
52
|
+
if (S.flight) { const f = S.flight; const p = clamp((now - f.t0) / f.ms, 0, 1); const e = ease(p); cam.x = lerp(f.x0, f.x, e); cam.y = lerp(f.y0, f.y, e); cam.k = Math.exp(lerp(Math.log(f.k0), Math.log(f.k), e)); if (p >= 1) S.flight = null; S.dirty = true; }
|
|
53
|
+
if (S.playing && now - S.lastTick > 60) { S.lastTick = now; SKY.setClock(S.clock + 86400000 * 1.5); if (S.clock >= NOW) { S.playing = false; $('play').setAttribute('aria-pressed', 'false'); } }
|
|
54
|
+
const animate = !SKY.reduce && !document.hidden; const charting = [...S.charted.values()].some((j) => !j.done || now - j.t0 - j.ms < 3200);
|
|
55
|
+
if (S.dirty || animate || S.flying || charting) { SKY.draw(now); S.dirty = false; $('alt').innerHTML = SKY.altText(); }
|
|
56
|
+
// Under prefers-reduced-motion nothing animates, so the loop must not spin
|
|
57
|
+
// a requestAnimationFrame (the kit's reduced-motion floor): a slow timer
|
|
58
|
+
// polls for a redraw a hover or a key made necessary, and draws only then.
|
|
59
|
+
if (SKY.reduce) setTimeout(pollFrame, 120); else requestAnimationFrame(frame);
|
|
60
|
+
}
|
|
61
|
+
function pollFrame() { frame(performance.now()); }
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------- boot
|
|
64
|
+
// The reads, then the prototype's own start-up line. A read that fails
|
|
65
|
+
// leaves an empty sky, which still draws (an empty trail is a state).
|
|
66
|
+
async function boot() {
|
|
67
|
+
try { await SKY.load(); } catch (_) { if (!S.loaded) SKY.buildWorld({}); }
|
|
68
|
+
// The Full Idea switch needs the template's key to file; without the read there is nothing to file as.
|
|
69
|
+
$('fullToggle').hidden = !S.template;
|
|
70
|
+
S.t0 = performance.now();
|
|
71
|
+
SKY.resize(); SKY.setClock(NOW); SKY.setLens('mine', true); SKY.renderPanel(null); $('time').hidden = S.clockHidden;
|
|
72
|
+
S.cam.k = SKY.fitRad(S.BANDS[0].r1 + 120);
|
|
73
|
+
document.body.dataset.skyReady = '1';
|
|
74
|
+
requestAnimationFrame(frame);
|
|
75
|
+
if (store.get('sky-tour-seen') !== '4') setTimeout(startTour, 600);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Object.assign(SKY, { STEPS, spot, showStep, startTour, endTour, frame, boot });
|
|
79
|
+
boot();
|
|
80
|
+
})();
|