explico 0.7.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -2,23 +2,44 @@
2
2
  /**
3
3
  * A collapsible side-note authored inline and moved between the surrounding
4
4
  * paragraphs by `remark-memo.mjs`. The remark adapter owns `number` and
5
- * `anchorId`; this component owns the block, persistence, hover preview, and
6
- * click/deep-link behavior.
5
+ * `anchorId`; this component owns the block, its marker's hover preview, and
6
+ * the marker's click behavior.
7
7
  *
8
- * The numbered permalink and chain-icon permalink are siblings of `<details>`,
9
- * positioned into its title row. They cannot be children of `<summary>`:
10
- * nesting a focusable link inside that disclosure control is invalid and trips
11
- * axe's `nested-interactive` rule.
8
+ * Named like every float (docs/plans/titles-and-containers.md Part 3): the
9
+ * required `caption` is the memo's bar and heads its preview; the optional
10
+ * `short` caption is what its marker's tooltip calls it. The marker is the
11
+ * memo's per-page letter (`a`, `b`, …), set as a superscript in the prose so
12
+ * it never reads as a citation's `[1]` (Q7); the bar shows the same letter.
13
+ *
14
+ * **A memo is a `######`-rank section** (Part 4): its bar is an `<h6>` in the
15
+ * same `.sl-heading-wrapper` a Markdown heading gets, marked collapsible, so
16
+ * `CollapsibleSections.astro` folds it exactly like a section — the gutter
17
+ * chevron, the hover peek, the "Hidden here: …" line, the animation, the
18
+ * chain icon that copies its link, persistence and print-as-shown. Its
19
+ * first-visit state is the course's `disclosure.memos` (closed by default),
20
+ * independent of `subsections`; `defaultOpen` overrides it at one memo. The
21
+ * `<h6>` is drawn by this component, not written in the lesson, so it is not
22
+ * in the outline, "On this page", or the heading rules — nor in the
23
+ * accessibility tree's headings (`role="none"`: a memo after a `##` is not a
24
+ * sixth-level heading of the page, and axe's heading order says so; the
25
+ * summary it sits in still reads its caption). The frame is marked
26
+ * `data-section-contained`, so neither its own heading nor any in its body
27
+ * ends a section around it (`section-parts.ts`), and its wrapper
28
+ * `data-section-to-end`, so its section is the whole body.
12
29
  */
