explico 0.8.0 → 0.11.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 (230) hide show
  1. package/CHANGELOG.md +1257 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  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 +4 -1
  15. package/dist/compiler/logging.js +4 -1
  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 +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +5 -3
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1152 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1467 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -42,16 +42,25 @@ export interface Props {
42
42
  * links to reveal an explanation on hover.
43
43
  */
44
44
  presentation?: 'full' | 'panel';
45
- /** Author-chosen state for a reader with nothing saved yet. Default: `false` (closed) — see `docs/authoring` on `CourseConfig.disclosureDefaults`. */
45
+ /** Author-chosen state for a reader with nothing saved yet. Default: `false` (closed) — from the course's `disclosure.notation` (`CourseConfig.disclosure`). */
46
46
  defaultOpen?: boolean;
47
47
  /** Persist the reader's open/closed choice across visits, per page. Default: `true`. */
48
48
  rememberState?: boolean;
49
+ /**
50
+ * Decorations this page says are not operators (`notation.suspend`, rule
51
+ * N22), shown at the top of the list so a reader who knows `'` as the
52
+ * derivative is told it means something else here.
53
+ */
54
+ suspended?: readonly NotationSuspension[];
49
55
  }
50
56
 
57
+ import HoverPanel from '../HoverPanel.astro';
51
58
  import NotationMath from './NotationMath.astro';
52
59
  import NotationFormula from './NotationFormula.astro';
53
60
  import { stripNotationMarkers } from './render-notation-math';
61
+ import { operatorDisplayLatex } from '../../reference/math-glyphs.mjs';
54
62
  import type { GlyphScopeEntry, ResolvedGloss } from '../../reference/gloss';
63
+ import type { NotationSuspension } from '../../reference/types';
55
64
 
