@brandon_m_behring/book-scaffold-astro 3.6.5 → 3.7.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.
@@ -2,59 +2,91 @@
2
2
  /**
3
3
  * /chapters — book index page.
4
4
  *
5
- * Groups non-draft chapters by Part in ascending order. Each card exposes
6
- * `data-tools="<slug> <slug>..."` so the ToolFilter island can hide cards via
7
- * a single attribute selector without touching card rendering.
5
+ * Route-level concerns kept here:
6
+ * - Data fetch (getAllChapters)
7
+ * - byPart Map grouping (insertion-order preserved across number + string keys)
8
+ * - ToolFilter island wiring (inline script, data-tools attribute on cards,
9
+ * part-group hide-when-empty, filter hint)
10
+ * - CSS, structural JSX, <Base> wrapping
8
11
  *
9
- * v3.5.2 (closes #24): schema-aware. Previously hardcoded the tools-profile
10
- * shape and crashed on academic profile (no `chapter` / `volatility` /
11
- * `tools_compared` / `last_verified`). Now renders either shape:
12
- * - tools: `Chapter N` + volatility badge + freshness + tools-compared tags
13
- * - academic: `Week N` + status badge (no freshness or tools-compared)
14
- * Field presence is the schema discriminator (cheaper than reading
15
- * BOOK_PROFILE at the route layer).
12
+ * Per-profile concerns dispatched via `PROFILES[BOOK_PROFILE].chaptersRenderer`:
13
+ * - Meta-row composition (numbering, badges)
14
+ * - Sort key strategy
15
+ * - data-tools value computation
16
+ *
17
+ * v3.7.0 (closes #35): replaces the field-presence discriminator that
18
+ * lived inline in v3.5.2. Each profile now ships a ChaptersRenderer
19
+ * implementing the strategy interface; consumers opting profiles without
20
+ * a dedicated renderer (e.g., minimal + routes.chapters: true) get the
21
+ * fallbackChaptersRenderer which dispatches via field presence — exactly
22
+ * the v3.5.2 behavior, preserved as a safety net.
16
23
  */
17
24
  import Base from '../layouts/Base.astro';
18
25
  import { getAllChapters, type Chapter } from '../src/lib/chapters';
19
- import { getFreshness, freshnessLabel } from '../src/lib/freshness';
20
-
21
- const chapters = await getAllChapters();
26
+ import { PROFILES } from '../src/profiles/index';
27
+ import { fallbackChaptersRenderer } from '../src/profiles/renderers/fallback-chapters';
28
+ import type { ChaptersRenderer, PartKey } from '../src/lib/chapters-renderer';
22
29
 
23
- // Stable insertion-order grouping by `part`. Map key may be number (tools) or
24
- // string (academic enum); Map preserves insertion order for both.
25
- type PartKey = string | number;
26
- const byPart = new Map<PartKey, Chapter[]>();
27
- for (const c of chapters) {
28
- const key = (c.data as { part: PartKey }).part;
29
- const list = byPart.get(key);
30
- if (list) list.push(c);
31
- else byPart.set(key, [c]);
32
- }
30
+ const profileName = (import.meta.env.BOOK_PROFILE ?? 'minimal') as keyof typeof PROFILES;
31
+ const profileDef = PROFILES[profileName];
32
+ const renderer: ChaptersRenderer =
33
+ (profileDef as { chaptersRenderer?: ChaptersRenderer } | undefined)?.chaptersRenderer
34
+ ?? fallbackChaptersRenderer;
33
35
 
34
- function formatDate(d: Date): string {
35
- return d.toISOString().slice(0, 10);
36
- }
36
+ const chapters = await getAllChapters();
37
37
 
