@bongos/core 1.19.298 → 1.19.300

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/docs/file-map.md CHANGED
@@ -234,7 +234,7 @@
234
234
  │ ├── terrain.js ← (Phase D + ADR 0012, CJS): TERRAIN + OBJECT enums + BLOCKING_TERRAINS + BLOCKING_OBJECTS (server twin of public/game/world/terrain.js)
235
235
  │ └── map.js ← (Phase D + ADR 0012): two-grid INITIAL_MAP {ground, objects}; isBlocked checks both layers (mirrors public/game/world/{terrain,map}.js)
236
236
  │ (public-status/ moved to modules/status-ui/public/ in BV1.R84 — see the modules/ block above)
237
- │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
237
+ │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; ideas.{html,js,css}=the Full Idea composer (task 1002928 / R152 of goal 1000062 — the five R141 fields with a live R142 completeness meter, both fetched from the server so the page carries no copy of the rules; drafts in localStorage so navigation costs nothing); idea.html/idea.js=the idea-detail PAGE (/idea/:id, task 1002929 / R153 — the SECOND two-segment hall page after /task/:id: grade, the five answers in template order, the score, and both attributions. The GRADE decides what the page contains — a quick idea gets no five-field skeleton. task-detail.js keeps the #/idea/N overlay because ~320 of its lines are the Metic+ triage panel; the overlay now links here); profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
238
238
  │
239
239
  ├── infra/ ← ON-BOX scripts (task 1002699): the four reporter/heartbeat crons every dev box runs from its /workspace clone — box-heartbeat.sh (idle-sweep activity signal), box-report-version.sh (HEAD → POST /box/version), box-report-host-keys.sh (SSH host keys → POST /box/host-keys), box-report-terminal.sh (ttyd URL+credential → POST /box/terminal). Delivered by the workspace clone itself (modules/dev-box/box-access.js sparse scope includes infra/); crons self-installed by box-source-fetch.sh's per-tick bring-up. API base resolves GDS_BASE env → session-file api_base → skip (no hardcoded host in the neutral core).
240
240
  ├── devbox-app/ ← OTB Dev Box — Electron menu-bar/tray switch for the builder's box (#904, ADR 0045; internal). SELF-CONTAINED: own package.json (never feeds the droplet's npm ci); built by .github/workflows/build-devbox-app.yml; published to the public releases repo per config/devbox-app.json; release recipe docs/recipes/devbox-app-release.md
@@ -416,8 +416,12 @@ function createClient(opts = {}) {
416
416
  postInboxIdSpark: (args) => request("POST", "/inbox/{id}/spark", { hasBody: true }, args),
417
417
  // GET /inbox/awaiting-nod — rank: metic+archon — GET /inbox/awaiting-nod
418
418
  getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
419
+ // POST /inbox/score — rank: any-builder — POST /inbox/score
420
+ postInboxScore: (args) => request("POST", "/inbox/score", { hasBody: true }, args),
419
421
  // GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
420
422
  getInboxSparks: (args) => request("GET", "/inbox/sparks", { hasBody: false }, args),
423
+ // GET /inbox/template — rank: any-builder — GET /inbox/template
424
+ getInboxTemplate: (args) => request("GET", "/inbox/template", { hasBody: false }, args),
421
425
  },
