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,274 @@
1
+ /**
2
+ * The toolbar on a float's number. Hovering or focusing the number of an
3
+ * equation, a figure, a table or a diagram shows Pin and Dock just above the
4
+ * number and Copy link just below it, none of them over the number. Pin and
5
+ * Dock open the float's own reference card already kept, hung from the
6
+ * number (`card-manager.ts`, `keepCardFor`): pinned next to the float, or
7
+ * docked beside the article. Copy link is a `data-copy-link` link, so
8
+ * `HeadingAnchorEnhancer.astro` copies it, names it and confirms it, as it
9
+ * did for the chain icon this replaced, and the click still puts the anchor
10
+ * in the address bar. A click on the number itself only does that.
11
+ *
12
+ * There is one toolbar, moved next to the number in use, so Tab runs from
13
+ * the number through Pin, Dock and Copy link. A screen with no hover gets
14
+ * none: no pinning or docking there, and a long press on the number already
15
+ * offers the browser's own "Copy link".
16
+ *
17
+ * An equation's Copy LaTeX button is its own (`remark-notation.mjs`) and
18
+ * sits on the bottom border of the equation, which this module measures:
19
+ * under the number when the link button fits between the two, else slid
20
+ * along the border, clear of the number (`--eq-copy-inline-end`,
21
+ * `data-eq-compact`). The geometry is `copyLatexPlace`, pure, so
22
+ * `tests/unit/float-toolbar.test.ts` checks it in Node.
23
+ */
24
+ import { keepCardFor } from './card-manager';
25
+
26
+ /** A float's own number: an equation's, or a figure's, table's or diagram's caption number. */
27
+ export const FLOAT_NUMBER = 'a.keyed-equation__number, a.float-caption__number';
28
+
29
+ /** Between the number and a button, in px. */
30
+ const GAP = 3;
31
+ const HIDE_DELAY_MS = 250;
32
+
33
+ const PIN_ICON =
34
+ '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 1.75h4v4.25l2.25 2.75h-8.5L6 6z"/><path d="M8 8.75V14.5"/></svg>';
35
+ const DOCK_ICON =
36
+ '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.75" width="12.5" height="10.5" rx="1.5"/><path d="M10.25 2.75v10.5"/></svg>';
37
+ const LINK_ICON =
38
+ '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"/></svg>';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // The geometry, pure.
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface CopyLatexInput {
45
+ /** The equation's box: its inline-end edge and its bottom, in px along each axis. */
46
+ readonly blockEnd: number;
47
+ readonly blockBottom: number;
48
+ /** The number's box. */
49
+ readonly numberStart: number;
50
+ readonly numberEnd: number;
51
+ readonly numberBottom: number;
52
+ /** The buttons' size, and the gap between the number and a button. */
53
+ readonly size: number;
54
+ readonly gap: number;
55
+ }
56
+
57
+ export interface CopyLatexPlace {
58
+ /** From the block's inline-end edge to the button's. */
59
+ readonly inlineEnd: number;
60
+ /** Too short for the link button between the number and the border. */
61
+ readonly compact: boolean;
62
+ }
63
+
64
+ /**
65
+ * Where an equation's Copy LaTeX button sits on its bottom border. Its
66
+ * centre is on the border. Under the number when the number's Copy link
67
+ * button (right below the number) fits above it; otherwise just past the
68
+ * number's inline-start edge, so neither button covers the number or the
69
+ * other. Coordinates run along the inline axis from the block's start, so
70
+ * the same arithmetic serves a right-to-left page.
71
+ */
72
+ export function copyLatexPlace(input: CopyLatexInput): CopyLatexPlace {
73
+ const { blockEnd, blockBottom, numberStart, numberEnd, numberBottom, size, gap } = input;
74
+ const linkBottom = numberBottom + gap + size + gap;
75
+ const copyTop = blockBottom - size / 2;
76
+ const compact = linkBottom > copyTop;
77
+ const numberCentre = (numberStart + numberEnd) / 2;
78
+ const inlineEnd = compact
79
+ ? blockEnd - numberStart + gap
80
+ : blockEnd - numberCentre - size / 2;
81
+ return { inlineEnd: Math.max(0, Math.round(inlineEnd)), compact };
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // The DOM layer.
86
+ // ---------------------------------------------------------------------------
87
+
88
+ interface Toolbar {
89
+ readonly root: HTMLElement;
90
+ readonly above: HTMLElement;
91
+ readonly pin: HTMLButtonElement;
92
+ readonly dock: HTMLButtonElement;
93
+ readonly link: HTMLAnchorElement;
94
+ number: HTMLAnchorElement | null;
95
+ hideTimer: number | null;
96
+ }
97
+
98
+ function button(icon: string, label: string, title: string): HTMLButtonElement {
99
+ const el = document.createElement('button');
100
+ el.type = 'button';
101
+ el.className = 'float-toolbar__button';
102
+ el.innerHTML = icon;
103
+ el.setAttribute('aria-label', label);
104
+ el.title = title;
105
+ return el;
106
+ }
107
+
108
+ function build(): Toolbar {
109
+ const root = document.createElement('span');
110
+ // `not-content`: Starlight's article styles (the flow margin between
111
+ // siblings) stay off the buttons.
112
+ root.className = 'float-toolbar not-content';
113
+ root.hidden = true;
114
+ const above = document.createElement('span');
115
+ above.className = 'float-toolbar__above';
116
+ const pin = button(PIN_ICON, 'Pin', 'Keep this card open');
117
+ pin.dataset.floatPin = '';
118
+ const dock = button(DOCK_ICON, 'Beside the article', 'Keep this card beside the article');
119
+ dock.dataset.floatDock = '';
120
+ const link = document.createElement('a');
121
+ link.className = 'float-toolbar__link';
122
+ link.innerHTML = LINK_ICON;
123
+ link.title = 'Copy link';
124
+ above.append(pin, dock);
125
+ root.append(above, link);
126
+ return { root, above, pin, dock, link, number: null, hideTimer: null };
127
+ }
128
+
129
+ /** "Equation (2.1.1)", "Figure 2.1.1": what Copy link names the float. */
130
+ function nameOf(number: HTMLElement): string {
131
+ const text = (number.textContent ?? '').trim();
132
+ return number.matches('a.keyed-equation__number') ? `Equation ${text}` : text;
133
+ }
134
+
135
+ /** Each part against the number, in the coordinates of the box they are placed in. */
136
+ function place(toolbar: Toolbar, number: HTMLElement): void {
137
+ const parent = (toolbar.above.offsetParent as HTMLElement | null) ?? document.body;
138
+ const box = parent.getBoundingClientRect();
139
+ const n = number.getBoundingClientRect();
140
+ const centre = n.left + n.width / 2 - box.left - parent.clientLeft + parent.scrollLeft;
141
+ const top = n.top - box.top - parent.clientTop + parent.scrollTop;
142
+ toolbar.above.style.left = `${Math.round(centre - toolbar.above.offsetWidth / 2)}px`;
143
+ toolbar.above.style.top = `${Math.round(top - toolbar.above.offsetHeight - GAP)}px`;
144
+ toolbar.link.style.left = `${Math.round(centre - toolbar.link.offsetWidth / 2)}px`;
145
+ toolbar.link.style.top = `${Math.round(top + n.height + GAP)}px`;
146
+ }
147
+
148
+ function layoutEquation(equation: HTMLElement): void {
149
+ const number = equation.querySelector<HTMLElement>('a.keyed-equation__number');
150
+ const copy = equation.querySelector<HTMLElement>('.keyed-equation__copy');
151
+ if (!number || !copy) return;
152
+ const e = equation.getBoundingClientRect();
153
+ const n = number.getBoundingClientRect();
154
+ if (e.height === 0 || n.width === 0) return;
155
+ const rtl = getComputedStyle(equation).direction === 'rtl';
156
+ // Along the inline axis, from the block's start edge.
157
+ const along = (x: number) => (rtl ? e.right - x : x - e.left);
158
+ const { inlineEnd, compact } = copyLatexPlace({
159
+ blockEnd: e.width,
160
+ blockBottom: e.height,
161
+ numberStart: Math.min(along(n.left), along(n.right)),
162
+ numberEnd: Math.max(along(n.left), along(n.right)),
163
+ numberBottom: n.bottom - e.top,
164
+ size: copy.offsetWidth || 22,
165
+ gap: GAP,
166
+ });
167
+ equation.style.setProperty('--eq-copy-inline-end', `${inlineEnd}px`);
168
+ equation.toggleAttribute('data-eq-compact', compact);
169
+ }
170
+
171
+ /** Place every equation's Copy LaTeX button, and again whenever an equation changes size. */
172
+ function watchEquations(root: ParentNode): void {
173
+ const equations = Array.from(root.querySelectorAll<HTMLElement>('.keyed-equation'));
174
+ if (equations.length === 0) return;
175
+ const observer = new ResizeObserver((entries) => {
176
+ for (const entry of entries) layoutEquation(entry.target as HTMLElement);
177
+ });
178
+ for (const equation of equations) {
179
+ layoutEquation(equation);
180
+ observer.observe(equation);
181
+ }
182
+ document.fonts?.ready.then(() => equations.forEach(layoutEquation), () => {});
183
+ }
184
+
185
+ /** Wire the toolbar for every float number in `root`, the article. */
186
+ export function startFloatToolbar(root: HTMLElement): void {
187
+ watchEquations(root);
188
+
189
+ const canHover = window.matchMedia('(hover: hover)');
190
+ const toolbar = build();
191
+
192
+ const numberFrom = (node: EventTarget | null): HTMLAnchorElement | null => {
193
+ if (!(node instanceof Element)) return null;
194
+ const number = node.closest<HTMLAnchorElement>(FLOAT_NUMBER);
195
+ return number && root.contains(number) ? number : null;
196
+ };
197
+ const inToolbar = (node: EventTarget | null) =>
198
+ node instanceof Node && toolbar.root.contains(node);
199
+ const keeps = (node: EventTarget | null) =>
200
+ inToolbar(node) || (node instanceof Node && !!toolbar.number?.contains(node));
201
+
202
+ const cancelHide = () => {
203
+ if (toolbar.hideTimer !== null) window.clearTimeout(toolbar.hideTimer);
204
+ toolbar.hideTimer = null;
205
+ };
206
+ const hide = () => {
207
+ cancelHide();
208
+ toolbar.root.hidden = true;
209
+ };
210
+ const scheduleHide = () => {
211
+ cancelHide();
212
+ toolbar.hideTimer = window.setTimeout(hide, HIDE_DELAY_MS);
213
+ };
214
+
215
+ const show = (number: HTMLAnchorElement) => {
216
+ cancelHide();
217
+ if (toolbar.number !== number || !toolbar.root.isConnected) {
218
+ const name = nameOf(number);
219
+ number.after(toolbar.root);
220
+ toolbar.number = number;
221
+ toolbar.link.href = number.getAttribute('href') ?? '';
222
+ toolbar.link.dataset.copyLink = name;
223
+ toolbar.link.setAttribute('aria-label', `Copy the link to ${name}`);
224
+ toolbar.pin.setAttribute('aria-label', `Pin ${name}`);
225
+ toolbar.dock.setAttribute('aria-label', `Keep ${name} beside the article`);
226
+ }
227
+ toolbar.root.hidden = false;
228
+ place(toolbar, number);
229
+ };
230
+
231
+ root.addEventListener('pointerover', (event) => {
232
+ if (event.pointerType === 'touch' || !canHover.matches) return;
233
+ const number = numberFrom(event.target);
234
+ if (number) show(number);
235
+ else if (inToolbar(event.target)) cancelHide();
236
+ });
237
+ root.addEventListener('pointerout', (event) => {
238
+ if (toolbar.root.hidden || !keeps(event.target)) return;
239
+ if (keeps(event.relatedTarget)) return;
240
+ scheduleHide();
241
+ });
242
+ // Escape gives focus back to the number without showing the toolbar again.
243
+ let quietFocus = false;
244
+ root.addEventListener('focusin', (event) => {
245
+ if (quietFocus) return;
246
+ const number = numberFrom(event.target);
247
+ if (number && canHover.matches && number.matches(':focus-visible')) show(number);
248
+ else if (inToolbar(event.target)) cancelHide();
249
+ });
250
+ root.addEventListener('focusout', (event) => {
251
+ if (toolbar.root.hidden || keeps(event.relatedTarget)) return;
252
+ scheduleHide();
253
+ });
254
+ root.addEventListener('keydown', (event) => {
255
+ if (event.key !== 'Escape' || toolbar.root.hidden) return;
256
+ const inside = inToolbar(document.activeElement);
257
+ hide();
258
+ if (inside && toolbar.number) {
259
+ quietFocus = true;
260
+ toolbar.number.focus();
261
+ quietFocus = false;
262
+ }
263
+ });
264
+
265
+ const keep = (mode: 'pinned' | 'side') => {
266
+ const number = toolbar.number;
267
+ hide();
268
+ if (number) keepCardFor('reference', number, mode);
269
+ };
270
+ toolbar.pin.addEventListener('click', () => keep('pinned'));
271
+ toolbar.dock.addEventListener('click', () => keep('side'));
272
+ // Copy link copies and navigates through `HeadingAnchorEnhancer.astro`.
273
+ toolbar.link.addEventListener('click', () => window.setTimeout(hide, 0));
274
+ }
@@ -358,16 +358,10 @@ const name = `Memo ${number}: ${short ?? caption}`;
358
358
 
359
359
  // The marker opens the memo where it is and goes there, and closes
360
360
  // it again: the section's own summary does the folding, so the
361
- // animation, the peek and the saved choice stay the section's.
362
- marker.addEventListener('click', (event) => {
363
- const details = sectionOf(heading);
364
- const summary = details?.querySelector<HTMLElement>(':scope > summary');
365
- if (!details || !summary) return;
366
- event.preventDefault();
367
- const opening = !isOpen();
368
- summary.click();
369
- if (!opening) return;
370
-
361
+ // animation, the peek and the saved choice stay the section's. An
362
+ // open memo the reader cannot see is gone to, not folded: closing it
363
+ // off-screen would look as if the click did nothing.
364
+ const goTo = () => {
371
365
  closeHoverPeek(marker);
372
366
  if (location.hash !== `#${heading.id}`) {
373
367
  history.pushState(null, '', `#${heading.id}`);
@@ -375,6 +369,23 @@ const name = `Memo ${number}: ${short ?? caption}`;
375
369
  requestAnimationFrame(() => {
376
370
  animatedScrollIntoView(frame, { block: 'start' });
377
371
  });
372
+ };
373
+ const onScreen = () => {
374
+ const box = frame.getBoundingClientRect();
375
+ return box.bottom > 0 && box.top < window.innerHeight;
376
+ };
377
+ marker.addEventListener('click', (event) => {
378
+ const details = sectionOf(heading);
379
+ const summary = details?.querySelector<HTMLElement>(':scope > summary');
380
+ if (!details || !summary) return;
381
+ event.preventDefault();
382
+ const opening = !isOpen();
383
+ if (!opening && !onScreen()) {
384
+ goTo();
385
+ return;
386
+ }
387
+ summary.click();
388
+ if (opening) goTo();
378
389
  });
379
390
  }
380
391
  });
@@ -216,5 +216,9 @@ if (!anchorId) {
216
216
  };
217
217
  window.addEventListener('hashchange', revealForHash);
218
218
  revealForHash();
219
+ // A number alias (`#eq-2.1.1`) is rewritten to its named anchor by
220
+ // `KeyedFloatEnhancer.astro`, whose script runs after this one and
221
+ // fires no event; look again once every script has run.
222
+ document.addEventListener('DOMContentLoaded', revealForHash, { once: true });
219
223
  });
