@brandon_m_behring/book-scaffold-astro 4.25.3 → 4.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ChapterNav.astro +23 -6
- package/components/EvidenceTag.astro +6 -3
- package/components/NavContent.astro +286 -0
- package/components/Provenance.astro +2 -2
- package/components/Sidebar.astro +16 -210
- package/dist/index.d.ts +66 -6
- package/dist/index.mjs +87 -5
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +14 -2
- package/dist/{types-Bn7rKhgP.d.ts → types-Hue-uSeQ.d.ts} +66 -0
- package/layouts/Base.astro +118 -1
- package/package.json +2 -1
- package/recipes/08-decisions-ledger.md +10 -0
- package/recipes/22-responsive-nav-and-multibook-routing.md +92 -0
- package/src/lib/chapters.ts +22 -9
- package/src/lib/define-style.ts +2 -0
- package/src/lib/nav-href.ts +100 -0
- package/src/profiles/renderers/fallback-chapters.ts +3 -3
- package/src/profiles/renderers/research-portfolio-chapters.ts +26 -0
- package/src/profiles/research-portfolio.ts +4 -3
- package/styles/callouts.css +10 -6
- package/styles/layout.css +94 -3
- package/styles/section-map.css +5 -1
- package/styles/tokens.css +17 -13
- package/styles/typography.css +16 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration, MarkdownHeading } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer, l as Style } from './types-
|
|
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-
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer, l as Style } from './types-Hue-uSeQ.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-Hue-uSeQ.js';
|
|
4
4
|
import { E as volatilityLevels, c as academicParts, Q as Question } from './schemas-CKipJ5Ie.js';
|
|
5
5
|
export { A as AcademicChapter, B as BloomLevel, C as CourseNotesChapter, G as GlossaryTerm, M as MinimalChapter, P as Provenance, a as QuestionType, R as ResearchPortfolioChapter, T as ToolsChapter, b as academicChapterSchema, d as bloomLevels, e as changeKinds, f as changelogSchema, g as chapterStatus, h as citationBackstops, i as courseNotesChapterSchema, j as glossarySchema, l as layoutModes, m as minimalChapterSchema, p as patternCategories, k as patternsSchema, n as provenanceObject, o as provenanceSchema, q as questionDifficulties, r as questionSchema, s as questionTypes, t as refineQuestion, u as refinedQuestionSchema, v as researchPortfolioChapterSchema, w as sourceTiers, x as sourceTiersResearch, y as sourcesSchema, z as toolSlugs, D as toolsChapterSchema } from './schemas-CKipJ5Ie.js';
|
|
6
6
|
export { KIND_LABEL, ResolvedTheoremLabel, THEOREM_KINDS, TheoremKind, TheoremLabelProps, resolveTheoremNumber, theoremLabel } from './lib/theorem-label.js';
|
|
@@ -144,18 +144,31 @@ declare const academicChaptersRenderer: ChaptersRenderer;
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* src/profiles/renderers/fallback-chapters.ts — ChaptersRenderer used by
|
|
147
|
-
* profiles that don't ship a dedicated renderer (minimal, course-notes
|
|
148
|
-
*
|
|
147
|
+
* profiles that don't ship a dedicated renderer (minimal, course-notes).
|
|
148
|
+
* Dispatches by field presence — exactly the v3.5.2
|
|
149
149
|
* logic that lived inline in pages/chapters.astro before #35.
|
|
150
150
|
*
|
|
151
151
|
* Safety net for shapes we haven't designed for explicitly. If a consumer
|
|
152
|
-
* opts a course-notes
|
|
152
|
+
* opts a course-notes book into `routes.chapters: true`,
|
|
153
153
|
* the fallback renders reasonably without crashing. Custom output for those
|
|
154
154
|
* profiles is a v4+ extension point (consumer-overridable renderer).
|
|
155
155
|
*/
|
|
156
156
|
|
|
157
157
|
declare const fallbackChaptersRenderer: ChaptersRenderer;
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Research-portfolio /chapters renderer.
|
|
161
|
+
*
|
|
162
|
+
* The portfolio schema intentionally permits numeric Parts through 20. The
|
|
163
|
+
* generic fallback renderer inherits the tools profile's historical
|
|
164
|
+
* `part >= 6` appendix convention, which mislabels a portfolio's sixth Part
|
|
165
|
+
* as "Appendices" and its chapters as Appendix a/b/.... Keep the fallback's
|
|
166
|
+
* field-shape dispatch for every other affordance, but make numeric Parts
|
|
167
|
+
* ordinary numbered Parts throughout the portfolio schema's full range.
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
declare const researchPortfolioChaptersRenderer: ChaptersRenderer;
|
|
171
|
+
|
|
159
172
|
/**
|
|
160
173
|
* src/lib/academic-parts.ts — single source of truth for academic-profile
|
|
161
174
|
* part labels (#95).
|
|
@@ -294,6 +307,53 @@ declare function assertEnumProp<T extends string>(value: unknown, allowed: reado
|
|
|
294
307
|
*/
|
|
295
308
|
declare function resolveBookHref(siblingBooks: Record<string, string> | null | undefined, book: string, to: string): string;
|
|
296
309
|
|
|
310
|
+
/**
|
|
311
|
+
* src/lib/nav-href.ts — pure route-href resolver (#80 multi-book navigation).
|
|
312
|
+
*
|
|
313
|
+
* No `astro:content` import (mirrors chapter-sort.ts) so tsup can include it in
|
|
314
|
+
* the DTS bundle without dragging Astro virtual modules into the build graph.
|
|
315
|
+
* The nav components (Sidebar, ChapterNav, NavContent, …) call these helpers
|
|
316
|
+
* instead of hardcoding the single-book `/chapters/<id>/` URL shape — so ONE set
|
|
317
|
+
* of components serves both a single-book site (the default pattern) and a
|
|
318
|
+
* multi-book consumer whose chapters render at `/<book>/<slug>/`.
|
|
319
|
+
*
|
|
320
|
+
* Patterns are base-relative TOKEN STRINGS (a resolver *function* could not
|
|
321
|
+
* survive the book-config virtual module's `JSON.stringify`, so the config
|
|
322
|
+
* surface is a declarative string resolved here):
|
|
323
|
+
* :id → entry.id verbatim, slashes preserved e.g. 'kg/01-intro'
|
|
324
|
+
* :book → the entry's book name (see `bookField`), or '' e.g. 'kg'
|
|
325
|
+
* :slug → entry.id with a leading '<book>/' stripped e.g. '01-intro'
|
|
326
|
+
* BASE_URL is applied here, so patterns are written base-relative (lead '/').
|
|
327
|
+
*
|
|
328
|
+
* Defaults reproduce the single-book behavior BYTE-FOR-BYTE:
|
|
329
|
+
* chapterRoute = '/chapters/:id/' → `${base}chapters/${id}/`
|
|
330
|
+
* bookField = 'book' (academic/tools schemas have no `book` → bookOf
|
|
331
|
+
* returns null → "show all chapters", today's nav)
|
|
332
|
+
* apparatusRoute = '/:route/' → `${base}<route>/`
|
|
333
|
+
*/
|
|
334
|
+
/** Minimal shape the resolver needs from a chapter collection entry. */
|
|
335
|
+
interface ChapterLike {
|
|
336
|
+
id: string;
|
|
337
|
+
data: Record<string, unknown>;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The entry's book name from `data[bookField]`, or `null` when absent/blank.
|
|
341
|
+
* Single-book schemas (academic/tools/minimal) have no such field → `null`,
|
|
342
|
+
* which callers read as "this is the only book — show every chapter".
|
|
343
|
+
*/
|
|
344
|
+
declare function bookOf(entry: ChapterLike, bookField?: string): string | null;
|
|
345
|
+
/** `entry.id` with a leading `'<book>/'` stripped (multi-book) or unchanged. */
|
|
346
|
+
declare function slugOf(entry: ChapterLike, bookField?: string): string;
|
|
347
|
+
/** Resolve a chapter entry to a base-prefixed href via the `chapterRoute` pattern. */
|
|
348
|
+
declare function chapterHref(entry: ChapterLike, pattern?: string, baseUrl?: string, bookField?: string): string;
|
|
349
|
+
/**
|
|
350
|
+
* Resolve a per-book apparatus route (glossary / practice-exam / flashcards /
|
|
351
|
+
* answers) to a base-prefixed href via the `apparatusRoute` pattern.
|
|
352
|
+
*/
|
|
353
|
+
declare function apparatusHref(route: string, book: string | null, pattern?: string, baseUrl?: string): string;
|
|
354
|
+
/** Whether `entry` is the page at `currentPath` (trailing-slash tolerant). */
|
|
355
|
+
declare function isCurrentChapter(entry: ChapterLike, currentPath: string, pattern?: string, baseUrl?: string, bookField?: string): boolean;
|
|
356
|
+
|
|
297
357
|
/**
|
|
298
358
|
* exam-domains — validate a question's `domain` against the consumer's closed
|
|
299
359
|
* `examDomains` taxonomy (Tier 3, #112).
|
|
@@ -551,4 +611,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
551
611
|
*/
|
|
552
612
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
553
613
|
|
|
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 };
|
|
614
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, type ChapterLike, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, type PartReviewGroup, type PartReviewSelection, Question, type ReviewChapter, type ReviewExercise, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VisibleHeading, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, apparatusHref, assertEnumProp, assertKnownDomain, bookOf, bookScaffoldIntegration, buildGithubUrl, chapterHref, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, isCurrentChapter, minimalStyle, originUrlFromGitConfig, parseRepoSlug, pickActive, researchPortfolioChaptersRenderer, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, selectPartExercises, slugOf, sortQuestions, tocHeadings, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -851,6 +851,17 @@ var courseNotesProfile = defineProfile({
|
|
|
851
851
|
sitemapFilter: (page) => !page.includes("/print/")
|
|
852
852
|
});
|
|
853
853
|
|
|
854
|
+
// src/profiles/renderers/research-portfolio-chapters.ts
|
|
855
|
+
var researchPortfolioChaptersRenderer = {
|
|
856
|
+
...fallbackChaptersRenderer,
|
|
857
|
+
formatPartLabel(part) {
|
|
858
|
+
return typeof part === "number" ? `Part ${part}` : fallbackChaptersRenderer.formatPartLabel(part);
|
|
859
|
+
},
|
|
860
|
+
isAppendix(_part) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
854
865
|
// src/profiles/research-portfolio.ts
|
|
855
866
|
var researchPortfolioProfile = defineProfile({
|
|
856
867
|
name: "research-portfolio",
|
|
@@ -883,8 +894,9 @@ var researchPortfolioProfile = defineProfile({
|
|
|
883
894
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
|
|
884
895
|
katex: true,
|
|
885
896
|
// math is common in research content
|
|
886
|
-
//
|
|
887
|
-
|
|
897
|
+
// Portfolio schema is a union of academic + tools shapes, but unlike the
|
|
898
|
+
// tools profile its numeric Parts remain numbered through the schema max 20.
|
|
899
|
+
chaptersRenderer: researchPortfolioChaptersRenderer
|
|
888
900
|
});
|
|
889
901
|
|
|
890
902
|
// src/profiles/index.ts
|
|
@@ -1199,6 +1211,7 @@ function bookScaffoldIntegration(opts) {
|
|
|
1199
1211
|
// v4.5.0: landing-page data, propagated via virtual module to /index.astro.
|
|
1200
1212
|
title,
|
|
1201
1213
|
subtitle,
|
|
1214
|
+
releaseStatus,
|
|
1202
1215
|
description,
|
|
1203
1216
|
portfolio,
|
|
1204
1217
|
// v4.6.0: book-level author + SEO config, propagated through the
|
|
@@ -1211,7 +1224,12 @@ function bookScaffoldIntegration(opts) {
|
|
|
1211
1224
|
// v4.16.0 (#96): sibling-book registry for cross-book <BookLink>.
|
|
1212
1225
|
siblingBooks,
|
|
1213
1226
|
// v4.17.0 (#112): exam-domain taxonomy for the questions collection.
|
|
1214
|
-
examDomains
|
|
1227
|
+
examDomains,
|
|
1228
|
+
// v4.26.0 (#80): book-aware nav route patterns.
|
|
1229
|
+
chapterRoute,
|
|
1230
|
+
bookField,
|
|
1231
|
+
apparatusRoute,
|
|
1232
|
+
apparatusRoutes
|
|
1215
1233
|
} = opts;
|
|
1216
1234
|
const def = PROFILES[profile];
|
|
1217
1235
|
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
@@ -1263,6 +1281,7 @@ function bookScaffoldIntegration(opts) {
|
|
|
1263
1281
|
makeBookConfigVitePlugin({
|
|
1264
1282
|
title: title ?? null,
|
|
1265
1283
|
subtitle: subtitle ?? null,
|
|
1284
|
+
releaseStatus: releaseStatus ?? null,
|
|
1266
1285
|
description: description ?? null,
|
|
1267
1286
|
portfolio: portfolio ?? false,
|
|
1268
1287
|
enabledRoutes: enabledRouteNames,
|
|
@@ -1274,7 +1293,13 @@ function bookScaffoldIntegration(opts) {
|
|
|
1274
1293
|
githubRepo: resolvedGithubRepo,
|
|
1275
1294
|
githubBranch: resolvedGithubBranch,
|
|
1276
1295
|
siblingBooks: siblingBooks ?? {},
|
|
1277
|
-
examDomains: examDomains ?? []
|
|
1296
|
+
examDomains: examDomains ?? [],
|
|
1297
|
+
// v4.26.0 (#80): book-aware nav route patterns; defaults
|
|
1298
|
+
// reproduce the single-book `/chapters/<id>/` behavior exactly.
|
|
1299
|
+
chapterRoute: chapterRoute ?? "/chapters/:id/",
|
|
1300
|
+
bookField: bookField ?? "book",
|
|
1301
|
+
apparatusRoute: apparatusRoute ?? "/:route/",
|
|
1302
|
+
apparatusRoutes: apparatusRoutes ?? []
|
|
1278
1303
|
})
|
|
1279
1304
|
],
|
|
1280
1305
|
define: {
|
|
@@ -1402,6 +1427,8 @@ async function defineBookConfig(opts) {
|
|
|
1402
1427
|
title: opts.title,
|
|
1403
1428
|
// v4.23.0 (#135): sidebar brand subtitle.
|
|
1404
1429
|
subtitle: opts.subtitle,
|
|
1430
|
+
// v4.27.0 (#149): release-state banner (top-level beats the style chain).
|
|
1431
|
+
releaseStatus: opts.releaseStatus ?? composed.releaseStatus,
|
|
1405
1432
|
description: opts.description,
|
|
1406
1433
|
portfolio: resolvedPortfolio,
|
|
1407
1434
|
// v4.6.0: book-level author + SEO config (ogImage, twitterHandle),
|
|
@@ -1416,7 +1443,12 @@ async function defineBookConfig(opts) {
|
|
|
1416
1443
|
// v4.16.0 (#96): cross-book link registry.
|
|
1417
1444
|
siblingBooks: opts.siblingBooks,
|
|
1418
1445
|
// v4.17.0 (#112): per-book exam-domain taxonomy for the questions collection.
|
|
1419
|
-
examDomains: opts.examDomains
|
|
1446
|
+
examDomains: opts.examDomains,
|
|
1447
|
+
// v4.26.0 (#80): book-aware nav route patterns (undefined → single-book defaults).
|
|
1448
|
+
chapterRoute: opts.chapterRoute,
|
|
1449
|
+
bookField: opts.bookField,
|
|
1450
|
+
apparatusRoute: opts.apparatusRoute,
|
|
1451
|
+
apparatusRoutes: opts.apparatusRoutes
|
|
1420
1452
|
}),
|
|
1421
1453
|
...mergedExtraIntegrations
|
|
1422
1454
|
];
|
|
@@ -1454,6 +1486,8 @@ async function defineBookConfig(opts) {
|
|
|
1454
1486
|
// v4.5.0: strip new landing-related opts so they don't leak into AstroUserConfig.
|
|
1455
1487
|
title: _title,
|
|
1456
1488
|
subtitle: _subtitle,
|
|
1489
|
+
// v4.27.0 (#149): strip the release-state banner config.
|
|
1490
|
+
releaseStatus: _releaseStatus,
|
|
1457
1491
|
description: _description,
|
|
1458
1492
|
portfolio: _portfolio,
|
|
1459
1493
|
// v4.6.0: strip new book-level SEO opts (author + seo block).
|
|
@@ -1466,6 +1500,11 @@ async function defineBookConfig(opts) {
|
|
|
1466
1500
|
siblingBooks: _siblingBooks,
|
|
1467
1501
|
// v4.17.0: strip exam-domain taxonomy.
|
|
1468
1502
|
examDomains: _examDomains,
|
|
1503
|
+
// v4.26.0 (#80): strip book-aware nav route patterns.
|
|
1504
|
+
chapterRoute: _chapterRoute,
|
|
1505
|
+
bookField: _bookField,
|
|
1506
|
+
apparatusRoute: _apparatusRoute,
|
|
1507
|
+
apparatusRoutes: _apparatusRoutes,
|
|
1469
1508
|
...rest
|
|
1470
1509
|
} = opts;
|
|
1471
1510
|
void _styles;
|
|
@@ -1479,6 +1518,7 @@ async function defineBookConfig(opts) {
|
|
|
1479
1518
|
void _katexMacros;
|
|
1480
1519
|
void _title;
|
|
1481
1520
|
void _subtitle;
|
|
1521
|
+
void _releaseStatus;
|
|
1482
1522
|
void _description;
|
|
1483
1523
|
void _portfolio;
|
|
1484
1524
|
void _author;
|
|
@@ -1487,6 +1527,10 @@ async function defineBookConfig(opts) {
|
|
|
1487
1527
|
void _githubBranch;
|
|
1488
1528
|
void _siblingBooks;
|
|
1489
1529
|
void _examDomains;
|
|
1530
|
+
void _chapterRoute;
|
|
1531
|
+
void _bookField;
|
|
1532
|
+
void _apparatusRoute;
|
|
1533
|
+
void _apparatusRoutes;
|
|
1490
1534
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
1491
1535
|
const restVite = rest.vite ?? {};
|
|
1492
1536
|
const restSsr = restVite.ssr ?? {};
|
|
@@ -1592,6 +1636,38 @@ function resolveBookHref(siblingBooks, book, to) {
|
|
|
1592
1636
|
return `${base.replace(/\/+$/, "")}/${to.replace(/^\/+/, "")}`;
|
|
1593
1637
|
}
|
|
1594
1638
|
|
|
1639
|
+
// src/lib/nav-href.ts
|
|
1640
|
+
function normBase(baseUrl) {
|
|
1641
|
+
return (baseUrl || "/").replace(/\/*$/, "/");
|
|
1642
|
+
}
|
|
1643
|
+
function fillTokens(pattern, tokens) {
|
|
1644
|
+
return pattern.replace(/:(book|slug|route|id)\b/g, (_m, k) => tokens[k] ?? "");
|
|
1645
|
+
}
|
|
1646
|
+
function bookOf(entry, bookField = "book") {
|
|
1647
|
+
const v = entry.data[bookField];
|
|
1648
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
1649
|
+
}
|
|
1650
|
+
function slugOf(entry, bookField = "book") {
|
|
1651
|
+
const book = bookOf(entry, bookField);
|
|
1652
|
+
return book && entry.id.startsWith(`${book}/`) ? entry.id.slice(book.length + 1) : entry.id;
|
|
1653
|
+
}
|
|
1654
|
+
function chapterHref(entry, pattern = "/chapters/:id/", baseUrl = "/", bookField = "book") {
|
|
1655
|
+
const path = fillTokens(pattern, {
|
|
1656
|
+
id: entry.id,
|
|
1657
|
+
book: bookOf(entry, bookField) ?? "",
|
|
1658
|
+
slug: slugOf(entry, bookField)
|
|
1659
|
+
}).replace(/\/{2,}/g, "/").replace(/^\//, "");
|
|
1660
|
+
return normBase(baseUrl) + path;
|
|
1661
|
+
}
|
|
1662
|
+
function apparatusHref(route, book, pattern = "/:route/", baseUrl = "/") {
|
|
1663
|
+
const path = fillTokens(pattern, { route, book: book ?? "" }).replace(/\/{2,}/g, "/").replace(/^\//, "");
|
|
1664
|
+
return normBase(baseUrl) + path;
|
|
1665
|
+
}
|
|
1666
|
+
function isCurrentChapter(entry, currentPath, pattern = "/chapters/:id/", baseUrl = "/", bookField = "book") {
|
|
1667
|
+
const href = chapterHref(entry, pattern, baseUrl, bookField);
|
|
1668
|
+
return currentPath === href || currentPath === href.replace(/\/$/, "");
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1595
1671
|
// src/lib/exam-domains.ts
|
|
1596
1672
|
function assertKnownDomain(examDomains, domain, ctx) {
|
|
1597
1673
|
if (!examDomains || !examDomains.includes(domain)) {
|
|
@@ -1845,15 +1921,18 @@ export {
|
|
|
1845
1921
|
academicPartOrdinal,
|
|
1846
1922
|
academicParts,
|
|
1847
1923
|
academicStyle,
|
|
1924
|
+
apparatusHref,
|
|
1848
1925
|
assertEnumProp,
|
|
1849
1926
|
assertKnownDomain,
|
|
1850
1927
|
bloomLevels,
|
|
1928
|
+
bookOf,
|
|
1851
1929
|
bookScaffoldIntegration,
|
|
1852
1930
|
buildExamManifest,
|
|
1853
1931
|
buildFlashcardDeck,
|
|
1854
1932
|
buildGithubUrl,
|
|
1855
1933
|
changeKinds,
|
|
1856
1934
|
changelogSchema,
|
|
1935
|
+
chapterHref,
|
|
1857
1936
|
chapterLabel,
|
|
1858
1937
|
chapterSortKey,
|
|
1859
1938
|
chapterStatus,
|
|
@@ -1875,6 +1954,7 @@ export {
|
|
|
1875
1954
|
glossarySchema,
|
|
1876
1955
|
groupByChapter,
|
|
1877
1956
|
groupByDomain,
|
|
1957
|
+
isCurrentChapter,
|
|
1878
1958
|
layoutModes,
|
|
1879
1959
|
minimalChapterSchema,
|
|
1880
1960
|
minimalStyle,
|
|
@@ -1892,6 +1972,7 @@ export {
|
|
|
1892
1972
|
refineQuestion,
|
|
1893
1973
|
refinedQuestionSchema,
|
|
1894
1974
|
researchPortfolioChapterSchema,
|
|
1975
|
+
researchPortfolioChaptersRenderer,
|
|
1895
1976
|
researchPortfolioStyle,
|
|
1896
1977
|
resolveBookHref,
|
|
1897
1978
|
resolveGithubRepo,
|
|
@@ -1902,6 +1983,7 @@ export {
|
|
|
1902
1983
|
scoreExam,
|
|
1903
1984
|
selectPartExercises,
|
|
1904
1985
|
shuffle,
|
|
1986
|
+
slugOf,
|
|
1905
1987
|
sortQuestions,
|
|
1906
1988
|
sourceTiers,
|
|
1907
1989
|
sourceTiersResearch,
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -729,6 +729,17 @@ var courseNotesProfile = defineProfile({
|
|
|
729
729
|
sitemapFilter: (page) => !page.includes("/print/")
|
|
730
730
|
});
|
|
731
731
|
|
|
732
|
+
// src/profiles/renderers/research-portfolio-chapters.ts
|
|
733
|
+
var researchPortfolioChaptersRenderer = {
|
|
734
|
+
...fallbackChaptersRenderer,
|
|
735
|
+
formatPartLabel(part) {
|
|
736
|
+
return typeof part === "number" ? `Part ${part}` : fallbackChaptersRenderer.formatPartLabel(part);
|
|
737
|
+
},
|
|
738
|
+
isAppendix(_part) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
732
743
|
// src/profiles/research-portfolio.ts
|
|
733
744
|
var researchPortfolioProfile = defineProfile({
|
|
734
745
|
name: "research-portfolio",
|
|
@@ -761,8 +772,9 @@ var researchPortfolioProfile = defineProfile({
|
|
|
761
772
|
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css", "section-map.css"],
|
|
762
773
|
katex: true,
|
|
763
774
|
// math is common in research content
|
|
764
|
-
//
|
|
765
|
-
|
|
775
|
+
// Portfolio schema is a union of academic + tools shapes, but unlike the
|
|
776
|
+
// tools profile its numeric Parts remain numbered through the schema max 20.
|
|
777
|
+
chaptersRenderer: researchPortfolioChaptersRenderer
|
|
766
778
|
});
|
|
767
779
|
|
|
768
780
|
// src/profiles/index.ts
|
|
@@ -379,6 +379,12 @@ interface Style {
|
|
|
379
379
|
* - `'pages'`: Cloudflare Pages (default for research-portfolio/course-notes)
|
|
380
380
|
* Closes #50. */
|
|
381
381
|
readonly deploy?: 'pages' | 'workers';
|
|
382
|
+
/** v4.27.0 (#149): release-state banner; shallow override (last wins). */
|
|
383
|
+
readonly releaseStatus?: {
|
|
384
|
+
state: 'alpha' | 'beta' | 'rc' | 'locked';
|
|
385
|
+
dismissAt?: string;
|
|
386
|
+
message?: string;
|
|
387
|
+
};
|
|
382
388
|
/**
|
|
383
389
|
* Scoped consumer-side metadata. Ignored by the toolkit; survives composition
|
|
384
390
|
* as per-key spread (last wins per key). Use this for workflow data that
|
|
@@ -600,6 +606,17 @@ interface BookConfigOptions {
|
|
|
600
606
|
* hardcoded both strings, so every consumer shipped the placeholder).
|
|
601
607
|
*/
|
|
602
608
|
subtitle?: string;
|
|
609
|
+
/**
|
|
610
|
+
* v4.27.0 (#149): book-level release state. When set, Base.astro renders
|
|
611
|
+
* the existing <PreReleaseBanner> site-wide (top of <body>) with these
|
|
612
|
+
* props — previously the banner was author-import-only, so auto-layout
|
|
613
|
+
* consumers (ssm-foundations) had no way to wire it. Omit for no banner.
|
|
614
|
+
*/
|
|
615
|
+
releaseStatus?: {
|
|
616
|
+
state: 'alpha' | 'beta' | 'rc' | 'locked';
|
|
617
|
+
dismissAt?: string;
|
|
618
|
+
message?: string;
|
|
619
|
+
};
|
|
603
620
|
/**
|
|
604
621
|
* v4.5.0: Book description. Read by the auto-injected `/` landing page (lead paragraph + <meta description>).
|
|
605
622
|
* Optional; landing renders no description paragraph if unset.
|
|
@@ -684,6 +701,39 @@ interface BookConfigOptions {
|
|
|
684
701
|
* @example examDomains: ['secure-network-architecture', 'identity-and-access']
|
|
685
702
|
*/
|
|
686
703
|
examDomains?: readonly string[];
|
|
704
|
+
/**
|
|
705
|
+
* v4.26.0 (#80): URL pattern for a chapter entry, consumed by the book-aware
|
|
706
|
+
* Sidebar / ChapterNav / NavContent so they emit correct links on a multi-book
|
|
707
|
+
* consumer. Base-relative token string — `:id` (entry.id), `:book` (the
|
|
708
|
+
* entry's book, see `bookField`), `:slug` (id minus the leading `<book>/`).
|
|
709
|
+
* Default `'/chapters/:id/'` reproduces the single-book behavior byte-for-byte;
|
|
710
|
+
* a multi-book consumer whose chapters render at `/<book>/<slug>/` (entry.id =
|
|
711
|
+
* `'<book>/<slug>'`) sets `'/:id/'`. Resolved by the pure `chapterHref` helper.
|
|
712
|
+
*/
|
|
713
|
+
chapterRoute?: string;
|
|
714
|
+
/**
|
|
715
|
+
* v4.26.0 (#80): frontmatter field naming a chapter's book (multi-book book
|
|
716
|
+
* scoping). Default `'book'`. Absent on single-book schemas → the sidebar shows
|
|
717
|
+
* every chapter (today's behavior); present → the sidebar filters to the
|
|
718
|
+
* current book and the `:book` / `:slug` route tokens resolve.
|
|
719
|
+
*/
|
|
720
|
+
bookField?: string;
|
|
721
|
+
/**
|
|
722
|
+
* v4.26.0 (#80): URL pattern for a per-book apparatus route (practice-exam /
|
|
723
|
+
* glossary / flashcards / answers) surfaced in the nav. Tokens `:book` +
|
|
724
|
+
* `:route`. Default `'/:route/'` (single-book, flat); a multi-book consumer
|
|
725
|
+
* sets `'/:book/:route/'`. Resolved by the pure `apparatusHref` helper.
|
|
726
|
+
*/
|
|
727
|
+
apparatusRoute?: string;
|
|
728
|
+
/**
|
|
729
|
+
* v4.26.0 (#80): the apparatus route slugs to surface in the nav (sidebar +
|
|
730
|
+
* drawer) — a subset of `practice-exam | glossary | flashcards | answers`.
|
|
731
|
+
* Default `[]` (no apparatus links in nav — existing single-book books render
|
|
732
|
+
* unchanged). A multi-book consumer that owns per-book apparatus routes sets
|
|
733
|
+
* e.g. `['practice-exam','glossary','flashcards','answers']`; each is rendered
|
|
734
|
+
* via `apparatusHref(slug, currentBook, apparatusRoute, base)`.
|
|
735
|
+
*/
|
|
736
|
+
apparatusRoutes?: readonly string[];
|
|
687
737
|
/** Escape hatch for any other AstroUserConfig field. */
|
|
688
738
|
[key: string]: unknown;
|
|
689
739
|
}
|
|
@@ -719,6 +769,13 @@ interface BookScaffoldIntegrationOptions {
|
|
|
719
769
|
/** v4.23.0 (#135): sidebar brand subtitle, propagated via the book-config
|
|
720
770
|
* virtual module to Sidebar.astro. */
|
|
721
771
|
subtitle?: string;
|
|
772
|
+
/** v4.27.0 (#149): release-state banner, propagated via the book-config
|
|
773
|
+
* virtual module; Base.astro renders <PreReleaseBanner> when set. */
|
|
774
|
+
releaseStatus?: {
|
|
775
|
+
state: 'alpha' | 'beta' | 'rc' | 'locked';
|
|
776
|
+
dismissAt?: string;
|
|
777
|
+
message?: string;
|
|
778
|
+
};
|
|
722
779
|
/** v4.5.0: book description, propagated to `/` landing via vite.define. */
|
|
723
780
|
description?: string;
|
|
724
781
|
/**
|
|
@@ -756,6 +813,15 @@ interface BookScaffoldIntegrationOptions {
|
|
|
756
813
|
siblingBooks?: Record<string, string>;
|
|
757
814
|
/** v4.17.0 (#112): closed exam-domain taxonomy for the questions collection. */
|
|
758
815
|
examDomains?: readonly string[];
|
|
816
|
+
/** v4.26.0 (#80): chapter-route token pattern, propagated via the book-config
|
|
817
|
+
* virtual module to the book-aware nav. Default '/chapters/:id/'. */
|
|
818
|
+
chapterRoute?: string;
|
|
819
|
+
/** v4.26.0 (#80): frontmatter field naming a chapter's book. Default 'book'. */
|
|
820
|
+
bookField?: string;
|
|
821
|
+
/** v4.26.0 (#80): apparatus-route token pattern. Default '/:route/'. */
|
|
822
|
+
apparatusRoute?: string;
|
|
823
|
+
/** v4.26.0 (#80): apparatus route slugs to surface in the nav. Default []. */
|
|
824
|
+
apparatusRoutes?: readonly string[];
|
|
759
825
|
}
|
|
760
826
|
/** Raised when the resolved profile is not one of `BOOK_PROFILES`. */
|
|
761
827
|
declare class BookConfigError extends Error {
|
package/layouts/Base.astro
CHANGED
|
@@ -59,6 +59,10 @@ import '../styles/print.css';
|
|
|
59
59
|
import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
|
|
60
60
|
import ToolFilter from '@brandon_m_behring/book-scaffold-astro/components/ToolFilter';
|
|
61
61
|
import Sidebar from '../components/Sidebar.astro';
|
|
62
|
+
// v4.27.0 (#149): site-wide release-state banner, driven by defineBookConfig.
|
|
63
|
+
import PreReleaseBanner from '../components/PreReleaseBanner.astro';
|
|
64
|
+
// v4.26.0 (#80): the mobile/tablet drawer reuses the same book-scoped nav source.
|
|
65
|
+
import NavContent from '../components/NavContent.astro';
|
|
62
66
|
// v4.6.0: SEO meta tags read book-level identity (title fallback,
|
|
63
67
|
// description fallback, ogImage default, twitterHandle) from the
|
|
64
68
|
// book-config virtual module (was landing-config in v4.5.1).
|
|
@@ -115,7 +119,7 @@ const absoluteOgImage = resolvedOgImage
|
|
|
115
119
|
const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
|
|
116
120
|
const ogSiteName = bookConfig.title ?? title;
|
|
117
121
|
const ogDescription = description ?? bookConfig.description ?? '';
|
|
118
|
-
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(
|
|
122
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
|
|
119
123
|
---
|
|
120
124
|
|
|
121
125
|
<!doctype html>
|
|
@@ -163,7 +167,33 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
|
163
167
|
<slot name="head" />
|
|
164
168
|
</head>
|
|
165
169
|
<body>
|
|
170
|
+
{/* v4.27.0 (#149): book-level release banner — first element in <body> so
|
|
171
|
+
every page (chapters, apparatus, landing) declares its state. Renders
|
|
172
|
+
only when defineBookConfig({ releaseStatus }) is set. */}
|
|
173
|
+
{bookConfig.releaseStatus && (
|
|
174
|
+
<PreReleaseBanner
|
|
175
|
+
state={bookConfig.releaseStatus.state}
|
|
176
|
+
dismissAt={bookConfig.releaseStatus.dismissAt}
|
|
177
|
+
message={bookConfig.releaseStatus.message}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
166
180
|
<div class="chrome-buttons">
|
|
181
|
+
{/* v4.26.0 (#80): hamburger → mobile/tablet nav drawer. <a href="#nav-drawer">
|
|
182
|
+
so it opens via :target with no JS; the controller below enhances it. */}
|
|
183
|
+
{showSidebar && (
|
|
184
|
+
<a
|
|
185
|
+
id="nav-toggle"
|
|
186
|
+
class="chrome-button nav-toggle"
|
|
187
|
+
href="#nav-drawer"
|
|
188
|
+
role="button"
|
|
189
|
+
aria-controls="nav-drawer"
|
|
190
|
+
aria-expanded="false"
|
|
191
|
+
aria-haspopup="dialog"
|
|
192
|
+
aria-label="Open chapter navigation"
|
|
193
|
+
>
|
|
194
|
+
<span aria-hidden="true">☰</span>
|
|
195
|
+
</a>
|
|
196
|
+
)}
|
|
167
197
|
{showToolsChrome && <ToolFilter client:idle />}
|
|
168
198
|
{showToolsChrome && <VersionSelector client:idle />}
|
|
169
199
|
<a
|
|
@@ -185,6 +215,20 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
|
185
215
|
<span class="theme-toggle-icon dark-icon" aria-hidden="true">☾</span>
|
|
186
216
|
</button>
|
|
187
217
|
</div>
|
|
218
|
+
{/* v4.26.0 (#80): mobile/tablet nav drawer (sibling of <main>, NOT a <main>).
|
|
219
|
+
CSS-hidden ≥64rem where the Sidebar is the nav. role=dialog + the inline
|
|
220
|
+
controller below provide focus-trap/ESC; `:target` is the no-JS fallback. */}
|
|
221
|
+
{showSidebar && (
|
|
222
|
+
<div id="nav-drawer" class="nav-drawer" data-nav-drawer>
|
|
223
|
+
<a class="nav-drawer-backdrop" href="#" data-nav-close tabindex="-1" aria-hidden="true"></a>
|
|
224
|
+
<div class="nav-drawer-panel" role="dialog" aria-modal="true" aria-label="Chapter navigation" tabindex="-1">
|
|
225
|
+
<a class="nav-drawer-dismiss chrome-button" href="#" data-nav-close aria-label="Close navigation">
|
|
226
|
+
<span aria-hidden="true">✕</span>
|
|
227
|
+
</a>
|
|
228
|
+
<NavContent />
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
188
232
|
{showSidebar ? (
|
|
189
233
|
<div class="layout-with-sidebar">
|
|
190
234
|
<Sidebar />
|
|
@@ -236,6 +280,79 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/?$/, '/');
|
|
|
236
280
|
} catch (e) { /* older browsers: no media-query change events */ }
|
|
237
281
|
})();
|
|
238
282
|
</script>
|
|
283
|
+
{/*
|
|
284
|
+
Nav-drawer controller (v4.26.0, #80). Progressive enhancement over the
|
|
285
|
+
`:target` no-JS baseline: intercepts the hamburger to toggle `.is-open`
|
|
286
|
+
(so no history hash is pushed), traps focus inside the dialog, closes on
|
|
287
|
+
ESC / backdrop / dismiss, restores focus to the opener, and locks body
|
|
288
|
+
scroll. No-op on pages without the drawer (landing: showSidebar=false).
|
|
289
|
+
*/}
|
|
290
|
+
<script is:inline>
|
|
291
|
+
(function () {
|
|
292
|
+
var drawer = document.getElementById('nav-drawer');
|
|
293
|
+
var toggle = document.getElementById('nav-toggle');
|
|
294
|
+
if (!drawer || !toggle) return;
|
|
295
|
+
var panel = drawer.querySelector('.nav-drawer-panel');
|
|
296
|
+
var opener = null;
|
|
297
|
+
function focusables() {
|
|
298
|
+
return Array.prototype.slice
|
|
299
|
+
.call(panel.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'))
|
|
300
|
+
.filter(function (el) { return el.offsetParent !== null; });
|
|
301
|
+
}
|
|
302
|
+
// C1 (#80, codex@medium): count the :target no-JS-fallback state too, so a
|
|
303
|
+
// drawer opened via a '#nav-drawer' hash (with JS active) is still closable by
|
|
304
|
+
// backdrop / dismiss / ESC (close() clears the hash); else it would be stranded.
|
|
305
|
+
function isOpen() { return drawer.classList.contains('is-open') || location.hash === '#nav-drawer'; }
|
|
306
|
+
function open(e) {
|
|
307
|
+
if (e) e.preventDefault();
|
|
308
|
+
opener = document.activeElement;
|
|
309
|
+
drawer.classList.add('is-open');
|
|
310
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
311
|
+
document.documentElement.classList.add('nav-drawer-locked');
|
|
312
|
+
var f = focusables();
|
|
313
|
+
(f[0] || panel).focus();
|
|
314
|
+
}
|
|
315
|
+
function close(e) {
|
|
316
|
+
if (e) e.preventDefault();
|
|
317
|
+
if (!isOpen()) return;
|
|
318
|
+
drawer.classList.remove('is-open');
|
|
319
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
320
|
+
document.documentElement.classList.remove('nav-drawer-locked');
|
|
321
|
+
if (location.hash === '#nav-drawer') {
|
|
322
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
323
|
+
}
|
|
324
|
+
if (opener && typeof opener.focus === 'function') opener.focus();
|
|
325
|
+
}
|
|
326
|
+
toggle.addEventListener('click', function (e) { isOpen() ? close(e) : open(e); });
|
|
327
|
+
drawer.addEventListener('click', function (e) {
|
|
328
|
+
var t = e.target;
|
|
329
|
+
if (t && t.closest && t.closest('[data-nav-close]')) close(e);
|
|
330
|
+
});
|
|
331
|
+
document.addEventListener('keydown', function (e) {
|
|
332
|
+
if (!isOpen()) return;
|
|
333
|
+
if (e.key === 'Escape') { close(e); return; }
|
|
334
|
+
if (e.key === 'Tab') {
|
|
335
|
+
var f = focusables();
|
|
336
|
+
if (!f.length) return;
|
|
337
|
+
var first = f[0], last = f[f.length - 1];
|
|
338
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
|
339
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// F5 (a11y, #80): role="button" must activate on Space; native anchors only
|
|
343
|
+
// fire click on Enter, so bind Space explicitly (preventDefault stops scroll).
|
|
344
|
+
toggle.addEventListener('keydown', function (e) {
|
|
345
|
+
if (e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); isOpen() ? close(e) : open(e); }
|
|
346
|
+
});
|
|
347
|
+
// F1 (#80): if the viewport grows into the desktop range (>=80rem) while the
|
|
348
|
+
// drawer is open, the drawer + toggle + backdrop go display:none — close() to
|
|
349
|
+
// clear .is-open and release the body scroll-lock so the page isn't stranded.
|
|
350
|
+
var mq = window.matchMedia('(min-width: 80rem)');
|
|
351
|
+
var onDesktop = function (ev) { if (ev.matches) close(); };
|
|
352
|
+
if (mq.addEventListener) mq.addEventListener('change', onDesktop);
|
|
353
|
+
else if (mq.addListener) mq.addListener(onDesktop);
|
|
354
|
+
})();
|
|
355
|
+
</script>
|
|
239
356
|
</body>
|
|
240
357
|
</html>
|
|
241
358
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.26.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"./components/KeyIdea.astro": "./components/KeyIdea.astro",
|
|
70
70
|
"./components/MarginFigure.astro": "./components/MarginFigure.astro",
|
|
71
71
|
"./components/MarginNote.astro": "./components/MarginNote.astro",
|
|
72
|
+
"./components/NavContent.astro": "./components/NavContent.astro",
|
|
72
73
|
"./components/Newthought.astro": "./components/Newthought.astro",
|
|
73
74
|
"./components/NoteBox.astro": "./components/NoteBox.astro",
|
|
74
75
|
"./components/ObjectiveMap.astro": "./components/ObjectiveMap.astro",
|
|
@@ -122,6 +122,16 @@ Until v3.0 triggers, accept that bug fixes don't auto-flow to existing books. Bo
|
|
|
122
122
|
**Reasoning**: #82 itself was a coverage gap — don't trade coverage for speed.
|
|
123
123
|
**Deviate when**: Never delete a visual gate without equivalent-or-better coverage in the replacement.
|
|
124
124
|
|
|
125
|
+
### D21 — Book-aware nav config is declarative token strings, not resolver functions (#80, v4.26.0)
|
|
126
|
+
**Decision**: `defineBookConfig`'s nav-route fields (`chapterRoute` / `bookField` / `apparatusRoute` / `apparatusRoutes`) are declarative TOKEN STRINGS (`'/:id/'`, `':book'`, `':slug'`, `':route'`) resolved by a pure `nav-href` lib — NOT a `chapterHref(entry) => string` callback.
|
|
127
|
+
**Reasoning**: the book-config virtual module serializes via `JSON.stringify` (`integration.ts`); a function serializes to `undefined`. Token strings cross the boundary intact and cover every known multi-book route shape. A consumer needing real logic already owns its route components (recipe 18) and can import `chapterHref` directly.
|
|
128
|
+
**Deviate when**: a consumer's URL scheme can't be expressed with `:id`/`:book`/`:slug` token substitution — then they own the route components, no worse than pre-4.26.
|
|
129
|
+
|
|
130
|
+
### D22 — Mobile nav is an inline controller + the 3-column gutter FITS (not suppressed) (#80, v4.26.0)
|
|
131
|
+
**Decision**: (a) the sub-1280px nav drawer is an inline vanilla `<script>` controller over a `:target` no-JS baseline, NOT a Preact island. (b) The right-gutter overflow (the Tufte `.section-map`/sidenote float overflowing beside the sidebar at 1024/1440px) is fixed by making it FIT — trim the shared measures (`--measure-main` 60/66/78ch, `--measure-side` 20/20/24ch) + the sidebar (14/16rem), and activate the full 3-column (sidebar + floated gutter) at **≥80rem (1280px)** instead of 64rem (below that: drawer + full-width content + ChapterTOC). NOT suppressing/hiding the scrollspy.
|
|
132
|
+
**Reasoning**: the drawer is event-wiring (open/close/focus-trap), not Preact rendering — the inline script is instant (no hydration flash), zero `.tsx` compile risk, and matches the existing theme-toggle precedent. The fit-not-suppress gutter keeps the Tufte scrollspy at true-desktop widths (it's the design's point); narrower measures are also closer to the ideal reading column. Verified `scrollWidth == clientWidth` across the consumer's responsive audit harness (390–1440px).
|
|
133
|
+
**Deviate when**: a consumer wants the gutter scrollspy at 1024–1280px — they'd need an even narrower measure or a different sidebar treatment at that band; document the trade-off.
|
|
134
|
+
|
|
125
135
|
## How to use this ledger
|
|
126
136
|
|
|
127
137
|
When a future change in the scaffold contradicts a decision above, update this ledger first. Don't change behavior silently — the ledger is the durable record of "why this is shaped like it is."
|