56
65
  const {
57
66
  definitions,
@@ -61,6 +70,7 @@ const {
61
70
  presentation = 'full',
62
71
  defaultOpen = false,
63
72
  rememberState = true,
73
+ suspended = [],
64
74
  } = Astro.props;
65
75
 
66
76
  const panelOnly = presentation === 'panel';
@@ -121,7 +131,7 @@ for (const gloss of glossItems) {
121
131
  >
122
132
  <div class="notation-list-titlebar">
123
133
  <details
124
- class="notation-list"
134
+ class="notation-list ex-tier-meta"
125
135
  id={id}
126
136
  hidden={panelOnly}
127
137
  data-remember-state={String(rememberState)}
@@ -130,6 +140,28 @@ for (const gloss of glossItems) {
130
140
  <summary>
131
141
  {label} <span class="notation-count">({items.length})</span>
132
142
  </summary>
143
+ {/* How the cards work, inside the panel: shown once the panel is
144
+ opened, not as three lines under every closed one. */}
145
+ {!panelOnly && (
146
+ <p class="notation-enhancement-help" data-notation-help hidden>
147
+ Point to a marked symbol or term, here or in the text, for a quick
148
+ explanation; a symbol inside an open card explains itself in a
149
+ card of its own. Click a symbol or term to keep its card open, and
150
+ click it again, use the card’s “×” or press Escape to close the
151
+ card.
152
+ </p>
153
+ )}
154
+ {!panelOnly && suspended.length > 0 && (
155
+ <ul class="notation-suspended" data-notation-suspended>
156
+ {suspended.map((item) => (
157
+ <li>
158
+ On this page,{' '}
159
+ <NotationMath latex={operatorDisplayLatex(item.operator)} /> is
160
+ not an operator.{item.reason ? ` ${item.reason}` : ''}
161
+ </li>
162
+ ))}
163
+ </ul>
164
+ )}
133
165
  <dl>
134
166
  {items.map((definition) => (
135
167
  <div
@@ -232,54 +264,44 @@ for (const gloss of glossItems) {
232
264
  )}
233
265
  </div>
234
266
 
235
- {!panelOnly && (
236
- <p class="notation-enhancement-help" data-notation-help hidden>
237
- Point to a marked symbol for a quick explanation, or use the list
238
- above — pointing at a symbol inside an open card explains that one
239
- too, alongside it. Click a symbol to keep its card open; click the
240
- card (or its “×”) to close it, or press Escape.
241
- </p>
242
- )}
243
-
244
- <template data-notation-popup-template>
245
- <aside
246
- class="notation-popup"
247
- role="region"
248
- aria-atomic="true"
249
- tabindex="-1"
250
- >
251
- <button
252
- class="notation-popup-close"
253
- type="button"
254
- data-popup-close
255
- aria-label="Close"
267
+ <HoverPanel
268
+ id={`${id}-card`}
269
+ label="Notation"
270
+ class="notation-popup"
271
+ data-notation-popup
272
+ >
273
+ <div class="notation-panel-heading" slot="heading">
274
+ <span class="notation-panel-symbol" data-slot-symbol></span>
275
+ <h3 data-slot-title></h3>
276
+ </div>
277
+ <div class="notation-panel-body">
278
+ <p data-slot-summary hidden></p>
279
+ <div data-slot-formula hidden></div>
280
+ <p class="notation-units" data-slot-units hidden></p>
281
+ <a data-slot-link hidden
282
+ >Go to the full definition<span aria-hidden="true" data-jump-arrow> →</span></a
256
283
  >
257
- <span aria-hidden="true">&times;</span>
258
- </button>
259
- <div class="notation-panel-body">
260
- <header class="notation-panel-heading">
261
- <span class="notation-panel-symbol" data-slot-symbol />
262
- <h3 data-slot-title />
263
- </header>
264
- <p data-slot-summary hidden />
265
- <div data-slot-formula hidden />
266
- <p class="notation-units" data-slot-units hidden />
267
- <a data-slot-link hidden>
268
- Open the full definition
269
- </a>
270
- </div>
271
- </aside>
272
- </template>
284
+ </div>
285
+ </HoverPanel>
273
286
  </section>
274
287
  )
275
288
  }
276
289
 
277
290
  <script>
278
- import { createPopupStack, closestElement } from '../popup-stack';
291
+ import {
292
+ cloneCardShell,
293
+ isKept,
294
+ registerCardSource,
295
+ } from '../card-manager';
296
+ import { wireJumpArrow } from '../card-jump';
279
297
  import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
280
298
 
281
299
  const triggerSelector = '[data-notation-key]';
282
300
 
301
+ function closestElement(node: EventTarget | null, selector: string) {
302
+ return node instanceof Element ? node.closest<HTMLElement>(selector) : null;
303
+ }
304
+
283
305
  function setOptionalText(element: HTMLElement, value: string) {
284
306
  element.textContent = value;
285
307
  element.hidden = value.length === 0;
@@ -299,18 +321,19 @@ for (const gloss of glossItems) {
299
321
  if (entries.size === 0) return;
300
322
 
301
323
  const template = root.querySelector<HTMLTemplateElement>(
302
- '[data-notation-popup-template]',
324
+ 'template[data-card-template]',
303
325
  );
304
326
  if (!template) return;
305
327
 
306
328
  // Static triggers, computed once: markers embedded in prose or an
307
329
  // equation, and the symbol glyphs in the "Notation used on this page"
308
330
  // list. These get the always-on `.notation-trigger` cursor/hover
309
- // treatment. Symbols that appear *inside a popup* (a card's own formula,
310
- // D15) are not in this list — there's no bound on how many of those can
311
- // exist — but they still work as triggers: `triggerFrom` below finds
312
- // them live, and the shared `[data-notation-key]:hover` rule (declared
313
- // `:global`, further down) already paints them the same way.
331
+ // treatment. Symbols that appear *inside a card* (a card's own formula,
332
+ // D15, or an equation a reference card previews) are not in this list —
333
+ // there's no bound on how many of those can exist — but they still work
334
+ // as triggers: `symbolFrom` below finds them live, and the shared
335
+ // `[data-notation-key]:hover` rule (declared `:global`, further down)
336
+ // already paints them the same way.
314
337
  const externalTriggers = Array.from(
315
338
  document.querySelectorAll<HTMLElement>(triggerSelector),
316
339
  ).filter(
@@ -327,14 +350,19 @@ for (const gloss of glossItems) {
327
350
  for (const trigger of highlightTargets) {
328
351
  trigger.classList.add('notation-trigger');
329
352
  }
353
+ // C13: a `[[key]]` term's `title` is its no-JavaScript explanation; with
354
+ // the card here it would only be a second tooltip on top of it.
355
+ for (const term of externalTriggers) {
356
+ if (term.matches('a.notation-term')) term.removeAttribute('title');
357
+ }
330
358
  const help = root.querySelector<HTMLElement>('[data-notation-help]');
331
359
  if (help) help.hidden = false;
332
360
 
333
361
  // The visible list's own symbol glyphs are the only keyboard path to
334
- // pinning a card from this list (the old "Pin explanation" button was
335
- // the other one, and is gone) — make them a real, labelled, Enter/Space-
336
- // operable control rather than a mouse-only `<span>`. `symbolTriggers`
337
- // (defined above) is already scoped to entries inside this list.
362
+ // pinning a card from this list — make them a real, labelled,
363
+ // Enter/Space-operable control rather than a mouse-only `<span>`.
364
+ // `symbolTriggers` (defined above) is already scoped to entries inside
365
+ // this list.
338
366
  for (const trigger of symbolTriggers) {
339
367
  const key = trigger.dataset.notationKey ?? '';
340
368
  const entryLabel =
@@ -351,18 +379,9 @@ for (const gloss of glossItems) {
351
379
  });
352
380
  }
353
381
 
354
- const keyFor = (element: HTMLElement | null) =>
355
- element?.dataset.notationKey ?? element?.dataset.notationLink ?? null;
356
-
357
- const staticLinkFrom = (node: EventTarget | null) => {
358
- const candidate = closestElement(node, '[data-notation-link]');
359
- return candidate && root.contains(candidate) ? candidate : null;
360
- };
361
-
362
382
  // A symbol trigger, wherever it lives: prose, the equation, the visible
363
- // list, or — the D15 case this exists for — inside an already-open
364
- // popup's own formula. Unlike the static sets above, this has to be
365
- // live: a popup's content doesn't exist until the reader opens it.
383
+ // list, or — the D15 case — inside an open card. This has to be live: a
384
+ // card's content doesn't exist until the reader opens it.
366
385
  const symbolFrom = (node: EventTarget | null) => {
367
386
  const candidate = closestElement(node, triggerSelector);
368
387
  if (!candidate || candidate.closest('.katex-mathml')) return null;
@@ -371,36 +390,27 @@ for (const gloss of glossItems) {
371
390
  : null;
372
391
  };
373
392
 
374
- const triggerFrom = (node: EventTarget | null) =>
375
- staticLinkFrom(node) ?? symbolFrom(node);
376
-
377
393
  const labelOf = (entry: HTMLElement, key: string) =>
378
394
  entry
379
395
  .querySelector<HTMLElement>('[data-notation-link]')
380
396
  ?.textContent?.trim() ?? key;
381
397
 
382
- const createPopup = (key: string, _trigger: HTMLElement) => {
398
+ const buildCard = (key: string) => {
383
399
  const entry = entries.get(key);
384
400
  if (!entry) return null;
385
401
 
386
- const fragment = template.content.cloneNode(true) as DocumentFragment;
387
- const el = fragment.querySelector<HTMLElement>('.notation-popup');
388
- const symbolHost =
389
- fragment.querySelector<HTMLElement>('[data-slot-symbol]');
390
- const title = fragment.querySelector<HTMLElement>('[data-slot-title]');
391
- const summary = fragment.querySelector<HTMLElement>(
392
- '[data-slot-summary]',
393
- );
394
- const formulaHost = fragment.querySelector<HTMLElement>(
395
- '[data-slot-formula]',
396
- );
397
- const units = fragment.querySelector<HTMLElement>('[data-slot-units]');
398
- const link =
399
- fragment.querySelector<HTMLAnchorElement>('[data-slot-link]');
402
+ const el = cloneCardShell(template);
403
+ const symbolHost = el?.querySelector<HTMLElement>('[data-slot-symbol]');
404
+ const title = el?.querySelector<HTMLElement>('[data-slot-title]');
405
+ const summary = el?.querySelector<HTMLElement>('[data-slot-summary]');
406
+ const formulaHost = el?.querySelector<HTMLElement>('[data-slot-formula]');
407
+ const units = el?.querySelector<HTMLElement>('[data-slot-units]');
408
+ const link = el?.querySelector<HTMLAnchorElement>('[data-slot-link]');
400
409
  if (!el || !symbolHost || !title || !summary || !formulaHost || !units) {
401
410
  return null;
402
411
  }
403
412
 
413
+ el.dataset.popupKey = key;
404
414
  el.setAttribute('aria-label', `Notation: ${labelOf(entry, key)}`);
405
415
 
406
416
  const sourceSymbol = entry.querySelector<HTMLElement>('.notation-symbol');
@@ -431,30 +441,28 @@ for (const gloss of glossItems) {
431
441
  ?.textContent?.trim() ?? '',
432
442
  );
433
443
 
434
- // A gloss has no card to open. Its own popup is the whole of it.
444
+ // A gloss has no entry to go to. Its own card is the whole of it.
435
445
  const href = entry.dataset.notationHref;
436
446
  if (link) {
437
447
  link.hidden = href === undefined;
438
- if (href !== undefined) link.href = href;
448
+ if (href !== undefined) {
449
+ link.href = href;
450
+ wireJumpArrow(link);
451
+ }
439
452
  }
440
453
 
441
454
  return el;
442
455
  };
443
456
 
444
- createPopupStack({
445
- // Popups are `position: fixed` and float above everything regardless
446
- // of DOM placement — *except* z-index only ever wins within its own
447
- // stacking context. Nesting them under the article risked landing
448
- // inside a stacking context an ancestor happens to establish (found
449
- // live: the Starlight right sidebar then ate their clicks even though
450
- // its own z-index is far lower). Mounting on `<body>` sidesteps that
451
- // entirely.
452
- container: document.body,
453
- groupId: 'notation',
454
- triggerFrom,
455
- keyFor,
456
- createPopup,
457
- onChange: (open, pinned) => {
457
+ // The card manager mounts every card in one layer on `<body>`, never in
458
+ // the article: an ancestor's stacking context once let the Starlight
459
+ // right sidebar eat a card's clicks.
460
+ registerCardSource({
461
+ layer: 'notation',
462
+ triggerFrom: symbolFrom,
463
+ itemFor: (trigger) => trigger.dataset.notationKey ?? null,
464
+ render: (key) => buildCard(key),
465
+ onChange: (open) => {
458
466
  for (const trigger of highlightTargets) {
459
467
  trigger.classList.toggle(
460
468
  'is-notation-active',
@@ -462,10 +470,8 @@ for (const gloss of glossItems) {
462
470
  );
463
471
  }
464
472
  for (const trigger of symbolTriggers) {
465
- trigger.setAttribute(
466
- 'aria-pressed',
467
- String(pinned.has(trigger.dataset.notationKey ?? '')),
468
- );
473
+ const mode = open.get(trigger.dataset.notationKey ?? '');
474
+ trigger.setAttribute('aria-pressed', String(!!mode && isKept(mode)));
469
475
  }
470
476
  },
471
477
  });
@@ -560,7 +566,7 @@ for (const gloss of glossItems) {
560
566
  // delegation needs.
561
567
  document.addEventListener('mouseover', (event) => {
562
568
  const origin = event.target instanceof Element ? event.target : null;
563
- if (!origin || origin.getAttribute('title') === 'Copy LaTeX') return;
569
+ if (!origin || origin.getAttribute('title')?.startsWith('Copy LaTeX')) return;
564
570
  if (
565
571
  origin.closest(
566
572
  '[data-notation-key], [data-notation-formula-copy], .notation-formula-number',
@@ -568,14 +574,20 @@ for (const gloss of glossItems) {
568
574
  ) {
569
575
  return;
570
576
  }
571
- if (!origin.closest('[data-notation-formula]')) return;
572
- origin.setAttribute('title', 'Copy LaTeX');
577
+ const formula = origin.closest<HTMLElement>('[data-notation-formula]');
578
+ if (!formula) return;
579
+ // "Copy LaTeX — ‹entry label›", the same title as the copy button.
580
+ origin.setAttribute('title', formula.dataset.copyTitle ?? 'Copy LaTeX');
581
+ origin.setAttribute('data-copy-hint', '');
573
582
  });
574
583
 
575
584
  document.addEventListener('mouseout', (event) => {
576
585
  const origin = event.target instanceof Element ? event.target : null;
577
- if (origin?.getAttribute('title') === 'Copy LaTeX') {
586
+ // Only a title this script added — the copy button's own static
587
+ // `title` must survive the pointer leaving it (or crossing onto its icon).
588
+ if (origin?.hasAttribute('data-copy-hint')) {
578
589
  origin.removeAttribute('title');
590
+ origin.removeAttribute('data-copy-hint');
579
591
  }
580
592
  });
581
593
  </script>
@@ -594,20 +606,16 @@ for (const gloss of glossItems) {
594
606
  }
595
607
 
596
608
  /* Positioning context for `.notation-list-anchor`, mirroring
597
- `Figure.astro`'s `.keyed-figure__titlebar` — the anchor link is a DOM
598
- sibling of `<details>`, not nested inside `<summary>` (a focusable `<a>`
599
- inside `<summary>` trips axe's `nested-interactive` check the same way
600
- it would there). */
609
+ `Memo.astro`'s frame — the anchor link is a DOM sibling of `<details>`,
610
+ not nested inside `<summary>` (a focusable `<a>` inside `<summary>`
611
+ trips axe's `nested-interactive` check). */
601
612
  .notation-list-titlebar {
602
613
  position: relative;
603
614
  }
604
615
 
605
- .notation-list {
606
- border: 1px solid var(--sl-color-gray-5);
607
- border-radius: 0.6rem;
608
- background: var(--sl-color-gray-7);
609
- }
610
-
616
+ /* `.notation-list` itself has no box styles: the panel is page
617
+ logistics, and the meta tier (`ex-tier-meta`, `surfaces.css`) draws
618
+ its surface, border and radius. */
611
619
  .notation-list > summary {
612
620
  padding: 0.8rem 1rem;
613
621
  padding-inline-end: 2.2rem;
@@ -671,6 +679,18 @@ for (const gloss of glossItems) {
671
679
  padding: 0 1rem 1rem;
672
680
  }
673
681
 
682
+ .notation-suspended {
683
+ margin: 0;
684
+ padding: 0 1rem 0.85rem;
685
+ list-style: none;
686
+ color: var(--sl-color-gray-2);
687
+ font-size: var(--sl-text-sm);
688
+ }
689
+
690
+ .notation-suspended li + li {
691
+ margin-top: 0.35rem;
692
+ }
693
+
674
694
  .notation-entry {
675
695
  display: grid;
676
696
  grid-template-columns: minmax(9rem, 0.65fr) minmax(12rem, 1.35fr);
@@ -768,61 +788,21 @@ for (const gloss of glossItems) {
768
788
  }
769
789
 
770
790
  .notation-enhancement-help {
771
- margin: 0.55rem 0 0;
791
+ margin: 0 0 0.75rem;
792
+ padding: 0 1rem;
772
793
  font-size: var(--sl-text-xs);
773
794
  }
774
795
 
775
- /* Each open explanation is its own floating card — see `popup-stack.ts`.
776
- They're mounted on `<body>`, not here (a stacking-context trap found
777
- live: an ancestor of this section could otherwise cap how high a
778
- popup's own z-index reaches). `.notation-panel-body` and its
779
- descendants below style the fields common to both this static list
780
- and a popup's cloned content. */
781
- .notation-popup {
782
- position: fixed;
783
- z-index: 100;
784
- margin: 0;
785
- width: min(23rem, calc(100vw - 1.5rem));
786
- overflow: auto;
787
- padding-block: 0.75rem;
788
- padding-inline: 1rem 2.4rem;
789
- border: 1px solid var(--sl-color-gray-5);
790
- border-top: 0.25rem solid var(--sl-color-accent);
791
- border-radius: 0.65rem;
792
- color: var(--sl-color-text);
793
- background: var(--sl-color-bg);
794
- box-shadow: var(--sl-shadow-lg);
795
- /* Relative to the surrounding prose's own font-size, not a fixed
796
- Starlight step — the card is mounted on `<body>`, alongside the
797
- article rather than nested inside it, so `%` still resolves against
798
- the same base the article's paragraphs use. */
796
+ /* Each open explanation is its own card, cloned from the `HoverPanel`
797
+ shell above and mounted by the card manager (`card-manager.ts`) on
798
+ `<body>`. Relative to the surrounding prose's own font-size, not a
799
+ fixed Starlight step: `%` resolves against the same base the article's
800
+ paragraphs use. `.notation-panel-body` and its descendants below style
801
+ the fields common to both this static list and a card's content. */
802
+ .notation-popup.hover-panel {
799
803
  font-size: 90%;
800
804
  }
801
805
 
802
- .notation-popup-close {
803
- position: absolute;
804
- top: 0.5rem;
805
- right: 0.5rem;
806
- width: 1.6rem;
807
- height: 1.6rem;
808
- display: flex;
809
- align-items: center;
810
- justify-content: center;
811
- padding: 0;
812
- border: 0;
813
- border-radius: 0.35rem;
814
- color: var(--sl-color-gray-3);
815
- background: transparent;
816
- font-size: 1.15rem;
817
- line-height: 1;
818
- cursor: pointer;
819
- }
820
-
821
- .notation-popup-close:hover {
822
- color: var(--sl-color-text);
823
- background: var(--sl-color-gray-6);
824
- }
825
-
826
806
  /* Symbols inside a card's formula are click/hover-through triggers for
827
807
  another card — the D15 case this whole file exists for. */
828
808
  .notation-panel-body [data-slot-formula] :global([data-notation-key]) {
@@ -901,6 +881,33 @@ for (const gloss of glossItems) {
901
881
  pointer-events: none;
902
882
  }
903
883
 
884
+ /* An accent's glyph box (`\hat`, `\dot`, `\vec`, …) is a full line tall and
885
+ painted over the letter it decorates, so it took every hover on that
886
+ letter: under an operator entry (rule N22) `\dot{x}` showed the dot's
887
+ card wherever the reader pointed. The glyph box lets the pointer through,
888
+ and a strip across the top of the decorated form — where KaTeX puts the
889
+ accent's ink, since the vlist's height is the accent's top — is the
890
+ accent's hit area instead. It sits inside the decorated form's trigger
891
+ and outside the letter's, so the letter shows its card and the accent
892
+ shows the operator's. Measured over 14 accents on short, tall and
893
+ subscripted letters; rendering is pixel-identical. */
894
+ :global(.katex-html .accent-body) {
895
+ pointer-events: none;
896
+ }
897
+
898
+ :global(.katex-html .accent > .vlist-t > .vlist-r > .vlist) {
899
+ position: relative;
900
+ }
901
+
902
+ :global(.katex-html .accent > .vlist-t > .vlist-r > .vlist::after) {
903
+ content: '';
904
+ position: absolute;
905
+ inset-inline: -0.1em;
906
+ top: 0;
907
+ height: 0.25em;
908
+ pointer-events: auto;
909
+ }
910
+
904
911
  :global(.katex-html [data-notation-key]) {
905
912
  cursor: help;
906
913
  }
@@ -1,6 +1,10 @@
1
1
  ---
2
2
  import { renderNotationMath } from './render-notation-math';
3
3
  import SafeMathNode from './SafeMathNode.astro';
4
+ import {
5
+ decorationOfEntry,
6
+ operatorDisplayLatex,
7
+ } from '../../reference/math-glyphs.mjs';
4
8
 
5
9
  interface Props {
6
10
  latex: string;
@@ -8,7 +12,12 @@ interface Props {
8
12
  }
9
13
 
10
14
  const { latex, class: className } = Astro.props;
11
- const nodes = renderNotationMath(latex);
15
+ // An operator entry (`\dot{}`, rule N22) is shown over a placeholder square;
16
+ // on its own the decoration is a speck.
17
+ const operator = decorationOfEntry(latex);
18
+ const nodes = renderNotationMath(
19
+ operator === undefined ? latex : operatorDisplayLatex(operator),
20
+ );
12
21
  ---
13
22
 
14
23
  <span class:list={['notation-math', className]} data-notation-math>