@brandon_m_behring/book-scaffold-astro 4.1.2 → 4.3.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.
@@ -17,6 +17,7 @@ const handlers = {
17
17
  'build-labels': '../scripts/build-labels.mjs',
18
18
  'build-bib': '../scripts/build-bib.mjs',
19
19
  'build-figures': '../scripts/build-figures.mjs',
20
+ 'build-tips': '../scripts/build-tips.mjs',
20
21
  'render-notebooks': '../scripts/render-notebooks.mjs',
21
22
  };
22
23
 
@@ -26,7 +27,8 @@ Sub-commands:
26
27
  validate Pre-flight content validator (XRef ids, Cite keys, Figure srcs).
27
28
  build-labels Emit src/data/labels.json for cross-references (Phase C).
28
29
  build-bib BibTeX -> CSL JSON for the <Cite> component.
29
- build-figures PDF -> SVG via pdftocairo / pdftoppm fallback.
30
+ build-figures PDF -> SVG via pdftocairo / pdftoppm fallback (+ TikZ in v4.2.0).
31
+ build-tips Scan chapters for <Tip> instances; emit src/data/tips.json (v4.3.0).
30
32
  render-notebooks ipynb -> HTML via Jupyter nbconvert.
31
33
 
32
34
  --help, -h This message.
