explico 0.12.0 → 0.13.1

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 (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Diagnostics only a rendered page can give, kept from one `astro build` for
3
+ * `content warnings`.
4
+ *
5
+ * Most checks read the content tree, so the manifest carries them and every
6
+ * `content` command recomputes them. A few need the page as Astro renders it:
7
+ * a diagram drawn by a wrapper component lays out to a width only its render
8
+ * knows (rule V8, `reference/diagram-fit.ts`). `compiler/astro.ts` reports
9
+ * those as the build finishes and writes them to
10
+ * `build/render-diagnostics.json`, replacing the previous build's; the
11
+ * `content` CLI lists them beside the manifest's under the `render:` source.
12
+ * They are as current as the last build: run `pnpm build` (or `astro build`)
13
+ * after changing a diagram to refresh them.
14
+ */
15
+
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+
19
+ import type { DiagnosticSeverity } from '../reference/types';
20
+ import {
21
+ diagramTooWideMessage,
22
+ oversizedDiagrams,
23
+ } from '../reference/diagram-fit';
24
+ import type { Manifest } from './manifest';
25
+
26
+ export const RENDER_DIAGNOSTICS_PATH = join(
27
+ process.cwd(),
28
+ 'build',
29
+ 'render-diagnostics.json',
30
+ );
31
+
32
+ export interface RenderDiagnostic {
33
+ readonly code: string;
34
+ readonly severity: DiagnosticSeverity;
35
+ readonly message: string;
36
+ /** The lesson's source file, e.g. `content/docs/motion/range.mdx`; else the built page. */
37
+ readonly file: string;
38
+ readonly lessonId?: string;
39
+ /** The float the finding is about, e.g. a diagram's `id`. */
40
+ readonly key?: string;
41
+ }
42
+
43
+ /** A built page's path (`motion/range/index.html`) → the slug it serves. */
44
+ function slugOfPage(page: string): string {
45
+ return page
46
+ .split(/[\\/]/u)
47
+ .join('/')
48
+ .replace(/(^|\/)index\.html$/u, '')
49
+ .replace(/\.html$/u, '');
50
+ }
51
+
52
+ /**
53
+ * The render diagnostics of one built page. A diagram that appears on more
54
+ * than one page (a lesson and its standalone export) is reported once per
55
+ * lesson by {@link writeRenderDiagnostics}.
56
+ */
57
+ export function pageRenderDiagnostics(
58
+ manifest: Pick<Manifest, 'lessons'>,
59
+ page: string,
60
+ html: string,
61
+ ): RenderDiagnostic[] {
62
+ const oversized = oversizedDiagrams(html);
63
+ if (oversized.length === 0) return [];
64
+ const slug = slugOfPage(page);
65
+ const lesson = manifest.lessons.find((candidate) => candidate.slug === slug);
66
+ return oversized.map((diagram) => ({
67
+ code: diagram.code,
68
+ severity: 'warning',
69
+ message: diagramTooWideMessage(diagram.key, diagram.width, diagram.limit),
70
+ file: lesson?.file ?? page,
71
+ ...(lesson ? { lessonId: lesson.id } : {}),
72
+ key: diagram.key,
73
+ }));
74
+ }
75
+
76
+ /** Replace the last build's render diagnostics with these, deduplicated and sorted. */
77
+ export function writeRenderDiagnostics(
78
+ diagnostics: readonly RenderDiagnostic[],
79
+ path: string = RENDER_DIAGNOSTICS_PATH,
80
+ ): readonly RenderDiagnostic[] {
81
+ const unique = new Map<string, RenderDiagnostic>();
82
+ for (const diagnostic of diagnostics) {
83
+ unique.set(`${diagnostic.file}\0${diagnostic.code}\0${diagnostic.key ?? diagnostic.message}`, diagnostic);
84
+ }
85
+ const sorted = [...unique.values()].sort(
86
+ (a, b) =>
87
+ a.file.localeCompare(b.file) ||
88
+ a.code.localeCompare(b.code) ||
89
+ (a.key ?? '').localeCompare(b.key ?? ''),
90
+ );
91
+ mkdirSync(dirname(path), { recursive: true });
92
+ writeFileSync(path, `${JSON.stringify({ diagnostics: sorted }, null, 2)}\n`);
93
+ return sorted;
94
+ }
95
+
96
+ /** What the last build reported; `[]` before the first build. */
97
+ export function readRenderDiagnostics(
98
+ path: string = RENDER_DIAGNOSTICS_PATH,
99
+ ): readonly RenderDiagnostic[] {
100
+ if (!existsSync(path)) return [];
101
+ try {
102
+ const parsed = JSON.parse(readFileSync(path, 'utf8')) as {
103
+ readonly diagnostics?: unknown;
104
+ };
105
+ return Array.isArray(parsed.diagnostics)
106
+ ? (parsed.diagnostics as RenderDiagnostic[])
107
+ : [];
108
+ } catch {
109
+ return [];
110
+ }
111
+ }
@@ -11,8 +11,9 @@
11
11
  * Pin toggle and a × — a note line under it, and the layer's own fields in
12
12
  * the default slot. The card manager owns the buttons (`data-card-grip`,
13
13
  * `data-card-side`, `data-card-pin`, `data-card-close`), the note
14
- * (`data-card-note`), the mode (`data-card-mode`: hover, pinned, side or
15
- * moved), the flash (`is-card-flashing`) and where the card goes
14
+ * (`data-card-note`), the carrying mark before the buttons (`data-card-mark`,
15
+ * rule K8), the mode (`data-card-mode`: hover, pinned, side or moved), the
16
+ * flash (`is-card-flashing`) and where the card goes
16
17
  * (`./card-placement.ts`, `./card-side.ts`, `./card-drag.ts`).
17
18
  *
18
19
  * Extra attributes (`data-citation-panel`, `data-float-ref-panel`, …) go on
@@ -59,6 +60,33 @@ const { id, label, class: className, ...rest } = Astro.props;
59
60
  </button>
60
61
  <div class="hover-panel-heading"><slot name="heading" /></div>
61
62
  <div class="hover-panel-tools">
63
+ {/* K8: with carrying on, whether a kept card means the same on every
64
+ page (a globe) or only on its own (`card-manager.ts`). */}
65
+ <span class="hover-panel-mark" data-card-mark role="img" hidden>
66
+ <svg
67
+ class="hover-panel-mark-global"
68
+ viewBox="0 0 16 16"
69
+ width="14"
70
+ height="14"
71
+ aria-hidden="true"
72
+ >
73
+ <circle cx="8" cy="8" r="6.25"></circle>
74
+ <path
75
+ d="M1.75 8h12.5M8 1.75c1.8 1.9 2.7 4 2.7 6.25S9.8 12.35 8 14.25M8 1.75C6.2 3.65 5.3 5.75 5.3 8s.9 4.35 2.7 6.25"
76
+ ></path>
77
+ </svg>
78
+ <svg
79
+ class="hover-panel-mark-page"
80
+ viewBox="0 0 16 16"
81
+ width="14"
82
+ height="14"
83
+ aria-hidden="true"
84
+ >
85
+ <path d="M3.75 1.75h5.5l3 3v9.5h-8.5z"></path>
86
+ <path d="M9.25 1.75v3h3"></path>
87
+ </svg>
88
+ <span class="hover-panel-mark-page" aria-hidden="true">this page</span>
89
+ </span>
62
90
  <button
63
91
  class="hover-panel-side"
64
92
  type="button"
@@ -104,6 +132,16 @@ const { id, label, class: className, ...rest } = Astro.props;
104
132
  <div class="hover-panel-scroll">
105
133
  <slot />
106
134
  </div>
135
+ <div
136
+ class="hover-panel-resize"
137
+ data-card-resize
138
+ role="separator"
139
+ aria-orientation="vertical"
140
+ aria-label="Card width"
141
+ title="Drag to widen this card. Arrow keys change it; Home or a double click puts it back."
142
+ tabindex="0"
143
+ >
144
+ </div>
107
145
  </aside>
108
146
  </template>
109
147
 
@@ -129,7 +167,10 @@ const { id, label, class: className, ...rest } = Astro.props;
129
167
  color: var(--sl-color-text);
130
168
  background: var(--sl-color-bg);
131
169
  box-shadow: var(--sl-shadow-lg);
132
- font-size: var(--sl-text-sm);
170
+ /* 14 px, the article's caption size; never under 12 px, which a
171
+ reader's smallest text size (70%, `TextSizeControl.astro`) would
172
+ otherwise take a card's text to. */
173
+ font-size: max(12px, var(--sl-text-sm));
133
174
  }
