@brandon_m_behring/book-scaffold-astro 3.0.0-alpha.3 → 3.0.0-alpha.5

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.
@@ -1,13 +1,16 @@
1
1
  ---
2
2
  /**
3
3
  * ChapterHeader — renders the metadata block at the top of every chapter.
4
- * Surfaces provenance signals the LaTeX book conveyed implicitly:
5
- * - Part + chapter number (position in the book)
6
- * - Volatility class (freshness calibration for the reader)
7
- * - Tools compared (scope signal)
8
- * - Last verified date (how stale are the claims?)
9
4
  *
10
- * Driven entirely from the chapter's frontmatter (see content.config.ts).
5
+ * Schema-agnostic: the package supports two profile schemas (academic and
6
+ * tools), so this component renders only the fields that are present on
7
+ * the chapter data. Tools-profile metadata (volatility, last_verified,
8
+ * tools_compared) appears when defined; academic-profile metadata (week,
9
+ * status) appears in its place.
10
+ *
11
+ * Use a chapter card's metadata to calibrate how much trust to place in
12
+ * its specific claims — stable principles age slowly; feature surfaces
13
+ * age fast.
11
14
  */
12
15
  import type { CollectionEntry } from 'astro:content';
13
16
  import { getFreshness, freshnessLabel } from '../src/lib/freshness';
@@ -17,45 +20,97 @@ interface Props {
17
20
  }
18
21
  const { data } = Astro.props;
19
22
 
