@brandon_m_behring/book-scaffold-astro 4.24.0 → 4.25.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.
Files changed (55) hide show
  1. package/CLAUDE.md +9 -1
  2. package/components/AssessmentTest.astro +6 -2
  3. package/components/ChapterNav.astro +1 -1
  4. package/components/ChapterTOC.astro +8 -1
  5. package/components/Cite.astro +1 -1
  6. package/components/Epigraph.astro +28 -0
  7. package/components/EvidenceTag.astro +102 -0
  8. package/components/MarginFigure.astro +49 -0
  9. package/components/Newthought.astro +18 -0
  10. package/components/PartReview.astro +1 -1
  11. package/components/SectionMap.astro +56 -0
  12. package/components/SectionMap.tsx +170 -0
  13. package/components/Sidebar.astro +1 -1
  14. package/components/Term.astro +1 -1
  15. package/components/TipsCard.astro +1 -1
  16. package/components/WeekRef.astro +3 -1
  17. package/components/XRef.astro +5 -1
  18. package/dist/components/ExamRunner.d.ts +2 -2
  19. package/dist/components/Flashcards.d.ts +2 -2
  20. package/dist/components/SectionMap.d.ts +10 -0
  21. package/dist/components/SectionMap.mjs +107 -0
  22. package/dist/{exam-manifest-DbTHo90M.d.ts → exam-manifest-X9IrX1G3.d.ts} +8 -3
  23. package/dist/{flashcards-DPFFQhP8.d.ts → flashcards-okekZcl8.d.ts} +1 -1
  24. package/dist/index.d.ts +71 -8
  25. package/dist/index.mjs +41 -7
  26. package/dist/{schemas-DDWDRUxs.d.ts → schemas-CKipJ5Ie.d.ts} +22 -1
  27. package/dist/schemas.d.ts +2 -2
  28. package/dist/schemas.mjs +15 -5
  29. package/dist/{types-CGN95mrX.d.ts → types-Bn7rKhgP.d.ts} +1 -1
  30. package/layouts/Base.astro +1 -1
  31. package/layouts/Chapter.astro +36 -1
  32. package/package.json +11 -1
  33. package/pages/answers.astro +4 -2
  34. package/pages/chapters.astro +4 -1
  35. package/pages/exercises.astro +5 -2
  36. package/pages/index.astro +11 -7
  37. package/pages/search.astro +6 -2
  38. package/pages/tips.astro +4 -1
  39. package/recipes/04-component-library.md +7 -3
  40. package/recipes/16-tikz-figures.md +39 -0
  41. package/recipes/20-anki-export.md +1 -1
  42. package/recipes/21-multi-guide-single-app.md +4 -4
  43. package/recipes/README.md +1 -1
  44. package/scripts/build-labels.mjs +3 -1
  45. package/src/lib/exam-manifest.ts +11 -2
  46. package/src/lib/section-map.ts +88 -0
  47. package/src/profiles/academic.ts +1 -1
  48. package/src/profiles/course-notes.ts +1 -1
  49. package/src/profiles/minimal.ts +1 -1
  50. package/src/profiles/research-portfolio.ts +1 -1
  51. package/src/profiles/tools.ts +1 -0
  52. package/src/schemas.ts +15 -0
  53. package/styles/layout.css +64 -0
  54. package/styles/section-map.css +130 -0
  55. package/styles/typography.css +51 -0