134
175
 
135
176
  /* `.hover-panel { display: flex }` above has the same specificity as the
@@ -360,11 +401,144 @@ const { id, label, class: className, ...rest } = Astro.props;
360
401
  outline-offset: 2px;
361
402
  }
362
403
 
404
+ /* A card's footer links ("Go to the full definition", "Go to the
405
+ reference list", …) are actions in a row of their own, not words in a
406
+ sentence: an outline button says "this goes somewhere" without the
407
+ underline, and its arrow says where. They stay links, so they open in a
408
+ new tab and announce as links. The card layer sits outside the
409
+ article, whose link colour never reached them, so they set the site's
410
+ accent here; a link inside a card's text keeps its underline. */
411
+ /* S11: a docked card's right edge widens it (`card-manager.ts`): a thin
412
+ grip along the edge, shown on hover and focus. Only a docked card has
413
+ it. */
414
+ .hover-panel-resize {
415
+ display: none;
416
+ position: absolute;
417
+ inset-block: 0.75rem;
418
+ inset-inline-end: 0;
419
+ width: 0.5rem;
420
+ cursor: col-resize;
421
+ touch-action: none;
422
+ }
423
+
424
+ .hover-panel[data-card-mode='side'] .hover-panel-resize {
425
+ display: block;
426
+ }
427
+
428
+ .hover-panel-resize::after {
429
+ content: '';
430
+ position: absolute;
431
+ inset-block: 0;
432
+ inset-inline-end: 0.15rem;
433
+ width: 2px;
434
+ border-radius: 1px;
435
+ background: var(--sl-color-accent);
436
+ opacity: 0;
437
+ transition: opacity 120ms ease-out;
438
+ }
439
+
440
+ .hover-panel-resize:hover::after,
441
+ .hover-panel-resize:focus-visible::after {
442
+ opacity: 1;
443
+ }
444
+
445
+ .hover-panel-resize:focus-visible {
446
+ outline: 2px solid var(--sl-color-accent);
447
+ outline-offset: -2px;
448
+ }
449
+
450
+ /* Slim: about as tall as the text line it replaced, so a card keeps its
451
+ height and lands where it did. */
452
+ .hover-panel :global(.hover-panel-action) {
453
+ display: inline-flex;
454
+ align-items: center;
455
+ gap: 0.3em;
456
+ margin-block: 0.0625rem;
457
+ margin-inline-end: 0.4rem;
458
+ padding: 0.1rem 0.5rem;
459
+ border: 1px solid var(--sl-color-gray-5);
460
+ border-radius: 0.375rem;
461
+ color: var(--sl-color-text-accent);
462
+ font-size: var(--sl-text-xs);
463
+ line-height: 1.3;
464
+ text-decoration: none;
465
+ vertical-align: middle;
466
+ }
467
+
468
+ .hover-panel :global(.hover-panel-action:hover) {
469
+ border-color: var(--sl-color-accent);
470
+ background: var(--sl-color-accent-low);
471
+ }
472
+
473
+ .hover-panel :global(.hover-panel-action[hidden]) {
474
+ display: none;
475
+ }
476
+
477
+ /* ↗ is the one jump arrow with an emoji form, and a fallback font can
478
+ draw it as a coloured emoji box; ask for the text glyph, as ↑ ↓ → are
479
+ drawn (`card-jump.ts`). */
480
+ .hover-panel :global([data-jump-arrow]) {
481
+ font-variant-emoji: text;
482
+ }
483
+
363
484
  @media (forced-colors: active) {
364
485
  .hover-panel:is([data-card-mode='pinned'], [data-card-mode='side'], [data-card-mode='moved']) {
365
486
  border-color: Highlight;
366
487
  }
367
488
  }
