@brandon_m_behring/book-scaffold-astro 3.7.1 → 4.1.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.
- package/CLAUDE.md +3 -1
- package/components/Pitfall.astro +17 -0
- package/components/PocLayout.astro +28 -0
- package/components/WorkedExample.astro +29 -0
- package/components/YouWillLearn.astro +24 -0
- package/dist/index.d.ts +55 -3
- package/dist/index.mjs +212 -28
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.mjs +11 -2
- package/package.json +6 -1
- package/recipes/15-defining-styles.md +215 -0
- package/scripts/build-bib.mjs +16 -1
- package/src/lib/define-style.ts +302 -0
- package/styles/callouts.css +48 -0
- package/styles/poc-layouts.css +57 -0
- package/styles/tokens.css +5 -0
package/CLAUDE.md
CHANGED
|
@@ -66,7 +66,9 @@ Two callout families coexist. Authors import what they need.
|
|
|
66
66
|
|
|
67
67
|
**Academic family** (`src/components/callouts/`, 10 components): `NoteBox`, `ExampleBox`, `DynConnect`, `InsightBox`, `WarnBox`, `CounterBox`, `TipBox`, `OpenQuestion`, `PaperBox`, `ResultBox`. Plus `Theorem` (unified for theorem/proposition/lemma/corollary/definition/example/exercise/remark/proof).
|
|
68
68
|
|
|
69
|
-
**
|
|
69
|
+
**Pedagogy family** (v4.1.0+, any profile, 3 components): `Pitfall` (rose; "common mistake" — distinct from `WarnBox`'s preemptive warning), `WorkedExample` (plum; collapsible `<details>` block with `#worked-example-{id}` anchor for deep links), `YouWillLearn` (gold; chapter-opener with optional `prerequisites` prop). Slot bullets/code freely; render at any preset.
|
|
70
|
+
|
|
71
|
+
**Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`, `PocLayout` (v4.1.0+; wraps slot in a per-`kind` layout shell — 5 closed-union kinds; see `recipes/15-defining-styles.md`).
|
|
70
72
|
|
|
71
73
|
Full reference in `recipes/04-component-library.md`.
|
|
72
74
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Pitfall — distinct from generic <WarnBox>.
|
|
4
|
+
* Retrospective "this often goes wrong" callout (vs WarnBox's preemptive
|
|
5
|
+
* "this could go wrong"). React.dev "Pitfall" vocabulary. Closes #58.
|
|
6
|
+
*
|
|
7
|
+
* Family: crimson (--callout-pitfall, deeper than warn-rose).
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
title?: string;
|
|
11
|
+
}
|
|
12
|
+
const { title = 'Common mistake' } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
<aside class="callout callout-pitfall" role="note">
|
|
15
|
+
<strong class="callout-title">{title}</strong>
|
|
16
|
+
<div class="callout-body"><slot /></div>
|
|
17
|
+
</aside>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* PocLayout — per-PoC-kind layout selector (closes #56).
|
|
4
|
+
*
|
|
5
|
+
* Wraps slotted content in a <div> that swaps 3 CSS variables per kind
|
|
6
|
+
* (line-length, vertical rhythm, heading emphasis). 5 closed-union kinds.
|
|
7
|
+
*
|
|
8
|
+
* CSS variable surface defined in package/styles/poc-layouts.css. Authors
|
|
9
|
+
* can override per-kind in their own stylesheets via `:where(.poc-layout-X)`.
|
|
10
|
+
*
|
|
11
|
+
* Closed `kind` union: discriminated literal type. To add a 6th kind in
|
|
12
|
+
* a future release, expand the union + add a CSS block in poc-layouts.css.
|
|
13
|
+
*/
|
|
14
|
+
export type PocLayoutKind =
|
|
15
|
+
| 'tutorial'
|
|
16
|
+
| 'how-to'
|
|
17
|
+
| 'tldr'
|
|
18
|
+
| 'part-summary'
|
|
19
|
+
| 'cheat-sheet';
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
kind: PocLayoutKind;
|
|
23
|
+
}
|
|
24
|
+
const { kind } = Astro.props;
|
|
25
|
+
---
|
|
26
|
+
<div class={`poc-layout poc-layout-${kind}`}>
|
|
27
|
+
<slot />
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* WorkedExample — collapsible demonstration block (closes #57).
|
|
4
|
+
*
|
|
5
|
+
* Pedagogical worked-example-effect treatment (Sweller/Cooper). Uses
|
|
6
|
+
* native <details>; collapsed by default unless `expanded` prop set.
|
|
7
|
+
* The outer aside carries `id="worked-example-{id}"` for deep links
|
|
8
|
+
* from TL;DRs or cheat-sheets. Prefix avoids collision with author
|
|
9
|
+
* heading anchors.
|
|
10
|
+
*
|
|
11
|
+
* Family: plum (--callout-worked, reuses --callout-official authority hue).
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
expanded?: boolean;
|
|
17
|
+
}
|
|
18
|
+
const { id, title, expanded = false } = Astro.props;
|
|
19
|
+
const anchorId = `worked-example-${id}`;
|
|
20
|
+
---
|
|
21
|
+
<aside class="callout callout-worked" id={anchorId} role="note">
|
|
22
|
+
<details open={expanded}>
|
|
23
|
+
<summary>
|
|
24
|
+
<strong class="callout-title">{title}</strong>
|
|
25
|
+
<span class="callout-chip">Worked example</span>
|
|
26
|
+
</summary>
|
|
27
|
+
<div class="callout-body"><slot /></div>
|
|
28
|
+
</details>
|
|
29
|
+
</aside>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* YouWillLearn — chapter-opener "what this chapter delivers" callout
|
|
4
|
+
* (closes #59). React.dev pedagogy vocabulary; Bloom's-taxonomy framing.
|
|
5
|
+
*
|
|
6
|
+
* Slotted body — author writes markdown bullets directly.
|
|
7
|
+
* Optional `prerequisites` prop renders a small "Before you start" sub-block.
|
|
8
|
+
*
|
|
9
|
+
* Family: gold (--callout-learn, reuses --callout-insight to signal importance).
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
prerequisites?: string;
|
|
13
|
+
}
|
|
14
|
+
const { prerequisites } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
<aside class="callout callout-learn" role="note">
|
|
17
|
+
{prerequisites && (
|
|
18
|
+
<div class="callout-prereq">
|
|
19
|
+
<strong class="callout-prereq-label">Before you start:</strong> {prerequisites}
|
|
20
|
+
</div>
|
|
21
|
+
)}
|
|
22
|
+
<strong class="callout-title">You will learn</strong>
|
|
23
|
+
<div class="callout-body"><slot /></div>
|
|
24
|
+
</aside>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions,
|
|
3
|
-
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, M as MinimalChapter, P as PartKey,
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Q as volatilityLevels, h as ChaptersRenderer, n as Style } from './types-DR0-GwxO.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ResearchPortfolioChapter, m as RouteToggles, S as StatusBadge, o as StyleInput, T as ToolsChapter, V as VolatilityBadge, p as academicChapterSchema, q as academicParts, r as changeKinds, s as changelogSchema, t as chapterStatus, u as composeStyles, v as courseNotesChapterSchema, w as defineProfile, x as defineStyle, y as minimalChapterSchema, z as normalizeFrontmatterConfig, D as patternCategories, E as patternsSchema, G as researchPortfolioChapterSchema, H as resolvePreset, I as resolveProfile, J as sourceTiers, K as sourceTiersResearch, L as sourcesSchema, N as toolSlugs, O as toolsChapterSchema } from './types-DR0-GwxO.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
|
|
@@ -143,4 +143,56 @@ declare const academicChaptersRenderer: ChaptersRenderer;
|
|
|
143
143
|
|
|
144
144
|
declare const fallbackChaptersRenderer: ChaptersRenderer;
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
/**
|
|
147
|
+
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
148
|
+
*
|
|
149
|
+
* Each profile that the toolkit shipped in v3 (academic, tools, minimal,
|
|
150
|
+
* course-notes, research-portfolio) is now mirrored by a built-in Style
|
|
151
|
+
* importable by consumers:
|
|
152
|
+
*
|
|
153
|
+
* import { defineBookConfig, academicStyle } from '@brandon_m_behring/book-scaffold-astro';
|
|
154
|
+
* export default await defineBookConfig({ styles: [academicStyle], site: '...' });
|
|
155
|
+
*
|
|
156
|
+
* Consumers can compose built-in styles with their own:
|
|
157
|
+
*
|
|
158
|
+
* import { researchPortfolioStyle, defineStyle } from '@brandon_m_behring/book-scaffold-astro';
|
|
159
|
+
* const guidesFamilyStyle = defineStyle({ site: 'https://guides.brandon-behring.dev/' });
|
|
160
|
+
* export default await defineBookConfig({
|
|
161
|
+
* styles: [researchPortfolioStyle, guidesFamilyStyle],
|
|
162
|
+
* // ...
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* The v3 `preset: '...'` shorthand is replaced by this explicit style chain.
|
|
166
|
+
* See MIGRATION-v3-to-v4.md.
|
|
167
|
+
*/
|
|
168
|
+
|
|
169
|
+
/** Academic preset — weekly curriculum, 7-state status, KaTeX wired, BibTeX pipeline. */
|
|
170
|
+
declare const academicStyle: Style;
|
|
171
|
+
/** Tools preset — AI-CLI comparison content with volatility + sources. */
|
|
172
|
+
declare const toolsStyle: Style;
|
|
173
|
+
/** Minimal preset — single-author essays / manifestos. */
|
|
174
|
+
declare const minimalStyle: Style;
|
|
175
|
+
/** Course-notes preset — chapters derived from a video course / MOOC / book.
|
|
176
|
+
* Content-heavy static sites → defaults to Cloudflare Pages deploy. */
|
|
177
|
+
declare const courseNotesStyle: Style;
|
|
178
|
+
/** Research-portfolio preset — academic structure + tools-style provenance + portfolio components.
|
|
179
|
+
* Defaults to Pages deploy + frontmatter route enabled (portfolios universally need
|
|
180
|
+
* title-page / disclosure / banner pages). */
|
|
181
|
+
declare const researchPortfolioStyle: Style;
|
|
182
|
+
/**
|
|
183
|
+
* Registry of all toolkit-shipped styles, keyed by their preset name.
|
|
184
|
+
*
|
|
185
|
+
* `satisfies` (TS 4.9+) keeps the inferred narrow type while validating the
|
|
186
|
+
* shape: `BUILTIN_STYLES['academic']` resolves to `typeof academicStyle`,
|
|
187
|
+
* not to generic `Style`. Used by the v3 → v4 migration error path
|
|
188
|
+
* (config.ts) to construct auto-suggested replacements.
|
|
189
|
+
*/
|
|
190
|
+
declare const BUILTIN_STYLES: {
|
|
191
|
+
readonly academic: Style;
|
|
192
|
+
readonly tools: Style;
|
|
193
|
+
readonly minimal: Style;
|
|
194
|
+
readonly 'course-notes': Style;
|
|
195
|
+
readonly 'research-portfolio': Style;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export { BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, Style, type VolatilityLevel, academicChaptersRenderer, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -685,6 +685,72 @@ function resolveProfile(explicit) {
|
|
|
685
685
|
// src/integration.ts
|
|
686
686
|
import { fileURLToPath } from "url";
|
|
687
687
|
|
|
688
|
+
// src/lib/define-style.ts
|
|
689
|
+
function defineStyle(opts) {
|
|
690
|
+
return { __styleVersion: 1, ...opts };
|
|
691
|
+
}
|
|
692
|
+
function composeStyles(styles) {
|
|
693
|
+
if (styles.length === 0) {
|
|
694
|
+
return defineStyle({});
|
|
695
|
+
}
|
|
696
|
+
const merged = {};
|
|
697
|
+
for (const style of styles) {
|
|
698
|
+
if (style.name !== void 0) merged.name = style.name;
|
|
699
|
+
if (style.preset !== void 0) merged.preset = style.preset;
|
|
700
|
+
if (style.site !== void 0) merged.site = style.site;
|
|
701
|
+
if (style.deploy !== void 0) merged.deploy = style.deploy;
|
|
702
|
+
if (style.mdxComponentsModule !== void 0) {
|
|
703
|
+
merged.mdxComponentsModule = style.mdxComponentsModule;
|
|
704
|
+
}
|
|
705
|
+
if (style.routes !== void 0) {
|
|
706
|
+
merged.routes = {
|
|
707
|
+
...merged.routes ?? {},
|
|
708
|
+
...style.routes
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (style.katexMacros !== void 0) {
|
|
712
|
+
merged.katexMacros = {
|
|
713
|
+
...merged.katexMacros ?? {},
|
|
714
|
+
...style.katexMacros
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
if (style.extra !== void 0) {
|
|
718
|
+
merged.extra = {
|
|
719
|
+
...merged.extra ?? {},
|
|
720
|
+
...style.extra
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (style.extraStyles !== void 0) {
|
|
724
|
+
const prev = merged.extraStyles ?? [];
|
|
725
|
+
merged.extraStyles = [...prev, ...style.extraStyles];
|
|
726
|
+
}
|
|
727
|
+
if (style.extraIntegrations !== void 0) {
|
|
728
|
+
const prev = merged.extraIntegrations ?? [];
|
|
729
|
+
merged.extraIntegrations = [...prev, ...style.extraIntegrations];
|
|
730
|
+
}
|
|
731
|
+
if (style.markdown !== void 0) {
|
|
732
|
+
const prev = merged.markdown ?? void 0;
|
|
733
|
+
merged.markdown = mergeMarkdown(prev, style.markdown);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return defineStyle(merged);
|
|
737
|
+
}
|
|
738
|
+
function mergeMarkdown(a, b) {
|
|
739
|
+
if (!a) return b;
|
|
740
|
+
if (!b) return a;
|
|
741
|
+
return {
|
|
742
|
+
...a,
|
|
743
|
+
...b,
|
|
744
|
+
remarkPlugins: [...a.remarkPlugins ?? [], ...b.remarkPlugins ?? []],
|
|
745
|
+
rehypePlugins: [...a.rehypePlugins ?? [], ...b.rehypePlugins ?? []]
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function normalizeFrontmatterConfig(v) {
|
|
749
|
+
if (v === void 0) return void 0;
|
|
750
|
+
if (typeof v === "boolean") return { enabled: v };
|
|
751
|
+
return v;
|
|
752
|
+
}
|
|
753
|
+
|
|
688
754
|
// src/mdx-components-resolver.ts
|
|
689
755
|
import { existsSync as existsSync2 } from "fs";
|
|
690
756
|
import { resolve } from "path";
|
|
@@ -738,15 +804,32 @@ var ROUTE_REGISTRY = {
|
|
|
738
804
|
// v3.4.0 (#7): consumer-collection-backed frontmatter route. Opt-in via
|
|
739
805
|
// routes: { frontmatter: true } AND content.config.ts defining the
|
|
740
806
|
// collection (use frontmatterCollection() helper from /schemas subpath).
|
|
807
|
+
// v4.0.0 (#49): widened to object form `{ enabled, prefix? }`; pattern
|
|
808
|
+
// computed from `prefix` (default 'frontmatter' → '/frontmatter/[slug]';
|
|
809
|
+
// empty string → '/[slug]'; arbitrary string → '/<prefix>/[slug]').
|
|
741
810
|
frontmatter: { pattern: "/frontmatter/[slug]", file: "frontmatter/[...slug].astro" }
|
|
742
811
|
};
|
|
812
|
+
function frontmatterPatternFromPrefix(prefix) {
|
|
813
|
+
if (prefix === void 0) return ROUTE_REGISTRY.frontmatter.pattern;
|
|
814
|
+
if (prefix === "") return "/[slug]";
|
|
815
|
+
return `/${prefix}/[slug]`;
|
|
816
|
+
}
|
|
743
817
|
function resolvePage(file) {
|
|
744
818
|
return fileURLToPath(new URL(`../pages/${file}`, import.meta.url));
|
|
745
819
|
}
|
|
746
820
|
function bookScaffoldIntegration(opts) {
|
|
747
821
|
const { profile, routes: userOverrides = {}, extraStyles = [], mdxComponentsModule } = opts;
|
|
748
822
|
const def = PROFILES[profile];
|
|
749
|
-
const
|
|
823
|
+
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
824
|
+
const fmEnabled = fmNormalized?.enabled ?? def.routes.frontmatter;
|
|
825
|
+
const fmPrefix = fmNormalized && "prefix" in fmNormalized ? fmNormalized.prefix : void 0;
|
|
826
|
+
const enabledRoutes = {
|
|
827
|
+
...def.routes,
|
|
828
|
+
...Object.fromEntries(
|
|
829
|
+
Object.entries(userOverrides).filter(([k]) => k !== "frontmatter")
|
|
830
|
+
),
|
|
831
|
+
frontmatter: fmEnabled
|
|
832
|
+
};
|
|
750
833
|
return {
|
|
751
834
|
name: "book-scaffold-astro",
|
|
752
835
|
hooks: {
|
|
@@ -762,8 +845,9 @@ function bookScaffoldIntegration(opts) {
|
|
|
762
845
|
if (!on) continue;
|
|
763
846
|
const route = ROUTE_REGISTRY[name];
|
|
764
847
|
if (!route) continue;
|
|
848
|
+
const pattern = name === "frontmatter" ? frontmatterPatternFromPrefix(fmPrefix) : route.pattern;
|
|
765
849
|
injectRoute({
|
|
766
|
-
pattern
|
|
850
|
+
pattern,
|
|
767
851
|
entrypoint: resolvePage(route.file)
|
|
768
852
|
});
|
|
769
853
|
}
|
|
@@ -785,8 +869,62 @@ function bookScaffoldIntegration(opts) {
|
|
|
785
869
|
}
|
|
786
870
|
|
|
787
871
|
// src/config.ts
|
|
872
|
+
function v3MigrationError(opts) {
|
|
873
|
+
const v3Value = opts.preset ?? opts.profile;
|
|
874
|
+
const v3FieldUsed = "preset" in opts ? "preset" : "profile";
|
|
875
|
+
const styleExportName = v3Value && BOOK_PRESETS.includes(v3Value) ? `${v3Value === "research-portfolio" ? "researchPortfolio" : v3Value === "course-notes" ? "courseNotes" : v3Value}Style` : null;
|
|
876
|
+
const knownReplacement = styleExportName ? `
|
|
877
|
+
Replace this:
|
|
878
|
+
defineBookConfig({ ${v3FieldUsed}: ${JSON.stringify(v3Value)}, ... })
|
|
879
|
+
|
|
880
|
+
With this:
|
|
881
|
+
import { defineBookConfig, ${styleExportName} } from '@brandon_m_behring/book-scaffold-astro';
|
|
882
|
+
defineBookConfig({ styles: [${styleExportName}], ... })
|
|
883
|
+
` : `
|
|
884
|
+
Replace the \`${v3FieldUsed}: <value>\` field with a \`styles: [<...Style>]\` array.
|
|
885
|
+
The v4 toolkit exports built-in styles for each preset: academicStyle, toolsStyle,
|
|
886
|
+
minimalStyle, courseNotesStyle, researchPortfolioStyle.
|
|
887
|
+
`;
|
|
888
|
+
return new BookConfigError(
|
|
889
|
+
`book-scaffold-astro v4.0.0 removed the \`${v3FieldUsed}\` field on defineBookConfig.
|
|
890
|
+
${knownReplacement}
|
|
891
|
+
See https://github.com/brandon-behring/book-scaffold-astro/blob/main/package/MIGRATION-v3-to-v4.md
|
|
892
|
+
for the full migration guide. If you hit friction migrating, please file an issue at
|
|
893
|
+
https://github.com/brandon-behring/book-scaffold-astro/issues \u2014 v4 is fresh and the API
|
|
894
|
+
will evolve based on real friction reports.`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
788
897
|
async function defineBookConfig(opts) {
|
|
789
|
-
|
|
898
|
+
if ("preset" in opts || "profile" in opts) {
|
|
899
|
+
throw v3MigrationError(opts);
|
|
900
|
+
}
|
|
901
|
+
const composed = composeStyles(opts.styles ?? []);
|
|
902
|
+
const profile = opts.styles === void 0 && composed.preset === void 0 ? "minimal" : composed.preset ?? "minimal";
|
|
903
|
+
const site = opts.site ?? composed.site;
|
|
904
|
+
if (!site) {
|
|
905
|
+
throw new BookConfigError(
|
|
906
|
+
"book-scaffold-astro v4.0.0: `site` is required. Provide it via the top-level `site` field in defineBookConfig OR via a Style in the `styles` array."
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const mergedRoutes = {
|
|
910
|
+
...composed.routes ?? {},
|
|
911
|
+
...opts.routes ?? {}
|
|
912
|
+
};
|
|
913
|
+
const consumerKatexMacros = {
|
|
914
|
+
...composed.katexMacros ?? {},
|
|
915
|
+
...opts.katexMacros ?? {}
|
|
916
|
+
};
|
|
917
|
+
const mergedExtraStyles = [
|
|
918
|
+
...composed.extraStyles ?? [],
|
|
919
|
+
...opts.extraStyles ?? []
|
|
920
|
+
];
|
|
921
|
+
const mergedExtraIntegrations = [
|
|
922
|
+
...composed.extraIntegrations ?? [],
|
|
923
|
+
...opts.extraIntegrations ?? []
|
|
924
|
+
];
|
|
925
|
+
const mdxComponentsModule = opts.mdxComponentsModule ?? composed.mdxComponentsModule;
|
|
926
|
+
const composedMarkdown = composed.markdown ?? {};
|
|
927
|
+
const userMarkdown = opts.markdown ?? {};
|
|
790
928
|
const wantsKatex = PROFILES[profile]?.katex === true;
|
|
791
929
|
const remarkPlugins = [];
|
|
792
930
|
const rehypePlugins = [];
|
|
@@ -800,14 +938,11 @@ async function defineBookConfig(opts) {
|
|
|
800
938
|
"rehype-katex"
|
|
801
939
|
);
|
|
802
940
|
const { ssmMacros: ssmMacros2 } = await Promise.resolve().then(() => (init_katex_macros(), katex_macros_exports));
|
|
803
|
-
const macros = { ...ssmMacros2, ...
|
|
941
|
+
const macros = { ...ssmMacros2, ...consumerKatexMacros };
|
|
804
942
|
remarkPlugins.push(remarkMath);
|
|
805
943
|
rehypePlugins.push([
|
|
806
944
|
rehypeKatex,
|
|
807
945
|
{
|
|
808
|
-
// Strict mode: build fails on undefined macros, malformed expressions,
|
|
809
|
-
// unsupported AMS environments. Trades developer pain at write-time
|
|
810
|
-
// for catching errors before deploy.
|
|
811
946
|
strict: "error",
|
|
812
947
|
trust: true,
|
|
813
948
|
macros
|
|
@@ -819,45 +954,49 @@ async function defineBookConfig(opts) {
|
|
|
819
954
|
preact(),
|
|
820
955
|
bookScaffoldIntegration({
|
|
821
956
|
profile,
|
|
822
|
-
routes:
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
// v3.3.0 — explicit mdx-components path (issue #2)
|
|
826
|
-
extraStyles: opts.extraStyles
|
|
957
|
+
routes: mergedRoutes,
|
|
958
|
+
mdxComponentsModule,
|
|
959
|
+
extraStyles: mergedExtraStyles
|
|
827
960
|
}),
|
|
828
|
-
...
|
|
961
|
+
...mergedExtraIntegrations
|
|
962
|
+
];
|
|
963
|
+
const finalRemark = [
|
|
964
|
+
...remarkPlugins,
|
|
965
|
+
...composedMarkdown.remarkPlugins ?? [],
|
|
966
|
+
...userMarkdown.remarkPlugins ?? []
|
|
967
|
+
];
|
|
968
|
+
const finalRehype = [
|
|
969
|
+
...rehypePlugins,
|
|
970
|
+
...composedMarkdown.rehypePlugins ?? [],
|
|
971
|
+
...userMarkdown.rehypePlugins ?? []
|
|
829
972
|
];
|
|
830
|
-
const userMarkdown = opts.markdown ?? {};
|
|
831
973
|
const markdown = {
|
|
832
974
|
shikiConfig: {
|
|
833
|
-
// css-variables mode lets code blocks switch dark/light theme without
|
|
834
|
-
// rebuilding. Tokens map to --astro-code-* CSS vars in tokens.css.
|
|
835
975
|
theme: "css-variables",
|
|
836
976
|
wrap: false,
|
|
837
|
-
...userMarkdown.shikiConfig ?? {}
|
|
977
|
+
...userMarkdown.shikiConfig ?? composedMarkdown.shikiConfig ?? {}
|
|
838
978
|
},
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
979
|
+
...composedMarkdown,
|
|
980
|
+
...userMarkdown,
|
|
981
|
+
remarkPlugins: finalRemark,
|
|
982
|
+
rehypePlugins: finalRehype
|
|
842
983
|
};
|
|
843
984
|
const {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
profile: _profile,
|
|
985
|
+
styles: _styles,
|
|
986
|
+
site: _site,
|
|
847
987
|
routes: _routes,
|
|
848
|
-
|
|
988
|
+
deploy: _deploy,
|
|
849
989
|
mdxComponentsModule: _mdxComponentsModule,
|
|
850
|
-
// v3.3.0
|
|
851
990
|
extraIntegrations: _extraIntegrations,
|
|
852
991
|
extraStyles: _extraStyles,
|
|
853
992
|
markdown: _markdown,
|
|
854
993
|
katexMacros: _katexMacros,
|
|
855
|
-
// v3.6.0 (closes #22)
|
|
856
994
|
...rest
|
|
857
995
|
} = opts;
|
|
858
|
-
void
|
|
859
|
-
void
|
|
996
|
+
void _styles;
|
|
997
|
+
void _site;
|
|
860
998
|
void _routes;
|
|
999
|
+
void _deploy;
|
|
861
1000
|
void _mdxComponentsModule;
|
|
862
1001
|
void _extraIntegrations;
|
|
863
1002
|
void _extraStyles;
|
|
@@ -865,6 +1004,7 @@ async function defineBookConfig(opts) {
|
|
|
865
1004
|
void _katexMacros;
|
|
866
1005
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
867
1006
|
const config = {
|
|
1007
|
+
site,
|
|
868
1008
|
...rest,
|
|
869
1009
|
integrations,
|
|
870
1010
|
markdown,
|
|
@@ -895,29 +1035,72 @@ function chapterSortKey(data) {
|
|
|
895
1035
|
const within = typeof data.chapter === "number" ? data.chapter : typeof data.week === "number" ? data.week : 0;
|
|
896
1036
|
return partOrdinal * 1e3 + within;
|
|
897
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
// src/styles/built-in.ts
|
|
1040
|
+
var academicStyle = defineStyle({
|
|
1041
|
+
name: "academic",
|
|
1042
|
+
preset: "academic",
|
|
1043
|
+
deploy: "workers"
|
|
1044
|
+
});
|
|
1045
|
+
var toolsStyle = defineStyle({
|
|
1046
|
+
name: "tools",
|
|
1047
|
+
preset: "tools",
|
|
1048
|
+
deploy: "workers"
|
|
1049
|
+
});
|
|
1050
|
+
var minimalStyle = defineStyle({
|
|
1051
|
+
name: "minimal",
|
|
1052
|
+
preset: "minimal",
|
|
1053
|
+
deploy: "workers"
|
|
1054
|
+
});
|
|
1055
|
+
var courseNotesStyle = defineStyle({
|
|
1056
|
+
name: "course-notes",
|
|
1057
|
+
preset: "course-notes",
|
|
1058
|
+
deploy: "pages"
|
|
1059
|
+
});
|
|
1060
|
+
var researchPortfolioStyle = defineStyle({
|
|
1061
|
+
name: "research-portfolio",
|
|
1062
|
+
preset: "research-portfolio",
|
|
1063
|
+
deploy: "pages",
|
|
1064
|
+
routes: { frontmatter: { enabled: true, prefix: "frontmatter" } }
|
|
1065
|
+
});
|
|
1066
|
+
var BUILTIN_STYLES = {
|
|
1067
|
+
academic: academicStyle,
|
|
1068
|
+
tools: toolsStyle,
|
|
1069
|
+
minimal: minimalStyle,
|
|
1070
|
+
"course-notes": courseNotesStyle,
|
|
1071
|
+
"research-portfolio": researchPortfolioStyle
|
|
1072
|
+
};
|
|
898
1073
|
export {
|
|
899
1074
|
BOOK_PRESETS,
|
|
900
1075
|
BOOK_PROFILES,
|
|
1076
|
+
BUILTIN_STYLES,
|
|
901
1077
|
BookConfigError,
|
|
902
1078
|
academicChapterSchema,
|
|
903
1079
|
academicChaptersRenderer,
|
|
904
1080
|
academicParts,
|
|
1081
|
+
academicStyle,
|
|
905
1082
|
bookScaffoldIntegration,
|
|
906
1083
|
changeKinds,
|
|
907
1084
|
changelogSchema,
|
|
908
1085
|
chapterSortKey,
|
|
909
1086
|
chapterStatus,
|
|
1087
|
+
composeStyles,
|
|
910
1088
|
courseNotesChapterSchema,
|
|
1089
|
+
courseNotesStyle,
|
|
911
1090
|
defineBookConfig,
|
|
912
1091
|
defineMdxComponents,
|
|
913
1092
|
defineProfile,
|
|
1093
|
+
defineStyle,
|
|
914
1094
|
fallbackChaptersRenderer,
|
|
915
1095
|
freshnessLabel,
|
|
916
1096
|
getFreshness,
|
|
917
1097
|
minimalChapterSchema,
|
|
1098
|
+
minimalStyle,
|
|
1099
|
+
normalizeFrontmatterConfig,
|
|
918
1100
|
patternCategories,
|
|
919
1101
|
patternsSchema,
|
|
920
1102
|
researchPortfolioChapterSchema,
|
|
1103
|
+
researchPortfolioStyle,
|
|
921
1104
|
resolvePreset,
|
|
922
1105
|
resolveProfile,
|
|
923
1106
|
sourceTiers,
|
|
@@ -926,5 +1109,6 @@ export {
|
|
|
926
1109
|
toolSlugs,
|
|
927
1110
|
toolsChapterSchema,
|
|
928
1111
|
toolsChaptersRenderer,
|
|
1112
|
+
toolsStyle,
|
|
929
1113
|
volatilityLevels
|
|
930
1114
|
};
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/schemas-entry.ts
|
|
2
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3
3
|
import { defineCollection } from "astro:content";
|
|
4
4
|
import { glob, file } from "astro/loaders";
|
|
5
5
|
|
|
@@ -565,6 +565,15 @@ function resolvePreset(explicitPreset, explicitProfile) {
|
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
// src/schemas-entry.ts
|
|
568
|
+
function isYamlEmpty(path) {
|
|
569
|
+
try {
|
|
570
|
+
const raw = readFileSync2(path, "utf8");
|
|
571
|
+
const stripped = raw.split(/\r?\n/).map((line) => line.replace(/#.*$/, "").trim()).filter((line) => line.length > 0).join("");
|
|
572
|
+
return stripped === "" || stripped === "[]";
|
|
573
|
+
} catch {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
568
577
|
function frontmatterCollection(schema, base = "./src/content/frontmatter") {
|
|
569
578
|
return defineCollection({
|
|
570
579
|
loader: glob({
|
|
@@ -589,7 +598,7 @@ function defineBookSchemas(opts = {}) {
|
|
|
589
598
|
const collections = {
|
|
590
599
|
chapters
|
|
591
600
|
};
|
|
592
|
-
if (existsSync2("./sources/manifest.yaml")) {
|
|
601
|
+
if (existsSync2("./sources/manifest.yaml") && !isYamlEmpty("./sources/manifest.yaml")) {
|
|
593
602
|
collections.sources = defineCollection({
|
|
594
603
|
loader: file("sources/manifest.yaml"),
|
|
595
604
|
schema: sourcesSchema
|
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
|
+
"version": "4.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
"./components/OpenQuestion.astro": "./components/OpenQuestion.astro",
|
|
66
66
|
"./components/PaperBox.astro": "./components/PaperBox.astro",
|
|
67
67
|
"./components/PatternTimeline.astro": "./components/PatternTimeline.astro",
|
|
68
|
+
"./components/Pitfall.astro": "./components/Pitfall.astro",
|
|
69
|
+
"./components/PocLayout.astro": "./components/PocLayout.astro",
|
|
68
70
|
"./components/PolicyRef.astro": "./components/PolicyRef.astro",
|
|
69
71
|
"./components/PreReleaseBanner.astro": "./components/PreReleaseBanner.astro",
|
|
70
72
|
"./components/Recovery.astro": "./components/Recovery.astro",
|
|
@@ -88,7 +90,9 @@
|
|
|
88
90
|
},
|
|
89
91
|
"./components/WarnBox.astro": "./components/WarnBox.astro",
|
|
90
92
|
"./components/WeekRef.astro": "./components/WeekRef.astro",
|
|
93
|
+
"./components/WorkedExample.astro": "./components/WorkedExample.astro",
|
|
91
94
|
"./components/XRef.astro": "./components/XRef.astro",
|
|
95
|
+
"./components/YouWillLearn.astro": "./components/YouWillLearn.astro",
|
|
92
96
|
"./styles/tokens.css": "./styles/tokens.css",
|
|
93
97
|
"./styles/layout.css": "./styles/layout.css",
|
|
94
98
|
"./styles/callouts.css": "./styles/callouts.css",
|
|
@@ -96,6 +100,7 @@
|
|
|
96
100
|
"./styles/typography.css": "./styles/typography.css",
|
|
97
101
|
"./styles/print.css": "./styles/print.css",
|
|
98
102
|
"./styles/convergence.css": "./styles/convergence.css",
|
|
103
|
+
"./styles/poc-layouts.css": "./styles/poc-layouts.css",
|
|
99
104
|
"./styles/tool-filter.css": "./styles/tool-filter.css",
|
|
100
105
|
"./layouts/Base.astro": "./layouts/Base.astro",
|
|
101
106
|
"./layouts/Chapter.astro": "./layouts/Chapter.astro",
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Recipe 15 — Defining and composing Styles (v4.0.0+)
|
|
2
|
+
|
|
3
|
+
A **Style** is a typed, named, importable config bundle. Define a style once; import it into many books; override per-book explicitly.
|
|
4
|
+
|
|
5
|
+
This recipe replaces the v3 `preset: 'X'` shorthand with explicit composition. See `MIGRATION-v3-to-v4.md` for the migration steps.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## TL;DR
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// shared/styles/research-guide.ts
|
|
13
|
+
import { defineStyle, researchPortfolioStyle } from '@brandon_m_behring/book-scaffold-astro';
|
|
14
|
+
|
|
15
|
+
export const researchGuideStyle = defineStyle({
|
|
16
|
+
name: 'research-guide',
|
|
17
|
+
site: 'https://guides.brandon-behring.dev/',
|
|
18
|
+
routes: { frontmatter: { enabled: true, prefix: '' } },
|
|
19
|
+
// Composes naturally: styles: [researchPortfolioStyle, researchGuideStyle]
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// guides/foo/astro.config.mjs
|
|
23
|
+
import { defineBookConfig, researchPortfolioStyle } from '@brandon_m_behring/book-scaffold-astro';
|
|
24
|
+
import { researchGuideStyle } from '../shared/styles/research-guide.js';
|
|
25
|
+
|
|
26
|
+
export default await defineBookConfig({
|
|
27
|
+
styles: [researchPortfolioStyle, researchGuideStyle],
|
|
28
|
+
// any per-book overrides here
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## What is a Style
|
|
35
|
+
|
|
36
|
+
A Style is an object containing config values, branded for type safety. The full type is documented in JSDoc on `defineStyle()`. All fields are optional:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
defineStyle({
|
|
40
|
+
name?: string; // for debug/error messages; optional
|
|
41
|
+
preset?: 'academic' | 'tools' | ...; // determines schema + default routes + styles
|
|
42
|
+
site?: string;
|
|
43
|
+
routes?: PartialRouteToggles; // per-route override (frontmatter widened to object form)
|
|
44
|
+
katexMacros?: Record<string, string>;
|
|
45
|
+
extraStyles?: readonly string[];
|
|
46
|
+
extraIntegrations?: readonly AstroIntegration[];
|
|
47
|
+
mdxComponentsModule?: string;
|
|
48
|
+
markdown?: AstroUserConfig['markdown'];
|
|
49
|
+
deploy?: 'pages' | 'workers'; // v4.0.0 NEW (#50)
|
|
50
|
+
extra?: Record<string, unknown>; // scoped consumer-side metadata
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Pattern A: workspace-local style
|
|
57
|
+
|
|
58
|
+
Use when you have many books in a workspace + a style cluster you don't yet need to publish externally.
|
|
59
|
+
|
|
60
|
+
**File**: `shared/styles/research-guide.ts` (or any path in your workspace)
|
|
61
|
+
|
|
62
|
+
**Import**: relative path from each consuming book.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { researchGuideStyle } from '../../shared/styles/research-guide.js';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Versioning**: git. The style is just code in your repo; edit it and rebuild downstream books.
|
|
69
|
+
|
|
70
|
+
**Pros**: zero ceremony, co-located with the books that use it, no npm publish.
|
|
71
|
+
|
|
72
|
+
**Cons**: doesn't help OTHER consumers (outside your workspace) reuse the style.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Pattern B: separate npm package
|
|
77
|
+
|
|
78
|
+
Use when a style stabilizes + has interest beyond your workspace.
|
|
79
|
+
|
|
80
|
+
**Package**: e.g., `@brandon_m_behring/style-research-guides` (any name).
|
|
81
|
+
|
|
82
|
+
**Publish**: standard `npm publish`. Versioning via semver; consumers pin to `^1.0.0`.
|
|
83
|
+
|
|
84
|
+
**Import**: package name in each consuming book.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { researchGuideStyle } from '@brandon_m_behring/style-research-guides';
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Pros**: cleanest cross-consumer sharing; semver versioning out-of-the-box.
|
|
91
|
+
|
|
92
|
+
**Cons**: heavyweight for a workspace-internal style; publishing overhead per release.
|
|
93
|
+
|
|
94
|
+
**Promotion path**: start every style as workspace-local (Pattern A). When a style stabilizes + an external consumer asks for it, promote to npm — only the import path changes; the Style object itself is unchanged.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Composition: `styles: [...]` array
|
|
99
|
+
|
|
100
|
+
Multiple styles compose left-to-right. Later styles override earlier ones for conflicts.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
defineBookConfig({
|
|
104
|
+
styles: [baseStyle, brandStyle, projectStyle],
|
|
105
|
+
// top-level fields here override anything from the styles
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Precedence (highest last)**:
|
|
110
|
+
1. Built-in style's defaults (e.g., `academicStyle.routes`)
|
|
111
|
+
2. `styles[0]`
|
|
112
|
+
3. `styles[1]`
|
|
113
|
+
4. ...
|
|
114
|
+
5. `styles[N]`
|
|
115
|
+
6. Top-level `defineBookConfig` fields
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Per-key merge strategy
|
|
120
|
+
|
|
121
|
+
Different fields have different merge semantics. Documented:
|
|
122
|
+
|
|
123
|
+
| Field | Strategy |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `name`, `preset`, `site`, `deploy`, `mdxComponentsModule` | Shallow override (last wins) |
|
|
126
|
+
| `routes` | Per-route spread (each route key independently overridable) |
|
|
127
|
+
| `routes.frontmatter` | Per-route spread; later value (boolean OR object) wholly replaces earlier |
|
|
128
|
+
| `katexMacros` | Object spread (per-macro override) |
|
|
129
|
+
| `extra` | Object spread (per-key consumer-metadata override) |
|
|
130
|
+
| `extraStyles` | Array concat (additive — no dedup) |
|
|
131
|
+
| `extraIntegrations` | Array concat (additive) |
|
|
132
|
+
| `markdown.remarkPlugins` | Array concat (additive) |
|
|
133
|
+
| `markdown.rehypePlugins` | Array concat (additive) |
|
|
134
|
+
|
|
135
|
+
**Why arrays concat (not dedupe)**: matches Tailwind plugin arrays + ESLint flat config rules — one mental model: "arrays concat, non-arrays last-wins." If you compose styles that both list the same CSS file, you get it twice (browser dedups at parse time; benign in practice). If real consumer pain surfaces, we'll add a `dedupe: true` opt-in.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Built-in styles
|
|
140
|
+
|
|
141
|
+
The toolkit ships one style per preset. Import individually or via the registry:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import {
|
|
145
|
+
academicStyle,
|
|
146
|
+
toolsStyle,
|
|
147
|
+
minimalStyle,
|
|
148
|
+
courseNotesStyle,
|
|
149
|
+
researchPortfolioStyle,
|
|
150
|
+
BUILTIN_STYLES,
|
|
151
|
+
} from '@brandon_m_behring/book-scaffold-astro';
|
|
152
|
+
|
|
153
|
+
// Direct import:
|
|
154
|
+
defineBookConfig({ styles: [researchPortfolioStyle], ... });
|
|
155
|
+
|
|
156
|
+
// Or via registry (useful for dynamic dispatch):
|
|
157
|
+
defineBookConfig({ styles: [BUILTIN_STYLES['research-portfolio']], ... });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each built-in style has a `name` matching its preset, a `preset` field, and sane `deploy` defaults (academic/tools/minimal → 'workers'; course-notes/research-portfolio → 'pages').
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Escape hatch: consumer-side metadata via `extra`
|
|
165
|
+
|
|
166
|
+
Fields the toolkit knows about must be typed. For workflow-specific metadata that should travel with the style but isn't toolkit config, use the scoped `extra` field:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
defineStyle({
|
|
170
|
+
name: 'guides-v0.2',
|
|
171
|
+
preset: 'research-portfolio',
|
|
172
|
+
extra: {
|
|
173
|
+
pedagogyTier: 'experimental',
|
|
174
|
+
team: 'engineering',
|
|
175
|
+
docsVersion: '0.2',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- `extra` survives composition as per-key spread (later entries override earlier per key).
|
|
181
|
+
- The toolkit ignores `extra` entirely — it's for YOUR tooling (style linters, CI scripts, custom Astro integrations that read `style.extra.X`, etc.).
|
|
182
|
+
- This pattern preserves typo protection on known fields: `defineStyle({ presset: 'academic' })` errors at compile time because `presset` isn't `preset` and isn't `extra`.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Forward compatibility (`__styleVersion`)
|
|
187
|
+
|
|
188
|
+
Every Style carries a `__styleVersion: 1` marker (set automatically by `defineStyle()`). Future API-shape changes can detect old Style objects and apply version-appropriate handling.
|
|
189
|
+
|
|
190
|
+
**You don't set it.** It's auto-applied. Don't read it in consumer code.
|
|
191
|
+
|
|
192
|
+
When v5 ships (whenever that is), `__styleVersion: 1` styles continue to work via internal adaptation. New Style features in v5+ may bump this marker.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Feedback loop
|
|
197
|
+
|
|
198
|
+
v4 is fresh. The `defineStyle` API will evolve based on real friction reports.
|
|
199
|
+
|
|
200
|
+
**If you hit a wall** — composition pattern that doesn't compose, merge semantic that surprises you, type that won't infer, missing field — **file an issue at https://github.com/brandon-behring/book-scaffold-astro/issues** with:
|
|
201
|
+
|
|
202
|
+
- The `consumer:<your-workspace>` label (so we can batch reports from the same consumer)
|
|
203
|
+
- A minimal reproduction showing what you were trying to express
|
|
204
|
+
- What got in the way
|
|
205
|
+
|
|
206
|
+
The v4.x release line is explicitly the iteration window for this API. Use it.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## See also
|
|
211
|
+
|
|
212
|
+
- `MIGRATION-v3-to-v4.md` — step-by-step migration from v3 `preset:` shorthand
|
|
213
|
+
- `PACKAGE_DESIGN.md §4` — `defineBookConfig` API reference
|
|
214
|
+
- `PACKAGE_DESIGN.md §4a` — `defineStyle` API reference
|
|
215
|
+
- `recipes/12-where-to-file-issues.md` — the broader consumer-driven evolution loop this fits into
|
package/scripts/build-bib.mjs
CHANGED
|
@@ -83,7 +83,22 @@ async function main() {
|
|
|
83
83
|
}
|
|
84
84
|
throw err;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
// v4.0.0 (closes #54): strip `%`-comment lines before passing to citation-js.
|
|
87
|
+
// The plugin-bibtex lexer doesn't honor BibTeX's %-line-comment semantics —
|
|
88
|
+
// any `@TYPE` token inside a commented block is consumed as an entry start,
|
|
89
|
+
// then fails at the first real entry below. This caused 4 hotfix releases
|
|
90
|
+
// in v3.6.1→v3.6.4 chasing the same parse quirk; the pre-pass eliminates the
|
|
91
|
+
// entire class of bug.
|
|
92
|
+
//
|
|
93
|
+
// BibTeX's real comment grammar is "% at the start of a line (after optional
|
|
94
|
+
// whitespace) to end of line". Mid-line `%` (e.g., `note = {50% confidence}`)
|
|
95
|
+
// is NOT a comment and is preserved.
|
|
96
|
+
const sanitizedBib = bibText
|
|
97
|
+
.split(/\r?\n/)
|
|
98
|
+
.map((line) => (line.trimStart().startsWith('%') ? '' : line))
|
|
99
|
+
.join('\n');
|
|
100
|
+
|
|
101
|
+
const cite = new Cite(sanitizedBib);
|
|
87
102
|
const data = cite.data;
|
|
88
103
|
|
|
89
104
|
// Detect duplicates the way biber would (citation-js silently
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/define-style.ts — `defineStyle` API + composition (v4.0.0).
|
|
3
|
+
*
|
|
4
|
+
* Closes #35 followup architecture work. A `Style` is a typed, named,
|
|
5
|
+
* importable config bundle that consumers can compose across many books
|
|
6
|
+
* in the same cluster. Replaces the v3.x `preset` field as the primary
|
|
7
|
+
* way to configure a book.
|
|
8
|
+
*
|
|
9
|
+
* Design principles (D6 + D12 from the v4.0.0 plan):
|
|
10
|
+
* - Pure data (no Astro virtual modules) — safe for tsup's DTS bundle,
|
|
11
|
+
* same constraint as src/lib/chapter-sort.ts (v3.5.2).
|
|
12
|
+
* - Every field optional — composition fills gaps; no required fields
|
|
13
|
+
* means new fields are always additive.
|
|
14
|
+
* - Branded type — prevents confusion with plain Partial<BookConfigOptions>
|
|
15
|
+
* even though they're structurally close.
|
|
16
|
+
* - Closed shape for known fields (no `[key: string]: unknown` index
|
|
17
|
+
* signature) — catches typos at compile time (the v3 `convergance`
|
|
18
|
+
* lesson, see PR #9 v3.4.0).
|
|
19
|
+
* - Scoped escape hatch via `extra?: Record<string, unknown>` — consumer-
|
|
20
|
+
* side metadata can travel with the style without breaking typo
|
|
21
|
+
* protection on toolkit-known fields.
|
|
22
|
+
* - Readonly throughout — Style objects are immutable DTOs.
|
|
23
|
+
* - Version marker (`__styleVersion`) — future API-shape changes can
|
|
24
|
+
* be detected at composition time without breaking existing styles.
|
|
25
|
+
*
|
|
26
|
+
* See `recipes/15-defining-styles.md` for usage patterns + MIGRATION-v3-to-v4.md
|
|
27
|
+
* for migration from the v3 `preset:` shorthand.
|
|
28
|
+
*/
|
|
29
|
+
import type { AstroIntegration, AstroUserConfig } from 'astro';
|
|
30
|
+
import type { BookPreset, RouteToggles } from '../types.js';
|
|
31
|
+
|
|
32
|
+
// ===== Branded nominal type =====
|
|
33
|
+
|
|
34
|
+
/** Internal brand symbol — prevents confusion between Style and plain config objects.
|
|
35
|
+
* TYPE-ONLY: `declare const` means there's no runtime value. The brand exists
|
|
36
|
+
* purely at compile time as a structural guard: TypeScript will reject a plain
|
|
37
|
+
* `Partial<BookConfigOptions>` where a `Style` is expected, because the brand
|
|
38
|
+
* key isn't present in the plain object's type. At runtime, Style is just an
|
|
39
|
+
* object with `__styleVersion: 1` — no Symbol keys to enumerate or test for. */
|
|
40
|
+
declare const StyleBrand: unique symbol;
|
|
41
|
+
|
|
42
|
+
// ===== Widened route toggles =====
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Widened form of the frontmatter route config (closes #49).
|
|
46
|
+
* Pre-v4 was just `boolean`; v4 supports an object form with `prefix` so
|
|
47
|
+
* consumers can mount frontmatter pages at root (`prefix: ''` → `/[slug]`)
|
|
48
|
+
* or under a custom prefix (`prefix: 'pages'` → `/pages/[slug]`).
|
|
49
|
+
*
|
|
50
|
+
* Default prefix (when unset OR when boolean `true` is used): `'frontmatter'`,
|
|
51
|
+
* matching the v3 behavior.
|
|
52
|
+
*/
|
|
53
|
+
export type FrontmatterRouteConfig =
|
|
54
|
+
| boolean
|
|
55
|
+
| {
|
|
56
|
+
readonly enabled: boolean;
|
|
57
|
+
readonly prefix?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Partial route toggles for use inside Style. Same shape as `Partial<RouteToggles>`
|
|
62
|
+
* except `frontmatter` is widened to `FrontmatterRouteConfig` (closes #49).
|
|
63
|
+
*/
|
|
64
|
+
export type PartialRouteToggles = Partial<Omit<RouteToggles, 'frontmatter'>> & {
|
|
65
|
+
readonly frontmatter?: FrontmatterRouteConfig;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ===== Style type =====
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A typed config bundle. Composed via `styles: [...]` in `defineBookConfig`.
|
|
72
|
+
*
|
|
73
|
+
* All fields are optional and immutable. Use `defineStyle()` to create a
|
|
74
|
+
* Style with proper branding + version marker.
|
|
75
|
+
*
|
|
76
|
+
* For consumer-side metadata that should travel with the style but isn't
|
|
77
|
+
* toolkit configuration, use the scoped `extra` field rather than adding
|
|
78
|
+
* unknown top-level fields (which the closed shape rejects).
|
|
79
|
+
*/
|
|
80
|
+
export interface Style {
|
|
81
|
+
/** @internal Brand for nominal typing. Set by defineStyle(); not observable. */
|
|
82
|
+
readonly [StyleBrand]: true;
|
|
83
|
+
/** @internal Version marker for future API-shape evolution. Set by defineStyle(). */
|
|
84
|
+
readonly __styleVersion: 1;
|
|
85
|
+
|
|
86
|
+
/** Optional human-readable name; surfaces in debug output and error messages.
|
|
87
|
+
* Anonymous styles fall back to their index in the composition chain. */
|
|
88
|
+
readonly name?: string;
|
|
89
|
+
|
|
90
|
+
/** Profile that backs this style — determines schema + default routes + styles + KaTeX wiring. */
|
|
91
|
+
readonly preset?: BookPreset;
|
|
92
|
+
|
|
93
|
+
/** Book's deployed origin (sitemap, canonical, Pagefind). Required at composition end;
|
|
94
|
+
* optional inside a Style (so styles can omit it and consumers can provide per-book). */
|
|
95
|
+
readonly site?: string;
|
|
96
|
+
|
|
97
|
+
/** Per-route override; merges per-key across the style chain. */
|
|
98
|
+
readonly routes?: PartialRouteToggles;
|
|
99
|
+
|
|
100
|
+
/** Consumer-defined KaTeX macros, shallow-merged per macro across the style chain.
|
|
101
|
+
* Closes #22 (v3.6.0); same merge semantics. */
|
|
102
|
+
readonly katexMacros?: Readonly<Record<string, string>>;
|
|
103
|
+
|
|
104
|
+
/** CSS basenames injected in addition to the profile-resolved set.
|
|
105
|
+
* Composed via array concat (additive). */
|
|
106
|
+
readonly extraStyles?: readonly string[];
|
|
107
|
+
|
|
108
|
+
/** Appended to the package-provided integration list.
|
|
109
|
+
* Composed via array concat (additive). */
|
|
110
|
+
readonly extraIntegrations?: readonly AstroIntegration[];
|
|
111
|
+
|
|
112
|
+
/** Explicit path to consumer's mdx-components map (relative to project root).
|
|
113
|
+
* Last non-undefined value wins across the chain. */
|
|
114
|
+
readonly mdxComponentsModule?: string;
|
|
115
|
+
|
|
116
|
+
/** Spread-merged into the package-provided markdown config.
|
|
117
|
+
* `remarkPlugins` and `rehypePlugins` arrays concat across the chain;
|
|
118
|
+
* scalar fields override. */
|
|
119
|
+
readonly markdown?: AstroUserConfig['markdown'];
|
|
120
|
+
|
|
121
|
+
/** Deploy target — drives create-book's wrangler.toml shape.
|
|
122
|
+
* - `'workers'`: Cloudflare Workers + Static Assets (default for academic/tools/minimal)
|
|
123
|
+
* - `'pages'`: Cloudflare Pages (default for research-portfolio/course-notes)
|
|
124
|
+
* Closes #50. */
|
|
125
|
+
readonly deploy?: 'pages' | 'workers';
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scoped consumer-side metadata. Ignored by the toolkit; survives composition
|
|
129
|
+
* as per-key spread (last wins per key). Use this for workflow data that
|
|
130
|
+
* should travel with the style but isn't toolkit config.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* defineStyle({
|
|
134
|
+
* name: 'guides-family',
|
|
135
|
+
* preset: 'research-portfolio',
|
|
136
|
+
* extra: { pedagogyTier: 'experimental', team: 'engineering' },
|
|
137
|
+
* })
|
|
138
|
+
*/
|
|
139
|
+
readonly extra?: Readonly<Record<string, unknown>>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Input type for `defineStyle()`: same as Style, minus the internal brand + version fields. */
|
|
143
|
+
export type StyleInput = Omit<Style, typeof StyleBrand | '__styleVersion'>;
|
|
144
|
+
|
|
145
|
+
// ===== defineStyle helper =====
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a Style — a typed, named, importable config bundle.
|
|
149
|
+
*
|
|
150
|
+
* Identity function with auto-applied brand + version marker. Zero runtime
|
|
151
|
+
* overhead beyond object spread.
|
|
152
|
+
*
|
|
153
|
+
* @example workspace-local style
|
|
154
|
+
* // shared/styles/guides-family.ts
|
|
155
|
+
* import { defineStyle } from '@brandon_m_behring/book-scaffold-astro';
|
|
156
|
+
* export const guidesFamilyStyle = defineStyle({
|
|
157
|
+
* name: 'guides-family',
|
|
158
|
+
* preset: 'research-portfolio',
|
|
159
|
+
* site: 'https://guides.brandon-behring.dev/',
|
|
160
|
+
* routes: { frontmatter: { enabled: true, prefix: '' } },
|
|
161
|
+
* deploy: 'pages',
|
|
162
|
+
* });
|
|
163
|
+
*
|
|
164
|
+
* // guides/foo/astro.config.mjs
|
|
165
|
+
* import { defineBookConfig } from '@brandon_m_behring/book-scaffold-astro';
|
|
166
|
+
* import { guidesFamilyStyle } from '../shared/styles/guides-family';
|
|
167
|
+
* export default await defineBookConfig({
|
|
168
|
+
* styles: [guidesFamilyStyle],
|
|
169
|
+
* site: 'https://foo.guides.brandon-behring.dev/', // overrides style's site
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* @example built-in style composition
|
|
173
|
+
* import { defineBookConfig, researchPortfolioStyle } from '@brandon_m_behring/book-scaffold-astro';
|
|
174
|
+
* export default await defineBookConfig({
|
|
175
|
+
* styles: [researchPortfolioStyle],
|
|
176
|
+
* site: 'https://my-book.example/',
|
|
177
|
+
* });
|
|
178
|
+
*
|
|
179
|
+
* See `recipes/15-defining-styles.md` for the full pattern catalog.
|
|
180
|
+
*/
|
|
181
|
+
export function defineStyle(opts: StyleInput): Style {
|
|
182
|
+
return { __styleVersion: 1, ...opts } as Style;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ===== composeStyles =====
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Merge an array of Styles into a single resolved Style.
|
|
189
|
+
*
|
|
190
|
+
* Composition order (precedence ascending — last wins for conflicts):
|
|
191
|
+
* - Earlier styles in the array set defaults
|
|
192
|
+
* - Later styles override earlier
|
|
193
|
+
* - Top-level `defineBookConfig` fields beat any style (handled in config.ts)
|
|
194
|
+
*
|
|
195
|
+
* Per-key merge strategy:
|
|
196
|
+
* - `preset`, `site`, `deploy`, `mdxComponentsModule`, `name` → shallow override (last wins)
|
|
197
|
+
* - `routes` → per-route spread (each route key independently overridable)
|
|
198
|
+
* - `katexMacros` → per-macro spread (each macro key independently overridable)
|
|
199
|
+
* - `extra` → per-key spread (consumer metadata accumulates across the chain)
|
|
200
|
+
* - `extraStyles`, `extraIntegrations` → array concat (additive; no dedup)
|
|
201
|
+
* - `markdown` → spread, with `remarkPlugins` and `rehypePlugins` arrays concat
|
|
202
|
+
*
|
|
203
|
+
* @returns a fully-typed Style representing the composed configuration.
|
|
204
|
+
* Empty input returns an empty Style with no fields set (all undefined).
|
|
205
|
+
*/
|
|
206
|
+
export function composeStyles(styles: readonly Style[]): Style {
|
|
207
|
+
if (styles.length === 0) {
|
|
208
|
+
return defineStyle({});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const merged: Record<string, unknown> = {};
|
|
212
|
+
|
|
213
|
+
for (const style of styles) {
|
|
214
|
+
// Shallow override for primitives + readonly-scalar fields.
|
|
215
|
+
if (style.name !== undefined) merged.name = style.name;
|
|
216
|
+
if (style.preset !== undefined) merged.preset = style.preset;
|
|
217
|
+
if (style.site !== undefined) merged.site = style.site;
|
|
218
|
+
if (style.deploy !== undefined) merged.deploy = style.deploy;
|
|
219
|
+
if (style.mdxComponentsModule !== undefined) {
|
|
220
|
+
merged.mdxComponentsModule = style.mdxComponentsModule;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Per-route spread.
|
|
224
|
+
if (style.routes !== undefined) {
|
|
225
|
+
merged.routes = {
|
|
226
|
+
...((merged.routes as PartialRouteToggles | undefined) ?? {}),
|
|
227
|
+
...style.routes,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Per-macro spread.
|
|
232
|
+
if (style.katexMacros !== undefined) {
|
|
233
|
+
merged.katexMacros = {
|
|
234
|
+
...((merged.katexMacros as Record<string, string> | undefined) ?? {}),
|
|
235
|
+
...style.katexMacros,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Per-key spread for consumer metadata.
|
|
240
|
+
if (style.extra !== undefined) {
|
|
241
|
+
merged.extra = {
|
|
242
|
+
...((merged.extra as Record<string, unknown> | undefined) ?? {}),
|
|
243
|
+
...style.extra,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Array concat for additive fields.
|
|
248
|
+
if (style.extraStyles !== undefined) {
|
|
249
|
+
const prev = (merged.extraStyles as readonly string[] | undefined) ?? [];
|
|
250
|
+
merged.extraStyles = [...prev, ...style.extraStyles];
|
|
251
|
+
}
|
|
252
|
+
if (style.extraIntegrations !== undefined) {
|
|
253
|
+
const prev =
|
|
254
|
+
(merged.extraIntegrations as readonly AstroIntegration[] | undefined) ?? [];
|
|
255
|
+
merged.extraIntegrations = [...prev, ...style.extraIntegrations];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// markdown: spread with array-concat for the two plugin arrays.
|
|
259
|
+
if (style.markdown !== undefined) {
|
|
260
|
+
const prev = (merged.markdown as AstroUserConfig['markdown'] | undefined) ?? undefined;
|
|
261
|
+
merged.markdown = mergeMarkdown(prev, style.markdown);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return defineStyle(merged as StyleInput);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Merge two `markdown` config blocks. Scalar fields override (last wins);
|
|
270
|
+
* `remarkPlugins` and `rehypePlugins` arrays concat (additive — same
|
|
271
|
+
* semantics as `extraStyles` / `extraIntegrations`).
|
|
272
|
+
*/
|
|
273
|
+
function mergeMarkdown(
|
|
274
|
+
a: AstroUserConfig['markdown'] | undefined,
|
|
275
|
+
b: AstroUserConfig['markdown'] | undefined,
|
|
276
|
+
): AstroUserConfig['markdown'] {
|
|
277
|
+
if (!a) return b;
|
|
278
|
+
if (!b) return a;
|
|
279
|
+
return {
|
|
280
|
+
...a,
|
|
281
|
+
...b,
|
|
282
|
+
remarkPlugins: [...(a.remarkPlugins ?? []), ...(b.remarkPlugins ?? [])],
|
|
283
|
+
rehypePlugins: [...(a.rehypePlugins ?? []), ...(b.rehypePlugins ?? [])],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ===== Normalization helpers =====
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Normalize the widened `routes.frontmatter` value to its object form for
|
|
291
|
+
* downstream use. `true` → `{ enabled: true }`, `false` → `{ enabled: false }`,
|
|
292
|
+
* object → returned as-is.
|
|
293
|
+
*
|
|
294
|
+
* Used by the integration when computing the route URL pattern from the prefix.
|
|
295
|
+
*/
|
|
296
|
+
export function normalizeFrontmatterConfig(
|
|
297
|
+
v: FrontmatterRouteConfig | undefined,
|
|
298
|
+
): { enabled: boolean; prefix?: string } | undefined {
|
|
299
|
+
if (v === undefined) return undefined;
|
|
300
|
+
if (typeof v === 'boolean') return { enabled: v };
|
|
301
|
+
return v;
|
|
302
|
+
}
|
package/styles/callouts.css
CHANGED
|
@@ -162,6 +162,54 @@
|
|
|
162
162
|
background: var(--color-bg-subtle);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/* Crimson — Pitfall (v4.1.0, #58). Distinct from warn-rose: deeper, more
|
|
166
|
+
* saturated. Used for "common mistake" / retrospective error patterns. */
|
|
167
|
+
.callout-pitfall {
|
|
168
|
+
border-left-color: var(--callout-pitfall);
|
|
169
|
+
background: var(--warm-crimson-tint);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Plum-dashed — WorkedExample (v4.1.0, #57). Reuses callout-official
|
|
173
|
+
* (plum) bar; <details>/<summary> chrome is plain — no extra structure. */
|
|
174
|
+
.callout-worked {
|
|
175
|
+
border-left-color: var(--callout-worked);
|
|
176
|
+
background: var(--warm-plum-tint);
|
|
177
|
+
}
|
|
178
|
+
.callout-worked summary {
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
list-style: none;
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: baseline;
|
|
183
|
+
gap: var(--space-3);
|
|
184
|
+
}
|
|
185
|
+
.callout-worked summary::-webkit-details-marker {
|
|
186
|
+
display: none;
|
|
187
|
+
}
|
|
188
|
+
.callout-worked .callout-chip {
|
|
189
|
+
font-size: var(--text-xs);
|
|
190
|
+
text-transform: uppercase;
|
|
191
|
+
letter-spacing: 0.05em;
|
|
192
|
+
padding: 0 var(--space-2);
|
|
193
|
+
border: 1px solid var(--callout-worked);
|
|
194
|
+
border-radius: var(--radius-sm);
|
|
195
|
+
color: var(--callout-worked);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Gold — YouWillLearn (v4.1.0, #59). Reuses callout-insight (gold) hue;
|
|
199
|
+
* "Before you start" prereq sub-block sits above the title in muted style. */
|
|
200
|
+
.callout-learn {
|
|
201
|
+
border-left-color: var(--callout-learn);
|
|
202
|
+
background: var(--warm-gold-tint);
|
|
203
|
+
}
|
|
204
|
+
.callout-learn .callout-prereq {
|
|
205
|
+
font-size: var(--text-sm);
|
|
206
|
+
color: var(--color-text-muted);
|
|
207
|
+
margin-bottom: var(--space-2);
|
|
208
|
+
}
|
|
209
|
+
.callout-learn .callout-prereq-label {
|
|
210
|
+
color: var(--color-text);
|
|
211
|
+
}
|
|
212
|
+
|
|
165
213
|
/* ===== Theorem family (Theorem.astro) =====
|
|
166
214
|
* Plain (theorem/proposition/lemma/corollary): italic body
|
|
167
215
|
* Definition family (definition/example/exercise/remark/proof): upright body
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* poc-layouts.css — per-PoC-kind layout variables (v4.1.0, closes #56).
|
|
2
|
+
*
|
|
3
|
+
* Each kind swaps 3 CSS variables that downstream stylesheets (chapter.css,
|
|
4
|
+
* layout.css) can read. Consumers can override per-kind in their own
|
|
5
|
+
* stylesheets via `:where(.poc-layout-X) { --bs-content-line-length: ... }`.
|
|
6
|
+
*
|
|
7
|
+
* Variable surface (intentionally narrow):
|
|
8
|
+
* --bs-content-line-length — main column max line length (ch)
|
|
9
|
+
* --bs-content-vertical-rhythm — body line-height multiplier
|
|
10
|
+
* --bs-heading-emphasis — heading font-weight (400-900)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
:where(.poc-layout-tutorial) {
|
|
14
|
+
--bs-content-line-length: 70ch;
|
|
15
|
+
--bs-content-vertical-rhythm: 1.6;
|
|
16
|
+
--bs-heading-emphasis: 600;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:where(.poc-layout-how-to) {
|
|
20
|
+
--bs-content-line-length: 68ch;
|
|
21
|
+
--bs-content-vertical-rhythm: 1.5;
|
|
22
|
+
--bs-heading-emphasis: 700;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:where(.poc-layout-tldr) {
|
|
26
|
+
--bs-content-line-length: 65ch;
|
|
27
|
+
--bs-content-vertical-rhythm: 1.4;
|
|
28
|
+
--bs-heading-emphasis: 500;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:where(.poc-layout-part-summary) {
|
|
32
|
+
--bs-content-line-length: 90ch;
|
|
33
|
+
--bs-content-vertical-rhythm: 1.5;
|
|
34
|
+
--bs-heading-emphasis: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:where(.poc-layout-cheat-sheet) {
|
|
38
|
+
--bs-content-line-length: 55ch;
|
|
39
|
+
--bs-content-vertical-rhythm: 1.3;
|
|
40
|
+
--bs-heading-emphasis: 600;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* All kinds: apply the variables to the wrapper itself.
|
|
44
|
+
* Authors can read these vars from descendants for tighter control. */
|
|
45
|
+
.poc-layout {
|
|
46
|
+
max-width: var(--bs-content-line-length, 70ch);
|
|
47
|
+
line-height: var(--bs-content-vertical-rhythm, 1.6);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.poc-layout :is(h1, h2, h3, h4, h5, h6) {
|
|
51
|
+
font-weight: var(--bs-heading-emphasis, 600);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Cheat-sheet: tables should fill the available width since the column is narrower. */
|
|
55
|
+
.poc-layout-cheat-sheet table {
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
package/styles/tokens.css
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
--warm-green: #4A7E3F; /* positive: tips, practitioner */
|
|
15
15
|
--warm-plum: #8A4E82; /* authority: official, exercises */
|
|
16
16
|
--warm-gold: #C09840; /* insight: convergence, reasoning */
|
|
17
|
+
--warm-crimson:#A03838; /* pitfall: deeper red distinct from warm-rose */
|
|
17
18
|
|
|
18
19
|
/* Warm neutrals (Anthropic-derived) */
|
|
19
20
|
--paper: #FDFCF9; /* page background — slightly off-white */
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
--warm-green-tint: color-mix(in srgb, var(--warm-green) 6%, var(--paper));
|
|
28
29
|
--warm-plum-tint: color-mix(in srgb, var(--warm-plum) 6%, var(--paper));
|
|
29
30
|
--warm-gold-tint: color-mix(in srgb, var(--warm-gold) 6%, var(--paper));
|
|
31
|
+
--warm-crimson-tint:color-mix(in srgb, var(--warm-crimson) 6%, var(--paper));
|
|
30
32
|
|
|
31
33
|
/* ===== Layer 2: Semantic roles (light mode) ===== */
|
|
32
34
|
--color-bg: var(--paper);
|
|
@@ -45,6 +47,9 @@
|
|
|
45
47
|
--callout-tip: var(--warm-green);
|
|
46
48
|
--callout-official: var(--warm-plum);
|
|
47
49
|
--callout-insight: var(--warm-gold);
|
|
50
|
+
--callout-pitfall: var(--warm-crimson);
|
|
51
|
+
--callout-worked: var(--warm-plum);
|
|
52
|
+
--callout-learn: var(--warm-gold);
|
|
48
53
|
|
|
49
54
|
/* ===== Typography scale ===== */
|
|
50
55
|
--font-body: 'Roboto Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|