create-cmp-cli 0.23.0 → 0.25.0
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/bin/create-cmp.mjs +33 -3
- package/package.json +10 -4
- package/packages/harness/package.json +18 -8
- package/packages/harness/src/approve.mjs +46 -18
- package/packages/harness/src/console/console-data.mjs +117 -0
- package/packages/harness/src/console/console-evidence.mjs +122 -0
- package/packages/harness/src/console/console-overview.mjs +642 -0
- package/packages/harness/src/console/console-shell.mjs +1139 -0
- package/packages/harness/src/console/console-tabs.mjs +2828 -0
- package/packages/harness/src/console/contrast.mjs +74 -0
- package/packages/harness/src/console/preview-service.mjs +1384 -0
- package/packages/harness/src/framework-check.mjs +747 -0
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- package/packages/harness/src/lib/agent-hold.mjs +234 -0
- package/packages/harness/src/lib/approvals.mjs +177 -350
- package/packages/harness/src/lib/audit-cadence.mjs +1 -1
- package/packages/harness/src/lib/comments.mjs +18 -4
- package/packages/harness/src/lib/determinism.mjs +275 -14
- package/packages/harness/src/lib/evidence-badge.mjs +38 -6
- package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
- package/packages/harness/src/lib/evidence-level.mjs +144 -59
- package/packages/harness/src/lib/feature-brief.mjs +53 -16
- package/packages/harness/src/lib/flight-recorder.mjs +72 -8
- package/packages/harness/src/lib/framework-check.mjs +778 -0
- package/packages/harness/src/lib/harness-lock.mjs +87 -19
- package/packages/harness/src/lib/harness-manifest.mjs +208 -0
- package/packages/harness/src/lib/harness-region.mjs +106 -2
- package/packages/harness/src/lib/harness-source.mjs +142 -0
- package/packages/harness/src/lib/inputs-hash.mjs +180 -22
- package/packages/harness/src/lib/lane-markers.mjs +78 -0
- package/packages/harness/src/lib/lane-narrator.mjs +4 -2
- package/packages/harness/src/lib/lane-runner.mjs +72 -6
- package/packages/harness/src/lib/plan.mjs +44 -11
- package/packages/harness/src/lib/plant-calibration.mjs +135 -0
- package/packages/harness/src/lib/profile-loader.mjs +323 -0
- package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
- package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
- package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
- package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
- package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
- package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
- package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
- package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
- package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/packages/harness/src/lib/receipt-validate.mjs +84 -16
- package/packages/harness/src/lib/spec-coverage.mjs +179 -92
- package/packages/harness/src/lib/spec-model.mjs +262 -0
- package/packages/harness/src/lib/step-cache.mjs +11 -2
- package/packages/harness/src/lib/step-outcomes.mjs +84 -151
- package/packages/harness/src/lib/walk.mjs +1 -1
- package/packages/harness/src/plan.mjs +30 -1
- package/packages/harness/src/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +95 -26
- package/packages/harness/src/record-audit.mjs +1 -1
- package/packages/harness/src/scaffold-feature.mjs +10 -2
- package/packages/harness/src/verify.mjs +222 -142
- package/packages/harness/src/watch.mjs +146 -33
- package/packages/receipts/package.json +1 -1
- package/packages/receipts/src/index.mjs +2 -2
- package/packages/receipts/src/inputs-hash.mjs +180 -22
- package/packages/receipts/src/receipt-validate.mjs +84 -16
- package/src/commands/attach.mjs +128 -4
- package/src/commands/harden.mjs +7 -1
- package/src/commands/upgrade.mjs +7 -0
- package/src/lib/minimal.mjs +23 -1
- package/src/scaffold.mjs +8 -0
- package/template/.claude/settings.json +2 -2
- package/template/.claude/skills/add-feature/SKILL.md +7 -3
- package/template/.claude/skills/add-repository/SKILL.md +2 -2
- package/template/.claude/skills/add-screen/SKILL.md +8 -4
- package/template/.githooks/pre-push +9 -0
- package/template/CLAUDE.md +55 -186
- package/template/docs/TESTING.md +1 -1
- package/template/gitignore +4 -0
- package/template/qa/approve.mjs +46 -18
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +747 -0
- package/template/qa/harness-manifest.json +10 -0
- package/template/qa/lib/a11y.mjs +1 -1
- package/template/qa/lib/affected-tests.mjs +99 -44
- package/template/qa/lib/agent-hold.mjs +234 -0
- package/template/qa/lib/approvals.mjs +177 -350
- package/template/qa/lib/audit-cadence.mjs +1 -1
- package/template/qa/lib/comments.mjs +18 -4
- package/template/qa/lib/determinism.mjs +275 -14
- package/template/qa/lib/evidence-badge.mjs +38 -6
- package/template/qa/lib/evidence-ladder.mjs +202 -0
- package/template/qa/lib/evidence-level.mjs +144 -59
- package/template/qa/lib/feature-brief.mjs +53 -16
- package/template/qa/lib/flight-recorder.mjs +72 -8
- package/template/qa/lib/framework-check.mjs +778 -0
- package/template/qa/lib/harness-lock.mjs +87 -19
- package/template/qa/lib/harness-manifest.mjs +208 -0
- package/template/qa/lib/harness-region.mjs +106 -2
- package/template/qa/lib/harness-source.mjs +142 -0
- package/template/qa/lib/inputs-hash.mjs +180 -22
- package/template/qa/lib/lane-markers.mjs +78 -0
- package/template/qa/lib/lane-narrator.mjs +4 -2
- package/template/qa/lib/lane-runner.mjs +72 -6
- package/template/qa/lib/plan.mjs +44 -11
- package/template/qa/lib/plant-calibration.mjs +135 -0
- package/template/qa/lib/profile-loader.mjs +323 -0
- package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
- package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
- package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
- package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
- package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/template/qa/lib/profiles/cmp/index.mjs +47 -0
- package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
- package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
- package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
- package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/template/qa/lib/receipt-validate.mjs +84 -16
- package/template/qa/lib/spec-coverage.mjs +179 -92
- package/template/qa/lib/spec-model.mjs +262 -0
- package/template/qa/lib/step-cache.mjs +11 -2
- package/template/qa/lib/step-outcomes.mjs +84 -151
- package/template/qa/lib/walk.mjs +1 -1
- package/template/qa/plan.mjs +30 -1
- package/template/qa/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +95 -26
- package/template/qa/record-audit.mjs +1 -1
- package/template/qa/scaffold-feature.mjs +10 -2
- package/template/qa/verified-surface.json +17 -0
- package/template/qa/verify.mjs +222 -142
- package/template/qa/watch.mjs +146 -33
- /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
- /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
// MOVED from inspector/mcp/src/lib/console-shell.mjs — NORTH-STAR §9, stage 0.5,
|
|
2
|
+
// "the console into the harness". Distribution has to ship the console and could
|
|
3
|
+
// not while it lived in a sibling package; `prooflane-harness`'s `files` list
|
|
4
|
+
// ships `src/`, so it ships from here. Nothing in this file changed in the move
|
|
5
|
+
// — it had no imports at all, which is why it crossed first.
|
|
6
|
+
//
|
|
7
|
+
// Note the directory: `src/console/`, not `src/lib/`. scripts/sync-harness.mjs
|
|
8
|
+
// mirrors `src/lib` into `template/qa/lib`, so a console under lib/ would be
|
|
9
|
+
// vendored into every stamped app. REGION_DIRS is one level deep and REGION_TREES
|
|
10
|
+
// covers only `src/lib/profiles`, so `src/console/` is shipped and not vendored.
|
|
11
|
+
//
|
|
12
|
+
// console-shell.mjs — the ONE frame every console section renders inside
|
|
13
|
+
// (docs/STUDIO-REDESIGN.md §2). The shell is the whole design system: the
|
|
14
|
+
// sidebar ordering/coverage rail, the per-page header grammar, the type ramp,
|
|
15
|
+
// the spacing scale, the color roles, and the provenance footer. Sections
|
|
16
|
+
// contribute ONLY a document body — they may not invent their own chrome.
|
|
17
|
+
//
|
|
18
|
+
// Design rules encoded here (§2, non-negotiable for every section):
|
|
19
|
+
// - Ink/paper neutrals with ONE accent. Semantic red/amber/green are RESERVED
|
|
20
|
+
// for drift/reopened/signed (and gate FAIL/PASS, the same three meanings) —
|
|
21
|
+
// never decoration.
|
|
22
|
+
// - A 4-step type ramp and an 8px spacing scale. Readable measure for prose;
|
|
23
|
+
// full-bleed only for galleries.
|
|
24
|
+
// - The rail lists sections in the genesis definition order, each with its
|
|
25
|
+
// live state glyph (● signed · ○ unsigned · ◐ reopened · ⚠ drifted).
|
|
26
|
+
// - The dashboard function is ambient — with ONE exception, added 2026-08-22
|
|
27
|
+
// after the rule failed twice in the same way: the Overview section (§3.7,
|
|
28
|
+
// console-overview.mjs) is the front door, and it is the only section
|
|
29
|
+
// permitted to aggregate. It owns no facts and grows no signature control;
|
|
30
|
+
// everything else stays ambient, in the section that owns it.
|
|
31
|
+
// - Every page: header block (title · status line) → document body →
|
|
32
|
+
// provenance footer ("derived from the live tree … absence = not derivable").
|
|
33
|
+
// - Light + dark via prefers-color-scheme. Pure server-rendered HTML/CSS,
|
|
34
|
+
// zero external dependencies.
|
|
35
|
+
//
|
|
36
|
+
// The one import, added 2026-09-08: the rail foot shows an evidence rung, and
|
|
37
|
+
// NORTH-STAR.md §6.5 requires every surface that shows one to show the pack
|
|
38
|
+
// beside it. That spelling belongs to console-evidence.mjs so that the console
|
|
39
|
+
// has one of it rather than seven (see that file's header). The import is a
|
|
40
|
+
// sibling inside `src/console/`, so the reason this file crossed the package
|
|
41
|
+
// boundary cleanly still holds: nothing here points out of the package.
|
|
42
|
+
|
|
43
|
+
import { rungWithPack, rungPackNote } from "./console-evidence.mjs";
|
|
44
|
+
|
|
45
|
+
const esc = (s) =>
|
|
46
|
+
String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
47
|
+
|
|
48
|
+
// --- state glyphs -------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A governed artifact's live status -> its rail/header glyph. The four §2
|
|
52
|
+
* states, plus the express-lane nuance: approved-with-defaults is still
|
|
53
|
+
* SIGNED (●) — the "unshaped" caveat belongs in the status line's words, not
|
|
54
|
+
* in a fifth glyph nobody can read. `null`/unknown statuses get NO glyph
|
|
55
|
+
* (evidence-or-silence: an older project lib that never reports a status is
|
|
56
|
+
* not "unsigned", it's unknown).
|
|
57
|
+
* @param {object|null|undefined} record an approvals status record ({status, mode, ...})
|
|
58
|
+
* @returns {{ch: string, cls: string, label: string}|null}
|
|
59
|
+
*/
|
|
60
|
+
export function statusGlyph(record) {
|
|
61
|
+
if (!record || !record.status) return null;
|
|
62
|
+
switch (record.status) {
|
|
63
|
+
case "approved":
|
|
64
|
+
return { ch: "●", cls: "glyph-signed", label: record.mode === "defaults-accepted" ? "signed (defaults accepted — unshaped)" : "signed" };
|
|
65
|
+
case "changed-since-approval":
|
|
66
|
+
return { ch: "⚠", cls: "glyph-drift", label: "drifted — changed since approval" };
|
|
67
|
+
case "reopened":
|
|
68
|
+
return { ch: "◐", cls: "glyph-reopen", label: "reopened for redesign" };
|
|
69
|
+
case "unreviewed":
|
|
70
|
+
return { ch: "○", cls: "glyph-unsigned", label: "unsigned" };
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const shortHash = (h) => (h ? String(h).slice(0, 8) : null);
|
|
77
|
+
|
|
78
|
+
// --- the human's queue (ONE derivation — the strip, the guided prompt, and ---
|
|
79
|
+
// --- approval_status all read this; two derivations was the 07-28 audit's ----
|
|
80
|
+
// --- finding 3: the card said "waiting on you" while the queue said nothing --
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Everything currently waiting on the HUMAN, as actionable items, derived
|
|
84
|
+
* from the live statuses + feature board. Each item names the action in plain
|
|
85
|
+
* words and the tab where its signature control lives (sign-where-you-read).
|
|
86
|
+
*
|
|
87
|
+
* `reopened` is included ONLY when it has become the human's turn: a reopened
|
|
88
|
+
* feature brief whose feature derives provenDone (the redesign is finished
|
|
89
|
+
* and proven — same derivation acceptance trusts). A reopened artifact still
|
|
90
|
+
* mid-redesign stays out: prompting a re-approval there would invite signing
|
|
91
|
+
* an unfinished redesign.
|
|
92
|
+
* @param {{statuses?: object[], features?: object[]}} data
|
|
93
|
+
* @param {string} [excludeArtifact] the artifact just acted on (its own prompt
|
|
94
|
+
* should not re-list it)
|
|
95
|
+
* @returns {Array<{artifact: string, tab: string, label: string}>}
|
|
96
|
+
*/
|
|
97
|
+
export function deriveHumanQueue({ statuses = [], features = [] }, excludeArtifact) {
|
|
98
|
+
const tabOf = (id) =>
|
|
99
|
+
id === "intent" || id === "architecture" || id === "design-system" || id === "components"
|
|
100
|
+
? id
|
|
101
|
+
: id.startsWith("feature-brief:") || id.startsWith("feature-design:")
|
|
102
|
+
? "features"
|
|
103
|
+
: id === "exemplar-spec" || id.startsWith("feature-spec:")
|
|
104
|
+
? "specs"
|
|
105
|
+
: "approvals";
|
|
106
|
+
const readyBriefs = new Set(
|
|
107
|
+
features.filter((f) => f.phase === "reopened" && f.provenDone).map((f) => `feature-brief:${f.name}`),
|
|
108
|
+
);
|
|
109
|
+
const items = [];
|
|
110
|
+
for (const s of statuses) {
|
|
111
|
+
if (s.id === excludeArtifact) continue;
|
|
112
|
+
if (s.status === "unreviewed" && s.resolvable !== false) {
|
|
113
|
+
items.push({ artifact: s.id, tab: tabOf(s.id), label: `Approve ${s.id}` });
|
|
114
|
+
} else if (s.status === "changed-since-approval") {
|
|
115
|
+
items.push({ artifact: s.id, tab: tabOf(s.id), label: `Re-approve ${s.id} — it changed since signing` });
|
|
116
|
+
} else if (s.status === "reopened" && readyBriefs.has(s.id)) {
|
|
117
|
+
items.push({ artifact: s.id, tab: tabOf(s.id), label: `Re-approve ${s.id} — the redesign is proven` });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const f of features) {
|
|
121
|
+
if (f.phase === "proven" && `feature-brief:${f.name}` !== excludeArtifact) {
|
|
122
|
+
items.push({ artifact: `feature-brief:${f.name}`, tab: "features", label: `Accept ${f.name} — proven done` });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return items;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- the governance strip (rail-resident — visible on EVERY tab) --------------
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The always-visible aggregate (07-28 audit, fix 5 — the andon-board answer to
|
|
132
|
+
* "I should be able to see the status at all times"): one counts line, plus the
|
|
133
|
+
* door to the page that explains it. Renders in the rail, so no tab choice can
|
|
134
|
+
* hide it. Returns "" when there is nothing derivable (no statuses at all — an
|
|
135
|
+
* ungoverned or older project): silence, not a fabricated dashboard.
|
|
136
|
+
*
|
|
137
|
+
* It deliberately stops at the COUNTS. Naming the next act here as well — which
|
|
138
|
+
* it did until 2026-08-22 — made the rail a lower-fidelity copy of the front
|
|
139
|
+
* door, shown alongside the front door. The rail says THAT something waits; the
|
|
140
|
+
* front door says WHAT, with the evidence, and owns the acting.
|
|
141
|
+
* @param {{statuses?: object[], features?: object[]}} data
|
|
142
|
+
*/
|
|
143
|
+
export function governanceStripHtml({ statuses = [], features = [] }) {
|
|
144
|
+
if (statuses.length === 0) return "";
|
|
145
|
+
const queue = deriveHumanQueue({ statuses, features });
|
|
146
|
+
const signed = statuses.filter((s) => s.status === "approved").length;
|
|
147
|
+
const queued = new Set(queue.map((q) => q.artifact));
|
|
148
|
+
const redesigning = statuses.filter((s) => s.status === "reopened" && !queued.has(s.id));
|
|
149
|
+
const drifted = statuses.filter((s) => s.status === "changed-since-approval");
|
|
150
|
+
|
|
151
|
+
// A bare "1 in redesign" is a number with no referent — it names no artifact,
|
|
152
|
+
// no owner, and no way to reach it, which is exactly how a reader ends up
|
|
153
|
+
// asking "what does that mean and where do I see it?" (2026-07-28). So each
|
|
154
|
+
// non-zero count names its artifact when there is only one, and every count is
|
|
155
|
+
// a jump button to the Approvals row that explains itself.
|
|
156
|
+
const countBtn = (cls, text, artifact) =>
|
|
157
|
+
`<button type="button" class="gov-n ${cls} gov-jump" data-go-tab="approvals"${
|
|
158
|
+
artifact ? ` data-go-artifact="${esc(artifact)}"` : ""
|
|
159
|
+
} title="take me there">${esc(text)}</button>`;
|
|
160
|
+
const namesOf = (rows, noun) =>
|
|
161
|
+
rows.length === 1 ? `${noun}: ${rows[0].label || rows[0].id}` : `${rows.length} ${noun}`;
|
|
162
|
+
|
|
163
|
+
const counts = [
|
|
164
|
+
`<span class="gov-n gov-signed">${signed} signed</span>`,
|
|
165
|
+
queue.length > 0 ? `<span class="gov-n gov-awaiting">${queue.length} await${queue.length === 1 ? "s" : ""} you</span>` : null,
|
|
166
|
+
redesigning.length > 0
|
|
167
|
+
? countBtn("gov-redesign", namesOf(redesigning, "in redesign"), redesigning.length === 1 ? redesigning[0].id : null)
|
|
168
|
+
: null,
|
|
169
|
+
drifted.length > 0 ? countBtn("gov-drift", namesOf(drifted, "drifted"), drifted.length === 1 ? drifted[0].id : null) : null,
|
|
170
|
+
]
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join(" · ");
|
|
173
|
+
|
|
174
|
+
// The strip is a POINTER now, not a second copy of the front door (2026-08-22).
|
|
175
|
+
// It used to carry the next act in full and the journal's History — which, once
|
|
176
|
+
// the Overview page existed, meant the rail said the same things the page said,
|
|
177
|
+
// on the page itself, at half the fidelity. That duplication is precisely the
|
|
178
|
+
// "so much happening all over the place" complaint. So the division is: the
|
|
179
|
+
// rail tells you THAT something waits (on every tab, the 07-28 requirement);
|
|
180
|
+
// the front door tells you WHAT, with the evidence, and is where you go to act.
|
|
181
|
+
// History moved with it — see console-overview.mjs — so no fact was dropped.
|
|
182
|
+
const next =
|
|
183
|
+
queue.length > 0
|
|
184
|
+
? // data-go-* (NOT data-tab/data-artifact): a strip jump is not a rail tab
|
|
185
|
+
// button, and overloading the rail's attributes would collide with every
|
|
186
|
+
// selector that treats data-tab as "a tab exists here".
|
|
187
|
+
`<button type="button" class="gov-next" data-go-tab="overview" title="open the front door">${queue.length} thing${queue.length === 1 ? "" : "s"} need${queue.length === 1 ? "s" : ""} you <span class="gov-more">→ Drive</span></button>`
|
|
188
|
+
: `<p class="gov-clear">Nothing waits on you.</p>`;
|
|
189
|
+
|
|
190
|
+
return `<div id="gov-strip">
|
|
191
|
+
<p class="gov-counts">${counts}</p>
|
|
192
|
+
${next}
|
|
193
|
+
</div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The header status line for a section governed by ONE artifact — the §2
|
|
198
|
+
* grammar: "● signed a1b2c3 · approved <when>" / "○ unsigned" / "⚠ drifted
|
|
199
|
+
* (a1b2c3 → d4e5f6)" / "◐ reopened for redesign". Returns "" when there is
|
|
200
|
+
* no record (approvals unavailable / older scaffold): silence, not a
|
|
201
|
+
* fabricated state.
|
|
202
|
+
* @param {object|null|undefined} record an approvals status record
|
|
203
|
+
*/
|
|
204
|
+
export function artifactStatusHtml(record) {
|
|
205
|
+
const g = statusGlyph(record);
|
|
206
|
+
if (!g) return "";
|
|
207
|
+
const glyph = `<span class="glyph ${g.cls}">${g.ch}</span>`;
|
|
208
|
+
if (record.status === "approved") {
|
|
209
|
+
const unshaped = record.mode === "defaults-accepted" ? " · defaults accepted — unshaped" : "";
|
|
210
|
+
const at = record.approvedAt ? ` · approved ${esc(record.approvedAt)}` : "";
|
|
211
|
+
// The SIGNED hash is `storedHash` — what the human's signature was bound to.
|
|
212
|
+
// `hash` is the live recompute; the two agree for everything signed on the
|
|
213
|
+
// current basis, but on a `hashBasis: "raw-bytes"` row the live value is a
|
|
214
|
+
// number nobody ever signed, and printing it here would be a fabricated
|
|
215
|
+
// provenance claim in the one surface whose whole job is provenance.
|
|
216
|
+
// (`storedHash` is always set on an approved row; the fallback is belt.)
|
|
217
|
+
const signedHash = shortHash(record.storedHash) || shortHash(record.hash) || "?";
|
|
218
|
+
const basis =
|
|
219
|
+
record.hashBasis === "raw-bytes" ? " · signed pre-strip — bytes unchanged since" : "";
|
|
220
|
+
return `${glyph} signed <code>${esc(signedHash)}</code>${at}${unshaped}${basis}`;
|
|
221
|
+
}
|
|
222
|
+
if (record.status === "changed-since-approval") {
|
|
223
|
+
const from = shortHash(record.storedHash);
|
|
224
|
+
const to = shortHash(record.hash);
|
|
225
|
+
const move = from && to ? ` (<code>${esc(from)}</code> → <code>${esc(to)}</code>)` : "";
|
|
226
|
+
return `${glyph} <span class="status-drift">drifted — changed since approval${move}</span>`;
|
|
227
|
+
}
|
|
228
|
+
if (record.status === "reopened") {
|
|
229
|
+
return `${glyph} <span class="status-reopen">reopened for redesign</span>`;
|
|
230
|
+
}
|
|
231
|
+
return `${glyph} unsigned — not yet approved`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- the rail -----------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/** Coarse "2h ago" age, from a bridge-computed ageMs (pure — no clock read here). */
|
|
237
|
+
export function formatAgeCoarse(ageMs) {
|
|
238
|
+
if (typeof ageMs !== "number" || Number.isNaN(ageMs)) return "age unknown";
|
|
239
|
+
if (ageMs < 60_000) return "just now";
|
|
240
|
+
const mins = Math.floor(ageMs / 60_000);
|
|
241
|
+
if (mins < 60) return `${mins}m ago`;
|
|
242
|
+
const hours = Math.floor(mins / 60);
|
|
243
|
+
if (hours < 24) return `${hours}h ago`;
|
|
244
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A verify receipt's state -> its glyph — ONE derivation for everywhere the
|
|
249
|
+
* receipt shows as a symbol (the rail's Evidence item, the rail foot):
|
|
250
|
+
* ✓ fresh PASS · ✗ FAIL · ⚠ stale (never presented as a live PASS) · ○ none.
|
|
251
|
+
* @param {object|null|undefined} receipt receipt-bridge.mjs getLastReceipt() result
|
|
252
|
+
* @returns {{ch: string, cls: string, label: string}}
|
|
253
|
+
*/
|
|
254
|
+
export function receiptGlyph(receipt) {
|
|
255
|
+
if (!receipt || !receipt.available) {
|
|
256
|
+
return { ch: "○", cls: "glyph-unsigned", label: "no verify receipt yet" };
|
|
257
|
+
}
|
|
258
|
+
if (receipt.stale) {
|
|
259
|
+
return { ch: "⚠", cls: "glyph-drift", label: `verify ${receipt.verdict || "?"} — stale, the tree changed since` };
|
|
260
|
+
}
|
|
261
|
+
const verdict = receipt.verdict || "?";
|
|
262
|
+
if (verdict === "PASS") return { ch: "✓", cls: "glyph-signed", label: "verify PASS" };
|
|
263
|
+
if (verdict === "FAIL") return { ch: "✗", cls: "glyph-drift", label: "verify FAIL" };
|
|
264
|
+
return { ch: "○", cls: "glyph-unsigned", label: `verify ${verdict}` };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The verify-receipt line at the rail's foot (§2: "Bottom of the rail: last
|
|
269
|
+
* verify receipt verdict + age"). Honest about every degraded state: no
|
|
270
|
+
* receipt, unparseable receipt, stale (inputsHash no longer matches the
|
|
271
|
+
* tree), or freshness-unknown — a stale green is NEVER shown as a live PASS.
|
|
272
|
+
* The glyph itself comes from receiptGlyph, the one shared derivation.
|
|
273
|
+
* @param {object|null|undefined} receipt receipt-bridge.mjs getLastReceipt() result
|
|
274
|
+
*/
|
|
275
|
+
export function railReceiptHtml(receipt, formatAge = formatAgeCoarse) {
|
|
276
|
+
const g = receiptGlyph(receipt);
|
|
277
|
+
const glyph = `<span class="glyph ${g.cls}">${g.ch}</span>`;
|
|
278
|
+
if (!receipt || !receipt.available) {
|
|
279
|
+
return `${glyph} no verify receipt yet`;
|
|
280
|
+
}
|
|
281
|
+
const verdict = receipt.verdict || "?";
|
|
282
|
+
const age = typeof receipt.ageMs === "number" ? formatAge(receipt.ageMs) : "age unknown";
|
|
283
|
+
if (receipt.stale) {
|
|
284
|
+
return `${glyph} verify ${esc(verdict)} ${esc(age)} — stale (tree changed since)`;
|
|
285
|
+
}
|
|
286
|
+
const unknown = receipt.stale === null ? " · freshness unverified" : "";
|
|
287
|
+
// The evidence-ladder rung, verbatim from the receipt's own derived
|
|
288
|
+
// evidenceLevel — absent (FAIL / pre-ladder receipt) means no rung shown —
|
|
289
|
+
// AND THE PACK THAT GRADED IT, which is not decoration on the one line the
|
|
290
|
+
// console shows on every page. This foot said "verify PASS · L2 device" until
|
|
291
|
+
// 2026-09-08, and a reader who met that had no way to tell a phone-driven L2
|
|
292
|
+
// from a database-backed one (§8.9). The pack rides the same string so the
|
|
293
|
+
// two can never be separated by a later edit to this template literal.
|
|
294
|
+
const label = rungWithPack(receipt.evidenceLevel, receipt.packId ?? receipt.pack);
|
|
295
|
+
const note = rungPackNote(receipt.evidenceLevel, receipt.packId ?? receipt.pack);
|
|
296
|
+
const rung = label ? ` · <span title="${esc(note)}">${esc(label)}</span>` : "";
|
|
297
|
+
return `${glyph} verify ${esc(verdict)}${rung} ${esc(age)}${unknown}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* One rail nav item. Keeps the `.tab-btn`/`data-tab` contract the behavior
|
|
302
|
+
* script and the browser gates already speak — the rail is a restyled
|
|
303
|
+
* navigation, not a new mechanism.
|
|
304
|
+
* @param {{id: string, label: string, glyph: object|null, active?: boolean, badgeHtml?: string}} item
|
|
305
|
+
*/
|
|
306
|
+
function railItemHtml(item) {
|
|
307
|
+
const g = item.glyph;
|
|
308
|
+
const glyph = g
|
|
309
|
+
? `<span class="glyph ${g.cls}" title="${esc(g.label)}">${g.ch}</span>`
|
|
310
|
+
: `<span class="glyph glyph-none">·</span>`;
|
|
311
|
+
return `<button class="tab-btn${item.active ? " active" : ""}" data-tab="${esc(item.id)}">${glyph}<span class="rail-label">${esc(item.label)}</span>${item.badgeHtml || ""}</button>`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- the page frame -----------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One section, wrapped in the §2 page grammar: header block (title + status
|
|
318
|
+
* line) → the document body → the provenance footer. The `#tab-<id>` /
|
|
319
|
+
* `.tab-panel` contract is unchanged (SSE in-place swaps target these ids).
|
|
320
|
+
* @param {{id: string, title: string, statusHtml?: string, bodyHtml: string, active?: boolean, fullBleed?: boolean, headExtraHtml?: string}} s
|
|
321
|
+
* @param {string} provenanceHtml the shared footer line (same for every page)
|
|
322
|
+
*/
|
|
323
|
+
function sectionHtml(s, provenanceHtml) {
|
|
324
|
+
const status = s.statusHtml ? `<p class="page-status">${s.statusHtml}</p>` : "";
|
|
325
|
+
// Page anatomy (studio-drive-mode): a MIRROR section's default weight is its
|
|
326
|
+
// verdict (the page-status line above), with the complete derived document
|
|
327
|
+
// one disclosure away. The mirror stays byte-complete and stays the drift
|
|
328
|
+
// surface — only its default expansion changes. Tool sections (Drive,
|
|
329
|
+
// Screens, Live device) and any section the caller marks open render as
|
|
330
|
+
// before; the gov-jump handler opens the disclosure when a jump lands
|
|
331
|
+
// inside it, so "take me there" still lands ON the row.
|
|
332
|
+
const body = s.mirror
|
|
333
|
+
? `<details class="mirror-details"${s.mirrorOpen ? " open" : ""}><summary>Read the full ${esc(s.title)} — every fact, derived from the live tree</summary>
|
|
334
|
+
${s.bodyHtml}
|
|
335
|
+
</details>`
|
|
336
|
+
: s.bodyHtml;
|
|
337
|
+
return `<section id="tab-${esc(s.id)}" class="tab-panel${s.active ? " active" : ""}${s.fullBleed ? " full-bleed" : ""}" data-tab="${esc(s.id)}">
|
|
338
|
+
<header class="page-head">
|
|
339
|
+
<h2>${esc(s.title)}</h2>
|
|
340
|
+
${status}${s.headExtraHtml || ""}
|
|
341
|
+
</header>
|
|
342
|
+
<div class="page-body">
|
|
343
|
+
${body}
|
|
344
|
+
</div>
|
|
345
|
+
<footer class="page-foot">${provenanceHtml}</footer>
|
|
346
|
+
</section>`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The provenance footer (§2): what this page was derived FROM, and the
|
|
351
|
+
* standing epistemic rule. `treeHash` is the project's git HEAD when the
|
|
352
|
+
* caller could resolve one — omitted (not faked) otherwise.
|
|
353
|
+
* @param {{treeHash?: string|null, version?: number}} p
|
|
354
|
+
*/
|
|
355
|
+
export function provenanceHtml(p = {}) {
|
|
356
|
+
const tree = p.treeHash ? `derived from tree <code>@${esc(p.treeHash)}</code>` : "derived from the live tree";
|
|
357
|
+
const render = typeof p.version === "number" && p.version > 0 ? ` · render #${p.version}` : "";
|
|
358
|
+
// Which build of the CONSOLE drew this page. Provenance already answers
|
|
359
|
+
// "which tree is this derived from"; without this it could not answer "and
|
|
360
|
+
// which code did the deriving" — the question that cost two hours to answer
|
|
361
|
+
// by hand (2026-07-27/28).
|
|
362
|
+
const build = p.build && p.build.id ? ` · console <code>${esc(String(p.build.id).slice(0, 8))}</code>` : "";
|
|
363
|
+
return `${tree}${render}${build} · absence = not derivable`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The stale-console banner: this page was drawn by code that is no longer the
|
|
368
|
+
* code on disk. Sits with the other page-top banners because it invalidates
|
|
369
|
+
* everything below it — a reader who trusts a stale page is the exact failure
|
|
370
|
+
* (twice in two days) that this whole handshake exists to prevent.
|
|
371
|
+
*
|
|
372
|
+
* Silent when fresh, and silent when freshness is UNKNOWN (`stale: null`) —
|
|
373
|
+
* an unknown must not be dressed up as either a warning or a clean bill of
|
|
374
|
+
* health; the provenance footer still shows whatever id there is.
|
|
375
|
+
* @param {{id: string|null, mode: string, stale: boolean|null, diskId: string|null}|null|undefined} build
|
|
376
|
+
*/
|
|
377
|
+
export function staleConsoleBannerHtml(build) {
|
|
378
|
+
if (!build || build.stale !== true) return "";
|
|
379
|
+
const from = build.id ? `<code>${esc(String(build.id).slice(0, 8))}</code>` : "an unknown build";
|
|
380
|
+
const to = build.diskId ? `<code>${esc(String(build.diskId).slice(0, 8))}</code>` : "a newer build";
|
|
381
|
+
const how =
|
|
382
|
+
build.mode === "bundle"
|
|
383
|
+
? "the bundle was rebuilt after this console started"
|
|
384
|
+
: "the console's sources changed after it started";
|
|
385
|
+
return `<div class="banner banner-stale-build">This console is running ${from}; the code on disk is now ${to} — ${how}. Everything below was drawn by the older code. Restart the console to pick it up: <code>node inspector/mcp/bin/console.mjs <projectDir></code></div>`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** "HH:MM" in server-local time, from an ISO timestamp — null-safe. */
|
|
389
|
+
function clockTime(iso) {
|
|
390
|
+
if (!iso) return null;
|
|
391
|
+
try {
|
|
392
|
+
return new Date(iso).toTimeString().slice(0, 5);
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The renderer's OWN failure banner (FI-9 Change B) — distinct from `p.error`
|
|
400
|
+
* (which also covers compile/reload messages that already have their own
|
|
401
|
+
* presentation, right above each screen). This fires only when the render
|
|
402
|
+
* PIPELINE itself is dead (the last Gradle/daemon render call threw outright):
|
|
403
|
+
* "the eyes are stale", not "your edit didn't build". Amber (--reopen), the
|
|
404
|
+
* same "stale, not necessarily broken" vocabulary the rail-foot receipt line
|
|
405
|
+
* already uses for a stale-but-not-failed verify receipt — kept visually
|
|
406
|
+
* distinct from the red `.banner` a compile/reload failure shows.
|
|
407
|
+
* @param {{lastOutcome: string, lastSuccessAt: string|null, lastAttemptAt: string|null, consecutiveFailures: number, lastError?: string|null}|null} r
|
|
408
|
+
*/
|
|
409
|
+
export function rendererDownBannerHtml(r) {
|
|
410
|
+
if (!r || r.lastOutcome !== "failed") return "";
|
|
411
|
+
const since = clockTime(r.lastSuccessAt);
|
|
412
|
+
const headline = since
|
|
413
|
+
? `Renderer down since ${since} — screens below are stale.`
|
|
414
|
+
: `Renderer down — no render has completed yet, so there are no screens to show.`;
|
|
415
|
+
const streak = r.consecutiveFailures > 1 ? ` (${r.consecutiveFailures} renders in a row have failed.)` : "";
|
|
416
|
+
const errTail = r.lastError ? ` Last error: ${esc(r.lastError)}` : "";
|
|
417
|
+
return `<div class="banner banner-renderer">${headline}${streak}${errTail}</div>`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** "4 minutes" / "2 hours" / "3 days" — how old, in words a reader can act on. */
|
|
421
|
+
function ageWords(ms) {
|
|
422
|
+
if (ms === null || ms === undefined) return null;
|
|
423
|
+
const mins = Math.round(ms / 60000);
|
|
424
|
+
if (mins < 1) return "moments";
|
|
425
|
+
if (mins < 60) return `${mins} minute${mins === 1 ? "" : "s"}`;
|
|
426
|
+
const hrs = Math.round(mins / 60);
|
|
427
|
+
if (hrs < 24) return `${hrs} hour${hrs === 1 ? "" : "s"}`;
|
|
428
|
+
const days = Math.round(hrs / 24);
|
|
429
|
+
return `${days} day${days === 1 ? "" : "s"}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The freshness banner — the console's central honesty guarantee: pixels are NEVER shown
|
|
434
|
+
* without saying whether they are current, and when they are not, what is being done.
|
|
435
|
+
*
|
|
436
|
+
* Deliberately not an error dump. A concurrent build is normal and reads as a calm
|
|
437
|
+
* "refreshing"; only a render that stays stuck earns alarm styling, and even then the
|
|
438
|
+
* message says the screens are still being retried, because they are.
|
|
439
|
+
*
|
|
440
|
+
* @param {{state: string, phase: string, detail: string|null, ageMs: number|null}|null} f
|
|
441
|
+
*/
|
|
442
|
+
export function freshnessBannerHtml(f) {
|
|
443
|
+
if (!f) return "";
|
|
444
|
+
if (f.state === "never") {
|
|
445
|
+
return `<div class="banner banner-renderer">No render yet — there are no screens to show until the first one completes.</div>`;
|
|
446
|
+
}
|
|
447
|
+
if (f.state === "fresh") return ""; // current: the pixels speak for themselves
|
|
448
|
+
const age = ageWords(f.ageMs);
|
|
449
|
+
const shown = age ? `Showing the last good render from ${age} ago.` : "Showing the last good render.";
|
|
450
|
+
const because = f.detail ? ` ${esc(f.detail)}.` : "";
|
|
451
|
+
if (f.phase === "rendering") {
|
|
452
|
+
return `<div class="banner banner-stale">Refreshing now — ${shown.toLowerCase()}</div>`;
|
|
453
|
+
}
|
|
454
|
+
if (f.phase === "waiting-build" || f.phase === "waiting-lane") {
|
|
455
|
+
return `<div class="banner banner-stale">Waiting to refresh:${because} ${shown} This updates itself.</div>`;
|
|
456
|
+
}
|
|
457
|
+
if (f.phase === "stuck") {
|
|
458
|
+
return `<div class="banner banner-renderer">Cannot refresh right now — still retrying.${because} ${shown}</div>`;
|
|
459
|
+
}
|
|
460
|
+
if (f.phase === "unrefreshed") {
|
|
461
|
+
// Stale with nothing pending: a save did not reach the renderer. Say that plainly
|
|
462
|
+
// rather than promising a refresh that is not coming.
|
|
463
|
+
return `<div class="banner banner-renderer">Out of date and NOT refreshing — a change has not reached the renderer.${because} ${shown}</div>`;
|
|
464
|
+
}
|
|
465
|
+
return `<div class="banner banner-stale">Out of date — ${shown} A refresh is queued.</div>`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The full page. Everything visible is composed here; the caller supplies
|
|
470
|
+
* only data (rail items, section bodies) and behavior (`bodyScript`).
|
|
471
|
+
* @param {object} p
|
|
472
|
+
* @param {string} p.appName
|
|
473
|
+
* @param {Array} p.railItems railItemHtml inputs, in genesis order
|
|
474
|
+
* @param {string} p.railFootHtml the verify-receipt line
|
|
475
|
+
* @param {Array} p.sections sectionHtml inputs
|
|
476
|
+
* @param {string|null} [p.error] last render failure (banner above the pages)
|
|
477
|
+
* @param {object|null} [p.rendererDown] renderer health when dead (see rendererDownBannerHtml)
|
|
478
|
+
* @param {object|null} [p.freshness] derived freshness (see freshnessBannerHtml) — the
|
|
479
|
+
* provenance of the pixels below; absent only in callers that render no screens
|
|
480
|
+
* @param {string} [p.extraCss] caller-computed rules (viewport-derived sizes)
|
|
481
|
+
* @param {string} p.bodyScript the behavior <script> body (unowned by the shell)
|
|
482
|
+
* @param {{treeHash?: string|null, version?: number, build?: object}} [p.provenance]
|
|
483
|
+
* @param {string} [p.govStripHtml] the governance strip (governanceStripHtml) —
|
|
484
|
+
* rail-resident so it is visible on EVERY tab; "" renders nothing
|
|
485
|
+
* @param {object|null} [p.build] the console's own build handshake
|
|
486
|
+
* (buildStatus) — drives the stale banner; absent renders nothing
|
|
487
|
+
*/
|
|
488
|
+
export function renderShellPage(p) {
|
|
489
|
+
const prov = provenanceHtml(p.provenance || {});
|
|
490
|
+
return `<!doctype html>
|
|
491
|
+
<meta charset="utf-8">
|
|
492
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
493
|
+
<title>${esc(p.appName)} · studio</title>
|
|
494
|
+
<style>
|
|
495
|
+
${SHELL_CSS}
|
|
496
|
+
${p.extraCss || ""}
|
|
497
|
+
</style>
|
|
498
|
+
<aside id="rail">
|
|
499
|
+
<div class="rail-head">
|
|
500
|
+
<p class="rail-app">${esc(p.appName)}</p>
|
|
501
|
+
<p class="rail-sub">studio console</p>
|
|
502
|
+
<span id="pill">live</span>
|
|
503
|
+
</div>
|
|
504
|
+
${p.govStripHtml || ""}
|
|
505
|
+
<nav class="rail-nav">
|
|
506
|
+
${p.railItems.map(railItemHtml).join("\n")}
|
|
507
|
+
</nav>
|
|
508
|
+
<div class="rail-foot">${p.railFootHtml}</div>
|
|
509
|
+
</aside>
|
|
510
|
+
<main>
|
|
511
|
+
${staleConsoleBannerHtml(p.build)}
|
|
512
|
+
${freshnessBannerHtml(p.freshness)}
|
|
513
|
+
${rendererDownBannerHtml(p.rendererDown)}
|
|
514
|
+
${p.error ? `<div class="banner">last render FAILED — showing previous state\n${esc(p.error)}</div>` : ""}
|
|
515
|
+
${p.sections.map((s) => sectionHtml(s, prov)).join("\n")}
|
|
516
|
+
</main>
|
|
517
|
+
<script>
|
|
518
|
+
const CMP_CONSOLE_BUILD = ${JSON.stringify(p.build && p.build.id ? String(p.build.id) : null)};
|
|
519
|
+
${p.bodyScript}
|
|
520
|
+
</script>
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// --- the design system (one stylesheet, all sections) -------------------------
|
|
525
|
+
//
|
|
526
|
+
// Color roles: --paper/--surface/--ink/--ink-2/--muted/--line are the
|
|
527
|
+
// neutrals; --accent is the ONE accent (navigation, actions, attention that
|
|
528
|
+
// is not a verdict); --signed/--drift/--reopen (+ -bg) are the reserved
|
|
529
|
+
// semantic trio. Type ramp: --fs-title 18 / --fs-head 14 / --fs-body 13 /
|
|
530
|
+
// --fs-meta 11.5 (+ the uppercase .lbl micro-label). Spacing: 8px steps used
|
|
531
|
+
// literally (8/16/24/32/40). Prose sits in a readable measure via
|
|
532
|
+
// .page-body's max-width; .full-bleed pages (the screens gallery) opt out.
|
|
533
|
+
|
|
534
|
+
export const SHELL_CSS = `
|
|
535
|
+
:root {
|
|
536
|
+
--paper: #FFFFFF; --surface: #F5F6F8; --ink: #1D2126; --ink-2: #4A5361;
|
|
537
|
+
--muted: #8A919C; --line: #E4E7EB;
|
|
538
|
+
--accent: #3557C0; --accent-ink: #FFFFFF; --accent-bg: #EDF1FB;
|
|
539
|
+
--signed: #1E7A44; --signed-bg: #E8F4ED;
|
|
540
|
+
--drift: #C03A2E; --drift-bg: #FBEDEB;
|
|
541
|
+
--reopen: #96690A; --reopen-bg: #FAF2DF;
|
|
542
|
+
--fs-title: 18px; --fs-head: 14px; --fs-body: 13px; --fs-meta: 11.5px;
|
|
543
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
544
|
+
color-scheme: light dark;
|
|
545
|
+
}
|
|
546
|
+
@media (prefers-color-scheme: dark) {
|
|
547
|
+
:root {
|
|
548
|
+
--paper: #17191D; --surface: #1F2329; --ink: #E8EAEE; --ink-2: #B6BDC8;
|
|
549
|
+
--muted: #7E8794; --line: #2B3038;
|
|
550
|
+
--accent: #8AA3F0; --accent-ink: #10131A; --accent-bg: #232D45;
|
|
551
|
+
--signed: #5CB985; --signed-bg: #1B2E23;
|
|
552
|
+
--drift: #E4766A; --drift-bg: #392019;
|
|
553
|
+
--reopen: #D9A845; --reopen-bg: #33290F;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
* { box-sizing: border-box; }
|
|
557
|
+
body { margin: 0; display: flex; min-height: 100vh; background: var(--paper); color: var(--ink);
|
|
558
|
+
font-family: -apple-system, system-ui, "Segoe UI", sans-serif; font-size: var(--fs-body); line-height: 1.55; }
|
|
559
|
+
code { font-family: var(--mono); font-size: 0.92em; }
|
|
560
|
+
|
|
561
|
+
/* --- the rail (§2: ordering + coverage, ambient and permanent) --- */
|
|
562
|
+
#rail { width: 216px; flex: none; position: sticky; top: 0; height: 100vh; overflow-y: auto;
|
|
563
|
+
display: flex; flex-direction: column; gap: 24px; padding: 24px 12px 16px;
|
|
564
|
+
border-right: 1px solid var(--line); background: var(--paper); }
|
|
565
|
+
.rail-head { padding: 0 10px; }
|
|
566
|
+
.rail-app { margin: 0; font-size: var(--fs-head); font-weight: 650; letter-spacing: .01em; }
|
|
567
|
+
.rail-sub { margin: 2px 0 8px; font-size: var(--fs-meta); color: var(--muted); }
|
|
568
|
+
#pill { font-size: 10.5px; font-weight: 650; border-radius: 999px; padding: 2px 9px;
|
|
569
|
+
background: var(--signed-bg); color: var(--signed); }
|
|
570
|
+
#pill.rendering { background: var(--reopen-bg); color: var(--reopen); }
|
|
571
|
+
#pill.error { background: var(--drift-bg); color: var(--drift); }
|
|
572
|
+
/* --- the governance strip (07-28 audit fix 5: status visible at ALL times,
|
|
573
|
+
on every tab — counts, the one next human act, recent history) --- */
|
|
574
|
+
#gov-strip { margin: 0 4px; padding: 10px; border: 1px solid var(--line); border-radius: 10px;
|
|
575
|
+
background: var(--surface); display: flex; flex-direction: column; gap: 8px; }
|
|
576
|
+
.gov-counts { margin: 0; font-size: var(--fs-meta); color: var(--ink-2); line-height: 1.6; }
|
|
577
|
+
.gov-n { white-space: nowrap; }
|
|
578
|
+
/* A count that names an artifact wraps — the name is the point, not the digit. */
|
|
579
|
+
.gov-jump { white-space: normal; text-align: left; appearance: none; border: none; background: none;
|
|
580
|
+
padding: 0; margin: 0; font: inherit; cursor: pointer; text-decoration: underline;
|
|
581
|
+
text-decoration-style: dotted; text-underline-offset: 2px; }
|
|
582
|
+
.gov-jump:hover { text-decoration-style: solid; }
|
|
583
|
+
.gov-signed { color: var(--signed); font-weight: 600; }
|
|
584
|
+
.gov-awaiting { color: var(--accent); font-weight: 650; }
|
|
585
|
+
.gov-redesign { color: var(--reopen); font-weight: 600; }
|
|
586
|
+
.gov-drift { color: var(--drift); font-weight: 650; }
|
|
587
|
+
.gov-next { appearance: none; border: none; border-radius: 8px; padding: 6px 9px; cursor: pointer;
|
|
588
|
+
font: inherit; font-size: var(--fs-meta); font-weight: 600; text-align: left;
|
|
589
|
+
background: var(--accent-bg); color: var(--accent); line-height: 1.45; }
|
|
590
|
+
.gov-next:hover { filter: brightness(0.97); }
|
|
591
|
+
.gov-more { font-weight: 400; color: var(--muted); }
|
|
592
|
+
.gov-clear { margin: 0; font-size: var(--fs-meta); color: var(--muted); }
|
|
593
|
+
.gov-history summary { font-size: var(--fs-meta); color: var(--muted); cursor: pointer; }
|
|
594
|
+
.gov-event { margin: 6px 0 0; font-size: var(--fs-meta); color: var(--ink-2); line-height: 1.4;
|
|
595
|
+
overflow-wrap: anywhere; }
|
|
596
|
+
.gov-event-glyph { color: var(--muted); }
|
|
597
|
+
.gov-event-reason { display: block; color: var(--muted); font-style: italic; }
|
|
598
|
+
/* --- the front door (§3.7) ---------------------------------------------
|
|
599
|
+
Deliberately quiet: this page's job is triage, so the only colour on it is
|
|
600
|
+
the reserved semantic trio carried in by each row's own glyph. The queue
|
|
601
|
+
is an ordered list because the order is the derivation's (deriveHumanQueue),
|
|
602
|
+
not a visual choice. */
|
|
603
|
+
.fd-h { font-size: var(--fs-head); margin: 24px 0 8px; display: flex; align-items: baseline; gap: 8px; }
|
|
604
|
+
.fd-h:first-of-type { margin-top: 8px; }
|
|
605
|
+
.fd-count { font-size: var(--fs-meta); font-weight: 600; color: var(--accent);
|
|
606
|
+
background: var(--accent-bg); border-radius: 999px; padding: 1px 8px; }
|
|
607
|
+
.fd-since { font-size: var(--fs-meta); font-weight: 400; color: var(--muted); }
|
|
608
|
+
.fd-queue { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
609
|
+
.fd-item { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: var(--paper); }
|
|
610
|
+
.fd-act { margin: 0; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
|
|
611
|
+
font-size: var(--fs-body); line-height: 1.5; }
|
|
612
|
+
.fd-label { flex: 1; min-width: 12ch; color: var(--ink); }
|
|
613
|
+
.fd-actions { flex: none; display: inline-flex; align-items: center; gap: 10px; }
|
|
614
|
+
/* The row's own signature control. Accent-filled: it IS the primary act of
|
|
615
|
+
this page. "read it first" stays beside it as the quiet secondary route. */
|
|
616
|
+
.fd-sign { appearance: none; border: none; border-radius: 8px; padding: 5px 12px; cursor: pointer;
|
|
617
|
+
font: inherit; font-size: var(--fs-meta); font-weight: 650;
|
|
618
|
+
background: var(--accent); color: var(--accent-ink); }
|
|
619
|
+
.fd-sign:hover { filter: brightness(1.08); }
|
|
620
|
+
.fd-sign:disabled { opacity: .55; cursor: default; }
|
|
621
|
+
.fd-go { flex: none; color: var(--accent); font-size: var(--fs-meta); font-weight: 600; }
|
|
622
|
+
.fd-evidence { margin: 6px 0 0; font-size: var(--fs-meta); color: var(--ink-2); }
|
|
623
|
+
.fd-evidence summary { cursor: pointer; color: var(--ink-2); }
|
|
624
|
+
.fd-evidence-absent { color: var(--muted); font-style: italic; }
|
|
625
|
+
.fd-files { list-style: none; margin: 6px 0 0; padding: 0 0 0 2px;
|
|
626
|
+
display: flex; flex-direction: column; gap: 3px; }
|
|
627
|
+
.fd-files code { overflow-wrap: anywhere; }
|
|
628
|
+
.fd-status { display: inline-block; width: 1.4em; color: var(--muted); font-weight: 600; }
|
|
629
|
+
.fd-more { color: var(--muted); }
|
|
630
|
+
.fd-clear { margin: 0; font-size: var(--fs-body); color: var(--ink-2); }
|
|
631
|
+
.fd-history { border-top: 1px solid var(--line); padding-top: 8px; }
|
|
632
|
+
/* In flight (walk-status): the six-stage tracker. States: done=signed colour,
|
|
633
|
+
cur=accent + weight, todo=muted, skip=faint with its honesty note in title. */
|
|
634
|
+
.wk-card { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; margin: 0 0 8px; background: var(--paper); }
|
|
635
|
+
.wk-name { margin: 0 0 6px; font-weight: 600; display: flex; align-items: baseline; gap: 10px; }
|
|
636
|
+
.wk-promises { font-size: var(--fs-meta); font-weight: 400; color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
|
637
|
+
.wk-stages { margin: 0 0 6px; display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: var(--fs-meta); }
|
|
638
|
+
.wk-stage { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
|
|
639
|
+
.wk-dot { width: 8px; height: 8px; border-radius: 999px; background: var(--line); }
|
|
640
|
+
.wk-done { color: var(--ink-2); } .wk-done .wk-dot { background: var(--signed); }
|
|
641
|
+
.wk-cur { color: var(--accent); font-weight: 650; } .wk-cur .wk-dot { background: var(--accent); }
|
|
642
|
+
.wk-skip { opacity: .45; }
|
|
643
|
+
.wk-you { margin: 0; font-size: var(--fs-meta); color: var(--ink-2); }
|
|
644
|
+
.wk-turn { display: inline-block; padding: 1px 8px; border-radius: 999px; background: var(--accent); color: var(--accent-ink); font-weight: 700; font-size: 10.5px; }
|
|
645
|
+
.wk-agent { color: var(--muted); font-weight: 600; }
|
|
646
|
+
.wk-arrival { margin: 4px 0 0; padding: 8px 12px; border-radius: 10px; background: var(--reopen-bg); color: var(--reopen); font-size: var(--fs-meta); }
|
|
647
|
+
/* Walk-legibility L3/L4/L5 additions: the stage gloss, the promise list
|
|
648
|
+
with per-promise kept state, Prove's measured cost, and the arrival
|
|
649
|
+
now-or-after buttons. The list scrolls rather than truncates — a
|
|
650
|
+
collapsed tally is exactly what the rich card replaces. */
|
|
651
|
+
.wk-gloss { font-size: var(--fs-meta); font-weight: 400; color: var(--muted); }
|
|
652
|
+
.wk-note { font-size: 10.5px; color: var(--muted); font-weight: 400; }
|
|
653
|
+
.wk-plist { list-style: none; margin: 0 0 8px; padding: 0 0 0 2px; max-height: 180px; overflow-y: auto;
|
|
654
|
+
display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-meta); }
|
|
655
|
+
.wk-plist code { font-size: 10.5px; }
|
|
656
|
+
.wk-p-glyph { display: inline-block; width: 1.2em; text-align: center; }
|
|
657
|
+
.wk-p-kept { color: var(--muted); } .wk-p-kept .wk-p-glyph { color: var(--signed); }
|
|
658
|
+
.wk-p-cur { color: var(--ink-2); font-weight: 600; } .wk-p-cur .wk-p-glyph { color: var(--accent); }
|
|
659
|
+
.wk-p-todo { color: var(--muted); }
|
|
660
|
+
.wk-stops { color: var(--muted); }
|
|
661
|
+
.wk-lane { margin: 2px 0 0; font-size: var(--fs-meta); color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
662
|
+
.wk-arrival-btn { appearance: none; margin-left: 6px; padding: 2px 10px; border: 1px solid var(--reopen);
|
|
663
|
+
border-radius: 999px; background: none; color: var(--reopen); cursor: pointer;
|
|
664
|
+
font: inherit; font-size: 11px; font-weight: 600; }
|
|
665
|
+
.wk-arrival-btn:hover { background: var(--reopen); color: var(--paper); }
|
|
666
|
+
.wk-arrival-btn[disabled] { opacity: .6; cursor: default; }
|
|
667
|
+
/* The live chain (studio-drive-mode): request · step chain · freshness.
|
|
668
|
+
The one place the studio renders DECLARED state — the .ch-age label says
|
|
669
|
+
so, always. Busy (tier 3, derived) is the only accent that pulses. */
|
|
670
|
+
.ch-strip { border: 1px solid var(--accent); border-radius: 12px; padding: 12px 14px; margin: 0 0 16px;
|
|
671
|
+
background: var(--accent-bg); }
|
|
672
|
+
.ch-request { margin: 0 0 8px; font-size: var(--fs-head); font-weight: 600; color: var(--ink); }
|
|
673
|
+
.ch-request .lbl { margin-right: 8px; }
|
|
674
|
+
.ch-steps { margin: 0 0 6px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 6px;
|
|
675
|
+
font-size: var(--fs-meta); }
|
|
676
|
+
.ch-step { display: inline-flex; align-items: baseline; gap: 4px; }
|
|
677
|
+
.ch-glyph { font-size: 10.5px; }
|
|
678
|
+
.ch-done { color: var(--muted); } .ch-done .ch-glyph { color: var(--signed); }
|
|
679
|
+
.ch-cur { color: var(--accent); font-weight: 700; }
|
|
680
|
+
.ch-todo { color: var(--muted); }
|
|
681
|
+
.ch-arrow { color: var(--line); margin: 0 2px; }
|
|
682
|
+
.ch-busy { color: var(--accent); font-weight: 650; animation: ch-pulse 1.6s ease-in-out infinite; }
|
|
683
|
+
@keyframes ch-pulse { 50% { opacity: .45; } }
|
|
684
|
+
.ch-meta { margin: 0; font-size: var(--fs-meta); color: var(--ink-2); }
|
|
685
|
+
.ch-age { color: var(--muted); }
|
|
686
|
+
.ch-time { color: var(--muted); font-weight: 400; font-size: 10.5px; }
|
|
687
|
+
/* Provenance chips (drive-narration N3): observed/recorded = the machine's
|
|
688
|
+
word, declared = the agent's — the same fact the .ch-age label carried in
|
|
689
|
+
prose, now legible at a glance. */
|
|
690
|
+
.ch-prov { display: inline-block; padding: 0 5px; border-radius: 7px; font-size: 9.5px;
|
|
691
|
+
font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
|
|
692
|
+
vertical-align: 1px; cursor: help; }
|
|
693
|
+
.ch-prov-obs { color: var(--signed); border: 1px solid var(--signed); }
|
|
694
|
+
.ch-prov-dec { color: var(--muted); border: 1px solid var(--line); }
|
|
695
|
+
/* Recent requests (drive-narration N5): the closed-chain trail, one collapsed
|
|
696
|
+
click in — request → steps → outcome, newest first. */
|
|
697
|
+
.ch-hist { margin: 8px 0 0; }
|
|
698
|
+
.ch-hist > summary { cursor: pointer; color: var(--muted); font-size: var(--fs-meta);
|
|
699
|
+
font-weight: 600; padding: 2px 0; }
|
|
700
|
+
.ch-hist > summary:hover { color: var(--ink-2); }
|
|
701
|
+
.ch-hist-row { margin: 4px 0 0 12px; font-size: var(--fs-meta); color: var(--ink-2); }
|
|
702
|
+
.ch-hist-outcome { font-weight: 650; }
|
|
703
|
+
.ch-hist-outcome.ok { color: var(--signed); }
|
|
704
|
+
.ch-hist-outcome.bad { color: var(--reopen); }
|
|
705
|
+
/* Page anatomy (studio-drive-mode): the collapsed tails. .fd-fold is
|
|
706
|
+
Drive's own digest/history; .mirror-details wraps a mirror section's
|
|
707
|
+
complete body — verdict stays in the page head, the corpus one click in. */
|
|
708
|
+
.fd-fold { margin: 16px 0 0; }
|
|
709
|
+
.fd-fold > summary, .mirror-details > summary { cursor: pointer; color: var(--muted);
|
|
710
|
+
font-size: var(--fs-body); font-weight: 600; padding: 6px 0; }
|
|
711
|
+
.fd-fold > summary:hover, .mirror-details > summary:hover { color: var(--ink-2); }
|
|
712
|
+
.mirror-details { margin: 0; }
|
|
713
|
+
/* The digest keeps its own renderer and its own headings; inside the front
|
|
714
|
+
door it is a block, not a page, so its h3s step down a level. Nothing here
|
|
715
|
+
hides any of its content — the one duplicated line (the window) was deleted
|
|
716
|
+
at the source instead. */
|
|
717
|
+
.fd-digest h3 { font-size: var(--fs-body); margin: 16px 0 6px; color: var(--ink-2); }
|
|
718
|
+
.digest-files summary { cursor: pointer; color: var(--muted); font-size: var(--fs-meta); }
|
|
719
|
+
.digest-filelist { list-style: none; margin: 4px 0 0; padding: 0 0 0 2px;
|
|
720
|
+
display: flex; flex-direction: column; gap: 3px; font-size: var(--fs-meta); }
|
|
721
|
+
.digest-filelist code { overflow-wrap: anywhere; }
|
|
722
|
+
.rail-nav { display: flex; flex-direction: column; gap: 1px; }
|
|
723
|
+
.tab-btn { appearance: none; display: flex; align-items: center; gap: 9px; width: 100%;
|
|
724
|
+
padding: 7px 10px; border: none; border-radius: 8px; background: none; cursor: pointer;
|
|
725
|
+
font: inherit; font-size: var(--fs-body); color: var(--ink-2); text-align: left; }
|
|
726
|
+
.tab-btn:hover { background: var(--surface); }
|
|
727
|
+
.tab-btn.active { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
|
|
728
|
+
.rail-label { flex: 1; }
|
|
729
|
+
.glyph { flex: none; width: 1.1em; text-align: center; font-size: 11px; line-height: 1; }
|
|
730
|
+
.glyph-signed { color: var(--signed); }
|
|
731
|
+
/* Waiting-on-you states are ACCENT, not grey — the rail-truth rule: grey is
|
|
732
|
+
reserved for "truly nothing pending" (glyph-none); anything that needs the
|
|
733
|
+
human's signature (unsigned ○) or acceptance (attn ●) must read as colour. */
|
|
734
|
+
.glyph-unsigned { color: var(--accent); }
|
|
735
|
+
.glyph-attn { color: var(--accent); }
|
|
736
|
+
.glyph-reopen { color: var(--reopen); }
|
|
737
|
+
.glyph-drift { color: var(--drift); }
|
|
738
|
+
.glyph-none { color: var(--line); }
|
|
739
|
+
.rail-foot { margin-top: auto; padding: 12px 10px 0; border-top: 1px solid var(--line);
|
|
740
|
+
font-size: var(--fs-meta); color: var(--muted); line-height: 1.5; }
|
|
741
|
+
/* The rail foot doubles as the deep link to Evidence — same .tab-btn/data-tab
|
|
742
|
+
wiring as the nav items, styled as the quiet meta line it always was. */
|
|
743
|
+
.rail-foot .tab-btn { padding: 0; border-radius: 6px; font-size: var(--fs-meta);
|
|
744
|
+
color: var(--muted); gap: 6px; }
|
|
745
|
+
.rail-foot .tab-btn:hover { background: none; color: var(--ink-2); text-decoration: underline; }
|
|
746
|
+
.rail-foot .tab-btn.active { background: none; color: var(--accent); font-weight: 600; }
|
|
747
|
+
.tab-badge { display: inline-block; min-width: 16px; padding: 1px 6px; border-radius: 999px;
|
|
748
|
+
background: var(--drift); color: #fff; font-size: 10px; font-weight: 700; text-align: center; }
|
|
749
|
+
.tab-badge[hidden] { display: none !important; }
|
|
750
|
+
|
|
751
|
+
/* --- pages (§2 grammar: head → body → provenance foot) --- */
|
|
752
|
+
main { flex: 1; min-width: 0; }
|
|
753
|
+
.banner { margin: 24px 40px 0; padding: 10px 14px; border-radius: 10px; background: var(--drift-bg);
|
|
754
|
+
color: var(--drift); font-size: var(--fs-body); white-space: pre-wrap; }
|
|
755
|
+
/* Renderer-down (FI-9 Change B): amber/--reopen, not red/--drift — "the eyes
|
|
756
|
+
are stale", the same "stale, not broken" vocabulary the rail-foot receipt
|
|
757
|
+
line uses, kept visually distinct from a compile/reload .banner. */
|
|
758
|
+
.banner-renderer { background: var(--reopen-bg); color: var(--reopen); }
|
|
759
|
+
/* Stale-but-working-on-it: informational, NOT alarm. A concurrent build is normal;
|
|
760
|
+
only a render that stays stuck earns the renderer banner's colour. */
|
|
761
|
+
.banner-stale { background: var(--surface-2, rgba(255,255,255,.05)); color: var(--muted); }
|
|
762
|
+
/* A stale console invalidates the whole page below it, so it reads as DRIFT
|
|
763
|
+
(the reserved red), never as the quiet grey a queued refresh gets. */
|
|
764
|
+
.banner-stale-build { background: var(--drift-bg); color: var(--drift); font-weight: 600; }
|
|
765
|
+
.banner-stale-build code { font-weight: 500; }
|
|
766
|
+
.tab-panel { display: none; padding: 32px 40px 48px; }
|
|
767
|
+
.tab-panel.active { display: block; }
|
|
768
|
+
.page-head { margin: 0 0 24px; padding-bottom: 16px; border-bottom: 1px solid var(--line); }
|
|
769
|
+
.page-head h2 { margin: 0; font-size: var(--fs-title); font-weight: 650; letter-spacing: .01em; }
|
|
770
|
+
.page-status { margin: 6px 0 0; font-size: var(--fs-meta); color: var(--muted);
|
|
771
|
+
display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
772
|
+
.page-status .glyph { font-size: 10px; }
|
|
773
|
+
.status-drift { color: var(--drift); font-weight: 600; }
|
|
774
|
+
.status-reopen { color: var(--reopen); font-weight: 600; }
|
|
775
|
+
.page-body { max-width: 860px; }
|
|
776
|
+
.full-bleed .page-body { max-width: none; }
|
|
777
|
+
.page-foot { margin-top: 40px; padding-top: 12px; border-top: 1px solid var(--line);
|
|
778
|
+
font-size: var(--fs-meta); color: var(--muted); }
|
|
779
|
+
|
|
780
|
+
/* --- shared document vocabulary (labels, tables, chips, empty states) --- */
|
|
781
|
+
h3 { font-size: var(--fs-head); font-weight: 650; margin: 24px 0 8px; }
|
|
782
|
+
.page-body > h3:first-child { margin-top: 0; }
|
|
783
|
+
.lbl { font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin: 0 0 4px; }
|
|
784
|
+
.meta { color: var(--muted); font-size: var(--fs-meta); margin: 0 0 8px; }
|
|
785
|
+
.empty { padding: 16px; border: 1px dashed var(--line); border-radius: 10px; color: var(--ink-2);
|
|
786
|
+
font-size: var(--fs-body); max-width: 66ch; }
|
|
787
|
+
.empty p { margin: 0 0 6px; } .empty p:last-child { margin: 0; }
|
|
788
|
+
.empty-inline { color: var(--muted); font-size: var(--fs-meta); }
|
|
789
|
+
.badge { font-size: 10.5px; font-weight: 650; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
|
|
790
|
+
.badge-approved { background: var(--signed-bg); color: var(--signed); }
|
|
791
|
+
.badge-unreviewed { background: var(--surface); color: var(--ink-2); }
|
|
792
|
+
.badge-changed { background: var(--drift-bg); color: var(--drift); }
|
|
793
|
+
.badge-reopened { background: var(--reopen-bg); color: var(--reopen); }
|
|
794
|
+
.badge-unshaped { box-shadow: inset 0 0 0 1px var(--reopen); }
|
|
795
|
+
.badge-open { background: var(--accent-bg); color: var(--accent); }
|
|
796
|
+
.badge-resolved { background: var(--signed-bg); color: var(--signed); }
|
|
797
|
+
.evidence-rung { background: var(--accent-bg); color: var(--accent); }
|
|
798
|
+
|
|
799
|
+
/* --- screens (§3.4: the screen × state matrix) --- */
|
|
800
|
+
.screens-toolbar { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
|
|
801
|
+
#filter { font: inherit; font-size: var(--fs-body); padding: 5px 12px; width: 240px;
|
|
802
|
+
border: 1px solid var(--line); border-radius: 999px; background: var(--paper); color: inherit; }
|
|
803
|
+
.matrix { display: flex; flex-direction: column; }
|
|
804
|
+
.matrix-head { display: flex; gap: 16px; padding-bottom: 8px; }
|
|
805
|
+
.matrix-gutter { flex: 0 0 184px; }
|
|
806
|
+
.matrix-col { flex: none; font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
|
|
807
|
+
.matrix-row { border-top: 1px solid var(--line); padding: 16px 0; }
|
|
808
|
+
.matrix-line { display: flex; gap: 16px; align-items: flex-start; }
|
|
809
|
+
.matrix-rowhead { flex: 0 0 184px; min-width: 0; }
|
|
810
|
+
.matrix-rowhead h3 { margin: 0 0 2px; }
|
|
811
|
+
.matrix-rowhead h3 .flag { font-size: 10px; font-weight: 700; color: var(--accent); vertical-align: middle;
|
|
812
|
+
margin-left: 6px; letter-spacing: .05em; }
|
|
813
|
+
.matrix-cells { display: flex; gap: 16px; }
|
|
814
|
+
.matrix-cell { flex: none; }
|
|
815
|
+
.matrix-cell img { border: 1px solid var(--line); border-radius: 10px; display: block; }
|
|
816
|
+
.matrix-cell.changed img { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-bg); }
|
|
817
|
+
.matrix-cell .lbl { margin: 4px 0 0; }
|
|
818
|
+
.matrix-none { display: grid; place-items: center; min-height: 96px; border: 1px dashed var(--line);
|
|
819
|
+
border-radius: 10px; color: var(--muted); opacity: .6; font-size: 15px; }
|
|
820
|
+
.matrix-rowend { flex: 0 0 190px; }
|
|
821
|
+
.matrix-rowend .meta { margin: 0 0 4px; }
|
|
822
|
+
.matrix-note { margin-top: 16px; max-width: 75ch; }
|
|
823
|
+
.row-detail { margin-top: 8px; }
|
|
824
|
+
.row-detail summary { cursor: pointer; font-size: var(--fs-meta); color: var(--ink-2); width: max-content; }
|
|
825
|
+
.row-detail summary:hover { color: var(--accent); }
|
|
826
|
+
.row-detail-body { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; margin-top: 12px; }
|
|
827
|
+
.row-clauses { flex: 1 1 320px; min-width: 0; }
|
|
828
|
+
.meta .fail { color: var(--drift); font-weight: 600; }
|
|
829
|
+
.meta .pass { color: var(--signed); font-weight: 600; }
|
|
830
|
+
.meta .chg { color: var(--accent); font-weight: 600; }
|
|
831
|
+
.cmp img.prev { display: none; }
|
|
832
|
+
.cmp:hover img.prev { display: block; }
|
|
833
|
+
.cmp:hover img.cur { display: none; }
|
|
834
|
+
.wire { border: 1px dashed var(--line); border-radius: 10px; overflow: hidden; flex: none; }
|
|
835
|
+
.wire svg { height: auto; display: block; }
|
|
836
|
+
|
|
837
|
+
/* --- intent (§3.0: the product strategist's brief) --- */
|
|
838
|
+
.brief-section { margin-bottom: 28px; }
|
|
839
|
+
.brief-section h3 { margin: 0 0 8px; }
|
|
840
|
+
.brief-unfilled h3 { color: var(--ink-2); }
|
|
841
|
+
.brief-pending { padding: 24px; border: 1px dashed var(--line); border-radius: 12px; max-width: 66ch; }
|
|
842
|
+
.brief-pending p { margin: 0 0 8px; color: var(--ink-2); } .brief-pending p:last-child { margin: 0; }
|
|
843
|
+
.brief-pending-state { font-size: var(--fs-head); font-weight: 650; color: var(--ink) !important; }
|
|
844
|
+
.brief-pending-inline { margin: 0 0 6px; color: var(--muted); font-style: italic; }
|
|
845
|
+
.brief-guidance { margin: 0; font-size: var(--fs-meta); color: var(--muted); max-width: 66ch; }
|
|
846
|
+
|
|
847
|
+
/* --- design language (§3.1: the designer's handoff spec) --- */
|
|
848
|
+
.tok-table, .approvals-table, .comments-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); margin-top: 8px; }
|
|
849
|
+
.tok-table td, .tok-table th, .approvals-table td, .approvals-table th, .comments-table td, .comments-table th {
|
|
850
|
+
padding: 8px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
|
851
|
+
.tok-table th, .approvals-table th, .comments-table th { color: var(--muted); font-weight: 600; font-size: var(--fs-meta); }
|
|
852
|
+
.tok-table td { vertical-align: middle; }
|
|
853
|
+
.tok-swatch-cell { width: 44px; }
|
|
854
|
+
.tok-swatch { display: block; width: 32px; height: 24px; border-radius: 6px; border: 1px solid var(--line); }
|
|
855
|
+
.tok-usage { font-size: var(--fs-meta); color: var(--ink-2); white-space: nowrap; }
|
|
856
|
+
.contrast-sample { display: inline-grid; place-items: center; width: 44px; height: 26px; border-radius: 6px;
|
|
857
|
+
border: 1px solid var(--line); font-size: 12px; font-weight: 650; margin-right: 10px;
|
|
858
|
+
vertical-align: middle; }
|
|
859
|
+
.contrast-ratio { font-family: var(--mono); font-size: var(--fs-meta); white-space: nowrap; }
|
|
860
|
+
.wcag-pass { color: var(--signed); font-weight: 600; font-size: var(--fs-meta); }
|
|
861
|
+
.wcag-fail { color: var(--drift); font-weight: 600; font-size: var(--fs-meta); }
|
|
862
|
+
.scale-list { margin-top: 8px; }
|
|
863
|
+
.scale-row { display: flex; align-items: center; gap: 14px; padding: 7px 0; border-bottom: 1px solid var(--line); }
|
|
864
|
+
.scale-name { flex: 0 0 176px; font-size: var(--fs-meta); }
|
|
865
|
+
.scale-bar { height: 12px; border-radius: 3px; background: var(--accent); flex: none; max-width: 55%; }
|
|
866
|
+
.scale-value { font-size: var(--fs-meta); font-family: var(--mono); color: var(--muted); }
|
|
867
|
+
.candidates-strip { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 8px; }
|
|
868
|
+
.candidate-card { flex: 1 1 260px; border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--paper); }
|
|
869
|
+
.candidate-card h4 { margin: 0 0 8px; font-size: var(--fs-body); }
|
|
870
|
+
.candidate-shots { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
|
|
871
|
+
.candidate-shot { width: 90px; }
|
|
872
|
+
.candidate-shot img { width: 100%; border: 1px solid var(--line); border-radius: 8px; display: block; }
|
|
873
|
+
|
|
874
|
+
/* --- buttons (actions use the accent; destructive-direction uses amber) --- */
|
|
875
|
+
.approve-btn, .pick-btn { font: inherit; font-size: var(--fs-meta); font-weight: 600; padding: 6px 12px;
|
|
876
|
+
border-radius: 8px; border: 1px solid var(--accent); background: var(--accent); color: var(--accent-ink); cursor: pointer; }
|
|
877
|
+
.approve-btn:disabled, .pick-btn:disabled { background: var(--surface); border-color: var(--line); color: var(--muted); cursor: not-allowed; }
|
|
878
|
+
.reopen-btn { font: inherit; font-size: var(--fs-meta); font-weight: 600; padding: 6px 12px; border-radius: 8px;
|
|
879
|
+
border: 1px solid var(--reopen); background: var(--paper); color: var(--reopen); cursor: pointer; }
|
|
880
|
+
|
|
881
|
+
/* --- approvals detail --- */
|
|
882
|
+
.artifact-id, .approved-at { font-size: var(--fs-meta); color: var(--muted); }
|
|
883
|
+
.approved-at { margin-top: 2px; }
|
|
884
|
+
.unresolvable-note, .missing-note { font-size: var(--fs-meta); color: var(--reopen); margin: 4px 0 0; }
|
|
885
|
+
.reopen-note { font-size: var(--fs-meta); color: var(--reopen); margin: 4px 0 0; overflow-wrap: anywhere; }
|
|
886
|
+
.order-num { color: var(--muted); }
|
|
887
|
+
.artifact-banner { font-size: var(--fs-meta); margin-top: 4px; padding: 4px 8px; border-radius: 8px; max-width: 340px; }
|
|
888
|
+
.banner-mode { font-weight: 700; text-transform: uppercase; letter-spacing: .04em; margin-right: 4px; font-size: 9.5px; }
|
|
889
|
+
.banner-genesis { background: var(--accent-bg); color: var(--accent); }
|
|
890
|
+
.banner-steward { background: var(--surface); color: var(--ink-2); }
|
|
891
|
+
.banner-unshaped { background: var(--reopen-bg); color: var(--reopen); }
|
|
892
|
+
|
|
893
|
+
/* --- specs / clauses --- */
|
|
894
|
+
.spec-file h3 { margin: 24px 0 8px; }
|
|
895
|
+
.spec-file:first-child h3 { margin-top: 0; }
|
|
896
|
+
.clause-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
897
|
+
.clause { display: flex; align-items: baseline; gap: 10px; font-size: var(--fs-body); padding: 8px 10px;
|
|
898
|
+
border: 1px solid var(--line); border-radius: 10px; background: var(--paper); }
|
|
899
|
+
.clause.withdrawn { opacity: .55; }
|
|
900
|
+
.clause-id { font-size: var(--fs-meta); color: var(--accent); font-weight: 700; flex: 0 0 auto; }
|
|
901
|
+
.clause-prose { flex: 1; }
|
|
902
|
+
.cov-badge, .receipt-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 999px; flex: 0 0 auto; white-space: nowrap; }
|
|
903
|
+
.cov-yes, .receipt-pass { background: var(--signed-bg); color: var(--signed); }
|
|
904
|
+
.cov-no, .receipt-fail { background: var(--drift-bg); color: var(--drift); }
|
|
905
|
+
.cov-na, .receipt-none { background: var(--surface); color: var(--muted); }
|
|
906
|
+
.receipt-stale { background: var(--reopen-bg); color: var(--reopen); }
|
|
907
|
+
.receipt-age { font-size: 10px; color: var(--muted); white-space: nowrap; }
|
|
908
|
+
|
|
909
|
+
/* --- architecture document --- */
|
|
910
|
+
.arch-section { margin-bottom: 32px; }
|
|
911
|
+
.arch-section h3 { margin: 0 0 10px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
|
|
912
|
+
.arch-section h4 { margin: 16px 0 8px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
913
|
+
.doc-table { width: 100%; border-collapse: collapse; font-size: var(--fs-meta); margin: 4px 0 12px; }
|
|
914
|
+
.doc-table th, .doc-table td { padding: 7px 9px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
|
915
|
+
.doc-table th { color: var(--muted); font-weight: 600; }
|
|
916
|
+
/* Type ramp: the specimen column is set in the rung's OWN size/weight/tracking,
|
|
917
|
+
so the row heights are deliberately uneven — that unevenness is the ramp. */
|
|
918
|
+
.type-ramp td { vertical-align: middle; }
|
|
919
|
+
.type-ramp .ramp-name { width: 16ch; white-space: nowrap; }
|
|
920
|
+
.type-ramp .ramp-specimen { color: var(--ink); overflow: hidden; }
|
|
921
|
+
.type-ramp .ramp-numbers { width: 34ch; color: var(--muted); text-align: right; white-space: nowrap; }
|
|
922
|
+
.doc-prose { font-size: var(--fs-body); max-width: 75ch; }
|
|
923
|
+
.doc-prose p { margin: 0 0 10px; }
|
|
924
|
+
.doc-prose h4, .doc-prose h5 { margin: 14px 0 6px; font-size: var(--fs-meta); color: var(--ink); }
|
|
925
|
+
.doc-list { margin: 0 0 10px; padding-left: 20px; }
|
|
926
|
+
.doc-list li { margin-bottom: 4px; }
|
|
927
|
+
.doc-code, .component-sig { font-family: var(--mono); font-size: var(--fs-meta); line-height: 1.55; background: var(--surface);
|
|
928
|
+
border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; overflow-x: auto; white-space: pre; }
|
|
929
|
+
.doc-quote { margin: 0 0 10px; padding: 6px 10px; border-left: 3px solid var(--line); color: var(--ink-2); }
|
|
930
|
+
.dep-edges { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
931
|
+
.dep-edge { display: flex; align-items: center; gap: 8px; font-size: var(--fs-body); padding: 6px 10px;
|
|
932
|
+
border: 1px solid var(--line); border-radius: 8px; background: var(--paper); }
|
|
933
|
+
.dep-edge.dep-violation { border-color: var(--drift); background: var(--drift-bg); }
|
|
934
|
+
.dep-count { font-size: var(--fs-meta); color: var(--muted); }
|
|
935
|
+
.dep-violations { margin-top: 6px; }
|
|
936
|
+
.dep-violation-list { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
937
|
+
.dep-violation-item { font-size: var(--fs-meta); padding: 6px 10px; border: 1px solid var(--drift); border-radius: 8px;
|
|
938
|
+
background: var(--drift-bg); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
939
|
+
.dep-advisory { font-size: var(--fs-meta); color: var(--muted); margin: 8px 0 0; max-width: 75ch; }
|
|
940
|
+
.layer-map { display: flex; flex-wrap: wrap; gap: 14px; }
|
|
941
|
+
.layer-box { flex: 1 1 220px; border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; background: var(--paper); }
|
|
942
|
+
.layer-box.layer-empty { opacity: .55; border-style: dashed; }
|
|
943
|
+
.layer-box h4 { margin: 0 0 4px; font-size: var(--fs-body); font-weight: 650; text-transform: none; letter-spacing: 0;
|
|
944
|
+
color: var(--ink); display: flex; align-items: center; gap: 6px; }
|
|
945
|
+
.layer-desc { font-size: var(--fs-meta); color: var(--muted); margin: 0 0 8px; }
|
|
946
|
+
.layer-files, .feature-tree, .component-used-in { list-style: none; margin: 0; padding: 0;
|
|
947
|
+
font-size: var(--fs-meta); font-family: var(--mono); display: flex; flex-direction: column; gap: 3px;
|
|
948
|
+
max-height: 220px; overflow-y: auto; }
|
|
949
|
+
.layer-files li, .feature-tree li { display: flex; align-items: center; gap: 6px; }
|
|
950
|
+
.layer-others { margin-top: 14px; }
|
|
951
|
+
|
|
952
|
+
/* --- system context (§3.2: C4 level 1 as clean CSS boxes) --- */
|
|
953
|
+
.ctx-diagram { display: flex; flex-direction: column; align-items: center; margin: 12px 0 16px; }
|
|
954
|
+
.ctx-app { padding: 10px 22px; border: 1.5px solid var(--ink-2); border-radius: 10px;
|
|
955
|
+
background: var(--surface); font-weight: 650; font-size: var(--fs-body); }
|
|
956
|
+
.ctx-nodes { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px; margin-top: 24px; }
|
|
957
|
+
.ctx-node { position: relative; flex: 1 1 150px; max-width: 210px; border: 1px solid var(--line);
|
|
958
|
+
border-radius: 10px; padding: 10px 12px; background: var(--paper); }
|
|
959
|
+
.ctx-node::before { content: ""; position: absolute; top: -24px; left: 50%; width: 1px; height: 24px;
|
|
960
|
+
background: var(--line); }
|
|
961
|
+
.ctx-node h5 { margin: 0 0 4px; font-size: var(--fs-body); font-weight: 650; }
|
|
962
|
+
.ctx-node p { margin: 0; font-size: var(--fs-meta); color: var(--muted); }
|
|
963
|
+
|
|
964
|
+
/* --- specs RTM (§3.5: the QA lead's traceability matrix) --- */
|
|
965
|
+
.rtm-counts { font-size: var(--fs-meta); color: var(--muted); margin: 2px 0 8px; }
|
|
966
|
+
.rtm-table td { vertical-align: top; }
|
|
967
|
+
.rtm-tests { list-style: none; margin: 0; padding: 0; font-family: var(--mono); font-size: 10.5px;
|
|
968
|
+
display: flex; flex-direction: column; gap: 2px; white-space: nowrap; }
|
|
969
|
+
.rtm-gate { font-family: var(--mono); font-size: 10.5px; white-space: nowrap; }
|
|
970
|
+
.rtm-defect { color: var(--drift); font-weight: 600; font-size: var(--fs-meta); white-space: nowrap; }
|
|
971
|
+
.rtm-defect-list { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
972
|
+
.rtm-defect-item { font-size: var(--fs-meta); padding: 6px 10px; border: 1px solid var(--drift);
|
|
973
|
+
border-radius: 8px; background: var(--drift-bg); display: flex; align-items: center;
|
|
974
|
+
gap: 8px; flex-wrap: wrap; }
|
|
975
|
+
|
|
976
|
+
/* --- evidence (§3.6: the SDET's release-readiness report) --- */
|
|
977
|
+
.evidence-headline { border: 1px solid var(--line); border-radius: 14px; padding: 16px 18px; margin: 4px 0 16px; }
|
|
978
|
+
.evidence-headline.evidence-stale { border-color: var(--drift); }
|
|
979
|
+
.evidence-verdict { font-size: var(--fs-title); font-weight: 700; margin-right: 8px; }
|
|
980
|
+
.verdict-pass { color: var(--signed); }
|
|
981
|
+
.verdict-fail { color: var(--drift); }
|
|
982
|
+
.verdict-muted { color: var(--muted); }
|
|
983
|
+
.evidence-facts { list-style: none; display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 10px 0 0;
|
|
984
|
+
padding: 0; font-size: var(--fs-meta); color: var(--ink-2); }
|
|
985
|
+
.evidence-binding-stale { color: var(--drift); font-weight: 600; }
|
|
986
|
+
.step-table .step-reason { white-space: pre-wrap; font-size: var(--fs-meta); color: var(--ink-2); }
|
|
987
|
+
/* The tally leads the Detail cell — it is the one fact present on a green
|
|
988
|
+
row, where reason and note are both empty. */
|
|
989
|
+
.step-table .step-counts { display: block; font-size: var(--fs-meta); color: var(--ink-2);
|
|
990
|
+
font-variant-numeric: tabular-nums; }
|
|
991
|
+
.step-table .step-failed { color: var(--drift); }
|
|
992
|
+
.step-table .step-note { display: block; font-size: var(--fs-meta); color: var(--muted); font-style: italic; }
|
|
993
|
+
.step-verdict-pass { color: var(--signed); font-weight: 650; }
|
|
994
|
+
.step-verdict-fail { color: var(--drift); font-weight: 650; }
|
|
995
|
+
.step-verdict-skip { color: var(--muted); font-weight: 650; }
|
|
996
|
+
.step-verdict-error { color: var(--reopen); font-weight: 600; text-decoration: underline dotted; }
|
|
997
|
+
.evidence-timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
998
|
+
.evidence-timeline li { display: flex; gap: 10px; align-items: baseline; font-size: var(--fs-body);
|
|
999
|
+
padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; }
|
|
1000
|
+
|
|
1001
|
+
/* --- components (§3.3: the platform engineer's library reference) --- */
|
|
1002
|
+
.component-list { display: flex; flex-direction: column; }
|
|
1003
|
+
.component-entry { padding: 24px 0 28px; border-bottom: 1px solid var(--line); }
|
|
1004
|
+
.component-entry:first-child { padding-top: 4px; }
|
|
1005
|
+
.component-entry:last-child { border-bottom: none; }
|
|
1006
|
+
.component-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 0 0 2px; }
|
|
1007
|
+
.component-head h3 { margin: 0; }
|
|
1008
|
+
.component-file { margin: 2px 0 12px; }
|
|
1009
|
+
.params-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); margin: 4px 0 12px; }
|
|
1010
|
+
.params-table th, .params-table td { padding: 7px 9px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
|
1011
|
+
.params-table th { color: var(--muted); font-weight: 600; font-size: var(--fs-meta); }
|
|
1012
|
+
.param-required { color: var(--muted); font-size: var(--fs-meta); }
|
|
1013
|
+
.param-note { font-size: var(--fs-meta); color: var(--ink-2); }
|
|
1014
|
+
.component-facts { list-style: none; margin: 0 0 10px; padding: 0; font-size: var(--fs-meta); display: flex; flex-direction: column; gap: 4px; }
|
|
1015
|
+
.component-facts li { padding-left: 14px; position: relative; }
|
|
1016
|
+
.component-facts li::before { content: "\\2022"; position: absolute; left: 0; color: var(--muted); }
|
|
1017
|
+
.component-kdoc { margin: 4px 0 10px; padding: 8px 10px; border-left: 3px solid var(--line); font-size: var(--fs-meta);
|
|
1018
|
+
color: var(--ink-2); white-space: pre-wrap; }
|
|
1019
|
+
.component-story { margin-bottom: 10px; }
|
|
1020
|
+
.component-story .chg { color: var(--accent); font-weight: 600; }
|
|
1021
|
+
.component-story img { width: 180px; max-width: 100%; border: 1px solid var(--line); border-radius: 6px; display: block; }
|
|
1022
|
+
.component-live-variants { margin-bottom: 10px; }
|
|
1023
|
+
.state-variant-block { margin-bottom: 8px; }
|
|
1024
|
+
.state-variant-thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
1025
|
+
.state-variant-thumb { width: 90px; }
|
|
1026
|
+
.state-variant-thumb img { width: 100%; border: 1px solid var(--line); border-radius: 6px; display: block; }
|
|
1027
|
+
.violation-chip { margin-left: 4px; }
|
|
1028
|
+
|
|
1029
|
+
/* --- comments (ledger table + the margin-quiet affordance) --- */
|
|
1030
|
+
.comment-text-cell { max-width: 320px; white-space: pre-wrap; }
|
|
1031
|
+
.comment-resolution { margin-top: 4px; }
|
|
1032
|
+
.comment-resolution-note { font-size: var(--fs-meta); color: var(--ink-2); margin: 2px 0 0; }
|
|
1033
|
+
/* §2: comments are a QUIET affordance revealed on hover — no floating emoji
|
|
1034
|
+
over content. The control's emoji glyph is suppressed (font-size: 0) and
|
|
1035
|
+
replaced with a small outlined "+"; it stays invisible until its host
|
|
1036
|
+
element is hovered (or the button itself is keyboard-focused). */
|
|
1037
|
+
.comment-ctl { position: relative; display: inline-block; margin-left: 4px; vertical-align: middle; }
|
|
1038
|
+
.comment-btn { appearance: none; border: 1px solid var(--line); background: var(--paper); cursor: pointer;
|
|
1039
|
+
font-size: 0; width: 16px; height: 16px; line-height: 1; border-radius: 50%; padding: 0;
|
|
1040
|
+
opacity: 0; transition: opacity .12s; position: relative; }
|
|
1041
|
+
.comment-btn::before { content: "+"; font-size: 11px; color: var(--muted);
|
|
1042
|
+
position: absolute; inset: 0; display: grid; place-items: center; }
|
|
1043
|
+
*:hover > .comment-ctl .comment-btn, .comment-ctl:hover .comment-btn,
|
|
1044
|
+
.comment-btn:focus-visible { opacity: 1; }
|
|
1045
|
+
.comment-btn:hover::before { color: var(--accent); }
|
|
1046
|
+
.comment-btn:hover { border-color: var(--accent); }
|
|
1047
|
+
.comment-popover { position: absolute; z-index: 20; top: 100%; left: 0; margin-top: 4px; width: 230px;
|
|
1048
|
+
background: var(--paper); border: 1px solid var(--line); border-radius: 10px; padding: 10px;
|
|
1049
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.14); display: flex; flex-direction: column; gap: 6px; }
|
|
1050
|
+
/* The popover and badge are toggled with the hidden ATTRIBUTE, but their author
|
|
1051
|
+
display rules (flex / inline-block) override the UA stylesheet's [hidden]
|
|
1052
|
+
{ display: none } — without these guards every "hidden" popover stays painted,
|
|
1053
|
+
and its children (the textarea especially) overflow the 0x0 box and invisibly
|
|
1054
|
+
intercept clicks (elementFromPoint-verified in the VL-7 browser gate). */
|
|
1055
|
+
.comment-popover[hidden] { display: none !important; }
|
|
1056
|
+
.comment-popover textarea, .comment-popover input { font: inherit; font-size: var(--fs-meta); padding: 6px 8px;
|
|
1057
|
+
border: 1px solid var(--line); border-radius: 8px; resize: vertical; width: 100%; box-sizing: border-box;
|
|
1058
|
+
background: var(--paper); color: inherit; }
|
|
1059
|
+
.comment-popover-actions { display: flex; justify-content: flex-end; gap: 6px; }
|
|
1060
|
+
.comment-popover-actions button { font: inherit; font-size: var(--fs-meta); padding: 4px 10px; border-radius: 6px;
|
|
1061
|
+
border: 1px solid var(--line); background: var(--surface); color: inherit; cursor: pointer; }
|
|
1062
|
+
.comment-submit { border-color: var(--accent) !important; background: var(--accent) !important; color: var(--accent-ink); }
|
|
1063
|
+
.comment-error { color: var(--drift); font-size: var(--fs-meta); margin: 0; }
|
|
1064
|
+
|
|
1065
|
+
/* --- features (the post-genesis delivery board) --- */
|
|
1066
|
+
.feature-board { display: flex; flex-direction: column; gap: 14px; }
|
|
1067
|
+
.feature-card { border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; background: var(--surface); }
|
|
1068
|
+
.feature-card-closed { opacity: 0.65; }
|
|
1069
|
+
.feature-card-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
1070
|
+
.feature-card-head h3 { margin: 0; }
|
|
1071
|
+
.feature-phase { font-size: var(--fs-meta); font-weight: 600; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
|
|
1072
|
+
.phase-proposed { color: var(--muted); }
|
|
1073
|
+
.phase-approved { color: var(--accent); border-color: var(--accent); }
|
|
1074
|
+
.phase-proven { color: var(--reopen); border-color: var(--reopen); }
|
|
1075
|
+
.feature-done-yes { color: var(--ok, #3a8f5a); font-size: var(--fs-meta); font-weight: 600; }
|
|
1076
|
+
.feature-done { margin: 4px 0; font-size: var(--fs-meta); }
|
|
1077
|
+
.feature-next { margin: 6px 0 2px; font-size: var(--fs-meta); font-weight: 600; color: var(--accent); }
|
|
1078
|
+
.feature-decisions { border-left: 3px solid var(--accent); padding: 2px 0 2px 12px; margin: 8px 0; }
|
|
1079
|
+
.feature-decisions h4 { margin: 0 0 4px; font-size: var(--fs-meta); }
|
|
1080
|
+
.feature-brief-full { margin: 8px 0 2px; font-size: var(--fs-meta); }
|
|
1081
|
+
.feature-brief-full h4 { margin: 10px 0 4px; }
|
|
1082
|
+
.feature-brief-full .doc-prose, .feature-decisions .doc-prose { font-size: var(--fs-meta); }
|
|
1083
|
+
.phase-accepted { color: var(--ok, #3a8f5a); border-color: currentColor; }
|
|
1084
|
+
.phase-drift, .phase-reopened { color: var(--drift); border-color: var(--drift); }
|
|
1085
|
+
.feature-tally { color: var(--muted); font-size: var(--fs-meta); }
|
|
1086
|
+
.feature-doc-link { margin-left: auto; color: var(--muted); font-size: var(--fs-meta); text-decoration: none; }
|
|
1087
|
+
.feature-touches { font-size: var(--fs-meta); margin: 6px 0; }
|
|
1088
|
+
.feature-as-declared { color: var(--muted); font-style: italic; }
|
|
1089
|
+
/* Check details are file paths and regexes — long, unbreakable-looking
|
|
1090
|
+
strings. Without an explicit wrap they blow the table past the card and
|
|
1091
|
+
the right-hand end (the part that says WHY a check is unmet) is clipped
|
|
1092
|
+
off-screen, which is the one thing the row exists to tell you. */
|
|
1093
|
+
.feature-checks { margin-top: 6px; table-layout: auto; width: 100%; }
|
|
1094
|
+
.feature-checks td, .feature-checks th { vertical-align: top; }
|
|
1095
|
+
.feature-check-detail { color: var(--muted); overflow-wrap: anywhere; word-break: break-word; }
|
|
1096
|
+
.pending-inline { color: var(--muted); }
|
|
1097
|
+
.feature-actions { margin-top: 10px; display: flex; gap: 8px; align-items: center; }
|
|
1098
|
+
.feature-actions button { font: inherit; font-size: var(--fs-meta); padding: 4px 12px; border-radius: 6px;
|
|
1099
|
+
border: 1px solid var(--accent); background: var(--accent); color: var(--accent-ink); cursor: pointer; }
|
|
1100
|
+
/* The guided-flow prompt: every decision ends with "do you want to …?" */
|
|
1101
|
+
.next-prompt { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
|
|
1102
|
+
max-width: min(720px, calc(100vw - 48px)); z-index: 60;
|
|
1103
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
1104
|
+
border: 1px solid var(--accent); border-radius: 12px; padding: 12px 16px;
|
|
1105
|
+
background: var(--accent-bg); color: var(--ink); font-size: var(--fs-meta);
|
|
1106
|
+
box-shadow: 0 6px 24px rgba(0,0,0,.18); }
|
|
1107
|
+
.next-prompt-did { font-weight: 650; }
|
|
1108
|
+
.next-prompt button { font: inherit; font-size: var(--fs-meta); padding: 4px 12px; border-radius: 6px; cursor: pointer; }
|
|
1109
|
+
.next-prompt-primary { border: 1px solid var(--accent); background: var(--accent); color: var(--accent-ink); }
|
|
1110
|
+
.next-prompt-dismiss { border: 1px solid var(--line); background: var(--paper); color: var(--muted); }
|
|
1111
|
+
|
|
1112
|
+
/* Sign where you read: every governed section's own signature control. */
|
|
1113
|
+
.signature-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
1114
|
+
border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px;
|
|
1115
|
+
margin: 0 0 14px; background: var(--surface); font-size: var(--fs-meta); }
|
|
1116
|
+
.signature-line { color: var(--muted); }
|
|
1117
|
+
.signature-id { color: var(--muted); }
|
|
1118
|
+
.signature-actions { margin-left: auto; display: flex; gap: 8px; }
|
|
1119
|
+
.signature-actions button { font: inherit; font-size: var(--fs-meta); padding: 4px 12px; border-radius: 6px;
|
|
1120
|
+
border: 1px solid var(--line); background: var(--paper); color: var(--ink); cursor: pointer; }
|
|
1121
|
+
.signature-actions button:hover { border-color: var(--accent); color: var(--accent); }
|
|
1122
|
+
.spec-file .signature-bar { margin-top: 6px; }
|
|
1123
|
+
|
|
1124
|
+
/* The change surface: what changed vs. what is still exactly as signed —
|
|
1125
|
+
rendered at the top of the drifted artifact's own section AND inside the
|
|
1126
|
+
Approvals table (driftPanelHtml — one renderer, everywhere). */
|
|
1127
|
+
.drift-panel { border: 1px solid var(--drift); border-radius: 10px; padding: 12px 14px; margin-bottom: 14px;
|
|
1128
|
+
background: var(--drift-bg); font-size: var(--fs-meta); }
|
|
1129
|
+
.drift-panel .drift-head { margin: 0 0 6px; }
|
|
1130
|
+
.drift-summary { margin: 6px 0 4px; }
|
|
1131
|
+
.drift-files { list-style: none; margin: 0 0 6px; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
1132
|
+
.drift-files code, .drift-still-signed code { overflow-wrap: anywhere; }
|
|
1133
|
+
.drift-still-signed { margin: 4px 0; }
|
|
1134
|
+
.drift-still-signed ul { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
1135
|
+
.drift-diff { margin: 6px 0 0; }
|
|
1136
|
+
.approvals-table .drift-panel { margin-bottom: 0; }
|
|
1137
|
+
.feature-undeclared { border: 1px solid var(--drift); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px;
|
|
1138
|
+
font-size: var(--fs-meta); }
|
|
1139
|
+
`;
|