@bongos/core 1.25.15 → 1.25.16
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 +47 -32
- package/.claude/skills/goal-create/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +5 -4
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -0
- package/docs/copy-inventory.md +120 -109
- package/docs/copy-registry.json +275 -165
- package/docs/module-api-changelog.md +2 -2
- package/docs/page-inventory.json +2 -0
- package/docs/page-readings.json +107 -107
- package/docs/project-context.template.md +1 -0
- package/modules/hall-ui/public/dom-utils.js +5 -0
- package/modules/hall-ui/public/genesis-home.js +23 -8
- package/modules/hall-ui/public/setup-gate.js +1 -1
- package/modules/hall-ui/public/style.css +21 -1
- package/modules/lifecycle/kickoff-checklist.js +17 -3
- package/modules/lifecycle/kickoff-seed.js +27 -4
- package/modules/onboarding/genesis-home.js +11 -2
- package/modules/onboarding/genesis-work.js +28 -5
- package/modules/public-landing/public/assets/guide-scene.css +245 -0
- package/modules/public-landing/public/assets/guide-scene.js +566 -0
- package/modules/public-landing/public/index.html +62 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +13 -1
- package/scripts/bongos/genesis.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/genesis_home.mjs +70 -2
- package/tests/genesis_work.mjs +49 -0
- package/tests/landing_guide_scene.mjs +504 -0
|
@@ -529,6 +529,10 @@
|
|
|
529
529
|
} catch (err) { return null; }
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
// The hall's one completion tick (24x24, stroked, currentColor): the setup flow's step
|
|
533
|
+
// marks and the Genesis checklist both draw it, so a done thing looks done the same way.
|
|
534
|
+
const CHECK_SVG = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
535
|
+
|
|
532
536
|
const OTB = {
|
|
533
537
|
escapeHtml, sleep, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
|
|
534
538
|
plainMessage, toastKind, TOAST_MS, // task 1005086 — one toast, a duration per kind, no server address
|
|
@@ -542,6 +546,7 @@
|
|
|
542
546
|
splitCriterion,
|
|
543
547
|
categoryChip, // task 1003276 — shared by the Goals page, the Tasks board and the task page
|
|
544
548
|
hubOrigin, // task 1003485 — the hub account page's origin, for Settings and the profile nudge
|
|
549
|
+
CHECK_SVG, // task 1005110 — the one tick, setup-gate.js and genesis-home.js
|
|
545
550
|
};
|
|
546
551
|
if (typeof window !== 'undefined') window.OTB = OTB;
|
|
547
552
|
// Node: the hall libs that also load under require() (hall-kit.js, task-lib.js, …)
|
|
@@ -190,9 +190,10 @@
|
|
|
190
190
|
|
|
191
191
|
// task 1005109: Copy prompt on each task still to do, so the founder pastes it into
|
|
192
192
|
// Claude without leaving Genesis. The Tasks board's own button (work.js), and only that
|
|
193
|
-
// one verb: the owner declined a Claim button here.
|
|
193
|
+
// one verb: the owner declined a Claim button here. Only an OPEN task offers it: a done
|
|
194
|
+
// one has nothing left, and a locked one cannot be claimed yet (task 1005110).
|
|
194
195
|
function copyButtonHtml(t) {
|
|
195
|
-
if (t.
|
|
196
|
+
if (t.state !== 'open') return '';
|
|
196
197
|
return '<button class="task__copy" type="button" data-genesis-copy="' + esc(t.id) + '"'
|
|
197
198
|
+ ' title="Copy the task prompt to your clipboard" aria-label="Copy prompt for task ' + esc(t.id) + '">'
|
|
198
199
|
+ '<span class="task__copy-icon" aria-hidden="true">📋</span></button>';
|
|
@@ -212,16 +213,30 @@
|
|
|
212
213
|
return lib.copyTaskPrompt(t, btn, toast);
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
// task 1005110: one checklist row in one of three states, as the server decided them
|
|
217
|
+
// (shapeGenesis taskState): done (a filled circle with the tick), open (an empty
|
|
218
|
+
// circle, its status and Copy prompt) or locked (a dashed circle and what it waits on,
|
|
219
|
+
// no Copy prompt: the claim would be refused).
|
|
220
|
+
function taskRowHtml(t) {
|
|
221
|
+
var state = t.state === 'done' || t.state === 'locked' ? t.state : 'open';
|
|
222
|
+
var check = window.OTB.CHECK_SVG || '';
|
|
223
|
+
var mark = '<span class="genesis__mark" aria-hidden="true">' + (state === 'done' ? check : '') + '</span>';
|
|
224
|
+
var said = { done: 'Done', open: 'To do', locked: 'Locked' }[state];
|
|
225
|
+
var after = (t.waits_on || []).map(function (w) { return w.title; }).filter(Boolean);
|
|
226
|
+
var tail = state === 'locked'
|
|
227
|
+
? '<span class="genesis__task-wait">' + esc(after.length ? 'after ' + after.join(', ') : 'after an earlier task') + '</span>'
|
|
228
|
+
: window.OTB.statusPill(t.status, { small: true, extraClass: 'genesis__task-st' }) + copyButtonHtml(t);
|
|
229
|
+
return '<li class="genesis__task is-' + state + '">' + mark
|
|
230
|
+
+ '<span class="sr-only">' + said + ': </span>'
|
|
231
|
+
+ '<a class="genesis__task-t" href="/task/' + encodeURIComponent(t.id) + '">' + esc(t.title) + '</a>'
|
|
232
|
+
+ tail + '</li>';
|
|
233
|
+
}
|
|
234
|
+
|
|
215
235
|
function nowHtml(g, viewerId) {
|
|
216
236
|
var s = g.stages.find(function (x) { return x.state === 'now'; });
|
|
217
237
|
if (!s) return '';
|
|
218
238
|
var tasks = s.tasks.length
|
|
219
|
-
? '<ul class="genesis__tasks">' + s.tasks.map(
|
|
220
|
-
return '<li class="genesis__task' + (t.finished ? ' is-finished' : '') + '">'
|
|
221
|
-
+ '<a class="genesis__task-t" href="/task/' + encodeURIComponent(t.id) + '">' + esc(t.title) + '</a>'
|
|
222
|
-
+ window.OTB.statusPill(t.status, { small: true, extraClass: 'genesis__task-st' })
|
|
223
|
-
+ copyButtonHtml(t) + '</li>';
|
|
224
|
-
}).join('') + '</ul>'
|
|
239
|
+
? '<ul class="genesis__tasks">' + s.tasks.map(taskRowHtml).join('') + '</ul>'
|
|
225
240
|
: '<p class="genesis__note">No tasks for this stage yet.</p>';
|
|
226
241
|
return '<section class="genesis__now" aria-labelledby="genesis-now-h">'
|
|
227
242
|
+ '<h3 id="genesis-now-h" class="genesis__now-h">This stage’s work · ' + esc(s.label) + '</h3>'
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
function esc(s) { return window.OTB.escapeHtml(s); }
|
|
22
22
|
|
|
23
|
-
var CHECK =
|
|
23
|
+
var CHECK = window.OTB.CHECK_SVG; // the hall's one tick (dom-utils.js), shared with the Genesis checklist
|
|
24
24
|
|
|
25
25
|
function stepDetail(step) {
|
|
26
26
|
if (step.id === 'cli_setup') {
|
|
@@ -1054,7 +1054,27 @@ html[data-theme="dark"] .gplate__light { display: none; }
|
|
|
1054
1054
|
.genesis__stage.is-later { opacity: 0.6; }
|
|
1055
1055
|
.genesis__now-h { margin: 0 0 4px; font-size: 15px; font-weight: 500; }
|
|
1056
1056
|
.genesis__task-t { flex: 1; min-width: 0; padding: 3px 0; }
|
|
1057
|
-
|
|
1057
|
+
/* task 1005110: the stage's work reads as a checklist. Done is a filled circle with the
|
|
1058
|
+
hall's tick (the setup flow's mark), open an empty circle, locked a dashed one with
|
|
1059
|
+
what it waits on. Never struck through: a finished task was earned, not cancelled. */
|
|
1060
|
+
.genesis__mark {
|
|
1061
|
+
flex: none;
|
|
1062
|
+
display: inline-flex;
|
|
1063
|
+
align-items: center;
|
|
1064
|
+
justify-content: center;
|
|
1065
|
+
width: 24px;
|
|
1066
|
+
height: 24px;
|
|
1067
|
+
border-radius: var(--ds-radius-pill); /* round on purpose: the owner asked for a check filling a circle */
|
|
1068
|
+
border: 1.5px solid var(--rule);
|
|
1069
|
+
color: var(--ok);
|
|
1070
|
+
}
|
|
1071
|
+
.genesis__mark svg { width: 14px; height: 14px; }
|
|
1072
|
+
.genesis__task.is-done .genesis__mark { border-color: transparent; background: var(--ok-weak); }
|
|
1073
|
+
.genesis__task.is-done .genesis__task-t { color: var(--ink-soft); }
|
|
1074
|
+
.genesis__task.is-open .genesis__mark { border-color: var(--accent); }
|
|
1075
|
+
.genesis__task.is-locked .genesis__mark { border-style: dashed; }
|
|
1076
|
+
.genesis__task.is-locked .genesis__task-t { color: var(--ink-faint); }
|
|
1077
|
+
.genesis__task-wait { flex: none; max-width: 40%; font-size: var(--ds-text-xs); color: var(--ink-soft); text-align: right; }
|
|
1058
1078
|
.genesis__alive { display: grid; justify-items: center; text-align: center; gap: 10px; padding: 12px 0; }
|
|
1059
1079
|
.genesis__alive-plates { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
|
|
1060
1080
|
.genesis__alive-h { margin: 6px 0 0; font-size: 28px; font-weight: 400; }
|
|
@@ -144,7 +144,7 @@ function kickoffChecklist(spec = {}, { provisioned = false, firstSignIn = false
|
|
|
144
144
|
const GENESIS_STEPS = Object.freeze({
|
|
145
145
|
scope: [
|
|
146
146
|
{ title: 'Write the project scope: what we are making, for whom, and what is out of scope',
|
|
147
|
-
description: 'Genesis stage 1 (scope).
|
|
147
|
+
description: 'Genesis stage 1 (scope). The project interview: what you are creating and why, who it is for and who you want building it with you, what success looks like in two years, and what everyone else in this area is getting wrong. Every future goal and task is planned from your answers, and "not sure yet" is a fine answer you can come back to. You are the author and you choose how to work: be guided through the questions, write it yourself with the questions beside you, or have your own AI session walk you through them (it asks and pushes back; you decide) — switch any time, nothing is lost. The one-page scope is then drafted from your answers. Run `npx bongos genesis show scope` to see the questions and your answers, or `npx bongos genesis assist scope` for the brief to give your AI session. When it is written, Goals and the Roadmap wake.' },
|
|
148
148
|
],
|
|
149
149
|
strategize: [
|
|
150
150
|
{ title: 'Write out the strategy, or link the one you already have',
|
|
@@ -168,9 +168,19 @@ const GENESIS_EXISTING_FIRST = {
|
|
|
168
168
|
description: 'Genesis stage 1 (scope), for a project that already exists. Before writing anything new, link what is already there, so the scope is written from it rather than beside it.',
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
//
|
|
171
|
+
// GENESIS_AFTER — "this step follows that one", by ref (task 1005110). A step listed
|
|
172
|
+
// here is filed with a real dependency edge on the step it names, so it cannot be
|
|
173
|
+
// claimed (DEPS_NOT_SHIPPED) and the genesis home draws it locked until that one
|
|
174
|
+
// ships. Only where the order is real: a stage with no entry here is all open at once.
|
|
175
|
+
// Today that is brownfield scope alone — the scope is written FROM what was gathered.
|
|
176
|
+
const GENESIS_AFTER = Object.freeze({
|
|
177
|
+
'genesis:scope': 'genesis:scope:existing',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// genesisChecklist({ existing }) → { stageKey: [{ title, description, kind, discipline, ref, after }] }
|
|
172
181
|
// in genesis order. `ref` is the stable per-task key the seeder dedups on, so filing
|
|
173
|
-
// twice files nothing new.
|
|
182
|
+
// twice files nothing new. `after` is the ref of the step this one waits for, set only
|
|
183
|
+
// when that step is in the same list (GENESIS_AFTER), else null. Pure — no I/O.
|
|
174
184
|
function genesisChecklist({ existing = false } = {}) {
|
|
175
185
|
const out = {};
|
|
176
186
|
for (const [stage, steps] of Object.entries(GENESIS_STEPS)) {
|
|
@@ -181,6 +191,10 @@ function genesisChecklist({ existing = false } = {}) {
|
|
|
181
191
|
...t, kind: 'decision', discipline: 'artist', ref: t === GENESIS_EXISTING_FIRST ? 'genesis:scope:existing' : `genesis:${stage}`,
|
|
182
192
|
}));
|
|
183
193
|
}
|
|
194
|
+
for (const list of Object.values(out)) {
|
|
195
|
+
const refs = new Set(list.map((t) => t.ref));
|
|
196
|
+
for (const t of list) t.after = refs.has(GENESIS_AFTER[t.ref]) ? GENESIS_AFTER[t.ref] : null;
|
|
197
|
+
}
|
|
184
198
|
return out;
|
|
185
199
|
}
|
|
186
200
|
|
|
@@ -85,6 +85,7 @@ async function seedGenesisStages({ builderId = null, existing = false } = {}, de
|
|
|
85
85
|
const pool = deps.pool || api.pool;
|
|
86
86
|
const insert = deps.createTask || ((row) => db.createTask(row));
|
|
87
87
|
const find = deps.findTaskBySourceRef || ((q) => db.findTaskBySourceRef(q));
|
|
88
|
+
const addDep = deps.addTaskDependency || ((id, dep) => db.addTaskDependency(id, dep));
|
|
88
89
|
const { rows: versions } = await pool.query(FIRST_VERSION_SQL);
|
|
89
90
|
if (!versions.length) return { seeded: 0, skipped: 'no_version' };
|
|
90
91
|
const versionId = versions[0].id;
|
|
@@ -109,18 +110,39 @@ async function seedGenesisStages({ builderId = null, existing = false } = {}, de
|
|
|
109
110
|
seeded += 1;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
113
|
+
// task 1005110: the order genesisChecklist declares becomes a real edge, so the claim
|
|
114
|
+
// gate refuses the later step (DEPS_NOT_SHIPPED) until the earlier one ships. Written
|
|
115
|
+
// for steps already filed too (ON CONFLICT DO NOTHING), so a re-run repairs a missing edge.
|
|
116
|
+
for (const steps of Object.values(genesisChecklist({ existing }))) {
|
|
117
|
+
for (const t of steps) {
|
|
118
|
+
if (!t.after) continue;
|
|
119
|
+
const [me, before] = await Promise.all([
|
|
120
|
+
find({ source: GENESIS_SOURCE, sourceRef: t.ref }),
|
|
121
|
+
find({ source: GENESIS_SOURCE, sourceRef: t.after }),
|
|
122
|
+
]);
|
|
123
|
+
if (me && before) await addDep(me.id, before.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
112
126
|
return { seeded, versionId };
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
// genesisStageTasks(deps) → { stageKey: [{ id, title, status, description }] } for
|
|
116
|
-
// genesis task on the board, grouped by the stage its source_ref names. The genesis
|
|
117
|
-
// lists them, and a stage close hands their ids to the Board Room's finished-tasks gate.
|
|
129
|
+
// genesisStageTasks(deps) → { stageKey: [{ id, title, status, description, waits_on }] } for
|
|
130
|
+
// every genesis task on the board, grouped by the stage its source_ref names. The genesis
|
|
131
|
+
// home lists them, and a stage close hands their ids to the Board Room's finished-tasks gate.
|
|
132
|
+
// `waits_on` is [{ id, title }] of the tasks it depends on that have not shipped: the claim
|
|
133
|
+
// gate's own rule (unsatisfiedDeps, abandoned still blocks), so a row drawn locked is one
|
|
134
|
+
// POST /claims would refuse (task 1005110).
|
|
118
135
|
// The description rides along so each row's Copy prompt carries the task's own words
|
|
119
136
|
// (task 1005109); the read stays bounded, it is only the few seeded genesis tasks.
|
|
120
137
|
async function genesisStageTasks(deps = {}) {
|
|
121
138
|
const pool = deps.pool || api.pool;
|
|
122
139
|
const { rows } = await pool.query(
|
|
123
|
-
`SELECT id, title, status, description, source_ref
|
|
140
|
+
`SELECT t.id, t.title, t.status, t.description, t.source_ref,
|
|
141
|
+
COALESCE((SELECT json_agg(json_build_object('id', x.id, 'title', x.title) ORDER BY x.id)
|
|
142
|
+
FROM dependencies d JOIN tasks x ON x.id = d.to_id
|
|
143
|
+
WHERE d.from_kind = 'task' AND d.from_id = t.id
|
|
144
|
+
AND d.to_kind = 'task' AND x.status <> 'shipped'), '[]'::json) AS waits_on
|
|
145
|
+
FROM tasks t WHERE t.source = $1 ORDER BY t.id ASC`,
|
|
124
146
|
[GENESIS_SOURCE],
|
|
125
147
|
);
|
|
126
148
|
const out = {};
|
|
@@ -129,6 +151,7 @@ async function genesisStageTasks(deps = {}) {
|
|
|
129
151
|
if (!stage) continue;
|
|
130
152
|
(out[stage] = out[stage] || []).push({
|
|
131
153
|
id: String(r.id), title: r.title, status: r.status, description: r.description || '',
|
|
154
|
+
waits_on: (Array.isArray(r.waits_on) ? r.waits_on : []).map((w) => ({ id: String(w.id), title: w.title })),
|
|
132
155
|
});
|
|
133
156
|
}
|
|
134
157
|
return out;
|
|
@@ -143,10 +143,19 @@ function readPlanValue(raw) {
|
|
|
143
143
|
|
|
144
144
|
const FINISHED = Object.freeze(['shipped', 'abandoned']); // the Board Room's own gate, mirrored for display
|
|
145
145
|
|
|
146
|
+
// taskState(t) — one stage task as the checklist draws it (task 1005110).
|
|
147
|
+
function taskState(t) {
|
|
148
|
+
const finished = FINISHED.includes(t.status);
|
|
149
|
+
const waits = finished ? [] : (Array.isArray(t.waits_on) ? t.waits_on : []);
|
|
150
|
+
return { ...t, finished, waits_on: waits, state: finished ? 'done' : waits.length ? 'locked' : 'open' };
|
|
151
|
+
}
|
|
152
|
+
|
|
146
153
|
// shapeGenesis({ mode, plan, tasks, board, stageAt }) — PURE. The genesis home's view.
|
|
147
154
|
// mode foundingModeFor's shape (non-null)
|
|
148
155
|
// plan the saved plan or null (deciding)
|
|
149
|
-
// tasks { stage: [{ id, title, status, description }] }
|
|
156
|
+
// tasks { stage: [{ id, title, status, description, waits_on }] }
|
|
157
|
+
// Each task comes back with `state`: 'done' (finished), 'locked' (waits on an unshipped
|
|
158
|
+
// task, named in `waits_on`) or 'open' — decided here, never re-derived in a browser.
|
|
150
159
|
// board { stage: genesisStageStatus row } or {}
|
|
151
160
|
// stageAt { stage: ISO } — when each stage began, for its due date
|
|
152
161
|
function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
@@ -154,7 +163,7 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
154
163
|
const stages = PROPER.map((key) => {
|
|
155
164
|
const info = STAGE_INFO[key];
|
|
156
165
|
const idx = fm.STAGES.indexOf(key);
|
|
157
|
-
const list = (tasks[key] || []).map((t) => (
|
|
166
|
+
const list = (tasks[key] || []).map((t) => taskState(t));
|
|
158
167
|
const b = board[key] || null;
|
|
159
168
|
const allFinished = list.length > 0 && list.every((t) => t.finished);
|
|
160
169
|
const onBoard = !!(b && (b.state === 'sitting' || b.state === 'passed'));
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// un-answer appends a history row, so the founder can see what an answer used to say.
|
|
41
41
|
//
|
|
42
42
|
// WHICH STAGES. A stage adopts the pattern by declaring its questions in QUESTIONS.
|
|
43
|
-
// Strategize does
|
|
43
|
+
// Strategize does (task 1005116); scope's guided door is its project interview (task 1005114);
|
|
44
44
|
// charter, legalize and legislation adopt it when they are next touched. A stage with
|
|
45
45
|
// no questions still takes a free page and a link.
|
|
46
46
|
//
|
|
@@ -59,8 +59,30 @@ const LIMITS = Object.freeze({ answer: 4000, page: 20000, link: 500 });
|
|
|
59
59
|
|
|
60
60
|
// Each adopted stage's questions, in the order the guided door asks them. `id` is
|
|
61
61
|
// stable: a stored answer names it, so never rename or reuse one. `hint` is the shape
|
|
62
|
-
// of a good answer, shown under the question in every door.
|
|
62
|
+
// of a good answer, shown under the question in every door. `group` (optional) is a
|
|
63
|
+
// heading the guided door shows above the questions that share it.
|
|
64
|
+
//
|
|
65
|
+
// SCOPE IS THE PROJECT INTERVIEW (task 1005114): the questions every later goal and task
|
|
66
|
+
// should be planned inside — what this is for, who it is for, and the bet — rather than
|
|
67
|
+
// V1's edges, which is what came back thin before. The owner's starter set, as written;
|
|
68
|
+
// the list is "not final" and the owner invited more. These are prose questions; the
|
|
69
|
+
// closed-vocabulary ones (team shape, geography, entity, rewards) are the hub's project
|
|
70
|
+
// detail and are not repeated here.
|
|
63
71
|
const QUESTIONS = Object.freeze({
|
|
72
|
+
scope: Object.freeze([
|
|
73
|
+
Object.freeze({ id: 'what_creating', group: 'What and why', ask: 'In a few sentences, what are you creating with this project?',
|
|
74
|
+
hint: 'Say it the way you would to a friend. What exists when this is done that does not exist now?' }),
|
|
75
|
+
Object.freeze({ id: 'goal_impact', group: 'What and why', ask: 'What is your goal with it? What impact do you want it to have?',
|
|
76
|
+
hint: 'Why this is worth doing at all, and for whom it changes something.' }),
|
|
77
|
+
Object.freeze({ id: 'success_two_years', group: 'What and why', ask: 'In two years, what does success look like?',
|
|
78
|
+
hint: 'Something you could point at: who is using it, what they do with it, what it has become.' }),
|
|
79
|
+
Object.freeze({ id: 'reach_who', group: 'Who', ask: 'Who do you want to reach with this project?',
|
|
80
|
+
hint: 'The people it is ultimately for, not only the first ones. Be as specific as you can.' }),
|
|
81
|
+
Object.freeze({ id: 'work_with_who', group: 'Who', ask: 'Who do you want working on it with you?',
|
|
82
|
+
hint: 'The kind of people you hope join: their skills, how they work, what they care about.' }),
|
|
83
|
+
Object.freeze({ id: 'others_wrong', group: 'The bet', ask: 'What is everyone else in this area getting wrong?',
|
|
84
|
+
hint: 'The thing you believe that others in this space do not. This is the bet the project makes.' }),
|
|
85
|
+
]),
|
|
64
86
|
strategize: Object.freeze([
|
|
65
87
|
Object.freeze({ id: 'big_pieces', ask: 'What are the big pieces, and in what order do they get built?',
|
|
66
88
|
hint: 'Three to six pieces is plenty. Say why that order: what has to exist before what.' }),
|
|
@@ -118,7 +140,7 @@ function shapeStage(stage, work, answers = []) {
|
|
|
118
140
|
const questions = questionsFor(stage).map((q) => {
|
|
119
141
|
const a = byId.get(q.id);
|
|
120
142
|
return {
|
|
121
|
-
id: q.id, ask: q.ask, hint: q.hint,
|
|
143
|
+
id: q.id, ask: q.ask, hint: q.hint, group: q.group || null,
|
|
122
144
|
answer: a ? {
|
|
123
145
|
state: a.state, text: a.answer_md || null, origin: a.origin,
|
|
124
146
|
// A carried-over answer is shown as such until the founder makes it theirs.
|
|
@@ -196,11 +218,12 @@ function assistPrompt(stage) {
|
|
|
196
218
|
'- Offer two or three options when I am stuck, never one confident answer.',
|
|
197
219
|
'- "Not sure yet" is a fine answer. Record it as that rather than filling the gap for me.',
|
|
198
220
|
'- Record an answer only after I say it is mine, in my words (you may tidy the wording; read it back first).',
|
|
221
|
+
'- Once I have answered, you may offer a draft page built only from my answers. I edit it; save it only when I say it is mine.',
|
|
199
222
|
'',
|
|
200
223
|
];
|
|
201
224
|
if (qs.length) {
|
|
202
|
-
lines.push('The questions:');
|
|
203
|
-
qs.forEach((q, i) => lines.push(`${i + 1}. ${q.ask} (${q.hint})`));
|
|
225
|
+
lines.push('The questions (the id in brackets is what you record the answer under):');
|
|
226
|
+
qs.forEach((q, i) => lines.push(`${i + 1}. [${q.id}] ${q.ask} (${q.hint})`));
|
|
204
227
|
lines.push('');
|
|
205
228
|
}
|
|
206
229
|
lines.push('To see what I have already said: npx bongos genesis show ' + stage);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/* assets/guide-scene.css — the start-a-project guide as a scene in the game (task 1005275).
|
|
2
|
+
|
|
3
|
+
The scene wears the game's look, Helm (DESIGN.md "Helm: the hall's look"): a night panel
|
|
4
|
+
closed left and right by the strong ice hairline, names in the name voice (condensed
|
|
5
|
+
wide capitals), what you press cut at two corners, progress as skewed segments, and the
|
|
6
|
+
one accent bar on the thing to do next. It stands on the landing, so it spends the
|
|
7
|
+
landing's tokens, never the hall's literals: the night and the ice are mixes of the
|
|
8
|
+
pack's --sea into its --bg and --ink (the Fifteen Rule), and they flip with the world's
|
|
9
|
+
own mode tier, so this sheet writes no dark rule and no :root. Every value is a var()
|
|
10
|
+
or a color-mix() of the fifteen; black with alpha is the scrim. Motion is two short
|
|
11
|
+
transitions and the waiting dots, all stopped by world.css's reduced-motion switch.
|
|
12
|
+
Layout: the plan beside the talk. Every layout value that changes on a phone is a
|
|
13
|
+
--gd-* name read with its desktop value as the fallback, and the landing's ONE
|
|
14
|
+
breakpoint (900px, DESIGN.md "Responsive"; the @media (max-width:900px) body block in
|
|
15
|
+
index.html) restates those names: one column, the frame the whole screen, the plan
|
|
16
|
+
opening over the talk from the Plan button. So this sheet adds no breakpoint, and its
|
|
17
|
+
inset edges are named once here (--gd-edge*) rather than typed into each rule. */
|
|
18
|
+
.gd {
|
|
19
|
+
--gd-night: color-mix(in srgb, var(--sea) 9%, var(--bg));
|
|
20
|
+
--gd-panel: color-mix(in srgb, var(--sea) 13%, var(--bg-card));
|
|
21
|
+
--gd-ice: color-mix(in srgb, var(--sea) 12%, var(--ink));
|
|
22
|
+
--gd-rule: color-mix(in srgb, var(--gd-ice) 40%, transparent);
|
|
23
|
+
--gd-rule-soft: color-mix(in srgb, var(--gd-ice) 20%, transparent);
|
|
24
|
+
--gd-glass: color-mix(in srgb, var(--gd-ice) 9%, transparent);
|
|
25
|
+
--gd-strong: 1.5px;
|
|
26
|
+
--gd-name: "Barlow Condensed", var(--font-display);
|
|
27
|
+
--gd-body: "Barlow", var(--font-body);
|
|
28
|
+
--gd-cut: 12px;
|
|
29
|
+
--gd-cut-sm: 8px;
|
|
30
|
+
--gd-cut-path: polygon(var(--gd-cut) 0, 100% 0, 100% calc(100% - var(--gd-cut)), calc(100% - var(--gd-cut)) 100%, 0 100%, 0 var(--gd-cut));
|
|
31
|
+
--gd-cut-path-sm: polygon(var(--gd-cut-sm) 0, 100% 0, 100% calc(100% - var(--gd-cut-sm)), calc(100% - var(--gd-cut-sm)) 100%, 0 100%, 0 var(--gd-cut-sm));
|
|
32
|
+
--gd-focus: inset 0 0 0 2px var(--focus), inset 0 0 0 4px var(--gd-night);
|
|
33
|
+
/* the hairline drawn inside a cut shape (a border would be clipped by the cut) */
|
|
34
|
+
--gd-edge: inset 0 0 0 1px var(--gd-rule);
|
|
35
|
+
--gd-edge-soft: inset 0 0 0 1px var(--gd-rule-soft);
|
|
36
|
+
--gd-seg-edge: inset 0 0 0 var(--gd-strong) var(--gd-rule);
|
|
37
|
+
|
|
38
|
+
position: fixed; inset: 0; z-index: var(--ds-z-modal, 160);
|
|
39
|
+
display: flex; align-items: center; justify-content: center;
|
|
40
|
+
padding: var(--gd-pad, var(--ds-space-md));
|
|
41
|
+
background: rgba(0, 0, 0, .62);
|
|
42
|
+
-webkit-backdrop-filter: blur(6px) saturate(.7); backdrop-filter: blur(6px) saturate(.7);
|
|
43
|
+
color: var(--gd-ice); font-family: var(--gd-body); font-size: var(--ds-text-xl); line-height: 1.5;
|
|
44
|
+
}
|
|
45
|
+
.gd[hidden] { display: none; }
|
|
46
|
+
.gd .vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
47
|
+
html.gd-open, html.gd-open body { overflow: hidden; }
|
|
48
|
+
|
|
49
|
+
.gd__frame {
|
|
50
|
+
position: relative; box-sizing: border-box;
|
|
51
|
+
width: min(100%, var(--gd-frame-w, 1080px)); height: min(100%, var(--gd-frame-h, 760px));
|
|
52
|
+
display: grid; grid-template-columns: var(--gd-cols, minmax(0, 1fr) 340px);
|
|
53
|
+
background: var(--gd-night);
|
|
54
|
+
border: 0 solid var(--gd-rule); border-left-width: var(--gd-side, var(--gd-strong)); border-right-width: var(--gd-side, var(--gd-strong));
|
|
55
|
+
box-shadow: var(--ds-shadow-pop);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── the talk ── */
|
|
59
|
+
.gd__talk { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
60
|
+
.gd__head {
|
|
61
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-sm);
|
|
62
|
+
padding: var(--ds-space-sm) var(--gd-gutter, var(--ds-space-md));
|
|
63
|
+
border-bottom: var(--gd-strong) solid var(--gd-rule);
|
|
64
|
+
}
|
|
65
|
+
.gd__title, .gd__planh {
|
|
66
|
+
margin: 0; font-family: var(--gd-name); font-weight: 500; font-size: var(--ds-text-3xl);
|
|
67
|
+
letter-spacing: .24em; text-transform: uppercase; line-height: 1.2; color: var(--ink);
|
|
68
|
+
}
|
|
69
|
+
.gd__x {
|
|
70
|
+
flex: none; display: inline-grid; place-items: center; width: 40px; height: 40px; padding: 0;
|
|
71
|
+
background: transparent; color: var(--gd-ice); border: 0; cursor: pointer;
|
|
72
|
+
clip-path: var(--gd-cut-path-sm);
|
|
73
|
+
transition: background-color var(--ds-dur-fast) var(--ds-ease);
|
|
74
|
+
}
|
|
75
|
+
.gd__x svg { width: 18px; height: 18px; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; fill: none; }
|
|
76
|
+
.gd__x:hover { background: var(--gd-glass); }
|
|
77
|
+
|
|
78
|
+
/* the guide: the sprite in a cut frame, its name, the count as words and segments */
|
|
79
|
+
.gd__who-row {
|
|
80
|
+
display: flex; align-items: center; gap: var(--ds-space-sm);
|
|
81
|
+
padding: var(--ds-space-sm) var(--gd-gutter, var(--ds-space-md));
|
|
82
|
+
border-bottom: var(--ds-line, 1px) solid var(--gd-rule-soft);
|
|
83
|
+
}
|
|
84
|
+
.gd__face {
|
|
85
|
+
position: relative; flex: none; width: var(--gd-face, 72px); height: var(--gd-face, 72px); display: grid; place-items: center;
|
|
86
|
+
background: var(--gd-panel); clip-path: var(--gd-cut-path-sm);
|
|
87
|
+
box-shadow: var(--gd-edge);
|
|
88
|
+
}
|
|
89
|
+
.gd__face img { width: var(--gd-face-img, 64px); height: var(--gd-face-img, 64px); image-rendering: pixelated; }
|
|
90
|
+
.gd__face img[hidden] { display: none; }
|
|
91
|
+
.gd__meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
92
|
+
.gd__name {
|
|
93
|
+
margin: 0; font-family: var(--gd-name); font-weight: 600; font-size: var(--ds-text-xl);
|
|
94
|
+
letter-spacing: .2em; text-transform: uppercase; color: var(--ink);
|
|
95
|
+
}
|
|
96
|
+
.gd__left { margin: 0; font-size: var(--ds-text-md); color: var(--gd-ice); font-variant-numeric: tabular-nums; }
|
|
97
|
+
.gd__left[hidden], .gd__segs[hidden] { display: none; }
|
|
98
|
+
.gd__segs { display: flex; flex-wrap: wrap; gap: var(--ds-space-hair); padding-left: 4px; }
|
|
99
|
+
/* a message still to send is an outline; a sent one is filled ice (the Helm segment) */
|
|
100
|
+
.gd__seg {
|
|
101
|
+
width: 22px; height: 8px; transform: skewX(-28deg);
|
|
102
|
+
box-shadow: var(--gd-seg-edge);
|
|
103
|
+
}
|
|
104
|
+
.gd__seg--used { background: var(--gd-ice); box-shadow: none; }
|
|
105
|
+
|
|
106
|
+
/* the conversation */
|
|
107
|
+
.gd__log {
|
|
108
|
+
flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
|
|
109
|
+
margin: 0; padding: var(--ds-space-md) var(--gd-gutter, var(--ds-space-md)); list-style: none;
|
|
110
|
+
display: flex; flex-direction: column; gap: var(--ds-space-sm);
|
|
111
|
+
}
|
|
112
|
+
.gd__msg { display: flex; flex-direction: column; gap: 2px; max-width: 46rem; }
|
|
113
|
+
.gd__who {
|
|
114
|
+
font-family: var(--gd-name); font-size: var(--ds-text-xs); font-weight: 500;
|
|
115
|
+
letter-spacing: .2em; text-transform: uppercase; color: var(--gd-ice);
|
|
116
|
+
}
|
|
117
|
+
.gd__words { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; text-wrap: pretty; color: var(--ink); }
|
|
118
|
+
.gd__msg--visitor { align-self: flex-end; align-items: flex-end; }
|
|
119
|
+
.gd__msg--visitor .gd__words {
|
|
120
|
+
padding: var(--ds-space-hair) var(--ds-space-sm);
|
|
121
|
+
background: var(--gd-panel); clip-path: var(--gd-cut-path-sm);
|
|
122
|
+
box-shadow: var(--gd-edge-soft);
|
|
123
|
+
}
|
|
124
|
+
.gd__msg--note .gd__words {
|
|
125
|
+
padding-top: var(--ds-space-hair); border-top: var(--ds-line, 1px) solid var(--gd-rule-soft);
|
|
126
|
+
color: var(--gd-ice); font-size: var(--ds-text-md);
|
|
127
|
+
}
|
|
128
|
+
/* waiting: three dots that rise in turn; at rest (reduced motion) they simply sit */
|
|
129
|
+
.gd__dot {
|
|
130
|
+
display: inline-block; width: 6px; height: 6px; margin-right: 6px; border-radius: var(--ds-radius-pill);
|
|
131
|
+
background: var(--gd-ice); opacity: .5;
|
|
132
|
+
animation: gd-wait 1.2s var(--ds-ease) infinite;
|
|
133
|
+
}
|
|
134
|
+
.gd__dot:nth-child(3) { animation-delay: .15s; }
|
|
135
|
+
.gd__dot:nth-child(4) { animation-delay: .3s; }
|
|
136
|
+
@keyframes gd-wait { 0%, 60%, 100% { opacity: .35; transform: none; } 30% { opacity: 1; transform: translateY(-3px); } }
|
|
137
|
+
|
|
138
|
+
.gd__err {
|
|
139
|
+
margin: 0 var(--ds-space-md) var(--ds-space-hair); padding: var(--ds-space-hair) var(--ds-space-sm);
|
|
140
|
+
border-left: var(--gd-strong) solid var(--err); background: var(--gd-glass);
|
|
141
|
+
color: var(--ink); font-size: var(--ds-text-md);
|
|
142
|
+
}
|
|
143
|
+
.gd__err[hidden] { display: none; }
|
|
144
|
+
|
|
145
|
+
/* the composer */
|
|
146
|
+
.gd__say {
|
|
147
|
+
display: flex; align-items: flex-end; gap: var(--ds-space-hair); flex-wrap: wrap;
|
|
148
|
+
padding: var(--ds-space-sm) var(--gd-gutter, var(--ds-space-md)) var(--ds-space-md);
|
|
149
|
+
border-top: var(--gd-strong) solid var(--gd-rule);
|
|
150
|
+
}
|
|
151
|
+
.gd__say[hidden] { display: none; }
|
|
152
|
+
.gd__text {
|
|
153
|
+
flex: 1 1 16rem; min-width: 0; min-height: 48px; max-height: 9.5rem; resize: vertical; box-sizing: border-box;
|
|
154
|
+
padding: 11px var(--ds-space-sm);
|
|
155
|
+
background: var(--gd-panel); color: var(--ink); caret-color: var(--accent);
|
|
156
|
+
border: var(--ds-line, 1px) solid var(--gd-rule); border-radius: 0;
|
|
157
|
+
font: inherit; font-size: var(--ds-text-xl);
|
|
158
|
+
}
|
|
159
|
+
.gd__text::placeholder { color: var(--gd-ice); opacity: .8; }
|
|
160
|
+
.gd__text:focus-visible { outline: 2px solid var(--focus); outline-offset: 0; box-shadow: none; border-color: transparent; }
|
|
161
|
+
.gd__text:disabled { opacity: .7; }
|
|
162
|
+
.gd__hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
|
|
163
|
+
/* under the box: end the chat early for the plan, or leave for sign-up */
|
|
164
|
+
.gd__sayfoot { flex-basis: 100%; display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-sm); flex-wrap: wrap; }
|
|
165
|
+
.gd__skip {
|
|
166
|
+
display: inline-flex; align-items: center; padding: 2px 0; min-height: 24px;
|
|
167
|
+
color: var(--gd-ice); font-size: var(--ds-text-md); text-decoration: underline; text-underline-offset: 3px;
|
|
168
|
+
}
|
|
169
|
+
.gd__skip:hover { color: var(--ink); }
|
|
170
|
+
|
|
171
|
+
/* what you press: cut, in the name voice. The primary is the one accent bar. */
|
|
172
|
+
.gd__send, .gd__go, .gd__planbtn {
|
|
173
|
+
display: inline-flex; align-items: center; justify-content: center; gap: var(--ds-space-hair);
|
|
174
|
+
min-height: 48px; padding: 0 var(--ds-space-md); box-sizing: border-box; border: 0; cursor: pointer;
|
|
175
|
+
font-family: var(--gd-name); font-size: var(--ds-text-xl); font-weight: 600;
|
|
176
|
+
letter-spacing: .2em; text-transform: uppercase; text-decoration: none;
|
|
177
|
+
clip-path: var(--gd-cut-path);
|
|
178
|
+
transition: background-color var(--ds-dur-fast) var(--ds-ease);
|
|
179
|
+
}
|
|
180
|
+
.gd__send, .gd__go { background: var(--accent); color: var(--accent-on); }
|
|
181
|
+
.gd__send:hover, .gd__go:hover { background: var(--accent-lift); color: var(--accent-on); }
|
|
182
|
+
.gd__send:disabled { background: var(--gd-panel); color: var(--gd-ice); cursor: default; }
|
|
183
|
+
/* the Plan button is the one-column layout's only (the plan stands beside the talk above it) */
|
|
184
|
+
.gd__planbtn { display: var(--gd-planbtn, none); flex: none; background: var(--gd-panel); color: var(--ink); box-shadow: var(--gd-edge); min-height: 40px; padding: 0 var(--ds-space-sm); font-size: var(--ds-text-md); }
|
|
185
|
+
.gd__planbtn:hover { background: var(--gd-glass); }
|
|
186
|
+
/* a cut control's focus is the inset two-tone ring, since a clip-path clips an outline */
|
|
187
|
+
.gd__x:focus-visible, .gd__send:focus-visible, .gd__go:focus-visible, .gd__planbtn:focus-visible {
|
|
188
|
+
outline: 0; box-shadow: var(--gd-focus);
|
|
189
|
+
}
|
|
190
|
+
.gd__skip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; box-shadow: none; }
|
|
191
|
+
|
|
192
|
+
/* the end: one line and the way on */
|
|
193
|
+
.gd__end {
|
|
194
|
+
display: flex; align-items: center; justify-content: flex-end; gap: var(--ds-space-sm); flex-wrap: wrap;
|
|
195
|
+
padding: var(--ds-space-sm) var(--gd-gutter, var(--ds-space-md)) var(--ds-space-md);
|
|
196
|
+
border-top: var(--gd-strong) solid var(--gd-rule);
|
|
197
|
+
}
|
|
198
|
+
.gd__end[hidden] { display: none; }
|
|
199
|
+
.gd__go { flex: 0 1 auto; width: var(--gd-go-w, auto); box-shadow: var(--accent-glow); }
|
|
200
|
+
.gd__go[hidden] { display: none; }
|
|
201
|
+
/* the quiet press: a cut ghost in the name voice (Make my plan) */
|
|
202
|
+
.gd__more {
|
|
203
|
+
display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 var(--ds-space-sm);
|
|
204
|
+
background: transparent; color: var(--ink); border: 0; cursor: pointer;
|
|
205
|
+
box-shadow: var(--gd-edge); clip-path: var(--gd-cut-path-sm);
|
|
206
|
+
font-family: var(--gd-name); font-size: var(--ds-text-md); font-weight: 600; letter-spacing: .2em; text-transform: uppercase;
|
|
207
|
+
transition: background-color var(--ds-dur-fast) var(--ds-ease);
|
|
208
|
+
}
|
|
209
|
+
.gd__more:hover { background: var(--gd-glass); }
|
|
210
|
+
.gd__more:focus-visible { outline: 0; box-shadow: var(--gd-focus); }
|
|
211
|
+
.gd__more[hidden] { display: none; }
|
|
212
|
+
|
|
213
|
+
/* ── the plan ── */
|
|
214
|
+
.gd__plan {
|
|
215
|
+
display: var(--gd-plan-show, flex); flex-direction: column; min-height: 0;
|
|
216
|
+
position: var(--gd-plan-pos, static); inset: 0; z-index: 1;
|
|
217
|
+
background: var(--gd-panel);
|
|
218
|
+
border-left: var(--gd-plan-edge, var(--ds-line, 1px)) solid var(--gd-rule-soft);
|
|
219
|
+
}
|
|
220
|
+
/* one column: the Plan button lays the plan over the talk */
|
|
221
|
+
.gd--plan .gd__plan { display: flex; }
|
|
222
|
+
.gd--plan .gd__talk { visibility: var(--gd-talk-under-plan, visible); }
|
|
223
|
+
.gd__planhead {
|
|
224
|
+
display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-sm);
|
|
225
|
+
padding: var(--ds-space-sm) var(--ds-space-md);
|
|
226
|
+
border-bottom: var(--gd-strong) solid var(--gd-rule);
|
|
227
|
+
min-height: 72px; box-sizing: border-box;
|
|
228
|
+
}
|
|
229
|
+
.gd__planhead .gd__x { display: var(--gd-planx, none); }
|
|
230
|
+
.gd__planbody { flex: 1; min-height: 0; overflow-y: auto; padding: var(--ds-space-md); display: flex; flex-direction: column; gap: var(--ds-space-hair); }
|
|
231
|
+
.gd__planwait { margin: 0; color: var(--gd-ice); font-size: var(--ds-text-lg); }
|
|
232
|
+
.gd__plansum { margin: 0; color: var(--ink); font-size: var(--ds-text-lg); text-wrap: pretty; }
|
|
233
|
+
.gd__planh4 {
|
|
234
|
+
margin: var(--ds-space-sm) 0 0; font-family: var(--gd-name); font-size: var(--ds-text-xs); font-weight: 500;
|
|
235
|
+
letter-spacing: .2em; text-transform: uppercase; color: var(--gd-ice);
|
|
236
|
+
}
|
|
237
|
+
.gd__planlist { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
|
|
238
|
+
.gd__plannote, .gd__saved { margin: 0; color: var(--gd-ice); font-size: var(--ds-text-md); }
|
|
239
|
+
.gd__plannote { margin-top: var(--ds-space-sm); padding-top: var(--ds-space-hair); border-top: var(--ds-line, 1px) solid var(--gd-rule-soft); }
|
|
240
|
+
.gd__saved { padding: var(--ds-space-sm) var(--ds-space-md); border-top: var(--ds-line, 1px) solid var(--gd-rule-soft); }
|
|
241
|
+
.gd__saved[hidden] { display: none; }
|
|
242
|
+
.gd__planlist li {
|
|
243
|
+
padding: var(--ds-space-hair) 0; border-top: var(--ds-line, 1px) solid var(--gd-rule-soft);
|
|
244
|
+
color: var(--ink); font-size: var(--ds-text-lg);
|
|
245
|
+
}
|