@@ -0,0 +1,107 @@
1
+ // components/SectionMap.tsx
2
+ import { useEffect, useRef } from "preact/hooks";
3
+
4
+ // src/lib/section-map.ts
5
+ function pickActive(visible, prev) {
6
+ if (visible.length === 0) return prev;
7
+ let bestNonNeg = null;
8
+ let bestAbove = null;
9
+ for (const h of visible) {
10
+ if (h.top >= 0) {
11
+ if (bestNonNeg === null || h.top < bestNonNeg.top) bestNonNeg = h;
12
+ } else {
13
+ if (bestAbove === null || h.top > bestAbove.top) bestAbove = h;
14
+ }
15
+ }
16
+ if (bestNonNeg !== null) return bestNonNeg.slug;
17
+ if (bestAbove !== null) return bestAbove.slug;
18
+ return prev;
19
+ }
20
+
21
+ // components/SectionMap.tsx
22
+ import { jsx } from "preact/jsx-runtime";
23
+ function SectionMap({ headings }) {
24
+ const ref = useRef(null);
25
+ useEffect(() => {
26
+ const root = ref.current?.closest("[data-section-map-root]");
27
+ if (!root) {
28
+ throw new Error(
29
+ "SectionMap: no [data-section-map-root] ancestor \u2014 mount the island inside the <nav> that contains its links (see the DOM contract in SectionMap.tsx)."
30
+ );
31
+ }
32
+ const tracked = [];
33
+ for (const h of headings) {
34
+ const el = document.getElementById(h.slug);
35
+ if (el) tracked.push({ slug: h.slug, el });
36
+ }
37
+ if (tracked.length === 0) {
38
+ throw new Error(
39
+ `SectionMap: manifest/DOM drift \u2014 none of the ${headings.length} heading id(s) resolve to an element (#${headings.map((h) => h.slug).join(", #")}).`
40
+ );
41
+ }
42
+ const lastTracked = tracked[tracked.length - 1];
43
+ function linkFor(slug) {
44
+ return root.querySelector(
45
+ `a[data-section-id="${CSS.escape(slug)}"]`
46
+ );
47
+ }
48
+ const inView = /* @__PURE__ */ new Set();
49
+ let active = null;
50
+ function setActive(next) {
51
+ if (next === active) return;
52
+ if (active !== null) {
53
+ const prevLink = linkFor(active);
54
+ if (prevLink) {
55
+ prevLink.classList.remove("active");
56
+ prevLink.removeAttribute("aria-current");
57
+ }
58
+ }
59
+ if (next !== null) {
60
+ const nextLink = linkFor(next);
61
+ if (nextLink) {
62
+ nextLink.classList.add("active");
63
+ nextLink.setAttribute("aria-current", "page");
64
+ }
65
+ }
66
+ active = next;
67
+ }
68
+ function recompute() {
69
+ const visible = [];
70
+ for (const t of tracked) {
71
+ if (inView.has(t.el)) {
72
+ visible.push({ slug: t.slug, top: t.el.getBoundingClientRect().top });
73
+ }
74
+ }
75
+ setActive(pickActive(visible, active));
76
+ }
77
+ const observer = new IntersectionObserver(
78
+ (entries) => {
79
+ for (const entry of entries) {
80
+ if (entry.isIntersecting) inView.add(entry.target);
81
+ else inView.delete(entry.target);
82
+ }
83
+ recompute();
84
+ },
85
+ // A negative bottom margin biases "active" toward the heading nearest the
86
+ // top of the viewport (the section you're reading INTO), not the last one
87
+ // peeking in at the bottom.
88
+ { rootMargin: "0px 0px -70% 0px", threshold: 0 }
89
+ );
90
+ for (const t of tracked) observer.observe(t.el);
91
+ function onScroll() {
92
+ const atBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
93
+ if (atBottom) setActive(lastTracked.slug);
94
+ }
95
+ window.addEventListener("scroll", onScroll, { passive: true });
96
+ window.addEventListener("resize", onScroll, { passive: true });
97
+ return () => {
98
+ observer.disconnect();
99
+ window.removeEventListener("scroll", onScroll);
100
+ window.removeEventListener("resize", onScroll);
101
+ };
102
+ }, [headings]);
103
+ return /* @__PURE__ */ jsx("span", { ref, class: "section-map-mount", hidden: true, "aria-hidden": "true" });
104
+ }
105
+ export {
106
+ SectionMap as default
107
+ };
@@ -1,4 +1,4 @@
1
- import { Q as Question } from './schemas-DDWDRUxs.js';
1
+ import { Q as Question } from './schemas-CKipJ5Ie.js';
2
2
 
3
3
  /**
4
4
  * exam-engine.ts — PURE sampling + scoring for the interactive practice exam
@@ -115,11 +115,16 @@ interface RoutingChapter {
115
115
  * Derive the weak-domain → chapters routing map for `<AssessmentTest>` (#113):
116
116
  * for each domain, the distinct chapters (in book order) that carry ≥1 question
117
117
  * in it. String chapters are slugs (the schema's kebab-case branch) and link to
118
- * `/chapters/<slug>/`; numeric chapters render as plain labels.
118
+ * `${baseUrl}chapters/<slug>/`; numeric chapters render as plain labels.
119
+ *
120
+ * `baseUrl` (default '/') is the deploy base, injected by the .astro caller from
121
+ * import.meta.env.BASE_URL — NOT read here, because this lib ships pre-compiled
122
+ * in dist/ where Vite's env replacement does not reach, so a self-read would
123
+ * silently drop the base under a non-root deploy (#142).
119
124
  */
