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
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The slide map on a deck's study page (`docs/authoring/deck.md`, "Present
3
+ * mode and the study page"): every frame as a numbered square, pinned under
4
+ * the page's header while the deck scrolls. A deck too long for the map's
5
+ * line keeps its first few frames, the frames around the place to continue —
6
+ * a few before it, shown already, and some after it, not shown yet — the
7
+ * frames around the one in view, and its last few, with a gap for each run
8
+ * it leaves out. How many of each is what the line has room for.
9
+ *
10
+ * Pure: `deck-controller.ts` draws the map and measures its room;
11
+ * `tests/unit/deck-map.test.ts` checks this in Node.
12
+ */
13
+
14
+ /** One place on the map: a frame's square, or a gap standing for the frames from `from` to `to`. */
15
+ export type SlideMapItem =
16
+ | { readonly kind: 'slide'; readonly number: number }
17
+ | { readonly kind: 'gap'; readonly from: number; readonly to: number };
18
+
19
+ /** The frames the map keeps in sight, by number. */
20
+ export interface SlideMapFocus {
21
+ /** The frame to continue at, or else the first one not shown yet. */
22
+ readonly anchor?: number;
23
+ /** The frame in view on the study page. */
24
+ readonly current?: number;
25
+ }
26
+
27
+ /** How much a shortened map keeps: frames at each end, before and after the anchor, and on each side of the one in view. */
28
+ export interface SlideMapKeep {
29
+ readonly ends: number;
30
+ readonly before: number;
31
+ readonly after: number;
32
+ readonly around: number;
33
+ }
34
+
35
+ /** The least a shortened map keeps. */
36
+ const LEAST: SlideMapKeep = { ends: 1, before: 1, after: 1, around: 0 };
37
+
38
+ /** What a shortened map widens first, while the line has room: what comes next, then the ends, then what came before. */
39
+ const WIDEN: readonly (keyof SlideMapKeep)[] = ['after', 'ends', 'before', 'around'];
40
+
41
+ const slide = (number: number): SlideMapItem => ({ kind: 'slide', number });
42
+
43
+ /**
44
+ * A deck of `total` frames, numbered from 1, shortened to what `keep` keeps
45
+ * of its ends and of `focus`. A gap never stands for one frame alone: that
46
+ * frame is shown in its place.
47
+ */
48
+ export function slideMapKeeping(total: number, focus: SlideMapFocus, keep: SlideMapKeep): SlideMapItem[] {
49
+ const kept = new Set<number>();
50
+ const add = (from: number, to: number) => {
51
+ for (let number = Math.max(1, from); number <= Math.min(total, to); number += 1) kept.add(number);
52
+ };
53
+ add(1, keep.ends);
54
+ add(total - keep.ends + 1, total);
55
+ if (focus.anchor !== undefined) add(focus.anchor - keep.before, focus.anchor + keep.after);
56
+ if (focus.current !== undefined) add(focus.current - keep.around, focus.current + keep.around);
57
+ const items: SlideMapItem[] = [];
58
+ let last = 0;
59
+ for (const number of [...kept].sort((a, b) => a - b)) {
60
+ if (number - last === 2) items.push(slide(number - 1));
61
+ else if (number - last > 2) items.push({ kind: 'gap', from: last + 1, to: number - 1 });
62
+ items.push(slide(number));
63
+ last = number;
64
+ }
65
+ return items;
66
+ }
67
+
68
+ /**
69
+ * The map of a deck of `total` frames in a line with room for `room` places:
70
+ * every frame when they fit, else the least a shortened map keeps, widened
71
+ * ({@link WIDEN}, a frame at a time, round and round) while the line has
72
+ * room. Past the least, nothing fits: the map wraps.
73
+ */
74
+ export function slideMap(total: number, focus: SlideMapFocus, room = Number.POSITIVE_INFINITY): SlideMapItem[] {
75
+ if (total <= room) return Array.from({ length: total }, (_, index) => slide(index + 1));
76
+ let keep = LEAST;
77
+ let best = slideMapKeeping(total, focus, keep);
78
+ for (let widened = true; widened; ) {
79
+ widened = false;
80
+ for (const field of WIDEN) {
81
+ const wider = { ...keep, [field]: keep[field] + 1 };
82
+ const items = slideMapKeeping(total, focus, wider);
83
+ if (items.length <= room && items.length > best.length) {
84
+ keep = wider;
85
+ best = items;
86
+ widened = true;
87
+ }
88
+ }
89
+ }
90
+ return best;
91
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The slides column of a contents list (`CourseContents.astro`,
3
+ * `PartOverview.astro`): each lesson's deck at its row's end, as long as what
4
+ * it covers. A lesson's own deck is a **tile** on its title line; a part's
5
+ * deck is a **bar**, its covered chapters' cells joined into one shape from
6
+ * the first covered title to the last, each cell a link to that chapter's
7
+ * section of the deck, named as the sidebar names it (`sidebar-editions.ts`).
8
+ *
9
+ * Pure: the two components draw the cells, and `tests/unit/deck-lane.test.ts`
10
+ * checks this in Node.
11
+ */
12
+ import { editionLabel, sectionDetail } from './starlight/sidebar-editions';
13
+
14
+ /** One row's cell in one lane of the slides column. */
15
+ export interface LaneCell {
16
+ /** A tile is a whole deck on one row; a bar is a `start`, any `middle`s and an `end`. */
17
+ readonly kind: 'tile' | 'start' | 'middle' | 'end';
18
+ /** The glyph the cell always shows: on a tile and on a bar's start. */
19
+ readonly glyph?: 'deck' | 'part-deck';
20
+ /** Where it links, without the site's base: `/motion/slides/#slide-17`. */
21
+ readonly href: string;
22
+ /** The accessible name, the tooltip's first line: `Slides: Projectile Range`. */
23
+ readonly name: string;
24
+ /** The tooltip's second line, the link's description: `Slides 17–25 of the part's slides`. */
25
+ readonly detail?: string;
26
+ /**
27
+ * A slides-only lesson's tile: its title opens the same page, so the tile
28
+ * is left out of the tab order and hidden from assistive technology.
29
+ */
30
+ readonly duplicate?: boolean;
31
+ }
32
+
33
+ /** A lesson as the column reads it from the manifest. */
34
+ export interface LaneLesson {
35
+ readonly id: string;
36
+ readonly slug: string;
37
+ readonly title: string;
38
+ readonly editions: readonly ('chapter' | 'deck')[];
39
+ }
40
+
41
+ /** A deck as the column reads it from the manifest (`ManifestDeck`). */
42
+ export interface LaneDeck {
43
+ /** Its route, `motion/slides`. */
44
+ readonly slug: string;
45
+ /** A lesson's deck: the lesson's id. */
46
+ readonly lessonId?: string;
47
+ /** A part's deck: the part's directory. */
48
+ readonly part?: string;
49
+ /** A part's deck: each chapter it covers, by lesson slug, and the frame its section opens on. */
50
+ readonly covers: readonly { readonly chapter: string; readonly frame: number }[];
51
+ /** On a part's deck, each frame's chapter, by lesson slug. */
52
+ readonly frames: readonly { readonly number: number; readonly chapter?: string }[];
53
+ }
54
+
55
+ /**
56
+ * The frames a part's deck holds of one chapter, as the sidebar writes them
57
+ * (`withPartDeckFrames`): `2–16`, or `5` for one; none when no frame is the
58
+ * chapter's.
59
+ */
60
+ function sectionRange(deck: LaneDeck, chapter: string): string | undefined {
61
+ const numbers = deck.frames.filter((frame) => frame.chapter === chapter).map((frame) => frame.number);
62
+ if (numbers.length === 0) return undefined;
63
+ const first = Math.min(...numbers);
64
+ const last = Math.max(...numbers);
65
+ return first === last ? String(first) : `${first}–${last}`;
66
+ }
67
+
68
+ /**
69
+ * The slides column of one part: `lanes[lane][row]`, a cell or null, one
70
+ * row per lesson in `lessons`' order. Lane 0 holds each lesson's own deck,
71
+ * and the part's deck when no row it covers has a deck of its own; when one
72
+ * does, the part's deck takes lane 1. A part with no deck has no lanes.
73
+ */
74
+ export function deckLanes(input: {
75
+ readonly lessons: readonly LaneLesson[];
76
+ readonly decks: readonly LaneDeck[];
77
+ readonly part: { readonly dir: string };
78
+ }): (LaneCell | null)[][] {
79
+ const { lessons, decks, part } = input;
80
+ const own: (LaneCell | null)[] = lessons.map((lesson) => {
81
+ const deck = decks.find((candidate) => candidate.lessonId === lesson.id);
82
+ if (deck === undefined) return null;
83
+ return {
84
+ kind: 'tile',
85
+ glyph: 'deck',
86
+ href: `/${deck.slug}/`,
87
+ name: editionLabel('slides', lesson.title, 'other'),
88
+ ...(lesson.editions.includes('chapter') ? {} : { duplicate: true }),
89
+ };
90
+ });
91
+
92
+ const partDeck = decks.find((deck) => deck.part === part.dir);
93
+ const covered: (LaneCell | null)[] = lessons.map(() => null);
94
+ if (partDeck !== undefined) {
95
+ const frameOf = new Map(partDeck.covers.map((cover) => [cover.chapter, cover.frame]));
96
+ lessons.forEach((lesson, row) => {
97
+ const frame = frameOf.get(lesson.slug);
98
+ if (frame === undefined) return;
99
+ const range = sectionRange(partDeck, lesson.slug);
100
+ const above = row > 0 && frameOf.has(lessons[row - 1]!.slug);
101
+ const below = row < lessons.length - 1 && frameOf.has(lessons[row + 1]!.slug);
102
+ const kind = above ? (below ? 'middle' : 'end') : below ? 'start' : 'tile';
103
+ covered[row] = {
104
+ kind,
105
+ ...(above ? {} : { glyph: 'part-deck' as const }),
106
+ href: `/${partDeck.slug}/#slide-${frame}`,
107
+ name: editionLabel('slides', lesson.title, 'other'),
108
+ ...(range === undefined ? {} : { detail: sectionDetail(range) }),
109
+ };
110
+ });
111
+ }
112
+
113
+ const hasOwn = own.some((cell) => cell !== null);
114
+ const hasPart = covered.some((cell) => cell !== null);
115
+ if (!hasOwn && !hasPart) return [];
116
+ if (!hasPart) return [own];
117
+ if (!hasOwn) return [covered];
118
+ const clash = covered.some((cell, row) => cell !== null && own[row] !== null);
119
+ if (clash) return [own, covered];
120
+ return [own.map((cell, row) => cell ?? covered[row]!)];
121
+ }
122
+
123
+ /**
124
+ * A cell's link attributes, as both lists draw it: its kind and glyph as
125
+ * classes, its lane as the column counted from the row's end (the lanes are
126
+ * the row's last `lanes` columns, wherever the list puts its others), the
127
+ * tooltip's two lines in `title`, the second as `aria-description` too. A
128
+ * duplicate is out of the tab order and hidden from assistive technology.
129
+ * The link holds the name in a visually hidden span.
130
+ */
131
+ export function laneLink(cell: LaneCell, lane: number, lanes: number, base: string): Record<string, string | number> {
132
+ return {
133
+ class: ['deck-lane', `deck-lane--${cell.kind}`, ...(cell.glyph ? [`deck-lane--${cell.glyph}`] : [])].join(' '),
134
+ href: `${base}${cell.href}`,
135
+ style: `grid-column: ${lane - lanes - 1}`,
136
+ title: cell.detail === undefined ? cell.name : `${cell.name}\n${cell.detail}`,
137
+ ...(cell.detail === undefined ? {} : { 'aria-description': cell.detail }),
138
+ ...(cell.duplicate ? { 'aria-hidden': 'true', tabindex: -1 } : {}),
139
+ 'data-pagefind-ignore': '',
140
+ };
141
+ }
142
+
143
+ /** The glyph before a lesson's title: what it opens, its chapter or, for a slides-only lesson, its deck. */
144
+ export function titleGlyph(lesson: Pick<LaneLesson, 'editions'>): 'chapter' | 'deck' {
145
+ return lesson.editions.includes('chapter') ? 'chapter' : 'deck';
146
+ }
147
+
148
+ /** The glyphs a contents list uses, in the order its key names them. */
149
+ export type ContentsGlyph = 'chapter' | 'deck' | 'part-deck';
150
+
151
+ /**
152
+ * The key's entries: `chapter` when a lesson has a chapter, `deck` when a
153
+ * lesson has a deck of its own, `part-deck` when a part has one. Empty when
154
+ * the course has no deck: the list then draws no glyph at all.
155
+ */
156
+ export function contentsKey(lessons: readonly LaneLesson[], decks: readonly LaneDeck[]): ContentsGlyph[] {
157
+ if (decks.length === 0) return [];
158
+ return [
159
+ ...(lessons.some((lesson) => lesson.editions.includes('chapter')) ? (['chapter'] as const) : []),
160
+ ...(decks.some((deck) => deck.lessonId !== undefined) ? (['deck'] as const) : []),
161
+ ...(decks.some((deck) => deck.part !== undefined) ? (['part-deck'] as const) : []),
162
+ ];
163
+ }
@@ -41,8 +41,15 @@
41
41
  * overflows its column (`../starlight/DiagramEnhancer.astro`). To let a
42
42
  * reader fold a diagram away, wrap it in a `<Fold>` or put it in a
43
43
  * collapsible section.
44
+ *
45
+ * The whole drawing, boxes and text together, follows the reader's text size
46
+ * on a touch screen (`../starlight/TextSizeControl.astro`), so a diagram can
47
+ * be made to fit a phone: see `.diagram-frame` and `.diagram-canvas` below,
48
+ * and `docs/diagram-scaling.md` for why it is done this way and what not to
49
+ * change.
44
50
  */
45
51
  import { fitDiagramLayout, FORMULA_FONT_PX } from '../../reference/diagram-layout';
52
+ import { deckFloat } from '../deck/deck-context';
46
53
  import { measureFormula } from '../../reference/katex-measure';
47
54
  import { diagramTooWideMessage, diagramWidthLimit } from '../../reference/diagram-fit';
48
55
  import { loadManifest } from '../../compiler/manifest';
@@ -93,6 +100,7 @@ export interface Props {
93
100
  number?: string;
94
101
  /** Computed by `remark-notation.mjs` for a top-level lesson diagram. */
95
102
  anchorId?: string;
103
+
96
104
  /**
97
105
  * The widest this diagram may lay out, in px, before the build warns
98
106
  * (`dia-too-wide`, rule V8). Default: the course's `rules.V8.columnWidth`,
@@ -113,6 +121,9 @@ const {
113
121
  anchorId,
114
122
  warnAboveWidth,
115
123
  } = Astro.props;
124
+ // On a deck, the deck's own diagram carries no number and a recalled one's
125
+ // number links to its chapter (rule D8).
126
+ const { onDeck, numberHref } = await deckFloat(Astro.locals, anchorId, import.meta.env.BASE_URL);
116
127
 
117
128
  const idPattern = /^[A-Za-z][A-Za-z0-9_-]*$/;
118
129
  if (!idPattern.test(id)) {
@@ -141,7 +152,7 @@ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
141
152
  if (nodes.length === 0) {
142
153
  throw new Error(`Diagram "${id}" has no nodes.`);
143
154
  }
144
- if ((number && !anchorId) || (!number && anchorId)) {
155
+ if ((number && !anchorId) || (!number && anchorId && !onDeck)) {
145
156
  throw new Error(
146
157
  `Diagram "${id}" must receive both computed \`number\` and \`anchorId\`, or neither.`,
147
158
  );
@@ -306,91 +317,96 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
306
317
  aria-labelledby={captionTextId}
307
318
  >
308
319
  <div
309
- class="diagram-canvas"
310
- style={`--diagram-print-ratio: ${layout.width} / ${layout.height}; --diagram-width: ${layout.width}; --diagram-height: ${layout.height}; width: ${layout.width}px; height: ${layout.height}px;`}
320
+ class="diagram-frame"
321
+ style={`--diagram-width: ${layout.width}; --diagram-height: ${layout.height};`}
311
322
  >
312
- <svg
313
- viewBox={`0 0 ${layout.width} ${layout.height}`}
314
- width="100%"
315
- height="100%"
323
+ <div
324
+ class="diagram-canvas"
325
+ style={`--diagram-print-ratio: ${layout.width} / ${layout.height}; width: ${layout.width}px; height: ${layout.height}px;`}
316
326
  >
317
- <defs>
318
- <marker
319
- id={markerId}
320
- viewBox="0 0 10 10"
321
- refX="9"
322
- refY="5"
323
- markerWidth="7"
324
- markerHeight="7"
325
- orient="auto-start-reverse"
326
- >
327
- <path
328
- d="M 0 0 L 10 5 L 0 10 z"
329
- class="diagram-arrowhead"
330
- />
331
- </marker>
332
- </defs>
333
-
334
- {
335
- layout.edges.map((edge) => (
336
- <path
337
- d={pathFrom(edge.points)}
338
- class="diagram-edge"
339
- marker-end={`url(#${markerId})`}
340
- />
341
- ))
342
- }
343
-
344
- {
345
- layout.nodes.map((position) => {
346
- const node = nodeById.get(position.id)!;
347
- // Position each node with x/y attributes, never a
348
- // `transform` on the group: WebKit (every iOS browser)
349
- // paints a `foreignObject` descendant that gets its own
350
- // render layer — every KaTeX `position: relative` span —
351
- // without the ancestor transform, so formulas pile up at
352
- // the SVG origin while their boxes stay put.
353
- return (
354
- <g class="diagram-node">
355
- <rect
356
- x={position.x}
357
- y={position.y}
358
- width={position.width}
359
- height={position.height}
360
- rx="8"
361
- ry="8"
362
- />
363
- <foreignObject
364
- x={position.x}
365
- y={position.y}
366
- width={position.width}
367
- height={position.height}
368
- >
369
- <div
370
- xmlns="http://www.w3.org/1999/xhtml"
371
- class="diagram-node-content"
327
+ <svg
328
+ viewBox={`0 0 ${layout.width} ${layout.height}`}
329
+ width="100%"
330
+ height="100%"
331
+ >
332
+ <defs>
333
+ <marker
334
+ id={markerId}
335
+ viewBox="0 0 10 10"
336
+ refX="9"
337
+ refY="5"
338
+ markerWidth="7"
339
+ markerHeight="7"
340
+ orient="auto-start-reverse"
341
+ >
342
+ <path
343
+ d="M 0 0 L 10 5 L 0 10 z"
344
+ class="diagram-arrowhead"
345
+ />
346
+ </marker>
347
+ </defs>
348
+
349
+ {
350
+ layout.edges.map((edge) => (
351
+ <path
352
+ d={pathFrom(edge.points)}
353
+ class="diagram-edge"
354
+ marker-end={`url(#${markerId})`}
355
+ />
356
+ ))
357
+ }
358
+
359
+ {
360
+ layout.nodes.map((position) => {
361
+ const node = nodeById.get(position.id)!;
362
+ // Position each node with x/y attributes, never a
363
+ // `transform` on the group: WebKit (every iOS browser)
364
+ // paints a `foreignObject` descendant that gets its own
365
+ // render layer — every KaTeX `position: relative` span —
366
+ // without the ancestor transform, so formulas pile up at
367
+ // the SVG origin while their boxes stay put.
368
+ return (
369
+ <g class="diagram-node">
370
+ <rect
371
+ x={position.x}
372
+ y={position.y}
373
+ width={position.width}
374
+ height={position.height}
375
+ rx="8"
376
+ ry="8"
377
+ />
378
+ <foreignObject
379
+ x={position.x}
380
+ y={position.y}
381
+ width={position.width}
382
+ height={position.height}
372
383
  >
373
- {
374
- node.text && (
375
- <span class="diagram-node-text">{node.text}</span>
376
- )
377
- }
378
- {
379
- node.mathNodes && (
380
- <span class="diagram-node-formula notation-math">
381
- {node.mathNodes.map((mathNode) => (
382
- <SafeMathNode node={mathNode} />
383
- ))}
384
- </span>
385
- )
386
- }
387
- </div>
388
- </foreignObject>
389
- </g>
390
- );
391
- })
392
- }
393
- </svg>
384
+ <div
385
+ xmlns="http://www.w3.org/1999/xhtml"
386
+ class="diagram-node-content"
387
+ >
388
+ {
389
+ node.text && (
390
+ <span class="diagram-node-text">{node.text}</span>
391
+ )
392
+ }
393
+ {
394
+ node.mathNodes && (
395
+ <span class="diagram-node-formula notation-math">
396
+ {node.mathNodes.map((mathNode) => (
397
+ <SafeMathNode node={mathNode} />
398
+ ))}
399
+ </span>
400
+ )
401
+ }
402
+ </div>
403
+ </foreignObject>
404
+ </g>
405
+ );
406
+ })
407
+ }
408
+ </svg>
409
+ </div>
394
410
  </div>
395
411
  </div>
396
412
  </div>
@@ -403,6 +419,7 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
403
419
  short={short}
404
420
  anchorId={renderedAnchorId}
405
421
  number={number}
422
+ numberHref={numberHref}
406
423
  textId={captionTextId}
407
424
  >
408
425
  <button
@@ -479,6 +496,33 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
479
496
  flex: none;
480
497
  }
481
498
 
499
+ /* The reader's text size on a touch screen (`TextSizeControl.astro`)
500
+ shrinks the whole drawing, boxes and text, so that a wide diagram
501
+ fits a phone. It does so the way print does (`global.css`): the SVG
502
+ stays at its build-time size, and the HTML canvas around it is scaled
503
+ with a transform — never the `viewBox`, whose scale WebKit (every iOS
504
+ browser) does not apply to the HTML in a `foreignObject`. A transform
505
+ moves no layout box, so the frame takes the scaled size: it is what
506
+ the scroller measures (`scrollWidth`) and what sets the height. The
507
+ frame clips its content, which the scaled canvas just fills, so the
508
+ canvas's unscaled layout box adds nothing to the scroll width.
509
+ `--explico-text-scale` is 0.5 to 1; with no size chosen, or off a touch
510
+ screen, none of this applies. Only the lesson's own diagrams (also
511
+ when expanded) shrink: a card keeps the 12px floor on its text.
512
+ `docs/diagram-scaling.md` has the reasons and the rules for changing it. */
513
+ @media screen and (hover: none) {
514
+ :global(:root[data-text-size]) :global(.sl-markdown-content) .diagram-frame {
515
+ width: calc(var(--diagram-width) * 1px * var(--explico-text-scale, 1));
516
+ height: calc(var(--diagram-height) * 1px * var(--explico-text-scale, 1));
517
+ overflow: hidden;
518
+ }
519
+
520
+ :global(:root[data-text-size]) :global(.sl-markdown-content) .diagram-canvas {
521
+ transform: scale(var(--explico-text-scale, 1));
522
+ transform-origin: 0 0;
523
+ }
524
+ }
525
+
482
526
  .diagram.dia-ref-highlight {
483
527
  outline: 3px solid var(--sl-color-accent);
484
528
  outline-offset: 0.25rem;
@@ -494,9 +538,10 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
494
538
  box from these values; change them together. The size is in px, not
495
539
  rem: the boxes are measured at build time for 13.6px text, so text that
496
540
  followed a reader's larger default font size (20px, Chrome's "Large")
497
- outgrew its box and was cut off. Scaling the whole drawing instead is no
498
- way out — WebKit on iOS scales the SVG but not the HTML in a
499
- `foreignObject`. Browser zoom still enlarges it with everything else. */
541
+ outgrew its box and was cut off. Scaling the drawing through the SVG
542
+ is no way out — WebKit on iOS scales the SVG but not the HTML in a
543
+ `foreignObject`. Browser zoom still enlarges it with everything else,
544
+ and the text-size menu shrinks it as a whole (`.diagram-frame`). */
500
545
  .diagram-node-content {
501
546
  display: flex;
502
547
  height: 100%;
@@ -43,10 +43,18 @@ export function wirePersistedDetails(
43
43
  ): void {
44
44
  const fullKey = storageKey(key);
45
45
  const saved = store.get(fullKey);
46
- details.open = saved === null ? defaultOpen : saved === 'true';
46
+ // The state the page shows without the reader having touched it. Setting
47
+ // it below fires a `toggle` of its own when the markup started the other
48
+ // way; that one is not the reader's, so it stores nothing. Only a state
49
+ // the reader moved the element to is saved: a page that is merely opened
50
+ // writes nothing to the browser.
51
+ let shown = saved === null ? defaultOpen : saved === 'true';
52
+ details.open = shown;
47
53
 
48
54
  details.addEventListener('toggle', () => {
49
55
  if (details.hasAttribute('data-explico-temporary-open')) return;
56
+ if (details.open === shown) return;
57
+ shown = details.open;
50
58
  store.set(fullKey, String(details.open));
51
59
  });
52
60
  }
@@ -39,6 +39,7 @@
39
39
  * tools never print. A figure with no `tools` slot has no button and no tray.
40
40
  */
41
41
  import FloatCaption from './FloatCaption.astro';
42
+ import { deckFloat } from '../deck/deck-context';
42
43
  import { courseDisclosure } from '../disclosure/course-disclosure';
43
44
 
44
45
  export interface Props {
@@ -70,6 +71,9 @@ const {
70
71
  number,
71
72
  anchorId,
72
73
  } = Astro.props;
74
+ // On a deck, the deck's own figure carries no number and a recalled one's
75
+ // number links to its chapter (rule D8).
76
+ const { onDeck, numberHref } = await deckFloat(Astro.locals, anchorId, import.meta.env.BASE_URL);
73
77
 
74
78
  if ('title' in Astro.props) {
75
79
  throw new Error(
@@ -82,7 +86,7 @@ if (typeof caption !== 'string' || !caption.trim()) {
82
86
  if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
83
87
  throw new Error('Figure `short` must be a non-empty string when given.');
84
88
  }
85
- if (!number || !anchorId) {
89
+ if ((!number && !onDeck) || !anchorId) {
86
90
  throw new Error(
87
91
  `Figure is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Figure> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) show the content without <Figure>, and on a page that is not a chapter (rule S10) link the chapter's figure. At a lesson's top level, this page is not running remark-notation with the manifest's \`figures\` (see astro.config.mjs).`,
88
92
  );
@@ -91,7 +95,7 @@ if (!number || !anchorId) {
91
95
  const hasTools = Astro.slots.has('tools');
92
96
  const toolsId = `${anchorId}-tools`;
93
97
  const toolsOpen = hasTools && (defaultOpen ?? (await courseDisclosure()).figures.defaultOpen);
94
- const name = `Figure ${number}`;
98
+ const name = number ? `Figure ${number}` : 'Figure';
95
99
  ---
96
100
 
97
101
  <figure
@@ -113,6 +117,7 @@ const name = `Figure ${number}`;
113
117
  short={short}
114
118
  anchorId={anchorId}
115
119
  number={number}
120
+ numberHref={numberHref}
116
121
  live
117
122
  >
118
123
  {
@@ -27,7 +27,7 @@
27
27
  * `.float-caption__text`, where a lab states the case its plot shows
28
28
  * (`./figure-state.ts`).
29
29
  */
30
- import { captionTitle } from '../float-name';
30
+ import { captionTitle } from '../../reference/float-name';
31
31
 
32
32
  export interface Props {
33
33
  /** The reader-facing kind word: `Figure`, `Diagram`. */
@@ -42,13 +42,15 @@ export interface Props {
42
42
  anchorId: string;
43
43
  /** Its number; an unnumbered float shows none. */
44
44
  number?: string;
45
+ /** Where its number links: the float itself by default, a deck's recalled one to its chapter. */
46
+ numberHref?: string;
45
47
  /** An id for the caption text, for a control inside the body to be named by. */
46
48
  textId?: string;
47
49
  /** Give the line a live end a lab writes into (`./figure-state.ts`). */
48
50
  live?: boolean;
49
51
  }
50
52
 
51
- const { kind, prefix, caption, short, anchorId, number, textId, live = false } = Astro.props;
53
+ const { kind, prefix, caption, short, anchorId, number, numberHref, textId, live = false } = Astro.props;
52
54
  const name = number ? `${kind} ${number}` : kind.toLowerCase();
53
55
  const tools = Astro.slots.has('default') ? (await Astro.slots.render('default')).trim() : '';
54
56
  ---
@@ -58,7 +60,7 @@ const tools = Astro.slots.has('default') ? (await Astro.slots.render('default'))
58
60
  number && (
59
61
  <a
60
62
  class:list={['float-caption__number', `${prefix}__number`]}
61
- href={`#${anchorId}`}
63
+ href={numberHref ?? `#${anchorId}`}
62
64
  aria-label={name}
63
65
  >
64
66
  {name}
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * The bar is the `label`, set at body-text size. Without one it names the
21
21
  * numbered floats inside, "Diagram 2.1.1 Steps of the range derivation"
22
- * (`../float-name.ts`), read from the rendered content; with neither it
22
+ * (`../../reference/float-name.ts`), read from the rendered content; with neither it
23
23
  * throws, as `fold-unnamed` reports at `validate:content`. A bar that names
24
24
  * the floats (`data-names-floats`) is their name here: their caption lines
25
25
  * drop the bold short-caption title that would repeat it, and a folded
@@ -31,7 +31,7 @@
31
31
  * page.
32
32
  */
33
33
  import { courseDisclosure } from '../disclosure/course-disclosure';
34
- import { floatName, floatsInHtml } from '../float-name';
34
+ import { floatName, floatsInHtml } from '../../reference/float-name';
35
35
  import { foldId, foldSlug } from './fold-id';
36
36
 
37
37
  export interface Props {