20
- function formatDate(d: Date): string {
21
- return d.toISOString().slice(0, 10);
23
+ // Widen for cross-schema field probing. The consumer's content.config.ts
24
+ // resolves `data` to exactly one of the two schemas at build time; this
25
+ // runtime probe handles either without crashing.
26
+ const d = data as Record<string, unknown>;
27
+
28
+ const hasToolsMeta =
29
+ typeof d.volatility === 'string' &&
30
+ d.last_verified instanceof Date &&
31
+ Array.isArray(d.tools_compared);
32
+
33
+ const hasAcademicMeta =
34
+ typeof d.week === 'number' && typeof d.status === 'string';
35
+
36
+ function formatDate(date: unknown): string {
37
+ return date instanceof Date ? date.toISOString().slice(0, 10) : '';
22
38
  }
23
39
 
24
- const freshness = getFreshness(data.last_verified, data.volatility);
25
- const freshnessText =
26
- freshness.status === 'fresh'
40
+ // Tools-profile freshness only when all inputs are present + typed.
41
+ const freshness =
42
+ hasToolsMeta && d.last_verified instanceof Date && typeof d.volatility === 'string'
43
+ ? getFreshness(d.last_verified, d.volatility as Parameters<typeof getFreshness>[1])
44
+ : null;
45
+
46
+ const freshnessText = freshness
47
+ ? freshness.status === 'fresh'
27
48
  ? 'Fresh'
28
49
  : freshness.status === 'verify-soon'
29
50
  ? 'Verify soon'
30
- : 'Stale';
51
+ : 'Stale'
52
+ : null;
53
+
54
+ // Display strings, profile-tagged for clarity in markup.
55
+ const partLabel = (() => {
56
+ const p = d.part;
57
+ if (typeof p === 'number') return `Part ${p}`;
58
+ if (typeof p === 'string' && p.length > 0) return `Part: ${p}`;
59
+ return null;
60
+ })();
61
+ const chapterNum =
62
+ typeof d.chapter === 'number' ? `Chapter ${d.chapter}` : null;
63
+ const weekNum = typeof d.week === 'number' ? `Week ${d.week}` : null;
64
+ const statusBadge =
65
+ typeof d.status === 'string' ? d.status.replace(/_/g, ' ') : null;
66
+ const title = typeof d.title === 'string' ? d.title : '(untitled)';
67
+ const description = typeof d.description === 'string' ? d.description : null;
68
+ const toolsCompared = Array.isArray(d.tools_compared)
69
+ ? (d.tools_compared as string[])
70
+ : [];
71
+ const lastVerified = d.last_verified instanceof Date ? d.last_verified : null;
72
+ const updated = d.updated instanceof Date ? d.updated : null;
73
+ const volatility = typeof d.volatility === 'string' ? d.volatility : null;
31
74
  ---
32
75
  <header class="chapter-header">
33
76
  <div class="chapter-meta">
34
- <span>Part {data.part}</span>
35
- <span>Chapter {data.chapter}</span>
36
- <span>
37
- Last verified {formatDate(data.last_verified)}
38
- <span
39
- class="freshness-badge"
40
- data-status={freshness.status}
41
- aria-label={freshnessLabel(freshness)}
42
- title={freshnessLabel(freshness)}
43
- >{freshnessText}</span>
44
- </span>
45
- {data.updated && <span>Updated {formatDate(data.updated)}</span>}
46
- </div>
47
- <h1>{data.title}</h1>
48
- {data.description && <p class="chapter-description">{data.description}</p>}
49
- <div class="chapter-badge-row">
50
- <span class="chapter-badge-row-label">Volatility:</span>
51
- <span class={`volatility-badge volatility-${data.volatility}`}>
52
- {data.volatility}
53
- </span>
77
+ {partLabel && <span>{partLabel}</span>}
78
+ {chapterNum && <span>{chapterNum}</span>}
79
+ {weekNum && <span>{weekNum}</span>}
80
+ {statusBadge && (
81
+ <span class="status-badge" data-status={d.status as string}>
82
+ {statusBadge}
83
+ </span>
84
+ )}
85
+ {lastVerified && (
86
+ <span>
87
+ Last verified {formatDate(lastVerified)}
88
+ {freshness && freshnessText && (
89
+ <span
90
+ class="freshness-badge"
91
+ data-status={freshness.status}
92
+ aria-label={freshnessLabel(freshness)}
93
+ title={freshnessLabel(freshness)}
94
+ >{freshnessText}</span>
95
+ )}
96
+ </span>
97
+ )}
98
+ {updated && <span>Updated {formatDate(updated)}</span>}
54
99
  </div>
55
- {data.tools_compared.length > 0 && (
100
+ <h1>{title}</h1>
101
+ {description && <p class="chapter-description">{description}</p>}
102
+ {hasToolsMeta && volatility && (
103
+ <div class="chapter-badge-row">
104
+ <span class="chapter-badge-row-label">Volatility:</span>
105
+ <span class={`volatility-badge volatility-${volatility}`}>
106
+ {volatility}
107
+ </span>
108
+ </div>
109
+ )}
110
+ {hasToolsMeta && toolsCompared.length > 0 && (
56
111
  <div class="chapter-badge-row">
57
112
  <span class="chapter-badge-row-label">Tools compared:</span>
58
- {data.tools_compared.map((t) => <span class="tool-badge">{t}</span>)}
113
+ {toolsCompared.map((t) => <span class="tool-badge">{t}</span>)}
59
114
  </div>
60
115
  )}
61
116
  </header>
package/dist/index.mjs CHANGED
@@ -153,8 +153,8 @@ var ALWAYS_ON_STYLES = [
153
153
  var TOOLS_ONLY_STYLES = ["convergence.css", "tool-filter.css"];
154
154
  var DEFAULT_ROUTES_ALL = [
155
155
  { pattern: "/references", file: "references.astro" },
156
- { pattern: "/print", file: "print.astro" },
157
- { pattern: "/search", file: "search.astro" }
156
+ { pattern: "/search", file: "search.astro" },
157
+ { pattern: "/print", file: "print.astro" }
158
158
  ];
159
159
  var DEFAULT_ROUTES_TOOLS = [
160
160
  { pattern: "/chapters", file: "chapters.astro" },
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": "3.0.0-alpha.3",
4
+ "version": "3.0.0-alpha.5",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -40,6 +40,7 @@
40
40
  "types": "./dist/schemas.d.ts",
41
41
  "import": "./dist/schemas.mjs"
42
42
  },
43
+ "./package.json": "./package.json",
43
44
  "./components/CaseStudy.astro": "./components/CaseStudy.astro",
44
45
  "./components/ChapterHeader.astro": "./components/ChapterHeader.astro",
45
46
  "./components/ChapterNav.astro": "./components/ChapterNav.astro",
@@ -109,7 +110,7 @@
109
110
  "README.md"
110
111
  ],
111
112
  "scripts": {
112
- "build": "tsup",
113
+ "build": "tsup && rm -f dist/types-*.d.ts",
113
114
  "prepublishOnly": "npm run build"
114
115
  },
115
116
  "peerDependencies": {
@@ -1,61 +0,0 @@
1
- import { AstroIntegration, AstroUserConfig } from 'astro';
2
-
3
- /**
4
- * Shared types for @brandon_m_behring/book-scaffold-astro.
5
- *
6
- * Public types referenced from PACKAGE_DESIGN.md §4 / §5 / §6. Kept in
7
- * one place so consumer IntelliSense surfaces a coherent API.
8
- */
9
-
10
- type BookProfile = 'academic' | 'tools' | 'minimal';
11
- declare const BOOK_PROFILES: readonly ["academic", "tools", "minimal"];
12
- /**
13
- * Options for `defineBookConfig`. See PACKAGE_DESIGN.md §4.
14
- *
15
- * Note on the index signature: `AstroUserConfig` carries generic
16
- * parameters (`Locales`, `SessionDriverName`, fonts) that can't be
17
- * threaded cleanly through a wrapper. Instead we type the package-
18
- * specific fields strictly and allow arbitrary AstroUserConfig keys
19
- * via the index signature — consumer types will lint clean but lose
20
- * full IDE autocomplete on non-package fields. Acceptable trade.
21
- */
22
- interface BookConfigOptions {
23
- /** Required. Book's deployed origin (sitemap, canonical, Pagefind). */
24
- site: string;
25
- /**
26
- * Optional. Falls back to `process.env.BOOK_PROFILE`, then `'minimal'`.
27
- * Explicit param always wins over env.
28
- */
29
- profile?: BookProfile;
30
- /** Optional. Appended to the package-provided integration list. */
31
- extraIntegrations?: AstroIntegration[];
32
- /**
33
- * Optional. CSS basenames to inject in addition to the profile-resolved
34
- * set. Cross-profile escape hatch (e.g. an academic book using
35
- * `<Convergence>`). Example: `['convergence.css']`.
36
- */
37
- extraStyles?: string[];
38
- /** Optional. Spread-merged into the package-provided markdown config. */
39
- markdown?: AstroUserConfig['markdown'];
40
- /** Escape hatch for any other AstroUserConfig field. */
41
- [key: string]: unknown;
42
- }
43
- /** Options for `defineBookSchemas`. See PACKAGE_DESIGN.md §5. */
44
- interface BookSchemasOptions {
45
- profile?: BookProfile;
46
- /** Defaults to `'./src/content/chapters'`. */
47
- chaptersBase?: string;
48
- }
49
- /** Options for the internal `bookScaffoldIntegration`. See PACKAGE_DESIGN.md §6. */
50
- interface BookScaffoldIntegrationOptions {
51
- profile: BookProfile;
52
- extraStyles?: string[];
53
- }
54
- /** Raised when the resolved profile is not one of `BOOK_PROFILES`. */
55
- declare class BookConfigError extends Error {
56
- constructor(message: string);
57
- }
58
- /** Resolve profile from explicit param → env → default. Throws on invalid. */
59
- declare function resolveProfile(explicit?: BookProfile): BookProfile;
60
-
61
- export { BOOK_PROFILES as B, BookConfigError as a, type BookConfigOptions as b, type BookProfile as c, type BookScaffoldIntegrationOptions as d, type BookSchemasOptions as e, resolveProfile as r };