30
+ import { courseDisclosure } from '../disclosure/course-disclosure';
31
+
13
32
  export interface Props {
14
33
  /** Stable lowercase/hyphenated key used by the marker and persistence. */
15
34
  id: string;
16
- /** Short summary shown on the disclosure itself. */
17
- title: string;
18
- /** Required context shown before the body in the marker's hover preview. */
19
- description: string;
20
- /** Optional prose label for the marker; omission gives the numbered `[n]` form. */
35
+ /** The memo's caption: its bar, and the head of its marker's hover preview. */
36
+ caption: string;
37
+ /** Optional short caption: the marker's tooltip; rule S1 measures it. */
38
+ short?: string;
39
+ /** Optional prose label for the marker; omission gives the lettered form. */
21
40
  label?: string;
41
+ /** State for a reader with nothing saved yet. Default: the course's `disclosure.memos`. */
42
+ defaultOpen?: boolean;
22
43
  /** Persist the reader's open/closed choice across visits, per page. Default: `true`. */
23
44
  rememberState?: boolean;
24
45
  /** Computed by `remark-memo.mjs` — never author this directly. */
@@ -29,8 +50,9 @@ export interface Props {
29
50
 
30
51
  const {
31
52
  id,
32
- title,
33
- description,
53
+ caption,
54
+ short,
55
+ defaultOpen,
34
56
  rememberState = true,
35
57
  number,
36
58
  anchorId,
@@ -39,149 +61,107 @@ const {
39
61
  if (typeof id !== 'string' || !id.trim()) {
40
62
  throw new Error('Memo requires a non-empty `id` prop.');
41
63
  }
42
- if (typeof title !== 'string' || !title.trim()) {
43
- throw new Error('Memo requires a non-empty `title` prop.');
64
+ for (const removed of ['title', 'description'] as const) {
65
+ if (removed in Astro.props) {
66
+ throw new Error(
67
+ `<Memo id="${id}"> has no \`${removed}\` prop: a memo is named by its \`caption\` (its bar and preview) and an optional \`short\` caption.`,
68
+ );
69
+ }
44
70
  }
45
- if (typeof description !== 'string' || !description.trim()) {
46
- throw new Error('Memo requires a non-empty `description` prop.');
71
+ if (typeof caption !== 'string' || !caption.trim()) {
72
+ throw new Error('Memo requires a non-empty `caption` prop.');
47
73
  }
48
74
  if (!number || !anchorId) {
49
75
  throw new Error(
50
76
  'Memo is missing its computed `number`/`anchorId` — register remark-memo after remark-mdx in the Markdown remark pipeline.',
51
77
  );
52
78
  }
79
+
80
+ const open = defaultOpen ?? (await courseDisclosure()).memos.defaultOpen;
81
+ const name = `Memo ${number}: ${short ?? caption}`;
53
82
  ---
54
83
 
55
- <div class="memo-block-frame">
56
- <a
57
- class="memo-block__anchor"
58
- href={`#${anchorId}`}
59
- aria-label={`Link to memo ${number}: ${title}`}
60
- data-memo-self-link
61
- >[{number}]</a>
62
- <details
63
- class="memo-block"
64
- id={anchorId}
65
- data-memo-key={id}
66
- data-memo-number={number}
67
- data-memo-description={description}
84
+ <div
85
+ class="memo-block memo-block-frame ex-tier-aside ex-tier-aside--tinted"
86
+ data-memo-key={id}
87
+ data-memo-number={number}
88
+ data-memo-short={short}
89
+ data-section-contained
90
+ >
91
+ <div
92
+ class="sl-heading-wrapper level-h6 memo-block__titlebar"
93
+ data-collapsible="true"
94
+ data-default-open={String(open)}
68
95
  data-remember-state={String(rememberState)}
96
+ data-section-to-end
69
97
  >
70
- <summary class="memo-block__summary">{title}</summary>
71
- <div class="memo-block__body">
72
- <slot />
73
- </div>
74
- </details>
75
- <a
76
- class="memo-block__title-anchor"
77
- href={`#${anchorId}`}
78
- aria-label={`Link to memo ${number} titled “${title}”`}
79
- data-memo-anchor-link
80
- >
81
- <svg
82
- aria-hidden="true"
83
- width="16"
84
- height="16"
85
- viewBox="0 0 24 24"
86
- fill="currentColor"
87
- ><path
88
- 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"
89
- ></path></svg
98
+ <h6 id={anchorId} class="memo-block__heading" data-memo-number={number} role="none"
99
+ >{caption}</h6
100
+ >
101
+ <a class="sl-anchor-link" href={`#${anchorId}`}
102
+ ><span aria-hidden="true" class="sl-anchor-icon"
103
+ ><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
104
+ ><path
105
+ 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"
106
+ ></path></svg
107
+ ></span
108
+ ><span class="sr-only" data-pagefind-ignore>{name}</span></a
90
109
  >
91
- </a>
110
+ </div>
111
+ <div class="memo-block__body">
112
+ <slot />
113
+ </div>
92
114
  </div>
93
115
 
94
116
  <style>
117
+ /* The aside tier, tinted (`ex-tier-aside ex-tier-aside--tinted`,
118
+ `surfaces.css`): a left rule and the accent tint, no box around it. Its
119
+ inline-start padding is the gutter the section chevron centres in, as
120
+ a `[card]` section's padding is. The folding itself — chevron, peek,
121
+ "Hidden here" line, animation — is the section wrapper's
122
+ (`CollapsibleSections.astro`, `global.css`). */
95
123
  .memo-block-frame {
96
- position: relative;
124
+ --ex-section-gutter: 1.75rem;
97
125
  margin-block: 1rem;
98
- border: 1px solid var(--sl-color-gray-5);
99
- border-inline-start: 4px solid var(--sl-color-accent);
100
- border-radius: 0.35rem;
101
- background: color-mix(in srgb, var(--sl-color-bg) 95%, var(--sl-color-accent) 5%);
126
+ padding: 0.45rem 1rem 0.45rem var(--ex-section-gutter);
102
127
  }
103
128
 
104
- .memo-block {
105
- margin: 0;
106
- padding: 0;
107
- scroll-margin-block-start: 5rem;
129
+ .memo-block-frame > :global(*) {
130
+ margin-block: 0;
108
131
  }
109
132
 
110
- .memo-block__summary {
111
- padding: 0.7rem 2.75rem 0.7rem 3.25rem;
112
- color: var(--sl-color-white);
113
- font-weight: 600;
133
+ .memo-block__heading {
134
+ scroll-margin-block-start: 5rem;
114
135
  }
115
136
 
116
- .memo-block__anchor {
117
- position: absolute;
118
- z-index: 1;
119
- inset-block-start: 0.7rem;
120
- inset-inline-start: 0.9rem;
137
+ /* The memo's letter before its caption: the same italic accent letter the
138
+ marker in the prose shows. Generated content, so the heading's text —
139
+ its accessible name and what a preview copies — stays the caption. */
140
+ .memo-block__heading::before {
141
+ content: attr(data-memo-number);
142
+ margin-inline-end: 0.45em;
121
143
  color: var(--sl-color-text-accent);
144
+ font-style: italic;
122
145
  font-weight: 600;
123
- text-decoration: none;
124
- }
125
-
126
- .memo-block__anchor:hover,
127
- .memo-block__anchor:focus-visible {
128
- text-decoration: underline;
129
- text-underline-offset: 0.15em;
130
- }
131
-
132
- .memo-block__title-anchor {
133
- position: absolute;
134
- z-index: 1;
135
- inset-block-start: 0.55rem;
136
- inset-inline-end: 0.65rem;
137
- display: inline-flex;
138
- align-items: center;
139
- justify-content: center;
140
- width: 1.75rem;
141
- height: 1.75rem;
142
- color: var(--sl-color-gray-3);
143
- text-decoration: none;
144
- -webkit-user-select: none;
145
- user-select: none;
146
- }
147
-
148
- .memo-block__title-anchor svg {
149
- width: 1rem;
150
- height: 1rem;
151
- }
152
-
153
- .memo-block__title-anchor:hover,
154
- .memo-block__title-anchor:focus-visible {
155
- color: var(--sl-color-text-accent);
156
- }
157
-
158
- .memo-block__title-anchor:focus-visible {
159
- outline: 2px solid var(--sl-color-accent);
160
- outline-offset: 0.1rem;
161
- }
162
-
163
- @media (hover: hover) {
164
- .memo-block__title-anchor {
165
- opacity: 0;
166
- }
167
-
168
- .memo-block__title-anchor:focus-visible,
169
- .memo-block-frame:hover .memo-block__title-anchor {
170
- opacity: 1;
171
- }
146
+ font-size: 0.85em;
147
+ vertical-align: 0.3em;
172
148
  }
173
149
 
174
150
  .memo-block__body {
175
- padding: 0 0.9rem 0.9rem;
151
+ padding-block-start: 0.5rem;
176
152
  }
177
153
 
178
- .memo-block__body > :first-child {
154
+ .memo-block__body > :global(:first-child) {
179
155
  margin-block-start: 0;
180
156
  }
181
157
 
158
+ /* A superscript italic letter, the footnote convention — never the
159
+ bracketed `[1]` a citation prints. */
182
160
  :global(.sl-markdown-content .memo-marker) {
161
+ padding-inline: 0.1em;
183
162
  border-radius: 0.2em;
184
163
  color: var(--sl-color-text-accent);
164
+ font-style: italic;
185
165
  font-weight: 600;
186
166
  text-decoration: underline;
187
167
  text-underline-offset: 0.15em;
@@ -202,7 +182,7 @@ if (!number || !anchorId) {
202
182
  vertical-align: super;
203
183
  }
204
184
 
205
- :global(.memo-preview__description) {
185
+ :global(.memo-preview__caption) {
206
186
  margin: 0 0 0.65rem;
207
187
  color: var(--sl-color-gray-2);
208
188
  }
@@ -216,29 +196,13 @@ if (!number || !anchorId) {
216
196
 
217
197
  @media print {
218
198
  .memo-block-frame {
219
- border-color: currentColor;
220
199
  background: transparent;
221
200
  }
222
-
223
- .memo-block__summary::before {
224
- display: none !important;
225
- }
226
-
227
- .memo-block__title-anchor {
228
- display: none;
229
- }
230
-
231
- /* The script opens the native disclosure while print media is active.
232
- Keep the body explicit in case author CSS would otherwise hide it. */
233
- .memo-block__body {
234
- display: block !important;
235
- }
236
201
  }
237
202
  </style>
238
203
 
239
204
  <script>
240
205
  import { attachHoverPeek, closeHoverPeek } from '../disclosure/hover-preview';
241
- import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
242
206
  import { animatedScrollIntoView } from '../scroll-animation';
243
207
 
244
208
  let memoPreviewSerial = 0;
@@ -317,132 +281,99 @@ if (!number || !anchorId) {
317
281
  return clone;
318
282
  }
319
283
 
320
- function targetInside(details: HTMLDetailsElement): boolean {
321
- if (!location.hash) return false;
322
- let id = location.hash.slice(1);
323
- try {
324
- id = decodeURIComponent(id);
325
- } catch {
326
- /* keep the raw fragment */
327
- }
328
- const target = document.getElementById(id);
329
- return target === details || (!!target && details.contains(target));
330
- }
331
284
 
332
- document
333
- .querySelectorAll<HTMLDetailsElement>('.memo-block:not([data-memo-wired])')
334
- .forEach((details) => {
335
- details.dataset.memoWired = 'true';
336
- const key = details.dataset.memoKey;
337
- const number = details.dataset.memoNumber;
338
- const body = details.querySelector<HTMLElement>(':scope > .memo-block__body');
339
- const summary = details.querySelector<HTMLElement>(
340
- ':scope > .memo-block__summary',
341
- );
342
- const selfLinks = Array.from(
343
- details.parentElement?.querySelectorAll<HTMLAnchorElement>(
344
- ':scope > [data-memo-self-link], :scope > [data-memo-anchor-link]',
345
- ) ?? [],
346
- );
347
- if (!key || !number || !body || !summary || selfLinks.length === 0) return;
285
+ // The section wrapper (`CollapsibleSections.astro`) builds the memo's
286
+ // `<details>` at runtime, possibly after this script: find it lazily.
287
+ const sectionOf = (heading: HTMLElement) =>
288
+ heading.closest<HTMLDetailsElement>('details.sl-section-collapsible');
289
+ const whenSection = (
290
+ frame: HTMLElement,
291
+ heading: HTMLElement,
292
+ ready: (details: HTMLDetailsElement) => void,
293
+ ) => {
294
+ const found = sectionOf(heading);
295
+ if (found) return ready(found);
296
+ const observer = new MutationObserver(() => {
297
+ const details = sectionOf(heading);
298
+ if (!details) return;
299
+ observer.disconnect();
300
+ ready(details);
301
+ });
302
+ observer.observe(frame, { childList: true, subtree: true });
303
+ };
348
304
 
349
- if (details.dataset.rememberState === 'true') {
350
- wirePersistedDetails(details, `memo:${key}`, false);
351
- } else {
352
- details.open = false;
353
- }
305
+ document
306
+ .querySelectorAll<HTMLElement>('.memo-block[data-memo-key]:not([data-memo-wired])')
307
+ .forEach((frame) => {
308
+ frame.dataset.memoWired = 'true';
309
+ const key = frame.dataset.memoKey;
310
+ const number = frame.dataset.memoNumber;
311
+ const heading = frame.querySelector<HTMLElement>('.memo-block__heading');
312
+ const body = frame.querySelector<HTMLElement>('.memo-block__body');
313
+ if (!key || !number || !heading || !body) return;
354
314
 
355
315
  const markers = Array.from(
356
316
  document.querySelectorAll<HTMLAnchorElement>(
357
317
  `.memo-marker[data-memo-key="${key}"]`,
358
318
  ),
359
- ).filter(
360
- (marker) => marker.getAttribute('aria-controls') === details.id,
361
- );
362
-
363
- const syncMarkers = () => {
364
- for (const marker of markers) {
365
- marker.setAttribute('aria-expanded', String(details.open));
366
- if (details.open) closeHoverPeek(marker);
367
- }
368
- };
369
- details.addEventListener('toggle', syncMarkers);
370
- syncMarkers();
371
-
372
- const revealForHash = () => {
373
- if (targetInside(details)) details.open = true;
374
- };
375
- revealForHash();
376
- window.addEventListener('hashchange', revealForHash);
377
-
378
- for (const selfLink of selfLinks) {
379
- selfLink.addEventListener('click', (event) => {
380
- event.preventDefault();
381
- event.stopPropagation();
382
- details.open = true;
383
- if (location.hash !== `#${details.id}`) {
384
- history.pushState(null, '', `#${details.id}`);
319
+ ).filter((marker) => marker.getAttribute('aria-controls') === heading.id);
320
+ const isOpen = () => sectionOf(heading)?.dataset.state === 'open';
321
+
322
+ whenSection(frame, heading, (details) => {
323
+ const syncMarkers = () => {
324
+ for (const marker of markers) {
325
+ marker.setAttribute('aria-expanded', String(isOpen()));
326
+ if (isOpen()) closeHoverPeek(marker);
385
327
  }
328
+ };
329
+ new MutationObserver(syncMarkers).observe(details, {
330
+ attributes: true,
331
+ attributeFilter: ['data-state'],
386
332
  });
387
- }
388
-
389
- // A closed native <details> suppresses its body at the UA rendering
390
- // layer, beyond what an author stylesheet can reliably override. Open it
391
- // while print media is active, then put the reader's exact screen state
392
- // back when the print dialog/export ends (the same approach used by
393
- // CollapsibleSections for its native disclosures).
394
- const printMedia = window.matchMedia('print');
395
- let screenOpen: boolean | null = null;
396
- const syncPrintState = () => {
397
- if (document.querySelector('[data-explico-print-controller]')) return;
398
- if (printMedia.matches) {
399
- if (screenOpen === null) screenOpen = details.open;
400
- details.open = true;
401
- } else if (screenOpen !== null) {
402
- details.open = screenOpen;
403
- screenOpen = null;
404
- }
405
- };
406
- printMedia.addEventListener('change', syncPrintState);
407
- window.addEventListener('beforeprint', syncPrintState);
408
- window.addEventListener('afterprint', syncPrintState);
409
- syncPrintState();
333
+ syncMarkers();
334
+ });
410
335
 
411
336
  for (const marker of markers) {
412
337
  const suppressWhenOpen = (event: Event) => {
413
- if (details.open) event.stopImmediatePropagation();
338
+ if (isOpen()) event.stopImmediatePropagation();
414
339
  };
415
340
  marker.addEventListener('pointerenter', suppressWhenOpen);
416
341
  marker.addEventListener('focus', suppressWhenOpen);
417
342
 
418
343
  attachHoverPeek(marker, {
419
344
  label: `Memo ${number} preview`,
420
- // Keep the description visible above up to 20em of body content.
345
+ // Keep the caption visible above up to 20em of body content.
421
346
  maxHeightEm: 24,
422
347
  getContent: () => {
423
348
  const wrap = document.createElement('div');
424
- const heading = document.createElement('strong');
425
- heading.textContent = summary.textContent?.trim() || `Memo ${number}`;
426
- const description = document.createElement('p');
427
- description.className = 'memo-preview__description';
428
- description.textContent = details.dataset.memoDescription ?? '';
429
- wrap.append(heading, description, cloneMemoBody(body));
349
+ const caption = document.createElement('p');
350
+ caption.className = 'memo-preview__caption';
351
+ const letter = document.createElement('strong');
352
+ letter.textContent = `Memo ${number}`;
353
+ caption.append(letter, ` ${heading.textContent?.trim() ?? ''}`);
354
+ wrap.append(caption, cloneMemoBody(body));
430
355
  return wrap;
431
356
  },
432
357
  });
433
358
 
359
+ // The marker opens the memo where it is and goes there, and closes
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.
434
362
  marker.addEventListener('click', (event) => {
363
+ const details = sectionOf(heading);
364
+ const summary = details?.querySelector<HTMLElement>(':scope > summary');
365
+ if (!details || !summary) return;
435
366
  event.preventDefault();
436
- const opening = !details.open;
437
- details.open = opening;
367
+ const opening = !isOpen();
368
+ summary.click();
438
369
  if (!opening) return;
439
370
 
440
371
  closeHoverPeek(marker);
441
- if (location.hash !== `#${details.id}`) {
442
- history.pushState(null, '', `#${details.id}`);
372
+ if (location.hash !== `#${heading.id}`) {
373
+ history.pushState(null, '', `#${heading.id}`);
443
374
  }
444
375
  requestAnimationFrame(() => {
445
- animatedScrollIntoView(details, { block: 'start' });
376
+ animatedScrollIntoView(frame, { block: 'start' });
446
377
  });
447
378
  });
448
379
  }
@@ -30,7 +30,7 @@ if (!anchorId) {
30
30
  ---
31
31
 
32
32
  <aside
33
- class="short-note"
33
+ class="short-note ex-tier-aside"
34
34
  id={anchorId}
35
35
  data-short-note-key={id}
36
36
  aria-label={description}
@@ -43,11 +43,12 @@ if (!anchorId) {
43
43
  </aside>
44
44
 
45
45
  <style>
46
+ /* Opened, the note is the aside tier untinted (`ex-tier-aside`,
47
+ `surfaces.css`): the shared left rule and nothing else. */
46
48
  .short-note {
47
49
  margin-block: 0.65rem;
48
50
  padding-inline-start: 0.9rem;
49
51
  scroll-margin-block-start: 5rem;
50
- border-inline-start: 2px solid var(--sl-color-accent);
51
52
  }
52
53
 
53
54
  .short-note[hidden] {
@@ -105,10 +106,9 @@ if (!anchorId) {
105
106
  }
106
107
 
107
108
  @media print {
108
- .short-note[hidden] {
109
- display: block !important;
110
- }
111
-
109
+ /* No `.short-note[hidden]` override: print follows the screen
110
+ (`../disclosure/print-as-shown.ts`), so a note the reader left closed
111
+ stays closed and gets a "(content collapsed …)" note instead. */
112
112
  .short-note__collapse {
113
113
  display: none;
114
114
  }
@@ -56,6 +56,9 @@ registerPageNotationDependencies(
56
56
  );
57
57
  const latex = stripNotationMarkers(formula);
58
58
  const numbered = Boolean(number && numberHref);
59
+ // The copy tooltip names what it copies, the way a lesson equation's does
60
+ // with its `\caption{}`: here the owning entry's label.
61
+ const copyTitle = entryLabel ? `Copy LaTeX — ${entryLabel}` : 'Copy LaTeX';
59
62
  ---
60
63
 
61
64
  <div class="notation-formula-block">
@@ -63,6 +66,7 @@ const numbered = Boolean(number && numberHref);
63
66
  class:list={['notation-formula-math', numbered && 'notation-formula-math--numbered']}
64
67
  data-notation-formula
65
68
  data-latex={latex}
69
+ data-copy-title={copyTitle}
66
70
  role="group"
67
71
  aria-label={entryLabel ? `${entryLabel} formula` : 'Formula'}
68
72
  >
@@ -87,7 +91,7 @@ const numbered = Boolean(number && numberHref);
87
91
  data-notation-formula-copy
88
92
  data-latex={latex}
89
93
  aria-label={`Copy LaTeX for Equation ${number}`}
90
- title="Copy LaTeX"
94
+ title={copyTitle}
91
95
  >
92
96
  <svg viewBox="0 0 24 24" aria-hidden="true">
93
97
  <rect x="9" y="9" width="10" height="10" rx="1"></rect>
@@ -150,7 +150,7 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
150
150
  (left, right) => left.data.title.localeCompare(right.data.title),
151
151
  );
152
152
  return (
153
- <article id={`notation-${entry.data.key}`}>
153
+ <article id={`notation-${entry.data.key}`} class="ex-tier-meta">
154
154
  <header>
155
155
  <span
156
156
  class="glossary-symbol-trigger"
@@ -159,13 +159,19 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
159
159
  <NotationMath class="glossary-symbol" latex={entry.data.latex} />
160
160
  </span>
161
161
  <div>
162
- <h2>
163
- <a
164
- class="glossary-term-link"
165
- href={`#notation-${entry.data.key}`}
166
- >
167
- {labelOf(entry.data)}
168
- </a>
162
+ {/* The anchor is a sibling of the heading, not a child — the
163
+ same shape `rehype-heading-anchors` gives a lesson heading.
164
+ Inside the `h2` its `aria-label` joins the heading's
165
+ accessible name ("Velocity Link to “Velocity”"). */}
166
+ <div class="glossary-heading">
167
+ <h2>
168
+ <a
169
+ class="glossary-term-link"
170
+ href={`#notation-${entry.data.key}`}
171
+ >
172
+ {labelOf(entry.data)}
173
+ </a>
174
+ </h2>
169
175
  <a
170
176
  class="glossary-anchor"
171
177
  href={`#notation-${entry.data.key}`}
@@ -177,7 +183,7 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
177
183
  </svg>
178
184
  </span>
179
185
  </a>
180
- </h2>
186
+ </div>
181
187
  <code>{entry.data.key}</code>
182
188
  </div>
183
189
  <span class="glossary-status">{entry.data.editorialStatus}</span>
@@ -310,12 +316,11 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
310
316
  margin-block-start: 2rem;
311
317
  }
312
318
 
319
+ /* A generated reference entry: the meta tier (`ex-tier-meta`,
320
+ `surfaces.css`) draws its surface, border and radius. */
313
321
  article {
314
322
  scroll-margin-block-start: 5rem;
315
323
  padding: 1.25rem;
316
- border: 1px solid var(--sl-color-gray-5);
317
- border-radius: 0.7rem;
318
- background: var(--sl-color-gray-7);
319
324
  }
320
325
 
321
326
  article > header {
@@ -330,6 +335,11 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
330
335
  font-size: var(--sl-text-xl);
331
336
  }
332
337
 
338
+ .glossary-heading {
339
+ display: flex;
340
+ align-items: center;
341
+ }
342
+
333
343
  .glossary-term-link {
334
344
  color: inherit;
335
345
  text-decoration: none;
@@ -396,8 +406,8 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
396
406
  opacity: 0;
397
407
  }
398
408
 
399
- h2:hover .glossary-anchor,
400
- h2:focus-within .glossary-anchor,
409
+ .glossary-heading:hover .glossary-anchor,
410
+ .glossary-heading:focus-within .glossary-anchor,
401
411
  .glossary-anchor:focus-visible {
402
412
  opacity: 1;
403
413
  }