@brandon_m_behring/book-scaffold-astro 4.26.2 → 4.27.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.
Files changed (49) hide show
  1. package/AGENTS.md +6 -0
  2. package/CLAUDE.md +35 -10
  3. package/LATEX_TO_MDX_MAPPING.md +1 -1
  4. package/LICENSE +27 -0
  5. package/LICENSE-CONTENT +19 -0
  6. package/README.md +33 -0
  7. package/components/AssessmentTest.astro +2 -1
  8. package/components/ChapterNav.astro +2 -2
  9. package/components/Cite.astro +2 -1
  10. package/components/NavContent.astro +2 -2
  11. package/components/PartReview.astro +2 -1
  12. package/components/Rationale.astro +3 -2
  13. package/components/Sidebar.astro +2 -1
  14. package/components/Term.astro +2 -1
  15. package/components/TipsCard.astro +2 -1
  16. package/components/VersionSelector.tsx +39 -36
  17. package/components/WeekRef.astro +2 -1
  18. package/components/XRef.astro +2 -1
  19. package/dist/components/VersionSelector.d.ts +16 -2
  20. package/dist/components/VersionSelector.mjs +18 -17
  21. package/dist/index.d.ts +24 -6
  22. package/dist/index.mjs +96 -8
  23. package/dist/schemas.d.ts +1 -1
  24. package/dist/schemas.mjs +8 -1
  25. package/dist/{types-Hue-uSeQ.d.ts → types-CZrkqzpC.d.ts} +84 -48
  26. package/layouts/Base.astro +23 -21
  27. package/package.json +7 -4
  28. package/pages/answers.astro +2 -1
  29. package/pages/chapters.astro +2 -1
  30. package/pages/exercises.astro +2 -1
  31. package/pages/index.astro +4 -3
  32. package/pages/search.astro +2 -1
  33. package/pages/tips.astro +2 -1
  34. package/recipes/01-add-math.md +40 -28
  35. package/recipes/04-component-library.md +14 -4
  36. package/recipes/05-deploy-cloudflare.md +44 -0
  37. package/recipes/06-mobile-first-layout.md +25 -2
  38. package/recipes/09-validation.md +18 -9
  39. package/recipes/15-defining-styles.md +7 -3
  40. package/recipes/19-prevalidate-hook.md +29 -83
  41. package/scripts/build-bib.mjs +7 -3
  42. package/scripts/build-labels.mjs +33 -16
  43. package/scripts/read-env.mjs +25 -0
  44. package/scripts/resolve-book-config.mjs +96 -0
  45. package/scripts/validate.mjs +125 -84
  46. package/src/lib/define-style.ts +25 -8
  47. package/src/lib/nav-href.ts +24 -2
  48. package/styles/layout.css +14 -8
  49. package/styles/tokens.css +8 -10
package/dist/index.mjs CHANGED
@@ -912,6 +912,7 @@ var BOOK_PROFILES = Object.keys(PROFILES);
912
912
  // src/types.ts
913
913
  import { existsSync, readFileSync } from "fs";
914
914
  var BOOK_PRESETS = BOOK_PROFILES;
915
+ var NUMBER_STYLES = ["shared", "per-kind"];
915
916
  var BookConfigError = class extends Error {
916
917
  constructor(message) {
917
918
  super(message);
@@ -936,6 +937,7 @@ function readEnvFile(path = ".env") {
936
937
  return {};
937
938
  }
938
939
  }
940
+ var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
939
941
  function resolvePreset(explicitPreset, explicitProfile) {
940
942
  let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
941
943
  let source = "default";
@@ -956,7 +958,13 @@ function resolvePreset(explicitPreset, explicitProfile) {
956
958
  );
957
959
  }
958
960
  if (source === "default") {
959
- console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
961
+ const warningState = globalThis;
962
+ if (!warningState[PRESET_FALLBACK_WARNING]) {
963
+ warningState[PRESET_FALLBACK_WARNING] = true;
964
+ console.warn(
965
+ "book-scaffold-astro: no preset resolved; falling back to 'minimal' for v4 compatibility. This fallback will be removed in v5. Add a built-in style to defineBookConfig and pass the same preset to defineBookSchemas, or set BOOK_PRESET."
966
+ );
967
+ }
960
968
  }
961
969
  return candidate;
962
970
  }