422
426
  "instance": {
423
427
  // GET /instance — rank: public — GET /instance
@@ -0,0 +1,74 @@
1
+ /* modules/hall-ui/public/idea.css — the idea-detail page (task 1002929 / R153
2
+ of goal 1000062).
3
+
4
+ Promoted out of an inline <style> because tests/hall_audit.mjs budgets an inline
5
+ block at 15 lines and this is ~60: past that the documented pattern (task 2176)
6
+ is a per-page stylesheet. The exceptions in that budget map are grandfathered or
7
+ deliberate; a brand-new page has no business claiming one.
8
+
9
+ Built on the hall's tokens. No --line/--panel: neither is defined in style.css,
10
+ so a fallback is all you would ever get and both assume a dark ground the hall
11
+ does not have by default (--bg is #f5f6f8). */
12
+
13
+ /* Page-local, like task.html (task 751) — this page adds a handful of shapes
14
+ and reuses the hall's tokens for everything else. No --line/--panel here:
15
+ neither is defined in style.css, so a fallback is all you would ever get
16
+ and both assume a dark ground the hall does not have by default. */
17
+
18
+ .idea-detail__back { font-size: 13px; color: var(--ink-soft); text-decoration: none; display: inline-block; margin-bottom: 10px; }
19
+ .idea-detail__back:hover { color: var(--ink); text-decoration: underline; }
20
+
21
+ /* The grade is the first thing to read: it decides what the rest of the page
22
+ even contains. Three states, distinguished by colour AND by word — a badge
23
+ that only differs by hue is unreadable to a chunk of readers. */
24
+ .ig-grade {
25
+ display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 10px;
26
+ border-radius: 999px; font-size: 12px; font-weight: 600;
27
+ background: var(--surface-2); border: 1px solid var(--border); color: var(--ink-soft);
28
+ }
29
+ .ig-grade--full { background: var(--ok-weak); color: var(--ok); border-color: color-mix(in srgb, var(--ok) 30%, transparent); }
30
+ .ig-grade--spark { background: var(--warn-weak); color: var(--warn); border-color: color-mix(in srgb, var(--warn) 30%, transparent); }
31
+ .ig-grade--quick { background: var(--accent-weak); color: var(--accent-text); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
32
+
33
+ .ig-head { display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: center; margin-bottom: 4px; }
34
+ .ig-title { margin: 0; font-size: 1.15rem; text-wrap: balance; }
35
+
36
+ .ig-meta {
37
+ font-size: 13px; color: var(--ink-soft); margin: 0 0 16px;
38
+ display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: baseline;
39
+ }
40
+ .ig-meta strong { color: var(--ink); font-weight: 600; }
41
+
42
+ /* Attribution is its own block, not a line in the meta run: on a developed
43
+ idea it carries TWO people and the whole point of R146 was that both
44
+ survive. Burying that in a comma list is how one of them gets lost. */
45
+ .ig-credit {
46
+ display: grid; gap: 6px;
47
+ padding: 12px 14px; margin: 0 0 18px;
48
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
49
+ background: var(--surface-2); font-size: 13px;
50
+ }
51
+ .ig-credit__row { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline; }
52
+ .ig-credit__who { color: var(--ink); font-weight: 600; }
53
+ .ig-credit__role { color: var(--ink-faint); }
54
+
55
+ /* The five answers, laid out to be READ: one column, generous leading, a
56
+ measure that stops around 70 characters. */
57
+ .ig-answers { display: grid; gap: 18px; margin: 4px 0 0; }
58
+ .ig-answer__q { margin: 0 0 4px; font-size: 0.92rem; font-weight: 600; }
59
+ .ig-answer__a { margin: 0; max-width: 70ch; line-height: 1.65; white-space: pre-wrap; }
60
+ .ig-answer__a[data-empty="1"] { color: var(--ink-faint); font-style: italic; }
61
+
62
+ .ig-score { display: flex; align-items: center; gap: 10px; margin: 0 0 6px; }
63
+ .ig-score__track { width: 140px; height: 8px; border-radius: 4px; background: var(--border); overflow: hidden; }
64
+ .ig-score__fill { height: 100%; border-radius: 4px; background: var(--accent); }
65
+ .ig-score__fill[data-band="full"] { background: var(--ok); }
66
+ .ig-score__fill[data-band="part"] { background: var(--warn); }
67
+ .ig-score__fill[data-band="thin"] { background: var(--danger); }
68
+ .ig-score__n { font-size: 13px; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
69
+
70
+ .ig-body { max-width: 70ch; line-height: 1.65; white-space: pre-wrap; margin: 4px 0 0; }
71
+ .ig-empty { color: var(--ink-faint); font-size: 0.9rem; }
72
+ .ig-note { font-size: 0.82rem; color: var(--ink-faint); line-height: 1.55; margin: 14px 0 0; }
73
+ .ig-sub { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border); }
74
+ .ig-sub__h { margin: 0 0 8px; font-size: 0.95rem; }
@@ -0,0 +1,55 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Idea — {{worldName}}</title>
7
+ <meta name="description" content="One idea, read end to end: its grade, its five answers, how complete they are, who had the thought and who developed it.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
13
+ <link rel="stylesheet" href="/builders/idea.css">
14
+ </head>
15
+ <body data-page="idea" data-page-title="Idea">
16
+
17
+ <div class="app">
18
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
+ <div class="app-scrim" id="app-scrim" hidden></div>
20
+ <div class="app-body">
21
+ <header class="app-topbar" id="app-topbar"></header>
22
+ <div class="app-content">
23
+ <div class="page-head">
24
+ <!-- This page is deliberately not in the nav (it is always reached from a
25
+ link), so a deep-linked visitor needs a way back before the body loads
26
+ — the task.html precedent. -->
27
+ <a class="idea-detail__back" href="/ideas">← Back to Ideas</a>
28
+ <p class="page-head__sub" id="idea-sub">Loading idea…</p>
29
+ </div>
30
+
31
+ <main>
32
+ <section class="scroll" id="idea-scroll" aria-labelledby="idea-h">
33
+ <div id="idea-body">
34
+ <p class="deeplink-card__title" id="idea-h">Loading…</p>
35
+ </div>
36
+ </section>
37
+ </main>
38
+
39
+ <footer class="app-foot" id="app-foot"></footer>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
45
+
46
+ <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
47
+ <script src="/builders/palette.js" defer></script>
48
+ <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
49
+ <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
50
+ written inside an idea's own prose keeps resolving from this page. -->
51
+ <script src="/builders/task-detail.js?v=2026-07-03-taskdetail2" defer></script>
52
+ <script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
53
+ <script src="/builders/idea.js?v=2026-08-22-r153" defer></script>
54
+ </body>
55
+ </html>
@@ -0,0 +1,203 @@
1
+ // modules/hall-ui/public/idea.js — the idea-detail PAGE (/idea/:id).
2
+ // Task 1002929 / BV1.R153 of goal 1000062 (three grades of idea).
3
+ //
4
+ // A real page rather than a wider modal, following task 751: that task replaced
5
+ // the #/task/N overlay with /task/:id so a record got a shareable URL and working
6
+ // back/forward. A Full Idea is five prose answers — the thing an overlay is worst
7
+ // at — so it earns the same treatment.
8
+ //
9
+ // THE OVERLAY IS NOT REPLACED. task-detail.js keeps owning #/idea/N, because ~320
10
+ // of its ~650 lines are the Metic+ triage panel (promote → POST /tasks, merge,
11
+ // discard, with goal hints and version pickers). Moving that machinery is its own
12
+ // task; breaking a shipped triage flow to make a read prettier would be a bad
13
+ // trade. So the overlay stays the fast peek and links here, and this page stays a
14
+ // READ — the summary→detail relationship the hall already has between the Work
15
+ // board and /task/:id.
16
+ //
17
+ // WHAT THE GRADE DECIDES. The grade is not decoration on this page, it selects
18
+ // what the page contains:
19
+ // full → the five answers, each under its own question, plus the score
20
+ // spark → the body, plus "someone is invited to develop this"
21
+ // quick → the body, and NOTHING about the five fields
22
+ // The done-when names that last case explicitly: a quick idea must render with no
23
+ // empty five-field skeleton. Five headings over five dashes is not an empty
24
+ // state, it is a page telling a builder they did something wrong by filing a
25
+ // quick idea — which is the opposite of what a quick idea is for.
26
+ //
27
+ // The five QUESTIONS come from GET /inbox/template, not from this file, for the
28
+ // same reason the composer's do (R152): the labels live in the R141 declaration,
29
+ // and a copy here would silently ignore a per-project template.
30
+
31
+ (() => {
32
+ 'use strict';
33
+
34
+ const API = '/api/gds';
35
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
36
+ const { escapeHtml, $, fmtDate } = window.OTB;
37
+
38
+ const ideaId = () => {
39
+ const m = /\/idea\/(\d+)/.exec(location.pathname);
40
+ return m ? m[1] : null;
41
+ };
42
+
43
+ const hallBase = () => (location.hostname.startsWith('builders.') ? '' : '/builders');
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // pieces
47
+ // ---------------------------------------------------------------------------
48
+
49
+ // Colour AND word. A badge distinguished only by hue tells a colour-blind
50
+ // reader nothing, and this badge decides how to read the whole page.
51
+ const GRADE_WORD = { full: 'Full Idea', spark: 'Big Idea Spark', quick: 'Quick idea' };
52
+
53
+ function gradeBadge(grade) {
54
+ const g = GRADE_WORD[grade] ? grade : 'quick';
55
+ return `<span class="ig-grade ig-grade--${escapeHtml(g)}">${escapeHtml(GRADE_WORD[g])}</span>`;
56
+ }
57
+
58
+ function scoreRow(score) {
59
+ if (!Number.isInteger(score)) {
60
+ // A Full Idea with no score is a real state (R142 returns null for
61
+ // anything it cannot score, and that is stored as-is). Say so rather than
62
+ // drawing an empty bar that reads as zero.
63
+ return '<p class="ig-empty">Not scored.</p>';
64
+ }
65
+ const band = score >= 80 ? 'full' : score >= 40 ? 'part' : 'thin';
66
+ return `
67
+ <div class="ig-score">
68
+ <span class="ig-score__track"><span class="ig-score__fill" data-band="${band}" style="width:${score}%"></span></span>
69
+ <span class="ig-score__n">${score}/100 complete</span>
70
+ </div>`;
71
+ }
72
+
73
+ // Who had the thought, and who wrote it out. Two rows when they are two people
74
+ // — R146's whole point is that the author survives being developed by someone
75
+ // else, so this never collapses them into one line.
76
+ function credit(idea) {
77
+ const who = (login, id) => (login ? `@${escapeHtml(login)}` : (id ? `builder ${escapeHtml(String(id))}` : 'someone'));
78
+ const rows = [];
79
+ rows.push(`
80
+ <div class="ig-credit__row">
81
+ <span class="ig-credit__who">${who(idea.filed_by_login, idea.captured_by)}</span>
82
+ <span class="ig-credit__role">had the thought${idea.captured_at ? ` · ${escapeHtml(fmtDate(idea.captured_at))}` : ''}</span>
83
+ </div>`);
84
+ if (idea.developed_by) {
85
+ const same = String(idea.developed_by) === String(idea.captured_by);
86
+ rows.push(`
87
+ <div class="ig-credit__row">
88
+ <span class="ig-credit__who">${who(idea.developed_by_login, idea.developed_by)}</span>
89
+ <span class="ig-credit__role">${same ? 'developed it themselves' : 'developed it into a Full Idea'}${idea.developed_at ? ` · ${escapeHtml(fmtDate(idea.developed_at))}` : ''}</span>
90
+ </div>`);
91
+ }
92
+ return `<div class="ig-credit">${rows.join('')}</div>`;
93
+ }
94
+
95
+ function prose(text) {
96
+ const s = String(text == null ? '' : text).trim();
97
+ return s
98
+ ? `<p class="ig-body">${escapeHtml(s)}</p>`
99
+ : '<p class="ig-body" data-empty="1">No description.</p>';
100
+ }
101
+
102
+ // The five answers, in TEMPLATE order, each under its own question. Missing
103
+ // answers are marked rather than hidden: on a Full Idea a blank answer is
104
+ // information (it is why the score is what it is).
105
+ function answers(idea, template) {
106
+ const values = (idea.field_values && typeof idea.field_values === 'object') ? idea.field_values : {};
107
+ return `<div class="ig-answers">${template.fields.map((f) => {
108
+ const v = String(values[f.key] == null ? '' : values[f.key]).trim();
109
+ return `
110
+ <div class="ig-answer">
111
+ <p class="ig-answer__q">${escapeHtml(f.label)}</p>
112
+ <p class="ig-answer__a"${v ? '' : ' data-empty="1"'}>${v ? escapeHtml(v) : 'Not answered.'}</p>
113
+ </div>`;
114
+ }).join('')}</div>`;
115
+ }
116
+
117
+ function statusLine(idea) {
118
+ const bits = [];
119
+ bits.push(`<span><strong>${escapeHtml(idea.status || 'open')}</strong></span>`);
120
+ if (idea.kind) bits.push(`<span>${escapeHtml(idea.kind)}</span>`);
121
+ if (idea.promoted_to_task_id) {
122
+ bits.push(`<span>became <a href="${hallBase()}/task/${encodeURIComponent(idea.promoted_to_task_id)}">task ${escapeHtml(String(idea.promoted_to_task_id))}</a></span>`);
123
+ }
124
+ if (idea.merged_into_idea_id) {
125
+ bits.push(`<span>merged into <a href="${hallBase()}/idea/${encodeURIComponent(idea.merged_into_idea_id)}">idea ${escapeHtml(String(idea.merged_into_idea_id))}</a></span>`);
126
+ }
127
+ return `<p class="ig-meta">${bits.join('')}</p>`;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // render
132
+ // ---------------------------------------------------------------------------
133
+
134
+ function render(idea, template) {
135
+ const grade = GRADE_WORD[idea.grade] ? idea.grade : 'quick';
136
+ const parts = [];
137
+
138
+ parts.push(`<div class="ig-head">${gradeBadge(grade)}</div>`);
139
+ parts.push(`<h1 class="ig-title" id="idea-h">${escapeHtml(idea.title || `Idea ${idea.id}`)}</h1>`);
140
+ parts.push(statusLine(idea));
141
+ parts.push(credit(idea));
142
+
143
+ if (grade === 'full') {
144
+ parts.push(scoreRow(idea.completeness_score));
145
+ if (template) {
146
+ parts.push(answers(idea, template));
147
+ } else {
148
+ // The template read failed. Show the raw answers rather than nothing —
149
+ // but WITHOUT inventing an order, since jsonb key order is not the
150
+ // template's order and pretending otherwise would misreport the idea.
151
+ parts.push('<p class="ig-empty">Could not load the question labels; showing the answers as stored.</p>');
152
+ parts.push(`<div class="ig-answers">${Object.keys(idea.field_values || {}).map((k) =>
153
+ `<div class="ig-answer"><p class="ig-answer__q">${escapeHtml(k)}</p><p class="ig-answer__a">${escapeHtml(String(idea.field_values[k] || ''))}</p></div>`
154
+ ).join('')}</div>`);
155
+ }
156
+ // A Full Idea's body_md is the original quick-idea text it grew out of, when
157
+ // it had one. Shown after the answers, subordinate to them.
158
+ if (String(idea.body_md || '').trim()) {
159
+ parts.push(`<div class="ig-sub"><h2 class="ig-sub__h">As first filed</h2>${prose(idea.body_md)}</div>`);
160
+ }
161
+ } else {
162
+ // quick AND spark: the body, and NOT a five-field skeleton.
163
+ parts.push(prose(idea.body_md));
164
+ if (grade === 'spark') {
165
+ parts.push(`<p class="ig-note">Its author marked this as needing development — anyone can turn it into a Full Idea, and they keep the credit for having had it. <a href="${hallBase()}/ideas">Write one</a>.</p>`);
166
+ } else {
167
+ parts.push('<p class="ig-note">A quick idea is a thought filed fast. It is not an unfinished Full Idea — only its author can offer it up for development.</p>');
168
+ }
169
+ }
170
+
171
+ $('#idea-body').innerHTML = parts.join('');
172
+ $('#idea-sub').textContent = `Idea ${idea.id}`;
173
+ document.title = `${idea.title || `Idea ${idea.id}`} — idea ${idea.id}`;
174
+ }
175
+
176
+ function fail(msg) {
177
+ $('#idea-body').innerHTML = `<p class="deeplink-card__title" id="idea-h">${escapeHtml(msg)}</p>`;
178
+ $('#idea-sub').textContent = '';
179
+ }
180
+
181
+ async function main() {
182
+ const id = ideaId();
183
+ if (!id) return fail('No idea id in the address.');
184
+
185
+ const r = await api.inbox.getInboxId({ id });
186
+ if (r.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
187
+ if (r.status === 404) return fail(`No idea ${id}.`);
188
+ if (!r.ok || !r.data || !r.data.idea) return fail('Could not load this idea.');
189
+ const idea = r.data.idea;
190
+
191
+ // The labels are only needed for a Full Idea, so a quick idea costs one
192
+ // request, not two — and a template outage cannot stop a quick idea rendering.
193
+ let template = null;
194
+ if (idea.grade === 'full') {
195
+ const key = idea.template_key;
196
+ const t = await api.inbox.getInboxTemplate(key ? { query: { template_key: key } } : {});
197
+ if (t.ok && t.data && t.data.template) template = t.data.template;
198
+ }
199
+ render(idea, template);
200
+ }
201
+
202
+ main();
203
+ })();
@@ -0,0 +1,120 @@
1
+ /* modules/hall-ui/public/ideas.css — the Ideas tab (task 1002928 / BV1.R152 of
2
+ goal 1000062).
3
+
4
+ Built on the hall's existing tokens (style.css), not a new palette. Only the
5
+ shapes this page needs are here: a wide-screen two-column composer with the
6
+ meter alongside, collapsing to one column where there is no room for it.
7
+
8
+ USES THE REAL TOKEN NAMES — --border / --surface-2 / --accent / --ok / --warn /
9
+ --danger / --radius. Note for anyone copying a sibling sheet: copy-desk.css
10
+ reaches for `var(--line, rgba(255,255,255,.12))` and `var(--panel, …)`, and
11
+ NEITHER --line NOR --panel is defined in style.css — so those fallbacks apply,
12
+ and they assume a dark ground the hall does not have by default (--bg is
13
+ #f5f6f8). Borders drawn that way are invisible on the light theme. This sheet
14
+ deliberately does not inherit that. The hall's dark theme flips only the
15
+ neutrals, so every token below re-derives on its own and this page needs no
16
+ theme-specific block at all. */
17
+
18
+ /* The composer and its meter sit side by side once there is room. Below that
19
+ width the meter stacks ABOVE the fields — a readout you cannot see while
20
+ typing is not a live meter. */
21
+ @media (min-width: 62rem) {
22
+ .app-content > main { padding-right: 20rem; }
23
+ .fi-meter {
24
+ position: fixed;
25
+ top: 6.5rem;
26
+ right: 1.5rem;
27
+ width: 17rem;
28
+ max-height: calc(100vh - 8rem);
29
+ overflow-y: auto;
30
+ }
31
+ }
32
+
33
+ .fi-label {
34
+ display: block;
35
+ margin: 1.1rem 0 0.3rem;
36
+ font-size: 0.9rem;
37
+ font-weight: 600;
38
+ }
39
+ .fi-hint { font-weight: 400; font-size: 0.8rem; opacity: 0.7; }
40
+
41
+ .fi-input,
42
+ .fi-text {
43
+ width: 100%;
44
+ padding: 0.55rem 0.7rem;
45
+ border: 1px solid var(--border);
46
+ border-radius: var(--radius-sm);
47
+ background: var(--surface-2);
48
+ color: inherit;
49
+ font: inherit;
50
+ line-height: 1.55;
51
+ }
52
+ .fi-text { resize: vertical; min-height: 5.5rem; }
53
+ .fi-input:focus,
54
+ .fi-text:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
55
+
56
+ .fi-fields { margin-top: 0.5rem; }
57
+ .fi-field + .fi-field { margin-top: 0.35rem; }
58
+
59
+ .fi-fieldnote { margin: 0.3rem 0 0; font-size: 0.78rem; opacity: 0.7; min-height: 1.1em; }
60
+ .fi-fieldnote[data-ok="1"] { opacity: 0.85; color: var(--ok); }
61
+
62
+ .fi-note { font-size: 0.85rem; opacity: 0.75; }
63
+
64
+ .fi-err {
65
+ margin: 0.9rem 0 0;
66
+ font-size: 0.87rem;
67
+ line-height: 1.5;
68
+ color: var(--danger);
69
+ }
70
+ .fi-err[data-visible="0"] { display: none; }
71
+
72
+ .fi-actions {
73
+ display: flex;
74
+ flex-wrap: wrap;
75
+ gap: 0.6rem 0.75rem;
76
+ align-items: center;
77
+ margin-top: 1.2rem;
78
+ }
79
+ .fi-draft { font-size: 0.8rem; opacity: 0.7; }
80
+
81
+ /* ---- the meter ---------------------------------------------------------- */
82
+
83
+ .fi-meter {
84
+ border: 1px solid var(--border);
85
+ border-radius: var(--radius);
86
+ background: var(--surface-2);
87
+ padding: 0.9rem 1rem 1rem;
88
+ margin: 1.5rem;
89
+ }
90
+ .fi-meter__h { margin: 0 0 0.6rem; font-size: 0.95rem; }
91
+
92
+ .fi-gauge { display: flex; align-items: center; gap: 0.6rem; }
93
+ .fi-gauge__track {
94
+ flex: 1;
95
+ height: 0.55rem;
96
+ border-radius: 0.3rem;
97
+ background: var(--border);
98
+ overflow: hidden;
99
+ }
100
+ .fi-gauge__fill {
101
+ height: 100%;
102
+ width: 0;
103
+ border-radius: 0.3rem;
104
+ transition: width 0.18s ease-out;
105
+ background: var(--accent);
106
+ }
107
+ /* Three bands describing the TEXT, not a verdict — the pass bar lives in the
108
+ economy module and this readout deliberately does not know it. */
109
+ .fi-gauge__fill[data-band="thin"] { background: var(--danger); }
110
+ .fi-gauge__fill[data-band="part"] { background: var(--warn); }
111
+ .fi-gauge__fill[data-band="full"] { background: var(--ok); }
112
+ .fi-gauge__n { margin: 0; font-size: 0.9rem; font-variant-numeric: tabular-nums; min-width: 4.2rem; text-align: right; }
113
+
114
+ .fi-meter__list { list-style: none; margin: 0.85rem 0 0; padding: 0; display: grid; gap: 0.45rem; }
115
+ .fi-mrow { display: grid; gap: 0.2rem; }
116
+ .fi-mrow__l { font-size: 0.76rem; opacity: 0.8; line-height: 1.3; }
117
+ .fi-mrow__bar { display: block; height: 0.3rem; border-radius: 0.2rem; background: var(--border); overflow: hidden; }
118
+ .fi-mrow__fill { display: block; height: 100%; width: 0; border-radius: 0.2rem; background: var(--accent); transition: width 0.18s ease-out; }
119
+
120
+ .fi-meter__note { margin: 0.9rem 0 0; font-size: 0.76rem; opacity: 0.65; line-height: 1.5; }
@@ -0,0 +1,95 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Ideas — {{worldName}}</title>
7
+ <meta name="description" content="Write a Full Idea: five questions that turn a thought into a baseline scope of work someone else can deliver against, with a live completeness score.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
13
+ <link rel="stylesheet" href="/builders/ideas.css">
14
+ </head>
15
+ <body data-page="ideas" data-page-title="Ideas">
16
+
17
+ <div class="app">
18
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
+ <div class="app-scrim" id="app-scrim" hidden></div>
20
+ <div class="app-body">
21
+ <header class="app-topbar" id="app-topbar"></header>
22
+ <div class="app-content">
23
+ <div class="page-head"><p class="page-head__sub" id="fi-sub">Loading the idea template…</p></div>
24
+
25
+ <main>
26
+ <!-- The composer. ONE section, deliberately: this page's whole job is
27
+ "write the thing down", and the second panel that would explain the
28
+ grades belongs in the Primer, not above the field you came to fill in.
29
+ The idea detail view (R153) and the spark queue (R154) are separate
30
+ tasks and will add their own sections here. -->
31
+ <section class="scroll" id="fi-compose-scroll" aria-labelledby="fi-compose-h">
32
+ <h2 id="fi-compose-h" class="scroll__h">Write a Full Idea</h2>
33
+ <p class="scroll__lede">
34
+ A Full Idea answers five questions, so that someone else can deliver against it
35
+ without having to interview you first. Open to every role — the score below is
36
+ the only filter, and it counts substance, not who you are.
37
+ </p>
38
+
39
+ <form id="fi-form" novalidate>
40
+ <label class="fi-label" for="fi-title">
41
+ Title
42
+ <span class="fi-hint">One line. This is what the idea is called everywhere else.</span>
43
+ </label>
44
+ <input type="text" id="fi-title" class="fi-input" maxlength="200" autocomplete="off"
45
+ placeholder="what the idea is, in one line">
46
+
47
+ <!-- The five fields are rendered from GET /inbox/template — the R141
48
+ declaration — never hand-written here. A hard-coded form is the
49
+ same "second copy of the rules" defect as a hard-coded scorer,
50
+ one step earlier, and it would silently ignore a per-project
51
+ template (R156). -->
52
+ <div id="fi-fields" class="fi-fields">
53
+ <p class="fi-note">Loading the five questions…</p>
54
+ </div>
55
+
56
+ <p class="fi-err" id="fi-err" role="alert"></p>
57
+
58
+ <div class="fi-actions">
59
+ <button type="submit" class="btn" id="fi-submit" disabled>File this idea</button>
60
+ <button type="button" class="btn btn--ghost" id="fi-clear">Discard draft</button>
61
+ <span class="fi-draft" id="fi-draft" role="status" aria-live="polite"></span>
62
+ </div>
63
+ </form>
64
+ </section>
65
+ </main>
66
+
67
+ <footer class="app-foot" id="app-foot"></footer>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- The meter rides along beside the form on a wide screen and above it on a
73
+ narrow one (CSS decides). It is a READOUT, never an input: every number in
74
+ it comes from the server's own scorer. -->
75
+ <aside class="fi-meter" id="fi-meter" aria-labelledby="fi-meter-h" hidden>
76
+ <h2 class="fi-meter__h" id="fi-meter-h">Completeness</h2>
77
+ <div class="fi-gauge">
78
+ <div class="fi-gauge__track"><div class="fi-gauge__fill" id="fi-gauge-fill"></div></div>
79
+ <p class="fi-gauge__n" id="fi-gauge-n" role="status" aria-live="polite">—</p>
80
+ </div>
81
+ <ul class="fi-meter__list" id="fi-meter-list"></ul>
82
+ <p class="fi-meter__note">
83
+ Counted, not judged: each question is worth the same, and earns its share in
84
+ proportion to how much real text it carries. Whitespace and padding score nothing.
85
+ </p>
86
+ </aside>
87
+
88
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
89
+
90
+ <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
91
+ <script src="/builders/palette.js" defer></script>
92
+ <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
93
+ <script src="/builders/ideas.js?v=2026-08-22-r152"></script>
94
+ </body>
95
+ </html>