489
+
490
+ /* K3: a card held at the window's edge in a stack too tall for it shows
491
+ its header row only; a click on it, or the keyboard's focus, opens it
492
+ again (`card-manager.ts`). */
493
+ .hover-panel[data-card-collapsed] > :not(.hover-panel-header) {
494
+ display: none;
495
+ }
496
+
497
+ .hover-panel[data-card-collapsed] .hover-panel-header {
498
+ padding-block-end: 0.5rem;
499
+ cursor: pointer;
500
+ }
501
+
502
+ /* K8: a globe, or a page and "this page", muted, before the buttons. */
503
+ .hover-panel-mark {
504
+ display: inline-flex;
505
+ align-items: center;
506
+ gap: 0.25rem;
507
+ margin-inline-end: 0.3rem;
508
+ font-size: var(--sl-text-xs);
509
+ line-height: 1;
510
+ white-space: nowrap;
511
+ color: var(--sl-color-gray-3);
512
+ }
513
+
514
+ .hover-panel-mark[hidden] {
515
+ display: none;
516
+ }
517
+
518
+ .hover-panel-mark svg {
519
+ fill: none;
520
+ stroke: currentColor;
521
+ stroke-width: 1.3;
522
+ stroke-linecap: round;
523
+ stroke-linejoin: round;
524
+ }
525
+
526
+ .hover-panel-mark[data-card-mark='global'] .hover-panel-mark-page,
527
+ .hover-panel-mark[data-card-mark='page'] .hover-panel-mark-global {
528
+ display: none;
529
+ }
530
+
531
+ /* K5: a card carried in from another page has no trigger here to be
532
+ pinned next to; × ends its journey, Side still docks it. */
533
+ .hover-panel[data-card-away] .hover-panel-pin {
534
+ display: none;
535
+ }
536
+
537
+ /* P1: its trigger is folded away (`foldAway` in card-manager.ts); the card
538
+ keeps its size for when the trigger shows again. */
539
+ .hover-panel[data-card-folded] {
540
+ visibility: hidden;
541
+ }
368
542
  }