120
125
  declare function deriveDomainRouting(entries: readonly (QuestionLike & {
121
126
  data: Pick<Question, 'id' | 'chapter' | 'domain'>;
122
- })[]): Record<string, RoutingChapter[]>;
127
+ })[], baseUrl?: string): Record<string, RoutingChapter[]>;
123
128
  /**
124
129
  * Build the cross-domain blueprint for assessment mode: spread `count` evenly
125
130
  * across the domains present in the pool (floor, minimum 1 per domain), letting
@@ -1,4 +1,4 @@
1
- import { G as GlossaryTerm } from './schemas-DDWDRUxs.js';
1
+ import { G as GlossaryTerm } from './schemas-CKipJ5Ie.js';
2
2
 
3
3
  /**
4
4
  * flashcards.ts — PURE deck-manifest bridge between the `glossary` collection
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { AstroUserConfig, AstroIntegration } from 'astro';
2
- import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer, l as Style } from './types-CGN95mrX.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, P as PartKey, j as PartialRouteToggles, k as ProfileDefinition, R as RouteToggles, S as StatusBadge, m as StyleInput, V as VolatilityBadge, n as composeStyles, o as defineProfile, p as defineStyle, q as normalizeFrontmatterConfig, r as resolvePreset, s as resolveProfile } from './types-CGN95mrX.js';
4
- import { D as volatilityLevels, c as academicParts, Q as Question } from './schemas-DDWDRUxs.js';
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, m as minimalChapterSchema, p as patternCategories, k as patternsSchema, l as provenanceObject, n as provenanceSchema, q as questionDifficulties, o as questionSchema, r as questionTypes, s as refineQuestion, t as refinedQuestionSchema, u as researchPortfolioChapterSchema, v as sourceTiers, w as sourceTiersResearch, x as sourcesSchema, y as toolSlugs, z as toolsChapterSchema } from './schemas-DDWDRUxs.js';
1
+ import { AstroUserConfig, AstroIntegration, MarkdownHeading } from 'astro';
2
+ import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer, l as Style } from './types-Bn7rKhgP.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, P as PartKey, j as PartialRouteToggles, k as ProfileDefinition, R as RouteToggles, S as StatusBadge, m as StyleInput, V as VolatilityBadge, n as composeStyles, o as defineProfile, p as defineStyle, q as normalizeFrontmatterConfig, r as resolvePreset, s as resolveProfile } from './types-Bn7rKhgP.js';
4
+ import { E as volatilityLevels, c as academicParts, Q as Question } from './schemas-CKipJ5Ie.js';
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 { 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-DbTHo90M.js';
8
- export { F as FlashcardRef, b as buildFlashcardDeck } from './flashcards-DPFFQhP8.js';
7
+ 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
+ export { F as FlashcardRef, b as buildFlashcardDeck } from './flashcards-okekZcl8.js';
9
9
  import 'astro/zod';
10
10
 
11
11
  /**
@@ -360,6 +360,69 @@ declare function distinctChaptersSorted<T extends {
360
360
  data: Pick<Question, 'chapter'>;
361
361
  }>(entries: readonly T[]): string[];
362
362
 
363
+ /**
364
+ * section-map.ts — PURE heading-selection + scrollspy logic for the right-gutter
365
+ * "On this page" section map (#section-map).
366
+ *
367
+ * No DOM, no Preact: this is the testable core under both ChapterTOC.astro (the
368
+ * collapsed mobile fallback) and the SectionMap island (the sticky gutter nav).
369
+ * Two pure, total functions:
370
+ *
371
+ * - tocHeadings: the ONE filter (h2+h3) that both the fallback TOC and the
372
+ * gutter map share — a single source of truth so the two
373
+ * never disagree about which headings are "on this page".
374
+ * - pickActive: given the headings currently intersecting the viewport (slug
375
+ * + viewport-relative top), choose which one is "active". The
376
+ * island feeds it IntersectionObserver state; node:test feeds
377
+ * it plain arrays. Browser geometry stays OUT of this file.
378
+ *
379
+ * Both are unit-tested in tests/section-map.test.mjs (node:test, no browser),
380
+ * mirroring exam-engine.ts.
381
+ */
382
+
383
+ /**
384
+ * Filter a chapter's headings to the TOC set: depth 2–3 only. h1 is the chapter
385
+ * title (rendered by ChapterHeader, not the body) and h4+ is noise in an anchor
386
+ * list. This is the shared contract — ChapterTOC.astro and SectionMap.astro both
387
+ * call it so the fallback and the gutter map carry IDENTICAL entries. Pure: it
388
+ * copies (filter) rather than mutating the input.
389
+ */
390
+ declare function tocHeadings(headings: MarkdownHeading[]): MarkdownHeading[];
391
+ /**
392
+ * One visible heading, as the island measures it: its slug and its
393
+ * viewport-relative top (CSS pixels — `getBoundingClientRect().top`). A negative
394
+ * `top` means the heading has scrolled above the top of the viewport.
395
+ */
396
+ interface VisibleHeading {
397
+ slug: string;
398
+ /** Viewport-relative top in px (negative = scrolled above the fold). */
399
+ top: number;
400
+ }
401
+ /**
402
+ * Choose the active section slug from the headings currently intersecting the
403
+ * viewport.
404
+ *
405
+ * Rule (a stable, total scrollspy):
406
+ * 1. Prefer the topmost heading at or below the top of the viewport — the
407
+ * visible heading with the SMALLEST non-negative `top`. That's the section
408
+ * the reader has just scrolled to / is reading into.
409
+ * 2. If every visible heading is above the fold (all `top` negative — e.g. a
410
+ * heading STRADDLING the top edge: its top is slightly negative but its box
411
+ * still overlaps the top zone, so it's intersecting yet has no non-negative
412
+ * sibling below it), fall back to the one nearest the fold from above: the
413
+ * GREATEST `top` (closest to 0 from the negative side). That keeps the
414
+ * enclosing section lit instead of going dark mid-section. (A heading whose
415
+ * box FULLY scrolled above the top has left `inView` entirely — it isn't in
416
+ * `visible` at all, and is handled by the empty-set → `prev` branch below.)
417
+ * 3. If NOTHING is visible (the observer reports an empty set — between two
418
+ * sparse intersections, or scrolled past the last heading), retain `prev`
419
+ * so the highlight is sticky rather than flickering off.
420
+ *
421
+ * Pure + total: no DOM, no throw, deterministic. Ties (equal `top`) resolve to
422
+ * the first in iteration order, which the island passes in document order.
423
+ */
424
+ declare function pickActive(visible: ReadonlyArray<VisibleHeading>, prev: string | null): string | null;
425
+
363
426
  /** One exercise as emitted by `book-scaffold build-exercises` (src/data/exercises.json). */