220
224
  </script>
@@ -13,6 +13,8 @@ import {
13
13
  lessonIdFromSlug,
14
14
  } from '../../compiler/lesson-derivation';
15
15
  import { loadManifest } from '../../compiler/manifest';
16
+ import { isIndexEntryLatex } from '../../compiler/content-schemas';
17
+ import { indexEntryFormula } from '../../reference/derived-notation.mjs';
16
18
 
17
19
  const [notationEntries, lessonEntries, manifest] = await Promise.all([
18
20
  getCollection('notation'),
@@ -22,14 +24,20 @@ const [notationEntries, lessonEntries, manifest] = await Promise.all([
22
24
  // Starlight's `link-alt` icon — the same glyph its own heading anchors use
23
25
  // (`rehype-heading-anchors.mjs`). Reimplemented here rather than reused via
24
26
  // `.sl-anchor-link`/`.sl-heading-wrapper` because that CSS assumes a single
25
- // inline prose heading, not this entry's three-column grid header (symbol,
26
- // name, editorial status) — the glyph carries over, the layout doesn't, so
27
- // this file supplies its own reveal styling (below).
27
+ // inline prose heading, not this entry's two-column grid header (symbol,
28
+ // name) — the glyph carries over, the layout doesn't, so this file supplies
29
+ // its own reveal styling (below).
28
30
  const HEADING_LINK_ICON_PATH =
29
31
  'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42';
30
32
 
31
33
  const labelOf = (data: { key: string; label?: string }) =>
32
34
  resolveLabel(data.key, data.label);
35
+ // An index entry's formula names what it indexes as `{base}` (rule N23); on
36
+ // its own card that is a square bound to the entry.
37
+ const formulaOf = (data: { key: string; latex: string; formula?: string }) =>
38
+ data.formula !== undefined && isIndexEntryLatex(data.latex)
39
+ ? indexEntryFormula(data.formula, data.key)
40
+ : data.formula;
33
41
  const notationLabelByKey = new Map(
34
42
  notationEntries.map((entry) => [entry.data.key, labelOf(entry.data)]),
35
43
  );
@@ -72,7 +80,7 @@ const formulaScopeByKey = new Map<string, GlyphScopeEntry[]>(
72
80
  formulaGlyphScope({
73
81
  key: entry.data.key,
74
82
  notation: entry.data.latex,
75
- formula: entry.data.formula,
83
+ formula: formulaOf(entry.data),
76
84
  glosses: glossesByKey.get(entry.data.key) ?? [],
77
85
  cardsByKey,
78
86
  }),
@@ -113,12 +121,12 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
113
121
  notation: entry.data.latex,
114
122
  label: labelOf(entry.data),
115
123
  summary: entry.data.meaning,
116
- formula: entry.data.formula,
124
+ formula: formulaOf(entry.data),
117
125
  formulaScope: formulaScopeByKey.get(entry.data.key),
118
126
  glosses: glossesByKey.get(entry.data.key),
119
127
  units: entry.data.units,
128
+ range: entry.data.range,
120
129
  seeAlso: entry.data.seeAlso,
121
- editorialStatus: entry.data.editorialStatus,
122
130
  scope: 'shared',
123
131
  href: `#notation-${entry.data.key}`,
124
132
  }),
@@ -186,7 +194,6 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
186
194
  </div>
187
195
  <code>{entry.data.key}</code>
188
196
  </div>
189
- <span class="glossary-status">{entry.data.editorialStatus}</span>
190
197
  </header>
191
198
 
192
199
  <dl class="glossary-metadata">
@@ -200,6 +207,12 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
200
207
  <dd>{entry.data.units}</dd>
201
208
  </div>
202
209
  )}
210
+ {entry.data.range && (
211
+ <div>
212
+ <dt>Runs over</dt>
213
+ <dd><NotationMath latex={entry.data.range} /></dd>
214
+ </div>
215
+ )}
203
216
  {entry.data.aliases.length > 0 && (
204
217
  <div>
205
218
  <dt>Aliases</dt>
@@ -225,7 +238,7 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
225
238
  {entry.data.formula && (
226
239
  <div class="glossary-formula">
227
240
  <NotationFormula
228
- formula={entry.data.formula}
241
+ formula={formulaOf(entry.data) ?? ''}
229
242
  scope={formulaScopeByKey.get(entry.data.key) ?? []}
230
243
  glosses={glossesByKey.get(entry.data.key)}
231
244
  entryLabel={labelOf(entry.data)}
@@ -325,7 +338,7 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
325
338
 
326
339
  article > header {
327
340
  display: grid;
328
- grid-template-columns: minmax(4rem, auto) 1fr auto;
341
+ grid-template-columns: minmax(4rem, auto) 1fr;
329
342
  align-items: center;
330
343
  gap: 1rem;
331
344
  }
@@ -425,14 +438,6 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
425
438
  }
426
439
  }
427
440
 
428
- .glossary-status {
429
- padding: 0.15rem 0.5rem;
430
- border: 1px solid var(--sl-color-gray-5);
431
- border-radius: 999px;
432
- color: var(--sl-color-gray-3);
433
- font-size: var(--sl-text-xs);
434
- }
435
-
436
441
  .glossary-metadata {
437
442
  display: grid;
438
443
  gap: 0.45rem;