@brandon_m_behring/book-scaffold-astro 3.0.0-alpha.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 (84) hide show
  1. package/CLAUDE.md +179 -0
  2. package/bin/book-scaffold.mjs +61 -0
  3. package/components/CaseStudy.astro +36 -0
  4. package/components/ChapterHeader.astro +61 -0
  5. package/components/ChapterNav.astro +29 -0
  6. package/components/ChapterTOC.astro +33 -0
  7. package/components/Citation.astro +94 -0
  8. package/components/Cite.astro +71 -0
  9. package/components/CodeBlock.astro +115 -0
  10. package/components/CodeRef.astro +49 -0
  11. package/components/ConceptBox.astro +26 -0
  12. package/components/Convergence.astro +41 -0
  13. package/components/CounterBox.astro +15 -0
  14. package/components/Divergence.astro +32 -0
  15. package/components/DynConnect.astro +15 -0
  16. package/components/ExampleBox.astro +15 -0
  17. package/components/Figure.astro +35 -0
  18. package/components/InsightBox.astro +15 -0
  19. package/components/KeyIdea.astro +21 -0
  20. package/components/MarginNote.astro +37 -0
  21. package/components/NoteBox.astro +15 -0
  22. package/components/OpenQuestion.astro +15 -0
  23. package/components/PaperBox.astro +15 -0
  24. package/components/PatternTimeline.astro +133 -0
  25. package/components/Recovery.astro +34 -0
  26. package/components/ResultBox.astro +15 -0
  27. package/components/Sidebar.astro +268 -0
  28. package/components/Sidenote.astro +26 -0
  29. package/components/SkillBox.astro +24 -0
  30. package/components/SourceArchive.astro +285 -0
  31. package/components/StatusBadge.astro +51 -0
  32. package/components/Tag.astro +60 -0
  33. package/components/Theorem.astro +65 -0
  34. package/components/TipBox.astro +15 -0
  35. package/components/ToolFilter.tsx +160 -0
  36. package/components/TryThis.astro +23 -0
  37. package/components/VersionSelector.tsx +85 -0
  38. package/components/WarnBox.astro +15 -0
  39. package/components/WeekRef.astro +51 -0
  40. package/components/XRef.astro +40 -0
  41. package/dist/index.d.ts +135 -0
  42. package/dist/index.mjs +369 -0
  43. package/dist/lib/katex-macros.d.ts +26 -0
  44. package/dist/lib/katex-macros.mjs +98 -0
  45. package/dist/schemas.d.ts +17 -0
  46. package/dist/schemas.mjs +160 -0
  47. package/dist/types-Cz-pwE1N.d.ts +61 -0
  48. package/examples/chapter-template-academic.mdx +100 -0
  49. package/examples/chapter-template-tools.mdx +90 -0
  50. package/layouts/Base.astro +250 -0
  51. package/layouts/Chapter.astro +37 -0
  52. package/package.json +137 -0
  53. package/pages/chapters.astro +371 -0
  54. package/pages/convergence.astro +96 -0
  55. package/pages/print.astro +39 -0
  56. package/pages/references.astro +160 -0
  57. package/pages/search.astro +87 -0
  58. package/pedagogy/kf-chapter-shape.md +96 -0
  59. package/pedagogy/source-tiers.md +121 -0
  60. package/pedagogy/volatility-classes.md +110 -0
  61. package/recipes/00-getting-started.md +77 -0
  62. package/recipes/01-add-math.md +71 -0
  63. package/recipes/02-bibliography-pipeline.md +82 -0
  64. package/recipes/03-asset-pipelines.md +84 -0
  65. package/recipes/04-component-library.md +118 -0
  66. package/recipes/05-deploy-cloudflare.md +74 -0
  67. package/recipes/06-mobile-first-layout.md +73 -0
  68. package/recipes/07-chapter-shapes.md +84 -0
  69. package/recipes/08-decisions-ledger.md +110 -0
  70. package/recipes/09-validation.md +106 -0
  71. package/recipes/10-custom-domain.md +72 -0
  72. package/recipes/README.md +43 -0
  73. package/scripts/build-bib.mjs +99 -0
  74. package/scripts/build-figures.mjs +179 -0
  75. package/scripts/render-notebooks.mjs +223 -0
  76. package/scripts/validate.mjs +179 -0
  77. package/styles/callouts.css +303 -0
  78. package/styles/chapter.css +209 -0
  79. package/styles/convergence.css +349 -0
  80. package/styles/layout.css +156 -0
  81. package/styles/print.css +203 -0
  82. package/styles/tokens.css +194 -0
  83. package/styles/tool-filter.css +135 -0
  84. package/styles/typography.css +147 -0
