explico 0.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.
Files changed (113) hide show
  1. package/README.md +108 -0
  2. package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
  3. package/dist/analytics/AnalyticsEmitter.js +5 -0
  4. package/dist/compiler/cli.d.ts +189 -0
  5. package/dist/compiler/cli.js +552 -0
  6. package/dist/compiler/collections.d.ts +70 -0
  7. package/dist/compiler/collections.js +383 -0
  8. package/dist/compiler/course-config.d.ts +31 -0
  9. package/dist/compiler/course-config.js +0 -0
  10. package/dist/compiler/lesson-derivation.d.ts +89 -0
  11. package/dist/compiler/lesson-derivation.js +91 -0
  12. package/dist/compiler/manifest.d.ts +178 -0
  13. package/dist/compiler/manifest.js +345 -0
  14. package/dist/compiler/resolution-report.d.ts +45 -0
  15. package/dist/compiler/resolution-report.js +97 -0
  16. package/dist/curriculum/validation.d.ts +76 -0
  17. package/dist/curriculum/validation.js +320 -0
  18. package/dist/progress/ProgressRepository.d.ts +53 -0
  19. package/dist/progress/ProgressRepository.js +12 -0
  20. package/dist/reference/base-library.mjs +27 -0
  21. package/dist/reference/citation-format.mjs +133 -0
  22. package/dist/reference/consistency.d.ts +64 -0
  23. package/dist/reference/consistency.js +234 -0
  24. package/dist/reference/curriculum-alignment.d.ts +7 -0
  25. package/dist/reference/curriculum-alignment.js +166 -0
  26. package/dist/reference/equations-validate.d.ts +50 -0
  27. package/dist/reference/equations-validate.js +90 -0
  28. package/dist/reference/equations.mjs +144 -0
  29. package/dist/reference/gate-math.d.ts +41 -0
  30. package/dist/reference/gate-math.js +151 -0
  31. package/dist/reference/katex-options.mjs +91 -0
  32. package/dist/reference/lab-math-scope.d.ts +21 -0
  33. package/dist/reference/lab-math-scope.js +28 -0
  34. package/dist/reference/label.d.ts +11 -0
  35. package/dist/reference/label.js +38 -0
  36. package/dist/reference/lint-ignore.d.ts +32 -0
  37. package/dist/reference/lint-ignore.js +39 -0
  38. package/dist/reference/math-glyphs.mjs +830 -0
  39. package/dist/reference/prose.d.ts +15 -0
  40. package/dist/reference/prose.js +12 -0
  41. package/dist/reference/references.d.ts +17 -0
  42. package/dist/reference/references.js +42 -0
  43. package/dist/reference/registry.d.ts +6 -0
  44. package/dist/reference/registry.js +548 -0
  45. package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
  46. package/dist/reference/remark-citation.mjs +0 -0
  47. package/dist/reference/remark-notation.mjs +753 -0
  48. package/dist/reference/render-lab-math.d.ts +21 -0
  49. package/dist/reference/render-lab-math.js +58 -0
  50. package/dist/reference/types.d.ts +151 -0
  51. package/dist/reference/types.js +0 -0
  52. package/dist/reference/units-vocab.d.ts +26 -0
  53. package/dist/reference/units-vocab.js +119 -0
  54. package/dist/session/PreferenceStore.d.ts +28 -0
  55. package/dist/session/PreferenceStore.js +30 -0
  56. package/dist/session/user-context.d.ts +7 -0
  57. package/dist/session/user-context.js +10 -0
  58. package/dist/session/user.d.ts +21 -0
  59. package/dist/session/user.js +8 -0
  60. package/package.json +53 -0
  61. package/src/analytics/AnalyticsEmitter.ts +23 -0
  62. package/src/compiler/cli.ts +866 -0
  63. package/src/compiler/collections.ts +554 -0
  64. package/src/compiler/course-config.ts +32 -0
  65. package/src/compiler/lesson-derivation.ts +186 -0
  66. package/src/compiler/manifest.ts +601 -0
  67. package/src/compiler/resolution-report.ts +150 -0
  68. package/src/components/CurriculumMap.astro +208 -0
  69. package/src/components/HoverPanel.astro +67 -0
  70. package/src/components/assessments/AssessmentRunner.tsx +320 -0
  71. package/src/components/assessments/AssessmentSet.astro +54 -0
  72. package/src/components/citation/CitationLayer.astro +173 -0
  73. package/src/components/examples/CompactExample.astro +34 -0
  74. package/src/components/examples/CompactExamples.astro +253 -0
  75. package/src/components/hover-panel.ts +273 -0
  76. package/src/components/notation/NotationGlossary.astro +412 -0
  77. package/src/components/notation/NotationLayer.astro +594 -0
  78. package/src/components/notation/NotationMath.astro +24 -0
  79. package/src/components/notation/SafeMathNode.astro +23 -0
  80. package/src/components/notation/render-notation-math.ts +223 -0
  81. package/src/components/starlight/EquationEnhancer.astro +72 -0
  82. package/src/components/starlight/LayoutHeader.astro +102 -0
  83. package/src/components/starlight/LayoutPageSidebar.astro +33 -0
  84. package/src/components/starlight/LayoutSidebar.astro +29 -0
  85. package/src/components/starlight/LessonFooter.astro +58 -0
  86. package/src/content-config.ts +259 -0
  87. package/src/curriculum/validation.ts +481 -0
  88. package/src/progress/ProgressRepository.ts +64 -0
  89. package/src/reference/base-library.mjs +27 -0
  90. package/src/reference/citation-format.mjs +133 -0
  91. package/src/reference/consistency.ts +375 -0
  92. package/src/reference/curriculum-alignment.ts +228 -0
  93. package/src/reference/equations-validate.ts +167 -0
  94. package/src/reference/equations.mjs +144 -0
  95. package/src/reference/gate-math.ts +250 -0
  96. package/src/reference/katex-options.mjs +91 -0
  97. package/src/reference/lab-math-scope.ts +54 -0
  98. package/src/reference/label.ts +47 -0
  99. package/src/reference/lint-ignore.ts +75 -0
  100. package/src/reference/math-glyphs.mjs +830 -0
  101. package/src/reference/prose.ts +21 -0
  102. package/src/reference/references.ts +82 -0
  103. package/src/reference/registry.ts +723 -0
  104. package/src/reference/rehype-fail-katex-errors.mjs +55 -0
  105. package/src/reference/remark-citation.mjs +0 -0
  106. package/src/reference/remark-notation.mjs +753 -0
  107. package/src/reference/render-lab-math.ts +99 -0
  108. package/src/reference/types.ts +182 -0
  109. package/src/reference/units-vocab.ts +143 -0
  110. package/src/session/PreferenceStore.ts +54 -0
  111. package/src/session/user-context.ts +18 -0
  112. package/src/session/user.ts +24 -0
  113. package/src/styles/global.css +763 -0