@@ -966,7 +974,7 @@ function resolveProfile(explicit) {
966
974
 
967
975
  // src/integration.ts
968
976
  import { fileURLToPath } from "url";
969
- import { readFileSync as readFileSync2 } from "fs";
977
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
970
978
  import { join } from "path";
971
979
 
972
980
  // src/lib/define-style.ts
@@ -981,8 +989,12 @@ function composeStyles(styles) {
981
989
  for (const style of styles) {
982
990
  if (style.name !== void 0) merged.name = style.name;
983
991
  if (style.preset !== void 0) merged.preset = style.preset;
992
+ if (style.numberStyle !== void 0) merged.numberStyle = style.numberStyle;
984
993
  if (style.site !== void 0) merged.site = style.site;
985
994
  if (style.deploy !== void 0) merged.deploy = style.deploy;
995
+ if (style.releaseStatus !== void 0) {
996
+ merged.releaseStatus = style.releaseStatus;
997
+ }
986
998
  if (style.mdxComponentsModule !== void 0) {
987
999
  merged.mdxComponentsModule = style.mdxComponentsModule;
988
1000
  }
@@ -1194,6 +1206,17 @@ var ROUTE_REGISTRY = {
1194
1206
  // empty string → '/[slug]'; arbitrary string → '/<prefix>/[slug]').
1195
1207
  frontmatter: { pattern: "/frontmatter/[slug]", file: "frontmatter/[...slug].astro" }
1196
1208
  };
1209
+ var DEFAULT_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'";
1210
+ function renderSecurityHeaders(contentSecurityPolicy) {
1211
+ const csp = contentSecurityPolicy ?? DEFAULT_CONTENT_SECURITY_POLICY;
1212
+ return `/*
1213
+ Strict-Transport-Security: max-age=31536000; includeSubDomains
1214
+ X-Content-Type-Options: nosniff
1215
+ Referrer-Policy: strict-origin-when-cross-origin
1216
+ Permissions-Policy: camera=(), microphone=(), geolocation=()
1217
+ Content-Security-Policy: ${csp}
1218
+ `;
1219
+ }
1197
1220
  function frontmatterPatternFromPrefix(prefix) {
1198
1221
  if (prefix === void 0) return ROUTE_REGISTRY.frontmatter.pattern;
1199
1222
  if (prefix === "") return "/[slug]";
@@ -1205,6 +1228,7 @@ function resolvePage(file) {
1205
1228
  function bookScaffoldIntegration(opts) {
1206
1229
  const {
1207
1230
  profile,
1231
+ numberStyle = "shared",
1208
1232
  routes: userOverrides = {},
1209
1233
  extraStyles = [],
1210
1234
  mdxComponentsModule,
@@ -1212,6 +1236,7 @@ function bookScaffoldIntegration(opts) {
1212
1236
  title,
1213
1237
  subtitle,
1214
1238
  releaseStatus,
1239
+ securityHeaders,
1215
1240
  description,
1216
1241
  portfolio,
1217
1242
  // v4.6.0: book-level author + SEO config, propagated through the
@@ -1242,7 +1267,7 @@ function bookScaffoldIntegration(opts) {
1242
1267
  ),
1243
1268
  frontmatter: fmEnabled
1244
1269
  };
1245
- return {
1270
+ const integration = {
1246
1271
  name: "book-scaffold-astro",
1247
1272
  hooks: {
1248
1273
  "astro:config:setup": ({ injectScript, injectRoute, updateConfig, config }) => {
@@ -1312,9 +1337,38 @@ function bookScaffoldIntegration(opts) {
1312
1337
  }
1313
1338
  }
1314
1339
  });
1340
+ },
1341
+ // v4.27.0 (#188): emit defaults for every build, not only newly
1342
+ // scaffolded projects, so existing consumers become protected on their
1343
+ // next package upgrade. Astro copies public/_headers into the output
1344
+ // before build:done; an existing target therefore means the consumer
1345
+ // owns the complete file and must win byte-for-byte.
1346
+ "astro:build:done": ({ dir, logger }) => {
1347
+ if (securityHeaders === false) {
1348
+ logger.info("security-header emission disabled by defineBookConfig (#188)");
1349
+ return;
1350
+ }
1351
+ const target = join(fileURLToPath(dir), "_headers");
1352
+ if (existsSync3(target)) {
1353
+ logger.info("consumer public/_headers present; scaffold defaults skipped (#188)");
1354
+ return;
1355
+ }
1356
+ writeFileSync(
1357
+ target,
1358
+ renderSecurityHeaders(securityHeaders?.contentSecurityPolicy),
1359
+ "utf8"
1360
+ );
1361
+ logger.info("emitted default security headers; public/_headers overrides them (#188)");
1315
1362
  }
1316
1363
  }
1317
1364
  };
1365
+ Object.defineProperty(integration, "__bookScaffoldResolvedConfig", {
1366
+ value: Object.freeze({ preset: profile, numberStyle }),
1367
+ enumerable: false,
1368
+ configurable: false,
1369
+ writable: false
1370
+ });
1371
+ return integration;
1318
1372
  }
1319
1373
 
1320
1374
  // src/config.ts
@@ -1351,8 +1405,19 @@ async function defineBookConfig(opts) {
1351
1405
  if ("preset" in opts || "profile" in opts) {
1352
1406
  throw v3MigrationError(opts);
1353
1407
  }
1408
+ if (Object.prototype.hasOwnProperty.call(opts, "deploy")) {
1409
+ console.warn(
1410
+ "book-scaffold-astro: defineBookConfig({ deploy }) is inert and deprecated; create-book chooses wrangler.toml from its CLI preset. Remove this field before upgrading to v5 (#180)."
1411
+ );
1412
+ }
1354
1413
  const composed = composeStyles(opts.styles ?? []);
1355
- const profile = opts.styles === void 0 && composed.preset === void 0 ? "minimal" : composed.preset ?? "minimal";
1414
+ const profile = resolvePreset(composed.preset);
1415
+ const numberStyle = opts.numberStyle ?? composed.numberStyle ?? "shared";
1416
+ if (!NUMBER_STYLES.includes(numberStyle)) {
1417
+ throw new BookConfigError(
1418
+ `numberStyle must be one of ${NUMBER_STYLES.join(" | ")} (got ${JSON.stringify(numberStyle)})`
1419
+ );
1420
+ }
1356
1421
  const site = opts.site ?? composed.site;
1357
1422
  if (!site) {
1358
1423
  throw new BookConfigError(
@@ -1378,6 +1443,7 @@ async function defineBookConfig(opts) {
1378
1443
  const mdxComponentsModule = opts.mdxComponentsModule ?? composed.mdxComponentsModule;
1379
1444
  const composedMarkdown = composed.markdown ?? {};
1380
1445
  const userMarkdown = opts.markdown ?? {};
1446
+ const resolvedReleaseStatus = opts.releaseStatus !== void 0 ? opts.releaseStatus : composed.releaseStatus;
1381
1447
  const wantsKatex = PROFILES[profile]?.katex === true;
1382
1448
  const remarkPlugins = [];
1383
1449
  const rehypePlugins = [];
@@ -1419,6 +1485,7 @@ async function defineBookConfig(opts) {
1419
1485
  sitemap(sitemapOptions),
1420
1486
  bookScaffoldIntegration({
1421
1487
  profile,
1488
+ numberStyle,
1422
1489
  routes: mergedRoutes,
1423
1490
  mdxComponentsModule,
1424
1491
  extraStyles: mergedExtraStyles,
@@ -1427,8 +1494,13 @@ async function defineBookConfig(opts) {
1427
1494
  title: opts.title,
1428
1495
  // v4.23.0 (#135): sidebar brand subtitle.
1429
1496
  subtitle: opts.subtitle,
1430
- // v4.27.0 (#149): release-state banner (top-level beats the style chain).
1431
- releaseStatus: opts.releaseStatus ?? composed.releaseStatus,
1497
+ // v4.26.2 (#149; style inheritance + opt-out fixed in v4.26.3).
1498
+ releaseStatus: resolvedReleaseStatus === false ? void 0 : resolvedReleaseStatus,
1499
+ // v4.27.0 (#188): undefined enables the audited defaults, false opts
1500
+ // out, and an object can replace only the CSP. The integration owns
1501
+ // the build:done emission because that is where Astro's output dir is
1502
+ // known and a copied consumer public/_headers can be detected.
1503
+ securityHeaders: opts.securityHeaders,
1432
1504
  description: opts.description,
1433
1505
  portfolio: resolvedPortfolio,
1434
1506
  // v4.6.0: book-level author + SEO config (ogImage, twitterHandle),
@@ -1475,6 +1547,7 @@ async function defineBookConfig(opts) {
1475
1547
  };
1476
1548
  const {
1477
1549
  styles: _styles,
1550
+ numberStyle: _numberStyle,
1478
1551
  site: _site,
1479
1552
  routes: _routes,
1480
1553
  deploy: _deploy,
@@ -1486,8 +1559,10 @@ async function defineBookConfig(opts) {
1486
1559
  // v4.5.0: strip new landing-related opts so they don't leak into AstroUserConfig.
1487
1560
  title: _title,
1488
1561
  subtitle: _subtitle,
1489
- // v4.27.0 (#149): strip the release-state banner config.
1562
+ // v4.26.2 (#149): strip the release-state banner config.
1490
1563
  releaseStatus: _releaseStatus,
1564
+ // v4.27.0 (#188): consumed by the scaffold integration at build:done.
1565
+ securityHeaders: _securityHeaders,
1491
1566
  description: _description,
1492
1567
  portfolio: _portfolio,
1493
1568
  // v4.6.0: strip new book-level SEO opts (author + seo block).
@@ -1508,6 +1583,7 @@ async function defineBookConfig(opts) {
1508
1583
  ...rest
1509
1584
  } = opts;
1510
1585
  void _styles;
1586
+ void _numberStyle;
1511
1587
  void _site;
1512
1588
  void _routes;
1513
1589
  void _deploy;
@@ -1519,6 +1595,7 @@ async function defineBookConfig(opts) {
1519
1595
  void _title;
1520
1596
  void _subtitle;
1521
1597
  void _releaseStatus;
1598
+ void _securityHeaders;
1522
1599
  void _description;
1523
1600
  void _portfolio;
1524
1601
  void _author;
@@ -1624,6 +1701,9 @@ function assertEnumProp(value, allowed, ctx) {
1624
1701
  );
1625
1702
  }
1626
1703
 
1704
+ // src/index.ts
1705
+ init_katex_macros();
1706
+
1627
1707
  // src/lib/book-link.ts
1628
1708
  function resolveBookHref(siblingBooks, book, to) {
1629
1709
  const base = siblingBooks?.[book];
@@ -1637,9 +1717,13 @@ function resolveBookHref(siblingBooks, book, to) {
1637
1717
  }
1638
1718
 
1639
1719
  // src/lib/nav-href.ts
1640
- function normBase(baseUrl) {
1720
+ function normalizeBase(baseUrl) {
1641
1721
  return (baseUrl || "/").replace(/\/*$/, "/");
1642
1722
  }
1723
+ function baseNoSlash(baseUrl) {
1724
+ return (baseUrl || "/").replace(/\/+$/, "");
1725
+ }
1726
+ var normBase = normalizeBase;
1643
1727
  function fillTokens(pattern, tokens) {
1644
1728
  return pattern.replace(/:(book|slug|route|id)\b/g, (_m, k) => tokens[k] ?? "");
1645
1729
  }
@@ -1912,6 +1996,7 @@ export {
1912
1996
  BookConfigError,
1913
1997
  DEFAULT_GITHUB_BRANCH,
1914
1998
  KIND_LABEL,
1999
+ NUMBER_STYLES,
1915
2000
  THEOREM_KINDS,
1916
2001
  UNKNOWN_PART_ORDINAL,
1917
2002
  academicChapterSchema,
@@ -1924,6 +2009,7 @@ export {
1924
2009
  apparatusHref,
1925
2010
  assertEnumProp,
1926
2011
  assertKnownDomain,
2012
+ baseNoSlash,
1927
2013
  bloomLevels,
1928
2014
  bookOf,
1929
2015
  bookScaffoldIntegration,
@@ -1958,6 +2044,7 @@ export {
1958
2044
  layoutModes,
1959
2045
  minimalChapterSchema,
1960
2046
  minimalStyle,
2047
+ normalizeBase,
1961
2048
  normalizeFrontmatterConfig,
1962
2049
  originUrlFromGitConfig,
1963
2050
  parseRepoSlug,
@@ -1989,6 +2076,7 @@ export {
1989
2076
  sourceTiersResearch,
1990
2077
  sourcesSchema,
1991
2078
  spreadBlueprint,
2079
+ ssmMacros,
1992
2080
  theoremLabel,
1993
2081
  tocHeadings,
1994
2082
  toolSlugs,
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-Hue-uSeQ.js';
2
+ import { g as BookSchemasOptions } from './types-CZrkqzpC.js';
3
3
  import 'astro';
4
4
  import './schemas-CKipJ5Ie.js';
5
5
  import 'astro/zod';
package/dist/schemas.mjs CHANGED
@@ -814,6 +814,7 @@ function readEnvFile(path = ".env") {
814
814
  return {};
815
815
  }
816
816
  }
817
+ var PRESET_FALLBACK_WARNING = /* @__PURE__ */ Symbol.for("book-scaffold-astro:preset-fallback-warning");
817
818
  function resolvePreset(explicitPreset, explicitProfile) {
818
819
  let candidate = explicitPreset ?? explicitProfile ?? process.env.BOOK_PRESET ?? process.env.BOOK_PROFILE;
819
820
  let source = "default";
@@ -834,7 +835,13 @@ function resolvePreset(explicitPreset, explicitProfile) {
834
835
  );
835
836
  }
836
837
  if (source === "default") {
837
- console.warn("book-scaffold-astro: BOOK_PRESET not set; falling back to 'minimal'.");
838
+ const warningState = globalThis;
839
+ if (!warningState[PRESET_FALLBACK_WARNING]) {
840
+ warningState[PRESET_FALLBACK_WARNING] = true;
841
+ console.warn(
842
+ "book-scaffold-astro: no preset resolved; falling back to 'minimal' for v4 compatibility. This fallback will be removed in v5. Add a built-in style to defineBookConfig and pass the same preset to defineBookSchemas, or set BOOK_PRESET."
843
+ );
844
+ }
838
845
  }
839
846
  return candidate;
840
847
  }
@@ -353,6 +353,8 @@ interface Style {
353
353
  readonly name?: string;
354
354
  /** Profile that backs this style — determines schema + default routes + styles + KaTeX wiring. */
355
355
  readonly preset?: BookPreset;
356
+ /** Theorem-family numbering strategy; shallow override (last wins). */
357
+ readonly numberStyle?: NumberStyle;
356
358
  /** Book's deployed origin (sitemap, canonical, Pagefind). Required at composition end;
357
359
  * optional inside a Style (so styles can omit it and consumers can provide per-book). */
358
360
  readonly site?: string;
@@ -374,17 +376,17 @@ interface Style {
374
376
  * `remarkPlugins` and `rehypePlugins` arrays concat across the chain;
375
377
  * scalar fields override. */
376
378
  readonly markdown?: AstroUserConfig['markdown'];
377
- /** Deploy target drives create-book's wrangler.toml shape.
378
- * - `'workers'`: Cloudflare Workers + Static Assets (default for academic/tools/minimal)
379
- * - `'pages'`: Cloudflare Pages (default for research-portfolio/course-notes)
380
- * Closes #50. */
379
+ /** Reserved legacy metadata. It is composed but has no runtime or
380
+ * create-book effect: create-book chooses wrangler.toml from its CLI
381
+ * preset before a consumer Style exists (#180).
382
+ * @deprecated Inert in v4; scheduled for removal in v5. */
381
383
  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
- };
384
+ /**
385
+ * v4.26.2 (#149; style inheritance fixed in v4.26.3): release-state
386
+ * banner. Shallow override (last defined wins); `false` suppresses a
387
+ * banner inherited from an earlier style.
388
+ */
389
+ readonly releaseStatus?: ReleaseStatusConfig | false;
388
390
  /**
389
391
  * Scoped consumer-side metadata. Ignored by the toolkit; survives composition
390
392
  * as per-key spread (last wins per key). Use this for workflow data that
@@ -445,7 +447,8 @@ declare function defineStyle(opts: StyleInput): Style;
445
447
  * - Top-level `defineBookConfig` fields beat any style (handled in config.ts)
446
448
  *
447
449
  * Per-key merge strategy:
448
- * - `preset`, `site`, `deploy`, `mdxComponentsModule`, `name` → shallow override (last wins)
450
+ * - `preset`, `numberStyle`, `site`, `deploy`, `mdxComponentsModule`, `name`, `releaseStatus`
451
+ * → shallow override (last defined wins; `releaseStatus: false` suppresses)
449
452
  * - `routes` → per-route spread (each route key independently overridable)
450
453
  * - `katexMacros` → per-macro spread (each macro key independently overridable)
451
454
  * - `extra` → per-key spread (consumer metadata accumulates across the chain)
@@ -481,6 +484,41 @@ declare function normalizeFrontmatterConfig(v: FrontmatterRouteConfig | undefine
481
484
 
482
485
  type BookPreset = BookProfile;
483
486
  declare const BOOK_PRESETS: readonly ("academic" | "tools" | "minimal" | "course-notes" | "research-portfolio")[];
487
+ /** Theorem-family numbering strategy used by build-labels and validate. */
488
+ type NumberStyle = 'shared' | 'per-kind';
489
+ declare const NUMBER_STYLES: readonly ["shared", "per-kind"];
490
+ /**
491
+ * v4.27.0 (#188): build-time security-header customization.
492
+ *
493
+ * The integration emits a Cloudflare-compatible `dist/_headers` file by
494
+ * default. Consumers that need a different CSP can replace that one header
495
+ * while retaining the scaffold's other audited defaults.
496
+ */
497
+ interface SecurityHeadersConfig {
498
+ contentSecurityPolicy?: string;
499
+ }
500
+ /**
501
+ * v4.26.2 (#149): book-level release state rendered by
502
+ * `<PreReleaseBanner>` across every page.
503
+ *
504
+ * Style inheritance and explicit suppression were fixed in v4.26.3.
505
+ */
506
+ interface ReleaseStatusConfig {
507
+ state: 'alpha' | 'beta' | 'rc' | 'locked';
508
+ dismissAt?: string;
509
+ message?: string;
510
+ }
511
+ /**
512
+ * v4.26.2 (#149): book-level release state rendered by
513
+ * `<PreReleaseBanner>` across every page.
514
+ *
515
+ * Style inheritance and explicit suppression were fixed in v4.26.3.
516
+ */
517
+ interface ReleaseStatusConfig {
518
+ state: 'alpha' | 'beta' | 'rc' | 'locked';
519
+ dismissAt?: string;
520
+ message?: string;
521
+ }
484
522
  /**
485
523
  * Options for `defineBookConfig`. See PACKAGE_DESIGN.md §4.
486
524
  *
@@ -519,6 +557,13 @@ interface BookConfigOptions {
519
557
  * });
520
558
  */
521
559
  styles?: readonly Style[];
560
+ /**
561
+ * v4.27.0 (#175): theorem-family counter strategy. `shared` preserves the
562
+ * amsthm-style sequence used through v4.26; `per-kind` gives theorem,
563
+ * proposition, lemma, and each other theorem kind an independent sequence.
564
+ * May also be supplied by a Style; top-level config wins.
565
+ */
566
+ numberStyle?: NumberStyle;
522
567
  /**
523
568
  * Optional per-route override of the composed profile's defaults. Use to
524
569
  * disable an auto-injected route (e.g. multi-book consumer that ships
@@ -533,10 +578,13 @@ interface BookConfigOptions {
533
578
  */
534
579
  routes?: PartialRouteToggles;
535
580
  /**
536
- * v4.0.0 (NEW): deploy target drives `create-book`'s wrangler.toml shape.
537
- * Defaults from the composed style chain (academic/tools/minimal
538
- * 'workers'; course-notes/research-portfolio 'pages'); per-book override
539
- * here beats any style. Closes #50.
581
+ * v4.0.0 (#50): RESERVEDaccepted and style-chain-merged, but currently
582
+ * has NO runtime effect (#180). The wrangler.toml shape is decided once, at
583
+ * scaffold time, by `create-book` from the profile name (academic/tools/
584
+ * minimal Workers; course-notes/research-portfolio Pages); setting this
585
+ * field changes nothing afterward. Wire-or-remove is a v5 decision.
586
+ * @deprecated Inert in v4; remove this option before v5. Deployment shape
587
+ * is chosen by create-book or by the consumer's own deployment files.
540
588
  */
541
589
  deploy?: 'pages' | 'workers';
542
590
  /**
@@ -607,16 +655,20 @@ interface BookConfigOptions {
607
655
  */
608
656
  subtitle?: string;
609
657
  /**
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.
658
+ * v4.26.2 (#149; style inheritance fixed in v4.26.3): book-level release
659
+ * state. When set, Base.astro renders the existing <PreReleaseBanner>
660
+ * site-wide (top of <body>) with these props. Omit to inherit from the
661
+ * composed style chain; set `false` to suppress an inherited banner.
614
662
  */
615
- releaseStatus?: {
616
- state: 'alpha' | 'beta' | 'rc' | 'locked';
617
- dismissAt?: string;
618
- message?: string;
619
- };
663
+ releaseStatus?: ReleaseStatusConfig | false;
664
+ /**
665
+ * v4.27.0 (#188): security headers emitted to `dist/_headers` after an
666
+ * Astro build. Omit for the scaffold defaults; set `false` to emit no
667
+ * scaffold-owned file; provide `contentSecurityPolicy` to replace only the
668
+ * default CSP while retaining HSTS, XCTO, Referrer-Policy, and
669
+ * Permissions-Policy. A consumer-owned `public/_headers` always wins.
670
+ */
671
+ securityHeaders?: SecurityHeadersConfig | false;
620
672
  /**
621
673
  * v4.5.0: Book description. Read by the auto-injected `/` landing page (lead paragraph + <meta description>).
622
674
  * Optional; landing renders no description paragraph if unset.
@@ -749,6 +801,8 @@ interface BookSchemasOptions {
749
801
  /** Options for the internal `bookScaffoldIntegration`. See PACKAGE_DESIGN.md §6. */
750
802
  interface BookScaffoldIntegrationOptions {
751
803
  profile: BookProfile;
804
+ /** Resolved theorem-family counter strategy exposed to package CLI tooling. */
805
+ numberStyle?: NumberStyle;
752
806
  /**
753
807
  * Per-route override; merged into the profile's defaults.
754
808
  * v4.0.0: `routes.frontmatter` widened to `boolean | { enabled, prefix? }`
@@ -769,13 +823,11 @@ interface BookScaffoldIntegrationOptions {
769
823
  /** v4.23.0 (#135): sidebar brand subtitle, propagated via the book-config
770
824
  * virtual module to Sidebar.astro. */
771
825
  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
- };
826
+ /** v4.26.2 (#149; style inheritance fixed in v4.26.3): resolved
827
+ * release-state banner propagated via the book-config virtual module. */
828
+ releaseStatus?: ReleaseStatusConfig;
829
+ /** v4.27.0 (#188): resolved security-header emission policy. */
830
+ securityHeaders?: SecurityHeadersConfig | false;
779
831
  /** v4.5.0: book description, propagated to `/` landing via vite.define. */
780
832
  description?: string;
781
833
  /**
@@ -827,22 +879,6 @@ interface BookScaffoldIntegrationOptions {
827
879
  declare class BookConfigError extends Error {
828
880
  constructor(message: string);
829
881
  }
830
- /**
831
- * Resolve preset from explicit args → env → .env → default. Throws on invalid.
832
- *
833
- * v3.4.0 (closes #9): canonical resolver. Accepts both `preset` and `profile`
834
- * (back-compat) explicit args; reads both `BOOK_PRESET` (preferred) and
835
- * `BOOK_PROFILE` (alias) env vars; same for .env file lookups.
836
- *
837
- * Resolution order:
838
- * 1. explicitPreset (from defineBookConfig({ preset: ... }))
839
- * 2. explicitProfile (from defineBookConfig({ profile: ... }))
840
- * 3. process.env.BOOK_PRESET
841
- * 4. process.env.BOOK_PROFILE
842
- * 5. .env BOOK_PRESET
843
- * 6. .env BOOK_PROFILE
844
- * 7. 'minimal' (with console.warn)
845
- */
846
882
  declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: BookProfile): BookPreset;
847
883
  /**
848
884
  * Backward-compat alias. New code should use `resolvePreset()`.
@@ -851,4 +887,4 @@ declare function resolvePreset(explicitPreset?: BookPreset, explicitProfile?: Bo
851
887
  */
852
888
  declare function resolveProfile(explicit?: BookProfile): BookProfile;
853
889
 
854
- export { BOOK_PRESETS as B, type ChapterFor as C, type FreshnessAffordance as F, type PartKey as P, type RouteToggles as R, type StatusBadge as S, type VolatilityBadge as V, BOOK_PROFILES as a, BookConfigError as b, type BookConfigOptions as c, type BookPreset as d, type BookProfile as e, type BookScaffoldIntegrationOptions as f, type BookSchemasOptions as g, type ChaptersRenderer as h, type FrontmatterRouteConfig as i, type PartialRouteToggles as j, type ProfileDefinition as k, type Style as l, type StyleInput as m, composeStyles as n, defineProfile as o, defineStyle as p, normalizeFrontmatterConfig as q, resolvePreset as r, resolveProfile as s };
890
+ export { BOOK_PRESETS as B, type ChapterFor as C, type FreshnessAffordance as F, NUMBER_STYLES as N, type PartKey as P, type ReleaseStatusConfig as R, type SecurityHeadersConfig as S, type VolatilityBadge as V, BOOK_PROFILES as a, BookConfigError as b, type BookConfigOptions as c, type BookPreset as d, type BookProfile as e, type BookScaffoldIntegrationOptions as f, type BookSchemasOptions as g, type ChaptersRenderer as h, type FrontmatterRouteConfig as i, type NumberStyle as j, type PartialRouteToggles as k, type ProfileDefinition as l, type RouteToggles as m, type StatusBadge as n, type Style as o, type StyleInput as p, composeStyles as q, defineProfile as r, defineStyle as s, normalizeFrontmatterConfig as t, resolvePreset as u, resolveProfile as v };
@@ -15,15 +15,15 @@
15
15
  * lang — html lang attribute (default: en)
16
16
  * showSidebar — render the left chapter-navigation sidebar (default: true).
17
17
  * Set false for the landing page or any full-bleed surface.
18
- * Below 1024px the sidebar is auto-hidden via CSS regardless.
19
- * showChrome — render the tools chrome (ToolFilter + VersionSelector)
18
+ * Below 80rem (1280px) the sidebar is auto-hidden via CSS and
19
+ * the mobile nav drawer is the chapter nav (v4.26.0, #80).
20
+ * showChrome — render the automatic tools chrome (ToolFilter)
20
21
  * (default: true). Set false on a landing/hub page with no
21
- * chapters/tools/versions; search + theme toggle always render.
22
+ * chapter filtering; search + theme toggle always render.
22
23
  *
23
- * Tools chrome (ToolFilter, VersionSelector): only mounted when
24
- * BOOK_PROFILE !== 'academic' AND showChrome !== false. Academic-profile books
25
- * (and any page with showChrome={false}) get no JS islands in the chrome row
26
- * (search + theme toggle only).
24
+ * Tools chrome (ToolFilter): only mounted when BOOK_PROFILE !== 'academic' AND
25
+ * showChrome !== false. VersionSelector is a prop-driven public component that
26
+ * consumers mount explicitly once they have real deployed-version data.
27
27
  *
28
28
  * Usage:
29
29
  * ---
@@ -51,15 +51,15 @@ import '../styles/chapter.css';
51
51
  import '../styles/tool-filter.css';
52
52
  import '../styles/convergence.css';
53
53
  import '../styles/print.css';
54
- // Use package-path imports for the .tsx islands so Vite resolves them
54
+ // Use the package-path import for the .tsx island so Vite resolves it
55
55
  // via the exports map (→ pre-compiled dist/components/*.mjs with proper
56
56
  // preact JSX). The raw .tsx files are also shipped so consumers can
57
57
  // import them directly, but relative imports from inside the package
58
58
  // must route through the exports map, not the raw source.
59
- import VersionSelector from '@brandon_m_behring/book-scaffold-astro/components/VersionSelector';
60
59
  import ToolFilter from '@brandon_m_behring/book-scaffold-astro/components/ToolFilter';
61
60
  import Sidebar from '../components/Sidebar.astro';
62
- // v4.27.0 (#149): site-wide release-state banner, driven by defineBookConfig.
61
+ // v4.26.2 (#149; style inheritance fixed in v4.26.3): site-wide
62
+ // release-state banner, driven by defineBookConfig.
63
63
  import PreReleaseBanner from '../components/PreReleaseBanner.astro';
64
64
  // v4.26.0 (#80): the mobile/tablet drawer reuses the same book-scoped nav source.
65
65
  import NavContent from '../components/NavContent.astro';
@@ -67,6 +67,7 @@ import NavContent from '../components/NavContent.astro';
67
67
  // description fallback, ogImage default, twitterHandle) from the
68
68
  // book-config virtual module (was landing-config in v4.5.1).
69
69
  import bookConfig from 'virtual:book-scaffold/book-config';
70
+ import { normalizeBase } from '../src/lib/nav-href';
70
71
 
71
72
  const profile = import.meta.env.BOOK_PROFILE ?? 'minimal';
72
73
 
@@ -75,10 +76,10 @@ interface Props {
75
76
  description?: string;
76
77
  lang?: string;
77
78
  showSidebar?: boolean;
78
- /** #163: render the optional "tools chrome" — the ToolFilter +
79
- * VersionSelector islands (default true). Set false on landing/hub pages
80
- * with no chapters/tools/versions; the search + theme-toggle cluster always
81
- * renders. (No-op for the academic profile, which never shows them.) */
79
+ /** #163: render the optional automatic tools chrome — the ToolFilter island
80
+ * (default true). Set false on landing/hub pages with no chapter filtering;
81
+ * the search + theme-toggle cluster always renders. (No-op for the academic
82
+ * profile, which never shows the ToolFilter.) */
82
83
  showChrome?: boolean;
83
84
  /** v4.6.0: Open Graph image URL (relative to site root, or absolute). */
84
85
  ogImage?: string;
@@ -96,9 +97,9 @@ const {
96
97
  ogType = 'website',
97
98
  } = Astro.props;
98
99
 
99
- // Tools chrome (ToolFilter + VersionSelector) renders only for non-academic
100
+ // Automatic tools chrome (ToolFilter) renders only for non-academic
100
101
  // profiles AND when the page opts in via showChrome (default true). A
101
- // landing/hub page (no chapters/tools/versions) passes showChrome={false} to
102
+ // landing/hub page (no chapter filtering) passes showChrome={false} to
102
103
  // get the search + theme-toggle cluster only — the chrome-free path that
103
104
  // previously forced borrowing the academic profile (and its katex deps). #163.
104
105
  const showToolsChrome = (profile !== 'academic') && showChrome;
@@ -119,7 +120,7 @@ const absoluteOgImage = resolvedOgImage
119
120
  const twitterHandle = bookConfig.seo?.twitterHandle ?? null;
120
121
  const ogSiteName = bookConfig.title ?? title;
121
122
  const ogDescription = description ?? bookConfig.description ?? '';
122
- const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
123
+ const baseUrl = normalizeBase(import.meta.env.BASE_URL);
123
124
  ---
124
125
 
125
126
  <!doctype html>
@@ -167,7 +168,7 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
167
168
  <slot name="head" />
168
169
  </head>
169
170
  <body>
170
- {/* v4.27.0 (#149): book-level release banner — first element in <body> so
171
+ {/* v4.26.2 (#149; style inheritance fixed in v4.26.3): book-level release banner — first element in <body> so
171
172
  every page (chapters, apparatus, landing) declares its state. Renders
172
173
  only when defineBookConfig({ releaseStatus }) is set. */}
173
174
  {bookConfig.releaseStatus && (
@@ -195,7 +196,6 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
195
196
  </a>
196
197
  )}
197
198
  {showToolsChrome && <ToolFilter client:idle />}
198
- {showToolsChrome && <VersionSelector client:idle />}
199
199
  <a
200
200
  href={`${baseUrl}search/`}
201
201
  class="chrome-button"
@@ -216,7 +216,8 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
216
216
  </button>
217
217
  </div>
218
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
219
+ CSS-hidden ≥80rem where the Sidebar is the nav (layout.css and the
220
+ controller's desktop auto-close agree on 80rem). role=dialog + the inline
220
221
  controller below provide focus-trap/ESC; `:target` is the no-JS fallback. */}
221
222
  {showSidebar && (
222
223
  <div id="nav-drawer" class="nav-drawer" data-nav-drawer>
@@ -389,7 +390,8 @@ const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/*$/, '/');
389
390
  text-decoration: none;
390
391
  }
391
392
 
392
- /* Version selector dropdown */
393
+ /* Opt-in VersionSelector dropdown. Base provides its visual language but
394
+ * deliberately does not mount it without a consumer-owned version manifest. */
393
395
  .version-selector {
394
396
  position: relative;
395
397
  display: inline-block;