369
543
 
370
544
  /* D1: while a card is dragged, nothing on the page is selected and the
@@ -444,6 +618,12 @@ const { id, label, class: className, ...rest } = Astro.props;
444
618
  :global(.explico-card-dock) {
445
619
  display: none;
446
620
  }
621
+
622
+ /* K2: a held card is fixed to the window, which print repeats on every
623
+ sheet; it belongs to the screen it was held on. */
624
+ :global(.hover-panel[data-card-held]) {
625
+ display: none !important;
626
+ }
447
627
  }
448
628
 
449
629
  /* S5: the arrow from a kept card to an occurrence of its item
@@ -467,6 +647,16 @@ const { id, label, class: className, ...rest } = Astro.props;
467
647
  fill: var(--sl-color-accent);
468
648
  }
469
649
 
650
+ /* D4: to a symbol that only looks like the card's, defined elsewhere. */
651
+ :global(.explico-card-arrow.is-lookalike .explico-card-arrow-line) {
652
+ stroke: var(--sl-color-gray-3);
653
+ stroke-dasharray: 5 4;
654
+ }
655
+
656
+ :global(.explico-card-arrow.is-lookalike .explico-card-arrow-head) {
657
+ fill: var(--sl-color-gray-3);
658
+ }
659
+
470
660
  @media (prefers-reduced-motion: reduce) {
471
661
  :global(.explico-card-arrow) {
472
662
  transition: none;
@@ -481,6 +671,14 @@ const { id, label, class: className, ...rest } = Astro.props;
481
671
  :global(.explico-card-arrow-head) {
482
672
  fill: Highlight;
483
673
  }
674
+
675
+ :global(.explico-card-arrow.is-lookalike .explico-card-arrow-line) {
676
+ stroke: GrayText;
677
+ }
678
+
679
+ :global(.explico-card-arrow.is-lookalike .explico-card-arrow-head) {
680
+ fill: GrayText;
681
+ }
484
682
  }
485
683
 
486
684
  @media print {
@@ -489,9 +687,9 @@ const { id, label, class: className, ...rest } = Astro.props;
489
687
  }
490
688
  }