@@ -0,0 +1,320 @@
1
+ import { useEffect, useId, useState } from 'react';
2
+
3
+ import {
4
+ noopAnalyticsEmitter,
5
+ type AnalyticsEmitter,
6
+ } from '../../analytics/AnalyticsEmitter';
7
+ import {
8
+ noopProgressRepository,
9
+ type AssessmentAttempt,
10
+ type ProgressRepository,
11
+ } from '../../progress/ProgressRepository';
12
+ import { useSessionUser } from '../../session/user-context';
13
+
14
+ interface ChoiceOption {
15
+ id: string;
16
+ label: string;
17
+ }
18
+
19
+ interface AssessmentItemBase {
20
+ id: string;
21
+ competencyId?: string;
22
+ evidenceKind: 'direct' | 'transfer';
23
+ prompt: string;
24
+ explanation: string;
25
+ }
26
+
27
+ interface ChoiceItem extends AssessmentItemBase {
28
+ type: 'single-choice';
29
+ options: ChoiceOption[];
30
+ correctOptionId: string;
31
+ }
32
+
33
+ interface NumericItem extends AssessmentItemBase {
34
+ type: 'numeric';
35
+ answer: { value: number; tolerance: number };
36
+ }
37
+
38
+ type Item = ChoiceItem | NumericItem;
39
+
40
+ type Outcome = 'correct' | 'incorrect';
41
+
42
+ interface Props {
43
+ /** Assessment collection id — scopes recorded attempts and analytics events. */
44
+ assessmentId: string;
45
+ title: string;
46
+ items: Item[];
47
+ /**
48
+ * Seams (Phase H1). Both default to the no-op implementations the static
49
+ * site ships; a future SSR/LMS host injects real ones without changing the
50
+ * component. Props exist so tests can observe the calls.
51
+ */
52
+ progress?: ProgressRepository;
53
+ analytics?: AnalyticsEmitter;
54
+ }
55
+
56
+ const evidenceLabel: Record<AssessmentItemBase['evidenceKind'], string> = {
57
+ direct: 'Direct',
58
+ transfer: 'Transfer',
59
+ };
60
+
61
+ function statusFor(outcome: Outcome | undefined): {
62
+ tone: 'idle' | 'correct' | 'incorrect';
63
+ label: string;
64
+ } {
65
+ if (outcome === 'correct') return { tone: 'correct', label: 'Correct' };
66
+ if (outcome === 'incorrect') return { tone: 'incorrect', label: 'Revisit' };
67
+ return { tone: 'idle', label: 'Not answered' };
68
+ }
69
+
70
+ interface QuestionProps {
71
+ item: Item;
72
+ index: number;
73
+ outcome: Outcome | undefined;
74
+ onResult: (id: string, outcome: Outcome) => void;
75
+ onReset: (id: string) => void;
76
+ }
77
+
78
+ function Question({ item, index, outcome, onResult, onReset }: QuestionProps) {
79
+ const panelId = useId();
80
+ const groupName = useId();
81
+ const inputId = useId();
82
+ const [open, setOpen] = useState(false);
83
+ const [choice, setChoice] = useState<string | null>(null);
84
+ const [numeric, setNumeric] = useState('');
85
+
86
+ const checked = outcome !== undefined;
87
+ const parsed = Number(numeric);
88
+ const numericFilled = numeric.trim() !== '' && Number.isFinite(parsed);
89
+ const canCheck =
90
+ item.type === 'single-choice' ? choice !== null : numericFilled;
91
+
92
+ const evaluate = () => {
93
+ const isCorrect =
94
+ item.type === 'single-choice'
95
+ ? choice === item.correctOptionId
96
+ : numericFilled &&
97
+ Math.abs(parsed - item.answer.value) <= item.answer.tolerance;
98
+ onResult(item.id, isCorrect ? 'correct' : 'incorrect');
99
+ };
100
+
101
+ const retry = () => {
102
+ onReset(item.id);
103
+ setChoice(null);
104
+ setNumeric('');
105
+ };
106
+
107
+ const status = statusFor(outcome);
108
+
109
+ return (
110
+ <li className="assessment-item">
111
+ <h4 className="assessment-item-heading">
112
+ <button
113
+ type="button"
114
+ className="assessment-disclosure"
115
+ aria-expanded={open}
116
+ aria-controls={panelId}
117
+ onClick={() => setOpen((value) => !value)}
118
+ >
119
+ <span className="assessment-kind">
120
+ {evidenceLabel[item.evidenceKind]}
121
+ </span>
122
+ <span className="assessment-item-title">Question {index}</span>
123
+ <span className={`assessment-status is-${status.tone}`}>
124
+ {status.label}
125
+ </span>
126
+ <svg
127
+ className="assessment-chevron"
128
+ viewBox="0 0 16 16"
129
+ width="16"
130
+ height="16"
131
+ aria-hidden="true"
132
+ >
133
+ <path
134
+ d="M4 6l4 4 4-4"
135
+ fill="none"
136
+ stroke="currentColor"
137
+ strokeWidth="2"
138
+ strokeLinecap="round"
139
+ strokeLinejoin="round"
140
+ />
141
+ </svg>
142
+ </button>
143
+ </h4>
144
+
145
+ <div id={panelId} className="assessment-panel" hidden={!open}>
146
+ <p className="assessment-prompt">{item.prompt}</p>
147
+
148
+ {item.type === 'single-choice' ? (
149
+ <fieldset className="assessment-choices">
150
+ <legend className="sr-only">
151
+ Answer options for question {index}
152
+ </legend>
153
+ {item.options.map((option) => (
154
+ <label key={option.id} className="assessment-option">
155
+ <input
156
+ type="radio"
157
+ name={groupName}
158
+ value={option.id}
159
+ checked={choice === option.id}
160
+ disabled={checked}
161
+ onChange={() => setChoice(option.id)}
162
+ />
163
+ {option.label}
164
+ </label>
165
+ ))}
166
+ </fieldset>
167
+ ) : (
168
+ <label className="assessment-numeric" htmlFor={inputId}>
169
+ <span className="sr-only">Numeric answer for question {index}</span>
170
+ <input
171
+ id={inputId}
172
+ type="number"
173
+ inputMode="decimal"
174
+ value={numeric}
175
+ disabled={checked}
176
+ onChange={(event) => setNumeric(event.currentTarget.value)}
177
+ />
178
+ </label>
179
+ )}
180
+
181
+ <div className="assessment-actions">
182
+ {checked ? (
183
+ <button type="button" onClick={retry}>
184
+ Try again
185
+ </button>
186
+ ) : (
187
+ <button type="button" disabled={!canCheck} onClick={evaluate}>
188
+ Check
189
+ </button>
190
+ )}
191
+ </div>
192
+
193
+ <p
194
+ className={
195
+ checked
196
+ ? `assessment-feedback is-${outcome}`
197
+ : 'assessment-feedback is-idle'
198
+ }
199
+ aria-live="polite"
200
+ >
201
+ {checked ? (
202
+ <>
203
+ <strong>
204
+ {outcome === 'correct' ? 'Correct.' : 'Not quite.'}
205
+ </strong>{' '}
206
+ {item.explanation}
207
+ </>
208
+ ) : (
209
+ 'Check your answer to reveal the explanation.'
210
+ )}
211
+ </p>
212
+ </div>
213
+ </li>
214
+ );
215
+ }
216
+
217
+ export default function AssessmentRunner({
218
+ assessmentId,
219
+ title,
220
+ items,
221
+ progress = noopProgressRepository,
222
+ analytics = noopAnalyticsEmitter,
223
+ }: Props) {
224
+ const headingId = useId();
225
+ const user = useSessionUser();
226
+ const [results, setResults] = useState<Record<string, Outcome>>({});
227
+
228
+ const itemById = new Map(items.map((item) => [item.id, item]));
229
+
230
+ // Seed from any previously recorded attempts. The no-op repository returns an
231
+ // empty snapshot, so this is inert in the static build.
232
+ useEffect(() => {
233
+ let live = true;
234
+ void Promise.resolve(progress.load(user)).then((snapshot) => {
235
+ if (!live) return;
236
+ const seeded: Record<string, Outcome> = {};
237
+ for (const attempt of snapshot.attempts) {
238
+ if (
239
+ attempt.assessmentId === assessmentId &&
240
+ itemById.has(attempt.itemId)
241
+ ) {
242
+ seeded[attempt.itemId] = attempt.outcome;
243
+ }
244
+ }
245
+ if (Object.keys(seeded).length > 0) setResults(seeded);
246
+ });
247
+ return () => {
248
+ live = false;
249
+ };
250
+ // `assessmentId` and the repository identity are the only real inputs.
251
+ // eslint-disable-next-line react-hooks/exhaustive-deps
252
+ }, [assessmentId, progress, user]);
253
+
254
+ const answered = Object.keys(results).length;
255
+ const correct = Object.values(results).filter(
256
+ (value) => value === 'correct',
257
+ ).length;
258
+
259
+ const report = (id: string, outcome: Outcome) => {
260
+ setResults((prev) => ({ ...prev, [id]: outcome }));
261
+ const item = itemById.get(id);
262
+ const attempt: AssessmentAttempt = {
263
+ assessmentId,
264
+ itemId: id,
265
+ ...(item?.competencyId ? { competencyId: item.competencyId } : {}),
266
+ evidenceKind: item?.evidenceKind ?? 'direct',
267
+ outcome,
268
+ at: Date.now(),
269
+ };
270
+ void progress.recordAttempt(user, attempt);
271
+ analytics.emit({
272
+ name: 'assessment.attempt',
273
+ at: attempt.at,
274
+ props: {
275
+ assessmentId,
276
+ itemId: id,
277
+ outcome,
278
+ evidenceKind: attempt.evidenceKind,
279
+ },
280
+ });
281
+ };
282
+ const clear = (id: string) => {
283
+ setResults((prev) => {
284
+ const next = { ...prev };
285
+ delete next[id];
286
+ return next;
287
+ });
288
+ void progress.reset(user, { assessmentId, itemId: id });
289
+ analytics.emit({
290
+ name: 'assessment.retry',
291
+ at: Date.now(),
292
+ props: { assessmentId, itemId: id },
293
+ });
294
+ };
295
+
296
+ return (
297
+ <section className="assessment-shell" aria-labelledby={headingId}>
298
+ <div className="assessment-header">
299
+ <h3 id={headingId}>{title}</h3>
300
+ <p className="assessment-progress" aria-live="polite">
301
+ {answered === 0
302
+ ? `${items.length} question${items.length === 1 ? '' : 's'}`
303
+ : `${correct} of ${items.length} correct`}
304
+ </p>
305
+ </div>
306
+ <ol className="assessment-list">
307
+ {items.map((item, position) => (
308
+ <Question
309
+ key={item.id}
310
+ item={item}
311
+ index={position + 1}
312
+ outcome={results[item.id]}
313
+ onResult={report}
314
+ onReset={clear}
315
+ />
316
+ ))}
317
+ </ol>
318
+ </section>
319
+ );
320
+ }
@@ -0,0 +1,54 @@
1
+ ---
2
+ import { getCollection } from 'astro:content';
3
+ import { loadManifest } from '../../compiler/manifest';
4
+ import AssessmentRunner from './AssessmentRunner';
5
+
6
+ interface Props {
7
+ /**
8
+ * Explicit assessment id. Omitted in lesson bodies (Phase C2): the set is
9
+ * resolved from the route's colocated `<lesson>.checks.yml` via the manifest.
10
+ */
11
+ id?: string;
12
+ }
13
+
14
+ const explicitId = Astro.props.id;
15
+
16
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
17
+ const lessonId = Astro.url.pathname
18
+ .replace(new RegExp(`^${base}/`), '')
19
+ .replace(/\/$/, '')
20
+ .replace(/\//g, '.');
21
+
22
+ let ids: string[];
23
+ if (explicitId) {
24
+ ids = [explicitId];
25
+ } else {
26
+ const manifest = await loadManifest();
27
+ ids = [
28
+ ...(manifest.lessons.find((lesson) => lesson.id === lessonId)
29
+ ?.assessments ?? []),
30
+ ];
31
+ }
32
+
33
+ const assessments = await getCollection('assessments');
34
+ const entries = ids.map((id) => {
35
+ const entry = assessments.find((candidate) => candidate.data.id === id);
36
+ if (!entry) {
37
+ throw new Error(
38
+ `AssessmentSet: no assessment collection entry with id "${id}"`,
39
+ );
40
+ }
41
+ return entry;
42
+ });
43
+ ---
44
+
45
+ {
46
+ entries.map((entry) => (
47
+ <AssessmentRunner
48
+ client:visible
49
+ assessmentId={entry.data.id}
50
+ title={entry.data.title}
51
+ items={entry.data.items}
52
+ />
53
+ ))
54
+ }
@@ -0,0 +1,173 @@
1
+ ---
2
+ /**
3
+ * Optional hover/pin panel for the inline `[n]` citation markers that the
4
+ * `remarkCitation` adapter emits. The markers are real anchor links to the
5
+ * on-page "References" list and work with no JavaScript; this island only
6
+ * adds a quick look at the source without leaving the reader's place.
7
+ *
8
+ * It reads everything it shows from the page's own static markup — the
9
+ * marker's `data-*` and the matching `#cite-n` reference-list item — so no
10
+ * page serializes a second copy of the source database. Positioning, pin,
11
+ * Escape, and outside-close come from the shared `HoverPanel` primitive.
12
+ */
13
+ import HoverPanel from '../HoverPanel.astro';
14
+ ---
15
+
16
+ <HoverPanel id="citation-panel" label="Source citation" data-citation-panel>
17
+ <div class="citation-panel-body">
18
+ <p class="citation-panel-label" data-panel-label></p>
19
+ <p class="citation-panel-locator" data-panel-locator hidden></p>
20
+ <p class="citation-panel-status" data-panel-status hidden></p>
21
+ <a class="citation-panel-link" data-panel-link hidden>Open source link</a>
22
+ <a class="citation-panel-jump" data-panel-jump
23
+ >See it in the reference list</a
24
+ >
25
+ <button
26
+ class="citation-panel-pin"
27
+ type="button"
28
+ data-panel-pin
29
+ aria-pressed="false"
30
+ >
31
+ Pin
32
+ </button>
33
+ </div>
34
+ </HoverPanel>
35
+
36
+ <script>
37
+ import { createHoverPanel, closestElement } from '../hover-panel';
38
+ import { referenceLabelFromText } from '../../reference/citation-format.mjs';
39
+
40
+ function setText(element: HTMLElement, value: string) {
41
+ element.textContent = value;
42
+ element.hidden = value.length === 0;
43
+ }
44
+
45
+ function setup() {
46
+ const panel = document.querySelector<HTMLElement>('[data-citation-panel]');
47
+ if (!panel || panel.dataset.hoverPanelReady === 'true') return;
48
+
49
+ const markers = Array.from(
50
+ document.querySelectorAll<HTMLAnchorElement>(
51
+ '.sl-markdown-content a.citation-ref[data-citation-id]',
52
+ ),
53
+ );
54
+ if (markers.length === 0) return;
55
+ const markerSet = new Set<HTMLElement>(markers);
56
+
57
+ const label = panel.querySelector<HTMLElement>('[data-panel-label]')!;
58
+ const locator = panel.querySelector<HTMLElement>('[data-panel-locator]')!;
59
+ const status = panel.querySelector<HTMLElement>('[data-panel-status]')!;
60
+ const link = panel.querySelector<HTMLAnchorElement>('[data-panel-link]')!;
61
+ const jump = panel.querySelector<HTMLAnchorElement>('[data-panel-jump]')!;
62
+ const pin = panel.querySelector<HTMLButtonElement>('[data-panel-pin]')!;
63
+
64
+ const markerFrom = (node: EventTarget | null) => {
65
+ const candidate = closestElement(
66
+ node,
67
+ 'a.citation-ref[data-citation-id]',
68
+ );
69
+ return candidate && markerSet.has(candidate) ? candidate : null;
70
+ };
71
+
72
+ // Fill the panel from the marker plus its `#cite-n` reference-list item —
73
+ // never from a serialized source table.
74
+ const render = (marker: HTMLElement, number: string) => {
75
+ const item = document.getElementById(`cite-${number}`);
76
+ if (!item) return false;
77
+
78
+ // The reference list is a tight list, so the `<li>` has no `<p>`
79
+ // wrapper: clone it and drop the trailing link/status chrome.
80
+ const line = item.cloneNode(true) as HTMLElement;
81
+ for (const extra of line.querySelectorAll(
82
+ '.citation-url, .citation-status, .citation-backref',
83
+ )) {
84
+ extra.remove();
85
+ }
86
+ const referenceText = line.textContent ?? '';
87
+ const loc = marker.dataset.citationLocator ?? '';
88
+
89
+ label.textContent = referenceLabelFromText(referenceText, loc);
90
+ setText(locator, loc);
91
+
92
+ const sourceStatus = item
93
+ .querySelector('.citation-status')
94
+ ?.textContent?.trim();
95
+ setText(status, sourceStatus ? `Source record: ${sourceStatus}` : '');
96
+
97
+ const sourceUrl = item.querySelector<HTMLAnchorElement>('a.citation-url');
98
+ if (sourceUrl) {
99
+ link.href = sourceUrl.href;
100
+ link.hidden = false;
101
+ } else {
102
+ link.hidden = true;
103
+ }
104
+
105
+ jump.href = marker.getAttribute('href') ?? '#references';
106
+ return true;
107
+ };
108
+
109
+ createHoverPanel({
110
+ panel,
111
+ triggerFrom: markerFrom,
112
+ keyFor: (element) => element.dataset.citationNumber ?? null,
113
+ render,
114
+ pinControlFrom: (node) => closestElement(node, '[data-panel-pin]'),
115
+ onSync: ({ pinnedKey }) => {
116
+ const isPinned = pinnedKey !== null;
117
+ pin.setAttribute('aria-pressed', String(isPinned));
118
+ pin.textContent = isPinned ? 'Unpin' : 'Pin';
119
+ },
120
+ });
121
+ }
122
+
123
+ setup();
124
+ document.addEventListener('astro:page-load', setup);
125
+ </script>
126
+
127
+ <style>
128
+ @layer starlight.components {
129
+ .citation-panel-body p {
130
+ margin: 0 0 0.5rem;
131
+ line-height: 1.5;
132
+ }
133
+
134
+ .citation-panel-label {
135
+ font-weight: 600;
136
+ }
137
+
138
+ .citation-panel-locator {
139
+ color: var(--sl-color-gray-2);
140
+ }
141
+
142
+ .citation-panel-status {
143
+ color: var(--sl-color-gray-3);
144
+ font-size: var(--sl-text-xs);
145
+ text-transform: lowercase;
146
+ }
147
+
148
+ .citation-panel-link,
149
+ .citation-panel-jump {
150
+ display: inline-block;
151
+ margin-inline-end: 0.9rem;
152
+ font-size: var(--sl-text-xs);
153
+ }
154
+
155
+ .citation-panel-pin {
156
+ display: block;
157
+ margin-top: 0.6rem;
158
+ padding: 0.2rem 0.6rem;
159
+ border: 1px solid var(--sl-color-gray-5);
160
+ border-radius: 0.35rem;
161
+ background: var(--sl-color-bg);
162
+ color: var(--sl-color-text-accent);
163
+ font: inherit;
164
+ font-size: var(--sl-text-xs);
165
+ cursor: pointer;
166
+ }
167
+
168
+ .citation-panel-pin[aria-pressed='true'] {
169
+ border-color: var(--sl-color-accent);
170
+ background: var(--sl-color-accent-low);
171
+ }
172
+ }
173
+ </style>
@@ -0,0 +1,34 @@
1
+ ---
2
+ interface Props {
3
+ /** Short, descriptive tab label, for example “Mixed signed cash flows”. */
4
+ label: string;
5
+ /** Optional stable anchor for linking directly to this example. */
6
+ id?: string;
7
+ }
8
+
9
+ const { label, id } = Astro.props;
10
+
11
+ if (typeof label !== 'string' || !label.trim()) {
12
+ throw new Error('CompactExample requires a non-empty `label` prop.');
13
+ }
14
+ ---
15
+
16
+ <section
17
+ class="compact-example"
18
+ data-compact-example
19
+ data-example-label={label}
20
+ id={id}
21
+ >
22
+ <h3 class="compact-example__heading">{label}</h3>
23
+ <slot />
24
+ </section>
25
+
26
+ <style>
27
+ .compact-example__heading {
28
+ margin-block-start: 0;
29
+ }
30
+
31
+ .compact-example > :last-child {
32
+ margin-block-end: 0;
33
+ }
34
+ </style>