@brandon_m_behring/book-scaffold-astro 4.26.3 → 4.27.1
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/AGENTS.md +6 -0
- package/CLAUDE.md +35 -10
- package/LATEX_TO_MDX_MAPPING.md +1 -1
- package/LICENSE +27 -0
- package/LICENSE-CONTENT +19 -0
- package/README.md +33 -0
- package/components/AssessmentTest.astro +2 -1
- package/components/ChapterNav.astro +2 -2
- package/components/Cite.astro +2 -1
- package/components/NavContent.astro +2 -2
- package/components/PartReview.astro +2 -1
- package/components/Rationale.astro +3 -2
- package/components/Sidebar.astro +2 -1
- package/components/Term.astro +2 -1
- package/components/TipsCard.astro +2 -1
- package/components/VersionSelector.tsx +39 -36
- package/components/WeekRef.astro +2 -1
- package/components/XRef.astro +2 -1
- package/dist/components/VersionSelector.d.ts +16 -2
- package/dist/components/VersionSelector.mjs +18 -17
- package/dist/index.d.ts +24 -6
- package/dist/index.mjs +105 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +8 -1
- package/dist/{types-CWXP1S4b.d.ts → types-CZrkqzpC.d.ts} +58 -26
- package/layouts/Base.astro +30 -19
- package/package.json +7 -4
- package/pages/answers.astro +2 -1
- package/pages/chapters.astro +2 -1
- package/pages/exercises.astro +2 -1
- package/pages/index.astro +4 -3
- package/pages/search.astro +2 -1
- package/pages/tips.astro +2 -1
- package/recipes/01-add-math.md +40 -28
- package/recipes/04-component-library.md +14 -4
- package/recipes/05-deploy-cloudflare.md +44 -0
- package/recipes/06-mobile-first-layout.md +25 -2
- package/recipes/09-validation.md +18 -9
- package/recipes/15-defining-styles.md +5 -2
- package/recipes/19-prevalidate-hook.md +29 -83
- package/scripts/build-bib.mjs +7 -3
- package/scripts/build-labels.mjs +33 -16
- package/scripts/read-env.mjs +25 -0
- package/scripts/resolve-book-config.mjs +96 -0
- package/scripts/validate.mjs +125 -84
- package/src/lib/define-style.ts +15 -6
- package/src/lib/nav-href.ts +24 -2
- package/styles/layout.css +14 -8
- package/styles/tokens.css +8 -10
package/AGENTS.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
The canonical, cross-tool authoring guide is [`CLAUDE.md`](CLAUDE.md) in this
|
|
4
|
+
directory. Read and follow it before changing a book. The guide is
|
|
5
|
+
agent-agnostic despite its filename; this pointer exists so tools that discover
|
|
6
|
+
`AGENTS.md` load the same instructions without maintaining two copies.
|
package/CLAUDE.md
CHANGED
|
@@ -14,14 +14,22 @@ If the hub isn't available in your environment (e.g. external contributor), the
|
|
|
14
14
|
|
|
15
15
|
## Profile
|
|
16
16
|
|
|
17
|
-
Read `
|
|
17
|
+
Read `BOOK_PRESET` (preferred) or its `BOOK_PROFILE` compatibility alias from
|
|
18
|
+
the environment or `.env`. It controls:
|
|
18
19
|
|
|
19
|
-
- Which content-collection
|
|
20
|
-
- Which markdown integrations run (KaTeX
|
|
20
|
+
- Which of the five content-collection schemas is enforced
|
|
21
|
+
- Which markdown integrations run (KaTeX for `academic` and `research-portfolio`)
|
|
21
22
|
- Which callout family is the "default" import in templates
|
|
22
|
-
- Whether ToolFilter
|
|
23
|
+
- Whether the ToolFilter Preact island mounts in the automatic chrome row
|
|
23
24
|
|
|
24
|
-
When in doubt, run `grep
|
|
25
|
+
When in doubt, run `grep -E 'BOOK_(PRESET|PROFILE)' .env astro.config.mjs src/content.config.ts` to see the wiring.
|
|
26
|
+
|
|
27
|
+
`VersionSelector` is different: it is a manual, prop-driven island because only
|
|
28
|
+
the consuming book knows which versions are actually deployed. Import it from
|
|
29
|
+
`@brandon_m_behring/book-scaffold-astro/components/VersionSelector`, pass
|
|
30
|
+
`versions: [{ href, label, date, current? }]`, and hydrate it where your own
|
|
31
|
+
navigation belongs. It renders nothing for an omitted or empty manifest;
|
|
32
|
+
`Base.astro` never invents or auto-mounts version links.
|
|
25
33
|
|
|
26
34
|
## Frontmatter schemas
|
|
27
35
|
|
|
@@ -94,9 +102,9 @@ sources:
|
|
|
94
102
|
|
|
95
103
|
Two callout families coexist. Authors import what they need.
|
|
96
104
|
|
|
97
|
-
**Tools family** (
|
|
105
|
+
**Tools family** (8 components, imported from the flat `@brandon_m_behring/book-scaffold-astro/components/<Name>.astro` path): `SkillBox`, `CaseStudy`, `ConceptBox`, `KeyIdea`, `TryThis`, `Recovery`, `Convergence`, `Divergence`.
|
|
98
106
|
|
|
99
|
-
**Academic family** (
|
|
107
|
+
**Academic family** (10 components, using the same flat import path): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof). **Props (v4.14.3, #121):** `kind=` is canonical; `type=` is accepted as a legacy alias (likewise `title=`/`label=` alias `name=`). An absent or unknown kind **throws at build** (via `src/lib/theorem-label`) rather than rendering an empty label; `book-scaffold validate` flags a `<Theorem>` with neither `kind=` nor `type=` even earlier. **Numbering (v4.18.0+, #126/#175):** a theorem with an `id` auto-numbers from `labels.json` — the same index `<XRef>` reads — so the heading number equals every cross-reference to it by construction; explicit `n=` is a fallback for un-id'd theorems. `build-labels` indexes the kind-accurate word (`Proposition 8.1`, not a kind-blind `Theorem 8.1`) and throws on an unknown kind. The theorem family shares one counter by default; set `numberStyle: 'per-kind'` in `defineBookConfig` or a composed `defineStyle` for independent theorem/proposition/lemma/etc. sequences. A `label=` override stays unnumbered and consumes no counter.
|
|
100
108
|
|
|
101
109
|
**Pedagogy family** (v4.1.0+, any profile, 4 components): `Pitfall` (rose; "common mistake" — distinct from `WarnBox`'s preemptive warning), `WorkedExample` (plum; collapsible `<details>` block with `#worked-example-{id}` anchor for deep links), `YouWillLearn` (gold; chapter-opener with optional `prerequisites` prop), `Diagnostic` (v4.19.0, #110; teal; pre-reading "Do I Know This Already?" DIKTA self-check — a slotted question list + a skip/skim/read routing rubric via `skimTo`, plus an optional collapsible answer key via `slot="answers"`). Slot bullets/code freely; render at any preset.
|
|
102
110
|
|
|
@@ -162,24 +170,41 @@ npm run build:bib # rebuild references.json after .bib edit
|
|
|
162
170
|
npm run pdf # render dist-pdf/book.pdf via Paged.js
|
|
163
171
|
```
|
|
164
172
|
|
|
165
|
-
|
|
173
|
+
Every generated preset uses the same `prevalidate` hook to rebuild applicable
|
|
174
|
+
bibliography and label indexes, while `prebuild` delegates to validation.
|
|
175
|
+
Figure and notebook conversion remain explicit authoring commands because
|
|
176
|
+
their system tools are optional. `prepdf` always runs the full site build
|
|
177
|
+
before previewing `/print/` and rendering `dist-pdf/book.pdf`.
|
|
166
178
|
|
|
167
179
|
## Deploy
|
|
168
180
|
|
|
169
|
-
|
|
181
|
+
The generated `wrangler.toml` uses Cloudflare Workers + Static Assets for
|
|
182
|
+
academic/tools/minimal presets and Cloudflare Pages for course-notes and
|
|
183
|
+
research-portfolio. Recipe 05 documents both flows.
|
|
170
184
|
|
|
171
185
|
For monorepo Astro projects (Astro project in subdir), prefix build + deploy commands with `cd <subdir> &&`.
|
|
172
186
|
|
|
187
|
+
Every build emits an audited Cloudflare-format `dist/_headers` by default
|
|
188
|
+
(HSTS, XCTO, Referrer-Policy, Permissions-Policy, and a CSP compatible with
|
|
189
|
+
the shipped inline UI, Pagefind WASM, Cloudflare analytics, and HTTPS images).
|
|
190
|
+
A consumer `public/_headers` wins unchanged. Use
|
|
191
|
+
`securityHeaders: { contentSecurityPolicy: "..." }` to replace only the CSP,
|
|
192
|
+
or `securityHeaders: false` when another layer owns all headers. Recipe 05 has
|
|
193
|
+
the full precedence and customization contract.
|
|
194
|
+
|
|
173
195
|
## Validation
|
|
174
196
|
|
|
175
197
|
`npm run validate` (also runs in prebuild) catches:
|
|
176
198
|
|
|
177
199
|
- Unknown `<Cite key>` (academic) — bibkey not in `references.json`
|
|
178
200
|
- Unknown `<XRef id>` — id not in `labels.json` (XRef silently renders `[?label]` otherwise)
|
|
201
|
+
- Literal `<Theorem n>` values that disagree with `labels.json` (dynamic expressions and `label=` overrides are skipped)
|
|
179
202
|
- Missing `<Figure src>` files under `public/`
|
|
180
203
|
- Internal markdown links that don't resolve
|
|
181
204
|
- Study-guide questions (v4.17.0, #112) — a question whose frontmatter `domain` isn't in `examDomains`, and duplicate question `id`s
|
|
182
205
|
|
|
206
|
+
Missing `src/data/labels.json` or `references.json` self-heals before checks by running the corresponding package build script; an existing artifact is never rewritten implicitly.
|
|
207
|
+
|
|
183
208
|
See `recipes/09-validation.md` to extend.
|
|
184
209
|
|
|
185
210
|
## Common authoring tasks
|
|
@@ -250,6 +275,6 @@ Types: `feat` / `fix` / `refactor` / `docs` / `test` / `chore` / `release`. One
|
|
|
250
275
|
|
|
251
276
|
## Reading this guide didn't help?
|
|
252
277
|
|
|
253
|
-
- `recipes/README.md` — index of all
|
|
278
|
+
- `recipes/README.md` — index of all recipes
|
|
254
279
|
- `recipes/08-decisions-ledger.md` — why everything is shaped the way it is
|
|
255
280
|
- `~/.claude/plans/i-want-to-investigate-recursive-yao.md` — full design discussion
|
package/LATEX_TO_MDX_MAPPING.md
CHANGED
|
@@ -43,7 +43,7 @@ Component subset table for tools-profile-specific UI (volatility dashboards, con
|
|
|
43
43
|
| Volatility badge | `Tag` | `volatility` enum chip in chapter meta |
|
|
44
44
|
| Practice tag (`\official{}` / `\practitioner{}` / `\convergence{}`, also `\tagofficial{}` / `\tagpractitioner{}` / `\tagconvergence{}`) | `Tag` | `kind="official" \| "practitioner" \| "convergence"` — inline assertion of source authority. Both the bare and `\tag*` prefixed LaTeX forms map to the same component (see `package/components/Tag.astro`). |
|
|
45
45
|
| Tool comparison | `ToolFilter` (island) | Interactive comparison gate |
|
|
46
|
-
| Version selector | `VersionSelector` (island) |
|
|
46
|
+
| Version selector | `VersionSelector` (island) | Manual opt-in; pass real `{ href, label, date, current? }` deployment entries |
|
|
47
47
|
| Convergence event | `Convergence` | "All tools converged here" timeline marker |
|
|
48
48
|
| Divergence event | `Divergence` | "Tool X went its own way" annotation |
|
|
49
49
|
| Pattern timeline | `PatternTimeline` | Multi-event convergence dashboard |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brandon Behring
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This license covers the toolkit's code, configuration, scripts, and tooling.
|
|
26
|
+
Recipe, pedagogy, example, and substantive guide content is licensed
|
|
27
|
+
separately under CC BY 4.0; see LICENSE-CONTENT.
|
package/LICENSE-CONTENT
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Creative Commons Attribution 4.0 International License (CC BY 4.0)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brandon Behring
|
|
4
|
+
|
|
5
|
+
This license covers prose and authored content shipped in this package,
|
|
6
|
+
including `recipes/`, `pedagogy/`, `examples/`, and substantive prose in
|
|
7
|
+
README.md, CLAUDE.md, and AGENTS.md. Toolkit code and configuration are
|
|
8
|
+
licensed separately under MIT; see LICENSE.
|
|
9
|
+
|
|
10
|
+
You are free to share and adapt this material for any purpose, including
|
|
11
|
+
commercially, provided that you give appropriate credit, link to the license,
|
|
12
|
+
and indicate whether changes were made. You may not apply additional legal or
|
|
13
|
+
technological restrictions.
|
|
14
|
+
|
|
15
|
+
Full legal code: https://creativecommons.org/licenses/by/4.0/legalcode
|
|
16
|
+
Human-readable summary: https://creativecommons.org/licenses/by/4.0/
|
|
17
|
+
|
|
18
|
+
Suggested attribution: "Based on book-scaffold-astro by Brandon Behring,
|
|
19
|
+
licensed under CC BY 4.0."
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# `@brandon_m_behring/book-scaffold-astro`
|
|
2
|
+
|
|
3
|
+
Astro 6 + MDX infrastructure for long-form technical books: typed presets,
|
|
4
|
+
Tufte-inspired layouts, citations and cross-references, search, print/PDF
|
|
5
|
+
output, study-guide components, and fail-loud authoring checks.
|
|
6
|
+
|
|
7
|
+
Start a book with the lock-step CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @brandon_m_behring/create-book my-book --preset=academic
|
|
11
|
+
cd my-book
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Available presets are `academic`, `tools`, `minimal`, `course-notes`, and
|
|
17
|
+
`research-portfolio`. See the
|
|
18
|
+
[repository README](https://github.com/brandon-behring/book-scaffold-astro)
|
|
19
|
+
for the current release overview, and
|
|
20
|
+
[PACKAGE_DESIGN.md](https://github.com/brandon-behring/book-scaffold-astro/blob/main/PACKAGE_DESIGN.md)
|
|
21
|
+
for the complete API contract.
|
|
22
|
+
|
|
23
|
+
Astro builds emit a Cloudflare-compatible `dist/_headers` with audited
|
|
24
|
+
security defaults. A consumer-owned `public/_headers` wins unchanged;
|
|
25
|
+
`defineBookConfig({ securityHeaders: false })` disables scaffold emission,
|
|
26
|
+
and `securityHeaders.contentSecurityPolicy` replaces only the default CSP.
|
|
27
|
+
See [Recipe 05](./recipes/05-deploy-cloudflare.md#default-security-headers).
|
|
28
|
+
|
|
29
|
+
## Licensing
|
|
30
|
+
|
|
31
|
+
Code, configuration, and scripts are MIT-licensed. Recipes, pedagogy, examples,
|
|
32
|
+
and substantive documentation are CC BY 4.0. Both scoped license files ship in
|
|
33
|
+
the package as `LICENSE` and `LICENSE-CONTENT`.
|
|
@@ -31,6 +31,7 @@ import { getAllQuestions } from '../src/lib/questions';
|
|
|
31
31
|
import { assertKnownDomain } from '../src/lib/exam-domains';
|
|
32
32
|
import { buildExamManifest, deriveDomainRouting } from '../src/lib/exam-manifest';
|
|
33
33
|
import '../styles/exam-runner.css';
|
|
34
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
34
35
|
|
|
35
36
|
interface Props {
|
|
36
37
|
/** Heading above the test. Defaults to "Assessment test". */
|
|
@@ -83,7 +84,7 @@ const scoreable = entries.filter(
|
|
|
83
84
|
// #142: thread the deploy base through the routing map + practice-exam link.
|
|
84
85
|
// deriveDomainRouting takes baseUrl as a param (not import.meta.env) because it
|
|
85
86
|
// ships pre-compiled in dist/ where Vite's env replacement does not reach.
|
|
86
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
87
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
87
88
|
const manifest = buildExamManifest(entries);
|
|
88
89
|
const domainRouting = deriveDomainRouting(entries, baseUrl);
|
|
89
90
|
const practiceExamHref = (bookConfig.enabledRoutes ?? []).includes('practiceExam')
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Single-book consumers are byte-identical (defaults → global order + `/chapters/<id>/`).
|
|
9
9
|
*/
|
|
10
10
|
import { getNeighbors } from '../src/lib/chapters';
|
|
11
|
-
import { chapterHref } from '../src/lib/nav-href';
|
|
11
|
+
import { chapterHref, normalizeBase } from '../src/lib/nav-href';
|
|
12
12
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
@@ -18,7 +18,7 @@ const { currentId } = Astro.props;
|
|
|
18
18
|
|
|
19
19
|
const chapterRoute = (bookConfig as { chapterRoute?: string }).chapterRoute ?? '/chapters/:id/';
|
|
20
20
|
const bookField = (bookConfig as { bookField?: string }).bookField ?? 'book';
|
|
21
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
21
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
22
22
|
|
|
23
23
|
const { prev, next } = await getNeighbors(currentId, { bookField });
|
|
24
24
|
// Resolve hrefs in frontmatter (casts here, never in the JSX expression container).
|
package/components/Cite.astro
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
2
3
|
/**
|
|
3
4
|
* Cite — renders a `\cite{bibkey}` LaTeX citation as a clickable
|
|
4
5
|
* inline reference to the bibliography page.
|
|
@@ -45,7 +46,7 @@ interface Props {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const { key, page } = Astro.props;
|
|
48
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
49
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
49
50
|
const entry = map[key];
|
|
50
51
|
if (!entry) {
|
|
51
52
|
throw new Error(
|
|
@@ -22,11 +22,11 @@ import { getCollection } from 'astro:content';
|
|
|
22
22
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
23
23
|
import { academicParts } from '../src/schemas';
|
|
24
24
|
import { academicPartHeading } from '../src/lib/academic-parts';
|
|
25
|
-
import { chapterHref, apparatusHref, bookOf, isCurrentChapter, type ChapterLike } from '../src/lib/nav-href';
|
|
25
|
+
import { chapterHref, apparatusHref, bookOf, isCurrentChapter, type ChapterLike, normalizeBase } from '../src/lib/nav-href';
|
|
26
26
|
|
|
27
27
|
const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
|
|
28
28
|
// #142: normalize the base trailing slash (Astro allows base:'/foo' with none).
|
|
29
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
29
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
30
30
|
|
|
31
31
|
// v4.26.0 (#80): nav route config (defaults reproduce single-book behavior).
|
|
32
32
|
const chapterRoute = (bookConfig as { chapterRoute?: string }).chapterRoute ?? '/chapters/:id/';
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { getCollection } from 'astro:content';
|
|
21
21
|
import { selectPartExercises } from '../src/lib/part-review';
|
|
22
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
22
23
|
|
|
23
24
|
interface Props {
|
|
24
25
|
/** The part to aggregate. Number (tools/minimal/…) or string (academic enum). */
|
|
@@ -27,7 +28,7 @@ interface Props {
|
|
|
27
28
|
title?: string;
|
|
28
29
|
}
|
|
29
30
|
const { part, title } = Astro.props;
|
|
30
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
31
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
31
32
|
|
|
32
33
|
// Reuse the build-exercises index (keyed by chapter slug). Project-root-relative
|
|
33
34
|
// glob (the tips.astro / exercises.astro lesson) so it resolves across consumers.
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* inside src/content/questions/**.mdx. Slot freely: prose, math, <Cite>, code.
|
|
18
18
|
*/
|
|
19
19
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
20
|
+
import { baseNoSlash } from '../src/lib/nav-href';
|
|
20
21
|
|
|
21
22
|
interface Props {
|
|
22
23
|
/** Summary label for the reveal. Defaults to "Answer & rationale". */
|
|
@@ -48,8 +49,8 @@ if (appendix && !(bookConfig.enabledRoutes ?? []).includes('answers')) {
|
|
|
48
49
|
// full rationale must render (the appendix re-renders question bodies via
|
|
49
50
|
// render()). BASE_URL-prefixed + trailing-slash tolerant; an endsWith()
|
|
50
51
|
// heuristic would also fire on e.g. a chapter whose slug is "answers".
|
|
51
|
-
const basePath = (import.meta.env.BASE_URL
|
|
52
|
-
const onAnswersRoute = Astro.url.pathname
|
|
52
|
+
const basePath = baseNoSlash(import.meta.env.BASE_URL);
|
|
53
|
+
const onAnswersRoute = baseNoSlash(Astro.url.pathname) === `${basePath}/answers`;
|
|
53
54
|
const asLink = appendix && !onAnswersRoute;
|
|
54
55
|
---
|
|
55
56
|
{asLink ? (
|
package/components/Sidebar.astro
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
19
19
|
import NavContent from './NavContent.astro';
|
|
20
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
20
21
|
|
|
21
22
|
const siteTitle = bookConfig.title ?? 'Book';
|
|
22
23
|
const siteSubtitle = bookConfig.subtitle ?? 'A scaffold-astro book';
|
|
23
24
|
// #142: Astro does not guarantee a trailing slash on `base` — normalize so the
|
|
24
25
|
// home link is `${base}/`, not a slash-less `/foo`.
|
|
25
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
26
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
26
27
|
---
|
|
27
28
|
|
|
28
29
|
<aside class="sidebar" aria-label="Chapter navigation">
|
package/components/Term.astro
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
2
3
|
/**
|
|
3
4
|
* Term — inline glossary cross-link (v4.19.0, #115).
|
|
4
5
|
*
|
|
@@ -17,7 +18,7 @@ interface Props {
|
|
|
17
18
|
id: string;
|
|
18
19
|
}
|
|
19
20
|
const { id } = Astro.props;
|
|
20
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
21
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
21
22
|
---
|
|
22
23
|
<a href={`${baseUrl}glossary#term-${id}`} class="term-link"><slot /></a>
|
|
23
24
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
2
3
|
/**
|
|
3
4
|
* TipsCard — print-friendly pull-out card listing all numbered tips
|
|
4
5
|
* (v4.3.0, closes #70).
|
|
@@ -20,7 +21,7 @@ const tipsModules = import.meta.glob<{ default: Array<{ n: number; title: string
|
|
|
20
21
|
{ eager: true },
|
|
21
22
|
);
|
|
22
23
|
const tips = tipsModules['/src/data/tips.json']?.default ?? [];
|
|
23
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
24
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
24
25
|
---
|
|
25
26
|
<aside class="tips-card" role="contentinfo">
|
|
26
27
|
<h2 class="tips-card-title">Tips</h2>
|
|
@@ -1,33 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VersionSelector — Preact island for the version dropdown.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Hydrated with `client:idle` from Base.astro — won't run until the
|
|
10
|
-
* main thread is idle, keeping initial paint fast.
|
|
11
|
-
*
|
|
12
|
-
* Stage 3 will replace the STUB_VERSIONS constant with a runtime fetch
|
|
13
|
-
* or a build-time-injected prop that lists all deployed versions.
|
|
4
|
+
* Multi-version deployment is owned by the consuming book, so this component
|
|
5
|
+
* accepts its complete version manifest as a prop. It is a manual opt-in and
|
|
6
|
+
* is not mounted by Base.astro. With no manifest it renders nothing rather
|
|
7
|
+
* than presenting package-owned placeholder releases as real navigation.
|
|
14
8
|
*/
|
|
15
9
|
import { useState, useRef, useEffect } from 'preact/hooks';
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
export interface VersionEntry {
|
|
12
|
+
/** Resolved destination for this deployed version. */
|
|
13
|
+
href: string;
|
|
14
|
+
/** Human-readable release label, for example `v4.27`. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Human-readable release date. */
|
|
17
|
+
date: string;
|
|
18
|
+
/** Marks the version represented by the current page. */
|
|
19
|
+
current?: boolean;
|
|
20
|
+
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
];
|
|
22
|
+
export interface VersionSelectorProps {
|
|
23
|
+
/** Consumer-owned deployed-version manifest. Empty/omitted renders nothing. */
|
|
24
|
+
versions?: readonly VersionEntry[];
|
|
25
|
+
}
|
|
29
26
|
|
|
30
|
-
export default function VersionSelector() {
|
|
27
|
+
export default function VersionSelector({ versions = [] }: VersionSelectorProps) {
|
|
31
28
|
const [open, setOpen] = useState(false);
|
|
32
29
|
const ref = useRef<HTMLDivElement>(null);
|
|
33
30
|
|
|
@@ -50,9 +47,11 @@ export default function VersionSelector() {
|
|
|
50
47
|
};
|
|
51
48
|
}, [open]);
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
if (versions.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
// If the consumer omits `current`, present the first supplied release as
|
|
53
|
+
// the current label. The non-empty guard above makes the fallback defined.
|
|
54
|
+
const current = versions.find((version) => version.current) ?? versions[0]!;
|
|
56
55
|
|
|
57
56
|
return (
|
|
58
57
|
<div class="version-selector" ref={ref}>
|
|
@@ -69,17 +68,21 @@ export default function VersionSelector() {
|
|
|
69
68
|
</button>
|
|
70
69
|
{open && (
|
|
71
70
|
<ul class="version-selector-menu" role="listbox">
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
{versions.map((version) => {
|
|
72
|
+
const isCurrent = version === current;
|
|
73
|
+
return (
|
|
74
|
+
<li key={version.href} role="option" aria-selected={isCurrent}>
|
|
75
|
+
<a
|
|
76
|
+
href={version.href}
|
|
77
|
+
class={isCurrent ? 'version-current' : ''}
|
|
78
|
+
aria-current={isCurrent ? 'page' : undefined}
|
|
79
|
+
>
|
|
80
|
+
<span class="version-label">{version.label}</span>
|
|
81
|
+
<span class="version-date">{version.date}</span>
|
|
82
|
+
</a>
|
|
83
|
+
</li>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
83
86
|
</ul>
|
|
84
87
|
)}
|
|
85
88
|
</div>
|
package/components/WeekRef.astro
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
2
3
|
/**
|
|
3
4
|
* WeekRef — inline pointer to another chapter, mapping LaTeX `\weekref{N}{body}`.
|
|
4
5
|
*
|
|
@@ -21,7 +22,7 @@ interface Props {
|
|
|
21
22
|
const { week } = Astro.props;
|
|
22
23
|
const slug = `week${String(week).padStart(2, '0')}`;
|
|
23
24
|
// #142: prefix BASE_URL so the chapter link stays inside a non-root deploy base.
|
|
24
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
25
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
25
26
|
const href = `${baseUrl}chapters/${slug}/`;
|
|
26
27
|
---
|
|
27
28
|
<aside class="week-ref">
|
package/components/XRef.astro
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { normalizeBase } from '../src/lib/nav-href';
|
|
2
3
|
// XRef — resolves a `\cref{label}` LaTeX reference to a hyperlink.
|
|
3
4
|
//
|
|
4
5
|
// Reads src/data/labels.json built by scripts/build-labels.mjs
|
|
@@ -47,7 +48,7 @@ const entry = map[id];
|
|
|
47
48
|
// #142: labels.json stores a base-less `chapters/<slug>#<id>` ref; prefix
|
|
48
49
|
// BASE_URL at render. The leading-slash strip tolerates a labels.json built
|
|
49
50
|
// by an older (pre-#142) build-labels that still emits a root-absolute href.
|
|
50
|
-
const baseUrl = (import.meta.env.BASE_URL
|
|
51
|
+
const baseUrl = normalizeBase(import.meta.env.BASE_URL);
|
|
51
52
|
---
|
|
52
53
|
{entry ? (
|
|
53
54
|
<a href={`${baseUrl}${entry.href.replace(/^\//, '')}`} class="xref">{entry.display}</a>
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface VersionEntry {
|
|
4
|
+
/** Resolved destination for this deployed version. */
|
|
5
|
+
href: string;
|
|
6
|
+
/** Human-readable release label, for example `v4.27`. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Human-readable release date. */
|
|
9
|
+
date: string;
|
|
10
|
+
/** Marks the version represented by the current page. */
|
|
11
|
+
current?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface VersionSelectorProps {
|
|
14
|
+
/** Consumer-owned deployed-version manifest. Empty/omitted renders nothing. */
|
|
15
|
+
versions?: readonly VersionEntry[];
|
|
16
|
+
}
|
|
17
|
+
declare function VersionSelector({ versions }: VersionSelectorProps): preact.JSX.Element | null;
|
|
4
18
|
|
|
5
|
-
export { VersionSelector as default };
|
|
19
|
+
export { type VersionEntry, type VersionSelectorProps, VersionSelector as default };
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
// components/VersionSelector.tsx
|
|
2
2
|
import { useState, useRef, useEffect } from "preact/hooks";
|
|
3
3
|
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
4
|
-
|
|
5
|
-
{ id: "", label: "Latest (main)", date: "2026-04-17", current: true },
|
|
6
|
-
{ id: "v1.0", label: "v1.0", date: "2026-05-01", current: false }
|
|
7
|
-
];
|
|
8
|
-
function VersionSelector() {
|
|
4
|
+
function VersionSelector({ versions = [] }) {
|
|
9
5
|
const [open, setOpen] = useState(false);
|
|
10
6
|
const ref = useRef(null);
|
|
11
7
|
useEffect(() => {
|
|
@@ -25,7 +21,8 @@ function VersionSelector() {
|
|
|
25
21
|
window.removeEventListener("keydown", onKey);
|
|
26
22
|
};
|
|
27
23
|
}, [open]);
|
|
28
|
-
|
|
24
|
+
if (versions.length === 0) return null;
|
|
25
|
+
const current = versions.find((version) => version.current) ?? versions[0];
|
|
29
26
|
return /* @__PURE__ */ jsxs("div", { class: "version-selector", ref, children: [
|
|
30
27
|
/* @__PURE__ */ jsx(
|
|
31
28
|
"button",
|
|
@@ -40,17 +37,21 @@ function VersionSelector() {
|
|
|
40
37
|
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "v" })
|
|
41
38
|
}
|
|
42
39
|
),
|
|
43
|
-
open && /* @__PURE__ */ jsx("ul", { class: "version-selector-menu", role: "listbox", children:
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
open && /* @__PURE__ */ jsx("ul", { class: "version-selector-menu", role: "listbox", children: versions.map((version) => {
|
|
41
|
+
const isCurrent = version === current;
|
|
42
|
+
return /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": isCurrent, children: /* @__PURE__ */ jsxs(
|
|
43
|
+
"a",
|
|
44
|
+
{
|
|
45
|
+
href: version.href,
|
|
46
|
+
class: isCurrent ? "version-current" : "",
|
|
47
|
+
"aria-current": isCurrent ? "page" : void 0,
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx("span", { class: "version-label", children: version.label }),
|
|
50
|
+
/* @__PURE__ */ jsx("span", { class: "version-date", children: version.date })
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
) }, version.href);
|
|
54
|
+
}) })
|
|
54
55
|
] });
|
|
55
56
|
}
|
|
56
57
|
export {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration, MarkdownHeading } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer,
|
|
3
|
-
export { B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, F as FreshnessAffordance, i as FrontmatterRouteConfig, P as PartKey,
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer, o as Style } from './types-CZrkqzpC.js';
|
|
3
|
+
export { B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, F as FreshnessAffordance, i as FrontmatterRouteConfig, N as NUMBER_STYLES, j as NumberStyle, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ReleaseStatusConfig, m as RouteToggles, S as SecurityHeadersConfig, n as StatusBadge, p as StyleInput, V as VolatilityBadge, q as composeStyles, r as defineProfile, s as defineStyle, t as normalizeFrontmatterConfig, u as resolvePreset, v as resolveProfile } from './types-CZrkqzpC.js';
|
|
4
4
|
import { E as volatilityLevels, c as academicParts, Q as Question } from './schemas-CKipJ5Ie.js';
|
|
5
5
|
export { A as AcademicChapter, B as BloomLevel, C as CourseNotesChapter, G as GlossaryTerm, M as MinimalChapter, P as Provenance, a as QuestionType, R as ResearchPortfolioChapter, T as ToolsChapter, b as academicChapterSchema, d as bloomLevels, e as changeKinds, f as changelogSchema, g as chapterStatus, h as citationBackstops, i as courseNotesChapterSchema, j as glossarySchema, l as layoutModes, m as minimalChapterSchema, p as patternCategories, k as patternsSchema, n as provenanceObject, o as provenanceSchema, q as questionDifficulties, r as questionSchema, s as questionTypes, t as refineQuestion, u as refinedQuestionSchema, v as researchPortfolioChapterSchema, w as sourceTiers, x as sourceTiersResearch, y as sourcesSchema, z as toolSlugs, D as toolsChapterSchema } from './schemas-CKipJ5Ie.js';
|
|
6
6
|
export { KIND_LABEL, ResolvedTheoremLabel, THEOREM_KINDS, TheoremKind, TheoremLabelProps, resolveTheoremNumber, theoremLabel } from './lib/theorem-label.js';
|
|
7
|
+
export { ssmMacros } from './lib/katex-macros.js';
|
|
7
8
|
export { D as DomainScore, E as ExamBlueprint, a as ExamQuestion, b as ExamResult, R as RoutingChapter, c as buildExamManifest, d as deriveDomainRouting, s as sampleExam, e as scoreExam, f as shuffle, g as spreadBlueprint } from './exam-manifest-X9IrX1G3.js';
|
|
8
9
|
export { F as FlashcardRef, b as buildFlashcardDeck } from './flashcards-okekZcl8.js';
|
|
9
10
|
import 'astro/zod';
|
|
@@ -336,6 +337,24 @@ interface ChapterLike {
|
|
|
336
337
|
id: string;
|
|
337
338
|
data: Record<string, unknown>;
|
|
338
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Normalize a base URL to exactly one trailing slash (`''`/`undefined` → `'/'`).
|
|
342
|
+
*
|
|
343
|
+
* v4.27.0 (#182): promoted from this module's private helper to THE shared
|
|
344
|
+
* normalizer — 18 `.astro` files previously inlined the same normalization in
|
|
345
|
+
* three regex idioms. Astro does not guarantee a trailing slash on `base`
|
|
346
|
+
* (`'/foo'` is a documented form), so every `import.meta.env.BASE_URL` read
|
|
347
|
+
* must pass through here before href composition: `${normalizeBase(...)}chapters/`.
|
|
348
|
+
* Takes the base as a PARAMETER because src/lib ships pre-compiled in dist/,
|
|
349
|
+
* where Vite's import.meta.env replacement cannot reach (see exam-manifest.ts).
|
|
350
|
+
*/
|
|
351
|
+
declare function normalizeBase(baseUrl: string | undefined): string;
|
|
352
|
+
/**
|
|
353
|
+
* The composing variant (#182): strip ALL trailing slashes (`'/'` → `''`,
|
|
354
|
+
* `'/foo/'` → `'/foo'`) for `${baseNoSlash(...)}/answers`-style templates
|
|
355
|
+
* where the literal supplies the slash (Rationale.astro's route detection).
|
|
356
|
+
*/
|
|
357
|
+
declare function baseNoSlash(baseUrl: string | undefined): string;
|
|
339
358
|
/**
|
|
340
359
|
* The entry's book name from `data[bookField]`, or `null` when absent/blank.
|
|
341
360
|
* Single-book schemas (academic/tools/minimal) have no such field → `null`,
|
|
@@ -540,11 +559,10 @@ declare const toolsStyle: Style;
|
|
|
540
559
|
/** Minimal preset — single-author essays / manifestos. */
|
|
541
560
|
declare const minimalStyle: Style;
|
|
542
561
|
/** Course-notes preset — chapters derived from a video course / MOOC / book.
|
|
543
|
-
*
|
|
562
|
+
* `deploy` is retained as deprecated metadata only (#180). */
|
|
544
563
|
declare const courseNotesStyle: Style;
|
|
545
564
|
/** Research-portfolio preset — academic structure + tools-style provenance + portfolio components.
|
|
546
|
-
*
|
|
547
|
-
* title-page / disclosure / banner pages). */
|
|
565
|
+
* `deploy` is retained as deprecated metadata only (#180); frontmatter routes remain enabled. */
|
|
548
566
|
declare const researchPortfolioStyle: Style;
|
|
549
567
|
/**
|
|
550
568
|
* Registry of all toolkit-shipped styles, keyed by their preset name.
|
|
@@ -611,4 +629,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
611
629
|
*/
|
|
612
630
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
613
631
|
|
|
614
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, type ChapterLike, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, type PartReviewGroup, type PartReviewSelection, Question, type ReviewChapter, type ReviewExercise, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VisibleHeading, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, apparatusHref, assertEnumProp, assertKnownDomain, bookOf, bookScaffoldIntegration, buildGithubUrl, chapterHref, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, isCurrentChapter, minimalStyle, originUrlFromGitConfig, parseRepoSlug, pickActive, researchPortfolioChaptersRenderer, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, selectPartExercises, slugOf, sortQuestions, tocHeadings, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
|
632
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, type ChapterLike, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, type PartReviewGroup, type PartReviewSelection, Question, type ReviewChapter, type ReviewExercise, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VisibleHeading, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, apparatusHref, assertEnumProp, assertKnownDomain, baseNoSlash, bookOf, bookScaffoldIntegration, buildGithubUrl, chapterHref, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, isCurrentChapter, minimalStyle, normalizeBase, originUrlFromGitConfig, parseRepoSlug, pickActive, researchPortfolioChaptersRenderer, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, selectPartExercises, slugOf, sortQuestions, tocHeadings, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|