@@ -0,0 +1,24 @@
1
+ ---
2
+ /**
3
+ * Exercise — inline at concept introduction per CS:APP precedent
4
+ * (v4.3.0, closes #71).
5
+ *
6
+ * Reader is expected to attempt while reading. Solutions live at the
7
+ * chapter end via id reference (paired via <Solution for="{id}"> inside
8
+ * <ExerciseSolutions>; manual pairing — no auto-collection).
9
+ *
10
+ * Light treatment: border-left in neutral color + "Exercise" label +
11
+ * anchor id (`#exercise-{id}`) for cross-linking from <Solution>.
12
+ *
13
+ * Family: book-genre (cross-profile).
14
+ */
15
+ interface Props {
16
+ id: string;
17
+ }
18
+ const { id } = Astro.props;
19
+ const anchorId = `exercise-${id}`;
20
+ ---
21
+ <aside class="exercise" id={anchorId} role="note">
22
+ <strong class="exercise-label">Exercise</strong>
23
+ <div class="exercise-body"><slot /></div>
24
+ </aside>
@@ -0,0 +1,22 @@
1
+ ---
2
+ /**
3
+ * ExerciseSolutions — chapter-end wrapper that provides the section
4
+ * heading + container for <Solution> elements (v4.3.0, closes #71).
5
+ *
6
+ * Author places this at chapter end and fills it with <Solution for="...">
7
+ * elements. The wrapper provides:
8
+ * - <h2>Exercise solutions</h2> heading
9
+ * - Container styling (separates the section visually from preceding prose)
10
+ * - Anchor (`#exercise-solutions`) so the section is linkable
11
+ *
12
+ * v4.3.0 design: manual <Solution> placement (no auto-collection of
13
+ * <Exercise> content). Auto-collection deferred to v4.4.0+ if real
14
+ * demand surfaces — would require MDX AST traversal at build time.
15
+ *
16
+ * Family: book-genre (cross-profile).
17
+ */
18
+ ---
19
+ <section class="exercise-solutions" id="exercise-solutions">
20
+ <h2 class="exercise-solutions-heading">Exercise solutions</h2>
21
+ <div class="exercise-solutions-body"><slot /></div>
22
+ </section>
@@ -0,0 +1,30 @@
1
+ ---
2
+ /**
3
+ * Practice — end-of-chapter problem with difficulty marker per CS:APP
4
+ * (v4.3.0, closes #71).
5
+ *
6
+ * Difficulty 1-4 rendered as ◆ count (filled diamonds out of 4). No
7
+ * inline solutions; instructor-manual style — separate from Exercise
8
+ * which has solutions at chapter end.
9
+ *
10
+ * Anchor id (`#practice-{id}`) for cross-references.
11
+ *
12
+ * Family: book-genre (cross-profile).
13
+ */
14
+ type Difficulty = '1' | '2' | '3' | '4';
15
+ interface Props {
16
+ id: string;
17
+ difficulty: Difficulty;
18
+ }
19
+ const { id, difficulty } = Astro.props;
20
+ const anchorId = `practice-${id}`;
21
+ const filled = Number.parseInt(difficulty, 10);
22
+ const markers = '◆'.repeat(filled) + '◇'.repeat(4 - filled);
23
+ ---
24
+ <aside class="practice" id={anchorId} role="note">
25
+ <div class="practice-header">
26
+ <strong class="practice-label">Practice</strong>
27
+ <span class="practice-difficulty" aria-label={`Difficulty ${difficulty} of 4`}>{markers}</span>
28
+ </div>
29
+ <div class="practice-body"><slot /></div>
30
+ </aside>
@@ -0,0 +1,27 @@
1
+ ---
2
+ /**
3
+ * Solution — paired by id with an <Exercise> at chapter end
4
+ * (v4.3.0, closes #71).
5
+ *
6
+ * Author writes `<Solution for="ch1-ex-2">solution text</Solution>` inside
7
+ * an <ExerciseSolutions> wrapper. The `for` attribute matches the
8
+ * corresponding <Exercise>'s `id`; the rendered solution links back to
9
+ * the inline exercise via `#exercise-{for}`.
10
+ *
11
+ * Manual pairing — no build-time auto-collection of Exercise content
12
+ * (deferred to v4.4.0+). Author maintains the id↔for mapping by hand.
13
+ *
14
+ * Family: book-genre (cross-profile).
15
+ */
16
+ interface Props {
17
+ for: string;
18
+ }
19
+ const { for: forId } = Astro.props;
20
+ ---
21
+ <div class="solution" id={`solution-${forId}`}>
22
+ <div class="solution-header">
23
+ <strong class="solution-label">Solution</strong>
24
+ <a href={`#exercise-${forId}`} class="solution-backlink">↑ Exercise</a>
25
+ </div>
26
+ <div class="solution-body"><slot /></div>
27
+ </div>
@@ -0,0 +1,28 @@
1
+ ---
2
+ /**
3
+ * Tip — numbered cross-volume tip per Pragmatic Programmer precedent
4
+ * (v4.3.0, closes #70).
5
+ *
6
+ * Author writes `<Tip n="14" title="Care About Your Craft">rule statement</Tip>`.
7
+ * Number is explicit (registry doesn't auto-number). Body slot is the
8
+ * pull-quotable single-sentence rule.
9
+ *
10
+ * Gold border (reuses --callout-insight); distinctive single-line layout
11
+ * + anchor id (`#tip-{n}`) for cross-referencing from later chapters.
12
+ *
13
+ * Family: book-genre (cross-profile; usable from any preset).
14
+ */
15
+ interface Props {
16
+ n: string | number;
17
+ title: string;
18
+ }
19
+ const { n, title } = Astro.props;
20
+ const anchorId = `tip-${n}`;
21
+ ---
22
+ <aside class="callout callout-tip-numbered" id={anchorId} role="note">
23
+ <div class="callout-tip-header">
24
+ <span class="callout-tip-number">Tip {n}</span>
25
+ <strong class="callout-tip-title">{title}</strong>
26
+ </div>
27
+ <div class="callout-body"><slot /></div>
28
+ </aside>
@@ -0,0 +1,44 @@
1
+ ---
2
+ /**
3
+ * TipsCard — print-friendly pull-out card listing all numbered tips
4
+ * (v4.3.0, closes #70).
5
+ *
6
+ * Reads `src/data/tips.json` (emitted by `book-scaffold build-tips`).
7
+ * Author places it on a back-matter print route (e.g., `/print-tips`).
8
+ *
9
+ * Renders a single-column ordered list with page-break-inside: avoid
10
+ * so the card prints cleanly on one or more dedicated pages.
11
+ *
12
+ * Graceful skip: if tips.json doesn't exist or is empty, renders an
13
+ * empty container with a note (doesn't fail the build). Consumers who
14
+ * don't use the tips feature can include the component without prep.
15
+ */
16
+ let tips: Array<{ n: number; title: string; chapter: string; preview: string }> = [];
17
+ try {
18
+ const mod = await import('../../../src/data/tips.json', { with: { type: 'json' } });
19
+ tips = (mod.default ?? []) as typeof tips;
20
+ } catch {
21
+ // tips.json not generated yet — render empty card with hint.
22
+ }
23
+ ---
24
+ <aside class="tips-card" role="contentinfo">
25
+ <h2 class="tips-card-title">Tips</h2>
26
+ {tips.length === 0 && (
27
+ <p class="tips-card-empty">
28
+ No tips found. Run <code>book-scaffold build-tips</code> to generate
29
+ <code>src/data/tips.json</code> from <code>&lt;Tip&gt;</code> instances in chapters.
30
+ </p>
31
+ )}
32
+ {tips.length > 0 && (
33
+ <ol class="tips-card-list">
34
+ {tips.map((tip) => (
35
+ <li class="tips-card-item">
36
+ <a href={`/tips#tip-${tip.n}`} class="tips-card-link">
37
+ <span class="tips-card-number">{tip.n}.</span>
38
+ <strong class="tips-card-rule">{tip.title}</strong>
39
+ </a>
40
+ </li>
41
+ ))}
42
+ </ol>
43
+ )}
44
+ </aside>
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AstroUserConfig, AstroIntegration } from 'astro';
2
- import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-DR0-GwxO.js';
3
- export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-DR0-GwxO.js';
2
+ import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-B8Js3qF0.js';
3
+ export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-B8Js3qF0.js';
4
4
  import 'astro/zod';
