executable-stories-formatters 0.15.0 → 0.16.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.
Files changed (42) hide show
  1. package/dist/cli.js +238 -64
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.cjs +197 -55
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +11 -1
  6. package/dist/index.d.ts +11 -1
  7. package/dist/index.js +197 -55
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -2
  10. package/schemas/README.md +1 -1
  11. package/templates/astro-starlight/astro.config.mjs +57 -0
  12. package/templates/astro-starlight/gitignore +14 -0
  13. package/templates/astro-starlight/package.json +20 -0
  14. package/templates/astro-starlight/public/stories/assets/.gitkeep +0 -0
  15. package/templates/astro-starlight/public/stories/notes-index.json +4 -0
  16. package/templates/astro-starlight/public/stories/story-report.json +17 -0
  17. package/templates/astro-starlight/src/components/ApiOperations.astro +366 -0
  18. package/templates/astro-starlight/src/components/Checklist.astro +15 -0
  19. package/templates/astro-starlight/src/components/HealthDashboard.astro +171 -0
  20. package/templates/astro-starlight/src/components/PageTitle.astro +53 -0
  21. package/templates/astro-starlight/src/components/VerifiedBy.astro +281 -0
  22. package/templates/astro-starlight/src/components/VerifiedStep.astro +91 -0
  23. package/templates/astro-starlight/src/content/docs/examples/example-adr.mdx +45 -0
  24. package/templates/astro-starlight/src/content/docs/guides/behavior-portal.mdx +41 -0
  25. package/templates/astro-starlight/src/content/docs/guides/writing-docs.mdx +49 -0
  26. package/templates/astro-starlight/src/content/docs/index.mdx +49 -0
  27. package/templates/astro-starlight/src/content/docs/stories/.gitkeep +0 -0
  28. package/templates/astro-starlight/src/content.config.ts +18 -0
  29. package/templates/astro-starlight/src/lib/config.ts +50 -0
  30. package/templates/astro-starlight/src/lib/render-doc-entry.ts +154 -0
  31. package/templates/astro-starlight/src/lib/report-health.ts +61 -0
  32. package/templates/astro-starlight/src/lib/verification.ts +247 -0
  33. package/templates/astro-starlight/src/pages/explorer/explorer.css +729 -0
  34. package/templates/astro-starlight/src/pages/explorer/index.astro +404 -0
  35. package/templates/astro-starlight/src/styles/global.css +293 -0
  36. package/templates/astro-starlight/src/styles/themes/corporate.css +83 -0
  37. package/templates/astro-starlight/src/styles/themes/dashboard.css +76 -0
  38. package/templates/astro-starlight/src/styles/themes/default.css +86 -0
  39. package/templates/astro-starlight/src/styles/themes/minimal.css +87 -0
  40. package/templates/astro-starlight/src/styles/themes/playful.css +77 -0
  41. package/templates/astro-starlight/src/styles/themes/terminal.css +77 -0
  42. package/templates/astro-starlight/tsconfig.json +13 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "executable-stories-formatters",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Cucumber-compatible report formats (HTML, Markdown, JUnit XML, Cucumber JSON) for executable-stories test results.",
5
5
  "author": "Jag Reehal <jag@jagreehal.com>",
6
6
  "license": "MIT",
@@ -36,7 +36,8 @@
36
36
  "files": [
37
37
  "dist",
38
38
  "skills",
39
- "schemas"
39
+ "schemas",
40
+ "templates"
40
41
  ],