491
689
 
492
- /* The open mobile menu covers the page; the cards above it would float
493
- over the menu. */
494
- :global(body[data-mobile-menu-expanded] .explico-card-layer) {
690
+ /* The open mobile menu (Starlight's `#starlight__sidebar` popover) covers
691
+ the page; the cards would stay focusable behind it. */
692
+ :global(body:has(#starlight__sidebar:popover-open) .explico-card-layer) {
495
693
  display: none;
496
694
  }
497
695
 
@@ -152,19 +152,21 @@ const hasRail =
152
152
  ---
153
153
 
154
154
  <div class="part-overview not-content" data-rail={hasRail ? '' : undefined}>
155
- <p class="part-meta">
156
- <span>Part {part.ordinal} of {manifest.parts.length}</span>
157
- <span>{chapters.length} {chapters.length === 1 ? 'chapter' : 'chapters'}</span>
158
- {part.studyMinutes && <span>{minutes(part.studyMinutes)}</span>}
159
- {equationCount > 0 && <span>{equationCount} numbered equations</span>}
155
+ {/* A list, not spans in a row: search and a screen reader keep the items
156
+ apart ("Part 1 of 2", "3 chapters"), where spans ran into "23 chapters". */}
157
+ <ul class="part-meta" aria-label="This part at a glance">
158
+ <li>Part {part.ordinal} of {manifest.parts.length}</li>
159
+ <li>{chapters.length} {chapters.length === 1 ? 'chapter' : 'chapters'}</li>
160
+ {part.studyMinutes && <li>{minutes(part.studyMinutes)}</li>}
161
+ {equationCount > 0 && <li>{equationCount} numbered equations</li>}
160
162
  {
161
163
  checkCount > 0 && (
162
- <span>
164
+ <li>
163
165
  {checkCount} knowledge {checkCount === 1 ? 'check' : 'checks'}
164
- </span>
166
+ </li>
165
167
  )
166
168
  }
167
- </p>
169
+ </ul>
168
170
 
169
171
  <div class="part-body">
170
172
  <ol class="part-chapters">
@@ -189,7 +191,6 @@ const hasRail =
189
191
  {lesson.knowledgeChecks.length === 1 ? '' : 's'}
190
192
  </span>
191
193
  )}
192
- {lesson.status !== 'reviewed' && <span>{lesson.status}</span>}
193
194
  </p>
194
195
  </li>
195
196
  ))
@@ -273,6 +274,8 @@ const hasRail =
273
274
  flex-wrap: wrap;
274
275
  gap: 0.3rem 1rem;
275
276
  margin: 0 0 1.25rem;
277
+ padding: 0;
278
+ list-style: none;
276
279
  color: var(--sl-color-gray-3);
277
280
  font-size: var(--sl-text-xs);