364
427
  interface ReviewExercise {
365
428
  id: string;
@@ -488,4 +551,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
488
551
  */
489
552
  declare function defineTips(opts: TipsConfigInput): TipsConfig;
490
553
 
491
- export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, 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 VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, assertKnownDomain, bookScaffoldIntegration, buildGithubUrl, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, selectPartExercises, sortQuestions, toolsChaptersRenderer, toolsStyle, volatilityLevels };
554
+ export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, 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, assertEnumProp, assertKnownDomain, bookScaffoldIntegration, buildGithubUrl, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, minimalStyle, originUrlFromGitConfig, parseRepoSlug, pickActive, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, selectPartExercises, sortQuestions, tocHeadings, toolsChaptersRenderer, toolsStyle, volatilityLevels };
package/dist/index.mjs CHANGED
@@ -168,6 +168,7 @@ var chapterStatus = [
168
168
  "scaffolded",
169
169
  "planned"
170
170
  ];
171
+ var layoutModes = ["default", "wide"];
171
172
  var citationBackstops = ["research-kb", "manual", "unverified"];
172
173
  var provenanceObject = z.object({
173
174
  ai_tools: z.array(z.string()).default([]),
@@ -186,6 +187,8 @@ var academicChapterSchema = z.object({
186
187
  title: z.string().min(1),
187
188
  slug: z.string().optional(),
188
189
  // v4.9.0: explicit URL slug override (else filename → entry.id)
190
+ layout: z.enum(layoutModes).optional(),
191
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
189
192
  status: z.enum(chapterStatus),
190
193
  roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
191
194
  code_path: z.string().optional(),
@@ -207,6 +210,8 @@ var toolsChapterSchema = z.object({
207
210
  title: z.string().min(1),
208
211
  slug: z.string().optional(),
209
212
  // v4.9.0: explicit URL slug override (else filename → entry.id)
213
+ layout: z.enum(layoutModes).optional(),
214
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
210
215
  part: z.number().int().min(0).max(10),
211
216
  chapter: z.number().int().min(0).max(99),
212
217
  volatility: z.enum(volatilityLevels),
@@ -232,6 +237,8 @@ var courseNotesChapterSchema = z.object({
232
237
  title: z.string().min(1),
233
238
  slug: z.string().optional(),
234
239
  // v4.9.0: explicit URL slug override (else filename → entry.id)
240
+ layout: z.enum(layoutModes).optional(),
241
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
235
242
  chapter: z.number().int().min(0).max(99),
236
243
  part: z.number().int().min(0).max(20).default(1),
237
244
  description: z.string().optional(),
@@ -268,6 +275,8 @@ var researchPortfolioChapterSchema = z.object({
268
275
  title: z.string().min(1),
269
276
  slug: z.string().optional(),
270
277
  // explicit slug override (otherwise filename)
278
+ layout: z.enum(layoutModes).optional(),
279
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
271
280
  description: z.string().optional(),
272
281
  // Hierarchy — accept either academic-style or tools-style; all optional.
273
282
  // The academic 'part' field is a string enum; tools 'part' is a number.
@@ -576,7 +585,7 @@ var academicProfile = defineProfile({
576
585
  landing: true
577
586
  // v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
578
587
  },
579
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
588
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
580
589
  katex: true,
581
590
  chaptersRenderer: academicChaptersRenderer,
582
591
  // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
@@ -715,7 +724,8 @@ var toolsProfile = defineProfile({
715
724
  "typography.css",
716
725
  "print.css",
717
726
  "convergence.css",
718
- "tool-filter.css"
727
+ "tool-filter.css",
728
+ "section-map.css"
719
729
  ],
720
730
  chaptersRenderer: toolsChaptersRenderer
721
731
  // v3.7.0 (#35) — owns /chapters semantics for tools shape
@@ -800,7 +810,7 @@ var minimalProfile = defineProfile({
800
810
  landing: true
801
811
  // v4.5.0: auto-inject minimal root landing
802
812
  },
803
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
813
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
804
814
  // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
805
815
  chaptersRenderer: fallbackChaptersRenderer
806
816
  });
@@ -833,7 +843,7 @@ var courseNotesProfile = defineProfile({
833
843
  landing: true
834
844
  // v4.5.0: auto-inject minimal root landing
835
845
  },
836
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
846
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
837
847
  // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
838
848
  chaptersRenderer: fallbackChaptersRenderer,
839
849
  // v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
@@ -870,7 +880,7 @@ var researchPortfolioProfile = defineProfile({
870
880
  landing: true
871
881
  // v4.5.0: auto-inject minimal root landing
872
882
  },
873
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
883
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
874
884
  katex: true,
875
885
  // math is common in research content
876
886
  // v3.7.0 (#35): portfolio schema is a union of academic + tools shapes — fallback renderer dispatches per chapter via field presence
@@ -1719,7 +1729,8 @@ function buildExamManifest(entries) {
1719
1729
  options: (e.data.options ?? []).map((o) => ({ id: o.id, correct: o.correct === true }))
1720
1730
  }));
1721
1731
  }
1722
- function deriveDomainRouting(entries) {
1732
+ function deriveDomainRouting(entries, baseUrl = "/") {
1733
+ const base = baseUrl.replace(/\/?$/, "/");
1723
1734
  const out = {};
1724
1735
  const seen = /* @__PURE__ */ new Set();
1725
1736
  for (const e of sortQuestions(entries)) {
@@ -1729,7 +1740,7 @@ function deriveDomainRouting(entries) {
1729
1740
  seen.add(key);
1730
1741
  (out[e.data.domain] ??= []).push({
1731
1742
  label,
1732
- href: typeof e.data.chapter === "string" ? `/chapters/${e.data.chapter}/` : null
1743
+ href: typeof e.data.chapter === "string" ? `${base}chapters/${e.data.chapter}/` : null
1733
1744
  });
1734
1745
  }
1735
1746
  return out;
@@ -1749,6 +1760,26 @@ function buildFlashcardDeck(entries) {
1749
1760
  return entries.filter((e) => !e.data.draft).map((e) => ({ id: e.id, front: e.data.term })).sort((a, b) => a.front.localeCompare(b.front));
1750
1761
  }
1751
1762
 
1763
+ // src/lib/section-map.ts
1764
+ function tocHeadings(headings) {
1765
+ return headings.filter((h) => h.depth >= 2 && h.depth <= 3);
1766
+ }
1767
+ function pickActive(visible, prev) {
1768
+ if (visible.length === 0) return prev;
1769
+ let bestNonNeg = null;
1770
+ let bestAbove = null;
1771
+ for (const h of visible) {
1772
+ if (h.top >= 0) {
1773
+ if (bestNonNeg === null || h.top < bestNonNeg.top) bestNonNeg = h;
1774
+ } else {
1775
+ if (bestAbove === null || h.top > bestAbove.top) bestAbove = h;
1776
+ }
1777
+ }
1778
+ if (bestNonNeg !== null) return bestNonNeg.slug;
1779
+ if (bestAbove !== null) return bestAbove.slug;
1780
+ return prev;
1781
+ }
1782
+
1752
1783
  // src/lib/part-review.ts
1753
1784
  function selectPartExercises(chapters, byChapter, part) {
1754
1785
  const want = String(part);
@@ -1844,6 +1875,7 @@ export {
1844
1875
  glossarySchema,
1845
1876
  groupByChapter,
1846
1877
  groupByDomain,
1878
+ layoutModes,
1847
1879
  minimalChapterSchema,
1848
1880
  minimalStyle,
1849
1881
  normalizeFrontmatterConfig,
@@ -1851,6 +1883,7 @@ export {
1851
1883
  parseRepoSlug,
1852
1884
  patternCategories,
1853
1885
  patternsSchema,
1886
+ pickActive,
1854
1887
  provenanceObject,
1855
1888
  provenanceSchema,
1856
1889
  questionDifficulties,
@@ -1875,6 +1908,7 @@ export {
1875
1908
  sourcesSchema,
1876
1909
  spreadBlueprint,
1877
1910
  theoremLabel,
1911
+ tocHeadings,
1878
1912
  toolSlugs,
1879
1913
  toolsChapterSchema,
1880
1914
  toolsChaptersRenderer,
@@ -25,6 +25,7 @@ declare const changeKinds: readonly ["added", "removed", "changed", "deprecated"
25
25
  declare const patternCategories: readonly ["safety", "scale", "context", "interaction", "extension", "other"];
26
26
  declare const academicParts: readonly ["foundations", "ssm-core", "beyond-ssm", "integration", "synthesis"];
27
27
  declare const chapterStatus: readonly ["implemented", "chapter_only", "reading_only", "prose_only", "code_only", "scaffolded", "planned"];
28
+ declare const layoutModes: readonly ["default", "wide"];
28
29
  declare const citationBackstops: readonly ["research-kb", "manual", "unverified"];
29
30
  declare const provenanceObject: z.ZodObject<{
30
31
  ai_tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
@@ -67,6 +68,10 @@ declare const academicChapterSchema: z.ZodObject<{
67
68
  }>;
68
69
  title: z.ZodString;
69
70
  slug: z.ZodOptional<z.ZodString>;
71
+ layout: z.ZodOptional<z.ZodEnum<{
72
+ default: "default";
73
+ wide: "wide";
74
+ }>>;
70
75
  status: z.ZodEnum<{
71
76
  implemented: "implemented";
72
77
  chapter_only: "chapter_only";
@@ -106,6 +111,10 @@ declare const academicChapterSchema: z.ZodObject<{
106
111
  declare const toolsChapterSchema: z.ZodObject<{
107
112
  title: z.ZodString;
108
113
  slug: z.ZodOptional<z.ZodString>;
114
+ layout: z.ZodOptional<z.ZodEnum<{
115
+ default: "default";
116
+ wide: "wide";
117
+ }>>;
109
118
  part: z.ZodNumber;
110
119
  chapter: z.ZodNumber;
111
120
  volatility: z.ZodEnum<{
@@ -148,6 +157,10 @@ declare const toolsChapterSchema: z.ZodObject<{
148
157
  declare const minimalChapterSchema: z.ZodObject<{
149
158
  title: z.ZodString;
150
159
  slug: z.ZodOptional<z.ZodString>;
160
+ layout: z.ZodOptional<z.ZodEnum<{
161
+ default: "default";
162
+ wide: "wide";
163
+ }>>;
151
164
  part: z.ZodNumber;
152
165
  chapter: z.ZodNumber;
153
166
  volatility: z.ZodEnum<{
@@ -205,6 +218,10 @@ declare const sourceTiersResearch: readonly ["T1", "T2", "T3", "T4"];
205
218
  declare const courseNotesChapterSchema: z.ZodObject<{
206
219
  title: z.ZodString;
207
220
  slug: z.ZodOptional<z.ZodString>;
221
+ layout: z.ZodOptional<z.ZodEnum<{
222
+ default: "default";
223
+ wide: "wide";
224
+ }>>;
208
225
  chapter: z.ZodNumber;
209
226
  part: z.ZodDefault<z.ZodNumber>;
210
227
  description: z.ZodOptional<z.ZodString>;
@@ -269,6 +286,10 @@ declare const courseNotesChapterSchema: z.ZodObject<{
269
286
  declare const researchPortfolioChapterSchema: z.ZodObject<{
270
287
  title: z.ZodString;
271
288
  slug: z.ZodOptional<z.ZodString>;
289
+ layout: z.ZodOptional<z.ZodEnum<{
290
+ default: "default";
291
+ wide: "wide";
292
+ }>>;
272
293
  description: z.ZodOptional<z.ZodString>;
273
294
  part: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
274
295
  week: z.ZodOptional<z.ZodNumber>;
@@ -488,4 +509,4 @@ declare const glossarySchema: z.ZodObject<{
488
509
  }, z.core.$strict>;
489
510
  type GlossaryTerm = z.infer<typeof glossarySchema>;
490
511
 
491
- export { type AcademicChapter as A, type BloomLevel as B, type CourseNotesChapter as C, volatilityLevels as D, type GlossaryTerm as G, type MinimalChapter as M, type Provenance as P, type Question as Q, type ResearchPortfolioChapter as R, type ToolsChapter as T, type QuestionType as a, academicChapterSchema as b, academicParts as c, bloomLevels as d, changeKinds as e, changelogSchema as f, chapterStatus as g, citationBackstops as h, courseNotesChapterSchema as i, glossarySchema as j, patternsSchema as k, provenanceObject as l, minimalChapterSchema as m, provenanceSchema as n, questionSchema as o, patternCategories as p, questionDifficulties as q, questionTypes as r, refineQuestion as s, refinedQuestionSchema as t, researchPortfolioChapterSchema as u, sourceTiers as v, sourceTiersResearch as w, sourcesSchema as x, toolSlugs as y, toolsChapterSchema as z };
512
+ export { type AcademicChapter as A, type BloomLevel as B, type CourseNotesChapter as C, toolsChapterSchema as D, volatilityLevels as E, type GlossaryTerm as G, type MinimalChapter as M, type Provenance as P, type Question as Q, type ResearchPortfolioChapter as R, type ToolsChapter as T, type QuestionType as a, academicChapterSchema as b, academicParts as c, bloomLevels as d, changeKinds as e, changelogSchema as f, chapterStatus as g, citationBackstops as h, courseNotesChapterSchema as i, glossarySchema as j, patternsSchema as k, layoutModes as l, minimalChapterSchema as m, provenanceObject as n, provenanceSchema as o, patternCategories as p, questionDifficulties as q, questionSchema as r, questionTypes as s, refineQuestion as t, refinedQuestionSchema as u, researchPortfolioChapterSchema as v, sourceTiers as w, sourceTiersResearch as x, sourcesSchema as y, toolSlugs as z };
package/dist/schemas.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-CGN95mrX.js';
2
+ import { g as BookSchemasOptions } from './types-Bn7rKhgP.js';
3
3
  import 'astro';
4
- import './schemas-DDWDRUxs.js';
4
+ import './schemas-CKipJ5Ie.js';
5
5
  import 'astro/zod';
6
6
 
7
7
  /**
package/dist/schemas.mjs CHANGED
@@ -52,6 +52,7 @@ var chapterStatus = [
52
52
  "scaffolded",
53
53
  "planned"
54
54
  ];
55
+ var layoutModes = ["default", "wide"];
55
56
  var citationBackstops = ["research-kb", "manual", "unverified"];
56
57
  var provenanceObject = z.object({
57
58
  ai_tools: z.array(z.string()).default([]),
@@ -70,6 +71,8 @@ var academicChapterSchema = z.object({
70
71
  title: z.string().min(1),
71
72
  slug: z.string().optional(),
72
73
  // v4.9.0: explicit URL slug override (else filename → entry.id)
74
+ layout: z.enum(layoutModes).optional(),
75
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
73
76
  status: z.enum(chapterStatus),
74
77
  roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
75
78
  code_path: z.string().optional(),
@@ -91,6 +94,8 @@ var toolsChapterSchema = z.object({
91
94
  title: z.string().min(1),
92
95
  slug: z.string().optional(),
93
96
  // v4.9.0: explicit URL slug override (else filename → entry.id)
97
+ layout: z.enum(layoutModes).optional(),
98
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
94
99
  part: z.number().int().min(0).max(10),
95
100
  chapter: z.number().int().min(0).max(99),
96
101
  volatility: z.enum(volatilityLevels),
@@ -116,6 +121,8 @@ var courseNotesChapterSchema = z.object({
116
121
  title: z.string().min(1),
117
122
  slug: z.string().optional(),
118
123
  // v4.9.0: explicit URL slug override (else filename → entry.id)
124
+ layout: z.enum(layoutModes).optional(),
125
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
119
126
  chapter: z.number().int().min(0).max(99),
120
127
  part: z.number().int().min(0).max(20).default(1),
121
128
  description: z.string().optional(),
@@ -152,6 +159,8 @@ var researchPortfolioChapterSchema = z.object({
152
159
  title: z.string().min(1),
153
160
  slug: z.string().optional(),
154
161
  // explicit slug override (otherwise filename)
162
+ layout: z.enum(layoutModes).optional(),
163
+ // 1d: per-page width knob (default measure | 'wide'); threaded as data-layout
155
164
  description: z.string().optional(),
156
165
  // Hierarchy — accept either academic-style or tools-style; all optional.
157
166
  // The academic 'part' field is a string enum; tools 'part' is a number.
@@ -454,7 +463,7 @@ var academicProfile = defineProfile({
454
463
  landing: true
455
464
  // v4.5.0: auto-inject minimal root landing; consumers override via src/pages/index.astro
456
465
  },
457
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
466
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
458
467
  katex: true,
459
468
  chaptersRenderer: academicChaptersRenderer,
460
469
  // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
@@ -593,7 +602,8 @@ var toolsProfile = defineProfile({
593
602
  "typography.css",
594
603
  "print.css",
595
604
  "convergence.css",
596
- "tool-filter.css"
605
+ "tool-filter.css",
606
+ "section-map.css"
597
607
  ],
598
608
  chaptersRenderer: toolsChaptersRenderer
599
609
  // v3.7.0 (#35) — owns /chapters semantics for tools shape
@@ -678,7 +688,7 @@ var minimalProfile = defineProfile({
678
688
  landing: true
679
689
  // v4.5.0: auto-inject minimal root landing
680
690
  },
681
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
691
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
682
692
  // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
683
693
  chaptersRenderer: fallbackChaptersRenderer
684
694
  });
@@ -711,7 +721,7 @@ var courseNotesProfile = defineProfile({
711
721
  landing: true
712
722
  // v4.5.0: auto-inject minimal root landing
713
723
  },
714
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
724
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
715
725
  // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
716
726
  chaptersRenderer: fallbackChaptersRenderer,
717
727
  // v4.6.0 (#76 Secondary): exclude /print/ from sitemap — print-friendly
@@ -748,7 +758,7 @@ var researchPortfolioProfile = defineProfile({
748
758
  landing: true
749
759
  // v4.5.0: auto-inject minimal root landing
750
760
  },
751
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
761
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
752
762
  katex: true,
753
763
  // math is common in research content
754
764
  // v3.7.0 (#35): portfolio schema is a union of academic + tools shapes — fallback renderer dispatches per chapter via field presence
@@ -1,5 +1,5 @@
1
1
  import { AstroIntegration, AstroUserConfig } from 'astro';
2
- import { A as AcademicChapter, T as ToolsChapter, M as MinimalChapter, C as CourseNotesChapter, R as ResearchPortfolioChapter } from './schemas-DDWDRUxs.js';
2
+ import { A as AcademicChapter, T as ToolsChapter, M as MinimalChapter, C as CourseNotesChapter, R as ResearchPortfolioChapter } from './schemas-CKipJ5Ie.js';
3
3
 
4
4
  /**
5
5
  * src/lib/chapters-renderer.ts — per-profile strategy interface for the
@@ -99,7 +99,7 @@ const absoluteOgImage = resolvedOgImage
99
99
  const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
100
100
  const ogSiteName = bookConfig.title ?? title;
101
101
  const ogDescription = description ?? bookConfig.description ?? '';
102
- const baseUrl = import.meta.env.BASE_URL ?? '/';
102
+ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
103
103
  ---
104
104
 
105
105
  <!doctype html>
@@ -25,6 +25,7 @@ import Base from './Base.astro';
25
25
  import bookConfig from 'virtual:book-scaffold/book-config';
26
26
  import ChapterHeader from '../components/ChapterHeader.astro';
27
27
  import ChapterTOC from '../components/ChapterTOC.astro';
28
+ import SectionMap from '../components/SectionMap.astro';
28
29
  import ChapterNav from '../components/ChapterNav.astro';
29
30
 
30
31
  interface Props {
@@ -44,6 +45,39 @@ const articlePublished = (entry.data as { published?: Date }).published;
44
45
  const articleUpdated = (entry.data as { updated?: Date }).updated;
45
46
  const articleTags = ((entry.data as { tags?: string[] }).tags ?? []) as string[];
46
47
  const chapterImage = (entry.data as { image?: string }).image;
48
+
49
+ // (#section-map): best-effort context label for the gutter section map, e.g.
50
+ // "Part 2 · Ch 4" or "Week 1". All hierarchy fields are profile-dependent and
51
+ // Zod-optional (academic carries part+week; tools/research-portfolio carry
52
+ // part+chapter; minimal carries none), so each piece is included only when
53
+ // present — the label is omitted entirely (undefined) when nothing is derivable.
54
+ const dataAny = entry.data as {
55
+ part?: string | number;
56
+ chapter?: number;
57
+ week?: number;
58
+ layout?: 'default' | 'wide';
59
+ };
60
+
61
+ // 1d: per-page width knob. `layout` is Zod-optional on every profile schema
62
+ // (closed enum 'default'|'wide'). Emit `data-layout` ONLY for a non-default
63
+ // value so existing chapters (no `layout`, or `layout: default`) render the
64
+ // attribute-free <article> byte-identically — `undefined` makes Astro drop the
65
+ // attribute, and only `.prose[data-layout="wide"]` carries a rule in layout.css.
66
+ const layoutMode =
67
+ dataAny.layout && dataAny.layout !== 'default' ? dataAny.layout : undefined;
68
+ const labelParts: string[] = [];
69
+ if (dataAny.part !== undefined && dataAny.part !== '') {
70
+ // Numeric part → "Part N"; string-enum part (academic, e.g. "foundations")
71
+ // → title-case it as-is.
72
+ labelParts.push(
73
+ typeof dataAny.part === 'number'
74
+ ? `Part ${dataAny.part}`
75
+ : dataAny.part.charAt(0).toUpperCase() + dataAny.part.slice(1),
76
+ );
77
+ }
78
+ if (dataAny.chapter !== undefined) labelParts.push(`Ch ${dataAny.chapter}`);
79
+ else if (dataAny.week !== undefined) labelParts.push(`Week ${dataAny.week}`);
80
+ const sectionMapLabel = labelParts.length > 0 ? labelParts.join(' · ') : undefined;
47
81
  ---
48
82
 
49
83
  <Base
@@ -62,9 +96,10 @@ const chapterImage = (entry.data as { image?: string }).image;
62
96
  )}
63
97
  {articleTags.map((t) => <meta property="article:tag" content={t} />)}
64
98
  </Fragment>
65
- <article class="prose">
99
+ <article class="prose" data-layout={layoutMode}>
66
100
  <ChapterHeader data={entry.data} />
67
101
  <ChapterTOC headings={headings} />
102
+ <SectionMap headings={headings} label={sectionMapLabel} />
68
103
  <slot />
69
104
  <ChapterNav currentId={entry.id} />
70
105
  </article>