@brandon_m_behring/book-scaffold-astro 4.23.0 → 4.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/CLAUDE.md +9 -1
- package/components/ChapterNav.astro +3 -2
- package/components/ChapterTOC.astro +8 -1
- package/components/Cite.astro +2 -1
- package/components/Epigraph.astro +28 -0
- package/components/EvidenceTag.astro +102 -0
- package/components/MarginFigure.astro +49 -0
- package/components/Newthought.astro +18 -0
- package/components/PartReview.astro +2 -1
- package/components/Rationale.astro +1 -1
- package/components/SectionMap.astro +56 -0
- package/components/SectionMap.tsx +170 -0
- package/components/Sidebar.astro +10 -7
- package/components/Term.astro +2 -1
- package/components/TipsCard.astro +2 -1
- package/dist/components/ExamRunner.d.ts +2 -2
- package/dist/components/Flashcards.d.ts +2 -2
- package/dist/components/SectionMap.d.ts +10 -0
- package/dist/components/SectionMap.mjs +107 -0
- package/dist/{exam-manifest-DbTHo90M.d.ts → exam-manifest-DnqyzZ9I.d.ts} +1 -1
- package/dist/{flashcards-DPFFQhP8.d.ts → flashcards-okekZcl8.d.ts} +1 -1
- package/dist/index.d.ts +71 -8
- package/dist/index.mjs +38 -5
- package/dist/{schemas-DDWDRUxs.d.ts → schemas-CKipJ5Ie.d.ts} +22 -1
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.mjs +15 -5
- package/dist/{types-CGN95mrX.d.ts → types-Bn7rKhgP.d.ts} +1 -1
- package/layouts/Base.astro +4 -3
- package/layouts/Chapter.astro +36 -1
- package/package.json +11 -1
- package/recipes/04-component-library.md +6 -2
- package/recipes/16-tikz-figures.md +39 -0
- package/src/lib/section-map.ts +88 -0
- package/src/profiles/academic.ts +1 -1
- package/src/profiles/course-notes.ts +1 -1
- package/src/profiles/minimal.ts +1 -1
- package/src/profiles/research-portfolio.ts +1 -1
- package/src/profiles/tools.ts +1 -0
- package/src/schemas.ts +15 -0
- package/styles/layout.css +64 -0
- package/styles/section-map.css +130 -0
- package/styles/typography.css +51 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.25.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -59,13 +59,17 @@
|
|
|
59
59
|
"./components/Diagnostic.astro": "./components/Diagnostic.astro",
|
|
60
60
|
"./components/Divergence.astro": "./components/Divergence.astro",
|
|
61
61
|
"./components/DynConnect.astro": "./components/DynConnect.astro",
|
|
62
|
+
"./components/Epigraph.astro": "./components/Epigraph.astro",
|
|
63
|
+
"./components/EvidenceTag.astro": "./components/EvidenceTag.astro",
|
|
62
64
|
"./components/ExampleBox.astro": "./components/ExampleBox.astro",
|
|
63
65
|
"./components/Exercise.astro": "./components/Exercise.astro",
|
|
64
66
|
"./components/ExerciseSolutions.astro": "./components/ExerciseSolutions.astro",
|
|
65
67
|
"./components/Figure.astro": "./components/Figure.astro",
|
|
66
68
|
"./components/InsightBox.astro": "./components/InsightBox.astro",
|
|
67
69
|
"./components/KeyIdea.astro": "./components/KeyIdea.astro",
|
|
70
|
+
"./components/MarginFigure.astro": "./components/MarginFigure.astro",
|
|
68
71
|
"./components/MarginNote.astro": "./components/MarginNote.astro",
|
|
72
|
+
"./components/Newthought.astro": "./components/Newthought.astro",
|
|
69
73
|
"./components/NoteBox.astro": "./components/NoteBox.astro",
|
|
70
74
|
"./components/ObjectiveMap.astro": "./components/ObjectiveMap.astro",
|
|
71
75
|
"./components/OpenQuestion.astro": "./components/OpenQuestion.astro",
|
|
@@ -82,6 +86,7 @@
|
|
|
82
86
|
"./components/Rationale.astro": "./components/Rationale.astro",
|
|
83
87
|
"./components/Recovery.astro": "./components/Recovery.astro",
|
|
84
88
|
"./components/ResultBox.astro": "./components/ResultBox.astro",
|
|
89
|
+
"./components/SectionMap.astro": "./components/SectionMap.astro",
|
|
85
90
|
"./components/Sidebar.astro": "./components/Sidebar.astro",
|
|
86
91
|
"./components/Sidenote.astro": "./components/Sidenote.astro",
|
|
87
92
|
"./components/SkillBox.astro": "./components/SkillBox.astro",
|
|
@@ -111,6 +116,10 @@
|
|
|
111
116
|
"types": "./dist/components/Flashcards.d.ts",
|
|
112
117
|
"import": "./dist/components/Flashcards.mjs"
|
|
113
118
|
},
|
|
119
|
+
"./components/SectionMap": {
|
|
120
|
+
"types": "./dist/components/SectionMap.d.ts",
|
|
121
|
+
"import": "./dist/components/SectionMap.mjs"
|
|
122
|
+
},
|
|
114
123
|
"./components/WarnBox.astro": "./components/WarnBox.astro",
|
|
115
124
|
"./components/WeekRef.astro": "./components/WeekRef.astro",
|
|
116
125
|
"./components/WorkedExample.astro": "./components/WorkedExample.astro",
|
|
@@ -127,6 +136,7 @@
|
|
|
127
136
|
"./styles/tool-filter.css": "./styles/tool-filter.css",
|
|
128
137
|
"./styles/exam-runner.css": "./styles/exam-runner.css",
|
|
129
138
|
"./styles/flashcards.css": "./styles/flashcards.css",
|
|
139
|
+
"./styles/section-map.css": "./styles/section-map.css",
|
|
130
140
|
"./layouts/Base.astro": "./layouts/Base.astro",
|
|
131
141
|
"./layouts/Chapter.astro": "./layouts/Chapter.astro",
|
|
132
142
|
"./lib": {
|
|
@@ -65,8 +65,12 @@ Supported `type` values: `theorem`, `proposition`, `lemma`, `corollary`, `defini
|
|
|
65
65
|
| `Cite` | Inline citation linked to `/references` | `<Cite key="gu2024mamba" page="3" />` |
|
|
66
66
|
| `XRef` | Cross-reference to a labeled element | `<XRef id="thm:zoh-stability" />` |
|
|
67
67
|
| `Figure` | Image/SVG + caption + id; local SVGs inline for a11y + dark mode (`alt`, `desc`) | `<Figure src="/figures/week04/eigenvalues.svg" caption="…" alt="…" id="fig-eig" />` |
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
68
|
+
| `MarginFigure` | A `Figure` that **floats into the right gutter** (Tufte margin figure; ≥64rem, inline on mobile). Same props as `Figure`; rendering delegated to it. For full-bleed use `<Figure class="wide">` | `<MarginFigure src="/figures/week04/eig.svg" caption="…" alt="…" id="fig-eig-m" />` |
|
|
69
|
+
| `MarginNote` | **Inline** colored callout in the text column (does **not** float to the margin, despite the name) — load-bearing aside the reader must see | `<MarginNote>side comment</MarginNote>` |
|
|
70
|
+
| `Sidenote` | Auto-numbered Tufte marginalia that **floats into the right gutter** on desktop (inline on mobile) | `<Sidenote>numbered note</Sidenote>` |
|
|
71
|
+
| `EvidenceTag` | Inline claim-confidence pill after a claim (`verified`/`inference`/`audit-corrected`) | `<EvidenceTag kind="verified" /> … <EvidenceTag kind="inference" />` |
|
|
72
|
+
| `Newthought` | Tufte run-in small-caps section opener | `<p><Newthought>In practice</Newthought>, …</p>` |
|
|
73
|
+
| `Epigraph` | Chapter-opening italic quotation with right-aligned attribution | `<Epigraph>…<Fragment slot="attribution">Hamming</Fragment></Epigraph>` |
|
|
70
74
|
| `WeekRef` | Jump-link to a week chapter | `<WeekRef week={4} />` |
|
|
71
75
|
| `CodeRef` | GitHub-deep-link to file:line | `<CodeRef path="experiments/jax/foo.py" line={42} />` |
|
|
72
76
|
| `CodeBlock` | Embed code-file range with syntax highlight | `<CodeBlock src="…" lines="10-30" />` |
|
|
@@ -95,6 +95,45 @@ Common failures:
|
|
|
95
95
|
|
|
96
96
|
If you hit friction with the TikZ pipeline (a TikZ feature that doesn't compile, an obscure error message, a workflow pattern that doesn't fit), file an issue at https://github.com/brandon-behring/book-scaffold-astro/issues with the `consumer:<your-workspace>` label. v4.x is the iteration window.
|
|
97
97
|
|
|
98
|
+
## Margin & full-width figure placement (1d)
|
|
99
|
+
|
|
100
|
+
Once a figure exists, three placements are available — all reuse the existing Tufte float + gutter technique (the same one `Sidenote`/SectionMap use); none introduce a grid.
|
|
101
|
+
|
|
102
|
+
**In the running text (default).** Plain `<Figure>` constrained to the main measure:
|
|
103
|
+
|
|
104
|
+
```mdx
|
|
105
|
+
<Figure src="/figures/topic/diagram.svg" caption="…" alt="…" id="fig-x" />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**In the right margin** — `<MarginFigure>` floats into the gutter at ≥64rem and reflows inline below on mobile. Same props as `<Figure>` (rendering is delegated to it); `width` defaults to `100%` of the ~28ch gutter column, and the caption shrinks:
|
|
109
|
+
|
|
110
|
+
```mdx
|
|
111
|
+
import MarginFigure from '@brandon_m_behring/book-scaffold-astro/components/MarginFigure.astro';
|
|
112
|
+
|
|
113
|
+
<MarginFigure src="/figures/topic/diagram.svg" caption="A small aside figure." alt="…" id="fig-x-margin" />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Full-bleed** — a wide figure/table/code block that spans the gutter column too. Use the canonical `.wide` escape (or its alias `.column-page`):
|
|
117
|
+
|
|
118
|
+
```mdx
|
|
119
|
+
<Figure src="/figures/topic/wide-plot.svg" caption="…" alt="…" id="fig-wide" class="wide" />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Generic gutter block** — to float a non-figure block (a small note, a key figure number) into the margin, add `class="column-margin"` to any element. It uses the identical gutter float.
|
|
123
|
+
|
|
124
|
+
### Per-page width knob: `layout: wide`
|
|
125
|
+
|
|
126
|
+
For a chapter that's figure- or table-heavy, widen the **main text measure** via an optional frontmatter field (closed enum `default` | `wide`, accepted by every profile):
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
---
|
|
130
|
+
title: "Architecture Atlas"
|
|
131
|
+
layout: wide # widens --measure-main to 80ch; omit (or 'default') for the standard 65ch
|
|
132
|
+
---
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`Chapter.astro` threads this as `data-layout="wide"` on `<article class="prose">`, and `layout.css` maps `.prose[data-layout="wide"] { --measure-main: 80ch }`. Omitting the field (or setting `layout: default`) emits no attribute and applies no rule — existing chapters render unchanged. A value outside the enum fails the build at content load.
|
|
136
|
+
|
|
98
137
|
## See also
|
|
99
138
|
|
|
100
139
|
- `recipes/06-figures.md` — overall figure pipeline + matplotlib/svg sources
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* section-map.ts — PURE heading-selection + scrollspy logic for the right-gutter
|
|
3
|
+
* "On this page" section map (#section-map).
|
|
4
|
+
*
|
|
5
|
+
* No DOM, no Preact: this is the testable core under both ChapterTOC.astro (the
|
|
6
|
+
* collapsed mobile fallback) and the SectionMap island (the sticky gutter nav).
|
|
7
|
+
* Two pure, total functions:
|
|
8
|
+
*
|
|
9
|
+
* - tocHeadings: the ONE filter (h2+h3) that both the fallback TOC and the
|
|
10
|
+
* gutter map share — a single source of truth so the two
|
|
11
|
+
* never disagree about which headings are "on this page".
|
|
12
|
+
* - pickActive: given the headings currently intersecting the viewport (slug
|
|
13
|
+
* + viewport-relative top), choose which one is "active". The
|
|
14
|
+
* island feeds it IntersectionObserver state; node:test feeds
|
|
15
|
+
* it plain arrays. Browser geometry stays OUT of this file.
|
|
16
|
+
*
|
|
17
|
+
* Both are unit-tested in tests/section-map.test.mjs (node:test, no browser),
|
|
18
|
+
* mirroring exam-engine.ts.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { MarkdownHeading } from 'astro';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filter a chapter's headings to the TOC set: depth 2–3 only. h1 is the chapter
|
|
25
|
+
* title (rendered by ChapterHeader, not the body) and h4+ is noise in an anchor
|
|
26
|
+
* list. This is the shared contract — ChapterTOC.astro and SectionMap.astro both
|
|
27
|
+
* call it so the fallback and the gutter map carry IDENTICAL entries. Pure: it
|
|
28
|
+
* copies (filter) rather than mutating the input.
|
|
29
|
+
*/
|
|
30
|
+
export function tocHeadings(headings: MarkdownHeading[]): MarkdownHeading[] {
|
|
31
|
+
return headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One visible heading, as the island measures it: its slug and its
|
|
36
|
+
* viewport-relative top (CSS pixels — `getBoundingClientRect().top`). A negative
|
|
37
|
+
* `top` means the heading has scrolled above the top of the viewport.
|
|
38
|
+
*/
|
|
39
|
+
export interface VisibleHeading {
|
|
40
|
+
slug: string;
|
|
41
|
+
/** Viewport-relative top in px (negative = scrolled above the fold). */
|
|
42
|
+
top: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Choose the active section slug from the headings currently intersecting the
|
|
47
|
+
* viewport.
|
|
48
|
+
*
|
|
49
|
+
* Rule (a stable, total scrollspy):
|
|
50
|
+
* 1. Prefer the topmost heading at or below the top of the viewport — the
|
|
51
|
+
* visible heading with the SMALLEST non-negative `top`. That's the section
|
|
52
|
+
* the reader has just scrolled to / is reading into.
|
|
53
|
+
* 2. If every visible heading is above the fold (all `top` negative — e.g. a
|
|
54
|
+
* heading STRADDLING the top edge: its top is slightly negative but its box
|
|
55
|
+
* still overlaps the top zone, so it's intersecting yet has no non-negative
|
|
56
|
+
* sibling below it), fall back to the one nearest the fold from above: the
|
|
57
|
+
* GREATEST `top` (closest to 0 from the negative side). That keeps the
|
|
58
|
+
* enclosing section lit instead of going dark mid-section. (A heading whose
|
|
59
|
+
* box FULLY scrolled above the top has left `inView` entirely — it isn't in
|
|
60
|
+
* `visible` at all, and is handled by the empty-set → `prev` branch below.)
|
|
61
|
+
* 3. If NOTHING is visible (the observer reports an empty set — between two
|
|
62
|
+
* sparse intersections, or scrolled past the last heading), retain `prev`
|
|
63
|
+
* so the highlight is sticky rather than flickering off.
|
|
64
|
+
*
|
|
65
|
+
* Pure + total: no DOM, no throw, deterministic. Ties (equal `top`) resolve to
|
|
66
|
+
* the first in iteration order, which the island passes in document order.
|
|
67
|
+
*/
|
|
68
|
+
export function pickActive(
|
|
69
|
+
visible: ReadonlyArray<VisibleHeading>,
|
|
70
|
+
prev: string | null,
|
|
71
|
+
): string | null {
|
|
72
|
+
if (visible.length === 0) return prev;
|
|
73
|
+
|
|
74
|
+
let bestNonNeg: VisibleHeading | null = null; // smallest top >= 0
|
|
75
|
+
let bestAbove: VisibleHeading | null = null; // greatest top < 0
|
|
76
|
+
for (const h of visible) {
|
|
77
|
+
if (h.top >= 0) {
|
|
78
|
+
if (bestNonNeg === null || h.top < bestNonNeg.top) bestNonNeg = h;
|
|
79
|
+
} else {
|
|
80
|
+
if (bestAbove === null || h.top > bestAbove.top) bestAbove = h;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (bestNonNeg !== null) return bestNonNeg.slug;
|
|
85
|
+
// All above the fold: the one just above the top edge keeps its section lit.
|
|
86
|
+
if (bestAbove !== null) return bestAbove.slug;
|
|
87
|
+
return prev;
|
|
88
|
+
}
|
package/src/profiles/academic.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const academicProfile = defineProfile({
|
|
|
31
31
|
flashcards: false, // v4.22.0 #116: opt-in per book; requires src/content/glossary/
|
|
32
32
|
landing: true, // v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
|
|
33
33
|
},
|
|
34
|
-
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
34
|
+
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css', 'section-map.css'],
|
|
35
35
|
katex: true,
|
|
36
36
|
chaptersRenderer: academicChaptersRenderer, // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
|
|
37
37
|
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
|
@@ -33,7 +33,7 @@ export const courseNotesProfile = defineProfile({
|
|
|
33
33
|
flashcards: false, // v4.22.0 #116: opt-in per book; requires src/content/glossary/
|
|
34
34
|
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
35
35
|
},
|
|
36
|
-
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
36
|
+
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css', 'section-map.css'],
|
|
37
37
|
// v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
|
|
38
38
|
chaptersRenderer: fallbackChaptersRenderer,
|
|
39
39
|
// v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
|
package/src/profiles/minimal.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const minimalProfile = defineProfile({
|
|
|
28
28
|
flashcards: false, // v4.22.0 #116: opt-in per book; requires src/content/glossary/
|
|
29
29
|
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
30
30
|
},
|
|
31
|
-
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
31
|
+
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css', 'section-map.css'],
|
|
32
32
|
// v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
|
|
33
33
|
chaptersRenderer: fallbackChaptersRenderer,
|
|
34
34
|
});
|
|
@@ -44,7 +44,7 @@ export const researchPortfolioProfile = defineProfile({
|
|
|
44
44
|
flashcards: false, // v4.22.0 #116: opt-in per book; requires src/content/glossary/
|
|
45
45
|
landing: true, // v4.5.0: auto-inject minimal root landing
|
|
46
46
|
},
|
|
47
|
-
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
|
|
47
|
+
styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css', 'section-map.css'],
|
|
48
48
|
katex: true, // math is common in research content
|
|
49
49
|
// v3.7.0 (#35): portfolio schema is a union of academic + tools shapes — fallback renderer dispatches per chapter via field presence
|
|
50
50
|
chaptersRenderer: fallbackChaptersRenderer,
|
package/src/profiles/tools.ts
CHANGED
|
@@ -31,6 +31,7 @@ export const toolsProfile = defineProfile({
|
|
|
31
31
|
styles: [
|
|
32
32
|
'tokens.css', 'layout.css', 'callouts.css', 'chapter.css',
|
|
33
33
|
'typography.css', 'print.css', 'convergence.css', 'tool-filter.css',
|
|
34
|
+
'section-map.css',
|
|
34
35
|
],
|
|
35
36
|
chaptersRenderer: toolsChaptersRenderer, // v3.7.0 (#35) — owns /chapters semantics for tools shape
|
|
36
37
|
});
|
package/src/schemas.ts
CHANGED
|
@@ -70,6 +70,17 @@ export const chapterStatus = [
|
|
|
70
70
|
'planned',
|
|
71
71
|
] as const;
|
|
72
72
|
|
|
73
|
+
// ===== Per-page width knob (1d) — any profile =====
|
|
74
|
+
//
|
|
75
|
+
// Optional `layout:` on EVERY chapter schema below. A CLOSED enum (a typo'd
|
|
76
|
+
// value fails loud at content load — that IS the validation, no assertEnumProp
|
|
77
|
+
// needed). Threaded by Chapter.astro as `data-layout` on <article class="prose">;
|
|
78
|
+
// layout.css maps `[data-layout="wide"]` → a wider --measure-main. ADDITIVE:
|
|
79
|
+
// 'default' (or an omitted field) applies no rule, so existing chapters render
|
|
80
|
+
// byte-identically. 'wide' widens the main text measure (e.g. 80ch) for
|
|
81
|
+
// figure-heavy or table-heavy chapters.
|
|
82
|
+
export const layoutModes = ['default', 'wide'] as const;
|
|
83
|
+
|
|
73
84
|
// ===== Provenance (v4.8.0) — process-as-artifact audit trail =====
|
|
74
85
|
//
|
|
75
86
|
// Optional per-chapter block attached to EVERY profile schema below.
|
|
@@ -117,6 +128,7 @@ export const academicChapterSchema = z.object({
|
|
|
117
128
|
part: z.enum(academicParts),
|
|
118
129
|
title: z.string().min(1),
|
|
119
130
|
slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
131
|
+
layout: z.enum(layoutModes).optional(), // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
|
|
120
132
|
status: z.enum(chapterStatus),
|
|
121
133
|
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
122
134
|
code_path: z.string().optional(),
|
|
@@ -138,6 +150,7 @@ export const academicChapterSchema = z.object({
|
|
|
138
150
|
export const toolsChapterSchema = z.object({
|
|
139
151
|
title: z.string().min(1),
|
|
140
152
|
slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
153
|
+
layout: z.enum(layoutModes).optional(), // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
|
|
141
154
|
part: z.number().int().min(0).max(10),
|
|
142
155
|
chapter: z.number().int().min(0).max(99),
|
|
143
156
|
volatility: z.enum(volatilityLevels),
|
|
@@ -181,6 +194,7 @@ export const courseNotesChapterSchema = z.object({
|
|
|
181
194
|
// Identity
|
|
182
195
|
title: z.string().min(1),
|
|
183
196
|
slug: z.string().optional(), // v4.9.0: explicit URL slug override (else filename → entry.id)
|
|
197
|
+
layout: z.enum(layoutModes).optional(), // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
|
|
184
198
|
chapter: z.number().int().min(0).max(99),
|
|
185
199
|
part: z.number().int().min(0).max(20).default(1),
|
|
186
200
|
description: z.string().optional(),
|
|
@@ -244,6 +258,7 @@ export const researchPortfolioChapterSchema = z.object({
|
|
|
244
258
|
// Identity
|
|
245
259
|
title: z.string().min(1),
|
|
246
260
|
slug: z.string().optional(), // explicit slug override (otherwise filename)
|
|
261
|
+
layout: z.enum(layoutModes).optional(), // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
|
|
247
262
|
description: z.string().optional(),
|
|
248
263
|
|
|
249
264
|
// Hierarchy — accept either academic-style or tools-style; all optional.
|
package/styles/layout.css
CHANGED
|
@@ -154,3 +154,67 @@
|
|
|
154
154
|
max-width: none !important;
|
|
155
155
|
width: 100%;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/* ===== 1d: additive figure/content placement + per-page width knob =====
|
|
159
|
+
*
|
|
160
|
+
* STRICTLY ADDITIVE. Nothing here changes `.prose`'s block layout or the
|
|
161
|
+
* `.sidenote` float — the gutter placements below REUSE the same Tufte
|
|
162
|
+
* float + negative-margin-right technique `.sidenote` and SectionMap use
|
|
163
|
+
* (the full float→grid migration is deferred). All values are tokens.
|
|
164
|
+
*
|
|
165
|
+
* .column-margin — generic "float a block into the right gutter" utility
|
|
166
|
+
* (the un-figure version of MarginFigure's placement).
|
|
167
|
+
* .margin-figure — MarginFigure's wrapper: the same gutter float, tuned for
|
|
168
|
+
* a figure (small caption, no italic).
|
|
169
|
+
* .column-page — full-width breakout; an ALIAS of `.wide` (which already
|
|
170
|
+
* does the max-width override). Use either.
|
|
171
|
+
* .prose[data-layout="wide"] — the per-page width knob: widen the main text
|
|
172
|
+
* measure. Only applies when Chapter.astro emits
|
|
173
|
+
* data-layout="wide" from frontmatter `layout: wide`.
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/* Float a block into the right gutter — same mechanism as .sidenote: the
|
|
177
|
+
* negative margin-right pulls the box into the --measure-side column so it
|
|
178
|
+
* sits BESIDE the running text instead of pushing the measure. Shown only at
|
|
179
|
+
* >= 64rem (the gutter breakpoint, matching SectionMap); below that it reflows
|
|
180
|
+
* inline in document flow (the rule simply doesn't apply). */
|
|
181
|
+
@media (min-width: 64rem) {
|
|
182
|
+
.column-margin,
|
|
183
|
+
.margin-figure {
|
|
184
|
+
float: right;
|
|
185
|
+
clear: right;
|
|
186
|
+
width: var(--measure-side);
|
|
187
|
+
margin-right: calc(-1 * (var(--measure-side) + var(--space-6)));
|
|
188
|
+
margin-top: 0.3rem;
|
|
189
|
+
margin-bottom: var(--space-3);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* MarginFigure: a Figure wrapped for the gutter. The wrapped <figure> fills
|
|
194
|
+
* the column; the caption stays small (margin context = less room) without
|
|
195
|
+
* touching the global .figure/figcaption rules. */
|
|
196
|
+
.margin-figure > .figure {
|
|
197
|
+
margin: 0;
|
|
198
|
+
max-width: none;
|
|
199
|
+
}
|
|
200
|
+
.margin-figure figcaption {
|
|
201
|
+
font-size: var(--text-xs);
|
|
202
|
+
line-height: var(--leading-normal);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Full-width breakout: an alias of `.wide` (figures, wide tables, code that
|
|
206
|
+
* need the whole container including the gutter column). `.wide` is the
|
|
207
|
+
* canonical escape; `.column-page` reads better next to `.column-margin`. */
|
|
208
|
+
.column-page {
|
|
209
|
+
max-width: none !important;
|
|
210
|
+
width: 100%;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Per-page width knob (frontmatter `layout: wide`). Widen ONLY the main text
|
|
214
|
+
* measure; the container + gutter math in `.prose` already references
|
|
215
|
+
* --measure-main, so this one override flows through. Additive: absent
|
|
216
|
+
* data-layout (or "default") matches nothing, so existing chapters are
|
|
217
|
+
* unaffected. 80ch matches the wide-desktop tier's main measure (tokens.css). */
|
|
218
|
+
.prose[data-layout="wide"] {
|
|
219
|
+
--measure-main: 80ch;
|
|
220
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* section-map.css — right-gutter sticky "On this page" scrollspy (#section-map).
|
|
2
|
+
*
|
|
3
|
+
* The SectionMap island is framework-component-driven markup outside Astro's
|
|
4
|
+
* style scoping, so its nav ships as a plain stylesheet (like exam-runner.css /
|
|
5
|
+
* flashcards.css). Colors via design tokens only — recolors with [data-theme]
|
|
6
|
+
* automatically.
|
|
7
|
+
*
|
|
8
|
+
* Layout: the nav lives in the .prose RIGHT gutter — the same ~28ch column the
|
|
9
|
+
* sidenotes float into (layout.css). It uses the identical float + negative
|
|
10
|
+
* margin-right technique so it sits beside the running text rather than pushing
|
|
11
|
+
* it, and is `position: sticky` so it tracks the reader down the page.
|
|
12
|
+
*
|
|
13
|
+
* Visibility handshake with ChapterTOC (chapter.css): the gutter map shows ONLY
|
|
14
|
+
* at >= 64rem (the sidebar/gutter breakpoint, where there's room for a margin
|
|
15
|
+
* column); the collapsed top ChapterTOC is the < 64rem fallback. Each is hidden
|
|
16
|
+
* in the other's range so they never double-show. At >= 64rem the handshake is
|
|
17
|
+
* ALSO sidenote-aware: the gutter is a single column the sidenotes already float
|
|
18
|
+
* into (layout.css), so a chapter with <Sidenote>s yields it back to them — the
|
|
19
|
+
* map hides and ChapterTOC stays as the nav (see the @media block below, #151).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/* Default (< 64rem): the gutter has no room — the collapsed ChapterTOC is the
|
|
23
|
+
* fallback. Hidden until the breakpoint opens it below. */
|
|
24
|
+
.section-map {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* The island's mount node is a zero-footprint anchor; never shown. */
|
|
29
|
+
.section-map-mount {
|
|
30
|
+
display: none !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (min-width: 64rem) {
|
|
34
|
+
/* Sidenote-aware handshake (#151). The gutter map and <Sidenote>s float into
|
|
35
|
+
* the SAME ~28ch right column (layout.css), so they cannot coexist there:
|
|
36
|
+
* - A chapter WITH sidenotes → the gutter belongs to the sidenotes; suppress
|
|
37
|
+
* the map (else its sticky float overlaps the notes on scroll) and keep the
|
|
38
|
+
* collapsed ChapterTOC visible as the nav.
|
|
39
|
+
* - A chapter with NO sidenotes → the gutter map IS the nav; hide the
|
|
40
|
+
* collapsed top ChapterTOC so the two don't double-show (the original
|
|
41
|
+
* handshake).
|
|
42
|
+
* The suppressed .section-map stays in the DOM (display:none) — its island
|
|
43
|
+
* still runs but only toggles hidden links, which is harmless. (:has() is
|
|
44
|
+
* within the scaffold's CSS baseline — tokens.css already uses it.) */
|
|
45
|
+
.prose:has(.sidenote) .section-map {
|
|
46
|
+
display: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.prose:not(:has(.sidenote)) .chapter-toc {
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.section-map {
|
|
54
|
+
display: block;
|
|
55
|
+
/* Float into the right margin — same technique as .sidenote: the negative
|
|
56
|
+
* margin-right pulls it into the gutter column so it sits beside the text
|
|
57
|
+
* instead of pushing the measure. */
|
|
58
|
+
float: right;
|
|
59
|
+
clear: right;
|
|
60
|
+
width: var(--measure-side);
|
|
61
|
+
margin-right: calc(-1 * (var(--measure-side) + var(--space-6)));
|
|
62
|
+
margin-top: 0.3rem;
|
|
63
|
+
margin-bottom: var(--space-3);
|
|
64
|
+
padding-left: var(--space-3);
|
|
65
|
+
border-left: var(--border-bar) solid var(--color-border);
|
|
66
|
+
/* Sticky: stay in view as the reader scrolls; cap height + scroll its own
|
|
67
|
+
* overflow so a long TOC never runs off-screen. */
|
|
68
|
+
position: sticky;
|
|
69
|
+
top: var(--space-8);
|
|
70
|
+
max-height: calc(100vh - 2 * var(--space-8));
|
|
71
|
+
overflow: auto;
|
|
72
|
+
font-size: var(--text-sm);
|
|
73
|
+
line-height: var(--leading-normal);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.section-map-label {
|
|
77
|
+
margin: 0 0 var(--space-1);
|
|
78
|
+
font-size: var(--text-xs);
|
|
79
|
+
color: var(--color-text-muted);
|
|
80
|
+
font-style: italic;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.section-map-title {
|
|
84
|
+
margin: 0 0 var(--space-2);
|
|
85
|
+
font-size: var(--text-xs);
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: 0.06em;
|
|
88
|
+
color: var(--color-text-muted);
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.section-map-list {
|
|
93
|
+
list-style: none;
|
|
94
|
+
padding-left: 0;
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.section-map-list li {
|
|
99
|
+
margin: var(--space-1) 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.section-map-list a {
|
|
103
|
+
display: block;
|
|
104
|
+
text-decoration: none;
|
|
105
|
+
color: var(--color-text-muted);
|
|
106
|
+
/* The active link gains a left border (border-bar); reserve its width on
|
|
107
|
+
* every link so lighting one doesn't shift the others horizontally. */
|
|
108
|
+
border-left: var(--border-bar) solid transparent;
|
|
109
|
+
padding-left: var(--space-2);
|
|
110
|
+
margin-left: calc(-1 * var(--space-2));
|
|
111
|
+
transition: color 120ms ease;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* h3 links indent under their h2. */
|
|
115
|
+
.section-map-list a.toc-h3 {
|
|
116
|
+
padding-left: var(--space-5);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.section-map-list a:hover {
|
|
120
|
+
color: var(--color-link);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Active section (scrollspy): the island sets aria-current="page" + .active. */
|
|
124
|
+
.section-map-list a.active,
|
|
125
|
+
.section-map-list a[aria-current='page'] {
|
|
126
|
+
color: var(--color-link);
|
|
127
|
+
border-left-color: var(--color-link);
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
}
|
|
130
|
+
}
|
package/styles/typography.css
CHANGED
|
@@ -145,3 +145,54 @@ th {
|
|
|
145
145
|
font-weight: 600;
|
|
146
146
|
color: var(--color-heading);
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/* ===== Tufte typographic openers (v4.25.0) =====
|
|
150
|
+
* Ported from the LaTeX book (interview-preamble-tufte.sty): the run-in
|
|
151
|
+
* <Newthought> section opener and the chapter-opening <Epigraph>. Markup is in
|
|
152
|
+
* the matching .astro components; styling stays here so it's always loaded.
|
|
153
|
+
* Token-only colors — recolor with [data-theme] automatically. */
|
|
154
|
+
|
|
155
|
+
/* <Newthought>: run-in small-caps opener. TRUE small-caps via the smcp OpenType
|
|
156
|
+
* feature (the Roboto variable face ships it) — font-variant: small-caps would
|
|
157
|
+
* synthesize faux small-caps by scaling capitals. Slightly heavier + tracked
|
|
158
|
+
* out for the Tufte look. */
|
|
159
|
+
.newthought {
|
|
160
|
+
font-feature-settings: "smcp";
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
letter-spacing: 0.05em;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* <Epigraph>: italic chapter-opening quotation with a right-aligned
|
|
166
|
+
* attribution. Inherits the running measure from .prose / --measure-main rather
|
|
167
|
+
* than forcing a width that would fight the Tufte measure. */
|
|
168
|
+
.epigraph {
|
|
169
|
+
margin: 0 0 var(--space-8) 0;
|
|
170
|
+
}
|
|
171
|
+
.epigraph blockquote {
|
|
172
|
+
/* Override the global blockquote left-bar: an epigraph reads as an opener,
|
|
173
|
+
* not a pull-quote. Keep it italic + muted. */
|
|
174
|
+
border-left: 0;
|
|
175
|
+
padding: 0;
|
|
176
|
+
margin: 0;
|
|
177
|
+
font-style: italic;
|
|
178
|
+
color: var(--color-text-muted);
|
|
179
|
+
}
|
|
180
|
+
.epigraph .epigraph-attribution {
|
|
181
|
+
margin-top: var(--space-2);
|
|
182
|
+
text-align: right;
|
|
183
|
+
font-style: normal;
|
|
184
|
+
font-size: var(--text-sm);
|
|
185
|
+
color: var(--color-text-muted);
|
|
186
|
+
}
|
|
187
|
+
/* The em-dash lead-in is conventional for an attribution; authors may also
|
|
188
|
+
* write it themselves, so it is not auto-inserted. */
|
|
189
|
+
|
|
190
|
+
/* .heading-accent — OPT-IN colored-italic heading utility (warm-blue italic).
|
|
191
|
+
* The global h1–h6 are already blue-italic (above); this class is for a heading
|
|
192
|
+
* an author wants to force into the accent style outside that cascade (e.g. a
|
|
193
|
+
* heading inside a card whose local styles reset it). Deliberately NOT applied
|
|
194
|
+
* to the global headings — restyling those would churn every page + baseline. */
|
|
195
|
+
.heading-accent {
|
|
196
|
+
color: var(--warm-blue);
|
|
197
|
+
font-style: italic;
|
|
198
|
+
}
|