@@ -0,0 +1,268 @@
1
+ ---
2
+ /**
3
+ * Sidebar — left-pinned chapter navigation.
4
+ *
5
+ * Profile-aware: reads BOOK_PROFILE to decide how to group + sort.
6
+ * academic: groups by part (foundations / ssm-core / ...), sorts by week
7
+ * tools/minimal: groups by part (numeric 0-10), sorts by chapter
8
+ *
9
+ * Rendered by Base.astro when `showSidebar` is true (default).
10
+ * Hidden below 1024px (`@media (max-width: 64rem)` in layout.css) —
11
+ * mobile gets single-column chapter content with no nav (user reaches
12
+ * the chapter index via /chapters/).
13
+ *
14
+ * Visual: ~280px wide, sticky to top, scrollable independently of main.
15
+ * Site title at top doubles as a "home" link.
16
+ *
17
+ * Customize: edit the `siteTitle` / `siteSubtitle` strings below or pass
18
+ * them in as Astro.props from Base.astro if you want per-page overrides.
19
+ */
20
+ import { getCollection } from 'astro:content';
21
+
22
+ const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
23
+ const siteTitle = 'Book';
24
+ const siteSubtitle = 'A scaffold-astro book';
25
+
26
+ // Academic profile: part is a string enum.
27
+ const ACADEMIC_PART_ORDER = [
28
+ 'foundations',
29
+ 'ssm-core',
30
+ 'beyond-ssm',
31
+ 'integration',
32
+ 'synthesis',
33
+ ] as const;
34
+ const ACADEMIC_PART_LABEL: Record<string, string> = {
35
+ foundations: 'Part I · Foundations',
36
+ 'ssm-core': 'Part II · SSM Core',
37
+ 'beyond-ssm': 'Part III · Beyond SSMs',
38
+ integration: 'Part IV · Integration',
39
+ synthesis: 'Part V · Synthesis',
40
+ };
41
+
42
+ const rawChapters = await getCollection('chapters', (entry) => !entry.data.draft);
43
+
44
+ // Pre-shape chapters into render-ready rows. All TS casts happen here in
45
+ // the frontmatter so the template stays free of type annotations (Astro's
46
+ // JSX parser mis-tokenizes `as Type<X, Y>` inside expression containers).
47
+ interface ChapterRow {
48
+ id: string;
49
+ partKey: string;
50
+ prefix: string;
51
+ title: string;
52
+ sortPart: number;
53
+ sortRank: number;
54
+ }
55
+
56
+ function rowOf(entry: typeof rawChapters[number]): ChapterRow {
57
+ const d = entry.data as Record<string, unknown>;
58
+ if (profile === 'academic') {
59
+ const week = (d.week as number) ?? 0;
60
+ return {
61
+ id: entry.id,
62
+ partKey: (d.part as string) ?? '',
63
+ prefix: `W${String(week).padStart(2, '0')}`,
64
+ title: (d.title as string) ?? entry.id,
65
+ sortPart: ACADEMIC_PART_ORDER.indexOf((d.part as typeof ACADEMIC_PART_ORDER[number]) ?? 'foundations'),
66
+ sortRank: week,
67
+ };
68
+ }
69
+ const partNum = (d.part as number) ?? 0;
70
+ const chapter = (d.chapter as number) ?? 0;
71
+ return {
72
+ id: entry.id,
73
+ partKey: String(partNum),
74
+ prefix: `Ch${chapter}`,
75
+ title: (d.title as string) ?? entry.id,
76
+ sortPart: partNum,
77
+ sortRank: chapter,
78
+ };
79
+ }
80
+
81
+ const rows: ChapterRow[] = rawChapters.map(rowOf).sort((a, b) => {
82
+ if (a.sortPart !== b.sortPart) return a.sortPart - b.sortPart;
83
+ return a.sortRank - b.sortRank;
84
+ });
85
+
86
+ // Group rows by part. Academic profile uses the ACADEMIC_PART_ORDER as
87
+ // the canonical key sequence so empty parts still appear (in part headers
88
+ // rather than gaps).
89
+ const byPart = new Map<string, ChapterRow[]>();
90
+ if (profile === 'academic') {
91
+ for (const part of ACADEMIC_PART_ORDER) byPart.set(part, []);
92
+ }
93
+ for (const r of rows) {
94
+ if (!byPart.has(r.partKey)) byPart.set(r.partKey, []);
95
+ byPart.get(r.partKey)!.push(r);
96
+ }
97
+
98
+ const currentPath = Astro.url.pathname;
99
+ function isCurrent(id: string): boolean {
100
+ return currentPath === `/chapters/${id}/` || currentPath === `/chapters/${id}`;
101
+ }
102
+
103
+ function partLabel(key: string): string {
104
+ if (profile === 'academic') return ACADEMIC_PART_LABEL[key] ?? key;
105
+ return `Part ${key}`;
106
+ }
107
+ ---
108
+
109
+ <aside class="sidebar" aria-label="Chapter navigation">
110
+ <div class="sidebar-inner">
111
+ <a href="/" class="sidebar-home">
112
+ <strong>{siteTitle}</strong>
113
+ <span class="sidebar-subtitle">{siteSubtitle}</span>
114
+ </a>
115
+
116
+ <nav class="sidebar-nav">
117
+ <a
118
+ href="/chapters/"
119
+ class={`sidebar-link sidebar-link-index ${currentPath === '/chapters/' ? 'is-current' : ''}`}
120
+ >
121
+ All chapters
122
+ </a>
123
+ {profile === 'academic' && (
124
+ <a
125
+ href="/references/"
126
+ class={`sidebar-link sidebar-link-index ${currentPath === '/references/' ? 'is-current' : ''}`}
127
+ >
128
+ References
129
+ </a>
130
+ )}
131
+
132
+ {Array.from(byPart.entries()).map(([part, list]) => list.length === 0 ? null : (
133
+ <section class="sidebar-part">
134
+ <h3 class="sidebar-part-heading">{partLabel(part)}</h3>
135
+ <ol class="sidebar-list">
136
+ {list.map((r) => (
137
+ <li>
138
+ <a
139
+ href={`/chapters/${r.id}/`}
140
+ class={`sidebar-link ${isCurrent(r.id) ? 'is-current' : ''}`}
141
+ >
142
+ <span class="sidebar-week">{r.prefix}</span>
143
+ <span class="sidebar-chapter-title">{r.title}</span>
144
+ </a>
145
+ </li>
146
+ ))}
147
+ </ol>
148
+ </section>
149
+ ))}
150
+ </nav>
151
+ </div>
152
+ </aside>
153
+
154
+ <style>
155
+ .sidebar {
156
+ grid-area: sidebar;
157
+ border-right: 1px solid var(--color-border);
158
+ background: var(--color-bg-subtle);
159
+ font-size: var(--text-sm);
160
+ }
161
+ .sidebar-inner {
162
+ position: sticky;
163
+ top: 0;
164
+ max-height: 100vh;
165
+ overflow-y: auto;
166
+ padding: var(--space-6) var(--space-4) var(--space-6) var(--space-5);
167
+ }
168
+
169
+ .sidebar-home {
170
+ display: block;
171
+ text-decoration: none;
172
+ color: inherit;
173
+ margin-bottom: var(--space-5);
174
+ line-height: 1.3;
175
+ }
176
+ .sidebar-home strong {
177
+ display: block;
178
+ font-size: var(--text-base);
179
+ color: var(--color-heading);
180
+ }
181
+ .sidebar-home .sidebar-subtitle {
182
+ display: block;
183
+ font-size: var(--text-xs);
184
+ color: var(--color-text-muted);
185
+ margin-top: 0.15em;
186
+ }
187
+
188
+ .sidebar-nav {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: var(--space-4);
192
+ }
193
+
194
+ .sidebar-link-index {
195
+ display: block;
196
+ padding: var(--space-2) var(--space-3);
197
+ border-radius: var(--radius-sm);
198
+ text-decoration: none;
199
+ color: var(--color-text);
200
+ font-weight: 500;
201
+ margin-bottom: 1px;
202
+ border-left: 2px solid transparent;
203
+ }
204
+ .sidebar-link-index:hover {
205
+ background: var(--color-bg);
206
+ }
207
+ .sidebar-link-index.is-current {
208
+ background: var(--warm-blue-tint);
209
+ border-left-color: var(--callout-info);
210
+ }
211
+
212
+ .sidebar-part {
213
+ border-top: 1px solid var(--color-border);
214
+ padding-top: var(--space-3);
215
+ }
216
+ .sidebar-part:first-of-type {
217
+ border-top: 0;
218
+ padding-top: 0;
219
+ }
220
+ .sidebar-part-heading {
221
+ font-family: var(--font-code);
222
+ font-size: var(--text-xs);
223
+ text-transform: uppercase;
224
+ letter-spacing: 0.04em;
225
+ color: var(--color-text-muted);
226
+ margin: 0 0 var(--space-2) 0;
227
+ padding: 0 var(--space-3);
228
+ }
229
+
230
+ .sidebar-list {
231
+ list-style: none;
232
+ padding: 0;
233
+ margin: 0;
234
+ }
235
+ .sidebar-list li {
236
+ margin: 0;
237
+ }
238
+ .sidebar-link {
239
+ display: grid;
240
+ grid-template-columns: 2.4em 1fr;
241
+ gap: var(--space-2);
242
+ align-items: baseline;
243
+ padding: var(--space-2) var(--space-3);
244
+ border-radius: var(--radius-sm);
245
+ text-decoration: none;
246
+ color: var(--color-text);
247
+ line-height: var(--leading-normal);
248
+ border-left: 2px solid transparent;
249
+ }
250
+ .sidebar-link:hover {
251
+ background: var(--color-bg);
252
+ text-decoration: none;
253
+ }
254
+ .sidebar-link.is-current {
255
+ background: var(--warm-blue-tint);
256
+ border-left-color: var(--callout-info);
257
+ font-weight: 500;
258
+ }
259
+ .sidebar-week {
260
+ font-family: var(--font-code);
261
+ font-size: var(--text-xs);
262
+ color: var(--color-text-muted);
263
+ text-transform: uppercase;
264
+ }
265
+ .sidebar-chapter-title {
266
+ color: inherit;
267
+ }
268
+ </style>
@@ -0,0 +1,26 @@
1
+ ---
2
+ /**
3
+ * Sidenote — a marginalia note that appears in the right margin on
4
+ * desktop and as an inline-flow aside on mobile. Auto-numbered via CSS
5
+ * counters (.prose counter-resets, marker increments).
6
+ *
7
+ * Per Q6 in the plan: never hidden, never tap-to-reveal. The only
8
+ * thing that changes across breakpoints is where the note sits.
9
+ *
10
+ * Usage (inline within a paragraph):
11
+ * <p>
12
+ * The argument<Sidenote>supporting evidence here</Sidenote>
13
+ * convinced me.
14
+ * </p>
15
+ *
16
+ * Rendered HTML structure:
17
+ * <sup class="sidenote-marker"></sup> ← the numeric marker
18
+ * <span class="sidenote">content</span> ← the note itself
19
+ *
20
+ * The <span> (not <aside>) is deliberate: keeps HTML valid inside
21
+ * <p> and naturally wraps. Role="note" + .sidenote::before
22
+ * rendering the number give assistive tech the right semantics.
23
+ */
24
+ ---
25
+ <sup class="sidenote-marker" aria-hidden="true"></sup>
26
+ <span class="sidenote" role="note"><slot /></span>
@@ -0,0 +1,24 @@
1
+ ---
2
+ /**
3
+ * SkillBox — tactical how-to. Bounded, practitioner-oriented.
4
+ *
5
+ * Used when a chapter teaches a concrete technique: "How to warm the
6
+ * cache before a long session", "How to audit CLAUDE.md bloat", etc.
7
+ * Deliberately narrower than the surrounding prose — a skill box is a
8
+ * recipe, not a principle.
9
+ *
10
+ * Usage:
11
+ * <SkillBox title="Warm-cache workflow">
12
+ * 1. Run `/context` to see current budget.
13
+ * 2. ...
14
+ * </SkillBox>
15
+ */
16
+ interface Props {
17
+ title: string;
18
+ }
19
+ const { title } = Astro.props;
20
+ ---
21
+ <aside class="callout callout-skill" role="note" aria-label={`Skill: ${title}`}>
22
+ <strong class="callout-title">Skill · {title}</strong>
23
+ <div class="callout-body"><slot /></div>
24
+ </aside>
@@ -0,0 +1,285 @@
1
+ ---
2
+ /**
3
+ * SourceArchive — renders every entry in sources/manifest.yaml grouped
4
+ * by tier in descending authority (T1 → T4).
5
+ *
6
+ * Used from Appendix D (`d-source-archive-index.mdx`) to replace the
7
+ * static hand-maintained listing with an auto-generated view that
8
+ * always reflects the manifest.
9
+ *
10
+ * Empty-tier behavior: renders an honest placeholder ("No sources at
11
+ * this tier yet.") rather than hiding the section. This matches the
12
+ * pedagogy of Appendix D — the archive is intentionally sparse in the
13
+ * early book, and the gap is visible by design.
14
+ */
15
+ import { sourceTiers } from '../content.config';
16
+ import {
17
+ getSourcesByTier,
18
+ TIER_LABELS,
19
+ TIER_DESCRIPTIONS,
20
+ } from '../src/lib/sources';
21
+
22
+ const grouped = await getSourcesByTier();
23
+
24
+ function formatDate(d: Date | undefined): string {
25
+ if (!d) return '—';
26
+ return d.toISOString().slice(0, 10);
27
+ }
28
+
29
+ function year(d: Date | undefined): string | null {
30
+ if (!d) return null;
31
+ return String(d.getUTCFullYear());
32
+ }
33
+ ---
34
+ <section class="source-archive" aria-label="Source archive">
35
+ {sourceTiers.map((tier) => {
36
+ const entries = grouped[tier];
37
+ const empty = entries.length === 0;
38
+ return (
39
+ <section
40
+ class="source-archive-tier"
41
+ data-tier={tier}
42
+ id={`tier-${tier}`}
43
+ >
44
+ <header class="source-archive-tier-header">
45
+ <h3 class="source-archive-tier-label">
46
+ <span class={`tier-badge tier-${tier}`}>{TIER_LABELS[tier]}</span>
47
+ <span class="source-archive-tier-count">
48
+ {empty ? 'no entries yet' : `${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}`}
49
+ </span>
50
+ </h3>
51
+ <p class="source-archive-tier-description">{TIER_DESCRIPTIONS[tier]}</p>
52
+ </header>
53
+
54
+ {empty ? (
55
+ <p class="source-archive-empty">
56
+ No sources at this tier yet. Appendix D is intentionally sparse in the early book; this slot fills as chapters leave draft.
57
+ </p>
58
+ ) : (
59
+ <ol
60
+ class="source-archive-list"
61
+ role="list"
62
+ aria-label={`${TIER_LABELS[tier]} sources`}
63
+ >
64
+ {entries.map((entry) => {
65
+ const d = entry.data;
66
+ return (
67
+ <li class="source-archive-entry" id={`source-${entry.id}`}>
68
+ <div class="source-archive-entry-title">
69
+ <a href={d.url} rel="external noopener">{d.title}</a>
70
+ </div>
71
+ <div class="source-archive-entry-meta">
72
+ {d.author && (
73
+ <span class="source-archive-entry-author">{d.author}</span>
74
+ )}
75
+ {year(d.publish_date) && (
76
+ <span class="source-archive-entry-year">{year(d.publish_date)}</span>
77
+ )}
78
+ <span class="source-archive-entry-captured">
79
+ captured {formatDate(d.captured_at)}
80
+ </span>
81
+ <span class="source-archive-entry-tool">tool: {d.tool}</span>
82
+ </div>
83
+ <div class="source-archive-entry-links">
84
+ <a href={d.url} rel="external noopener" class="source-archive-link">original</a>
85
+ {d.perma_cc && (
86
+ <a href={d.perma_cc} rel="external noopener" class="source-archive-link">archived (Perma.cc)</a>
87
+ )}
88
+ <code class="source-archive-slug">id: {entry.id}</code>
89
+ </div>
90
+ </li>
91
+ );
92
+ })}
93
+ </ol>
94
+ )}
95
+ </section>
96
+ );
97
+ })}
98
+ </section>
99
+
100
+ <style>
101
+ .source-archive {
102
+ margin: var(--space-6) 0;
103
+ }
104
+ .source-archive-tier {
105
+ margin: var(--space-6) 0;
106
+ padding-bottom: var(--space-4);
107
+ border-bottom: 1px solid var(--color-border);
108
+ }
109
+ .source-archive-tier:last-child {
110
+ border-bottom: none;
111
+ }
112
+
113
+ .source-archive-tier-header {
114
+ margin-bottom: var(--space-3);
115
+ }
116
+ .source-archive-tier-label {
117
+ display: flex;
118
+ align-items: baseline;
119
+ gap: var(--space-3);
120
+ margin: 0 0 var(--space-2) 0;
121
+ font-size: var(--text-lg);
122
+ }
123
+ .source-archive-tier-count {
124
+ font-family: var(--font-code);
125
+ font-size: var(--text-sm);
126
+ color: var(--color-text-muted);
127
+ font-weight: 400;
128
+ }
129
+ .source-archive-tier-description {
130
+ margin: 0;
131
+ font-size: var(--text-sm);
132
+ color: var(--color-text-muted);
133
+ text-indent: 0;
134
+ }
135
+
136
+ /* Tier badges: muted palette, rendered at the heading level.
137
+ * Follows the citation-tier visual language so the archive and
138
+ * inline citations feel like the same system. */
139
+ .tier-badge {
140
+ display: inline-block;
141
+ padding: 0.1em 0.55em;
142
+ font-family: var(--font-code);
143
+ font-size: 0.8em;
144
+ font-weight: 500;
145
+ letter-spacing: 0.05em;
146
+ border-radius: var(--radius-sm);
147
+ border: 1px solid var(--color-border);
148
+ background: var(--color-bg);
149
+ color: var(--color-text);
150
+ }
151
+ .tier-T1-official {
152
+ background: var(--warm-plum-tint);
153
+ color: var(--warm-plum);
154
+ border-color: var(--warm-plum);
155
+ }
156
+ .tier-T2-release-notes {
157
+ background: var(--warm-blue-tint);
158
+ color: var(--warm-blue);
159
+ border-color: var(--warm-blue);
160
+ }
161
+ .tier-T3-practitioner {
162
+ background: var(--warm-green-tint);
163
+ color: var(--warm-green);
164
+ border-color: var(--warm-green);
165
+ }
166
+ .tier-T4-conjecture {
167
+ background: var(--warm-rose-tint);
168
+ color: var(--warm-rose);
169
+ border-color: var(--warm-rose);
170
+ }
171
+
172
+ .source-archive-empty {
173
+ color: var(--color-text-muted);
174
+ font-style: italic;
175
+ padding: var(--space-3);
176
+ background: var(--color-bg-subtle);
177
+ border-radius: var(--radius-sm);
178
+ text-indent: 0;
179
+ }
180
+
181
+ .source-archive-list {
182
+ list-style: none;
183
+ padding: 0;
184
+ margin: 0;
185
+ display: grid;
186
+ gap: var(--space-3);
187
+ }
188
+ .source-archive-entry {
189
+ padding: var(--space-3);
190
+ border: 1px solid var(--color-border);
191
+ border-radius: var(--radius-md);
192
+ background: var(--color-bg);
193
+ scroll-margin-top: var(--space-6); /* anchor targets clear of fixed chrome */
194
+ transition: border-color 120ms ease, background 120ms ease;
195
+ }
196
+ .source-archive-entry:hover {
197
+ border-color: var(--color-link);
198
+ }
199
+ .source-archive-entry:target {
200
+ border-color: var(--warm-blue);
201
+ background: var(--warm-blue-tint);
202
+ }
203
+ .source-archive-entry-title {
204
+ font-size: var(--text-lg);
205
+ font-weight: 500;
206
+ margin-bottom: var(--space-1);
207
+ }
208
+ .source-archive-entry-title a {
209
+ color: var(--color-link);
210
+ text-decoration: none;
211
+ }
212
+ .source-archive-entry-title a:hover {
213
+ text-decoration: underline;
214
+ }
215
+ .source-archive-entry-meta {
216
+ display: flex;
217
+ flex-wrap: wrap;
218
+ gap: var(--space-3);
219
+ font-size: var(--text-sm);
220
+ color: var(--color-text-muted);
221
+ font-family: var(--font-code);
222
+ margin-bottom: var(--space-2);
223
+ }
224
+ .source-archive-entry-author {
225
+ color: var(--color-text);
226
+ }
227
+ .source-archive-entry-links {
228
+ display: flex;
229
+ flex-wrap: wrap;
230
+ gap: var(--space-3);
231
+ align-items: center;
232
+ font-size: var(--text-sm);
233
+ }
234
+ .source-archive-link {
235
+ color: var(--color-link);
236
+ }
237
+ .source-archive-slug {
238
+ font-family: var(--font-code);
239
+ font-size: var(--text-xs);
240
+ color: var(--color-text-muted);
241
+ background: var(--color-bg-subtle);
242
+ padding: 0.1em 0.4em;
243
+ border-radius: var(--radius-sm);
244
+ border: 0;
245
+ }
246
+
247
+ @media (max-width: 48rem) {
248
+ .source-archive-tier-label {
249
+ flex-direction: column;
250
+ align-items: flex-start;
251
+ gap: var(--space-1);
252
+ }
253
+ .source-archive-entry-meta,
254
+ .source-archive-entry-links {
255
+ gap: var(--space-2);
256
+ }
257
+ }
258
+
259
+ /* Long URLs in the slug or author line can force horizontal scroll
260
+ * on narrow viewports. Let them wrap anywhere. */
261
+ .source-archive-entry a,
262
+ .source-archive-slug {
263
+ word-break: break-word;
264
+ overflow-wrap: anywhere;
265
+ }
266
+
267
+ /* Print: borders soften, hover/target highlighting off, slug code
268
+ * visible (readers of the printed edition still need the citation
269
+ * key to cross-reference chapters). */
270
+ @media print {
271
+ .source-archive-entry {
272
+ border-width: 0.5pt;
273
+ border-color: #aaa;
274
+ background: transparent !important;
275
+ break-inside: avoid;
276
+ }
277
+ .source-archive-entry:target {
278
+ background: transparent !important;
279
+ border-color: #aaa !important;
280
+ }
281
+ .source-archive-tier {
282
+ break-inside: avoid;
283
+ }
284
+ }
285
+ </style>
@@ -0,0 +1,51 @@
1
+ ---
2
+ /**
3
+ * StatusBadge — translates the internal 7-state taxonomy from
4
+ * docs/STATUS.md into a simplified 3-state public label.
5
+ *
6
+ * Public mapping (locked in plan Q4):
7
+ * Published = implemented | chapter_only | prose_only | reading_only
8
+ * Draft = code_only
9
+ * Coming Soon = scaffolded | planned
10
+ *
11
+ * Internal frontmatter retains the 7-state value for STATUS.md alignment;
12
+ * this component is the public-facing translator.
13
+ */
14
+ type InternalStatus =
15
+ | 'implemented'
16
+ | 'chapter_only'
17
+ | 'reading_only'
18
+ | 'prose_only'
19
+ | 'code_only'
20
+ | 'scaffolded'
21
+ | 'planned';
22
+
23
+ type PublicStatus = 'published' | 'draft' | 'coming-soon';
24
+
25
+ const PUBLIC_OF: Record<InternalStatus, PublicStatus> = {
26
+ implemented: 'published',
27
+ chapter_only: 'published',
28
+ reading_only: 'published',
29
+ prose_only: 'published',
30
+ code_only: 'draft',
31
+ scaffolded: 'coming-soon',
32
+ planned: 'coming-soon',
33
+ };
34
+
35
+ const PUBLIC_LABEL: Record<PublicStatus, string> = {
36
+ published: 'Published',
37
+ draft: 'Draft',
38
+ 'coming-soon': 'Coming Soon',
39
+ };
40
+
41
+ interface Props {
42
+ status: InternalStatus;
43
+ }
44
+
45
+ const { status } = Astro.props;
46
+ const publicStatus = PUBLIC_OF[status];
47
+ const label = PUBLIC_LABEL[publicStatus];
48
+ ---
49
+ <span class="status-badge" data-status={publicStatus} title={`Internal: ${status}`}>
50
+ {label}
51
+ </span>