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
@@ -0,0 +1,182 @@
1
+ /* Style primitives and semantic tiers (docs/plans/titles-and-containers.md
2
+ Part 2). Every container the engine draws is one of four tiers, and every
3
+ tier is composed from the same three primitives:
4
+
5
+ surface none · tint-neutral · tint-accent
6
+ edge none · border · rule-left
7
+ depth flat · raised
8
+
9
+ plain none + none + flat references, tables, equations,
10
+ figures, sections by default
11
+ meta tint-neutral + border + flat notation panel, part rail,
12
+ About panel, Starlight <Card>
13
+ accent tint-accent + border + raised knowledge checks, worked
14
+ examples, `[card]` sections
15
+ aside (tint-accent)+ rule-left + flat memo (tinted), opened short
16
+ note, Starlight asides
17
+
18
+ What a tier means: grey (meta) is logistics or generated; no card (plain)
19
+ is what a book would print in its main flow; an accent card is enrichment
20
+ the author marked. `plain` has no class — it is the absence of one.
21
+
22
+ There is no top rule anywhere in the system, and elevation never scales
23
+ with heading depth: hierarchy stays in type size and numbers. Radius and
24
+ shadow are tokens, never author-selectable; an author reaches a tier only
25
+ through a heading's `[card]` / `[card=meta]` marker. Imported by
26
+ `global.css`, so a course's `customCss` needs no second entry.
27
+
28
+ Dark is Starlight's base theme (`:root`), light its override. */
29
+
30
+ :root {
31
+ --ex-surface-none: transparent;
32
+ /* Starlight defines `--sl-color-gray-7` for its light theme only: a dark
33
+ page reading it got no background at all. Each theme names its own. */
34
+ --ex-surface-tint-neutral: var(--sl-color-gray-6);
35
+ --ex-surface-tint-accent: color-mix(in srgb, var(--sl-color-bg) 92%, var(--sl-color-accent) 8%);
36
+
37
+ --ex-edge-color: var(--sl-color-gray-5);
38
+ --ex-edge-border: 1px solid var(--ex-edge-color);
39
+ --ex-edge-rule-width: 3px;
40
+ --ex-edge-rule-color: var(--sl-color-accent);
41
+
42
+ --ex-depth-flat: none;
43
+ --ex-depth-raised: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 14px rgba(0, 0, 0, 0.35);
44
+
45
+ /* One radius per boxed tier; an aside is a rule, not a box, and has none. */
46
+ --ex-radius-meta: 0.5rem;
47
+ --ex-radius-accent: 0.5rem;
48
+
49
+ /* Inner spacing shared by the boxed tiers and the aside tier. A box's
50
+ sides hold a section's chevron, so they stay a little wider than its
51
+ top and bottom. */
52
+ --ex-pad-box: clamp(1rem, 2.5vw, 1.25rem);
53
+ --ex-pad-box-block: 1rem;
54
+ --ex-pad-aside: 0.75rem 1rem;
55
+
56
+ /* Starlight asides (`:::caution`, `:::danger`) keep a warning / danger hue:
57
+ a caution in the course accent stops reading as one. */
58
+ --ex-aside-caution: var(--sl-color-orange);
59
+ --ex-aside-caution-surface: color-mix(in srgb, var(--sl-color-bg) 88%, var(--sl-color-orange) 12%);
60
+ --ex-aside-caution-text: var(--sl-color-orange-high);
61
+ --ex-aside-danger: var(--sl-color-red);
62
+ --ex-aside-danger-surface: color-mix(in srgb, var(--sl-color-bg) 88%, var(--sl-color-red) 12%);
63
+ --ex-aside-danger-text: var(--sl-color-red-high);
64
+ }
65
+
66
+ :root[data-theme='light'] {
67
+ --ex-surface-tint-neutral: var(--sl-color-gray-7);
68
+ --ex-surface-tint-accent: color-mix(in srgb, var(--sl-color-bg) 93%, var(--sl-color-accent) 7%);
69
+ --ex-edge-color: var(--sl-color-gray-5);
70
+ --ex-depth-raised: 0 1px 2px rgba(30, 42, 35, 0.08), 0 4px 14px rgba(30, 42, 35, 0.07);
71
+ --ex-aside-caution-surface: color-mix(in srgb, var(--sl-color-bg) 90%, var(--sl-color-orange) 10%);
72
+ --ex-aside-danger-surface: color-mix(in srgb, var(--sl-color-bg) 92%, var(--sl-color-red) 8%);
73
+ }
74
+
75
+ .ex-tier-meta {
76
+ background: var(--ex-surface-tint-neutral);
77
+ border: var(--ex-edge-border);
78
+ border-radius: var(--ex-radius-meta);
79
+ box-shadow: var(--ex-depth-flat);
80
+ }
81
+
82
+ .ex-tier-accent {
83
+ background: var(--ex-surface-tint-accent);
84
+ border: var(--ex-edge-border);
85
+ border-radius: var(--ex-radius-accent);
86
+ box-shadow: var(--ex-depth-raised);
87
+ }
88
+
89
+ /* The aside tier's surface is per use: a memo is tinted, an opened short note
90
+ is not. `--ex-aside-surface` and `--ex-aside-rule` are the two knobs. */
91
+ .ex-tier-aside {
92
+ background: var(--ex-aside-surface, var(--ex-surface-none));
93
+ border: 0;
94
+ border-inline-start: var(--ex-edge-rule-width) solid var(--ex-aside-rule, var(--ex-edge-rule-color));
95
+ border-radius: 0;
96
+ box-shadow: var(--ex-depth-flat);
97
+ }
98
+
99
+ .ex-tier-aside--tinted {
100
+ --ex-aside-surface: var(--ex-surface-tint-accent);
101
+ }
102
+
103
+ /* A `[card]` / `[card=meta]` section (`CollapsibleSections.astro` wraps the
104
+ heading and its body). Its padding holds the collapse chevron: the gutter
105
+ the chevron centres in is the card's own inner edge, not the page's. */
106
+ .sl-markdown-content .ex-section-card {
107
+ --ex-section-gutter: var(--ex-pad-box);
108
+ margin-block: 1.5rem;
109
+ padding: var(--ex-pad-box-block) var(--ex-pad-box);
110
+ }
111
+
112
+ .sl-markdown-content .ex-section-card > :first-child {
113
+ margin-block-start: 0;
114
+ }
115
+
116
+ .sl-markdown-content .ex-section-card > :last-child {
117
+ margin-block-end: 0;
118
+ }
119
+
120
+ /* Starlight asides (Q10): the aside tier's shape — left rule, tint, explico
121
+ spacing — with Starlight's own icon per type kept. `note` and `tip` take the
122
+ course accent; `caution` and `danger` keep their hue on the rule, tint and
123
+ title. `.starlight-aside` rules live in `@layer starlight.content`; these
124
+ are unlayered, so they win. */
125
+ .starlight-aside {
126
+ --ex-aside-surface: var(--ex-surface-tint-accent);
127
+ --sl-color-asides-text-accent: var(--sl-color-text-accent);
128
+ --sl-color-asides-border: var(--ex-edge-rule-color);
129
+ padding: var(--ex-pad-aside);
130
+ background: var(--ex-aside-surface);
131
+ border: 0;
132
+ border-inline-start: var(--ex-edge-rule-width) solid var(--sl-color-asides-border);
133
+ border-radius: 0;
134
+ color: var(--sl-color-text);
135
+ }
136
+
137
+ .starlight-aside--caution {
138
+ --ex-aside-surface: var(--ex-aside-caution-surface);
139
+ --sl-color-asides-text-accent: var(--ex-aside-caution-text);
140
+ --sl-color-asides-border: var(--ex-aside-caution);
141
+ }
142
+
143
+ .starlight-aside--danger {
144
+ --ex-aside-surface: var(--ex-aside-danger-surface);
145
+ --sl-color-asides-text-accent: var(--ex-aside-danger-text);
146
+ --sl-color-asides-border: var(--ex-aside-danger);
147
+ }
148
+
149
+ .starlight-aside__title {
150
+ font-weight: 600;
151
+ }
152
+
153
+ /* `:::note{icon=""}` / `<Aside icon="">` — the author dropped the icon
154
+ (`remark-asides.mjs` wraps the aside in this marker; Starlight itself
155
+ reads an empty icon as "use the default"). */
156
+ .ex-aside-no-icon > .starlight-aside .starlight-aside__icon {
157
+ display: none;
158
+ }
159
+
160
+ /* Starlight `<Card>` (Q10): the meta tier. Starlight's own card is a black
161
+ box with a coloured icon chip; the chip stays, the box becomes meta.
162
+ `article.card` is the element `Card.astro` renders, so a course's own
163
+ `.card` class elsewhere is left alone. */
164
+ .sl-markdown-content article.card {
165
+ background: var(--ex-surface-tint-neutral);
166
+ border: var(--ex-edge-border);
167
+ border-radius: var(--ex-radius-meta);
168
+ box-shadow: var(--ex-depth-flat);
169
+ }
170
+
171
+ @media (forced-colors: active) {
172
+ .ex-tier-meta,
173
+ .ex-tier-accent,
174
+ .sl-markdown-content article.card {
175
+ border-color: CanvasText;
176
+ }
177
+
178
+ .ex-tier-aside,
179
+ .starlight-aside {
180
+ border-inline-start-color: CanvasText;
181
+ }
182
+ }
@@ -1,339 +0,0 @@
1
- /**
2
- * Interaction engine for the shared hover/pin panel (see `HoverPanel.astro`).
3
- *
4
- * One copy of the positioning, pin, Escape, outside-close, and
5
- * article-column-clamp logic that the notation layer and the citation layer
6
- * both need. Each layer supplies the domain-specific seams — how to find a
7
- * trigger, how to key it, how to fill the panel — and keeps its own markup.
8
- *
9
- * The panel is `position: fixed`. Placement clears the whole display equation
10
- * a marker sits inside (never just the glyph), decides the side against the
11
- * space left by the sticky site header, and keeps the box within the article
12
- * column so it never lands under a navigation rail.
13
- */
14
-
15
- export interface HoverPanelState {
16
- activeKey: string | null;
17
- pinnedKey: string | null;
18
- visible: boolean;
19
- }
20
-
21
- export interface HoverPanelConfig {
22
- /** The `<aside data-hover-panel>` element. */
23
- panel: HTMLElement;
24
- /** Exclusivity group (see the `activeSurface` note below `HoverPanelState`). */
25
- groupId: string;
26
- /**
27
- * Element used only to locate the enclosing article column for the
28
- * horizontal clamp. Defaults to the panel itself.
29
- */
30
- root?: HTMLElement;
31
- /** Interactive element (marker, list row, control) for an event target. */
32
- triggerFrom: (node: EventTarget | null) => HTMLElement | null;
33
- /** Stable key identifying the content a trigger points at. */
34
- keyFor: (trigger: HTMLElement) => string | null;
35
- /**
36
- * Fill the panel for `trigger`/`key`. Return `false` to abort the open
37
- * (unknown key); the panel stays hidden.
38
- */
39
- render: (trigger: HTMLElement, key: string) => boolean | void;
40
- /** A pin toggle for an event target (a list button, an in-panel button). */
41
- pinControlFrom?: (node: EventTarget | null) => HTMLElement | null;
42
- /** Called after the panel is shown for `key`. */
43
- onShow?: (key: string, trigger: HTMLElement) => void;
44
- /** Called after the panel is hidden. */
45
- onClose?: () => void;
46
- /** Called after every open, close, or pin change. */
47
- onSync?: (state: HoverPanelState) => void;
48
- }
49
-
50
- function closestElement(node: EventTarget | null, selector: string) {
51
- return node instanceof Element ? node.closest<HTMLElement>(selector) : null;
52
- }
53
-
54
- /**
55
- * The notation layer and the citation layer each own their explanations —
56
- * the notation layer through its own popup stack (`./popup-stack.ts`), the
57
- * citation layer through this module's `createHoverPanel`. Neither knows the
58
- * other exists, so a pinned citation and an open notation popup — or the
59
- * reverse — can each be individually correct and still land on top of each
60
- * other, or on top of the equation the other one is explaining. Rather than
61
- * teach every positioning calculation about every other surface that might
62
- * exist, the two are mutually exclusive *groups*: opening the first popup in
63
- * one group closes whatever the other group had open. Within a group,
64
- * surfaces manage their own overlap (the citation panel is a singleton; the
65
- * notation stack positions its own popups against each other). A pin is not
66
- * exempt — the reader's attention already moved to the other group.
67
- */
68
- let activeSurface: {
69
- readonly groupId: string;
70
- readonly close: () => void;
71
- } | null = null;
72
-
73
- export function claimActiveSurface(groupId: string, close: () => void): void {
74
- if (activeSurface && activeSurface.groupId !== groupId) {
75
- activeSurface.close();
76
- }
77
- activeSurface = { groupId, close };
78
- }
79
-
80
- export function releaseActiveSurface(groupId: string): void {
81
- if (activeSurface?.groupId === groupId) activeSurface = null;
82
- }
83
-
84
- export function createHoverPanel(config: HoverPanelConfig) {
85
- const { panel } = config;
86
- if (panel.dataset.hoverPanelReady === 'true') return;
87
- panel.dataset.hoverPanelReady = 'true';
88
-
89
- const anchorRoot = config.root ?? panel;
90
-
91
- let activeKey: string | null = null;
92
- let activeAnchor: HTMLElement | null = null;
93
- let pinned: { key: string; anchor: HTMLElement } | null = null;
94
-
95
- const sync = () => {
96
- config.onSync?.({
97
- activeKey,
98
- pinnedKey: pinned?.key ?? null,
99
- visible: panel.hidden === false,
100
- });
101
- };
102
-
103
- const pinControlFrom = (node: EventTarget | null) =>
104
- config.pinControlFrom?.(node) ?? null;
105
-
106
- const interactiveFrom = (node: EventTarget | null) =>
107
- pinControlFrom(node) ?? config.triggerFrom(node);
108
-
109
- const position = () => {
110
- if (panel.hidden || !activeAnchor) return;
111
-
112
- const edge = 12;
113
- const gap = 10;
114
- const displayEquation = activeAnchor.closest<HTMLElement>('.katex-display');
115
- const safeRegion = displayEquation ?? activeAnchor;
116
- const regionRect = safeRegion.getBoundingClientRect();
117
- const anchorRect = activeAnchor.getBoundingClientRect();
118
-
119
- // The usable band is bounded by the sticky header, not the raw viewport
120
- // edge. Decide the side against that real space so a clamp against the
121
- // header can never push the panel back down over the equation.
122
- const headerBottom =
123
- document.querySelector<HTMLElement>('.header')?.getBoundingClientRect()
124
- .bottom ?? 0;
125
- const verticalStart = Math.max(edge, headerBottom + edge);
126
- const verticalEnd = window.innerHeight - edge;
127
- const spaceAbove = regionRect.top - gap - verticalStart;
128
- const spaceBelow = verticalEnd - regionRect.bottom - gap;
129
-
130
- // Measure the panel in the roomier band, then keep it on whichever side
131
- // actually clears the safe region; shrink only when neither side fits.
132
- panel.style.maxHeight = `${Math.max(spaceAbove, spaceBelow, 0)}px`;
133
- let panelRect = panel.getBoundingClientRect();
134
- const placeBelow =
135
- spaceBelow >= panelRect.height || spaceBelow >= spaceAbove;
136
- panel.style.maxHeight = `${Math.max(placeBelow ? spaceBelow : spaceAbove, 0)}px`;
137
- panelRect = panel.getBoundingClientRect();
138
-
139
- let top = placeBelow
140
- ? regionRect.bottom + gap
141
- : regionRect.top - gap - panelRect.height;
142
- let left = anchorRect.left + anchorRect.width / 2 - panelRect.width / 2;
143
-
144
- const article =
145
- anchorRoot
146
- .closest<HTMLElement>('.main-pane')
147
- ?.querySelector<HTMLElement>('.sl-markdown-content') ??
148
- document.querySelector<HTMLElement>('.sl-markdown-content');
149
- const articleRect = article?.getBoundingClientRect();
150
- const horizontalStart = Math.max(edge, articleRect?.left ?? edge);
151
- const horizontalEnd = Math.min(
152
- window.innerWidth - edge,
153
- articleRect?.right ?? window.innerWidth - edge,
154
- );
155
-
156
- top = Math.max(
157
- verticalStart,
158
- Math.min(top, verticalEnd - panelRect.height),
159
- );
160
- left =
161
- horizontalEnd - horizontalStart >= panelRect.width
162
- ? Math.max(
163
- horizontalStart,
164
- Math.min(left, horizontalEnd - panelRect.width),
165
- )
166
- : Math.max(
167
- edge,
168
- Math.min(left, window.innerWidth - panelRect.width - edge),
169
- );
170
-
171
- panel.style.top = `${Math.round(top)}px`;
172
- panel.style.left = `${Math.round(left)}px`;
173
- };
174
-
175
- const show = (anchor: HTMLElement) => {
176
- const key = config.keyFor(anchor);
177
- if (!key) return;
178
- if (config.render(anchor, key) === false) return;
179
-
180
- claimActiveSurface(config.groupId, forceClose);
181
-
182
- activeKey = key;
183
- activeAnchor = anchor;
184
- config.onShow?.(key, anchor);
185
-
186
- panel.style.visibility = 'hidden';
187
- panel.hidden = false;
188
- position();
189
- panel.style.visibility = 'visible';
190
- sync();
191
- };
192
-
193
- const close = () => {
194
- activeKey = null;
195
- activeAnchor = null;
196
- panel.hidden = true;
197
- config.onClose?.();
198
- releaseActiveSurface(config.groupId);
199
- sync();
200
- };
201
-
202
- // A plain `close()` deliberately leaves `pinned` set — a stray pointerout or
203
- // focusout elsewhere on the page must not kill a pin the reader placed on
204
- // purpose, and `restorePinnedOrClose` below relies on `pinned` surviving a
205
- // transient close to bring the panel back. Being displaced by a DIFFERENT
206
- // panel taking over (`claimActivePanel`) is not transient, though: without
207
- // clearing the pin here, the very next stray blur anywhere on the page finds
208
- // `pinned` still truthy and restores this panel — undoing the takeover a
209
- // frame later and fighting the other panel for the screen.
210
- const forceClose = () => {
211
- pinned = null;
212
- close();
213
- };
214
-
215
- const restorePinnedOrClose = () => {
216
- window.requestAnimationFrame(() => {
217
- const focused = config.triggerFrom(document.activeElement);
218
- if (focused && config.keyFor(focused) === activeKey) return;
219
- if (pinned) {
220
- show(pinned.anchor);
221
- return;
222
- }
223
- if (
224
- activeAnchor?.matches(':hover') ||
225
- panel.matches(':hover') ||
226
- panel.contains(document.activeElement)
227
- ) {
228
- return;
229
- }
230
- close();
231
- });
232
- };
233
-
234
- const togglePin = (key: string, anchor: HTMLElement) => {
235
- if (pinned?.key === key && pinned.anchor === anchor) {
236
- pinned = null;
237
- close();
238
- return;
239
- }
240
- pinned = { key, anchor };
241
- show(anchor);
242
- };
243
-
244
- document.addEventListener('pointerover', (event) => {
245
- if (event.pointerType === 'touch' || pinned) return;
246
- const anchor = config.triggerFrom(event.target);
247
- const key = anchor ? config.keyFor(anchor) : null;
248
- if (anchor && key && anchor !== config.triggerFrom(event.relatedTarget)) {
249
- show(anchor);
250
- }
251
- });
252
-
253
- document.addEventListener('pointerout', (event) => {
254
- if (event.pointerType === 'touch') return;
255
- const anchor = config.triggerFrom(event.target);
256
- if (!anchor || anchor === config.triggerFrom(event.relatedTarget)) return;
257
- if (
258
- event.relatedTarget instanceof Node &&
259
- panel.contains(event.relatedTarget)
260
- ) {
261
- return;
262
- }
263
- restorePinnedOrClose();
264
- });
265
-
266
- document.addEventListener('focusin', (event) => {
267
- const anchor = config.triggerFrom(event.target);
268
- const key = anchor ? config.keyFor(anchor) : null;
269
- if (anchor && key) show(anchor);
270
- });
271
- document.addEventListener('focusout', restorePinnedOrClose);
272
-
273
- document.addEventListener('click', (event) => {
274
- const control = pinControlFrom(event.target);
275
- if (control) {
276
- const ownKey = config.keyFor(control);
277
- if (ownKey) {
278
- togglePin(ownKey, control);
279
- } else if (activeKey && activeAnchor) {
280
- togglePin(activeKey, activeAnchor);
281
- }
282
- return;
283
- }
284
-
285
- const trigger = config.triggerFrom(event.target);
286
- if (trigger && !trigger.closest('a')) {
287
- const key = config.keyFor(trigger);
288
- if (key) {
289
- event.preventDefault();
290
- togglePin(key, trigger);
291
- return;
292
- }
293
- }
294
- if (trigger) return; // a real link trigger: let it navigate.
295
-
296
- // Anywhere else inside the panel collapses it, same as the corner "×" —
297
- // its own body is never a trigger, so a click there can only mean "I'm
298
- // done with this." A real link (e.g. "Open source link") is excluded so
299
- // it still navigates instead of being swallowed.
300
- if (
301
- !panel.hidden &&
302
- event.target instanceof Node &&
303
- panel.contains(event.target) &&
304
- !closestElement(event.target, 'a')
305
- ) {
306
- pinned = null;
307
- close();
308
- }
309
- });
310
-
311
- document.addEventListener('pointerdown', (event) => {
312
- if (!pinned) return;
313
- if (
314
- interactiveFrom(event.target) ||
315
- (event.target instanceof Node && panel.contains(event.target))
316
- ) {
317
- return;
318
- }
319
- pinned = null;
320
- close();
321
- });
322
-
323
- document.addEventListener('keydown', (event) => {
324
- if (event.key !== 'Escape' || (!activeKey && !pinned)) return;
325
- pinned = null;
326
- close();
327
- });
328
-
329
- panel.addEventListener('pointerleave', restorePinnedOrClose);
330
- window.addEventListener('resize', position, { passive: true });
331
- window.addEventListener('scroll', position, {
332
- passive: true,
333
- capture: true,
334
- });
335
-
336
- sync();
337
- }
338
-
339
- export { closestElement };