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,146 @@
1
+ /**
2
+ * The heading a knowledge check and a worked-example set draw for themselves
3
+ * (docs/plans/titles-and-containers.md Part 6, Q11). The author writes only
4
+ * the topic; the engine draws the *kind* before it — an icon and, by
5
+ * default, its word — and the number, which shows and hides by the section
6
+ * rules. `BlockHeading.astro` renders it; `LayoutPageSidebar.astro` lists it
7
+ * in "On this page" through {@link tocWithTitledBlocks}.
8
+ */
9
+ import {
10
+ BLOCK_LABELS_DEFAULT,
11
+ type BlockLabels,
12
+ } from '../compiler/course-config';
13
+ import {
14
+ loadManifest,
15
+ type Manifest,
16
+ type ManifestTitledBlock,
17
+ } from '../compiler/manifest';
18
+
19
+ export type BlockKind = ManifestTitledBlock['kind'];
20
+
21
+ export const BLOCK_KINDS: Readonly<
22
+ Record<BlockKind, { readonly word: string; readonly numberAttribute: string }>
23
+ > = {
24
+ // `data-check-number` / `data-ex-number` beside the heading's `id` is what
25
+ // a number-form fragment (`#ex-1.1.1`) resolves against
26
+ // (`KeyedFloatEnhancer.astro`), and what the chain icon's "Link copied ·
27
+ // Example 1.1.1" reads (`HeadingAnchorEnhancer.astro`).
28
+ check: { word: 'Knowledge check', numberAttribute: 'data-check-number' },
29
+ example: { word: 'Example', numberAttribute: 'data-ex-number' },
30
+ };
31
+
32
+ export interface BlockHeadingSettings {
33
+ /** The course's badge form (`blockLabels`). */
34
+ readonly labels: BlockLabels;
35
+ /** Whether numbers show at all (`numbering.sections.show`). */
36
+ readonly showNumbers: boolean;
37
+ }
38
+
39
+ // Read once per build, like `courseDisclosure()`: every check, every example
40
+ // set and every page's "On this page" asks, and the manifest does not change
41
+ // under a running build (a content edit in dev restarts Astro).
42
+ let cached: Promise<Manifest> | undefined;
43
+ const manifestOnce = (): Promise<Manifest> => (cached ??= loadManifest());
44
+
45
+ /** The course's badge form and number visibility. */
46
+ export async function blockHeadingSettings(): Promise<BlockHeadingSettings> {
47
+ const manifest = await manifestOnce();
48
+ return {
49
+ labels: manifest.blockLabels ?? BLOCK_LABELS_DEFAULT,
50
+ showNumbers: manifest.numbering?.sections?.show !== false,
51
+ };
52
+ }
53
+
54
+ /** A lesson's titled blocks (`manifest.lessons[].titledBlocks`), by its id. */
55
+ export async function titledBlocksOf(
56
+ lessonId: string,
57
+ ): Promise<readonly ManifestTitledBlock[]> {
58
+ const lesson = (await manifestOnce()).lessons.find((entry) => entry.id === lessonId);
59
+ return lesson?.titledBlocks ?? [];
60
+ }
61
+
62
+ /** A heading as Astro and Starlight list it (`MarkdownHeading`). */
63
+ export interface OutlineHeading {
64
+ readonly depth: number;
65
+ readonly slug: string;
66
+ readonly text: string;
67
+ }
68
+
69
+ /** One "On this page" entry (Starlight's `TocItem`). */
70
+ export interface TocItem extends OutlineHeading {
71
+ children: TocItem[];
72
+ }
73
+
74
+ /**
75
+ * A page's headings with its titled blocks' `<h2>`s put in: each right after
76
+ * the top-level heading it follows (`after`), or ahead of every heading when
77
+ * it follows none. Blocks after one heading keep their document order; one
78
+ * whose heading the page does not list goes at the end. The entry is the
79
+ * author's title alone, as a section's is — the badge, like a section's
80
+ * number, is the engine's; `LayoutPageSidebar.astro` draws the badge's icon
81
+ * before it.
82
+ */
83
+ export function withTitledBlocks(
84
+ headings: readonly OutlineHeading[],
85
+ blocks: readonly ManifestTitledBlock[],
86
+ ): OutlineHeading[] {
87
+ const asHeading = (block: ManifestTitledBlock): OutlineHeading => ({
88
+ depth: 2,
89
+ slug: block.anchor,
90
+ text: block.title,
91
+ });
92
+ const leading: OutlineHeading[] = [];
93
+ const byAfter = new Map<string, OutlineHeading[]>();
94
+ for (const block of blocks) {
95
+ if (block.after === undefined) {
96
+ leading.push(asHeading(block));
97
+ } else {
98
+ const list = byAfter.get(block.after) ?? [];
99
+ list.push(asHeading(block));
100
+ byAfter.set(block.after, list);
101
+ }
102
+ }
103
+ const out = [...leading];
104
+ for (const heading of headings) {
105
+ out.push(heading);
106
+ out.push(...(byAfter.get(heading.slug) ?? []));
107
+ byAfter.delete(heading.slug);
108
+ }
109
+ for (const rest of byAfter.values()) out.push(...rest);
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Starlight's "On this page" tree rebuilt with the titled blocks in it: the
115
+ * same nesting `generateToC` does (`@astrojs/starlight/utils/generateToC.ts`)
116
+ * — its first item, the page-title "Overview", kept as it was, and every
117
+ * heading in the level range nested under the last shallower one. A
118
+ * component draws these `<h2>`s, so Astro's heading list never had them
119
+ * (audit §8.7). Returns `items` unchanged when the first item is not the
120
+ * overview Starlight puts there.
121
+ */
122
+ export function tocWithTitledBlocks(
123
+ toc: {
124
+ readonly minHeadingLevel: number;
125
+ readonly maxHeadingLevel: number;
126
+ readonly items: readonly TocItem[];
127
+ },
128
+ headings: readonly OutlineHeading[],
129
+ blocks: readonly ManifestTitledBlock[],
130
+ ): TocItem[] {
131
+ const [overview] = toc.items;
132
+ if (blocks.length === 0 || overview?.slug !== '_top') return [...toc.items];
133
+ const items: TocItem[] = [{ ...overview, children: [] }];
134
+ const inject = (list: TocItem[], item: TocItem): void => {
135
+ const last = list.at(-1);
136
+ if (!last || last.depth >= item.depth) list.push(item);
137
+ else inject(last.children, item);
138
+ };
139
+ for (const heading of withTitledBlocks(headings, blocks)) {
140
+ if (heading.depth < toc.minHeadingLevel || heading.depth > toc.maxHeadingLevel) {
141
+ continue;
142
+ }
143
+ inject(items, { ...heading, children: [] });
144
+ }
145
+ return items;
146
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * The arrow from a kept card to an occurrence of its item (rule S5 of
3
+ * `docs/plans/hover-card-consistency.md`).
4
+ *
5
+ * A pinned or docked card can sit far from the place the reader is pointing
6
+ * at: another occurrence of its symbol, or — for a card in the side column —
7
+ * its own trigger. The arrow leaves the card's edge on the side facing that
8
+ * occurrence, arcs over the occurrence's line and ends on it with an
9
+ * arrowhead. It stays while the occurrence or the card is hovered or focused,
10
+ * and fades a while after both are left.
11
+ *
12
+ * `arrowShape` is the geometry, pure, over plain boxes, so
13
+ * `tests/unit/card-arrow.test.ts` can check it in Node. The overlay is one
14
+ * SVG at the bottom of the card layer (`./card-placement.ts`): above the
15
+ * article, below every card, in page coordinates, so it scrolls with both.
16
+ */
17
+ import type { Box, Point } from './card-travel';
18
+
19
+ const clamp = (value: number, low: number, high: number) =>
20
+ Math.max(low, Math.min(value, high));
21
+
22
+ /** The arrowhead's length and half its width (px). */
23
+ const HEAD_LENGTH = 8;
24
+ const HEAD_HALF_WIDTH = 4.5;
25
+
26
+ export interface ArrowShape {
27
+ /** Where the line leaves the card. */
28
+ readonly start: Point;
29
+ /** The two control points of the line's cubic curve. */
30
+ readonly controls: readonly [Point, Point];
31
+ /** Where the line meets the arrowhead. */
32
+ readonly neck: Point;
33
+ /** The arrowhead's point: on the occurrence's top edge. */
34
+ readonly tip: Point;
35
+ /** The line, as an SVG path. */
36
+ readonly path: string;
37
+ /** The arrowhead, as SVG polygon points. */
38
+ readonly head: string;
39
+ }
40
+
41
+ const format = (value: number) => String(Math.round(value * 10) / 10);
42
+
43
+ /**
44
+ * S5: from `card`'s edge to the top of `target`, the occurrence's box on its
45
+ * first line. The line leaves the card on the side facing the occurrence, and
46
+ * its last stretch comes straight down onto the occurrence, so the arc passes
47
+ * over the occurrence's line instead of through it.
48
+ */
49
+ export function arrowShape(card: Box, target: Box): ArrowShape {
50
+ const tip = { x: (target.left + target.right) / 2, y: target.top - 1 };
51
+ const leftward = tip.x < (card.left + card.right) / 2;
52
+ // Leave level with the occurrence's line where the card allows it, clear
53
+ // of its rounded corners. A side card's top is 14 px above its line (S2),
54
+ // so this starts at or above the line's top, never through its text.
55
+ const inset = Math.min(12, (card.bottom - card.top) / 2);
56
+ const start = {
57
+ x: leftward ? card.left : card.right,
58
+ y: clamp(tip.y, card.top + inset, card.bottom - inset),
59
+ };
60
+ const reach = Math.abs(start.x - tip.x);
61
+ const lift = clamp(reach * 0.2, 20, 48);
62
+ const out = leftward ? -1 : 1;
63
+ const controls: [Point, Point] = [
64
+ {
65
+ x: start.x + out * Math.max(24, reach * 0.4),
66
+ // An occurrence level with or above the start: arc over it. One below:
67
+ // leave level, then come down onto it.
68
+ y: tip.y <= start.y ? tip.y - lift : start.y,
69
+ },
70
+ { x: tip.x, y: tip.y - lift },
71
+ ];
72
+ const neck = { x: tip.x, y: tip.y - HEAD_LENGTH + 1 };
73
+ const [c1, c2] = controls;
74
+ return {
75
+ start,
76
+ controls,
77
+ neck,
78
+ tip,
79
+ path:
80
+ `M ${format(start.x)} ${format(start.y)} ` +
81
+ `C ${format(c1.x)} ${format(c1.y)} ${format(c2.x)} ${format(c2.y)} ` +
82
+ `${format(neck.x)} ${format(neck.y)}`,
83
+ head: [
84
+ [tip.x, tip.y],
85
+ [tip.x - HEAD_HALF_WIDTH, tip.y - HEAD_LENGTH],
86
+ [tip.x + HEAD_HALF_WIDTH, tip.y - HEAD_LENGTH],
87
+ ]
88
+ .map(([x, y]) => `${format(x)},${format(y)}`)
89
+ .join(' '),
90
+ };
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // The overlay.
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export interface ArrowOverlay {
98
+ /** Draw the arrow from `card` to `target`, replacing any other. */
99
+ show(card: HTMLElement, target: HTMLElement): void;
100
+ /** Draw it again after the card or the occurrence moved. */
101
+ redraw(): void;
102
+ /**
103
+ * The pointer or focus reached `node`: on the card or the occurrence, the
104
+ * arrow stays; anywhere else, it fades a while later.
105
+ */
106
+ follow(node: EventTarget | null): void;
107
+ /** The pointer or focus left both: fade a while later, unless one is hovered or focused by then. */
108
+ release(): void;
109
+ /** Take the arrow away now. */
110
+ hide(): void;
111
+ /** The card the arrow runs from, if one is shown. */
112
+ readonly card: HTMLElement | null;
113
+ }
114
+
115
+ export interface ArrowOverlayOptions {
116
+ /** The card layer; the arrow mounts at its bottom. */
117
+ layer(): HTMLElement;
118
+ /** S5: how long the arrow stays after the card and the occurrence are left (ms). */
119
+ readonly holdMs: number;
120
+ /** How long it takes to fade out (ms); not at all under reduced motion. */
121
+ readonly fadeMs: number;
122
+ }
123
+
124
+ const SVG_NS = 'http://www.w3.org/2000/svg';
125
+
126
+ const reducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
127
+
128
+ const hovered = (el: Element) => el.matches(':hover');
129
+
130
+ const focused = (el: Element) =>
131
+ el.matches(':focus-visible') || el.querySelector(':focus-visible') !== null;
132
+
133
+ export function createArrowOverlay(options: ArrowOverlayOptions): ArrowOverlay {
134
+ let svg: SVGSVGElement | null = null;
135
+ let line: SVGPathElement | null = null;
136
+ let head: SVGPolygonElement | null = null;
137
+ let from: HTMLElement | null = null;
138
+ let to: HTMLElement | null = null;
139
+ let holdTimer: ReturnType<typeof setTimeout> | null = null;
140
+ let fadeTimer: ReturnType<typeof setTimeout> | null = null;
141
+
142
+ const clearTimers = () => {
143
+ if (holdTimer !== null) clearTimeout(holdTimer);
144
+ if (fadeTimer !== null) clearTimeout(fadeTimer);
145
+ holdTimer = null;
146
+ fadeTimer = null;
147
+ };
148
+
149
+ const mount = () => {
150
+ if (!svg) {
151
+ svg = document.createElementNS(SVG_NS, 'svg');
152
+ svg.classList.add('explico-card-arrow');
153
+ svg.setAttribute('aria-hidden', 'true');
154
+ // A zero-size box at the layer's origin: the drawing overflows it in
155
+ // page coordinates, and never takes a click.
156
+ svg.style.cssText =
157
+ 'position:absolute;left:0;top:0;width:1px;height:1px;overflow:visible;pointer-events:none;z-index:0';
158
+ line = document.createElementNS(SVG_NS, 'path');
159
+ line.classList.add('explico-card-arrow-line');
160
+ head = document.createElementNS(SVG_NS, 'polygon');
161
+ head.classList.add('explico-card-arrow-head');
162
+ svg.append(line, head);
163
+ }
164
+ const layer = options.layer();
165
+ if (svg.parentElement !== layer || layer.firstChild !== svg) layer.prepend(svg);
166
+ return svg;
167
+ };
168
+
169
+ const hide = () => {
170
+ clearTimers();
171
+ from = null;
172
+ to = null;
173
+ svg?.remove();
174
+ svg?.classList.remove('is-fading');
175
+ };
176
+
177
+ const draw = () => {
178
+ if (!from || !to) return;
179
+ const box = to.getClientRects()[0];
180
+ if (!from.isConnected || !to.isConnected || !box) {
181
+ hide();
182
+ return;
183
+ }
184
+ const layer = options.layer();
185
+ const origin = layer.getBoundingClientRect();
186
+ const shift = (rect: Box): Box => ({
187
+ left: rect.left - origin.left,
188
+ top: rect.top - origin.top,
189
+ right: rect.right - origin.left,
190
+ bottom: rect.bottom - origin.top,
191
+ });
192
+ const shape = arrowShape(shift(from.getBoundingClientRect()), shift(box));
193
+ mount();
194
+ line?.setAttribute('d', shape.path);
195
+ head?.setAttribute('points', shape.head);
196
+ if (svg) svg.dataset.cardArrow = from.dataset.cardKey ?? '';
197
+ };
198
+
199
+ const fade = () => {
200
+ holdTimer = null;
201
+ if (!from || !to) return;
202
+ // Still on the card or the occurrence: keep it until they are left again.
203
+ if (hovered(from) || hovered(to) || focused(from) || focused(to)) return;
204
+ if (reducedMotion() || !svg) {
205
+ hide();
206
+ return;
207
+ }
208
+ svg.classList.add('is-fading');
209
+ fadeTimer = setTimeout(hide, options.fadeMs);
210
+ };
211
+
212
+ const hold = () => {
213
+ clearTimers();
214
+ svg?.classList.remove('is-fading');
215
+ };
216
+
217
+ const release = () => {
218
+ if (!from || holdTimer !== null) return;
219
+ if (fadeTimer !== null) return; // already on its way out
220
+ holdTimer = setTimeout(fade, options.holdMs);
221
+ };
222
+
223
+ return {
224
+ show(card, target) {
225
+ hold();
226
+ from = card;
227
+ to = target;
228
+ draw();
229
+ },
230
+ redraw: draw,
231
+ follow(node) {
232
+ if (!from || !to) return;
233
+ const on = node instanceof Node && (from.contains(node) || to.contains(node));
234
+ if (on) hold();
235
+ else release();
236
+ },
237
+ release,
238
+ hide,
239
+ get card() {
240
+ return from;
241
+ },
242
+ };
243
+ }