278
281
  letter-spacing: 0.04em;
@@ -24,7 +24,7 @@ const entries = await getCollection('abbreviations');
24
24
  <p class="abbreviation-panel-term" data-panel-term slot="heading"></p>
25
25
  <div class="abbreviation-panel-body">
26
26
  <p class="abbreviation-panel-expansion" data-panel-expansion></p>
27
- <a class="abbreviation-panel-jump" data-panel-jump
27
+ <a class="abbreviation-panel-jump hover-panel-action" data-panel-jump
28
28
  >Go to the glossary<span aria-hidden="true" data-jump-arrow> →</span></a
29
29
  >
30
30
  </div>
@@ -40,6 +40,7 @@ const entries = await getCollection('abbreviations');
40
40
 
41
41
  <script>
42
42
  import { cloneCardShell, registerCardSource } from '../card-manager';
43
+ import { fetchDoc, hrefFrom } from '../card-fetch';
43
44
  import { wireJumpArrow } from '../card-jump';
44
45
 
45
46
  function closestElement(node: EventTarget | null, selector: string) {
@@ -51,18 +52,21 @@ const entries = await getCollection('abbreviations');
51
52
 
52
53
  function setup() {
53
54
  const template = document.getElementById('abbreviation-panel');
55
+ // Every page registers, with abbreviations or none: a card carried in
56
+ // from another page is built here (rule K5).
54
57
  if (
55
58
  !(template instanceof HTMLTemplateElement) ||
56
- template.dataset.cardSourceReady === 'true' ||
57
- !document.querySelector('a.abbreviation-term[data-abbr-key]')
59
+ template.dataset.cardSourceReady === 'true'
58
60
  ) {
59
61
  return;
60
62
  }
61
63
  template.dataset.cardSourceReady = 'true';
62
64
 
63
65
  // Fill a card from the matching hidden `[data-abbr-entry]` block — never
64
- // from a second serialized copy of the table.
65
- const render = (key: string, marker: HTMLElement) => {
66
+ // from a second serialized copy of the table. Every page has every
67
+ // entry; a card carried in from another page (K5) takes its term, as the
68
+ // text wrote it, from the marker there, and its link goes from there.
69
+ const render = (key: string, marker: HTMLElement, page?: string) => {
66
70
  const entry = entryFor(key);
67
71
  const el = cloneCardShell(template);
68
72
  const term = el?.querySelector<HTMLElement>('[data-panel-term]');
@@ -72,7 +76,8 @@ const entries = await getCollection('abbreviations');
72
76
 
73
77
  term.textContent = marker.textContent ?? '';
74
78
  expansion.textContent = entry.textContent ?? '';
75
- jump.href = marker.getAttribute('href') ?? '#abbreviations';
79
+ const jumpTo = marker.getAttribute('href') ?? '#abbreviations';
80
+ jump.href = page ? hrefFrom(jumpTo, page) : jumpTo;
76
81
  wireJumpArrow(jump);
77
82
  el.setAttribute('aria-label', `Abbreviation: ${term.textContent}`);
78
83
  return el;
@@ -85,7 +90,16 @@ const entries = await getCollection('abbreviations');
85
90
  const key = marker.dataset.abbrKey;
86
91
  return key && entryFor(key) ? key : null;
87
92
  },
88
- render,
93
+ render: (key, marker) => render(key, marker),
94
+ build: (key, page) =>
95
+ fetchDoc(page).then((doc) => {
96
+ const marker = doc.querySelector<HTMLElement>(
97
+ `a.abbreviation-term[data-abbr-key="${CSS.escape(key)}"]`,
98
+ );
99
+ const el = marker ? render(key, marker, page) : null;
100
+ el?.setAttribute('data-card-foreign', '');
101
+ return el;
102
+ }),
89
103
  });
90
104
  }
91
105
 
