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.
- package/CHANGELOG.md +1257 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +4 -1
- package/dist/compiler/logging.js +4 -1
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +3 -2
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +60 -31
- package/dist/reference/equations.mjs +83 -43
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +1 -0
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +45 -21
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +22 -6
- package/dist/reference/gate-math.js +42 -8
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +572 -37
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +4 -4
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +17 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +5 -3
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1152 -2
- package/docs/authoring/notation.md +576 -322
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +8 -2
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +572 -37
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +16 -31
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +17 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1467 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- 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
|
+
}
|