explico 0.13.1 → 0.14.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 (239) hide show
  1. package/CHANGELOG.md +879 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1045 -16
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +15 -7
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -9,6 +9,13 @@
9
9
  * each part heading linking its overview page where one exists, and study
10
10
  * times totalled per part and for the course when every chapter states one.
11
11
  *
12
+ * When the course has a deck, each title starts with the glyph of what it
13
+ * opens (a chapter's sheet, a slides-only lesson's frame), a key and a deck
14
+ * count join the counts, and a slides column ends every row: a lesson's own
15
+ * deck is a tile on its title line, a part's deck one bar along the chapters
16
+ * it covers (`deck-lane.ts`). It lists every edition whatever the sidebar's
17
+ * controls say: it answers "what is in the course".
18
+ *
12
19
  * Usage, on a splash page:
13
20
  *
14
21
  * ```mdx
@@ -18,15 +25,21 @@
18
25
  * ```
19
26
  */
20
27
  import { loadManifest } from '../compiler/manifest';
28
+ import { contentsKey, deckLanes, laneLink, titleGlyph } from './deck-lane';
21
29
 
22
30
  interface Props {
23
31
  /** Heading level for each part's name. Defaults to `h2`. */
24
32
  readonly headingLevel?: 2 | 3;
25
33
  /** Set `false` to drop the per-chapter description column. */
26
34
  readonly descriptions?: boolean;
35
+ /**
36
+ * Set `false` to drop the slides column, the glyphs, the key and the deck
37
+ * count. They show only when the course has a deck.
38
+ */
39
+ readonly slides?: boolean;
27
40
  }
28
41
 
29
- const { headingLevel = 2, descriptions = true } = Astro.props;
42
+ const { headingLevel = 2, descriptions = true, slides = true } = Astro.props;
30
43
  const Heading = `h${headingLevel}` as 'h2' | 'h3';
31
44
 
32
45
  const manifest = await loadManifest();
@@ -44,7 +57,24 @@ const parts = manifest.parts
44
57
  (lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined,
45
58
  ),
46
59
  }))
47
- .filter((part) => part.chapters.length > 0);
60
+ .filter((part) => part.chapters.length > 0)
61
+ .map((part) => ({
62
+ ...part,
63
+ lanes: slides ? deckLanes({ lessons: part.chapters, decks: manifest.decks, part }) : [],
64
+ }));
65
+
66
+ // The glyphs, the key and the count say which lessons have slides: in a
67
+ // course with no deck they would explain one glyph, so the list stays as it
68
+ // was. The column is as wide as the part that needs most lanes, so it lines
69
+ // up across parts.
70
+ const key = slides ? contentsKey(manifest.lessons, manifest.decks) : [];
71
+ const glyphs = key.length > 0;
72
+ const laneCount = Math.max(0, ...parts.map((part) => part.lanes.length));
73
+ const keyLabel = {
74
+ chapter: 'Chapter',
75
+ deck: 'Slides',
76
+ 'part-deck': 'Slides for a whole part',
77
+ } as const;
48
78
 