@@ -116,10 +130,5 @@ const entries = await getCollection('abbreviations');
116
130
  margin: 0;
117
131
  font-weight: 600;
118
132
  }
119
-
120
- .abbreviation-panel-jump {
121
- display: inline-block;
122
- font-size: var(--sl-text-xs);
123
- }
124
133
  }
125
134
  </style>
@@ -17,6 +17,8 @@ import { shuffled } from './assessment-randomization';
17
17
  interface ChoiceOption {
18
18
  id: string;
19
19
  label: string;
20
+ /** The label split into text and rendered math (`AssessmentSet.astro`). */
21
+ labelContent?: MixedContentNode[];
20
22
  }
21
23
 
22
24
  interface AssessmentItemBase {
@@ -194,7 +196,11 @@ function Question({
194
196
  disabled={checked}
195
197
  onChange={() => setChoice(option.id)}
196
198
  />
197
- {option.label}
199
+ {option.labelContent ? (
200
+ <RenderedContent content={option.labelContent} />
201
+ ) : (
202
+ option.label
203
+ )}
198
204
  </label>
199
205
  ))}
200
206
  </fieldset>
@@ -67,7 +67,8 @@ const entries = ids.map((id, index) => {
67
67
  });
68
68
 
69
69
  // `manifest.assessmentContent` (schema v8) already resolved every glyph in
70
- // every item's `prompt` / `explanation` against this page's lesson scope —
70
+ // every item's `prompt` / `explanation` (and, v30, a choice's label) against
71
+ // this page's lesson scope —
71
72
  // same `pageScope`, same relationship, as `gate-math.ts`'s assessment check.
72
73
  // Rendering the resolved LaTeX to a `SafeMathNode` tree happens here, in an
73
74
  // Astro component, not in `manifest.ts`: the compiler layer stays
@@ -127,6 +128,18 @@ function renderedContent(
127
128
  item.explanation,
128
129
  content?.explanationSegments,
129
130
  ),
131
+ // A choice's label renders its math like the prompt (schema v30).
132
+ ...(item.type === 'single-choice'
133
+ ? {
134
+ options: item.options.map((option: { id: string; label: string }) => ({
135
+ ...option,
136
+ labelContent: renderedContent(
137
+ option.label,
138
+ content?.options?.find((entry) => entry.id === option.id)?.labelSegments,
139
+ ),
140
+ })),
141
+ }
142
+ : {}),
130
143
  };
131
144
  })}
132
145
  />
@@ -59,6 +59,44 @@ export async function titledBlocksOf(
59
59
  return lesson?.titledBlocks ?? [];
60
60
  }
61
61
 
62
+ /**
63
+ * The text a reader sees for each outline heading with inline math, by its
64
+ * anchor (`manifest.lessons[].headings`, v30). Astro lists a heading by all
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`.
67
+ */
68
+ export async function mathHeadingTextsOf(lessonId: string): Promise<ReadonlyMap<string, string>> {
69
+ const lesson = (await manifestOnce()).lessons.find((entry) => entry.id === lessonId);
70
+ return new Map(
71
+ (lesson?.headings ?? [])
72
+ .filter((heading) => heading.math)
73
+ .map((heading) => [heading.slug, heading.text]),
74
+ );
75
+ }
76
+
77
+ /** `headings` with the manifest's text for each one in `texts`. */
78
+ export function withHeadingTexts<T extends OutlineHeading>(
79
+ headings: readonly T[],
80
+ texts: ReadonlyMap<string, string>,
81
+ ): T[] {
82
+ return headings.map((heading) => {
83
+ const text = texts.get(heading.slug);
84
+ return text === undefined ? heading : { ...heading, text };
85
+ });
86
+ }
87
+
88
+ /** An "On this page" tree with the manifest's text for each entry in `texts`. */
89
+ export function tocWithHeadingTexts(
90
+ items: readonly TocItem[],
91
+ texts: ReadonlyMap<string, string>,
92
+ ): TocItem[] {
93
+ return items.map((item) => ({
94
+ ...item,
95
+ text: texts.get(item.slug) ?? item.text,
96
+ children: tocWithHeadingTexts(item.children, texts),
97
+ }));
98
+ }
99
+
62
100
  /** A heading as Astro and Starlight list it (`MarkdownHeading`). */
