@brandon_m_behring/book-scaffold-astro 4.27.1 → 4.29.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 +16 -16
- package/README.md +8 -0
- package/components/BookLink.astro +3 -2
- package/components/demo/DemoFrame.tsx +96 -0
- package/components/demo/Slider.tsx +150 -0
- package/components/demo/StatCards.tsx +100 -0
- package/components/demo/index.ts +24 -0
- package/components/demo/useThemeColors.ts +141 -0
- package/dist/demo.d.ts +96 -0
- package/dist/demo.mjs +353 -0
- package/dist/index.d.ts +8 -70
- package/dist/index.mjs +21 -4
- package/dist/lib/nav-href.d.ts +66 -0
- package/dist/lib/nav-href.mjs +44 -0
- package/dist/schemas.d.ts +1 -1
- package/dist/{types-CZrkqzpC.d.ts → types-DgSlAew3.d.ts} +32 -8
- package/layouts/Base.astro +1 -2
- package/package.json +10 -1
- package/recipes/09-validation.md +30 -2
- package/recipes/22-responsive-nav-and-multibook-routing.md +5 -0
- package/recipes/23-interactive-demo-substrate.md +103 -0
- package/recipes/README.md +2 -0
- package/scripts/build-labels.mjs +88 -34
- package/scripts/resolve-book-config.mjs +74 -0
- package/scripts/validate.mjs +239 -29
- package/scripts/walk-mdx.mjs +122 -8
- package/src/lib/book-link.ts +25 -5
- package/styles/demo.css +207 -0
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as preact from 'preact';
|
|
2
|
+
import { ComponentChildren } from 'preact';
|
|
3
|
+
|
|
4
|
+
declare const DEMO_HEADING_LEVELS: readonly [2, 3, 4, 5, 6];
|
|
5
|
+
type DemoHeadingLevel = (typeof DEMO_HEADING_LEVELS)[number];
|
|
6
|
+
interface DemoFrameProps {
|
|
7
|
+
/** Visible accessible name for the teaching figure. */
|
|
8
|
+
title: string;
|
|
9
|
+
/** Short explanation rendered directly below the title. */
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Figure caption rendered after the interactive body. */
|
|
12
|
+
caption?: ComponentChildren;
|
|
13
|
+
/** Consumer-owned controls and visualization. */
|
|
14
|
+
children?: ComponentChildren;
|
|
15
|
+
/** Optional stable root id for deep links and deterministic child ids. */
|
|
16
|
+
id?: string;
|
|
17
|
+
/** Additional class on the root figure. */
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Reflect a consumer-owned recomputation/loading state to assistive tech. */
|
|
20
|
+
busy?: boolean;
|
|
21
|
+
/** Heading depth in the surrounding document outline. Defaults to 3. */
|
|
22
|
+
headingLevel?: DemoHeadingLevel;
|
|
23
|
+
}
|
|
24
|
+
declare function DemoFrame({ title, description, caption, children, id, className, busy, headingLevel, }: DemoFrameProps): preact.JSX.Element;
|
|
25
|
+
|
|
26
|
+
interface SliderProps {
|
|
27
|
+
/** Visible label associated with the native range input. */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Controlled numeric value. */
|
|
30
|
+
value: number;
|
|
31
|
+
min: number;
|
|
32
|
+
max: number;
|
|
33
|
+
/** Positive increment. Defaults to the native range-input step of 1. */
|
|
34
|
+
step?: number;
|
|
35
|
+
/** Receives the parsed numeric value for every native input event. */
|
|
36
|
+
onValueChange: (value: number) => void;
|
|
37
|
+
/** Optional stable input id. A hydration-stable id is generated otherwise. */
|
|
38
|
+
id?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Formats the visible current-value output. */
|
|
44
|
+
formatValue?: (value: number) => string;
|
|
45
|
+
/** Optional spoken value text when the visible format is not descriptive. */
|
|
46
|
+
getValueText?: (value: number) => string;
|
|
47
|
+
}
|
|
48
|
+
declare function Slider(props: SliderProps): preact.JSX.Element;
|
|
49
|
+
|
|
50
|
+
/** Semantic metric-card list for interactive teaching figures (#143). */
|
|
51
|
+
declare const STAT_CARD_TONES: readonly ["neutral", "accent", "positive", "warning"];
|
|
52
|
+
type StatCardTone = (typeof STAT_CARD_TONES)[number];
|
|
53
|
+
interface StatCardItem {
|
|
54
|
+
/** Optional stable key. Defaults to the label, which must then be unique. */
|
|
55
|
+
id?: string;
|
|
56
|
+
label: string;
|
|
57
|
+
value: string | number;
|
|
58
|
+
/** Optional unit rendered beside the primary value. */
|
|
59
|
+
unit?: string;
|
|
60
|
+
/** Secondary explanatory line. */
|
|
61
|
+
detail?: string;
|
|
62
|
+
tone?: StatCardTone;
|
|
63
|
+
}
|
|
64
|
+
interface StatCardsProps {
|
|
65
|
+
items: readonly StatCardItem[];
|
|
66
|
+
/** Accessible name for the definition list. */
|
|
67
|
+
label?: string;
|
|
68
|
+
/** Opt-in announcements for values that change independently of a control. */
|
|
69
|
+
live?: 'off' | 'polite';
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
declare function StatCards({ items, label, live, className, }: StatCardsProps): preact.JSX.Element;
|
|
73
|
+
|
|
74
|
+
type BookTheme = 'light' | 'dark';
|
|
75
|
+
type ThemeColorToken = `--${string}`;
|
|
76
|
+
type ThemeColorSpec = readonly [token: ThemeColorToken, fallback: string];
|
|
77
|
+
type ThemeColorMap = Readonly<Record<string, ThemeColorSpec>>;
|
|
78
|
+
type ResolvedThemeColors<T extends ThemeColorMap> = {
|
|
79
|
+
readonly [Key in keyof T]: string;
|
|
80
|
+
};
|
|
81
|
+
interface ThemeColorsSnapshot<T extends ThemeColorMap> {
|
|
82
|
+
/** Null during SSR; resolved on the first client effect. */
|
|
83
|
+
theme: BookTheme | null;
|
|
84
|
+
colors: ResolvedThemeColors<T>;
|
|
85
|
+
/** Null until the client preference is known; animate only when explicitly false. */
|
|
86
|
+
reducedMotion: boolean | null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve CSS custom properties and refresh after theme/motion changes.
|
|
90
|
+
*
|
|
91
|
+
* Keep the spec object outside the component (or otherwise stable) so its
|
|
92
|
+
* intent is obvious and the hook does not need to re-subscribe each render.
|
|
93
|
+
*/
|
|
94
|
+
declare function useThemeColors<const T extends ThemeColorMap>(specs: T): ThemeColorsSnapshot<T>;
|
|
95
|
+
|
|
96
|
+
export { type BookTheme, DEMO_HEADING_LEVELS, DemoFrame, type DemoFrameProps, type DemoHeadingLevel, type ResolvedThemeColors, STAT_CARD_TONES, Slider, type SliderProps, type StatCardItem, type StatCardTone, StatCards, type StatCardsProps, type ThemeColorMap, type ThemeColorSpec, type ThemeColorToken, type ThemeColorsSnapshot, useThemeColors };
|
package/dist/demo.mjs
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// components/demo/DemoFrame.tsx
|
|
2
|
+
import { useId } from "preact/hooks";
|
|
3
|
+
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
4
|
+
var DEMO_HEADING_LEVELS = [2, 3, 4, 5, 6];
|
|
5
|
+
function assertNonEmpty(value, prop) {
|
|
6
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
7
|
+
throw new Error(`DemoFrame: ${prop} must be a non-empty string.`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function generatedBaseId(id, generated) {
|
|
11
|
+
if (id !== void 0) {
|
|
12
|
+
assertNonEmpty(id, "id");
|
|
13
|
+
if (/\s/.test(id)) {
|
|
14
|
+
throw new Error("DemoFrame: id must not contain whitespace.");
|
|
15
|
+
}
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
return `demo-${generated.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
19
|
+
}
|
|
20
|
+
function DemoFrame({
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
caption,
|
|
24
|
+
children,
|
|
25
|
+
id,
|
|
26
|
+
className,
|
|
27
|
+
busy = false,
|
|
28
|
+
headingLevel = 3
|
|
29
|
+
}) {
|
|
30
|
+
assertNonEmpty(title, "title");
|
|
31
|
+
if (description !== void 0) assertNonEmpty(description, "description");
|
|
32
|
+
if (typeof caption === "string") assertNonEmpty(caption, "caption");
|
|
33
|
+
if (!DEMO_HEADING_LEVELS.includes(headingLevel)) {
|
|
34
|
+
throw new Error("DemoFrame: headingLevel must be one of 2 | 3 | 4 | 5 | 6.");
|
|
35
|
+
}
|
|
36
|
+
const baseId = generatedBaseId(id, useId());
|
|
37
|
+
const titleId = `${baseId}-title`;
|
|
38
|
+
const descriptionId = description ? `${baseId}-description` : null;
|
|
39
|
+
const captionId = caption != null ? `${baseId}-caption` : null;
|
|
40
|
+
const describedBy = [descriptionId, captionId].filter(Boolean).join(" ") || void 0;
|
|
41
|
+
const classes = ["demo-frame", className].filter(Boolean).join(" ");
|
|
42
|
+
const Heading = `h${headingLevel}`;
|
|
43
|
+
return /* @__PURE__ */ jsxs(
|
|
44
|
+
"figure",
|
|
45
|
+
{
|
|
46
|
+
id,
|
|
47
|
+
class: classes,
|
|
48
|
+
"aria-labelledby": titleId,
|
|
49
|
+
"aria-describedby": describedBy,
|
|
50
|
+
"aria-busy": busy || void 0,
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsxs("header", { class: "demo-frame__header", children: [
|
|
53
|
+
/* @__PURE__ */ jsx(Heading, { id: titleId, class: "demo-frame__title", children: title }),
|
|
54
|
+
description && /* @__PURE__ */ jsx("p", { id: descriptionId, class: "demo-frame__description", children: description })
|
|
55
|
+
] }),
|
|
56
|
+
/* @__PURE__ */ jsx("div", { class: "demo-frame__body", children }),
|
|
57
|
+
caption != null && /* @__PURE__ */ jsx("figcaption", { id: captionId, class: "demo-frame__caption", children: caption })
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// components/demo/Slider.tsx
|
|
64
|
+
import { useId as useId2 } from "preact/hooks";
|
|
65
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
66
|
+
function assertFinite(value, prop) {
|
|
67
|
+
if (!Number.isFinite(value)) {
|
|
68
|
+
throw new Error(`Slider: ${prop} must be a finite number.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function isStepAligned(value, min, step) {
|
|
72
|
+
const steps = (value - min) / step;
|
|
73
|
+
const nearest = Math.round(steps);
|
|
74
|
+
const tolerance = Number.EPSILON * Math.max(1, Math.abs(steps)) * 16;
|
|
75
|
+
return Math.abs(steps - nearest) <= tolerance;
|
|
76
|
+
}
|
|
77
|
+
function assertSliderProps({
|
|
78
|
+
label,
|
|
79
|
+
value,
|
|
80
|
+
min,
|
|
81
|
+
max,
|
|
82
|
+
step = 1,
|
|
83
|
+
id,
|
|
84
|
+
description,
|
|
85
|
+
onValueChange,
|
|
86
|
+
formatValue,
|
|
87
|
+
getValueText
|
|
88
|
+
}) {
|
|
89
|
+
if (typeof label !== "string" || label.trim() === "") {
|
|
90
|
+
throw new Error("Slider: label must be a non-empty string.");
|
|
91
|
+
}
|
|
92
|
+
assertFinite(value, "value");
|
|
93
|
+
assertFinite(min, "min");
|
|
94
|
+
assertFinite(max, "max");
|
|
95
|
+
assertFinite(step, "step");
|
|
96
|
+
if (max <= min) throw new Error("Slider: max must be greater than min.");
|
|
97
|
+
if (step <= 0) throw new Error("Slider: step must be greater than 0.");
|
|
98
|
+
if (value < min || value > max) {
|
|
99
|
+
throw new Error(`Slider: value ${value} must be within [${min}, ${max}].`);
|
|
100
|
+
}
|
|
101
|
+
if (!isStepAligned(value, min, step)) {
|
|
102
|
+
throw new Error("Slider: value must align with min + (n \xD7 step).");
|
|
103
|
+
}
|
|
104
|
+
if (id !== void 0 && (typeof id !== "string" || id.trim() === "" || /\s/.test(id))) {
|
|
105
|
+
throw new Error("Slider: id must be a non-empty string without whitespace.");
|
|
106
|
+
}
|
|
107
|
+
if (description !== void 0 && (typeof description !== "string" || description.trim() === "")) {
|
|
108
|
+
throw new Error("Slider: description must be a non-empty string.");
|
|
109
|
+
}
|
|
110
|
+
if (typeof onValueChange !== "function") {
|
|
111
|
+
throw new Error("Slider: onValueChange must be a function.");
|
|
112
|
+
}
|
|
113
|
+
if (formatValue !== void 0 && typeof formatValue !== "function") {
|
|
114
|
+
throw new Error("Slider: formatValue must be a function.");
|
|
115
|
+
}
|
|
116
|
+
if (getValueText !== void 0 && typeof getValueText !== "function") {
|
|
117
|
+
throw new Error("Slider: getValueText must be a function.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function Slider(props) {
|
|
121
|
+
assertSliderProps(props);
|
|
122
|
+
const {
|
|
123
|
+
label,
|
|
124
|
+
value,
|
|
125
|
+
min,
|
|
126
|
+
max,
|
|
127
|
+
step = 1,
|
|
128
|
+
onValueChange,
|
|
129
|
+
id,
|
|
130
|
+
name,
|
|
131
|
+
description,
|
|
132
|
+
disabled = false,
|
|
133
|
+
className,
|
|
134
|
+
formatValue = String,
|
|
135
|
+
getValueText = formatValue
|
|
136
|
+
} = props;
|
|
137
|
+
const generated = useId2().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
138
|
+
const inputId = id ?? `demo-slider-${generated}`;
|
|
139
|
+
const descriptionId = description ? `${inputId}-description` : void 0;
|
|
140
|
+
const displayValue = formatValue(value);
|
|
141
|
+
if (typeof displayValue !== "string" || displayValue.trim() === "") {
|
|
142
|
+
throw new Error("Slider: formatValue must return a non-empty string.");
|
|
143
|
+
}
|
|
144
|
+
const valueText = getValueText(value);
|
|
145
|
+
if (typeof valueText !== "string" || valueText.trim() === "") {
|
|
146
|
+
throw new Error("Slider: getValueText must return a non-empty string.");
|
|
147
|
+
}
|
|
148
|
+
const classes = ["demo-slider", className].filter(Boolean).join(" ");
|
|
149
|
+
return /* @__PURE__ */ jsxs2("div", { class: classes, "data-disabled": disabled || void 0, children: [
|
|
150
|
+
/* @__PURE__ */ jsxs2("div", { class: "demo-slider__head", children: [
|
|
151
|
+
/* @__PURE__ */ jsx2("label", { class: "demo-slider__label", for: inputId, children: label }),
|
|
152
|
+
/* @__PURE__ */ jsx2("output", { class: "demo-slider__value", for: inputId, children: displayValue })
|
|
153
|
+
] }),
|
|
154
|
+
description && /* @__PURE__ */ jsx2("p", { id: descriptionId, class: "demo-slider__description", children: description }),
|
|
155
|
+
/* @__PURE__ */ jsx2(
|
|
156
|
+
"input",
|
|
157
|
+
{
|
|
158
|
+
id: inputId,
|
|
159
|
+
class: "demo-slider__input",
|
|
160
|
+
type: "range",
|
|
161
|
+
name,
|
|
162
|
+
min,
|
|
163
|
+
max,
|
|
164
|
+
step,
|
|
165
|
+
value,
|
|
166
|
+
disabled,
|
|
167
|
+
"aria-describedby": descriptionId,
|
|
168
|
+
"aria-valuetext": valueText,
|
|
169
|
+
onInput: (event) => {
|
|
170
|
+
const next = Number.parseFloat(event.currentTarget.value);
|
|
171
|
+
if (!Number.isFinite(next)) {
|
|
172
|
+
throw new Error("Slider: native input produced a non-finite value.");
|
|
173
|
+
}
|
|
174
|
+
onValueChange(next);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
] });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// components/demo/StatCards.tsx
|
|
182
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
|
|
183
|
+
var STAT_CARD_TONES = ["neutral", "accent", "positive", "warning"];
|
|
184
|
+
function assertText(value, path) {
|
|
185
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
186
|
+
throw new Error(`StatCards: ${path} must be a non-empty string.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function assertItems(items) {
|
|
190
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
191
|
+
throw new Error("StatCards: items must contain at least one statistic.");
|
|
192
|
+
}
|
|
193
|
+
const keys = /* @__PURE__ */ new Set();
|
|
194
|
+
for (const [index, item] of items.entries()) {
|
|
195
|
+
if (item === null || typeof item !== "object") {
|
|
196
|
+
throw new Error(`StatCards: items[${index}] must be a statistic object.`);
|
|
197
|
+
}
|
|
198
|
+
assertText(item.label, `items[${index}].label`);
|
|
199
|
+
if (typeof item.value !== "string" && typeof item.value !== "number") {
|
|
200
|
+
throw new Error(`StatCards: items[${index}].value must be a string or number.`);
|
|
201
|
+
}
|
|
202
|
+
if (typeof item.value === "number" && !Number.isFinite(item.value)) {
|
|
203
|
+
throw new Error(`StatCards: items[${index}].value must be finite.`);
|
|
204
|
+
}
|
|
205
|
+
if (typeof item.value === "string") assertText(item.value, `items[${index}].value`);
|
|
206
|
+
if (item.unit !== void 0) assertText(item.unit, `items[${index}].unit`);
|
|
207
|
+
if (item.detail !== void 0) assertText(item.detail, `items[${index}].detail`);
|
|
208
|
+
const tone = item.tone ?? "neutral";
|
|
209
|
+
if (!STAT_CARD_TONES.includes(tone)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`StatCards: items[${index}].tone must be one of ${STAT_CARD_TONES.join(" | ")}.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const key = item.id ?? item.label;
|
|
215
|
+
assertText(key, `items[${index}].id`);
|
|
216
|
+
if (keys.has(key)) throw new Error(`StatCards: duplicate item key "${key}".`);
|
|
217
|
+
keys.add(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function StatCards({
|
|
221
|
+
items,
|
|
222
|
+
label = "Key statistics",
|
|
223
|
+
live = "off",
|
|
224
|
+
className
|
|
225
|
+
}) {
|
|
226
|
+
assertItems(items);
|
|
227
|
+
assertText(label, "label");
|
|
228
|
+
if (live !== "off" && live !== "polite") {
|
|
229
|
+
throw new Error("StatCards: live must be off | polite.");
|
|
230
|
+
}
|
|
231
|
+
const classes = ["demo-stat-cards", className].filter(Boolean).join(" ");
|
|
232
|
+
return /* @__PURE__ */ jsx3(
|
|
233
|
+
"dl",
|
|
234
|
+
{
|
|
235
|
+
class: classes,
|
|
236
|
+
"aria-label": label,
|
|
237
|
+
"aria-live": live,
|
|
238
|
+
"aria-atomic": live === "polite" || void 0,
|
|
239
|
+
children: items.map((item) => {
|
|
240
|
+
const tone = item.tone ?? "neutral";
|
|
241
|
+
return /* @__PURE__ */ jsxs3("div", { class: "demo-stat-card", "data-tone": tone, children: [
|
|
242
|
+
/* @__PURE__ */ jsx3("dt", { class: "demo-stat-card__label", children: item.label }),
|
|
243
|
+
/* @__PURE__ */ jsxs3("dd", { class: "demo-stat-card__value", children: [
|
|
244
|
+
item.value,
|
|
245
|
+
item.unit && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
246
|
+
" ",
|
|
247
|
+
/* @__PURE__ */ jsx3("span", { class: "demo-stat-card__unit", children: item.unit })
|
|
248
|
+
] })
|
|
249
|
+
] }),
|
|
250
|
+
item.detail && /* @__PURE__ */ jsx3("dd", { class: "demo-stat-card__detail", children: item.detail })
|
|
251
|
+
] }, item.id ?? item.label);
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// components/demo/useThemeColors.ts
|
|
258
|
+
import { useEffect, useState } from "preact/hooks";
|
|
259
|
+
function fallbackColors(specs) {
|
|
260
|
+
return Object.fromEntries(
|
|
261
|
+
Object.entries(specs).map(([key, [, fallback]]) => [key, fallback])
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function unresolvedSnapshot(specs) {
|
|
265
|
+
return {
|
|
266
|
+
theme: null,
|
|
267
|
+
colors: fallbackColors(specs),
|
|
268
|
+
reducedMotion: null
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function assertSpecs(specs) {
|
|
272
|
+
if (specs === null || typeof specs !== "object" || Array.isArray(specs)) {
|
|
273
|
+
throw new Error("useThemeColors: specs must be a token-map object.");
|
|
274
|
+
}
|
|
275
|
+
const entries = Object.entries(specs);
|
|
276
|
+
if (entries.length === 0) {
|
|
277
|
+
throw new Error("useThemeColors: provide at least one CSS token mapping.");
|
|
278
|
+
}
|
|
279
|
+
for (const [key, spec] of entries) {
|
|
280
|
+
if (key.trim() === "") {
|
|
281
|
+
throw new Error("useThemeColors: color keys must be non-empty strings.");
|
|
282
|
+
}
|
|
283
|
+
if (!Array.isArray(spec) || spec.length !== 2) {
|
|
284
|
+
throw new Error(`useThemeColors: "${key}" must be a [token, fallback] pair.`);
|
|
285
|
+
}
|
|
286
|
+
const [token, fallback] = spec;
|
|
287
|
+
if (typeof token !== "string" || !/^--\S+$/.test(token)) {
|
|
288
|
+
throw new Error(`useThemeColors: "${key}" token must start with -- and contain no whitespace.`);
|
|
289
|
+
}
|
|
290
|
+
if (typeof fallback !== "string" || fallback.trim() === "") {
|
|
291
|
+
throw new Error(`useThemeColors: "${key}" fallback must be a non-empty string.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function effectiveTheme() {
|
|
296
|
+
const explicit = document.documentElement.getAttribute("data-theme");
|
|
297
|
+
if (explicit === "light" || explicit === "dark") return explicit;
|
|
298
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
299
|
+
}
|
|
300
|
+
function snapshot(specs, themeHint) {
|
|
301
|
+
const computed = getComputedStyle(document.documentElement);
|
|
302
|
+
const colors = Object.fromEntries(
|
|
303
|
+
Object.entries(specs).map(([key, [token, fallback]]) => {
|
|
304
|
+
const resolved = computed.getPropertyValue(token).trim();
|
|
305
|
+
return [key, resolved || fallback];
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
return {
|
|
309
|
+
theme: themeHint ?? effectiveTheme(),
|
|
310
|
+
colors,
|
|
311
|
+
reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function useThemeColors(specs) {
|
|
315
|
+
assertSpecs(specs);
|
|
316
|
+
const signature = JSON.stringify(specs);
|
|
317
|
+
const [current, setCurrent] = useState(() => ({
|
|
318
|
+
signature,
|
|
319
|
+
value: unresolvedSnapshot(specs)
|
|
320
|
+
}));
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const refresh = (themeHint) => setCurrent({
|
|
323
|
+
signature,
|
|
324
|
+
value: snapshot(specs, themeHint)
|
|
325
|
+
});
|
|
326
|
+
const onThemeChange = (event) => {
|
|
327
|
+
const detail = event.detail;
|
|
328
|
+
const theme = detail?.theme === "light" || detail?.theme === "dark" ? detail.theme : void 0;
|
|
329
|
+
refresh(theme);
|
|
330
|
+
};
|
|
331
|
+
const onMediaChange = () => refresh();
|
|
332
|
+
const colorScheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
333
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
334
|
+
refresh();
|
|
335
|
+
window.addEventListener("book:theme:change", onThemeChange);
|
|
336
|
+
colorScheme.addEventListener("change", onMediaChange);
|
|
337
|
+
reducedMotion.addEventListener("change", onMediaChange);
|
|
338
|
+
return () => {
|
|
339
|
+
window.removeEventListener("book:theme:change", onThemeChange);
|
|
340
|
+
colorScheme.removeEventListener("change", onMediaChange);
|
|
341
|
+
reducedMotion.removeEventListener("change", onMediaChange);
|
|
342
|
+
};
|
|
343
|
+
}, [signature]);
|
|
344
|
+
return current.signature === signature ? current.value : unresolvedSnapshot(specs);
|
|
345
|
+
}
|
|
346
|
+
export {
|
|
347
|
+
DEMO_HEADING_LEVELS,
|
|
348
|
+
DemoFrame,
|
|
349
|
+
STAT_CARD_TONES,
|
|
350
|
+
Slider,
|
|
351
|
+
StatCards,
|
|
352
|
+
useThemeColors
|
|
353
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration, MarkdownHeading } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer,
|
|
3
|
-
export { B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, F as FreshnessAffordance, i as FrontmatterRouteConfig, N as NUMBER_STYLES, j as NumberStyle, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ReleaseStatusConfig, m as RouteToggles, S as SecurityHeadersConfig, n as StatusBadge,
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, h as ChaptersRenderer, p as SiblingBooks, r as Style } from './types-DgSlAew3.js';
|
|
3
|
+
export { B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, F as FreshnessAffordance, i as FrontmatterRouteConfig, N as NUMBER_STYLES, j as NumberStyle, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, R as ReleaseStatusConfig, m as RouteToggles, S as SecurityHeadersConfig, n as SiblingBookDescriptor, o as SiblingBookEntry, q as StatusBadge, s as StyleInput, V as VolatilityBadge, t as composeStyles, u as defineProfile, v as defineStyle, w as normalizeFrontmatterConfig, x as resolvePreset, y as resolveProfile } from './types-DgSlAew3.js';
|
|
4
4
|
import { E as volatilityLevels, c as academicParts, Q as Question } from './schemas-CKipJ5Ie.js';
|
|
5
5
|
export { A as AcademicChapter, B as BloomLevel, C as CourseNotesChapter, G as GlossaryTerm, M as MinimalChapter, P as Provenance, a as QuestionType, R as ResearchPortfolioChapter, T as ToolsChapter, b as academicChapterSchema, d as bloomLevels, e as changeKinds, f as changelogSchema, g as chapterStatus, h as citationBackstops, i as courseNotesChapterSchema, j as glossarySchema, l as layoutModes, m as minimalChapterSchema, p as patternCategories, k as patternsSchema, n as provenanceObject, o as provenanceSchema, q as questionDifficulties, r as questionSchema, s as questionTypes, t as refineQuestion, u as refinedQuestionSchema, v as researchPortfolioChapterSchema, w as sourceTiers, x as sourceTiersResearch, y as sourcesSchema, z as toolSlugs, D as toolsChapterSchema } from './schemas-CKipJ5Ie.js';
|
|
6
6
|
export { KIND_LABEL, ResolvedTheoremLabel, THEOREM_KINDS, TheoremKind, TheoremLabelProps, resolveTheoremNumber, theoremLabel } from './lib/theorem-label.js';
|
|
7
7
|
export { ssmMacros } from './lib/katex-macros.js';
|
|
8
|
+
export { ChapterLike, apparatusHref, baseNoSlash, bookOf, chapterHref, isCurrentChapter, normalizeBase, slugOf } from './lib/nav-href.js';
|
|
8
9
|
export { D as DomainScore, E as ExamBlueprint, a as ExamQuestion, b as ExamResult, R as RoutingChapter, c as buildExamManifest, d as deriveDomainRouting, s as sampleExam, e as scoreExam, f as shuffle, g as spreadBlueprint } from './exam-manifest-X9IrX1G3.js';
|
|
9
10
|
export { F as FlashcardRef, b as buildFlashcardDeck } from './flashcards-okekZcl8.js';
|
|
10
11
|
import 'astro/zod';
|
|
@@ -303,75 +304,12 @@ declare function assertEnumProp<T extends string>(value: unknown, allowed: reado
|
|
|
303
304
|
* sibling redeploys or extracts to its own repo. An unknown `book` THROWS
|
|
304
305
|
* rather than emitting a dead cross-origin link (fail-loud, like #109).
|
|
305
306
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
307
|
+
* #147 extends each registry entry from a URL string to the backward-compatible
|
|
308
|
+
* `{ url, labels? }` descriptor. Runtime href resolution uses `url`; the
|
|
309
|
+
* validator uses `labels` to check literal sibling path/fragment targets.
|
|
308
310
|
*/
|
|
309
|
-
declare function resolveBookHref(siblingBooks: Record<string, string> | null | undefined, book: string, to: string): string;
|
|
310
311
|
|
|
311
|
-
|
|
312
|
-
* src/lib/nav-href.ts — pure route-href resolver (#80 multi-book navigation).
|
|
313
|
-
*
|
|
314
|
-
* No `astro:content` import (mirrors chapter-sort.ts) so tsup can include it in
|
|
315
|
-
* the DTS bundle without dragging Astro virtual modules into the build graph.
|
|
316
|
-
* The nav components (Sidebar, ChapterNav, NavContent, …) call these helpers
|
|
317
|
-
* instead of hardcoding the single-book `/chapters/<id>/` URL shape — so ONE set
|
|
318
|
-
* of components serves both a single-book site (the default pattern) and a
|
|
319
|
-
* multi-book consumer whose chapters render at `/<book>/<slug>/`.
|
|
320
|
-
*
|
|
321
|
-
* Patterns are base-relative TOKEN STRINGS (a resolver *function* could not
|
|
322
|
-
* survive the book-config virtual module's `JSON.stringify`, so the config
|
|
323
|
-
* surface is a declarative string resolved here):
|
|
324
|
-
* :id → entry.id verbatim, slashes preserved e.g. 'kg/01-intro'
|
|
325
|
-
* :book → the entry's book name (see `bookField`), or '' e.g. 'kg'
|
|
326
|
-
* :slug → entry.id with a leading '<book>/' stripped e.g. '01-intro'
|
|
327
|
-
* BASE_URL is applied here, so patterns are written base-relative (lead '/').
|
|
328
|
-
*
|
|
329
|
-
* Defaults reproduce the single-book behavior BYTE-FOR-BYTE:
|
|
330
|
-
* chapterRoute = '/chapters/:id/' → `${base}chapters/${id}/`
|
|
331
|
-
* bookField = 'book' (academic/tools schemas have no `book` → bookOf
|
|
332
|
-
* returns null → "show all chapters", today's nav)
|
|
333
|
-
* apparatusRoute = '/:route/' → `${base}<route>/`
|
|
334
|
-
*/
|
|
335
|
-
/** Minimal shape the resolver needs from a chapter collection entry. */
|
|
336
|
-
interface ChapterLike {
|
|
337
|
-
id: string;
|
|
338
|
-
data: Record<string, unknown>;
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Normalize a base URL to exactly one trailing slash (`''`/`undefined` → `'/'`).
|
|
342
|
-
*
|
|
343
|
-
* v4.27.0 (#182): promoted from this module's private helper to THE shared
|
|
344
|
-
* normalizer — 18 `.astro` files previously inlined the same normalization in
|
|
345
|
-
* three regex idioms. Astro does not guarantee a trailing slash on `base`
|
|
346
|
-
* (`'/foo'` is a documented form), so every `import.meta.env.BASE_URL` read
|
|
347
|
-
* must pass through here before href composition: `${normalizeBase(...)}chapters/`.
|
|
348
|
-
* Takes the base as a PARAMETER because src/lib ships pre-compiled in dist/,
|
|
349
|
-
* where Vite's import.meta.env replacement cannot reach (see exam-manifest.ts).
|
|
350
|
-
*/
|
|
351
|
-
declare function normalizeBase(baseUrl: string | undefined): string;
|
|
352
|
-
/**
|
|
353
|
-
* The composing variant (#182): strip ALL trailing slashes (`'/'` → `''`,
|
|
354
|
-
* `'/foo/'` → `'/foo'`) for `${baseNoSlash(...)}/answers`-style templates
|
|
355
|
-
* where the literal supplies the slash (Rationale.astro's route detection).
|
|
356
|
-
*/
|
|
357
|
-
declare function baseNoSlash(baseUrl: string | undefined): string;
|
|
358
|
-
/**
|
|
359
|
-
* The entry's book name from `data[bookField]`, or `null` when absent/blank.
|
|
360
|
-
* Single-book schemas (academic/tools/minimal) have no such field → `null`,
|
|
361
|
-
* which callers read as "this is the only book — show every chapter".
|
|
362
|
-
*/
|
|
363
|
-
declare function bookOf(entry: ChapterLike, bookField?: string): string | null;
|
|
364
|
-
/** `entry.id` with a leading `'<book>/'` stripped (multi-book) or unchanged. */
|
|
365
|
-
declare function slugOf(entry: ChapterLike, bookField?: string): string;
|
|
366
|
-
/** Resolve a chapter entry to a base-prefixed href via the `chapterRoute` pattern. */
|
|
367
|
-
declare function chapterHref(entry: ChapterLike, pattern?: string, baseUrl?: string, bookField?: string): string;
|
|
368
|
-
/**
|
|
369
|
-
* Resolve a per-book apparatus route (glossary / practice-exam / flashcards /
|
|
370
|
-
* answers) to a base-prefixed href via the `apparatusRoute` pattern.
|
|
371
|
-
*/
|
|
372
|
-
declare function apparatusHref(route: string, book: string | null, pattern?: string, baseUrl?: string): string;
|
|
373
|
-
/** Whether `entry` is the page at `currentPath` (trailing-slash tolerant). */
|
|
374
|
-
declare function isCurrentChapter(entry: ChapterLike, currentPath: string, pattern?: string, baseUrl?: string, bookField?: string): boolean;
|
|
312
|
+
declare function resolveBookHref(siblingBooks: SiblingBooks | null | undefined, book: string, to: string): string;
|
|
375
313
|
|
|
376
314
|
/**
|
|
377
315
|
* exam-domains — validate a question's `domain` against the consumer's closed
|
|
@@ -629,4 +567,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
629
567
|
*/
|
|
630
568
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
631
569
|
|
|
632
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions,
|
|
570
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, type PartReviewGroup, type PartReviewSelection, Question, type ReviewChapter, type ReviewExercise, SiblingBooks, Style, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VisibleHeading, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, assertKnownDomain, bookScaffoldIntegration, buildGithubUrl, chapterLabel, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, deriveObjectiveMap, distinctChaptersSorted, fallbackChaptersRenderer, freshnessLabel, getFreshness, groupByChapter, groupByDomain, minimalStyle, originUrlFromGitConfig, parseRepoSlug, pickActive, researchPortfolioChaptersRenderer, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, selectPartExercises, sortQuestions, tocHeadings, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -1379,7 +1379,13 @@ function bookScaffoldIntegration(opts) {
|
|
|
1379
1379
|
}
|
|
1380
1380
|
};
|
|
1381
1381
|
Object.defineProperty(integration, "__bookScaffoldResolvedConfig", {
|
|
1382
|
-
value: Object.freeze({
|
|
1382
|
+
value: Object.freeze({
|
|
1383
|
+
preset: profile,
|
|
1384
|
+
numberStyle,
|
|
1385
|
+
siblingBooks: siblingBooks ?? {},
|
|
1386
|
+
chapterRoute: chapterRoute ?? "/chapters/:id/",
|
|
1387
|
+
bookField: bookField ?? "book"
|
|
1388
|
+
}),
|
|
1383
1389
|
enumerable: false,
|
|
1384
1390
|
configurable: false,
|
|
1385
1391
|
writable: false
|
|
@@ -1528,7 +1534,7 @@ async function defineBookConfig(opts) {
|
|
|
1528
1534
|
// v4.15.0 (#109): repo/branch override; integration auto-detects when undefined.
|
|
1529
1535
|
githubRepo: opts.githubRepo,
|
|
1530
1536
|
githubBranch: opts.githubBranch,
|
|
1531
|
-
// v4.16.0 (#96): cross-book link registry.
|
|
1537
|
+
// v4.16.0 (#96), extended in #147: cross-book link registry.
|
|
1532
1538
|
siblingBooks: opts.siblingBooks,
|
|
1533
1539
|
// v4.17.0 (#112): per-book exam-domain taxonomy for the questions collection.
|
|
1534
1540
|
examDomains: opts.examDomains,
|
|
@@ -1721,14 +1727,25 @@ function assertEnumProp(value, allowed, ctx) {
|
|
|
1721
1727
|
init_katex_macros();
|
|
1722
1728
|
|
|
1723
1729
|
// src/lib/book-link.ts
|
|
1730
|
+
function entryUrl(entry) {
|
|
1731
|
+
if (typeof entry === "string") return entry;
|
|
1732
|
+
return entry?.url;
|
|
1733
|
+
}
|
|
1724
1734
|
function resolveBookHref(siblingBooks, book, to) {
|
|
1725
|
-
const
|
|
1726
|
-
if (!
|
|
1735
|
+
const registered = siblingBooks !== null && siblingBooks !== void 0 && Object.prototype.hasOwnProperty.call(siblingBooks, book);
|
|
1736
|
+
if (!registered) {
|
|
1727
1737
|
const known = siblingBooks ? Object.keys(siblingBooks) : [];
|
|
1728
1738
|
throw new Error(
|
|
1729
1739
|
`<BookLink book="${book}">: unknown sibling book. Register it in defineBookConfig({ siblingBooks: { "${book}": "https://\u2026" } })` + (known.length ? ` (known: ${known.join(", ")})` : "") + "."
|
|
1730
1740
|
);
|
|
1731
1741
|
}
|
|
1742
|
+
const entry = siblingBooks[book];
|
|
1743
|
+
const base = entryUrl(entry);
|
|
1744
|
+
if (typeof base !== "string" || base.length === 0) {
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
`<BookLink book="${book}">: invalid siblingBooks entry. Expected a URL string or { url: "https://\u2026", labels?: "./path/to/labels.json" }.`
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1732
1749
|
return `${base.replace(/\/+$/, "")}/${to.replace(/^\/+/, "")}`;
|
|
1733
1750
|
}
|
|
1734
1751
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/lib/nav-href.ts — pure route-href resolver (#80 multi-book navigation).
|
|
3
|
+
*
|
|
4
|
+
* No `astro:content` import (mirrors chapter-sort.ts) so tsup can include it in
|
|
5
|
+
* the DTS bundle without dragging Astro virtual modules into the build graph.
|
|
6
|
+
* The nav components (Sidebar, ChapterNav, NavContent, …) call these helpers
|
|
7
|
+
* instead of hardcoding the single-book `/chapters/<id>/` URL shape — so ONE set
|
|
8
|
+
* of components serves both a single-book site (the default pattern) and a
|
|
9
|
+
* multi-book consumer whose chapters render at `/<book>/<slug>/`.
|
|
10
|
+
*
|
|
11
|
+
* Patterns are base-relative TOKEN STRINGS (a resolver *function* could not
|
|
12
|
+
* survive the book-config virtual module's `JSON.stringify`, so the config
|
|
13
|
+
* surface is a declarative string resolved here):
|
|
14
|
+
* :id → entry.id verbatim, slashes preserved e.g. 'kg/01-intro'
|
|
15
|
+
* :book → the entry's book name (see `bookField`), or '' e.g. 'kg'
|
|
16
|
+
* :slug → entry.id with a leading '<book>/' stripped e.g. '01-intro'
|
|
17
|
+
* BASE_URL is applied here, so patterns are written base-relative (lead '/').
|
|
18
|
+
*
|
|
19
|
+
* Defaults reproduce the single-book behavior BYTE-FOR-BYTE:
|
|
20
|
+
* chapterRoute = '/chapters/:id/' → `${base}chapters/${id}/`
|
|
21
|
+
* bookField = 'book' (academic/tools schemas have no `book` → bookOf
|
|
22
|
+
* returns null → "show all chapters", today's nav)
|
|
23
|
+
* apparatusRoute = '/:route/' → `${base}<route>/`
|
|
24
|
+
*/
|
|
25
|
+
/** Minimal shape the resolver needs from a chapter collection entry. */
|
|
26
|
+
interface ChapterLike {
|
|
27
|
+
id: string;
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize a base URL to exactly one trailing slash (`''`/`undefined` → `'/'`).
|
|
32
|
+
*
|
|
33
|
+
* v4.27.0 (#182): promoted from this module's private helper to THE shared
|
|
34
|
+
* normalizer — 18 `.astro` files previously inlined the same normalization in
|
|
35
|
+
* three regex idioms. Astro does not guarantee a trailing slash on `base`
|
|
36
|
+
* (`'/foo'` is a documented form), so every `import.meta.env.BASE_URL` read
|
|
37
|
+
* must pass through here before href composition: `${normalizeBase(...)}chapters/`.
|
|
38
|
+
* Takes the base as a PARAMETER because src/lib ships pre-compiled in dist/,
|
|
39
|
+
* where Vite's import.meta.env replacement cannot reach (see exam-manifest.ts).
|
|
40
|
+
*/
|
|
41
|
+
declare function normalizeBase(baseUrl: string | undefined): string;
|
|
42
|
+
/**
|
|
43
|
+
* The composing variant (#182): strip ALL trailing slashes (`'/'` → `''`,
|
|
44
|
+
* `'/foo/'` → `'/foo'`) for `${baseNoSlash(...)}/answers`-style templates
|
|
45
|
+
* where the literal supplies the slash (Rationale.astro's route detection).
|
|
46
|
+
*/
|
|
47
|
+
declare function baseNoSlash(baseUrl: string | undefined): string;
|
|
48
|
+
/**
|
|
49
|
+
* The entry's book name from `data[bookField]`, or `null` when absent/blank.
|
|
50
|
+
* Single-book schemas (academic/tools/minimal) have no such field → `null`,
|
|
51
|
+
* which callers read as "this is the only book — show every chapter".
|
|
52
|
+
*/
|
|
53
|
+
declare function bookOf(entry: ChapterLike, bookField?: string): string | null;
|
|
54
|
+
/** `entry.id` with a leading `'<book>/'` stripped (multi-book) or unchanged. */
|
|
55
|
+
declare function slugOf(entry: ChapterLike, bookField?: string): string;
|
|
56
|
+
/** Resolve a chapter entry to a base-prefixed href via the `chapterRoute` pattern. */
|
|
57
|
+
declare function chapterHref(entry: ChapterLike, pattern?: string, baseUrl?: string, bookField?: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a per-book apparatus route (glossary / practice-exam / flashcards /
|
|
60
|
+
* answers) to a base-prefixed href via the `apparatusRoute` pattern.
|
|
61
|
+
*/
|
|
62
|
+
declare function apparatusHref(route: string, book: string | null, pattern?: string, baseUrl?: string): string;
|
|
63
|
+
/** Whether `entry` is the page at `currentPath` (trailing-slash tolerant). */
|
|
64
|
+
declare function isCurrentChapter(entry: ChapterLike, currentPath: string, pattern?: string, baseUrl?: string, bookField?: string): boolean;
|
|
65
|
+
|
|
66
|
+
export { type ChapterLike, apparatusHref, baseNoSlash, bookOf, chapterHref, isCurrentChapter, normalizeBase, slugOf };
|