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
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
* once the pointer/focus leaves. See `docs/plans/collapsible-disclosure.md`
|
|
5
5
|
* Part 0.
|
|
6
6
|
*
|
|
7
|
-
* This is deliberately not
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* This is deliberately not an explanation card (`../card-manager.ts`): a
|
|
8
|
+
* peek has no pin state — its content is one click from being expanded for
|
|
9
|
+
* real — and takes no part in the cards' hover chain. It shares only their
|
|
10
|
+
* travel zone (rules C2 and C12 of `docs/plans/hover-card-consistency.md`,
|
|
11
|
+
* `../card-travel.ts`): after the pointer leaves the trigger toward the peek,
|
|
12
|
+
* the peek stays open while the pointer is on its way, another peek trigger
|
|
13
|
+
* crossed on the way is held back, and anywhere else the peek closes after
|
|
14
|
+
* the cards' grace.
|
|
15
15
|
*
|
|
16
16
|
* One peek panel is created lazily and shared across every trigger wired
|
|
17
17
|
* with `attachHoverPeek` — only one can be hovered at a time, so reusing a
|
|
18
18
|
* single element avoids paying DOM-churn cost per hover.
|
|
19
19
|
*/
|
|
20
|
+
import { CARD_CONSTANTS } from '../card-manager';
|
|
21
|
+
import { startTrip, type Point, type Trip } from '../card-travel';
|
|
20
22
|
|
|
21
23
|
export interface HoverPeekOptions {
|
|
22
24
|
/**
|
|
@@ -35,17 +37,23 @@ export interface HoverPeekOptions {
|
|
|
35
37
|
let panel: HTMLElement | null = null;
|
|
36
38
|
let scroller: HTMLElement | null = null;
|
|
37
39
|
let activeTrigger: HTMLElement | null = null;
|
|
38
|
-
let
|
|
40
|
+
let trip: Trip | null = null;
|
|
41
|
+
let graceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
39
42
|
let globalListenersReady = false;
|
|
43
|
+
/** The pointer's last position: where it was when it left a trigger (see `../card-manager.ts`). */
|
|
44
|
+
let lastPointer: Point | null = null;
|
|
45
|
+
/** Each wired trigger's `show`, so resting on one mid-trip can switch to it. */
|
|
46
|
+
const showFor = new WeakMap<HTMLElement, () => void>();
|
|
40
47
|
|
|
41
48
|
function cancelLeave(): void {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
trip?.end();
|
|
50
|
+
trip = null;
|
|
51
|
+
if (graceTimer !== null) clearTimeout(graceTimer);
|
|
52
|
+
graceTimer = null;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
function close(): void {
|
|
56
|
+
cancelLeave();
|
|
49
57
|
if (!panel) return;
|
|
50
58
|
panel.hidden = true;
|
|
51
59
|
scroller?.replaceChildren();
|
|
@@ -63,26 +71,70 @@ export function closeHoverPeek(trigger: HTMLElement): void {
|
|
|
63
71
|
if (activeTrigger === trigger) close();
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
function closeUnlessWanted(): void {
|
|
75
|
+
if (!activeTrigger || !panel) return;
|
|
76
|
+
// `:focus-visible`, not `=== document.activeElement` / `:focus`: the
|
|
77
|
+
// trigger is very often something like a `<summary>` that a *click* also
|
|
78
|
+
// focuses as a side effect (e.g. clicking it to toggle a `<details>`) — a
|
|
79
|
+
// plain focus check would then treat that leftover, non-keyboard focus as
|
|
80
|
+
// "still viewing it" forever, since nothing else ever takes focus to fire
|
|
81
|
+
// `blur`. `:focus-visible` is only true for focus a browser considers
|
|
82
|
+
// keyboard/keyboard-equivalent, so a mouse click doesn't leave the peek
|
|
83
|
+
// stuck open after the pointer leaves.
|
|
84
|
+
const stillActive =
|
|
85
|
+
activeTrigger.matches(':hover') ||
|
|
86
|
+
activeTrigger.matches(':focus-visible') ||
|
|
87
|
+
panel.matches(':hover') ||
|
|
88
|
+
panel.matches(':focus-within');
|
|
89
|
+
if (!stillActive) close();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The cards' grace: close after it unless the peek is wanted again by then. */
|
|
66
93
|
function scheduleLeave(): void {
|
|
67
94
|
cancelLeave();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
graceTimer = setTimeout(() => {
|
|
96
|
+
graceTimer = null;
|
|
97
|
+
closeUnlessWanted();
|
|
98
|
+
}, CARD_CONSTANTS.travelGraceMs);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function peekTriggerAt(point: Point): HTMLElement | null {
|
|
102
|
+
return (
|
|
103
|
+
document
|
|
104
|
+
.elementFromPoint(point.x, point.y)
|
|
105
|
+
?.closest<HTMLElement>('[data-hover-peek-wired="true"]') ?? null
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The pointer left `trigger` at `apex`, perhaps on its way to the peek. */
|
|
110
|
+
function travelFrom(trigger: HTMLElement, apex: Point): void {
|
|
111
|
+
cancelLeave();
|
|
112
|
+
let own: Trip | null = null;
|
|
113
|
+
const release = () => {
|
|
114
|
+
if (trip === own) trip = null;
|
|
115
|
+
};
|
|
116
|
+
own = startTrip(apex, {
|
|
117
|
+
graceMs: CARD_CONSTANTS.travelGraceMs,
|
|
118
|
+
restMs: CARD_CONSTANTS.travelRestMs,
|
|
119
|
+
tripMaxMs: CARD_CONSTANTS.travelTripMaxMs,
|
|
120
|
+
cardBox: () => (panel && !panel.hidden ? panel.getBoundingClientRect() : null),
|
|
121
|
+
triggerBox: () => (trigger.isConnected ? trigger.getBoundingClientRect() : null),
|
|
122
|
+
onExit: () => {
|
|
123
|
+
release();
|
|
124
|
+
closeUnlessWanted();
|
|
125
|
+
},
|
|
126
|
+
onLand: () => {
|
|
127
|
+
release();
|
|
128
|
+
scheduleLeave();
|
|
129
|
+
},
|
|
130
|
+
onRest: (point) => {
|
|
131
|
+
const other = peekTriggerAt(point);
|
|
132
|
+
if (!other || other === trigger) return;
|
|
133
|
+
cancelLeave();
|
|
134
|
+
showFor.get(other)?.();
|
|
135
|
+
},
|
|
85
136
|
});
|
|
137
|
+
trip = own.active ? own : null;
|
|
86
138
|
}
|
|
87
139
|
|
|
88
140
|
function position(trigger: HTMLElement, target: HTMLElement): void {
|
|
@@ -138,7 +190,18 @@ function ensurePanel(): { panel: HTMLElement; scroller: HTMLElement } {
|
|
|
138
190
|
document.body.append(panel);
|
|
139
191
|
|
|
140
192
|
panel.addEventListener('pointerenter', cancelLeave);
|
|
141
|
-
panel.addEventListener('pointerleave',
|
|
193
|
+
panel.addEventListener('pointerleave', (event) => {
|
|
194
|
+
if (event.pointerType === 'touch') return;
|
|
195
|
+
// Straight back onto the trigger is the trigger's own business.
|
|
196
|
+
if (
|
|
197
|
+
activeTrigger &&
|
|
198
|
+
event.relatedTarget instanceof Node &&
|
|
199
|
+
activeTrigger.contains(event.relatedTarget)
|
|
200
|
+
) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
scheduleLeave();
|
|
204
|
+
});
|
|
142
205
|
|
|
143
206
|
if (!globalListenersReady) {
|
|
144
207
|
globalListenersReady = true;
|
|
@@ -152,6 +215,15 @@ function ensurePanel(): { panel: HTMLElement; scroller: HTMLElement } {
|
|
|
152
215
|
const insidePanel = !!target && panel.contains(target);
|
|
153
216
|
if (!insideTrigger && !insidePanel) close();
|
|
154
217
|
});
|
|
218
|
+
document.addEventListener(
|
|
219
|
+
'pointermove',
|
|
220
|
+
(event) => {
|
|
221
|
+
if (event.pointerType === 'touch') return;
|
|
222
|
+
lastPointer = { x: event.clientX, y: event.clientY };
|
|
223
|
+
trip?.move(lastPointer);
|
|
224
|
+
},
|
|
225
|
+
{ passive: true },
|
|
226
|
+
);
|
|
155
227
|
}
|
|
156
228
|
|
|
157
229
|
return { panel, scroller };
|
|
@@ -174,29 +246,37 @@ export function attachHoverPeek(
|
|
|
174
246
|
|
|
175
247
|
const show = () => {
|
|
176
248
|
const { panel: el, scroller: scrollEl } = ensurePanel();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
249
|
+
cancelLeave();
|
|
250
|
+
if (activeTrigger === trigger) return;
|
|
181
251
|
activeTrigger = trigger;
|
|
182
252
|
el.setAttribute('aria-label', label);
|
|
183
253
|
el.style.setProperty('--hover-peek-max-height', `${maxHeightEm}em`);
|
|
184
254
|
scrollEl.replaceChildren(options.getContent());
|
|
185
255
|
position(trigger, el);
|
|
186
256
|
};
|
|
187
|
-
|
|
188
|
-
const leave = () => {
|
|
189
|
-
if (activeTrigger === trigger) scheduleLeave();
|
|
190
|
-
};
|
|
257
|
+
showFor.set(trigger, show);
|
|
191
258
|
|
|
192
259
|
trigger.addEventListener('pointerenter', (event) => {
|
|
193
260
|
if (event.pointerType === 'touch') return;
|
|
261
|
+
// Crossed on the way to another trigger's peek: held back.
|
|
262
|
+
if (
|
|
263
|
+
trip &&
|
|
264
|
+
activeTrigger !== trigger &&
|
|
265
|
+
trip.holds({ x: event.clientX, y: event.clientY })
|
|
266
|
+
) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
194
269
|
show();
|
|
195
270
|
});
|
|
196
271
|
trigger.addEventListener('pointerleave', (event) => {
|
|
197
|
-
if (event.pointerType === 'touch') return;
|
|
198
|
-
|
|
272
|
+
if (event.pointerType === 'touch' || activeTrigger !== trigger) return;
|
|
273
|
+
if (panel && event.relatedTarget instanceof Node && panel.contains(event.relatedTarget)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
travelFrom(trigger, lastPointer ?? { x: event.clientX, y: event.clientY });
|
|
199
277
|
});
|
|
200
278
|
trigger.addEventListener('focus', show);
|
|
201
|
-
trigger.addEventListener('blur',
|
|
279
|
+
trigger.addEventListener('blur', () => {
|
|
280
|
+
if (activeTrigger === trigger) scheduleLeave();
|
|
281
|
+
});
|
|
202
282
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared open/closed persistence for a native `<details>` disclosure.
|
|
3
3
|
*
|
|
4
|
-
* Every collapsible built on top of this (
|
|
4
|
+
* Every collapsible built on top of this (sections, the notation panel) wants
|
|
5
5
|
* the same three behaviors: restore the reader's last choice before the
|
|
6
6
|
* element paints, save reader-triggered native `toggle`s, and degrade to the
|
|
7
7
|
* author's `defaultOpen` when nothing is saved yet or storage is unavailable.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Storage goes through `PreferenceStore` only (never `localStorage`
|
|
12
12
|
* directly — `tests/unit/seams.test.ts` enforces that boundary). The key is
|
|
13
|
-
* scoped by `location.pathname` so the same
|
|
14
|
-
*
|
|
13
|
+
* scoped by `location.pathname` so the same `id` can repeat across pages
|
|
14
|
+
* without one page's saved state leaking into another's.
|
|
15
15
|
*
|
|
16
16
|
* **Wiring**: call this from a plain top-level `<script>` in the consuming
|
|
17
17
|
* Astro component — not from inside an `astro:page-load` listener — the same
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Print as shown.** A printed lesson keeps every reader-controlled
|
|
3
|
+
* disclosure in the state the reader left it on screen: an open section,
|
|
4
|
+
* memo, glossary, worked-example set, short note, or question prints its
|
|
5
|
+
* content; a closed one prints its title followed by a "(content collapsed —
|
|
6
|
+
* full text: <url>)" note, so a paper reader knows something was left out
|
|
7
|
+
* and where to read it. A figure or a diagram is not a disclosure: it always
|
|
8
|
+
* prints whole.
|
|
9
|
+
*
|
|
10
|
+
* Content hidden only to save screen space — overflowing table rows, the
|
|
11
|
+
* inactive tabs of an open worked-example set — was never the reader's
|
|
12
|
+
* choice to hide, and still prints in full (the global print stylesheet and
|
|
13
|
+
* `LessonExportActions.astro` handle those).
|
|
14
|
+
*
|
|
15
|
+
* A disclosure marked `data-print-omit` — a lab's tools (`Figure.astro`) —
|
|
16
|
+
* never prints, open or folded, and is left exactly as it is: no note, not
|
|
17
|
+
* opened for an export.
|
|
18
|
+
*
|
|
19
|
+
* Two callers share this module:
|
|
20
|
+
* - `LessonExportActions.astro`, the print controller a course gets with
|
|
21
|
+
* `lessonExports.print`: `'as-shown'` for its print button and the
|
|
22
|
+
* browser's own print, `'expanded'` for the Markdown export, which needs
|
|
23
|
+
* every word regardless of screen state.
|
|
24
|
+
* - `installPrintAsShownFallback`, for a course without that controller, so
|
|
25
|
+
* the browser's own print behaves the same way there.
|
|
26
|
+
*
|
|
27
|
+
* A closed native `<details>` never renders its body, whatever the author
|
|
28
|
+
* CSS says, so "as shown" mostly means *not* opening anything. What this
|
|
29
|
+
* does have to fix up is transient state: a section mid-hover-peek or
|
|
30
|
+
* mid-animation has `details.open === true` while reading as closed on
|
|
31
|
+
* screen, so its `data-state` — not `.open` — decides.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export type DisclosureExportMode = 'as-shown' | 'expanded';
|
|
35
|
+
|
|
36
|
+
export interface PrepareDisclosureOptions {
|
|
37
|
+
/** Absolute URL of this page, at `#anchorId` when one is given. */
|
|
38
|
+
noteHref(anchorId: string | undefined): string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PreparedDisclosures {
|
|
42
|
+
/** Put every disclosure back exactly as the reader left it and remove the notes. */
|
|
43
|
+
restore(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Drop the temporary marker that keeps disclosure persistence from saving
|
|
46
|
+
* the prepare/restore toggles. Native `toggle` events are queued, so call
|
|
47
|
+
* this one task after `restore()`, not in the same one.
|
|
48
|
+
*/
|
|
49
|
+
releaseMarkers(): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const COLLAPSED_NOTE_CLASS = 'explico-collapsed-note';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether a disclosure reads as open on screen. A component that animates
|
|
56
|
+
* its own open/close (a section) keeps `data-state` as the truth —
|
|
57
|
+
* `peeking` and a collapse still in flight both read as closed.
|
|
58
|
+
*/
|
|
59
|
+
export function isShownOpen(open: boolean, state: string | undefined): boolean {
|
|
60
|
+
return state === undefined ? open : state === 'open';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `https://host/path/#id` → `host/path/#id`: what a paper reader types. */
|
|
64
|
+
export function displayUrl(href: string): string {
|
|
65
|
+
return href.replace(/^https?:\/\//u, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `id` values React's `useId` generates are not stable across builds. */
|
|
69
|
+
function isStableId(id: string): boolean {
|
|
70
|
+
return /^[A-Za-z]/u.test(id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The permalink a note points at: the disclosure's own anchor if it has
|
|
75
|
+
* one, otherwise the nearest enclosing one (a question's knowledge-check
|
|
76
|
+
* section, say). A collapsible section's anchor is its heading's id.
|
|
77
|
+
*/
|
|
78
|
+
function anchorIdFor(element: Element): string | undefined {
|
|
79
|
+
for (
|
|
80
|
+
let node: Element | null = element;
|
|
81
|
+
node && node.tagName !== 'MAIN';
|
|
82
|
+
node = node.parentElement
|
|
83
|
+
) {
|
|
84
|
+
if (node.matches('details.sl-section-collapsible')) {
|
|
85
|
+
const heading = node.querySelector(':scope > summary > [id]');
|
|
86
|
+
if (heading && isStableId(heading.id)) return heading.id;
|
|
87
|
+
}
|
|
88
|
+
if (node.id && isStableId(node.id)) return node.id;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createNote(href: string): HTMLElement {
|
|
94
|
+
const note = document.createElement('div');
|
|
95
|
+
note.className = COLLAPSED_NOTE_CLASS;
|
|
96
|
+
const link = document.createElement('a');
|
|
97
|
+
link.href = href;
|
|
98
|
+
link.textContent = displayUrl(href);
|
|
99
|
+
note.append('(content collapsed — full text: ', link, ')');
|
|
100
|
+
return note;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type SavedDetails = {
|
|
104
|
+
readonly element: HTMLDetailsElement;
|
|
105
|
+
readonly open: boolean;
|
|
106
|
+
readonly state: string | undefined;
|
|
107
|
+
readonly style: string | null;
|
|
108
|
+
readonly body: HTMLElement | null;
|
|
109
|
+
readonly bodyStyle: string | null;
|
|
110
|
+
readonly temporaryMarker: string | null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export function prepareDisclosures(
|
|
114
|
+
mode: DisclosureExportMode,
|
|
115
|
+
options: PrepareDisclosureOptions,
|
|
116
|
+
): PreparedDisclosures {
|
|
117
|
+
const main = document.querySelector('main');
|
|
118
|
+
const allDetails = main
|
|
119
|
+
? Array.from(main.querySelectorAll<HTMLDetailsElement>('details:not([data-print-omit])'))
|
|
120
|
+
: [];
|
|
121
|
+
|
|
122
|
+
const saved: SavedDetails[] = allDetails.map((element) => {
|
|
123
|
+
const body = element.querySelector<HTMLElement>(':scope > .sl-section-body');
|
|
124
|
+
const record: SavedDetails = {
|
|
125
|
+
element,
|
|
126
|
+
open: element.open,
|
|
127
|
+
state: element.dataset.state,
|
|
128
|
+
style: element.getAttribute('style'),
|
|
129
|
+
body,
|
|
130
|
+
bodyStyle: body?.getAttribute('style') ?? null,
|
|
131
|
+
temporaryMarker: element.getAttribute('data-explico-temporary-open'),
|
|
132
|
+
};
|
|
133
|
+
const open =
|
|
134
|
+
mode === 'expanded' || isShownOpen(element.open, element.dataset.state);
|
|
135
|
+
element.dataset.explicoTemporaryOpen = mode;
|
|
136
|
+
element.open = open;
|
|
137
|
+
if (element.dataset.state !== undefined) {
|
|
138
|
+
element.dataset.state = open ? 'open' : 'closed';
|
|
139
|
+
}
|
|
140
|
+
// A peek or an open/close animation leaves an inline `max-height` (and a
|
|
141
|
+
// clipping `overflow`) behind; the printed box needs its natural height.
|
|
142
|
+
body?.style.removeProperty('max-height');
|
|
143
|
+
body?.style.removeProperty('overflow');
|
|
144
|
+
body?.style.removeProperty('transition');
|
|
145
|
+
return record;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const notes: HTMLElement[] = [];
|
|
149
|
+
if (main && mode === 'as-shown') {
|
|
150
|
+
// Only the outermost closed disclosure gets a note: anything nested in
|
|
151
|
+
// it is already out of the printed page along with it.
|
|
152
|
+
const insideClosed = (element: Element) =>
|
|
153
|
+
!!element.parentElement?.closest('details:not([open])');
|
|
154
|
+
|
|
155
|
+
for (const details of main.querySelectorAll('details:not([open]):not([data-print-omit])')) {
|
|
156
|
+
if (insideClosed(details)) continue;
|
|
157
|
+
const note = createNote(options.noteHref(anchorIdFor(details)));
|
|
158
|
+
details.after(note);
|
|
159
|
+
notes.push(note);
|
|
160
|
+
}
|
|
161
|
+
// Disclosures built on `hidden` rather than `<details>`.
|
|
162
|
+
for (const element of main.querySelectorAll(
|
|
163
|
+
':is(.short-note, .assessment-panel)[hidden]',
|
|
164
|
+
)) {
|
|
165
|
+
if (insideClosed(element)) continue;
|
|
166
|
+
const note = createNote(options.noteHref(anchorIdFor(element)));
|
|
167
|
+
element.before(note);
|
|
168
|
+
notes.push(note);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
restore() {
|
|
174
|
+
for (const note of notes) note.remove();
|
|
175
|
+
for (const state of saved) {
|
|
176
|
+
state.element.open = state.open;
|
|
177
|
+
if (state.state === undefined) delete state.element.dataset.state;
|
|
178
|
+
else state.element.dataset.state = state.state;
|
|
179
|
+
if (state.style === null) state.element.removeAttribute('style');
|
|
180
|
+
else state.element.setAttribute('style', state.style);
|
|
181
|
+
if (state.body) {
|
|
182
|
+
if (state.bodyStyle === null) state.body.removeAttribute('style');
|
|
183
|
+
else state.body.setAttribute('style', state.bodyStyle);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
releaseMarkers() {
|
|
188
|
+
for (const state of saved) {
|
|
189
|
+
if (state.temporaryMarker === null) {
|
|
190
|
+
state.element.removeAttribute('data-explico-temporary-open');
|
|
191
|
+
} else {
|
|
192
|
+
state.element.setAttribute(
|
|
193
|
+
'data-explico-temporary-open',
|
|
194
|
+
state.temporaryMarker,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** This page's own URL (no canonical rewrite available) at `#anchorId`. */
|
|
203
|
+
function pageHref(anchorId: string | undefined): string {
|
|
204
|
+
const url = new URL(location.href);
|
|
205
|
+
url.hash = anchorId ? `#${anchorId}` : '';
|
|
206
|
+
return url.href;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Print-as-shown for the browser's own print on a page without the print
|
|
211
|
+
* controller. Idempotent across every component that calls it — a flag on
|
|
212
|
+
* `<html>` rather than module state, since separately bundled scripts may
|
|
213
|
+
* each carry their own copy of this module.
|
|
214
|
+
*/
|
|
215
|
+
export function installPrintAsShownFallback(): void {
|
|
216
|
+
const root = document.documentElement;
|
|
217
|
+
if (root.hasAttribute('data-explico-print-as-shown')) return;
|
|
218
|
+
root.setAttribute('data-explico-print-as-shown', '');
|
|
219
|
+
|
|
220
|
+
let prepared: PreparedDisclosures | null = null;
|
|
221
|
+
let pendingRelease: (() => void) | null = null;
|
|
222
|
+
const hasController = () =>
|
|
223
|
+
!!document.querySelector('[data-explico-print-controller]');
|
|
224
|
+
|
|
225
|
+
const enter = () => {
|
|
226
|
+
if (prepared || hasController()) return;
|
|
227
|
+
pendingRelease?.();
|
|
228
|
+
prepared = prepareDisclosures('as-shown', { noteHref: pageHref });
|
|
229
|
+
};
|
|
230
|
+
const leave = () => {
|
|
231
|
+
const done = prepared;
|
|
232
|
+
if (!done) return;
|
|
233
|
+
prepared = null;
|
|
234
|
+
done.restore();
|
|
235
|
+
const release = () => {
|
|
236
|
+
if (pendingRelease !== release) return;
|
|
237
|
+
done.releaseMarkers();
|
|
238
|
+
pendingRelease = null;
|
|
239
|
+
};
|
|
240
|
+
pendingRelease = release;
|
|
241
|
+
window.setTimeout(release, 0);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const printMedia = window.matchMedia('print');
|
|
245
|
+
printMedia.addEventListener('change', (event) => {
|
|
246
|
+
if (event.matches) enter();
|
|
247
|
+
else leave();
|
|
248
|
+
});
|
|
249
|
+
window.addEventListener('beforeprint', enter);
|
|
250
|
+
window.addEventListener('afterprint', leave);
|
|
251
|
+
if (printMedia.matches) enter();
|
|
252
|
+
}
|
|
@@ -28,7 +28,9 @@ if (typeof label !== 'string' || !label.trim()) {
|
|
|
28
28
|
margin-block-start: 0;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/* `:global`: the last child is slotted MDX content, which a scoped
|
|
32
|
+
selector never matches — a closing display equation kept its margin. */
|
|
33
|
+
.compact-example > :global(:last-child) {
|
|
32
34
|
margin-block-end: 0;
|
|
33
35
|
}
|
|
34
36
|
</style>
|