5
5
 
6
6
  declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
@@ -195,4 +195,53 @@ declare const BUILTIN_STYLES: {
195
195
  readonly 'research-portfolio': Style;
196
196
  };
197
197
 
198
- export { BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type VolatilityLevel, academicChaptersRenderer, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
198
+ /**
199
+ * src/lib/define-tips.ts — `defineTips()` API for cross-volume tip registry
200
+ * (v4.3.0, closes #70).
201
+ *
202
+ * Pragmatic Programmer-style numbered tips can be distributed across multiple
203
+ * volumes (e.g., Handbook tips 1-25, Architect's Reference 26-40, Field-Guide
204
+ * 41-50). Authors write `<Tip n="14" ...>` with explicit numbers; defineTips()
205
+ * lets per-volume books offset their displayed numbers + label without
206
+ * renumbering source tags.
207
+ *
208
+ * Branded type follows the same convention as `defineStyle` (v4.0.0 D6):
209
+ * type-only `unique symbol` brand, closed shape, readonly fields, no public
210
+ * index signature. Consumer-side metadata goes in scoped `extra` if needed.
211
+ */
212
+ declare const TipsConfigBrand: unique symbol;
213
+ interface TipsConfig {
214
+ /** Type-only brand for nominal typing. Set automatically by defineTips. */
215
+ readonly [TipsConfigBrand]: true;
216
+ /** Internal version marker; auto-set to 1 by defineTips. */
217
+ readonly __tipsConfigVersion: 1;
218
+ /** Display offset added to each `<Tip n="N">` for cross-volume coordination.
219
+ * Example: Vol B with volumeOffset=25 renders `<Tip n="1">` as "Tip 26". */
220
+ readonly volumeOffset?: number;
221
+ /** Optional label shown alongside tip numbers in the /tips index + TipsCard.
222
+ * Example: "Vol B" → "Vol B Tip 26". */
223
+ readonly volumeLabel?: string;
224
+ /** Scoped consumer-side metadata (matches defineStyle pattern). */
225
+ readonly extra?: Readonly<Record<string, unknown>>;
226
+ }
227
+ /** Input type for defineTips — omits the auto-set internal fields. */
228
+ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVersion'>;
229
+ /**
230
+ * Identity helper that creates a typed, branded TipsConfig.
231
+ * Zero runtime overhead beyond an object spread + version marker.
232
+ *
233
+ * Usage:
234
+ *
235
+ * import { defineTips } from '@brandon_m_behring/book-scaffold-astro';
236
+ *
237
+ * export const tipsConfig = defineTips({
238
+ * volumeOffset: 25,
239
+ * volumeLabel: 'Vol B',
240
+ * });
241
+ *
242
+ * Consumed by `<Tip>` and `<TipsCard>` components + the auto-injected
243
+ * `/tips` route to compute display numbers from `<Tip n="N">` source tags.
244
+ */
245
+ declare function defineTips(opts: TipsConfigInput): TipsConfig;
246
+
247
+ export { BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type TipsConfig, type TipsConfigInput, type VolatilityLevel, academicChaptersRenderer, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
package/dist/index.mjs CHANGED
@@ -376,8 +376,10 @@ var academicProfile = defineProfile({
376
376
  // academic consumers ship their own week-based /chapters listing
377
377
  convergence: false,
378
378
  // tools-profile-specific
379
- frontmatter: false
379
+ frontmatter: false,
380
380
  // opt-in per book; see #7
381
+ tips: false
382
+ // v4.3.0 #70: opt-in per book; requires build-tips
381
383
  },
382
384
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
383
385
  katex: true,
@@ -490,8 +492,10 @@ var toolsProfile = defineProfile({
490
492
  // tools profile ships a flat chapter index
491
493
  convergence: true,
492
494
  // tools profile ships convergence dashboard
493
- frontmatter: false
495
+ frontmatter: false,
494
496
  // opt-in per book; see #7
497
+ tips: false
498
+ // v4.3.0 #70: opt-in per book
495
499
  },
496
500
  styles: [
497
501
  "tokens.css",
@@ -568,8 +572,10 @@ var minimalProfile = defineProfile({
568
572
  print: true,
569
573
  chapters: false,
570
574
  convergence: false,
571
- frontmatter: false
575
+ frontmatter: false,
572
576
  // opt-in per book; see #7
577
+ tips: false
578
+ // v4.3.0 #70: opt-in per book
573
579
  },
574
580
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
575
581
  // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
@@ -587,8 +593,10 @@ var courseNotesProfile = defineProfile({
587
593
  chapters: false,
588
594
  // multi-book consumers route via [book]/[slug] themselves
589
595
  convergence: false,
590
- frontmatter: false
596
+ frontmatter: false,
591
597
  // opt-in per book; see #7
598
+ tips: false
599
+ // v4.3.0 #70: opt-in per book
592
600
  },
593
601
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
594
602
  // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
@@ -607,8 +615,10 @@ var researchPortfolioProfile = defineProfile({
607
615
  // portfolio books ship their own landing/index
608
616
  convergence: false,
609
617
  // tools-profile-specific
610
- frontmatter: true
618
+ frontmatter: true,
611
619
  // portfolios universally need title/disclosure/banner pages
620
+ tips: false
621
+ // v4.3.0 #70: opt-in per book
612
622
  },
613
623
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
614
624
  katex: true,
@@ -800,7 +810,16 @@ var ROUTE_REGISTRY = {
800
810
  search: { pattern: "/search", file: "search.astro" },
801
811
  print: { pattern: "/print", file: "print.astro" },
802
812
  chapters: { pattern: "/chapters", file: "chapters.astro" },
813
+ // v4.3.0 (#69): per-chapter dynamic route auto-injected when
814
+ // routes.chapters: true. Mirrors the frontmatter pattern — toolkit ships
815
+ // BOTH the /chapters/ index AND the /chapters/<slug>/ dynamic route.
816
+ // Pre-v4.3.0 each consumer wrote this file by hand; all instances were
817
+ // mechanical copies of the same boilerplate.
818
+ chaptersSlug: { pattern: "/chapters/[...slug]", file: "chapters/[...slug].astro" },
803
819
  convergence: { pattern: "/convergence", file: "convergence.astro" },
820
+ // v4.3.0 (#70): cross-volume numbered-tips index. Opt-in via
821
+ // routes.tips: true; pairs with build-tips script + <Tip> component.
822
+ tips: { pattern: "/tips", file: "tips.astro" },
804
823
  // v3.4.0 (#7): consumer-collection-backed frontmatter route. Opt-in via
805
824
  // routes: { frontmatter: true } AND content.config.ts defining the
806
825
  // collection (use frontmatterCollection() helper from /schemas subpath).
@@ -841,8 +860,13 @@ function bookScaffoldIntegration(opts) {
841
860
  if (def.katex) {
842
861
  injectScript("page-ssr", "import 'katex/dist/katex.min.css';");
843
862
  }
863
+ const routesToInject = [];
844
864
  for (const [name, on] of Object.entries(enabledRoutes)) {
845
865
  if (!on) continue;
866
+ routesToInject.push(name);
867
+ if (name === "chapters") routesToInject.push("chaptersSlug");
868
+ }
869
+ for (const name of routesToInject) {
846
870
  const route = ROUTE_REGISTRY[name];
847
871
  if (!route) continue;
848
872
  const pattern = name === "frontmatter" ? frontmatterPatternFromPrefix(fmPrefix) : route.pattern;
@@ -1070,6 +1094,11 @@ var BUILTIN_STYLES = {
1070
1094
  "course-notes": courseNotesStyle,
1071
1095
  "research-portfolio": researchPortfolioStyle
1072
1096
  };
1097
+
1098
+ // src/lib/define-tips.ts
1099
+ function defineTips(opts) {
1100
+ return { __tipsConfigVersion: 1, ...opts };
1101
+ }
1073
1102
  export {
1074
1103
  BOOK_PRESETS,
1075
1104
  BOOK_PROFILES,
@@ -1091,6 +1120,7 @@ export {
1091
1120
  defineMdxComponents,
1092
1121
  defineProfile,
1093
1122
  defineStyle,
1123
+ defineTips,
1094
1124
  fallbackChaptersRenderer,
1095
1125
  freshnessLabel,
1096
1126
  getFreshness,
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-DR0-GwxO.js';
2
+ import { g as BookSchemasOptions } from './types-B8Js3qF0.js';
3
3
  import 'astro';
4
4
  import 'astro/zod';
5
5
 
package/dist/schemas.mjs CHANGED
@@ -261,8 +261,10 @@ var academicProfile = defineProfile({
261
261
  // academic consumers ship their own week-based /chapters listing
262
262
  convergence: false,
263
263
  // tools-profile-specific
264
- frontmatter: false
264
+ frontmatter: false,
265
265
  // opt-in per book; see #7
266
+ tips: false
267
+ // v4.3.0 #70: opt-in per book; requires build-tips
266
268
  },
267
269
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
268
270
  katex: true,
@@ -375,8 +377,10 @@ var toolsProfile = defineProfile({
375
377
  // tools profile ships a flat chapter index
376
378
  convergence: true,
377
379
  // tools profile ships convergence dashboard
378
- frontmatter: false
380
+ frontmatter: false,
379
381
  // opt-in per book; see #7
382
+ tips: false
383
+ // v4.3.0 #70: opt-in per book
380
384
  },
381
385
  styles: [
382
386
  "tokens.css",
@@ -453,8 +457,10 @@ var minimalProfile = defineProfile({
453
457
  print: true,
454
458
  chapters: false,
455
459
  convergence: false,
456
- frontmatter: false
460
+ frontmatter: false,
457
461
  // opt-in per book; see #7
462
+ tips: false
463
+ // v4.3.0 #70: opt-in per book
458
464
  },
459
465
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
460
466
  // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
@@ -472,8 +478,10 @@ var courseNotesProfile = defineProfile({
472
478
  chapters: false,
473
479
  // multi-book consumers route via [book]/[slug] themselves
474
480
  convergence: false,
475
- frontmatter: false
481
+ frontmatter: false,
476
482
  // opt-in per book; see #7
483
+ tips: false
484
+ // v4.3.0 #70: opt-in per book
477
485
  },
478
486
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
479
487
  // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
@@ -492,8 +500,10 @@ var researchPortfolioProfile = defineProfile({
492
500
  // portfolio books ship their own landing/index
493
501
  convergence: false,
494
502
  // tools-profile-specific
495
- frontmatter: true
503
+ frontmatter: true,
496
504
  // portfolios universally need title/disclosure/banner pages
505
+ tips: false
506
+ // v4.3.0 #70: opt-in per book
497
507
  },
498
508
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
499
509
  katex: true,
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.1.2",
4
+ "version": "4.3.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -57,6 +57,8 @@
57
57
  "./components/Divergence.astro": "./components/Divergence.astro",
58
58
  "./components/DynConnect.astro": "./components/DynConnect.astro",
59
59
  "./components/ExampleBox.astro": "./components/ExampleBox.astro",
60
+ "./components/Exercise.astro": "./components/Exercise.astro",
61
+ "./components/ExerciseSolutions.astro": "./components/ExerciseSolutions.astro",
60
62
  "./components/Figure.astro": "./components/Figure.astro",
61
63
  "./components/InsightBox.astro": "./components/InsightBox.astro",
62
64
  "./components/KeyIdea.astro": "./components/KeyIdea.astro",
@@ -68,17 +70,21 @@
68
70
  "./components/Pitfall.astro": "./components/Pitfall.astro",
69
71
  "./components/PocLayout.astro": "./components/PocLayout.astro",
70
72
  "./components/PolicyRef.astro": "./components/PolicyRef.astro",
73
+ "./components/Practice.astro": "./components/Practice.astro",
71
74
  "./components/PreReleaseBanner.astro": "./components/PreReleaseBanner.astro",
72
75
  "./components/Recovery.astro": "./components/Recovery.astro",
73
76
  "./components/ResultBox.astro": "./components/ResultBox.astro",
74
77
  "./components/Sidebar.astro": "./components/Sidebar.astro",
75
78
  "./components/Sidenote.astro": "./components/Sidenote.astro",
76
79
  "./components/SkillBox.astro": "./components/SkillBox.astro",
80
+ "./components/Solution.astro": "./components/Solution.astro",
77
81
  "./components/SourceArchive.astro": "./components/SourceArchive.astro",
78
82
  "./components/StatusBadge.astro": "./components/StatusBadge.astro",
79
83
  "./components/Tag.astro": "./components/Tag.astro",
80
84
  "./components/Theorem.astro": "./components/Theorem.astro",
85
+ "./components/Tip.astro": "./components/Tip.astro",
81
86
  "./components/TipBox.astro": "./components/TipBox.astro",
87
+ "./components/TipsCard.astro": "./components/TipsCard.astro",
82
88
  "./components/ToolFilter": {
83
89
  "types": "./dist/components/ToolFilter.d.ts",
84
90
  "import": "./dist/components/ToolFilter.mjs"
@@ -0,0 +1,40 @@
1
+ ---
2
+ /**
3
+ * Auto-injected per-chapter dynamic route (v4.3.0, closes #69).
4
+ *
5
+ * Gated on `routes.chapters: true` via bookScaffoldIntegration's
6
+ * injectRoute call. Mirrors the frontmatter route pattern (v3.4.0 #7):
7
+ * the toolkit ships BOTH the /chapters/ index AND the /chapters/<slug>/
8
+ * dynamic route, so consumers don't have to write the boilerplate.
9
+ *
10
+ * Layout switching by preset:
11
+ * - academic + research-portfolio → Chapter.astro (KaTeX + theorem chrome)
12
+ * - all others → Base.astro (lighter; for tools/minimal/course-notes)
13
+ *
14
+ * Pre-v4.3.0 each consumer wrote this file by hand; all instances were
15
+ * mechanical copies (see post_transformers/guides/web/src/pages/chapters/
16
+ * [...slug].astro and double_ml_time_series/web/src/pages/chapters/
17
+ * [...slug].astro for the canonical pattern).
18
+ */
19
+ import { getCollection, render } from 'astro:content';
20
+ import Chapter from '../../layouts/Chapter.astro';
21
+ import Base from '../../layouts/Base.astro';
22
+
23
+ const BOOK_PRESET = import.meta.env.BOOK_PRESET ?? 'minimal';
24
+ const USE_CHAPTER_LAYOUT = ['academic', 'research-portfolio'].includes(BOOK_PRESET);
25
+
26
+ export async function getStaticPaths() {
27
+ const chapters = await getCollection('chapters', (entry) => !entry.data.draft);
28
+ return chapters.map((entry) => ({
29
+ params: { slug: entry.id },
30
+ props: { entry },
31
+ }));
32
+ }
33
+
34
+ const { entry } = Astro.props;
35
+ const { Content, headings } = await render(entry);
36
+ const Layout = USE_CHAPTER_LAYOUT ? Chapter : Base;
37
+ ---
38
+ <Layout entry={entry} headings={headings}>
39
+ <Content />
40
+ </Layout>
@@ -112,7 +112,7 @@ for (const c of chapters) {
112
112
  <ol class="chapter-list">
113
113
  {cards.map((card) => (
114
114
  <li class="chapter-card" data-tools={card.toolsAttr}>
115
- <a href={`/${card.c.id}/`} class="chapter-card-link">
115
+ <a href={`/chapters/${card.c.id}/`} class="chapter-card-link">
116
116
  <div class="chapter-card-meta">
117
117
  <span class="chapter-card-number">{card.number}</span>
118
118
  {card.volatility && (
@@ -0,0 +1,51 @@
1
+ ---
2
+ /**
3
+ * Auto-injected /tips route (v4.3.0, closes #70).
4
+ *
5
+ * Gated on `routes.tips: true`. Reads `src/data/tips.json` (emitted by
6
+ * `book-scaffold build-tips`); renders an ordered list with anchor
7
+ * permalinks (`/tips#tip-N`).
8
+ *
9
+ * Graceful skip: if tips.json doesn't exist, renders an instructions
10
+ * page (doesn't fail the build).
11
+ */
12
+ import Base from '../layouts/Base.astro';
13
+
14
+ let tips: Array<{ n: number; title: string; chapter: string; preview: string }> = [];
15
+ let loadError: string | null = null;
16
+ try {
17
+ const mod = await import('../../../src/data/tips.json', { with: { type: 'json' } });
18
+ tips = (mod.default ?? []) as typeof tips;
19
+ } catch {
20
+ loadError = 'src/data/tips.json not found — run `npx book-scaffold build-tips` to generate.';
21
+ }
22
+ ---
23
+ <Base title="Tips" description="Numbered tips from this book, drawn from <Tip> instances in chapters.">
24
+ <article class="prose">
25
+ <h1>Tips</h1>
26
+ {loadError && (
27
+ <p class="tips-empty">{loadError}</p>
28
+ )}
29
+ {!loadError && tips.length === 0 && (
30
+ <p class="tips-empty">No tips defined yet. Add <code>&lt;Tip n="1" title="..."&gt;...&lt;/Tip&gt;</code> to a chapter and rebuild.</p>
31
+ )}
32
+ {tips.length > 0 && (
33
+ <ol class="tips-index">
34
+ {tips.map((tip) => (
35
+ <li id={`tip-${tip.n}`} class="tips-index-item">
36
+ <h2 class="tips-index-title">
37
+ <span class="tips-index-number">Tip {tip.n}.</span>
38
+ {tip.title}
39
+ </h2>
40
+ {tip.preview && (
41
+ <p class="tips-index-preview">{tip.preview}</p>
42
+ )}
43
+ <p class="tips-index-meta">
44
+ From <a href={`/chapters/${tip.chapter}/`}>{tip.chapter}</a>
45
+ </p>
46
+ </li>
47
+ ))}
48
+ </ol>
49
+ )}
50
+ </article>
51
+ </Base>