explico 0.8.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 (230) hide show
  1. package/CHANGELOG.md +1239 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  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 +4 -1
  15. package/dist/compiler/logging.js +4 -1
  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 +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +2 -2
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1096 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1471 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -0,0 +1,113 @@
1
+ ---
2
+ /**
3
+ * The `<h2>` a knowledge check (`AssessmentSet.astro`) and a worked-example
4
+ * set (`CompactExamples.astro`) draw where their tag stands
5
+ * (docs/plans/titles-and-containers.md Part 6, Q11). The author writes only
6
+ * the topic; the engine names the kind and the number:
7
+ *
8
+ * KNOWLEDGE CHECK 1.1.1
9
+ * ✓ Position and coordinates
10
+ *
11
+ * The kind's icon leads the title, which is one line of inline text, not a
12
+ * flex row, so it stays whole when it fits and wraps word by word when it
13
+ * does not. The badge — the kind's word and the number — is a small label in
14
+ * the card's top-right corner (`global.css`), out of the text flow, so the
15
+ * title keeps the whole line. Its source order still names the heading
16
+ * "Knowledge check 1.1.1 Position and coordinates". The word is for a screen
17
+ * reader only when the course sets `blockLabels: "icon"`. The number shows,
18
+ * and hides, by the section rules (`global.css`: on a wide screen, in print
19
+ * and on a folded bar; never when `numbering.sections.show` is off, which
20
+ * also leaves `data-‹kind›-number` off the heading, so no number-form alias
21
+ * resolves to it).
22
+ *
23
+ * The wrapper is the same `.sl-heading-wrapper` a Markdown heading gets
24
+ * (`rehype-heading-anchors.mjs`), chain icon included, so
25
+ * `CollapsibleSections.astro` folds and cards it and `HeadingAnchorEnhancer`
26
+ * copies its link. "On this page" lists it through the manifest's
27
+ * `titledBlocks` (`LayoutPageSidebar.astro`), by its title with the badge's
28
+ * icon — the same two icons as below, as CSS masks (`--block-kind-icon` in
29
+ * `global.css`).
30
+ */
31
+ import { BLOCK_KINDS, blockHeadingSettings, type BlockKind } from './block-heading';
32
+
33
+ interface Props {
34
+ kind: BlockKind;
35
+ /** The heading's `id`: `check-‹id›` or `ex-‹id›`. */
36
+ anchorId: string;
37
+ /** The author's topical title. */
38
+ title: string;
39
+ /** The block's number (`1.1.1`), when it has one. */
40
+ number?: string | undefined;
41
+ /** Whether the section folds (`data-collapsible`). */
42
+ collapsible: boolean;
43
+ /** A folding section's first-visit state (`data-default-open`). */
44
+ defaultOpen: boolean;
45
+ /** Keep the reader's open/closed choice (`data-remember-state`). Default `true`. */
46
+ rememberState?: boolean;
47
+ /** `data-card`: `CollapsibleSections` cards heading and section in this tier. */
48
+ card?: 'accent' | undefined;
49
+ /** `data-section-to-end`: the section is the rest of the block's container. */
50
+ sectionToEnd?: boolean;
51
+ /** Extra classes on the wrapper. */
52
+ class?: string;
53
+ }
54
+
55
+ const {
56
+ kind,
57
+ anchorId,
58
+ title,
59
+ number,
60
+ collapsible,
61
+ defaultOpen,
62
+ rememberState = true,
63
+ card,
64
+ sectionToEnd = false,
65
+ class: className,
66
+ } = Astro.props;
67
+
68
+ const { labels, showNumbers } = await blockHeadingSettings();
69
+ const { word, numberAttribute } = BLOCK_KINDS[kind];
70
+ const shown = showNumbers && number ? number : undefined;
71
+ const name = shown ? `${word} ${shown}: ${title}` : `${word}: ${title}`;
72
+ ---
73
+
74
+ <div
75
+ class:list={['sl-heading-wrapper', 'level-h2', 'block-heading-wrapper', className]}
76
+ data-collapsible={String(collapsible)}
77
+ data-default-open={String(defaultOpen)}
78
+ data-remember-state={String(rememberState)}
79
+ data-card={card}
80
+ {...sectionToEnd ? { 'data-section-to-end': '' } : {}}
81
+ >
82
+ <h2
83
+ id={anchorId}
84
+ class="block-heading"
85
+ data-block-kind={kind}
86
+ {...shown ? { [numberAttribute]: shown } : {}}
87
+ >
88
+ <span class:list={['block-badge', `block-badge--${kind}`]} data-labels={labels}
89
+ ><span class:list={['block-badge__word', { 'sr-only': labels === 'icon' }]}>{word}</span
90
+ >{shown && <span class="block-heading__number">{` ${shown}`}</span>}</span
91
+ >{' '}{
92
+ kind === 'check' ? (
93
+ <svg class="block-heading__icon" viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
94
+ <path d="M2 8.5l4 4 8-9" />
95
+ </svg>
96
+ ) : (
97
+ <svg class="block-heading__icon" viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
98
+ <rect x="2" y="2" width="12" height="12" rx="2" />
99
+ <path d="M5 8h6M8 5v6" />
100
+ </svg>
101
+ )
102
+ }<span class="block-heading__title">{title}</span>
103
+ </h2>
104
+ <a class="sl-anchor-link" href={`#${anchorId}`}
105
+ ><span aria-hidden="true" class="sl-anchor-icon"
106
+ ><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
107
+ ><path
108
+ 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
+ ></path></svg
110
+ ></span
111
+ ><span class="sr-only" data-pagefind-ignore>Link to {name}</span></a
112
+ >
113
+ </div>
@@ -0,0 +1,221 @@
1
+ ---
2
+ /**
3
+ * Every chapter of the course, grouped under its part — the landing page's
4
+ * answer to "what is in here, and how long is it?".
5
+ *
6
+ * A hero that links only the first chapter hides the other eighteen, and a
7
+ * hand-written list of them goes stale the first time a chapter is renamed.
8
+ * This reads the manifest: parts in config order, chapters in reading order,
9
+ * each part heading linking its overview page where one exists, and study
10
+ * times totalled per part and for the course when every chapter states one.
11
+ *
12
+ * Usage, on a splash page:
13
+ *
14
+ * ```mdx
15
+ * import CourseContents from 'explico/components/CourseContents.astro';
16
+ *
17
+ * <CourseContents />
18
+ * ```
19
+ */
20
+ import { loadManifest } from '../compiler/manifest';
21
+
22
+ interface Props {
23
+ /** Heading level for each part's name. Defaults to `h2`. */
24
+ readonly headingLevel?: 2 | 3;
25
+ /** Set `false` to drop the per-chapter description column. */
26
+ readonly descriptions?: boolean;
27
+ }
28
+
29
+ const { headingLevel = 2, descriptions = true } = Astro.props;
30
+ const Heading = `h${headingLevel}` as 'h2' | 'h3';
31
+
32
+ const manifest = await loadManifest();
33
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
34
+ const lessonById = new Map(
35
+ manifest.lessons.map((lesson) => [lesson.id, lesson]),
36
+ );
37
+
38
+ const parts = manifest.parts
39
+ .map((part) => ({
40
+ ...part,
41
+ chapters: part.lessons
42
+ .map((id) => lessonById.get(id))
43
+ .filter(
44
+ (lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined,
45
+ ),
46
+ }))
47
+ .filter((part) => part.chapters.length > 0);
48
+
49
+ const chapterCount = parts.reduce(
50
+ (total, part) => total + part.chapters.length,
51
+ 0,
52
+ );
53
+ // Same rule as a part's own total: state one only when nothing is missing
54
+ // from it.
55
+ const courseMinutes = parts.every((part) => part.studyMinutes !== undefined)
56
+ ? parts.reduce(
57
+ (total, part) => ({
58
+ min: total.min + part.studyMinutes!.min,
59
+ max: total.max + part.studyMinutes!.max,
60
+ }),
61
+ { min: 0, max: 0 },
62
+ )
63
+ : undefined;
64
+
65
+ const minutes = (range: { min: number; max: number }) =>
66
+ range.min === range.max
67
+ ? `${range.min} min`
68
+ : `${range.min}–${range.max} min`;
69
+
70
+ const hours = (range: { min: number; max: number }) => {
71
+ const low = Math.round((range.min / 60) * 10) / 10;
72
+ const high = Math.round((range.max / 60) * 10) / 10;
73
+ return low === high ? `${low} hours` : `${low}–${high} hours`;
74
+ };
75
+ ---
76
+
77
+ <div class="course-contents not-content">
78
+ <p class="course-meta">
79
+ <span>{parts.length} parts</span>
80
+ <span>{chapterCount} chapters</span>
81
+ {courseMinutes && <span>{hours(courseMinutes)} of study</span>}
82
+ </p>
83
+
84
+ {
85
+ parts.map((part) => (
86
+ <section class="course-part">
87
+ <Heading id={`contents-${part.dir}`}>
88
+ {part.overview ? (
89
+ <a href={`${base}${part.link}`}>{part.label}</a>
90
+ ) : (
91
+ part.label
92
+ )}
93
+ </Heading>
94
+ <p class="part-line">
95
+ <span>
96
+ Part {part.ordinal} · {part.chapters.length}{' '}
97
+ {part.chapters.length === 1 ? 'chapter' : 'chapters'}
98
+ </span>
99
+ {part.studyMinutes && <span>{minutes(part.studyMinutes)}</span>}
100
+ </p>
101
+ <ol aria-labelledby={`contents-${part.dir}`}>
102
+ {part.chapters.map((lesson) => (
103
+ <li>
104
+ <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
105
+ {lesson.studyMinutes && (
106
+ <span class="chapter-time">{minutes(lesson.studyMinutes)}</span>
107
+ )}
108
+ {descriptions && <p>{lesson.description}</p>}
109
+ </li>
110
+ ))}
111
+ </ol>
112
+ </section>
113
+ ))
114
+ }
115
+ </div>
116
+
117
+ <style>
118
+ .course-contents {
119
+ display: grid;
120
+ gap: 1.75rem;
121
+ margin-block-start: 1.5rem;
122
+ }
123
+
124
+ .course-meta,
125
+ .part-line {
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ gap: 0.25rem 1rem;
129
+ margin: 0;
130
+ color: var(--sl-color-gray-3);
131
+ font-size: var(--sl-text-xs);
132
+ letter-spacing: 0.04em;
133
+ text-transform: uppercase;
134
+ }
135
+
136
+ .course-part {
137
+ display: grid;
138
+ gap: 0.4rem;
139
+ }
140
+
141
+ .course-part :global(h2),
142
+ .course-part :global(h3) {
143
+ margin: 0;
144
+ font-size: var(--sl-text-lg);
145
+ line-height: 1.3;
146
+ }
147
+
148
+ .course-part :global(h2) a,
149
+ .course-part :global(h3) a {
150
+ text-decoration: none;
151
+ }
152
+
153
+ .course-part :global(h2) a:hover,
154
+ .course-part :global(h3) a:hover {
155
+ text-decoration: underline;
156
+ }
157
+
158
+ .course-part ol {
159
+ counter-reset: chapter;
160
+ display: grid;
161
+ gap: 0.1rem;
162
+ margin: 0.35rem 0 0;
163
+ padding: 0;
164
+ list-style: none;
165
+ }
166
+
167
+ .course-part li {
168
+ counter-increment: chapter;
169
+ display: grid;
170
+ grid-template-columns: 1.9rem minmax(0, 1fr) auto;
171
+ gap: 0 0.5rem;
172
+ margin: 0;
173
+ padding-block: 0.4rem;
174
+ border-block-start: 1px solid var(--sl-color-gray-6);
175
+ }
176
+
177
+ .course-part li::before {
178
+ content: counter(chapter, decimal-leading-zero);
179
+ color: var(--sl-color-gray-4);
180
+ font-size: var(--sl-text-xs);
181
+ font-variant-numeric: tabular-nums;
182
+ line-height: 1.7;
183
+ }
184
+
185
+ .course-part li > a {
186
+ text-decoration: none;
187
+ font-weight: 500;
188
+ }
189
+
190
+ .course-part li > a:hover {
191
+ text-decoration: underline;
192
+ }
193
+
194
+ .course-part li > .chapter-time {
195
+ color: var(--sl-color-gray-3);
196
+ font-size: var(--sl-text-xs);
197
+ font-variant-numeric: tabular-nums;
198
+ text-align: end;
199
+ white-space: nowrap;
200
+ line-height: 1.7;
201
+ }
202
+
203
+ .course-part li > p {
204
+ grid-column: 2 / -1;
205
+ margin: 0;
206
+ color: var(--sl-color-gray-3);
207
+ font-size: var(--sl-text-sm);
208
+ line-height: 1.45;
209
+ }
210
+
211
+ @media (max-width: 40rem) {
212
+ .course-part li {
213
+ grid-template-columns: 1.9rem minmax(0, 1fr);
214
+ }
215
+
216
+ .course-part li > .chapter-time {
217
+ grid-column: 2;
218
+ text-align: start;
219
+ }
220
+ }
221
+ </style>
@@ -65,7 +65,7 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
65
65
  if (!lesson || !entry) return null;
66
66
  return (
67
67
  <li>
68
- <article>
68
+ <article class="ex-tier-meta">
69
69
  <p class="step">Lesson {index + 1}</p>
70
70
  <h3>
71
71
  <a
@@ -130,7 +130,7 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
130
130
  competencyNodes.length > 0 && (
131
131
  <Diagram
132
132
  id="competency-graph"
133
- label="Competency dependency graph"
133
+ caption="The competency dependency graph: an arrow runs from a prerequisite to the competency that builds on it"
134
134
  nodes={competencyNodes}
135
135
  edges={competencyEdges}
136
136
  />
@@ -142,7 +142,7 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
142
142
  competencies
143
143
  .sort((left, right) => left.data.id.localeCompare(right.data.id))
144
144
  .map(({ data }) => (
145
- <li>
145
+ <li class="ex-tier-meta">
146
146
  <strong>{data.title}</strong>
147
147
  <br />
148
148
  <code>{data.id}</code>
@@ -181,12 +181,11 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
181
181
  font-weight: 700;
182
182
  }
183
183
 
184
+ /* Generated course logistics: the meta tier (`ex-tier-meta`,
185
+ `surfaces.css`) draws each card's surface, border and radius. */
184
186
  .lesson-sequence article,
185
187
  .competency-map li {
186
188
  padding: 1rem;
187
- border: 1px solid var(--sl-color-gray-5);
188
- border-radius: 0.55rem;
189
- background: var(--sl-color-gray-7);
190
189
  }
191
190
 
192
191
  .lesson-sequence h3,
@@ -0,0 +1,200 @@
1
+ ---
2
+ /**
3
+ * The course's "Figures and tables" page — LaTeX's `\listoffigures` and
4
+ * `\listoftables` in one (docs/plans/figures-and-short-captions.md, decision
5
+ * 9.4): every numbered figure, table and diagram with an `id`, by part and
6
+ * chapter.
7
+ *
8
+ * An entry is the float's number, linked to its place in its chapter, and
9
+ * its short caption; or its long caption, muted, when it has no short one;
10
+ * or the number alone when the manifest has neither — a float whose captions
11
+ * a wrapper component sets (a lab's `DerivationDiagram.astro`) reaches the
12
+ * manifest without them. A float without an `id` has no stable anchor and
13
+ * is not in the manifest's lists, so it is not listed.
14
+ *
15
+ * Modelled on `CourseContents.astro`: it reads the manifest — parts in
16
+ * config order, chapters in reading order, then each chapter's figures,
17
+ * tables and diagrams, by kind and number. A chapter or a part with none is
18
+ * left out.
19
+ *
20
+ * Usage, on a top-level page the sidebar's Reference group lists
21
+ * (`content/docs/figures-and-tables.mdx`):
22
+ *
23
+ * ```mdx
24
+ * import FloatIndex from 'explico/components/FloatIndex.astro';
25
+ *
26
+ * <FloatIndex />
27
+ * ```
28
+ */
29
+ import { loadManifest, type ManifestLessonFloat } from '../compiler/manifest';
30
+ import { FLOAT_KIND_WORDS, type FloatKind } from './float-name';
31
+
32
+ interface Props {
33
+ /** Heading level for each part's name; a chapter's is one below. Defaults to `h2`. */
34
+ readonly headingLevel?: 2 | 3;
35
+ }
36
+
37
+ const { headingLevel = 2 } = Astro.props;
38
+ const PartHeading = `h${headingLevel}` as 'h2' | 'h3';
39
+ const ChapterHeading = `h${headingLevel + 1}` as 'h3' | 'h4';
40
+
41
+ const manifest = await loadManifest();
42
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
43
+ const lessonById = new Map(manifest.lessons.map((lesson) => [lesson.id, lesson]));
44
+
45
+ const KINDS = [
46
+ ['figures', 'fig'],
47
+ ['tables', 'tab'],
48
+ ['diagrams', 'dia'],
49
+ ] as const satisfies readonly (readonly [string, FloatKind])[];
50
+
51
+ const byNumber = (a: ManifestLessonFloat, b: ManifestLessonFloat) =>
52
+ a.number.localeCompare(b.number, undefined, { numeric: true });
53
+
54
+ const parts = manifest.parts
55
+ .map((part) => ({
56
+ ...part,
57
+ chapters: part.lessons
58
+ .map((id) => lessonById.get(id))
59
+ .filter((lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined)
60
+ .map((lesson, index) => ({
61
+ lesson,
62
+ // The chapter's number, as its sections' numbers begin.
63
+ number: `${part.ordinal}.${index + 1}`,
64
+ entries: KINDS.flatMap(([field, kind]) =>
65
+ [...lesson[field]].sort(byNumber).map((float) => ({
66
+ ...float,
67
+ label: `${FLOAT_KIND_WORDS[kind]} ${float.number}`,
68
+ href: `${base}/${lesson.slug}/#${kind}-${float.key}`,
69
+ })),
70
+ ),
71
+ }))
72
+ .filter((chapter) => chapter.entries.length > 0),
73
+ }))
74
+ .filter((part) => part.chapters.length > 0);
75
+ ---
76
+
77
+ <div class="float-index not-content">
78
+ {
79
+ parts.length === 0 && (
80
+ <p class="float-index__empty">No chapter has a numbered figure, table or diagram yet.</p>
81
+ )
82
+ }
83
+ {
84
+ parts.map((part) => (
85
+ <section class="float-index__part">
86
+ <PartHeading id={`floats-${part.dir}`}>{part.label}</PartHeading>
87
+ {part.chapters.map(({ lesson, number, entries }) => (
88
+ <section class="float-index__chapter">
89
+ <ChapterHeading id={`floats-${lesson.slug.replace(/\//g, '-')}`}>
90
+ <span class="float-index__chapter-number">{number}</span>{' '}
91
+ <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
92
+ </ChapterHeading>
93
+ <ol class="float-index__list">
94
+ {entries.map((entry) => (
95
+ <li>
96
+ <a class="float-index__number" href={entry.href}>
97
+ {entry.label}
98
+ </a>
99
+ {entry.short ? (
100
+ <span class="float-index__name">{entry.short}</span>
101
+ ) : entry.caption ? (
102
+ <span class="float-index__name float-index__name--long">{entry.caption}</span>
103
+ ) : null}
104
+ </li>
105
+ ))}
106
+ </ol>
107
+ </section>
108
+ ))}
109
+ </section>
110
+ ))
111
+ }
112
+ </div>
113
+
114
+ <style>
115
+ .float-index {
116
+ display: grid;
117
+ gap: 1.75rem;
118
+ margin-block-start: 1.5rem;
119
+ }
120
+
121
+ .float-index__part,
122
+ .float-index__chapter {
123
+ display: grid;
124
+ gap: 0.4rem;
125
+ }
126
+
127
+ .float-index__part :global(h2),
128
+ .float-index__part :global(h3) {
129
+ margin: 0;
130
+ font-size: var(--sl-text-lg);
131
+ line-height: 1.3;
132
+ }
133
+
134
+ .float-index__chapter :global(h3),
135
+ .float-index__chapter :global(h4) {
136
+ margin: 0.5rem 0 0;
137
+ font-size: var(--sl-text-base);
138
+ line-height: 1.4;
139
+ }
140
+
141
+ .float-index__chapter-number {
142
+ color: var(--sl-color-gray-3);
143
+ font-variant-numeric: tabular-nums;
144
+ }
145
+
146
+ /* `not-content` drops Starlight's link colour with the rest of its prose
147
+ styles; the index keeps the accent for its links. */
148
+ .float-index__chapter a {
149
+ color: var(--sl-color-text-accent);
150
+ text-decoration: none;
151
+ }
152
+
153
+ .float-index__chapter a:hover,
154
+ .float-index__chapter a:focus-visible {
155
+ text-decoration: underline;
156
+ }
157
+
158
+ /* One row per float, as a list of figures sets it: the number in a column
159
+ of its own, the name beside it. */
160
+ .float-index__list {
161
+ display: grid;
162
+ gap: 0;
163
+ margin: 0;
164
+ padding: 0;
165
+ list-style: none;
166
+ }
167
+
168
+ .float-index__list li {
169
+ display: grid;
170
+ grid-template-columns: 8.5rem minmax(0, 1fr);
171
+ gap: 0 0.75rem;
172
+ margin: 0;
173
+ padding-block: 0.35rem;
174
+ border-block-start: 1px solid var(--sl-color-gray-6);
175
+ line-height: 1.45;
176
+ }
177
+
178
+ .float-index__number {
179
+ font-weight: 600;
180
+ font-variant-numeric: tabular-nums;
181
+ white-space: nowrap;
182
+ }
183
+
184
+ /* A float with no short caption is listed by its long one, muted: the
185
+ name to give it is still to be written. */
186
+ .float-index__name--long {
187
+ color: var(--sl-color-gray-3);
188
+ font-size: var(--sl-text-sm);
189
+ }
190
+
191
+ .float-index__empty {
192
+ color: var(--sl-color-gray-3);
193
+ }
194
+
195
+ @media (max-width: 30rem) {
196
+ .float-index__list li {
197
+ grid-template-columns: minmax(0, 1fr);
198
+ }
199
+ }
200
+ </style>