38
- /** Render-ready label for a Part heading. Tools: "Part N" or "Appendices"
39
- * (parts >= 6 are appendices). Academic: titlecase the enum string. */
40
- function partLabel(part: PartKey): string {
41
- if (typeof part === 'number') {
42
- return part >= 6 ? 'Appendices' : `Part ${part}`;
43
- }
44
- return part
45
- .split('-')
46
- .map((w) => (w.length ? w[0].toUpperCase() + w.slice(1) : ''))
47
- .join(' ');
38
+ // Precompute each card's render-ready fields in the frontmatter block.
39
+ // This keeps TypeScript generics (Record<string, unknown> casts, the
40
+ // renderer return-type unions) out of the JSX template, where the
41
+ // Astro compiler treats `<` as a tag-start token and trips on type
42
+ // assertions inside expressions.
43
+ interface CardData {
44
+ c: Chapter;
45
+ toolsAttr: string;
46
+ number: string;
47
+ volatility: ReturnType<typeof renderer.getVolatilityData>;
48
+ status: ReturnType<typeof renderer.getStatusData>;
49
+ freshness: ReturnType<typeof renderer.getFreshnessData>;
50
+ freshnessText: string | null;
51
+ verifiedLabel: string | null;
52
+ toolsCompared: string[];
53
+ data: { title: unknown; description?: unknown };
48
54
  }
49
55
 
50
- function isAppendix(part: PartKey): boolean {
51
- return typeof part === 'number' && part >= 6;
56
+ function buildCard(c: Chapter, appendix: boolean): CardData {
57
+ const data = c.data as Record<string, unknown>;
58
+ const freshness = renderer.getFreshnessData(data);
59
+ const freshnessText = freshness
60
+ ? freshness.status === 'fresh'
61
+ ? 'Fresh'
62
+ : freshness.status === 'verify-soon'
63
+ ? 'Verify soon'
64
+ : 'Stale'
65
+ : null;
66
+ return {
67
+ c,
68
+ toolsAttr: renderer.getToolsAttr(data),
69
+ number: renderer.formatChapterNumber(data, appendix),
70
+ volatility: renderer.getVolatilityData(data),
71
+ status: renderer.getStatusData(data),
72
+ freshness,
73
+ freshnessText,
74
+ verifiedLabel: renderer.getVerifiedDateLabel(data),
75
+ toolsCompared: renderer.getToolsCompared(data),
76
+ data: { title: data.title, description: data.description as string | undefined },
77
+ };
52
78
  }
53
79
 
54
- /** Per-card schema detection. Cheaper than reading BOOK_PROFILE the shape
55
- * of the chapter's data already tells us which profile produced it. */
56
- function isToolsShape(data: Record<string, unknown>): boolean {
57
- return 'volatility' in data && 'chapter' in data;
80
+ // Stable insertion-order grouping. Map preserves order for both number and
81
+ // string keys (academic uses string-enum parts; tools uses numeric parts).
82
+ const byPart = new Map<PartKey, CardData[]>();
83
+ for (const c of chapters) {
84
+ const key = renderer.partKey(c.data as Record<string, unknown>);
85
+ const appendix = renderer.isAppendix(key);
86
+ const card = buildCard(c, appendix);
87
+ const list = byPart.get(key);
88
+ if (list) list.push(card);
89
+ else byPart.set(key, [card]);
58
90
  }
59
91
  ---
60
92
  <Base
@@ -72,89 +104,58 @@ function isToolsShape(data: Record<string, unknown>): boolean {
72
104
 
73
105
  <p class="chapters-filter-hint" id="filter-hint" aria-live="polite"></p>
74
106
 
75
- {Array.from(byPart.entries()).map(([part, list]) => {
76
- const appendix = isAppendix(part);
77
- return (
78
- <section class="part-group">
79
- <h2 class="part-heading">
80
- <span class="part-label">{partLabel(part)}</span>
81
- </h2>
82
- <ol class="chapter-list">
83
- {list.map((c) => {
84
- const data = c.data as Record<string, unknown>;
85
- const tools = isToolsShape(data);
86
- // data-tools attribute drives the ToolFilter island. Academic
87
- // cards opt out by claiming "cross-tool" — always visible
88
- // regardless of filter state.
89
- const toolsAttr = tools
90
- ? (data.tools_compared as string[]).join(' ')
91
- : 'cross-tool';
92
- const freshness = tools
93
- ? getFreshness(data.last_verified as Date, data.volatility as Parameters<typeof getFreshness>[1])
94
- : null;
95
- const freshnessText = freshness
96
- ? freshness.status === 'fresh'
97
- ? 'Fresh'
98
- : freshness.status === 'verify-soon'
99
- ? 'Verify soon'
100
- : 'Stale'
101
- : null;
102
- return (
103
- <li class="chapter-card" data-tools={toolsAttr}>
104
- <a href={`/${c.id}/`} class="chapter-card-link">
105
- <div class="chapter-card-meta">
106
- <span class="chapter-card-number">
107
- {tools
108
- ? appendix
109
- ? `Appendix ${String.fromCharCode(64 + (data.chapter as number)).toLowerCase()}`
110
- : `Chapter ${data.chapter}`
111
- : `Week ${data.week}`}
112
- </span>
113
- {tools && (
114
- <span
115
- class={`volatility-badge volatility-${data.volatility}`}
116
- title={`Volatility: ${data.volatility}`}
117
- >{data.volatility}</span>
118
- )}
119
- {!tools && data.status && (
120
- <span
121
- class={`status-badge status-${data.status}`}
122
- title={`Status: ${data.status}`}
123
- >{data.status}</span>
124
- )}
125
- {freshness && freshnessText && (
126
- <span
127
- class="freshness-badge"
128
- data-status={freshness.status}
129
- aria-label={freshnessLabel(freshness)}
130
- title={freshnessLabel(freshness)}
131
- >{freshnessText}</span>
132
- )}
133
- {tools && data.last_verified && (
134
- <span class="chapter-card-verified">
135
- verified {formatDate(data.last_verified as Date)}
136
- </span>
137
- )}
138
- </div>
139
- <h3 class="chapter-card-title">{data.title}</h3>
140
- {data.description && (
141
- <p class="chapter-card-description">{data.description as string}</p>
142
- )}
143
- {tools && (
144
- <div class="chapter-card-tools">
145
- {(data.tools_compared as string[]).map((t) => (
146
- <span class="tool-badge">{t}</span>
147
- ))}
148
- </div>
149
- )}
150
- </a>
151
- </li>
152
- );
153
- })}
154
- </ol>
155
- </section>
156
- );
157
- })}
107
+ {Array.from(byPart.entries()).map(([part, cards]) => (
108
+ <section class="part-group">
109
+ <h2 class="part-heading">
110
+ <span class="part-label">{renderer.formatPartLabel(part)}</span>
111
+ </h2>
112
+ <ol class="chapter-list">
113
+ {cards.map((card) => (
114
+ <li class="chapter-card" data-tools={card.toolsAttr}>
115
+ <a href={`/${card.c.id}/`} class="chapter-card-link">
116
+ <div class="chapter-card-meta">
117
+ <span class="chapter-card-number">{card.number}</span>
118
+ {card.volatility && (
119
+ <span
120
+ class={`volatility-badge volatility-${card.volatility.level}`}
121
+ title={`Volatility: ${card.volatility.label}`}
122
+ >{card.volatility.label}</span>
123
+ )}
124
+ {card.status && (
125
+ <span
126
+ class={`status-badge status-${card.status.status}`}
127
+ title={`Status: ${card.status.label}`}
128
+ >{card.status.label}</span>
129
+ )}
130
+ {card.freshness && card.freshnessText && (
131
+ <span
132
+ class="freshness-badge"
133
+ data-status={card.freshness.status}
134
+ aria-label={card.freshness.label}
135
+ title={card.freshness.label}
136
+ >{card.freshnessText}</span>
137
+ )}
138
+ {card.verifiedLabel && (
139
+ <span class="chapter-card-verified">{card.verifiedLabel}</span>
140
+ )}
141
+ </div>
142
+ <h3 class="chapter-card-title">{card.data.title}</h3>
143
+ {card.data.description && (
144
+ <p class="chapter-card-description">{card.data.description as string}</p>
145
+ )}
146
+ {card.toolsCompared.length > 0 && (
147
+ <div class="chapter-card-tools">
148
+ {card.toolsCompared.map((t) => (
149
+ <span class="tool-badge">{t}</span>
150
+ ))}
151
+ </div>
152
+ )}
153
+ </a>
154
+ </li>
155
+ ))}
156
+ </ol>
157
+ </section>
158
+ ))}
158
159
  </article>
159
160
  </Base>
160
161
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * src/lib/chapters-renderer.ts — per-profile strategy interface for the
3
+ * /chapters route. Pure-function strategy; no Astro imports here or in
4
+ * implementations (kept that way so profile modules can re-export renderers
5
+ * without dragging Astro virtual modules into tsup's DTS bundle — same
6
+ * constraint that motivated the chapter-sort.ts split in v3.5.2).
7
+ *
8
+ * v3.7.0 (closes #35): replaces the field-presence discriminator that
9
+ * v3.5.2 put inside pages/chapters.astro. Each profile module now owns
10
+ * its chapters-page rendering semantics via this interface, and the
11
+ * route file dispatches via PROFILES[BOOK_PROFILE].chaptersRenderer.
12
+ *
13
+ * Why pure data, not Astro components: profile modules are bundled by
14
+ * tsup into dist/, but Astro components (.astro files) cannot be processed
15
+ * by tsup's DTS bundler. Keeping the renderer surface as plain data
16
+ * (strings, numbers, plain objects, null) lets the route file own all
17
+ * JSX rendering while each profile owns the per-shape semantics.
18
+ */
19
+
20
+ export type PartKey = string | number;
21
+
22
+ export type FreshnessStatus = 'fresh' | 'verify-soon' | 'stale';
23
+
24
+ /** Volatility badge metadata for tools-profile rendering. */
25
+ export interface VolatilityBadge {
26
+ /** CSS modifier (`stable-principle | architectural-pattern | feature-surface`). */
27
+ level: string;
28
+ /** Visible chip text + tooltip body. */
29
+ label: string;
30
+ }
31
+
32
+ /** Academic status badge metadata. */
33
+ export interface StatusBadge {
34
+ /** CSS modifier (`implemented | scaffolded | planned | ...`). */
35
+ status: string;
36
+ /** Visible chip text + tooltip body. */
37
+ label: string;
38
+ }
39
+
40
+ /** Freshness affordance with status band + ARIA-friendly label. */
41
+ export interface FreshnessAffordance {
42
+ status: FreshnessStatus;
43
+ label: string;
44
+ }
45
+
46
+ /**
47
+ * A renderer owns the per-shape semantics for the /chapters route. The
48
+ * route file orchestrates: data fetching, byPart grouping, ToolFilter
49
+ * island wiring, CSS, structural JSX. The renderer answers:
50
+ *
51
+ * - How is this chapter labelled?
52
+ * - Which badges apply?
53
+ * - What's the ToolFilter attribute?
54
+ * - How do these chapters sort?
55
+ *
56
+ * All methods are pure: same input → same output, no side effects.
57
+ */
58
+ export interface ChaptersRenderer {
59
+ /** Group key for the byPart Map. Tools = number, academic = string-enum. */
60
+ partKey(data: Record<string, unknown>): PartKey;
61
+
62
+ /** Human-facing heading label for a Part group ("Part 1", "Foundations"). */
63
+ formatPartLabel(part: PartKey): string;
64
+
65
+ /** Whether the group renders as an appendix (tools profile only). */
66
+ isAppendix(part: PartKey): boolean;
67
+
68
+ /** Chapter card heading text. Examples:
69
+ * tools → "Chapter 1" or "Appendix a"
70
+ * academic → "Week 3"
71
+ * fallback → best-effort from available fields
72
+ */
73
+ formatChapterNumber(data: Record<string, unknown>, appendix: boolean): string;
74
+
75
+ /** Value for the `data-tools` attribute (ToolFilter island wiring).
76
+ * Tools = space-joined slugs from `tools_compared`.
77
+ * Non-tools = "cross-tool" (always-visible regardless of filter). */
78
+ getToolsAttr(data: Record<string, unknown>): string;
79
+
80
+ /** Volatility badge metadata, or null if the profile doesn't use it. */
81
+ getVolatilityData(data: Record<string, unknown>): VolatilityBadge | null;
82
+
83
+ /** Academic status badge metadata, or null. */
84
+ getStatusData(data: Record<string, unknown>): StatusBadge | null;
85
+
86
+ /** Freshness affordance, or null if the profile doesn't track `last_verified`.
87
+ * `now` is injectable for deterministic tests. */
88
+ getFreshnessData(data: Record<string, unknown>, now?: Date): FreshnessAffordance | null;
89
+
90
+ /** "verified YYYY-MM-DD" meta-row text, or null. */
91
+ getVerifiedDateLabel(data: Record<string, unknown>): string | null;
92
+
93
+ /** Tools-compared tag list for the bottom card row, or empty array. */
94
+ getToolsCompared(data: Record<string, unknown>): string[];
95
+
96
+ /** Sort key — profile-aware. Tools = `part * 1000 + chapter`;
97
+ * academic = `partOrdinal * 1000 + week`. */
98
+ sortKey(data: Record<string, unknown>): number;
99
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * profile-kit — internal helper for declaring book profiles.
3
+ *
4
+ * Each profile (academic, tools, minimal, course-notes, future paper-review,
5
+ * etc.) lives in its own self-contained module under src/profiles/ and uses
6
+ * defineProfile() to declare its schema + auto-injected routes + auto-loaded
7
+ * styles. The PROFILES registry in src/profiles/index.ts wires them together;
8
+ * bookScaffoldIntegration consumes the registry.
9
+ *
10
+ * defineProfile() is an identity function — same pattern as Vite's
11
+ * defineConfig, Astro's defineConfig, Zod's z.object. Currently no generic
12
+ * constraint on the schema parameter: per-profile inferred chapter types
13
+ * are exported separately (AcademicChapter, ToolsChapter, etc.) via
14
+ * `z.infer<typeof schema>`, so the registry doesn't need to track each
15
+ * schema's exact shape. Keeping the schema typed as `unknown` here also
16
+ * avoids tsup's DTS bundler dragging deep Zod internals into the .d.ts
17
+ * (rollup-plugin-dts can't always resolve Zod's `default` export shape).
18
+ *
19
+ * Adding a new profile is a single-file change:
20
+ * 1. Create src/profiles/<name>.ts (define schema + type + profile config).
21
+ * 2. Register it in src/profiles/index.ts (one line in PROFILES + one line
22
+ * in ChapterFor<P>).
23
+ * 3. (Optional) ship a default chapter route page under package/pages/.
24
+ */
25
+
26
+ import type { ChaptersRenderer } from './lib/chapters-renderer.js';
27
+
28
+ /**
29
+ * The set of routes the toolkit can auto-inject. Per-profile defaults are
30
+ * declared in each profile module; consumers override via
31
+ * defineBookConfig({ routes: { … } }).
32
+ *
33
+ * The shape is fixed — adding a new auto-injected route requires updating
34
+ * this type AND adding a default to every profile module. The trade-off is
35
+ * worth it: consumers get TS autocomplete on the route names and TS errors
36
+ * on typos like `convergance: false`.
37
+ */
38
+ export interface RouteToggles {
39
+ references: boolean;
40
+ search: boolean;
41
+ print: boolean;
42
+ chapters: boolean;
43
+ convergence: boolean;
44
+ /**
45
+ * v3.4.0 (closes #7): auto-inject `/frontmatter/[slug]/` rendering a
46
+ * consumer-defined `frontmatter` content collection. Default `false` per
47
+ * profile — opt in via defineBookConfig({ routes: { frontmatter: true } })
48
+ * AND define the collection via `frontmatterCollection()` in content.config.ts.
49
+ * If enabled without defining the collection, Astro errors clearly at build.
50
+ */
51
+ frontmatter: boolean;
52
+ }
53
+
54
+ /** Profile definition — declarative shape for one book profile. */
55
+ export interface ProfileDefinition {
56
+ /** Stable name; must match the key in PROFILES + the BOOK_PROFILE env value. */
57
+ name: string;
58
+ /**
59
+ * The Zod schema used as the chapter collection schema. Typed as
60
+ * `unknown` here on purpose — per-profile inferred chapter types
61
+ * (AcademicChapter, ToolsChapter, …) are exported separately and give
62
+ * consumers the narrow typing where it matters. defineCollection
63
+ * (in src/schemas-entry.ts) accepts the schema runtime-style.
64
+ */
65
+ schema: unknown;
66
+ /** Auto-injected routes; consumers override via defineBookConfig({ routes }). */
67
+ routes: RouteToggles;
68
+ /** CSS basenames loaded for this profile (resolved from package/styles/). */
69
+ styles: string[];
70
+ /** Whether KaTeX should be wired in (academic profile only currently). */
71
+ katex?: boolean;
72
+ /**
73
+ * v3.7.0 (closes #35): per-profile renderer for the /chapters route.
74
+ * Owns the chapter-card meta-row composition, numbering format, sort key,
75
+ * and ToolFilter wiring for this profile's data shape. Pure-function
76
+ * strategy (no Astro imports — see src/lib/chapters-renderer.ts header).
77
+ *
78
+ * Optional: profiles that don't ship a dedicated renderer get the
79
+ * fallbackChaptersRenderer (field-presence dispatch) at route render time.
80
+ */
81
+ chaptersRenderer?: ChaptersRenderer;
82
+ }
83
+
84
+ /**
85
+ * Identity helper for declaring a profile module.
86
+ *
87
+ * export const courseNotesProfile = defineProfile({
88
+ * name: 'course-notes',
89
+ * schema: courseNotesChapterSchema,
90
+ * routes: { references: true, search: true, print: true, chapters: false, convergence: false },
91
+ * styles: ['tokens.css', ...],
92
+ * });
93
+ *
94
+ * No runtime work; the value goes through unchanged. Useful as a typed
95
+ * "this is a profile" marker that catches missing required fields at
96
+ * authoring time.
97
+ */
98
+ export function defineProfile(p: ProfileDefinition): ProfileDefinition {
99
+ return p;
100
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Academic profile — weekly curriculum with 7-state status taxonomy.
3
+ *
4
+ * Reference consumer: post_transformers. Schema definition + inferred
5
+ * chapter type live in src/schemas.ts (consolidated to keep all Zod-using
6
+ * code in one file — see schemas.ts header for the DTS-bundler rationale).
7
+ * This module composes the schema with routes + styles via defineProfile.
8
+ */
9
+ import { defineProfile } from '../profile-kit.js';
10
+ import { academicChapterSchema } from '../schemas.js';
11
+ import { academicChaptersRenderer } from './renderers/academic-chapters.js';
12
+
13
+ // Re-export for consumer ergonomics (`import { AcademicChapter } from '@brandon_m_behring/book-scaffold-astro'`).
14
+ export type { AcademicChapter } from '../schemas.js';
15
+
16
+ export const academicProfile = defineProfile({
17
+ name: 'academic',
18
+ schema: academicChapterSchema,
19
+ routes: {
20
+ references: true,
21
+ search: true,
22
+ print: true,
23
+ chapters: false, // academic consumers ship their own week-based /chapters listing
24
+ convergence: false, // tools-profile-specific
25
+ frontmatter: false, // opt-in per book; see #7
26
+ },
27
+ styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
28
+ katex: true,
29
+ chaptersRenderer: academicChaptersRenderer, // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
30
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Course-notes profile — chapters derived from a video course / MOOC / book.
3
+ *
4
+ * Reference consumer (forthcoming): DLAI knowledge-graphs-rag pilot. Schema
5
+ * + inferred type live in src/schemas.ts; this module composes with routes
6
+ * + styles. Multi-book corpus pattern is supported by consumer-side schema
7
+ * extension via Zod .extend() with a `book` discriminator.
8
+ *
9
+ * Distinct from the tools profile (which has tools_compared as an enum of
10
+ * AI CLIs) and academic profile (which is week-based). Don't reuse either.
11
+ */
12
+ import { defineProfile } from '../profile-kit.js';
13
+ import { courseNotesChapterSchema } from '../schemas.js';
14
+ import { fallbackChaptersRenderer } from './renderers/fallback-chapters.js';
15
+
16
+ export type { CourseNotesChapter } from '../schemas.js';
17
+
18
+ export const courseNotesProfile = defineProfile({
19
+ name: 'course-notes',
20
+ schema: courseNotesChapterSchema,
21
+ routes: {
22
+ references: true,
23
+ search: true,
24
+ print: true,
25
+ chapters: false, // multi-book consumers route via [book]/[slug] themselves
26
+ convergence: false,
27
+ frontmatter: false, // opt-in per book; see #7
28
+ },
29
+ styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
30
+ // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
31
+ chaptersRenderer: fallbackChaptersRenderer,
32
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Profile registry — single source of truth for the BOOK_PROFILE enum.
3
+ *
4
+ * Each profile is a self-contained module that declares its schema + routes
5
+ * + styles via defineProfile(). The PROFILES object below is the only place
6
+ * the toolkit looks up profile config; bookScaffoldIntegration consumes
7
+ * PROFILES[profile] to drive injectRoute / injectScript calls.
8
+ *
9
+ * To add a new profile: create src/profiles/<name>.ts, then add two lines
10
+ * here (one to PROFILES, one to ChapterFor). The discriminated union of
11
+ * BookProfile = keyof typeof PROFILES updates automatically.
12
+ */
13
+ import { academicProfile, type AcademicChapter } from './academic.js';
14
+ import { toolsProfile, type ToolsChapter } from './tools.js';
15
+ import { minimalProfile, type MinimalChapter } from './minimal.js';
16
+ import { courseNotesProfile, type CourseNotesChapter } from './course-notes.js';
17
+ import { researchPortfolioProfile, type ResearchPortfolioChapter } from './research-portfolio.js';
18
+
19
+ export const PROFILES = {
20
+ academic: academicProfile,
21
+ tools: toolsProfile,
22
+ minimal: minimalProfile,
23
+ 'course-notes': courseNotesProfile,
24
+ 'research-portfolio': researchPortfolioProfile,
25
+ } as const;
26
+
27
+ export type BookProfile = keyof typeof PROFILES;
28
+
29
+ export const BOOK_PROFILES = Object.keys(PROFILES) as readonly BookProfile[];
30
+
31
+ /**
32
+ * Generic chapter-shape lookup. Consumers writing helpers parametrized over
33
+ * a profile can use this for narrowed typing:
34
+ *
35
+ * function summarize<P extends BookProfile>(profile: P, chapter: ChapterFor<P>) {
36
+ * // chapter is narrowed to AcademicChapter / ToolsChapter / etc.
37
+ * }
38
+ */
39
+ export type ChapterFor<P extends BookProfile> =
40
+ P extends 'academic' ? AcademicChapter :
41
+ P extends 'tools' ? ToolsChapter :
42
+ P extends 'minimal' ? MinimalChapter :
43
+ P extends 'course-notes' ? CourseNotesChapter :
44
+ P extends 'research-portfolio' ? ResearchPortfolioChapter :
45
+ never;
46
+
47
+ // Re-export the inferred chapter types for consumer ergonomics.
48
+ export type {
49
+ AcademicChapter,
50
+ ToolsChapter,
51
+ MinimalChapter,
52
+ CourseNotesChapter,
53
+ ResearchPortfolioChapter,
54
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Minimal profile — single-author essays / manifestos. Currently aliases
3
+ * the tools chapter schema (defined in src/schemas.ts as
4
+ * minimalChapterSchema). If minimal-specific fields emerge from a real
5
+ * consumer, this is where they land.
6
+ */
7
+ import { defineProfile } from '../profile-kit.js';
8
+ import { minimalChapterSchema } from '../schemas.js';
9
+ import { fallbackChaptersRenderer } from './renderers/fallback-chapters.js';
10
+
11
+ export type { MinimalChapter } from '../schemas.js';
12
+
13
+ export const minimalProfile = defineProfile({
14
+ name: 'minimal',
15
+ schema: minimalChapterSchema,
16
+ routes: {
17
+ references: true,
18
+ search: true,
19
+ print: true,
20
+ chapters: false,
21
+ convergence: false,
22
+ frontmatter: false, // opt-in per book; see #7
23
+ },
24
+ styles: ['tokens.css', 'layout.css', 'callouts.css', 'chapter.css', 'typography.css', 'print.css'],
25
+ // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
26
+ chaptersRenderer: fallbackChaptersRenderer,
27
+ });