49
79
  const chapterCount = parts.reduce(
50
80
  (total, part) => total + part.chapters.length,
@@ -74,19 +104,42 @@ const hours = (range: { min: number; max: number }) => {
74
104
  };
75
105
  ---
76
106
 
77
- <div class="course-contents not-content">
107
+ <div
108
+ class="course-contents not-content"
109
+ data-deck-lanes={laneCount > 0 ? laneCount : undefined}
110
+ style={laneCount > 0 ? `--deck-lanes: ${laneCount}` : undefined}
111
+ >
78
112
  <p class="course-meta">
79
113
  <span>{parts.length} parts</span>
80
114
  <span>{chapterCount} chapters</span>
115
+ {
116
+ glyphs && (
117
+ <span>
118
+ {manifest.decks.length} {manifest.decks.length === 1 ? 'slide deck' : 'slide decks'}
119
+ </span>
120
+ )
121
+ }
81
122
  {courseMinutes && <span>{hours(courseMinutes)} of study</span>}
82
123
  </p>
124
+ {
125
+ glyphs && (
126
+ <ul class="contents-key" aria-label="Key">
127
+ {key.map((glyph) => (
128
+ <li>
129
+ <span class={`contents-glyph contents-glyph--${glyph}`} aria-hidden="true" />
130
+ {keyLabel[glyph]}
131
+ </li>
132
+ ))}
133
+ </ul>
134
+ )
135
+ }
83
136
 
84
137
  {
85
138
  parts.map((part) => (
86
139
  <section class="course-part">
87
140
  <Heading id={`contents-${part.dir}`}>
88
141
  {part.overview ? (
89
- <a href={`${base}${part.link}`}>{part.label}</a>
142
+ <a href={`${base}${part.link}`}>{glyphs && <span class="contents-glyph contents-glyph--part" aria-hidden="true" />}{part.label}</a>
90
143
  ) : (
91
144
  part.label
92
145
  )}
@@ -99,13 +152,23 @@ const hours = (range: { min: number; max: number }) => {
99
152
  {part.studyMinutes && <span>{minutes(part.studyMinutes)}</span>}
100
153
  </p>
101
154
  <ol aria-labelledby={`contents-${part.dir}`}>
102
- {part.chapters.map((lesson) => (
155
+ {part.chapters.map((lesson, row) => (
103
156
  <li>
104
- <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
157
+ <a href={`${base}/${lesson.slug}/`}>{glyphs && <span class={`contents-glyph contents-glyph--${titleGlyph(lesson)}`} aria-hidden="true" />}{lesson.title}</a>
105
158
  {lesson.studyMinutes && (
106
159
  <span class="chapter-time">{minutes(lesson.studyMinutes)}</span>
107
160
  )}
108
161
  {descriptions && <p>{lesson.description}</p>}
162
+ {part.lanes.map((lane, index) => {
163
+ const cell = lane[row];
164
+ return (
165
+ cell && (
166
+ <a {...laneLink(cell, index, laneCount, base)}>
167
+ <span class="sr-only">{cell.name}</span>
168
+ </a>
169
+ )
170
+ );
171
+ })}
109
172
  </li>
110
173
  ))}
111
174
  </ol>
@@ -224,4 +287,59 @@ const hours = (range: { min: number; max: number }) => {
224
287
  text-align: start;
225
288
  }
226
289
  }
290
+
291
+ /* The key under the counts: the glyphs the list uses, in the accent. */
292
+ .contents-key {
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: 0.25rem 1.25rem;
296
+ margin: -1.1rem 0 0;
297
+ padding: 0;
298
+ list-style: none;
299
+ color: var(--sl-color-gray-2);
300
+ font-size: var(--sl-text-sm);
301
+ }
302
+
303
+ .contents-key li {
304
+ margin: 0;
305
+ }
306
+
307
+ .contents-key :global(.contents-glyph) {
308
+ color: var(--sl-color-text-accent);
309
+ }
310
+
311
+ /* The slides column (`global.css`): one 1.7rem column per lane at the
312
+ row's end. The description keeps to the title's and time's columns. A
313
+ bar's cell spans a row's title and description, and reaches the line
314
+ between two rows: the row's bottom padding and the list's gap below it,
315
+ the next row's top border and padding above. */
316
+ .course-contents[data-deck-lanes] {
317
+ --deck-lane-rows: 2;
318
+ --deck-lane-down: 0.5rem;
319
+ --deck-lane-up: calc(0.4rem + 1px);
320
+ }
321
+
322
+ .course-contents[data-deck-lanes] .course-part li {
323
+ grid-template-columns: 1.9rem minmax(0, 1fr) auto repeat(var(--deck-lanes), 1.7rem);
324
+ }
325
+
326
+ .course-contents[data-deck-lanes] .course-part li > p {
327
+ grid-column: 2 / 4;
328
+ }
329
+
330
+ /* On a phone the time goes under the title, and the cells span the three
331
+ rows. */
332
+ @media (max-width: 40rem) {
333
+ .course-contents[data-deck-lanes] {
334
+ --deck-lane-rows: 3;
335
+ }
336
+
337
+ .course-contents[data-deck-lanes] .course-part li {
338
+ grid-template-columns: 1.9rem minmax(0, 1fr) repeat(var(--deck-lanes), 1.7rem);
339
+ }
340
+
341
+ .course-contents[data-deck-lanes] .course-part li > p {
342
+ grid-column: 2;
343
+ }
344
+ }
227
345
  </style>
@@ -2,8 +2,29 @@
2
2
  import { getCollection } from 'astro:content';
3
3
  import { loadManifest } from '../compiler/manifest';
4
4
  import { isLessonSlug, lessonIdFromSlug } from '../compiler/lesson-derivation';
5
+ import { competencyGraphsByPart } from '../curriculum/competency-graph';
5
6
  import Diagram, { type DiagramEdge, type DiagramNode } from './diagram/Diagram.astro';
6
7
 
8
+ export interface Props {
9
+ /**
10
+ * `'whole'` (the default) draws the competency graph as one diagram;
11
+ * `'per-part'` draws one diagram per part, in sidebar order, holding the
12
+ * competencies its lessons teach first and, labelled with their part, the
13
+ * earlier competencies they build on. A graph too wide for the column as
14
+ * one diagram (rule V8) usually fits as one per part.
15
+ */
16
+ graph?: 'whole' | 'per-part';
17
+ /** The layout direction of every competency diagram; `Diagram`'s default is `'TB'`. */
18
+ direction?: 'TB' | 'LR';
19
+ /**
20
+ * Passed to every competency diagram: the width, in px, above which rule V8
21
+ * warns, or `false` to let a whole-course graph scroll without a warning.
22
+ */
23
+ warnAboveWidth?: number | false;
24
+ }
25
+
26
+ const { graph = 'whole', direction, warnAboveWidth } = Astro.props;
27
+
7
28
  const [tracks, docs, competencies, manifest] = await Promise.all([
8
29
  getCollection('tracks'),
9
30
  getCollection('docs'),
@@ -37,6 +58,15 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
37
58
  to: data.id,
38
59
  })),
39
60
  );
61
+
62
+ const partGraphs =
63
+ graph === 'per-part'
64
+ ? competencyGraphsByPart(
65
+ manifest.parts,
66
+ new Map(manifest.lessons.map((lesson) => [lesson.id, lesson.teaches])),
67
+ competencies.map(({ data }) => data),
68
+ )
69
+ : [];
40
70
  ---
41
71
 
42
72
  {
@@ -127,12 +157,30 @@ const competencyEdges: DiagramEdge[] = competencies.flatMap(({ data }) =>
127
157
  </p>
128
158
 
129
159
  {
130
- competencyNodes.length > 0 && (
160
+ partGraphs.map((partGraph) => (
161
+ <>
162
+ <h3 id={`${partGraph.id}-heading`}>{partGraph.title}</h3>
163
+ <Diagram
164
+ id={partGraph.id}
165
+ caption={`The competencies taught in ${partGraph.title}: an arrow runs from a prerequisite to the competency that builds on it, and a box labelled with an earlier part is a competency taught there`}
166
+ nodes={partGraph.nodes}
167
+ edges={partGraph.edges}
168
+ direction={direction}
169
+ warnAboveWidth={warnAboveWidth}
170
+ />
171
+ </>
172
+ ))
173
+ }
174
+
175
+ {
176
+ graph === 'whole' && competencyNodes.length > 0 && (
131
177
  <Diagram
132
178
  id="competency-graph"
133
179
  caption="The competency dependency graph: an arrow runs from a prerequisite to the competency that builds on it"
134
180
  nodes={competencyNodes}
135
181
  edges={competencyEdges}
182
+ direction={direction}
183
+ warnAboveWidth={warnAboveWidth}
136
184
  />
137
185
  )
138
186
  }
@@ -27,7 +27,7 @@
27
27
  * ```
28
28
  */
29
29
  import { loadManifest, type ManifestLessonFloat } from '../compiler/manifest';
30
- import { FLOAT_KIND_WORDS, type FloatKind } from './float-name';
30
+ import { FLOAT_KIND_WORDS, type FloatKind } from '../reference/float-name';
31
31
 
32
32
  interface Props {
33
33
  /** Heading level for each part's name; a chapter's is one below. Defaults to `h2`. */
@@ -7,7 +7,9 @@
7
7
  * collection — chapter order, titles, descriptions, float counts, and which
8
8
  * parts this one builds on or feeds. A course authors the page's prose; it
9
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.
10
+ * leave a stale copy behind on the part page. When the course has a deck, the
11
+ * list has the contents list's glyphs and slides column (`deck-lane.ts`,
12
+ * `CourseContents.astro`).
11
13
  *
12
14
  * Usage, in `content/docs/<part>/index.mdx`:
13
15
  *
@@ -38,6 +40,7 @@
38
40
  */
39
41
  import { getCollection } from 'astro:content';
40
42
  import { loadManifest, type ManifestPart } from '../compiler/manifest';
43
+ import { deckLanes, laneLink, titleGlyph } from './deck-lane';
41
44
 
42
45
  interface Props {
43
46
  /**
@@ -53,9 +56,14 @@ interface Props {
53
56
  readonly chain?: readonly string[];
54
57
  /** Set `false` to drop the rail and keep only the reading list. */
55
58
  readonly rail?: boolean;
59
+ /**
60
+ * Set `false` to drop the slides column and the glyphs before the titles.
61
+ * They show only when the course has a deck.
62
+ */
63
+ readonly slides?: boolean;
56
64
  }
57
65
 
58
- const { part: partProp, chain = [], rail = true } = Astro.props;
66
+ const { part: partProp, chain = [], rail = true, slides = true } = Astro.props;
59
67
 
60
68
  const routeSlug = Astro.locals.starlightRoute.entry.id;
61
69
  const partDir = partProp ?? routeSlug.replace(/\/index$/, '').split('/')[0];
@@ -82,6 +90,11 @@ const chapters = part.lessons
82
90
  .map((id) => lessonById.get(id))
83
91
  .filter((lesson): lesson is NonNullable<typeof lesson> => lesson !== undefined);
84
92
 
93
+ // As in the contents list: glyphs only in a course with a deck, and the
94
+ // column only when this part has one.
95
+ const glyphs = slides && manifest.decks.length > 0;
96
+ const lanes = glyphs ? deckLanes({ lessons: chapters, decks: manifest.decks, part }) : [];
97
+
85
98
  const competencyById = new Map(
86
99
  competencyEntries.map(({ data }) => [data.id, data]),
87
100
  );
@@ -169,11 +182,15 @@ const hasRail =
169
182
  </ul>
170
183
 
171
184
  <div class="part-body">
172
- <ol class="part-chapters">
185
+ <ol
186
+ class="part-chapters"
187
+ data-deck-lanes={lanes.length > 0 ? lanes.length : undefined}
188
+ style={lanes.length > 0 ? `--deck-lanes: ${lanes.length}` : undefined}
189
+ >
173
190
  {
174
- chapters.map((lesson) => (
191
+ chapters.map((lesson, row) => (
175
192
  <li>
176
- <a href={`${base}/${lesson.slug}/`}>{lesson.title}</a>
193
+ <a href={`${base}/${lesson.slug}/`}>{glyphs && <span class={`contents-glyph contents-glyph--${titleGlyph(lesson)}`} aria-hidden="true" />}{lesson.title}</a>
177
194
  <p>{lesson.description}</p>
178
195
  <p class="chapter-meta">
179
196
  {lesson.studyMinutes && (
@@ -192,6 +209,16 @@ const hasRail =
192
209
  </span>
193
210
  )}
194
211
  </p>
212
+ {lanes.map((lane, index) => {
213
+ const cell = lane[row];
214
+ return (
215
+ cell && (
216
+ <a {...laneLink(cell, index, lanes.length, base)}>
217
+ <span class="sr-only">{cell.name}</span>
218
+ </a>
219
+ )
220
+ );
221
+ })}
195
222
  </li>
196
223
  ))
197
224
  }
@@ -342,6 +369,25 @@ const hasRail =
342
369
  line-height: 1.5;
343
370
  }
344
371
 
372
+ /* The slides column (`global.css`): one 1.7rem column per lane at the
373
+ row's end, the title, description and meta line kept to the second
374
+ column. A bar's cell spans the three, and reaches the line under its
375
+ row: the row's bottom padding, then the next row's top padding and the
376
+ line itself above. */
377
+ .part-chapters[data-deck-lanes] {
378
+ --deck-lane-rows: 3;
379
+ --deck-lane-down: 0.85rem;
380
+ --deck-lane-up: calc(0.85rem + 1px);
381
+ }
382
+
383
+ .part-chapters[data-deck-lanes] > li {
384
+ grid-template-columns: 2.25rem minmax(0, 1fr) repeat(var(--deck-lanes), 1.7rem);
385
+ }
386
+
387
+ .part-chapters[data-deck-lanes] > li > :not(.deck-lane) {
388
+ grid-column: 2;
389
+ }
390
+
345
391
  .chapter-meta {
346
392
  display: flex;
347
393
  flex-wrap: wrap;
@@ -7,6 +7,8 @@ import { renderNotationMath } from '../notation/render-notation-math';
7
7
  import type { MixedContentNode } from '../notation/render-notation-math';
8
8
  import { knowledgeCheckAnchorId } from '../../reference/knowledge-checks.mjs';
9
9
  import BlockHeading from '../BlockHeading.astro';
10
+ import { deckOfRoute } from '../deck/deck-context';
11
+ import { routeIdFromLocals } from '../../reference/page-notation';
10
12
  import AssessmentRunner from './AssessmentRunner';
11
13
 
12
14
  interface Props {
@@ -17,17 +19,29 @@ interface Props {
17
19
  * `[[check-…]]` references; undeclared legacy embeds keep a local fallback.
18
20
  */
19
21
  id?: string;
22
+ /**
23
+ * The lesson whose checks the set asks, by its id. Set by the engine on a
24
+ * part's deck (`deck-remark.mjs`): the chapter the frame's section covers.
25
+ * Elsewhere the page's own lesson.
26
+ */
27
+ lesson?: string;
20
28
  }
21
29
 
22
30
  const explicitId = Astro.props.id;
23
31
 
24
32
  const base = import.meta.env.BASE_URL.replace(/\/$/, '');
25
- const lessonId = Astro.url.pathname
26
- .replace(new RegExp(`^${base}/`), '')
27
- .replace(/\/$/, '')
28
- .replace(/\//g, '.');
29
-
30
33
  const manifest = await loadManifest();
34
+ // On a deck (docs/authoring/deck.md) the checks are its lesson's, and a check
35
+ // is a question frame: its label, not a heading of its own. On a part's deck
36
+ // they are the chapter's whose section the frame is in.
37
+ const deck = deckOfRoute(manifest, routeIdFromLocals(Astro.locals));
38
+ const lessonId =
39
+ Astro.props.lesson ??
40
+ deck?.lessonId ??
41
+ Astro.url.pathname
42
+ .replace(new RegExp(`^${base}/`), '')
43
+ .replace(/\/$/, '')
44
+ .replace(/\//g, '.');
31
45
  // A check folds like a section, by the course's `disclosure.knowledgeChecks`
32
46
  // (collapsible and open unless the course says otherwise).
33
47
  const { knowledgeChecks } = manifest.disclosure ?? DISCLOSURE_DEFAULTS;
@@ -101,16 +115,28 @@ function renderedContent(
101
115
  {
102
116
  entries.map(({ entry, number, anchorId }) => (
103
117
  <>
104
- <BlockHeading
105
- kind="check"
106
- anchorId={anchorId}
107
- title={entry.data.title}
108
- number={number}
109
- collapsible={knowledgeChecks.collapsible}
110
- defaultOpen={knowledgeChecks.defaultOpen}
111
- card="accent"
112
- class="assessment-heading-wrapper"
113
- />
118
+ {deck ? (
119
+ <p
120
+ class="deck-check__label"
121
+ id={anchorId}
122
+ data-block-kind="check"
123
+ data-check-number={number}
124
+ data-check-title={entry.data.title}
125
+ >
126
+ <span class="deck-check__kind">Knowledge check {number}</span> {entry.data.title}
127
+ </p>
128
+ ) : (
129
+ <BlockHeading
130
+ kind="check"
131
+ anchorId={anchorId}
132
+ title={entry.data.title}
133
+ number={number}
134
+ collapsible={knowledgeChecks.collapsible}
135
+ defaultOpen={knowledgeChecks.defaultOpen}
136
+ card="accent"
137
+ class="assessment-heading-wrapper"
138
+ />
139
+ )}
114
140
  <AssessmentRunner
115
141
  client:visible
116
142
  assessmentId={entry.data.id}
@@ -63,14 +63,23 @@ export async function titledBlocksOf(
63
63
  * The text a reader sees for each outline heading with inline math, by its
64
64
  * anchor (`manifest.lessons[].headings`, v30). Astro lists a heading by all
65
65
  * the text in it, and KaTeX writes a formula three ways — MathML, its LaTeX
66
- * annotation and its glyphs — so its own list reads `R\explain{k}{R}R`.
66
+ * annotation and its glyphs — so its own list reads `R\explain{k}{R}R`. A
67
+ * deck beside its chapter, or a part's, is no lesson: its page lists its
68
+ * frames by their titles as the reader sees them (`decks[].frames`).
67
69
  */
68
- export async function mathHeadingTextsOf(lessonId: string): Promise<ReadonlyMap<string, string>> {
69
- const lesson = (await manifestOnce()).lessons.find((entry) => entry.id === lessonId);
70
+ export async function mathHeadingTextsOf(pageId: string): Promise<ReadonlyMap<string, string>> {
71
+ const manifest = await manifestOnce();
72
+ const lesson = manifest.lessons.find((entry) => entry.id === pageId);
73
+ if (lesson) {
74
+ return new Map(
75
+ lesson.headings.filter((heading) => heading.math).map((heading) => [heading.slug, heading.text]),
76
+ );
77
+ }
78
+ const deck = (manifest.decks ?? []).find((entry) => entry.pageId === pageId);
70
79
  return new Map(
71
- (lesson?.headings ?? [])
72
- .filter((heading) => heading.math)
73
- .map((heading) => [heading.slug, heading.text]),
80
+ (deck?.frames ?? []).flatMap((frame): [string, string][] =>
81
+ frame.slug === undefined ? [] : [[frame.slug, frame.title]],
82
+ ),
74
83
  );
75
84
  }
76
85
 
@@ -63,6 +63,7 @@ import {
63
63
  createBrowserPreferenceStore,
64
64
  createSessionPreferenceStore,
65
65
  } from '../session/PreferenceStore';
66
+ import { rememberReading } from '../session/reading-memory';
66
67
  import { decodeCard, keyOf, type RememberedCard } from './card-memory';
67
68
  import type { HoldEdge } from './card-placement';
68
69
 
@@ -305,21 +306,23 @@ const setKey = (base: string) => `explico:cards:carried:${base}`;
305
306
 
306
307
  /**
307
308
  * K5, D9: this tab's own set; a tab with none takes the course's last set,
308
- * which is its own from then on.
309
+ * which is its own from then on. The course's last set is reading memory
310
+ * (`../session/reading-memory.ts`): it is read and kept only with the
311
+ * reader's yes, so without it a new tab starts with no cards.
309
312
  */
310
313
  export function readCarried(base: string): CarriedSet {
311
314
  const own = decodeCarried(tab.get(setKey(base)));
312
315
  if (own) return own;
313
- const last = decodeCarried(preferences.get(setKey(base))) ?? EMPTY_SET;
316
+ const last = (rememberReading(base) ? decodeCarried(preferences.get(setKey(base))) : null) ?? EMPTY_SET;
314
317
  tab.set(setKey(base), encodeCarried(last));
315
318
  return last;
316
319
  }
317
320
 
318
- /** K5: this tab's set, written as the course's last set too. Returns what was stored. */
321
+ /** K5: this tab's set, written as the course's last set too with the reader's yes. Returns what was stored. */
319
322
  export function writeCarried(base: string, set: CarriedSet): string {
320
323
  const text = encodeCarried(set);
321
324
  tab.set(setKey(base), text);
322
- preferences.set(setKey(base), text);
325
+ if (rememberReading(base)) preferences.set(setKey(base), text);
323
326
  return text;
324
327
  }
325
328
 
@@ -75,6 +75,10 @@ import {
75
75
  type CarriedSet,
76
76
  type OpenedAt,
77
77
  } from './card-carry';
78
+ import {
79
+ READING_MEMORY_EVENT,
80
+ type ReadingMemoryChange,
81
+ } from '../session/reading-memory';
78
82
  import { isThisPage, pageTitleOf } from './card-fetch';
79
83
  import { wireJumpArrow } from './card-jump';
80
84
  import { readableText } from './readable-text';
@@ -2051,7 +2055,7 @@ function rememberCards(): void {
2051
2055
  width: card.width,
2052
2056
  });
2053
2057
  }
2054
- writeRememberedCards({
2058
+ writeRememberedCards(base, {
2055
2059
  cards: saved,
2056
2060
  pointed: pointedKey !== null && keys.has(pointedKey) ? pointedKey : null,
2057
2061
  });
@@ -2599,11 +2603,19 @@ function install(): void {
2599
2603
  if (carry) {
2600
2604
  restoreCarriedSet(readCarried(base));
2601
2605
  } else {
2602
- const page = readRememberedCards();
2606
+ const page = readRememberedCards(base);
2603
2607
  remembered = [...page.cards];
2604
2608
  rememberedPointed = page.pointed;
2605
2609
  }
2606
2610
  window.addEventListener('pagehide', leavePage);
2611
+ // A yes to reading memory keeps what is open now beyond the tab; a no has
2612
+ // already deleted the kept copies, and the tab's own go on.
2613
+ window.addEventListener(READING_MEMORY_EVENT, (event) => {
2614
+ const { answer } = (event as CustomEvent<ReadingMemoryChange>).detail;
2615
+ if (answer !== 'yes') return;
2616
+ lastStored = null;
2617
+ leavePage();
2618
+ });
2607
2619
  document.addEventListener('visibilitychange', () => {
2608
2620
  if (document.visibilityState === 'hidden') leavePage();
2609
2621
  });
@@ -2,7 +2,10 @@
2
2
  * Remembering a page's open explanation cards. A reader who leaves a lesson
3
3
  * with cards pinned, docked or moved finds them where they were on coming
4
4
  * back to it: through a link, Back or a reload, in another tab, or after the
5
- * browser restarts. Closing the last card forgets the page.
5
+ * browser restarts. Closing the last card forgets the page. This is reading
6
+ * memory (`../session/reading-memory.ts`): kept in the browser only with the
7
+ * reader's yes; without it, in the tab's session, so Back, a reload and a
8
+ * link still bring the cards back until the tab is closed.
6
9
  *
7
10
  * The card manager (`./card-manager.ts`) saves the kept cards as the page is
8
11
  * left (`pagehide`) and restores them as each layer registers. A card is
@@ -17,7 +20,7 @@
17
20
  * `decodeCards` and `restorable` are pure, so `tests/unit/card-memory.test.ts`
18
21
  * checks them in Node.
19
22
  */
20
- import { createBrowserPreferenceStore } from '../session/PreferenceStore';
23
+ import { readingMemoryStore } from '../session/reading-memory';
21
24
  import type { MovedSpot } from './card-drag';
22
25
  import type { CardAnchor } from './card-placement';
23
26
 
@@ -159,21 +162,22 @@ export function restorable(
159
162
  // ---------------------------------------------------------------------------
160
163
 
161
164
  // The one seam for browser storage; this module never touches it itself.
162
- // Local storage, so the cards outlive the tab and a browser restart; tabs
163
- // open on the same page share one entry, and the last one left wins.
164
- const store = createBrowserPreferenceStore();
165
-
165
+ // With the reader's yes, local storage, so the cards outlive the tab and a
166
+ // browser restart (tabs open on the same page share one entry, and the last
167
+ // one left wins); without it, the tab's session storage.
166
168
  /**
167
169
  * One entry per page, kept only while the page has cards to bring back. The
168
170
  * path includes a course's base, so courses on one origin never share one.
169
171
  */
170
172
  const storageKey = () => `explico:cards:${window.location.pathname}`;
171
173
 
172
- /** The cards this page had open when the reader last left it. */
173
- export const readRememberedCards = (): RememberedPage => decodeCards(store.get(storageKey()));
174
+ /** The cards this page had open when the reader last left it, in the course at `base`. */
175
+ export const readRememberedCards = (base: string): RememberedPage =>
176
+ decodeCards(readingMemoryStore(base).get(storageKey()));
174
177
 
175
178
  /** Remember this page's cards; with none, the page is forgotten. */
176
- export function writeRememberedCards(page: RememberedPage): void {
179
+ export function writeRememberedCards(base: string, page: RememberedPage): void {
180
+ const store = readingMemoryStore(base);
177
181
  const key = storageKey();
178
182
  if (page.cards.length > 0) store.set(key, encodeCards(page));
179
183
  else if (store.remove) store.remove(key);
@@ -0,0 +1,13 @@
1
+ ---
2
+ /**
3
+ * A deck page's behaviour in the browser: the scaled frames, Present mode,
4
+ * the presenter window and the steps' effects (`deck-controller.ts`).
5
+ * `LessonFooter.astro` includes it on a deck's page only.
6
+ */
7
+ ---
8
+
9
+ <script>
10
+ import { installDecks } from './deck-controller';
11
+
12
+ installDecks();
13
+ </script>