41
42
  "engines": {
42
43
  "node": ">=22"
package/schemas/README.md CHANGED
@@ -172,7 +172,7 @@ SUBCOMMANDS
172
172
  | `--format <formats>` | `html` | Comma-separated: `html`, `markdown`, `junit`, `cucumber-json`, `cucumber-html`, `cucumber-messages` |
173
173
  | `--input-type <type>` | `raw` | Input type: `raw`, `canonical`, or `ndjson` |
174
174
  | `--output-dir <dir>` | `reports` | Output directory |
175
- | `--output-name <name>` | `test-results` | Base filename |
175
+ | `--output-name <name>` | `index` | Base filename |
176
176
  | `--synthesize-stories` | on | Synthesize story metadata for plain test results |
177
177
  | `--no-synthesize-stories` | | Disable story synthesis (strict mode) |
178
178
  | `--html-title <title>` | `Test Results` | HTML report title |
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+ import starlight from '@astrojs/starlight';
3
+ import astroMermaid from 'astro-mermaid';
4
+ import remarkGfm from 'remark-gfm';
5
+ import { defineConfig } from 'astro/config';
6
+
7
+ /*
8
+ * Available themes (match HTML formatter themes):
9
+ * default — cucumber green, IBM Plex typography
10
+ * corporate — navy, Playfair Display serif headings
11
+ * terminal — green-on-dark, JetBrains Mono, flat
12
+ * minimal — teal, Noto Serif Display headings, warm neutrals
13
+ * dashboard — blue, DM Sans + JetBrains Mono
14
+ * playful — coral, rounded corners, Source Sans 3
15
+ *
16
+ * To switch themes, replace `themes/default.css` below with the filename
17
+ * of the theme you want (e.g. `./src/styles/themes/corporate.css`).
18
+ */
19
+
20
+ export default defineConfig({
21
+ // GFM tables don't render in .mdx by default — enable them so hand-written
22
+ // and generated .mdx pages (ADRs, API coverage, decision logs) show tables.
23
+ markdown: {
24
+ remarkPlugins: [remarkGfm],
25
+ },
26
+ integrations: [
27
+ astroMermaid(),
28
+ starlight({
29
+ title: 'Story Docs',
30
+ description: 'Living documentation generated from executable stories.',
31
+ // Renders a live verification badge under the title of any page that
32
+ // declares `verifiedBy` in its frontmatter.
33
+ components: {
34
+ PageTitle: './src/components/PageTitle.astro',
35
+ },
36
+ sidebar: [
37
+ { label: 'Home', slug: 'index' },
38
+ { label: 'Explorer', link: '/explorer/' },
39
+ // Hand-written content — edited by the team, never overwritten by a regenerate.
40
+ { label: 'Guides', autogenerate: { directory: 'guides' } },
41
+ { label: 'Notes', autogenerate: { directory: 'notes' } },
42
+ // Autogenerated, not a hard-coded slug — clearing the sample ADR (or the
43
+ // whole directory) leaves the build green instead of 404ing on a missing slug.
44
+ { label: 'Examples', autogenerate: { directory: 'examples' } },
45
+ // Generated from tests by `format --format astro`.
46
+ {
47
+ label: 'Stories',
48
+ autogenerate: { directory: 'stories' },
49
+ },
50
+ ],
51
+ customCss: [
52
+ './src/styles/global.css',
53
+ './src/styles/themes/default.css',
54
+ ],
55
+ }),
56
+ ],
57
+ });
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ .astro/
3
+ dist/
4
+
5
+ # Machine-owned portal output. Regenerated by build-docs / CI.
6
+ src/content/docs/stories/*
7
+ !src/content/docs/stories/.gitkeep
8
+ public/stories/assets/*
9
+ !public/stories/assets/.gitkeep
10
+ public/stories/story-report.json
11
+ public/stories/scenario-links.json
12
+ public/stories/notes-index.json
13
+ !public/stories/notes-index.json
14
+ public/stories/changes.json
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "story-docs",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "private": true,
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro build",
9
+ "preview": "astro preview"
10
+ },
11
+ "dependencies": {
12
+ "@astrojs/starlight": "^0.38.3",
13
+ "astro": "^6.1.8",
14
+ "astro-mermaid": "^2.0.1",
15
+ "highlight.js": "^11.10.0",
16
+ "marked": "^12.0.0",
17
+ "mermaid": "^11.14.0",
18
+ "remark-gfm": "^4.0.0"
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "notes": []
4
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "runId": "empty",
4
+ "startedAtMs": 0,
5
+ "finishedAtMs": 0,
6
+ "durationMs": 0,
7
+ "projectRoot": "",
8
+ "summary": {
9
+ "total": 0,
10
+ "passed": 0,
11
+ "failed": 0,
12
+ "skipped": 0,
13
+ "pending": 0,
14
+ "durationMs": 0
15
+ },
16
+ "features": []
17
+ }
@@ -0,0 +1,366 @@
1
+ ---
2
+ /**
3
+ * <ApiOperations tag="Transfers" hasRun endpoints={[...]} summary={{...}} />
4
+ *
5
+ * Renders the API coverage table for one OpenAPI tag. Generated by
6
+ * `executable-stories import-openapi`, which writes an MDX page that imports
7
+ * this component and passes the endpoints + their test coverage.
8
+ *
9
+ * Each endpoint shows an HTTP-method pill, its path, a coverage badge, and the
10
+ * stories that exercise it — linked straight into the Scenario Explorer, so the
11
+ * API docs can never drift from the tests that prove them.
12
+ */
13
+ import { explorerUrl } from "../lib/config";
14
+
15
+ type CoverageStatus = "covered" | "failing" | "uncovered";
16
+
17
+ export interface ApiStoryRef {
18
+ id: string;
19
+ title: string;
20
+ status: string;
21
+ }
22
+
23
+ export interface ApiEndpoint {
24
+ method: string;
25
+ path: string;
26
+ summary: string;
27
+ status: CoverageStatus;
28
+ stories: ApiStoryRef[];
29
+ }
30
+
31
+ interface Props {
32
+ tag: string;
33
+ hasRun: boolean;
34
+ endpoints: ApiEndpoint[];
35
+ summary: { total: number; covered: number; failing: number; uncovered: number };
36
+ }
37
+
38
+ const { hasRun, endpoints, summary } = Astro.props;
39
+
40
+ const coverageLabel: Record<CoverageStatus, string> = {
41
+ covered: "Covered",
42
+ failing: "Failing",
43
+ uncovered: "Uncovered",
44
+ };
45
+
46
+ const methodClass = (method: string) => `m-${method.toLowerCase()}`;
47
+ ---
48
+
49
+ <section class="api-ops">
50
+ <header class="api-ops__bar">
51
+ <p class="api-ops__lede">
52
+ Generated from the OpenAPI spec.{" "}
53
+ {hasRun
54
+ ? "Coverage reflects the last test run."
55
+ : "Re-run with --run <story-report.json> to show per-endpoint coverage."}
56
+ </p>
57
+ {
58
+ hasRun && (
59
+ <div class="api-ops__stats" role="group" aria-label="Coverage summary">
60
+ <span class="stat stat--covered">
61
+ <strong>{summary.covered}</strong> covered
62
+ </span>
63
+ {summary.failing > 0 && (
64
+ <span class="stat stat--failing">
65
+ <strong>{summary.failing}</strong> failing
66
+ </span>
67
+ )}
68
+ <span class="stat stat--uncovered">
69
+ <strong>{summary.uncovered}</strong> no test
70
+ </span>
71
+ <span class="stat stat--total">of {summary.total}</span>
72
+ </div>
73
+ )
74
+ }
75
+ </header>
76
+
77
+ <div class="api-ops__rows" role="table" aria-label="API endpoints">
78
+ <div class="api-ops__row api-ops__row--head" role="row">
79
+ <span role="columnheader">Endpoint</span>
80
+ <span role="columnheader">Summary</span>
81
+ {hasRun && <span role="columnheader">Coverage</span>}
82
+ {hasRun && <span role="columnheader">Verified by</span>}
83
+ </div>
84
+ {
85
+ endpoints.map((ep) => (
86
+ <div class="api-ops__row" role="row">
87
+ <span class="cell-endpoint" role="cell">
88
+ <span class:list={["method", methodClass(ep.method)]}>{ep.method}</span>
89
+ <code class="path">{ep.path}</code>
90
+ </span>
91
+ <span class="cell-summary" role="cell">{ep.summary || "—"}</span>
92
+ {hasRun && (
93
+ <span role="cell">
94
+ <span class:list={["coverage", `coverage--${ep.status}`]}>
95
+ <span class="coverage__dot" aria-hidden="true" />
96
+ {coverageLabel[ep.status]}
97
+ </span>
98
+ </span>
99
+ )}
100
+ {hasRun && (
101
+ <span class="cell-stories" role="cell">
102
+ {ep.stories.length === 0 ? (
103
+ <span class="no-stories">—</span>
104
+ ) : (
105
+ ep.stories.map((s) => (
106
+ <a class:list={["story-chip", `story-chip--${s.status}`]} href={explorerUrl(s.id)}>
107
+ <span class="story-chip__dot" aria-hidden="true" />
108
+ {s.title}
109
+ </a>
110
+ ))
111
+ )}
112
+ </span>
113
+ )}
114
+ </div>
115
+ ))
116
+ }
117
+ </div>
118
+ </section>
119
+
120
+ <style>
121
+ .api-ops {
122
+ margin: 1.25rem 0 2rem;
123
+ }
124
+
125
+ .api-ops__bar {
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ align-items: center;
129
+ justify-content: space-between;
130
+ gap: 0.75rem 1.25rem;
131
+ margin-bottom: 1rem;
132
+ }
133
+
134
+ .api-ops__lede {
135
+ margin: 0;
136
+ color: var(--es-text-muted);
137
+ font-size: var(--sl-text-sm);
138
+ max-width: 52ch;
139
+ }
140
+
141
+ .api-ops__stats {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.4rem;
145
+ flex-wrap: wrap;
146
+ }
147
+
148
+ .stat {
149
+ display: inline-flex;
150
+ align-items: baseline;
151
+ gap: 0.3rem;
152
+ font-size: var(--sl-text-xs);
153
+ color: var(--es-text-muted);
154
+ padding: 0.25rem 0.6rem;
155
+ border: 1px solid var(--es-line);
156
+ border-radius: var(--es-radius-pill);
157
+ background: var(--es-surface);
158
+ }
159
+ .stat strong {
160
+ font-size: var(--sl-text-sm);
161
+ color: var(--es-text);
162
+ }
163
+ .stat--covered {
164
+ color: var(--es-passed);
165
+ border-color: color-mix(in oklab, var(--es-passed) 40%, var(--es-line));
166
+ background: var(--es-passed-soft);
167
+ }
168
+ .stat--covered strong {
169
+ color: var(--es-passed);
170
+ }
171
+ .stat--failing {
172
+ color: var(--es-failed);
173
+ border-color: color-mix(in oklab, var(--es-failed) 40%, var(--es-line));
174
+ background: var(--es-failed-soft);
175
+ }
176
+ .stat--failing strong {
177
+ color: var(--es-failed);
178
+ }
179
+ .stat--uncovered {
180
+ color: var(--es-skipped);
181
+ border-color: color-mix(in oklab, var(--es-skipped) 40%, var(--es-line));
182
+ background: var(--es-skipped-soft);
183
+ }
184
+ .stat--uncovered strong {
185
+ color: var(--es-skipped);
186
+ }
187
+ .stat--total {
188
+ border: none;
189
+ background: none;
190
+ }
191
+
192
+ .api-ops__rows {
193
+ border: 1px solid var(--es-line);
194
+ border-radius: var(--es-radius);
195
+ overflow: hidden;
196
+ background: var(--es-bg);
197
+ }
198
+
199
+ .api-ops__row {
200
+ display: grid;
201
+ grid-template-columns: minmax(220px, 1.2fr) minmax(140px, 1.4fr) auto minmax(160px, 1.3fr);
202
+ gap: 1rem;
203
+ align-items: center;
204
+ padding: 0.7rem 1rem;
205
+ border-top: 1px solid var(--es-line);
206
+ }
207
+ .api-ops__row:first-child {
208
+ border-top: none;
209
+ }
210
+ .api-ops__row:not(.api-ops__row--head):hover {
211
+ background: var(--es-surface);
212
+ }
213
+
214
+ .api-ops__row--head {
215
+ background: var(--es-surface-2);
216
+ font-size: var(--sl-text-xs);
217
+ font-weight: 600;
218
+ text-transform: uppercase;
219
+ letter-spacing: 0.06em;
220
+ color: var(--es-text-muted);
221
+ }
222
+
223
+ .cell-endpoint {
224
+ display: inline-flex;
225
+ align-items: center;
226
+ gap: 0.6rem;
227
+ min-width: 0;
228
+ }
229
+
230
+ .method {
231
+ flex: none;
232
+ font-family: var(--es-mono);
233
+ font-size: 0.7rem;
234
+ font-weight: 700;
235
+ letter-spacing: 0.04em;
236
+ padding: 0.2rem 0.45rem;
237
+ border-radius: var(--es-radius-sm);
238
+ color: #fff;
239
+ background: var(--es-method-default);
240
+ }
241
+ .m-get {
242
+ background: var(--es-method-get);
243
+ }
244
+ .m-post {
245
+ background: var(--es-method-post);
246
+ }
247
+ .m-put {
248
+ background: var(--es-method-put);
249
+ }
250
+ .m-patch {
251
+ background: var(--es-method-patch);
252
+ }
253
+ .m-delete {
254
+ background: var(--es-method-delete);
255
+ }
256
+
257
+ .path {
258
+ font-family: var(--es-mono);
259
+ font-size: var(--sl-text-sm);
260
+ color: var(--es-text);
261
+ background: none;
262
+ border: none;
263
+ padding: 0;
264
+ overflow: hidden;
265
+ text-overflow: ellipsis;
266
+ white-space: nowrap;
267
+ }
268
+
269
+ .cell-summary {
270
+ color: var(--es-text-soft);
271
+ font-size: var(--sl-text-sm);
272
+ }
273
+
274
+ .coverage {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ gap: 0.4rem;
278
+ font-size: var(--sl-text-xs);
279
+ font-weight: 600;
280
+ padding: 0.2rem 0.6rem;
281
+ border-radius: var(--es-radius-pill);
282
+ border: 1px solid currentcolor;
283
+ white-space: nowrap;
284
+ }
285
+ .coverage__dot {
286
+ width: 0.5rem;
287
+ height: 0.5rem;
288
+ border-radius: 50%;
289
+ background: currentcolor;
290
+ }
291
+ .coverage--covered {
292
+ color: var(--es-passed);
293
+ background: var(--es-passed-soft);
294
+ }
295
+ .coverage--failing {
296
+ color: var(--es-failed);
297
+ background: var(--es-failed-soft);
298
+ }
299
+ .coverage--uncovered {
300
+ color: var(--es-skipped);
301
+ background: var(--es-skipped-soft);
302
+ }
303
+
304
+ .cell-stories {
305
+ display: flex;
306
+ flex-wrap: wrap;
307
+ gap: 0.35rem;
308
+ }
309
+
310
+ .story-chip {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ gap: 0.35rem;
314
+ font-size: var(--sl-text-xs);
315
+ text-decoration: none;
316
+ color: var(--es-text-soft);
317
+ padding: 0.2rem 0.5rem;
318
+ border: 1px solid var(--es-line);
319
+ border-radius: var(--es-radius-pill);
320
+ background: var(--es-surface);
321
+ transition: border-color 120ms ease, color 120ms ease;
322
+ }
323
+ .story-chip:hover {
324
+ border-color: var(--es-accent);
325
+ color: var(--es-text);
326
+ }
327
+ .story-chip:focus-visible {
328
+ outline: 2px solid var(--es-accent);
329
+ outline-offset: 2px;
330
+ }
331
+ .story-chip__dot {
332
+ width: 0.45rem;
333
+ height: 0.45rem;
334
+ border-radius: 50%;
335
+ background: var(--chip-dot, var(--es-text-muted));
336
+ }
337
+ .story-chip--passed {
338
+ --chip-dot: var(--es-passed);
339
+ }
340
+ .story-chip--failed {
341
+ --chip-dot: var(--es-failed);
342
+ }
343
+ .story-chip--skipped {
344
+ --chip-dot: var(--es-skipped);
345
+ }
346
+ .story-chip--pending {
347
+ --chip-dot: var(--es-pending);
348
+ }
349
+
350
+ .no-stories {
351
+ color: var(--es-text-muted);
352
+ }
353
+
354
+ @media (max-width: 720px) {
355
+ .api-ops__row {
356
+ grid-template-columns: 1fr;
357
+ gap: 0.5rem;
358
+ }
359
+ .api-ops__row--head {
360
+ display: none;
361
+ }
362
+ .cell-summary {
363
+ color: var(--es-text-muted);
364
+ }
365
+ }
366
+ </style>
@@ -0,0 +1,15 @@
1
+ ---
2
+ /**
3
+ * <Checklist> wraps a set of <VerifiedStep> items into a clean runbook list.
4
+ */
5
+ ---
6
+
7
+ <ul class="checklist"><slot /></ul>
8
+
9
+ <style>
10
+ .checklist {
11
+ margin: 1rem 0;
12
+ padding: 0;
13
+ border-top: 1px solid var(--sl-color-gray-6);
14
+ }
15
+ </style>
@@ -0,0 +1,171 @@
1
+ ---
2
+ /**
3
+ * <HealthDashboard /> — a living landing widget.
4
+ *
5
+ * Reads the latest run at build time and shows pass rate, counts, freshness,
6
+ * and any failing stories. Drop it at the top of the home page so the first
7
+ * thing a stakeholder sees is whether the documented system is green.
8
+ */
9
+ import { report } from "../lib/config";
10
+ import { summarizeHealth, type FailingScenario } from "../lib/report-health";
11
+
12
+ const health = summarizeHealth(report);
13
+ const passPct = Math.round(health.passRate * 100);
14
+ const lastRun = health.lastRunMs
15
+ ? new Date(health.lastRunMs).toLocaleString(undefined, {
16
+ dateStyle: "medium",
17
+ timeStyle: "short",
18
+ })
19
+ : null;
20
+
21
+ const overall = health.empty
22
+ ? "empty"
23
+ : health.failed > 0
24
+ ? "failing"
25
+ : "healthy";
26
+
27
+ const metrics: Array<{ label: string; value: string | number }> = [
28
+ { label: "Pass rate", value: health.empty ? "—" : `${passPct}%` },
29
+ { label: "Scenarios", value: health.total },
30
+ { label: "Passed", value: health.passed },
31
+ { label: "Failed", value: health.failed },
32
+ ];
33
+
34
+ const failing: FailingScenario[] = health.failing;
35
+ ---
36
+
37
+ <section class:list={["health", `health--${overall}`]} aria-label="Test health dashboard">
38
+ <header class="health__head">
39
+ <span class="health__dot" aria-hidden="true"></span>
40
+ <strong class="health__title">
41
+ {health.empty ? "No test run yet" : health.failed > 0 ? "Attention needed" : "All systems documented & passing"}
42
+ </strong>
43
+ {lastRun && <span class="health__meta">last run {lastRun}</span>}
44
+ </header>
45
+
46
+ <div class="health__metrics">
47
+ {
48
+ metrics.map((m) => (
49
+ <div class="health__metric">
50
+ <span class="health__metric-value">{m.value}</span>
51
+ <span class="health__metric-label">{m.label}</span>
52
+ </div>
53
+ ))
54
+ }
55
+ </div>
56
+
57
+ {
58
+ health.empty && (
59
+ <p class="health__hint">
60
+ Generate a run with <code>executable-stories build-docs reports/raw-run.json --site-dir .</code> to light up this dashboard.
61
+ </p>
62
+ )
63
+ }
64
+
65
+ {
66
+ failing.length > 0 && (
67
+ <details class="health__failing" open>
68
+ <summary>{failing.length} failing {failing.length === 1 ? "story" : "stories"}</summary>
69
+ <ul>
70
+ {failing.map((f) => (
71
+ <li>
72
+ <span class="health__failing-title">{f.title}</span>
73
+ {f.feature && <span class="health__failing-feature"> · {f.feature}</span>}
74
+ </li>
75
+ ))}
76
+ </ul>
77
+ </details>
78
+ )
79
+ }
80
+ </section>
81
+
82
+ <style>
83
+ .health {
84
+ --health-accent: var(--sl-color-gray-3);
85
+ border: 1px solid var(--sl-color-gray-5);
86
+ border-radius: 0.75rem;
87
+ padding: 1.25rem 1.5rem;
88
+ margin: 0 0 2rem;
89
+ background: var(--sl-color-gray-7, var(--sl-color-gray-6));
90
+ }
91
+ .health--healthy {
92
+ --health-accent: #16a34a;
93
+ }
94
+ .health--failing {
95
+ --health-accent: #dc2626;
96
+ }
97
+ .health--empty {
98
+ --health-accent: #d97706;
99
+ }
100
+ .health__head {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 0.6rem;
104
+ margin-bottom: 1rem;
105
+ }
106
+ .health__dot {
107
+ width: 0.7rem;
108
+ height: 0.7rem;
109
+ border-radius: 999px;
110
+ background: var(--health-accent);
111
+ flex: none;
112
+ }
113
+ .health__title {
114
+ font-size: var(--sl-text-lg);
115
+ }
116
+ .health__meta {
117
+ margin-left: auto;
118
+ color: var(--sl-color-gray-3);
119
+ font-size: var(--sl-text-xs);
120
+ }
121
+ .health__metrics {
122
+ display: grid;
123
+ grid-template-columns: repeat(4, minmax(0, 1fr));
124
+ gap: 0.75rem;
125
+ }
126
+ .health__metric {
127
+ border: 1px solid var(--sl-color-gray-5);
128
+ border-radius: 0.5rem;
129
+ padding: 0.75rem;
130
+ text-align: center;
131
+ background: var(--sl-color-black, transparent);
132
+ }
133
+ .health__metric-value {
134
+ display: block;
135
+ font-size: var(--sl-text-2xl);
136
+ font-weight: 700;
137
+ line-height: 1;
138
+ }
139
+ .health__metric-label {
140
+ display: block;
141
+ margin-top: 0.35rem;
142
+ font-size: var(--sl-text-xs);
143
+ color: var(--sl-color-gray-3);
144
+ }
145
+ .health__hint {
146
+ margin: 1rem 0 0;
147
+ font-size: var(--sl-text-sm);
148
+ color: var(--sl-color-gray-2);
149
+ }
150
+ .health__failing {
151
+ margin-top: 1rem;
152
+ }
153
+ .health__failing > summary {
154
+ cursor: pointer;
155
+ color: #dc2626;
156
+ font-weight: 600;
157
+ }
158
+ .health__failing ul {
159
+ margin: 0.5rem 0 0;
160
+ padding-left: 1.1rem;
161
+ }
162
+ .health__failing-feature {
163
+ color: var(--sl-color-gray-3);
164
+ font-size: var(--sl-text-sm);
165
+ }
166
+ @media (max-width: 640px) {
167
+ .health__metrics {
168
+ grid-template-columns: repeat(2, minmax(0, 1fr));
169
+ }
170
+ }
171
+ </style>