@bongos/core 1.25.14 → 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 +101 -41
- package/.claude/skills/goal-create/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +5 -4
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +20 -0
- package/clients/bongos-client/index.cjs +20 -0
- package/clients/bongos-client/index.d.ts +24 -0
- package/clients/bongos-client/index.mjs +20 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +61 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +544 -2
- package/docs/api-reference.md +12 -2
- package/docs/copy-inventory.md +120 -109
- package/docs/copy-registry.json +275 -165
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- 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 +18 -4
- package/modules/lifecycle/kickoff-seed.js +27 -4
- package/modules/onboarding/genesis-home.js +11 -2
- package/modules/onboarding/genesis-work.js +387 -0
- package/modules/onboarding/migrations/onboarding_002_genesis_work.sql +76 -0
- package/modules/onboarding/routes/genesis-work.js +85 -0
- package/modules/onboarding/routes/onboarding.js +3 -0
- 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 +18 -0
- package/scripts/bongos/genesis.js +180 -0
- package/scripts/bongos/run-unit-tests.js +3 -0
- package/scripts/gds/genesis.js +7 -0
- package/src/module-api.js +1 -1
- package/tests/genesis_home.mjs +70 -2
- package/tests/genesis_work.mjs +304 -0
- package/tests/genesis_work_db.mjs +73 -0
- package/tests/genesis_work_routes.mjs +130 -0
- package/tests/landing_guide_scene.mjs +504 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
-- onboarding_002_genesis_work.sql — the founder's own work on each genesis stage
|
|
2
|
+
-- (task 1005116; read modules/onboarding/genesis-work.js for the why).
|
|
3
|
+
--
|
|
4
|
+
-- Every genesis stage offers three ways of working (guided · open · assisted) onto ONE
|
|
5
|
+
-- record per stage, so switching loses nothing:
|
|
6
|
+
-- onboarding_genesis_work one row per stage: the door the founder is using
|
|
7
|
+
-- now, their free page, and a link to a document
|
|
8
|
+
-- they already have
|
|
9
|
+
-- onboarding_genesis_answers one row per answered question. The state tells
|
|
10
|
+
-- "answered", "not sure yet" and "skipped" apart;
|
|
11
|
+
-- no row means never touched (empty). The origin
|
|
12
|
+
-- says whose words: the founder's, worked out with
|
|
13
|
+
-- their AI session, or carried over from before
|
|
14
|
+
-- sign-up (task 1005274).
|
|
15
|
+
-- onboarding_genesis_answer_history append-only: every write and un-answer, so a
|
|
16
|
+
-- revised answer keeps what it used to say. A NULL
|
|
17
|
+
-- state is an un-answer.
|
|
18
|
+
--
|
|
19
|
+
-- The questions themselves are code (genesis-work.js QUESTIONS); the API refuses an id
|
|
20
|
+
-- a stage does not name, so question_id is only shape-checked here.
|
|
21
|
+
--
|
|
22
|
+
-- ADDITIVE, module-owned (ADR 0083 §Decision #5). Idempotent: safe to re-run.
|
|
23
|
+
|
|
24
|
+
BEGIN;
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS onboarding_genesis_work (
|
|
27
|
+
stage text PRIMARY KEY,
|
|
28
|
+
door text NOT NULL DEFAULT 'guided',
|
|
29
|
+
page_md text,
|
|
30
|
+
link_url text,
|
|
31
|
+
updated_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
32
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
33
|
+
CONSTRAINT onboarding_genesis_work_stage_chk CHECK (stage IN ('scope', 'strategize', 'charter', 'legalize', 'legislation')),
|
|
34
|
+
CONSTRAINT onboarding_genesis_work_door_chk CHECK (door IN ('guided', 'open', 'assisted')),
|
|
35
|
+
CONSTRAINT onboarding_genesis_work_page_chk CHECK (page_md IS NULL OR length(page_md) <= 20000),
|
|
36
|
+
CONSTRAINT onboarding_genesis_work_link_chk CHECK (link_url IS NULL OR (length(link_url) <= 500 AND link_url ~* '^https?://'))
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS onboarding_genesis_answers (
|
|
40
|
+
stage text NOT NULL,
|
|
41
|
+
question_id text NOT NULL,
|
|
42
|
+
state text NOT NULL,
|
|
43
|
+
answer_md text,
|
|
44
|
+
origin text NOT NULL DEFAULT 'founder',
|
|
45
|
+
updated_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
46
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
47
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
48
|
+
PRIMARY KEY (stage, question_id),
|
|
49
|
+
CONSTRAINT onboarding_genesis_answers_stage_chk CHECK (stage IN ('scope', 'strategize', 'charter', 'legalize', 'legislation')),
|
|
50
|
+
CONSTRAINT onboarding_genesis_answers_qid_chk CHECK (question_id ~ '^[a-z][a-z0-9_]{1,39}$'),
|
|
51
|
+
CONSTRAINT onboarding_genesis_answers_state_chk CHECK (state IN ('answered', 'not_sure', 'skipped')),
|
|
52
|
+
CONSTRAINT onboarding_genesis_answers_origin_chk CHECK (origin IN ('founder', 'assisted', 'carried_over')),
|
|
53
|
+
-- "answered" always has words; the other two may carry a short note.
|
|
54
|
+
CONSTRAINT onboarding_genesis_answers_words_chk CHECK (state <> 'answered' OR length(btrim(coalesce(answer_md, ''))) > 0),
|
|
55
|
+
CONSTRAINT onboarding_genesis_answers_len_chk CHECK (answer_md IS NULL OR length(answer_md) <= 4000)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS onboarding_genesis_answer_history (
|
|
59
|
+
id bigserial PRIMARY KEY,
|
|
60
|
+
stage text NOT NULL,
|
|
61
|
+
question_id text NOT NULL,
|
|
62
|
+
state text,
|
|
63
|
+
answer_md text,
|
|
64
|
+
origin text,
|
|
65
|
+
changed_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
66
|
+
changed_at timestamptz NOT NULL DEFAULT now(),
|
|
67
|
+
CONSTRAINT onboarding_genesis_answer_history_state_chk CHECK (state IS NULL OR state IN ('answered', 'not_sure', 'skipped'))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS onboarding_genesis_answer_history_q_idx
|
|
71
|
+
ON onboarding_genesis_answer_history (stage, question_id, changed_at DESC);
|
|
72
|
+
|
|
73
|
+
INSERT INTO schema_migrations (version) VALUES ('onboarding_002_genesis_work')
|
|
74
|
+
ON CONFLICT (version) DO NOTHING;
|
|
75
|
+
|
|
76
|
+
COMMIT;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/routes/genesis-work.js — the HTTP surface of the founder's own work
|
|
4
|
+
// on each genesis stage (task 1005116; the why is in ../genesis-work.js). Mounted by the
|
|
5
|
+
// onboarding route factory (./onboarding.js), on the same /api/bongos router.
|
|
6
|
+
//
|
|
7
|
+
// Who may do what: any signed-in builder READS (the whole team, and every AI session
|
|
8
|
+
// planning a goal or task, reads what the founder said). Every WRITE is the project
|
|
9
|
+
// owner's, behind the same `project.settings.manage` atom as the rest of genesis — the
|
|
10
|
+
// founder is the author. Writes are allowed whether or not founding mode is on, so an
|
|
11
|
+
// answer can be revised long after the project came alive.
|
|
12
|
+
|
|
13
|
+
const api = require('../../../src/module-api');
|
|
14
|
+
const work = require('../genesis-work');
|
|
15
|
+
|
|
16
|
+
const REFUSALS = {
|
|
17
|
+
unknown_stage: 404, unknown_question: 404, bad_door: 400, bad_state: 400, bad_origin: 400,
|
|
18
|
+
empty_answer: 400, answer_too_long: 400, page_too_long: 400, link_too_long: 400, bad_link: 400,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// `deps` is for tests only (an in-memory store); production passes none.
|
|
22
|
+
module.exports = function mountGenesisWorkRoutes(router, deps = {}) {
|
|
23
|
+
const owns = api.requirePermission('project.settings.manage');
|
|
24
|
+
const handle = (fn) => async (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const r = await fn(req);
|
|
27
|
+
if (r && r.ok === false) {
|
|
28
|
+
return res.status(REFUSALS[r.reason] || 409).json({ error: { code: r.reason, message: r.reason } });
|
|
29
|
+
}
|
|
30
|
+
res.set('Cache-Control', 'no-store');
|
|
31
|
+
res.json(r);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
api.logger('onboarding').error('[onboarding] genesis work', err);
|
|
34
|
+
res.status(500).json({ error: { code: 'genesis_work_failed', message: 'internal error' } });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const stage = (req) => String(req.params.stage);
|
|
38
|
+
const q = (req) => String(req.params.question);
|
|
39
|
+
|
|
40
|
+
// The read path for every door and for AI sessions planning a goal or task.
|
|
41
|
+
router.get('/founding/work', api.requireBuilder, handle(async () => ({ stages: await work.readAll(deps) })));
|
|
42
|
+
router.get('/founding/context', api.requireBuilder, handle(() => work.readContext(deps)));
|
|
43
|
+
router.get('/founding/work/:stage', api.requireBuilder, handle((req) => work.readStage(stage(req), deps)));
|
|
44
|
+
router.get('/founding/work/:stage/assist', api.requireBuilder, handle(async (req) => {
|
|
45
|
+
const prompt = work.assistPrompt(stage(req));
|
|
46
|
+
return prompt ? { prompt } : { ok: false, reason: 'unknown_stage' };
|
|
47
|
+
}));
|
|
48
|
+
router.get('/founding/work/:stage/answers/:question/history', api.requireBuilder,
|
|
49
|
+
handle((req) => work.answerHistory(stage(req), q(req), deps)));
|
|
50
|
+
|
|
51
|
+
// The founder's writes.
|
|
52
|
+
router.put('/founding/work/:stage/door', api.requireBuilder, owns,
|
|
53
|
+
(req, res, next) => {
|
|
54
|
+
if (api.validateOrRespond(req, res, { door: { type: 'string', required: true, maxLength: 16 } })) return;
|
|
55
|
+
next();
|
|
56
|
+
},
|
|
57
|
+
handle((req) => work.setDoor(stage(req), req.body.door, req.builder.id, deps)));
|
|
58
|
+
router.put('/founding/work/:stage/page', api.requireBuilder, owns,
|
|
59
|
+
(req, res, next) => {
|
|
60
|
+
if (api.validateOrRespond(req, res, { page: { type: 'string', maxLength: work.LIMITS.page } })) return;
|
|
61
|
+
next();
|
|
62
|
+
},
|
|
63
|
+
handle((req) => work.setPage(stage(req), req.body.page, req.builder.id, deps)));
|
|
64
|
+
router.put('/founding/work/:stage/link', api.requireBuilder, owns,
|
|
65
|
+
(req, res, next) => {
|
|
66
|
+
if (api.validateOrRespond(req, res, { url: { type: 'string', maxLength: work.LIMITS.link } })) return;
|
|
67
|
+
next();
|
|
68
|
+
},
|
|
69
|
+
handle((req) => work.setLink(stage(req), req.body.url, req.builder.id, deps)));
|
|
70
|
+
router.put('/founding/work/:stage/answers/:question', api.requireBuilder, owns,
|
|
71
|
+
(req, res, next) => {
|
|
72
|
+
if (api.validateOrRespond(req, res, {
|
|
73
|
+
state: { type: 'string', maxLength: 16 },
|
|
74
|
+
text: { type: 'string', maxLength: work.LIMITS.answer },
|
|
75
|
+
origin: { type: 'string', maxLength: 16 },
|
|
76
|
+
if_empty: { type: 'boolean' },
|
|
77
|
+
})) return;
|
|
78
|
+
next();
|
|
79
|
+
},
|
|
80
|
+
handle((req) => work.setAnswer(stage(req), q(req), req.body || {}, req.builder.id, deps,
|
|
81
|
+
{ ifEmpty: req.body && req.body.if_empty === true })));
|
|
82
|
+
// Un-answer: back to empty, which is not the same as skipped.
|
|
83
|
+
router.delete('/founding/work/:stage/answers/:question', api.requireBuilder, owns,
|
|
84
|
+
handle((req) => work.clearAnswer(stage(req), q(req), req.builder.id, deps)));
|
|
85
|
+
};
|
|
@@ -38,6 +38,7 @@ const foundingBuilders = require('../founding-builders');
|
|
|
38
38
|
const genesis = require('../genesis-home');
|
|
39
39
|
const foundingBirth = require('../founding-birth');
|
|
40
40
|
const howtoState = require('../howto-state');
|
|
41
|
+
const mountGenesisWorkRoutes = require('./genesis-work');
|
|
41
42
|
|
|
42
43
|
// Register the `onboarding` kernel PORT. See the header + port.js for the full
|
|
43
44
|
// consumer list and the graceful-degradation contract.
|
|
@@ -65,6 +66,8 @@ module.exports = function buildOnboardingRouter() {
|
|
|
65
66
|
res.json({ builders });
|
|
66
67
|
});
|
|
67
68
|
mountGenesisRoutes(router);
|
|
69
|
+
// The founder's own work on each stage: three doors onto one record (task 1005116).
|
|
70
|
+
mountGenesisWorkRoutes(router);
|
|
68
71
|
mountHowtoRoutes(router);
|
|
69
72
|
return router;
|
|
70
73
|
};
|
|
@@ -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
|
+
}
|