explico 0.7.0 → 0.11.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 (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -0,0 +1,401 @@
1
+ ---
2
+ /**
3
+ * A part's own page: the chapters it holds, in reading order, beside the
4
+ * dependency rail that says where the part sits in the course.
5
+ *
6
+ * Everything here is read from the compiler manifest and the competency
7
+ * collection — chapter order, titles, descriptions, float counts, and which
8
+ * parts this one builds on or feeds. A course authors the page's prose; it
9
+ * never re-types the list, so a renamed chapter or a new prerequisite cannot
10
+ * leave a stale copy behind on the part page.
11
+ *
12
+ * Usage, in `content/docs/<part>/index.mdx`:
13
+ *
14
+ * ```mdx
15
+ * ---
16
+ * title: 'L1 · Foundations & ANOVA'
17
+ * description: Where the course fixes its prediction target.
18
+ * ---
19
+ *
20
+ * import PartOverview from 'explico/components/PartOverview.astro';
21
+ * import PartScope from 'explico/components/PartScope.astro';
22
+ *
23
+ * <PartScope>
24
+ *
25
+ * Authored introduction — what this part is for. Every part after the first
26
+ * also opens with a `<PartBridge>` paragraph (rule S2).
27
+ *
28
+ * </PartScope>
29
+ *
30
+ * <PartOverview
31
+ * chain={[
32
+ * 'The conditional mean minimises squared error',
33
+ * 'Its residual is orthogonal to every function of X',
34
+ * 'So total variance splits between and within',
35
+ * ]}
36
+ * />
37
+ * ```
38
+ */
39
+ import { getCollection } from 'astro:content';
40
+ import { loadManifest, type ManifestPart } from '../compiler/manifest';
41
+
42
+ interface Props {
43
+ /**
44
+ * The part directory. Defaults to the directory of the page being rendered,
45
+ * which is what a part's own `index.mdx` wants.
46
+ */
47
+ readonly part?: string;
48
+ /**
49
+ * The part's argument in order, one step per line — the thread that runs
50
+ * through its chapters. Authored, because no manifest field knows why one
51
+ * chapter follows another.
52
+ */
53
+ readonly chain?: readonly string[];
54
+ /** Set `false` to drop the rail and keep only the reading list. */
55
+ readonly rail?: boolean;
56
+ }
57
+
58
+ const { part: partProp, chain = [], rail = true } = Astro.props;
59
+
60
+ const routeSlug = Astro.locals.starlightRoute.entry.id;
61
+ const partDir = partProp ?? routeSlug.replace(/\/index$/, '').split('/')[0];
62
+
63
+ const [manifest, competencyEntries] = await Promise.all([
64
+ loadManifest(),
65
+ getCollection('competencies'),
66
+ ]);
67
+
68
+ const part = manifest.parts.find((entry) => entry.dir === partDir);
69
+ if (!part) {
70
+ throw new Error(
71
+ `<PartOverview> found no part "${partDir}". Parts come from ` +
72
+ '`courseConfig.parts`; pass `part="<dir>"` when rendering this ' +
73
+ 'component outside a part directory.',
74
+ );
75
+ }
76
+
77
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
78
+ const lessonById = new Map(
79
+ manifest.lessons.map((lesson) => [lesson.id, lesson]),
80
+ );
81
+ const chapters = part.lessons
82
+ .map((id) => lessonById.get(id))
83
+ .filter((lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined);
84
+
85
+ const competencyById = new Map(
86
+ competencyEntries.map(({ data }) => [data.id, data]),
87
+ );
88
+
89
+ const taught = new Set(chapters.flatMap((lesson) => lesson.teaches));
90
+ const required = new Set(
91
+ chapters.flatMap((lesson) => lesson.requires).filter((id) => !taught.has(id)),
92
+ );
93
+
94
+ /** The part each competency is taught in — how a rail edge finds its page. */
95
+ const partByCompetency = new Map<string, ManifestPart>();
96
+ for (const other of manifest.parts) {
97
+ for (const lessonId of other.lessons) {
98
+ for (const competencyId of lessonById.get(lessonId)?.teaches ?? []) {
99
+ if (!partByCompetency.has(competencyId)) {
100
+ partByCompetency.set(competencyId, other);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ const partsFor = (competencyIds: Iterable<string>): ManifestPart[] => {
107
+ const seen = new Map<string, ManifestPart>();
108
+ for (const id of competencyIds) {
109
+ const owner = partByCompetency.get(id);
110
+ if (owner && owner.dir !== part.dir) seen.set(owner.dir, owner);
111
+ }
112
+ return [...seen.values()].sort((a, b) => a.ordinal - b.ordinal);
113
+ };
114
+
115
+ const buildsOn = partsFor(required);
116
+ const leadsTo = manifest.parts
117
+ .filter((other) => other.dir !== part.dir)
118
+ .filter((other) =>
119
+ other.lessons.some((lessonId) =>
120
+ (lessonById.get(lessonId)?.requires ?? []).some((id) => taught.has(id)),
121
+ ),
122
+ )
123
+ .sort((a, b) => a.ordinal - b.ordinal);
124
+
125
+ const outcomes = [...taught]
126
+ .map((id) => competencyById.get(id)?.outcome)
127
+ .filter((outcome): outcome is string => typeof outcome === 'string');
128
+
129
+ const equationCount = chapters.reduce(
130
+ (total, lesson) => total + lesson.equations.length,
131
+ 0,
132
+ );
133
+ const checkCount = chapters.reduce(
134
+ (total, lesson) => total + lesson.knowledgeChecks.length,
135
+ 0,
136
+ );
137
+
138
+ const partLink = (target: ManifestPart) =>
139
+ target.overview ? `${base}${target.link}` : undefined;
140
+
141
+ /** `30–40 min`, or `40 min` when the estimate is a single number. */
142
+ const minutes = (range: { min: number; max: number }) =>
143
+ range.min === range.max
144
+ ? `${range.min} min`
145
+ : `${range.min}–${range.max} min`;
146
+ const hasRail =
147
+ rail &&
148
+ (chain.length > 0 ||
149
+ outcomes.length > 0 ||
150
+ buildsOn.length > 0 ||
151
+ leadsTo.length > 0);
152
+ ---
153
+
154
+ <div class="part-overview not-content" data-rail={hasRail ? '' : undefined}>
155
+ <p class="part-meta">
156
+ <span>Part {part.ordinal} of {manifest.parts.length}</span>
157
+ <span>{chapters.length} {chapters.length === 1 ? 'chapter' : 'chapters'}</span>
158
+ {part.studyMinutes && <span>{minutes(part.studyMinutes)}</span>}
159
+ {equationCount > 0 && <span>{equationCount} numbered equations</span>}
160
+ {
161
+ checkCount > 0 && (
162
+ <span>
163
+ {checkCount} knowledge {checkCount === 1 ? 'check' : 'checks'}
164
+ </span>
165
+ )
166
+ }
167
+ </p>
168
+
169
+ <div class="part-body">
170
+ <ol class="part-chapters">
171
+ {
172
+ chapters.map((lesson) => (
173
+ <li>
174
+ <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
175
+ <p>{lesson.description}</p>
176
+ <p class="chapter-meta">
177
+ {lesson.studyMinutes && (
178
+ <span class="chapter-time">{minutes(lesson.studyMinutes)}</span>
179
+ )}
180
+ {lesson.equations.length > 0 && (
181
+ <span>
182
+ {lesson.equations.length} equation
183
+ {lesson.equations.length === 1 ? '' : 's'}
184
+ </span>
185
+ )}
186
+ {lesson.knowledgeChecks.length > 0 && (
187
+ <span>
188
+ {lesson.knowledgeChecks.length} check
189
+ {lesson.knowledgeChecks.length === 1 ? '' : 's'}
190
+ </span>
191
+ )}
192
+ {lesson.status !== 'reviewed' && <span>{lesson.status}</span>}
193
+ </p>
194
+ </li>
195
+ ))
196
+ }
197
+ </ol>
198
+
199
+ {
200
+ hasRail && (
201
+ <aside class="part-rail ex-tier-meta" aria-label={`About ${part.label}`}>
202
+ {chain.length > 0 && (
203
+ <section>
204
+ <h2>The argument</h2>
205
+ <ol class="part-chain">
206
+ {chain.map((step) => (
207
+ <li>{step}</li>
208
+ ))}
209
+ </ol>
210
+ </section>
211
+ )}
212
+ {outcomes.length > 0 && (
213
+ <section>
214
+ <h2>After this part you can</h2>
215
+ <ul>
216
+ {outcomes.map((outcome) => (
217
+ <li>{outcome}</li>
218
+ ))}
219
+ </ul>
220
+ </section>
221
+ )}
222
+ {buildsOn.length > 0 && (
223
+ <section>
224
+ <h2>Builds on</h2>
225
+ <ul>
226
+ {buildsOn.map((other) => (
227
+ <li>
228
+ {partLink(other) ? (
229
+ <a href={partLink(other)}>{other.label}</a>
230
+ ) : (
231
+ other.label
232
+ )}
233
+ </li>
234
+ ))}
235
+ </ul>
236
+ </section>
237
+ )}
238
+ {leadsTo.length > 0 && (
239
+ <section>
240
+ <h2>Leads to</h2>
241
+ <ul>
242
+ {leadsTo.map((other) => (
243
+ <li>
244
+ {partLink(other) ? (
245
+ <a href={partLink(other)}>{other.label}</a>
246
+ ) : (
247
+ other.label
248
+ )}
249
+ </li>
250
+ ))}
251
+ </ul>
252
+ </section>
253
+ )}
254
+ </aside>
255
+ )
256
+ }
257
+ </div>
258
+ </div>
259
+
260
+ <style>
261
+ .part-overview {
262
+ margin-block-start: 1.5rem;
263
+ }
264
+
265
+ .part-meta {
266
+ display: flex;
267
+ flex-wrap: wrap;
268
+ gap: 0.3rem 1rem;
269
+ margin: 0 0 1.25rem;
270
+ color: var(--sl-color-gray-3);
271
+ font-size: var(--sl-text-xs);
272
+ letter-spacing: 0.04em;
273
+ text-transform: uppercase;
274
+ }
275
+
276
+ .part-body {
277
+ display: grid;
278
+ gap: 1.75rem;
279
+ align-items: start;
280
+ }
281
+
282
+ .part-overview[data-rail] .part-body {
283
+ grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
284
+ }
285
+
286
+ @media (max-width: 62rem) {
287
+ .part-overview[data-rail] .part-body {
288
+ grid-template-columns: minmax(0, 1fr);
289
+ }
290
+ }
291
+
292
+ .part-chapters {
293
+ counter-reset: chapter;
294
+ margin: 0;
295
+ padding: 0;
296
+ list-style: none;
297
+ border-block-start: 1px solid var(--sl-color-gray-5);
298
+ }
299
+
300
+ .part-chapters > li {
301
+ counter-increment: chapter;
302
+ display: grid;
303
+ grid-template-columns: 2.25rem minmax(0, 1fr);
304
+ gap: 0.15rem 0.5rem;
305
+ margin: 0;
306
+ padding-block: 0.85rem;
307
+ border-block-end: 1px solid var(--sl-color-gray-5);
308
+ }
309
+
310
+ .part-chapters > li::before {
311
+ content: counter(chapter, decimal-leading-zero);
312
+ grid-row: 1 / span 3;
313
+ color: var(--sl-color-text-accent);
314
+ font-variant-numeric: tabular-nums;
315
+ font-size: var(--sl-text-xs);
316
+ line-height: 1.7;
317
+ }
318
+
319
+ .part-chapters > li > a {
320
+ font-weight: 600;
321
+ text-decoration: none;
322
+ }
323
+
324
+ .part-chapters > li > a:hover,
325
+ .part-chapters > li > a:focus-visible {
326
+ text-decoration: underline;
327
+ }
328
+
329
+ .part-chapters > li > p {
330
+ margin: 0;
331
+ color: var(--sl-color-gray-2);
332
+ font-size: var(--sl-text-sm);
333
+ line-height: 1.5;
334
+ }
335
+
336
+ .chapter-meta {
337
+ display: flex;
338
+ flex-wrap: wrap;
339
+ gap: 0.2rem 0.75rem;
340
+ color: var(--sl-color-gray-3);
341
+ font-size: var(--sl-text-xs);
342
+ }
343
+
344
+ .chapter-meta:empty {
345
+ display: none;
346
+ }
347
+
348
+ /* The estimate is the number a reader plans around, so it leads the row. */
349
+ .chapter-time {
350
+ color: var(--sl-color-text-accent);
351
+ font-variant-numeric: tabular-nums;
352
+ }
353
+
354
+ /* Part logistics: the meta tier (`ex-tier-meta`, `surfaces.css`) draws
355
+ the rail's surface, border and radius. */
356
+ .part-rail {
357
+ display: grid;
358
+ gap: 1rem;
359
+ padding: 1rem 1.1rem;
360
+ }
361
+
362
+ .part-rail section {
363
+ display: grid;
364
+ gap: 0.4rem;
365
+ }
366
+
367
+ .part-rail h2 {
368
+ margin: 0;
369
+ color: var(--sl-color-gray-3);
370
+ font-size: var(--sl-text-xs);
371
+ font-weight: 500;
372
+ letter-spacing: 0.07em;
373
+ text-transform: uppercase;
374
+ }
375
+
376
+ .part-rail ul,
377
+ .part-rail ol {
378
+ margin: 0;
379
+ padding-inline-start: 1.1rem;
380
+ display: grid;
381
+ gap: 0.3rem;
382
+ font-size: var(--sl-text-sm);
383
+ line-height: 1.45;
384
+ }
385
+
386
+ .part-rail li {
387
+ margin: 0;
388
+ }
389
+
390
+ .part-chain {
391
+ list-style: none;
392
+ padding-inline-start: 0;
393
+ }
394
+
395
+ .part-chain > li + li::before {
396
+ content: '\2193';
397
+ display: block;
398
+ color: var(--sl-color-gray-4);
399
+ line-height: 1.1;
400
+ }
401
+ </style>
@@ -0,0 +1,27 @@
1
+ ---
2
+ /**
3
+ * The paragraph on a part's overview page that says what this part itself
4
+ * covers — the question it answers, and how its chapters answer it. Rule S2
5
+ * requires one on every part page, after `<PartBridge>` when there is one;
6
+ * `gate-part-pages.ts` checks that it holds one paragraph within the course's
7
+ * `partPages.scope` word range.
8
+ *
9
+ * Usage, in `content/docs/<part>/index.mdx`:
10
+ *
11
+ * ```mdx
12
+ * import PartScope from 'explico/components/PartScope.astro';
13
+ *
14
+ * <PartScope>
15
+ *
16
+ * This part applies those results to one problem: a projectile launched over
17
+ * level ground. …
18
+ *
19
+ * </PartScope>
20
+ * ```
21
+ *
22
+ * Keep a blank line inside each tag, so the paragraph is parsed as Markdown
23
+ * and its math and `[[key]]` references render.
24
+ */
25
+ ---
26
+
27
+ <div class="part-intro part-scope" data-part-intro="scope"><slot /></div>
@@ -1,18 +1,18 @@
1
1
  ---
2
2
  /**
3
- * Hover/pin panel for the inline abbreviation links `remarkAbbreviations`
4
- * emits (N14, close sibling of `CitationLayer.astro`). The links are real
5
- * anchors to the glossary's "Abbreviations" section and work with no
6
- * JavaScript; this island only adds a quick look at the resolution without
7
- * leaving the reader's place.
3
+ * Cards for the inline abbreviation links `remarkAbbreviations` emits (N14,
4
+ * close sibling of `CitationLayer.astro`). The links are real anchors to the
5
+ * glossary's "Abbreviations" section and work with no JavaScript; this
6
+ * island only adds a quick look at the resolution without leaving the
7
+ * reader's place.
8
8
  *
9
9
  * The abbreviations table is small by nature (a name + one-line resolution
10
10
  * each), so unlike the math-notation layer this renders every entry
11
11
  * unconditionally on every lesson page — one hidden block per entry — rather
12
- * than computing a per-page dependency closure. The panel reads its content
12
+ * than computing a per-page dependency closure. A card reads its content
13
13
  * from that static markup, never from a second serialized copy of the table.
14
- * Positioning, pin, Escape, and outside-close come from the shared
15
- * `HoverPanel` primitive.
14
+ * Opening, pinning, Escape and placement come from the card manager
15
+ * (`../card-manager.ts`).
16
16
  */
17
17
  import { getCollection } from 'astro:content';
18
18
  import HoverPanel from '../HoverPanel.astro';
@@ -21,11 +21,11 @@ const entries = await getCollection('abbreviations');
21
21
  ---
22
22
 
23
23
  <HoverPanel id="abbreviation-panel" label="Abbreviation" data-abbreviation-panel>
24
+ <p class="abbreviation-panel-term" data-panel-term slot="heading"></p>
24
25
  <div class="abbreviation-panel-body">
25
- <p class="abbreviation-panel-term" data-panel-term></p>
26
26
  <p class="abbreviation-panel-expansion" data-panel-expansion></p>
27
27
  <a class="abbreviation-panel-jump" data-panel-jump
28
- >See it in the glossary</a
28
+ >Go to the glossary<span aria-hidden="true" data-jump-arrow> →</span></a
29
29
  >
30
30
  </div>
31
31
  </HoverPanel>
@@ -39,55 +39,52 @@ const entries = await getCollection('abbreviations');
39
39
  }
40
40
 
41
41
  <script>
42
- import { createHoverPanel, closestElement } from '../hover-panel';
42
+ import { cloneCardShell, registerCardSource } from '../card-manager';
43
+ import { wireJumpArrow } from '../card-jump';
43
44
 
44
- function setup() {
45
- const panel = document.querySelector<HTMLElement>(
46
- '[data-abbreviation-panel]',
47
- );
48
- if (!panel || panel.dataset.hoverPanelReady === 'true') return;
49
-
50
- const markers = Array.from(
51
- document.querySelectorAll<HTMLAnchorElement>(
52
- '.sl-markdown-content a.abbreviation-term[data-abbr-key]',
53
- ),
54
- );
55
- if (markers.length === 0) return;
56
- const markerSet = new Set<HTMLElement>(markers);
57
-
58
- const term = panel.querySelector<HTMLElement>('[data-panel-term]')!;
59
- const expansion = panel.querySelector<HTMLElement>(
60
- '[data-panel-expansion]',
61
- )!;
62
- const jump = panel.querySelector<HTMLAnchorElement>('[data-panel-jump]')!;
63
-
64
- const markerFrom = (node: EventTarget | null) => {
65
- const candidate = closestElement(
66
- node,
67
- 'a.abbreviation-term[data-abbr-key]',
68
- );
69
- return candidate && markerSet.has(candidate) ? candidate : null;
70
- };
45
+ function closestElement(node: EventTarget | null, selector: string) {
46
+ return node instanceof Element ? node.closest<HTMLElement>(selector) : null;
47
+ }
71
48
 
72
- // Fill the panel from the matching hidden `[data-abbr-entry]` block —
73
- // never from a second serialized copy of the table.
74
- const render = (marker: HTMLElement, key: string) => {
75
- const entry = document.querySelector<HTMLElement>(
76
- `[data-abbr-entry="${CSS.escape(key)}"]`,
77
- );
78
- if (!entry) return false;
49
+ const entryFor = (key: string) =>
50
+ document.querySelector<HTMLElement>(`[data-abbr-entry="${CSS.escape(key)}"]`);
51
+
52
+ function setup() {
53
+ const template = document.getElementById('abbreviation-panel');
54
+ if (
55
+ !(template instanceof HTMLTemplateElement) ||
56
+ template.dataset.cardSourceReady === 'true' ||
57
+ !document.querySelector('a.abbreviation-term[data-abbr-key]')
58
+ ) {
59
+ return;
60
+ }
61
+ template.dataset.cardSourceReady = 'true';
62
+
63
+ // Fill a card from the matching hidden `[data-abbr-entry]` block — never
64
+ // from a second serialized copy of the table.
65
+ const render = (key: string, marker: HTMLElement) => {
66
+ const entry = entryFor(key);
67
+ const el = cloneCardShell(template);
68
+ const term = el?.querySelector<HTMLElement>('[data-panel-term]');
69
+ const expansion = el?.querySelector<HTMLElement>('[data-panel-expansion]');
70
+ const jump = el?.querySelector<HTMLAnchorElement>('[data-panel-jump]');
71
+ if (!entry || !el || !term || !expansion || !jump) return null;
79
72
 
80
73
  term.textContent = marker.textContent ?? '';
81
74
  expansion.textContent = entry.textContent ?? '';
82
75
  jump.href = marker.getAttribute('href') ?? '#abbreviations';
83
- return true;
76
+ wireJumpArrow(jump);
77
+ el.setAttribute('aria-label', `Abbreviation: ${term.textContent}`);
78
+ return el;
84
79
  };
85
80
 
86
- createHoverPanel({
87
- panel,
88
- groupId: 'abbreviation',
89
- triggerFrom: markerFrom,
90
- keyFor: (element) => element.dataset.abbrKey ?? null,
81
+ registerCardSource({
82
+ layer: 'abbreviation',
83
+ triggerFrom: (node) => closestElement(node, 'a.abbreviation-term[data-abbr-key]'),
84
+ itemFor: (marker) => {
85
+ const key = marker.dataset.abbrKey;
86
+ return key && entryFor(key) ? key : null;
87
+ },
91
88
  render,
92
89
  });
93
90
  }
@@ -116,6 +113,7 @@ const entries = await getCollection('abbreviations');
116
113
  }
117
114
 
118
115
  .abbreviation-panel-term {
116
+ margin: 0;
119
117
  font-weight: 600;
120
118
  }
121
119
 
@@ -332,11 +332,6 @@ export default function AssessmentRunner({
332
332
  return (
333
333
  <section className="assessment-shell" aria-labelledby={headingId}>
334
334
  {!labelledBy && <h3 id={headingId}>{title}</h3>}
335
- <p className="assessment-progress" aria-live="polite">
336
- {answered === 0
337
- ? `${items.length} question${items.length === 1 ? '' : 's'}`
338
- : `${correct} of ${items.length} correct`}
339
- </p>
340
335
  <ol className="assessment-list">
341
336
  {items.map((item, position) => (
342
337
  <Question
@@ -350,6 +345,14 @@ export default function AssessmentRunner({
350
345
  />
351
346
  ))}
352
347
  </ol>
348
+ {/* The score, once there is one. Empty until the first answer — the
349
+ list already shows how many questions there are — and under the
350
+ list, so appearing never pushes down the question just answered.
351
+ The live region stays in the tree while empty, so the first score
352
+ is announced. */}
353
+ <p className="assessment-progress" aria-live="polite">
354
+ {answered === 0 ? '' : `${correct} of ${items.length} correct`}
355
+ </p>
353
356
  </section>
354
357
  );
355
358
  }
@@ -1,10 +1,12 @@
1
1
  ---
2
2
  import { getCollection } from 'astro:content';
3
3
  import { loadManifest } from '../../compiler/manifest';
4
+ import { DISCLOSURE_DEFAULTS } from '../../compiler/course-config';
4
5
  import type { ContentSegment } from '../../reference/mixed-content';
5
6
  import { renderNotationMath } from '../notation/render-notation-math';
6
7
  import type { MixedContentNode } from '../notation/render-notation-math';
7
8
  import { knowledgeCheckAnchorId } from '../../reference/knowledge-checks.mjs';
9
+ import BlockHeading from '../BlockHeading.astro';
8
10
  import AssessmentRunner from './AssessmentRunner';
9
11
 
10
12
  interface Props {
@@ -26,6 +28,9 @@ const lessonId = Astro.url.pathname
26
28
  .replace(/\//g, '.');
27
29
 
28
30
  const manifest = await loadManifest();
31
+ // A check folds like a section, by the course's `disclosure.knowledgeChecks`
32
+ // (collapsible and open unless the course says otherwise).
33
+ const { knowledgeChecks } = manifest.disclosure ?? DISCLOSURE_DEFAULTS;
29
34
  const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
30
35
  if (!lesson) {
31
36
  throw new Error(
@@ -55,7 +60,9 @@ const entries = ids.map((id, index) => {
55
60
  lesson.numbering.checks + lesson.assessments.length + index + 1;
56
61
  const number =
57
62
  publishedNumber ??
58
- (lesson.part > 0 ? `${lesson.part}.${localIndex}` : String(index + 1));
63
+ (lesson.numbering.prefix
64
+ ? `${lesson.numbering.prefix}.${localIndex}`
65
+ : String(index + 1));
59
66
  return { entry, number, anchorId: knowledgeCheckAnchorId(id) };
60
67
  });
61
68
 
@@ -85,34 +92,24 @@ function renderedContent(
85
92
  }
86
93
  ---
87
94
 
95
+ {/* The heading is the engine's (`BlockHeading.astro`): the "Knowledge
96
+ check" badge and the number before the assessment file's `title`.
97
+ `card="accent"`: the check is author-marked enrichment, so its heading and
98
+ questions sit in one accent card (`surfaces.css`), drawn by
99
+ `CollapsibleSections.astro` the way a `[card]` heading's section is. */}
88
100
  {
89
101
  entries.map(({ entry, number, anchorId }) => (
90
102
  <>
91
- <div
92
- class="sl-heading-wrapper level-h2 assessment-heading-wrapper"
93
- data-collapsible="true"
94
- data-default-open="true"
95
- >
96
- <h2 id={anchorId}>
97
- <span class="assessment-heading-number">Knowledge check {number}</span>
98
- <span aria-hidden="true"> — </span>
99
- {entry.data.title}
100
- </h2>
101
- <a
102
- class="sl-anchor-link"
103
- href={`#${anchorId}`}
104
- aria-label={`Knowledge check ${number} titled “${entry.data.title}”`}
105
- >
106
- <span aria-hidden="true" class="sl-anchor-icon">
107
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
108
- <path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42" />
109
- </svg>
110
- </span>
111
- <span class="sr-only" data-pagefind-ignore="true">
112
- Link to knowledge check {number}: {entry.data.title}
113
- </span>
114
- </a>
115
- </div>
103
+ <BlockHeading
104
+ kind="check"
105
+ anchorId={anchorId}
106
+ title={entry.data.title}
107
+ number={number}
108
+ collapsible={knowledgeChecks.collapsible}
109
+ defaultOpen={knowledgeChecks.defaultOpen}
110
+ card="accent"
111
+ class="assessment-heading-wrapper"
112
+ />
116
113
  <AssessmentRunner
117
114
  client:visible
118
115
  assessmentId={entry.data.id}