@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,371 @@
1
+ ---
2
+ /**
3
+ * /chapters — book index page.
4
+ *
5
+ * Groups non-draft chapters by Part in ascending part+chapter order.
6
+ * Each card exposes `data-tools="<slug> <slug>..."` so future tool-filter
7
+ * plumbing (Stage 3.2 commit 3) can hide cards via a single attribute
8
+ * selector without touching the card rendering.
9
+ */
10
+ import Base from '../layouts/Base.astro';
11
+ import { getAllChapters, type Chapter } from '../src/lib/chapters';
12
+ import { getFreshness, freshnessLabel } from '../src/lib/freshness';
13
+
14
+ const chapters = await getAllChapters();
15
+
16
+ // Stable insertion-order grouping: iterate sorted chapters once.
17
+ const byPart = new Map<number, Chapter[]>();
18
+ for (const c of chapters) {
19
+ const list = byPart.get(c.data.part);
20
+ if (list) list.push(c);
21
+ else byPart.set(c.data.part, [c]);
22
+ }
23
+
24
+ function formatDate(d: Date): string {
25
+ return d.toISOString().slice(0, 10);
26
+ }
27
+
28
+ function partLabel(part: number, appendix: boolean): string {
29
+ return appendix ? 'Appendices' : `Part ${part}`;
30
+ }
31
+ ---
32
+ <Base
33
+ title="Chapters — Agentic Coding"
34
+ description="All chapters grouped by Part, with volatility, freshness, and tools-compared metadata at a glance."
35
+ >
36
+ <article class="prose chapters-index">
37
+ <header class="chapters-index-header">
38
+ <h1>Chapters</h1>
39
+ <p class="chapters-index-lede">
40
+ Every chapter, grouped by Part. Each card shows its volatility
41
+ class, freshness status, and the tools it compares. Use a chapter
42
+ card's metadata to calibrate how much trust to place in its
43
+ specific claims — stable principles age slowly; feature surfaces
44
+ age fast.
45
+ </p>
46
+ <p class="chapters-index-cross-ref">
47
+ See also: <a href="/convergence/">convergence dashboard</a> —
48
+ which patterns have landed in which tools, when.
49
+ </p>
50
+ </header>
51
+
52
+ <p class="chapters-filter-hint" id="filter-hint" aria-live="polite"></p>
53
+
54
+ {Array.from(byPart.entries()).map(([part, list]) => {
55
+ const appendix = part >= 6;
56
+ return (
57
+ <section class="part-group">
58
+ <h2 class="part-heading">
59
+ <span class="part-label">{partLabel(part, appendix)}</span>
60
+ </h2>
61
+ <ol class="chapter-list">
62
+ {list.map((c) => {
63
+ const freshness = getFreshness(c.data.last_verified, c.data.volatility);
64
+ const freshnessText =
65
+ freshness.status === 'fresh'
66
+ ? 'Fresh'
67
+ : freshness.status === 'verify-soon'
68
+ ? 'Verify soon'
69
+ : 'Stale';
70
+ const toolsAttr = c.data.tools_compared.join(' ');
71
+ return (
72
+ <li
73
+ class="chapter-card"
74
+ data-tools={toolsAttr}
75
+ >
76
+ <a href={`/${c.id}/`} class="chapter-card-link">
77
+ <div class="chapter-card-meta">
78
+ <span class="chapter-card-number">
79
+ {appendix ? `Appendix ${String.fromCharCode(64 + c.data.chapter).toLowerCase()}` : `Chapter ${c.data.chapter}`}
80
+ </span>
81
+ <span
82
+ class={`volatility-badge volatility-${c.data.volatility}`}
83
+ title={`Volatility: ${c.data.volatility}`}
84
+ >{c.data.volatility}</span>
85
+ <span
86
+ class="freshness-badge"
87
+ data-status={freshness.status}
88
+ aria-label={freshnessLabel(freshness)}
89
+ title={freshnessLabel(freshness)}
90
+ >{freshnessText}</span>
91
+ <span class="chapter-card-verified">
92
+ verified {formatDate(c.data.last_verified)}
93
+ </span>
94
+ </div>
95
+ <h3 class="chapter-card-title">{c.data.title}</h3>
96
+ {c.data.description && (
97
+ <p class="chapter-card-description">{c.data.description}</p>
98
+ )}
99
+ <div class="chapter-card-tools">
100
+ {c.data.tools_compared.map((t) => (
101
+ <span class="tool-badge">{t}</span>
102
+ ))}
103
+ </div>
104
+ </a>
105
+ </li>
106
+ );
107
+ })}
108
+ </ol>
109
+ </section>
110
+ );
111
+ })}
112
+ </article>
113
+ </Base>
114
+
115
+ {/* Inline non-island script: applies tool filter from localStorage on
116
+ load, then reacts to ToolFilter island's "book:tool-filter:change"
117
+ CustomEvent. Handled as plain DOM so /chapters/ doesn't need its
118
+ own hydration island. */}
119
+ <script is:inline>
120
+ (function () {
121
+ var STORAGE_KEY = 'book:tool-filter';
122
+ var EVENT_NAME = 'book:tool-filter:change';
123
+
124
+ function readSelected() {
125
+ try {
126
+ var raw = localStorage.getItem(STORAGE_KEY);
127
+ if (!raw) return [];
128
+ var parsed = JSON.parse(raw);
129
+ return Array.isArray(parsed) ? parsed : [];
130
+ } catch (e) {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ function applyFilter(selected) {
136
+ var cards = document.querySelectorAll('.chapter-card[data-tools]');
137
+ var shownCount = 0;
138
+ var totalCount = cards.length;
139
+ var active = Array.isArray(selected) && selected.length > 0;
140
+
141
+ cards.forEach(function (card) {
142
+ var attr = card.getAttribute('data-tools') || '';
143
+ var tools = attr.split(/\s+/).filter(Boolean);
144
+ var isCrossTool = tools.indexOf('cross-tool') !== -1;
145
+ var overlaps = false;
146
+ for (var i = 0; i < tools.length; i++) {
147
+ if (selected.indexOf(tools[i]) !== -1) {
148
+ overlaps = true;
149
+ break;
150
+ }
151
+ }
152
+
153
+ var hidden = active && !overlaps && !isCrossTool;
154
+ if (hidden) {
155
+ card.setAttribute('data-hidden-by-filter', 'true');
156
+ } else {
157
+ card.removeAttribute('data-hidden-by-filter');
158
+ shownCount++;
159
+ }
160
+ });
161
+
162
+ // Hide entire part sections that become empty under the filter.
163
+ document.querySelectorAll('.part-group').forEach(function (group) {
164
+ var visible = group.querySelectorAll(
165
+ '.chapter-card:not([data-hidden-by-filter="true"])',
166
+ );
167
+ if (visible.length === 0) {
168
+ group.setAttribute('data-hidden-by-filter', 'true');
169
+ } else {
170
+ group.removeAttribute('data-hidden-by-filter');
171
+ }
172
+ });
173
+
174
+ // Update the hint line.
175
+ var hint = document.getElementById('filter-hint');
176
+ if (hint) {
177
+ if (!active) {
178
+ hint.textContent = '';
179
+ } else {
180
+ hint.textContent =
181
+ 'Showing ' + shownCount + ' of ' + totalCount +
182
+ ' chapters (filter: ' + selected.join(', ') + ').' +
183
+ ' Cross-tool chapters stay visible.';
184
+ }
185
+ }
186
+ }
187
+
188
+ // Apply saved filter on first load, before the island hydrates.
189
+ applyFilter(readSelected());
190
+
191
+ // React to subsequent changes from the ToolFilter island.
192
+ window.addEventListener(EVENT_NAME, function (e) {
193
+ var detail = e && e.detail ? e.detail : {};
194
+ applyFilter(Array.isArray(detail.selected) ? detail.selected : []);
195
+ });
196
+ })();
197
+ </script>
198
+
199
+ <style>
200
+ .chapters-index-header {
201
+ margin-bottom: var(--space-8);
202
+ border-bottom: 1px solid var(--color-border);
203
+ padding-bottom: var(--space-6);
204
+ }
205
+ .chapters-index-lede {
206
+ font-size: var(--text-lg);
207
+ color: var(--color-text-muted);
208
+ max-width: 65ch;
209
+ }
210
+ .chapters-index-cross-ref {
211
+ font-size: var(--text-sm);
212
+ color: var(--color-text-muted);
213
+ margin-top: var(--space-3);
214
+ text-indent: 0;
215
+ }
216
+
217
+ .part-group {
218
+ margin: var(--space-8) 0;
219
+ }
220
+ .part-heading {
221
+ font-size: var(--text-sm);
222
+ font-family: var(--font-code);
223
+ text-transform: uppercase;
224
+ letter-spacing: 0.08em;
225
+ color: var(--color-text-muted);
226
+ border-bottom: 1px solid var(--color-border);
227
+ padding-bottom: var(--space-2);
228
+ margin-bottom: var(--space-4);
229
+ }
230
+ .part-label {
231
+ font-weight: 500;
232
+ }
233
+
234
+ .chapter-list {
235
+ list-style: none;
236
+ padding: 0;
237
+ margin: 0;
238
+ display: grid;
239
+ gap: var(--space-4);
240
+ }
241
+
242
+ .chapter-card {
243
+ border: 1px solid var(--color-border);
244
+ border-radius: var(--radius-md);
245
+ background: var(--color-bg);
246
+ transition:
247
+ border-color 120ms ease,
248
+ background 120ms ease,
249
+ opacity 180ms ease,
250
+ max-height 220ms ease;
251
+ max-height: 40rem; /* ceiling for the transition; actual height is auto */
252
+ overflow: hidden;
253
+ }
254
+ .chapter-card:hover {
255
+ border-color: var(--color-link);
256
+ background: var(--color-bg-subtle);
257
+ }
258
+ .chapter-card-link {
259
+ display: block;
260
+ padding: var(--space-4);
261
+ text-decoration: none;
262
+ color: inherit;
263
+ }
264
+ .chapter-card-link:hover {
265
+ text-decoration: none;
266
+ }
267
+
268
+ .chapter-card-meta {
269
+ display: flex;
270
+ flex-wrap: wrap;
271
+ gap: var(--space-2);
272
+ align-items: center;
273
+ font-size: var(--text-sm);
274
+ color: var(--color-text-muted);
275
+ margin-bottom: var(--space-2);
276
+ }
277
+ .chapter-card-number {
278
+ font-family: var(--font-code);
279
+ font-weight: 500;
280
+ color: var(--color-text);
281
+ text-transform: uppercase;
282
+ letter-spacing: 0.04em;
283
+ font-size: var(--text-xs);
284
+ }
285
+ .chapter-card-verified {
286
+ font-family: var(--font-code);
287
+ font-size: var(--text-xs);
288
+ }
289
+
290
+ .chapter-card-title {
291
+ font-size: var(--text-lg);
292
+ font-weight: 500;
293
+ margin: 0 0 var(--space-2) 0;
294
+ color: var(--color-link);
295
+ }
296
+ .chapter-card-description {
297
+ color: var(--color-text);
298
+ margin: 0 0 var(--space-3) 0;
299
+ text-indent: 0;
300
+ line-height: 1.5;
301
+ }
302
+ .chapter-card-tools {
303
+ display: flex;
304
+ flex-wrap: wrap;
305
+ gap: var(--space-2);
306
+ }
307
+
308
+ /* Filter target: cards fade + collapse smoothly; full display:none
309
+ * kicks in after the transition so we don't keep a focusable node
310
+ * in the list. Part-groups collapse immediately because they're
311
+ * structural, not reader-facing. */
312
+ .chapter-card[data-hidden-by-filter='true'] {
313
+ opacity: 0;
314
+ max-height: 0;
315
+ margin: 0;
316
+ padding: 0;
317
+ border-width: 0;
318
+ pointer-events: none;
319
+ overflow: hidden;
320
+ }
321
+ .chapter-card[data-hidden-by-filter='true'] .chapter-card-link {
322
+ visibility: hidden;
323
+ }
324
+ .part-group[data-hidden-by-filter='true'] {
325
+ display: none;
326
+ }
327
+
328
+ .chapters-filter-hint {
329
+ font-size: var(--text-sm);
330
+ color: var(--color-text-muted);
331
+ font-style: italic;
332
+ margin: var(--space-2) 0 var(--space-4) 0;
333
+ min-height: 1.4em;
334
+ }
335
+ .chapters-filter-hint:empty {
336
+ margin: 0;
337
+ min-height: 0;
338
+ }
339
+
340
+ /* Print override: the filter UI doesn't exist in print (chrome is
341
+ * hidden), so all chapters must render regardless of any filter
342
+ * state left over in the viewer. */
343
+ @media print {
344
+ .chapter-card[data-hidden-by-filter='true'] {
345
+ opacity: 1;
346
+ max-height: none;
347
+ margin: unset;
348
+ padding: unset;
349
+ border-width: 1px;
350
+ pointer-events: auto;
351
+ overflow: visible;
352
+ }
353
+ .chapter-card[data-hidden-by-filter='true'] .chapter-card-link {
354
+ visibility: visible;
355
+ }
356
+ .part-group[data-hidden-by-filter='true'] {
357
+ display: block;
358
+ }
359
+ .chapters-filter-hint {
360
+ display: none;
361
+ }
362
+ }
363
+
364
+ /* Reduced-motion: skip the opacity/height transition for anyone who
365
+ * prefers less motion. The display change is instant either way. */
366
+ @media (prefers-reduced-motion: reduce) {
367
+ .chapter-card {
368
+ transition: border-color 120ms ease, background 120ms ease;
369
+ }
370
+ }
371
+ </style>
@@ -0,0 +1,96 @@
1
+ ---
2
+ /**
3
+ * /convergence — dashboard of agentic-coding patterns across tools.
4
+ *
5
+ * For each category, renders every pattern in that category as a
6
+ * PatternTimeline card. Categories with no patterns render an honest
7
+ * placeholder so readers see the coverage gaps rather than inferring
8
+ * the registry is exhaustive.
9
+ *
10
+ * Driven entirely from changelog/patterns.yaml + changelog/tools/*.yaml.
11
+ * No code change is needed to grow the dashboard — add manifest rows.
12
+ */
13
+ import Base from '../layouts/Base.astro';
14
+ import PatternTimeline from '../components/PatternTimeline.astro';
15
+ import {
16
+ getPatternsByCategory,
17
+ CATEGORY_LABELS,
18
+ } from '../src/lib/patterns';
19
+ import { patternCategories } from '../content.config';
20
+
21
+ const grouped = await getPatternsByCategory();
22
+ const totalPatterns = Object.values(grouped).reduce(
23
+ (n, arr) => n + arr.length,
24
+ 0,
25
+ );
26
+ ---
27
+ <Base
28
+ title="Convergence — Agentic Coding"
29
+ description="Which agentic-coding patterns have converged across Claude Code, Gemini CLI, and Codex CLI — and when? A live timeline driven from the changelog manifest."
30
+ >
31
+ <article class="prose convergence-dashboard">
32
+ <header class="convergence-header">
33
+ <h1>Convergence dashboard</h1>
34
+ <p class="convergence-lede">
35
+ Each card below tracks one agentic-coding pattern across the
36
+ three primary tools. A pattern is <em>converged</em> when all
37
+ three tools have shipped it. The timeline shows the sequence of
38
+ adoptions — who first, who followed, who has not yet.
39
+ </p>
40
+ <p class="convergence-metric">
41
+ <strong>{totalPatterns}</strong> pattern{totalPatterns === 1 ? '' : 's'}
42
+ {' '}currently tracked. The registry grows as new patterns
43
+ converge or as historical patterns get backfilled; expect the
44
+ count to drift upward, not the existing entries.
45
+ </p>
46
+ </header>
47
+
48
+ {patternCategories.map((cat) => {
49
+ const patterns = grouped[cat];
50
+ return (
51
+ <section class="convergence-category" data-category={cat} id={`category-${cat}`}>
52
+ <h2 class="convergence-category-heading">
53
+ <span class="convergence-category-label">{CATEGORY_LABELS[cat]}</span>
54
+ <span class="convergence-category-count">
55
+ {patterns.length === 0
56
+ ? 'no patterns'
57
+ : `${patterns.length} pattern${patterns.length === 1 ? '' : 's'}`}
58
+ </span>
59
+ </h2>
60
+ {patterns.length === 0 ? (
61
+ <p class="convergence-category-empty">
62
+ No tracked patterns in this category yet. This is a
63
+ coverage gap, not an assertion that nothing belongs here.
64
+ </p>
65
+ ) : (
66
+ <div class="convergence-card-list">
67
+ {patterns.map((p) => <PatternTimeline pattern={p} />)}
68
+ </div>
69
+ )}
70
+ </section>
71
+ );
72
+ })}
73
+
74
+ <section class="convergence-method">
75
+ <h2>How to read this</h2>
76
+ <p>
77
+ The registry lives at <code>changelog/patterns.yaml</code>.
78
+ Each tool's adoption timeline lives at
79
+ <code>changelog/tools/&lt;tool&gt;.yaml</code>. The dashboard
80
+ joins them at build time — no code edits needed to add new
81
+ patterns or new adoption events.
82
+ </p>
83
+ <p>
84
+ A pattern with <em>convergence_date: null</em> is either a
85
+ partial convergence (1 or 2 of 3 tools) or an open design
86
+ space where the right shape is still being argued. Either
87
+ way, it is information worth surfacing, not hiding.
88
+ </p>
89
+ <p>
90
+ Coverage is intentionally sparse in the early book — the
91
+ registry grows organically through quarterly audit cycles.
92
+ Pattern nominations are welcome via Issues.
93
+ </p>
94
+ </section>
95
+ </article>
96
+ </Base>
@@ -0,0 +1,39 @@
1
+ ---
2
+ /**
3
+ * print.astro — concatenated book rendered as one long document, the
4
+ * input for pagedjs-cli.
5
+ *
6
+ * Walks the chapters collection in reading order, renders each chapter
7
+ * body, and wraps in a <section.chapter-print> so print.css can force
8
+ * page breaks between chapters.
9
+ *
10
+ * Build pipeline:
11
+ * npm run build → Astro emits dist/print/index.html
12
+ * npm run pdf → pagedjs-cli fetches dist/print/ via preview
13
+ * server, runs Paged.js in headless Chrome,
14
+ * outputs dist-pdf/book.pdf
15
+ */
16
+ import Base from '../layouts/Base.astro';
17
+ import { render } from 'astro:content';
18
+ import { getAllChapters } from '../src/lib/chapters';
19
+ import ChapterHeader from '../components/ChapterHeader.astro';
20
+
21
+ const chapters = await getAllChapters();
22
+ const rendered = await Promise.all(
23
+ chapters.map(async (entry) => {
24
+ const { Content } = await render(entry);
25
+ return { entry, Content };
26
+ })
27
+ );
28
+ ---
29
+
30
+ <Base title="Book (print edition)" description="Full book rendered as a single paginated document for PDF export.">
31
+ <main class="prose print-edition">
32
+ {rendered.map(({ entry, Content }) => (
33
+ <section class="chapter-print">
34
+ <ChapterHeader data={entry.data} />
35
+ <Content />
36
+ </section>
37
+ ))}
38
+ </main>
39
+ </Base>
@@ -0,0 +1,160 @@
1
+ ---
2
+ /**
3
+ * /references — the book's bibliography page.
4
+ *
5
+ * Renders every entry in src/data/references.json (produced by
6
+ * scripts/build-bib.mjs from guides/shared/references.bib). Each
7
+ * entry gets an anchor ID matching its bibkey, so `<Cite key="gu2024mamba" />`
8
+ * elsewhere on the site links to /references#gu2024mamba.
9
+ *
10
+ * Sorted alphabetically by first-author surname, then by year.
11
+ * arXiv-style notes are surfaced as direct links when present.
12
+ */
13
+ import Base from '../layouts/Base.astro';
14
+
15
+ type CslAuthor = { family?: string; given?: string; literal?: string };
16
+ type CslEntry = {
17
+ id: string;
18
+ type?: string;
19
+ author?: CslAuthor[];
20
+ issued?: { 'date-parts'?: number[][] };
21
+ title?: string;
22
+ 'container-title'?: string;
23
+ publisher?: string;
24
+ volume?: string;
25
+ issue?: string;
26
+ page?: string;
27
+ URL?: string;
28
+ DOI?: string;
29
+ note?: string;
30
+ };
31
+
32
+ // Resolve references.json from consumer's project root. Missing file -> empty
33
+ // list (page renders an "empty bibliography" notice rather than crashing).
34
+ const refsModules = import.meta.glob<{ default: Record<string, CslEntry> }>(
35
+ '/src/data/references.json',
36
+ { eager: true },
37
+ );
38
+ const refsModule = refsModules['/src/data/references.json'];
39
+ const map = (refsModule?.default ?? {}) as Record<string, CslEntry>;
40
+ const entries = Object.values(map);
41
+
42
+ const surname = (a: CslAuthor): string =>
43
+ (a.family ?? a.literal ?? '').toLowerCase();
44
+
45
+ const firstAuthor = (e: CslEntry): string =>
46
+ e.author && e.author.length > 0 ? surname(e.author[0]) : '~';
47
+
48
+ const year = (e: CslEntry): number =>
49
+ e.issued?.['date-parts']?.[0]?.[0] ?? 0;
50
+
51
+ entries.sort((a, b) => {
52
+ const sa = firstAuthor(a);
53
+ const sb = firstAuthor(b);
54
+ if (sa !== sb) return sa < sb ? -1 : 1;
55
+ return year(a) - year(b);
56
+ });
57
+
58
+ function formatAuthors(authors: CslAuthor[] | undefined): string {
59
+ if (!authors || authors.length === 0) return '(anon)';
60
+ const names = authors.map((a) => {
61
+ const family = a.family ?? a.literal ?? '';
62
+ const given = a.given ? `${a.given} ` : '';
63
+ return `${given}${family}`.trim();
64
+ });
65
+ if (names.length === 1) return names[0];
66
+ if (names.length === 2) return `${names[0]} and ${names[1]}`;
67
+ return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`;
68
+ }
69
+
70
+ // arXiv URL extraction from `note` field (existing .bib uses `note = {arXiv:2111.00396}`).
71
+ function arxivUrl(note?: string): string | null {
72
+ if (!note) return null;
73
+ const m = note.match(/arXiv:\s*(\S+)/i);
74
+ return m ? `https://arxiv.org/abs/${m[1]}` : null;
75
+ }
76
+ ---
77
+ <Base
78
+ title="References — Post-Transformers"
79
+ description="Bibliography for the post_transformers guide, generated from guides/shared/references.bib."
80
+ >
81
+ <article class="prose">
82
+ <header>
83
+ <h1>References</h1>
84
+ <p class="lede">
85
+ Every paper, book, and software citation in this guide, sorted
86
+ alphabetically by first-author surname. Click an entry's
87
+ anchor to share a deep link, or follow the arXiv / DOI / URL
88
+ for the source.
89
+ </p>
90
+ <p>
91
+ <small>
92
+ {entries.length} entries. Generated from
93
+ <code>guides/shared/references.bib</code> at build time via
94
+ <code>scripts/build-bib.mjs</code>.
95
+ </small>
96
+ </p>
97
+ </header>
98
+
99
+ <ol class="references-list">
100
+ {entries.map((e) => {
101
+ const y = year(e);
102
+ const arxiv = arxivUrl(e.note);
103
+ const primaryUrl = arxiv ?? e.URL ?? (e.DOI ? `https://doi.org/${e.DOI}` : null);
104
+ return (
105
+ <li id={e.id} class="reference-entry">
106
+ <span class="reference-key" aria-label="bibkey">[{e.id}]</span>
107
+ <span class="reference-text">
108
+ {formatAuthors(e.author)}
109
+ {y > 0 && <> ({y})</>}.
110
+ {' '}
111
+ <em>{e.title}</em>.
112
+ {e['container-title'] && <> {e['container-title']}.</>}
113
+ {e.publisher && !e['container-title'] && <> {e.publisher}.</>}
114
+ {e.volume && <> Vol. {e.volume}{e.issue && <>, no. {e.issue}</>}.</>}
115
+ {e.page && <> pp. {e.page}.</>}
116
+ {primaryUrl && (
117
+ <>
118
+ {' '}
119
+ <a href={primaryUrl} rel="external noopener">link</a>.
120
+ </>
121
+ )}
122
+ </span>
123
+ </li>
124
+ );
125
+ })}
126
+ </ol>
127
+ </article>
128
+ </Base>
129
+
130
+ <style>
131
+ .references-list {
132
+ list-style: none;
133
+ padding: 0;
134
+ counter-reset: ref;
135
+ }
136
+ .reference-entry {
137
+ margin: var(--space-3) 0;
138
+ padding: var(--space-2) 0 var(--space-2) var(--space-4);
139
+ border-left: 2px solid transparent;
140
+ transition: border-color 200ms ease;
141
+ }
142
+ .reference-entry:target {
143
+ border-left-color: var(--callout-info);
144
+ background: var(--warm-blue-tint);
145
+ }
146
+ .reference-key {
147
+ display: inline-block;
148
+ font-family: var(--font-code);
149
+ font-size: var(--text-xs);
150
+ color: var(--color-text-muted);
151
+ margin-right: var(--space-2);
152
+ vertical-align: baseline;
153
+ }
154
+ .reference-text {
155
+ line-height: var(--leading-normal);
156
+ }
157
+ .reference-text em {
158
+ font-style: italic;
159
+ }
160
+ </style>