63
101
  export interface OutlineHeading {
64
102
  readonly depth: number;
@@ -9,6 +9,10 @@
9
9
  * arrowhead. It stays while the occurrence or the card is hovered or focused,
10
10
  * and fades a while after both are left.
11
11
  *
12
+ * D4, K6: from a card carried in from another page for one of that page's
13
+ * own symbols, the arrow to a symbol here that looks the same is dashed and
14
+ * grey: the same letters, defined elsewhere.
15
+ *
12
16
  * `arrowShape` is the geometry, pure, over plain boxes, so
13
17
  * `tests/unit/card-arrow.test.ts` can check it in Node. The overlay is one
14
18
  * SVG at the bottom of the card layer (`./card-placement.ts`): above the
@@ -94,9 +98,14 @@ export function arrowShape(card: Box, target: Box): ArrowShape {
94
98
  // The overlay.
95
99
  // ---------------------------------------------------------------------------
96
100
 
101
+ export interface ArrowOptions {
102
+ /** D4: `target` only looks like the card's symbol: the arrow is dashed and grey. */
103
+ readonly lookalike?: boolean;
104
+ }
105
+
97
106
  export interface ArrowOverlay {
98
107
  /** Draw the arrow from `card` to `target`, replacing any other. */
99
- show(card: HTMLElement, target: HTMLElement): void;
108
+ show(card: HTMLElement, target: HTMLElement, options?: ArrowOptions): void;
100
109
  /** Draw it again after the card or the occurrence moved. */
101
110
  redraw(): void;
102
111
  /**
@@ -136,6 +145,7 @@ export function createArrowOverlay(options: ArrowOverlayOptions): ArrowOverlay {
136
145
  let head: SVGPolygonElement | null = null;
137
146
  let from: HTMLElement | null = null;
138
147
  let to: HTMLElement | null = null;
148
+ let lookalike = false;
139
149
  let holdTimer: ReturnType<typeof setTimeout> | null = null;
140
150
  let fadeTimer: ReturnType<typeof setTimeout> | null = null;
141
151
 
@@ -171,7 +181,7 @@ export function createArrowOverlay(options: ArrowOverlayOptions): ArrowOverlay {
171
181
  from = null;
172
182
  to = null;
173
183
  svg?.remove();
174
- svg?.classList.remove('is-fading');
184
+ svg?.classList.remove('is-fading', 'is-lookalike');
175
185
  };
176
186
 
177
187
  const draw = () => {
@@ -193,7 +203,10 @@ export function createArrowOverlay(options: ArrowOverlayOptions): ArrowOverlay {
193
203
  mount();
194
204
  line?.setAttribute('d', shape.path);
195
205
  head?.setAttribute('points', shape.head);
196
- if (svg) svg.dataset.cardArrow = from.dataset.cardKey ?? '';
206
+ if (svg) {
207
+ svg.dataset.cardArrow = from.dataset.cardKey ?? '';
208
+ svg.classList.toggle('is-lookalike', lookalike);
209
+ }
197
210
  };
198
211
 
199
212
  const fade = () => {
@@ -221,10 +234,11 @@ export function createArrowOverlay(options: ArrowOverlayOptions): ArrowOverlay {
221
234
  };
222
235
 
223
236
  return {
224
- show(card, target) {
237
+ show(card, target, options = {}) {
225
238
  hold();
226
239
  from = card;
227
240
  to = target;
241
+ lookalike = options.lookalike === true;
228
242
  draw();
229
243
  },
230
244
  redraw: draw,