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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* Progressive enhancement for `[[eq-key]]` / `[[tab-key]]` / `[[fig-key]]` /
|
|
4
|
-
* `[[dia-key]]`
|
|
4
|
+
* `[[dia-key]]` / `[[sec-slug]]` / `[[ex-key]]` / `[[check-key]]`
|
|
5
5
|
* references in prose (F4, extended F-tables-figures) — one hover/pin/jump
|
|
6
|
-
* engine shared by
|
|
6
|
+
* engine shared by the four keyed floats and sections. This generalizes the original
|
|
7
7
|
* equation-only `EquationRefLayer`; the positioning, mutual-exclusion, and
|
|
8
8
|
* pin/jump/Escape mechanics are unchanged, parameterized over `KIND_CONFIG`
|
|
9
9
|
* below so a hover on any of the three trigger classes is handled the same
|
|
@@ -17,9 +17,19 @@
|
|
|
17
17
|
* it points to:
|
|
18
18
|
*
|
|
19
19
|
* - if the referenced float is on the current page and already fully on
|
|
20
|
-
* screen, it is highlighted in place instead of opening a card
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* screen, it is highlighted in place instead of opening a card, and a
|
|
21
|
+
* click navigates to it natively (highlight mode);
|
|
22
|
+
* - otherwise a card floats near the reference showing its number,
|
|
23
|
+
* caption (table/figure), content, and a "Go to …" link, whose arrow
|
|
24
|
+
* says where it lands (`../card-jump.ts`): ↑ / ↓ on this page, → with the
|
|
25
|
+
* lesson's name on another one. A click on the
|
|
26
|
+
* reference pins the card, a second one closes it (the card manager's
|
|
27
|
+
* contract, `../card-manager.ts`); the card's own links navigate.
|
|
28
|
+
*
|
|
29
|
+
* A float with a short caption — an equation's `\caption{…}`, the `short`
|
|
30
|
+
* prop of a table, figure or diagram — heads the card with it
|
|
31
|
+
* (docs/plans/titles-and-containers.md Part 3), read off the target's
|
|
32
|
+
* `data-‹kind›-short`.
|
|
23
33
|
*
|
|
24
34
|
* What the card's content shows differs by kind:
|
|
25
35
|
* - equation: the KaTeX formula, cloned from the target (unchanged from the
|
|
@@ -30,27 +40,36 @@
|
|
|
30
40
|
* `previewable={false}` shows its caption and number only, since a
|
|
31
41
|
* cloned copy would be an inert, non-hydrated, id-colliding duplicate,
|
|
32
42
|
* not a working preview.
|
|
43
|
+
* - section: the heading's title and the section's first paragraph, with
|
|
44
|
+
* `§ 1.1.2` as the jump link when the heading shows a number.
|
|
45
|
+
* - worked-example set and knowledge check (docs/plans/titles-and-containers.md
|
|
46
|
+
* Part 6): the block's topical title — not its badge — and one line of
|
|
47
|
+
* what it holds (the examples' names, the number of questions), with
|
|
48
|
+
* `Example 1.1.1` / `Knowledge check 1.1.1` as the jump link when the
|
|
49
|
+
* heading shows a number.
|
|
33
50
|
*
|
|
34
51
|
* Cross-page references (`[[lesson#eq-key]]` etc.) get the same card: their
|
|
35
|
-
* `href` is a real path to another lesson page, so `
|
|
36
|
-
* and parses that page once (cached), pulls the
|
|
37
|
-
* node out of it, and fills the card exactly as
|
|
38
|
-
* documents via `importNode`).
|
|
39
|
-
*
|
|
40
|
-
*
|
|
52
|
+
* `href` is a real path to another lesson page, so the card's `render`
|
|
53
|
+
* fetches and parses that page once (cached), pulls the
|
|
54
|
+
* `#eq-…`/`#tab-…`/`#fig-…` node out of it, and fills the card exactly as
|
|
55
|
+
* above (cloning across documents via `importNode`). There is nothing in this
|
|
56
|
+
* document to highlight in place, so the card always opens; its link goes to
|
|
57
|
+
* the other page, where `KeyedFloatEnhancer.astro` handles the landing. A
|
|
58
|
+
* standalone export has no other pages to fetch: there a cross-page
|
|
59
|
+
* reference stays a plain link.
|
|
41
60
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* hand-rolled here rather than built on `createHoverPanel`.
|
|
46
|
-
* `claimActiveSurface`/`releaseActiveSurface` are still reused so an open
|
|
47
|
-
* float card and an open citation or notation popup remain mutually
|
|
48
|
-
* exclusive.
|
|
61
|
+
* One card per referenced float, several at once; the card manager places
|
|
62
|
+
* them clear of every other open card, and a symbol inside a card opens its
|
|
63
|
+
* notation card as a child of it.
|
|
49
64
|
*/
|
|
50
65
|
import HoverPanel from '../HoverPanel.astro';
|
|
51
66
|
---
|
|
52
67
|
|
|
53
68
|
<HoverPanel id="float-ref-panel" label="Reference preview" data-float-ref-panel>
|
|
69
|
+
<p class="float-ref-panel-title" slot="heading">
|
|
70
|
+
<span class="float-ref-panel-kind" data-panel-kind></span>
|
|
71
|
+
<span class="float-ref-panel-heading" data-panel-heading hidden></span>
|
|
72
|
+
</p>
|
|
54
73
|
<div class="float-ref-panel-body">
|
|
55
74
|
<div class="float-ref-panel-main">
|
|
56
75
|
<div class="float-ref-panel-content" data-panel-content></div>
|
|
@@ -58,16 +77,15 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
58
77
|
</div>
|
|
59
78
|
<p class="float-ref-panel-caption" data-panel-caption></p>
|
|
60
79
|
<a class="float-ref-panel-jump" data-panel-jump
|
|
61
|
-
>
|
|
62
|
-
><span aria-hidden="true">
|
|
80
|
+
>Go to <span data-panel-jump-label></span
|
|
81
|
+
><span aria-hidden="true" data-jump-arrow> →</span></a
|
|
63
82
|
>
|
|
64
83
|
</div>
|
|
65
84
|
</HoverPanel>
|
|
66
85
|
|
|
67
86
|
<script>
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
const GROUP = 'float-ref';
|
|
87
|
+
import { cloneCardShell, isKept, registerCardSource } from '../card-manager';
|
|
88
|
+
import { wireJumpArrow } from '../card-jump';
|
|
71
89
|
|
|
72
90
|
interface KindConfig {
|
|
73
91
|
readonly triggerClass: string;
|
|
@@ -82,6 +100,10 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
82
100
|
readonly revealOnPin: boolean;
|
|
83
101
|
fillContent(target: HTMLElement, contentEl: HTMLElement): void;
|
|
84
102
|
captionFor(target: HTMLElement): string;
|
|
103
|
+
/** The target's `data-*` key holding its short caption, if the kind has one. */
|
|
104
|
+
readonly shortDataKey?: string;
|
|
105
|
+
/** The jump link's text, when not `(n)` / `‹Kind› n`; empty hides it. */
|
|
106
|
+
numberLabel?(number: string): string;
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
// `importNode`, not `cloneNode`: `target` may come from a *different*
|
|
@@ -154,22 +176,16 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
154
176
|
.forEach((button) => button.remove());
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
let
|
|
179
|
+
let cloneSerial = 0;
|
|
158
180
|
|
|
159
181
|
/**
|
|
160
|
-
* A
|
|
161
|
-
*
|
|
162
|
-
*
|
|
182
|
+
* A picture's copy keeps its SVG fragment references (`url(#marker-id)`, a
|
|
183
|
+
* clip path) working without stealing the original's: prefix every cloned
|
|
184
|
+
* id and rewrite the references to it, so neither the card nor the page
|
|
185
|
+
* resolves `#id` to the other's element.
|
|
163
186
|
*/
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
if (!clone) return null;
|
|
167
|
-
clone.removeAttribute('id');
|
|
168
|
-
clone.removeAttribute('tabindex');
|
|
169
|
-
clone.removeAttribute('aria-labelledby');
|
|
170
|
-
clone.setAttribute('aria-label', 'Diagram preview');
|
|
171
|
-
|
|
172
|
-
const prefix = `float-ref-diagram-${++diagramCloneSerial}-`;
|
|
187
|
+
function isolateIds(clone: HTMLElement, kind: string): void {
|
|
188
|
+
const prefix = `float-ref-${kind}-${++cloneSerial}-`;
|
|
173
189
|
const ids = new Map<string, string>();
|
|
174
190
|
for (const element of clone.querySelectorAll<HTMLElement>('[id]')) {
|
|
175
191
|
const oldId = element.id;
|
|
@@ -200,10 +216,101 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
200
216
|
if (rewritten !== value) element.setAttribute(attribute, rewritten);
|
|
201
217
|
}
|
|
202
218
|
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cloneDiagramBody(target: HTMLElement): HTMLElement | null {
|
|
222
|
+
const clone = cloneBody(target, 'diagram-scroll');
|
|
223
|
+
if (!clone) return null;
|
|
224
|
+
clone.removeAttribute('id');
|
|
225
|
+
clone.removeAttribute('tabindex');
|
|
226
|
+
clone.removeAttribute('aria-labelledby');
|
|
227
|
+
clone.setAttribute('aria-label', 'Diagram preview');
|
|
228
|
+
isolateIds(clone, 'diagram');
|
|
229
|
+
return clone;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A figure's picture, as a still copy (docs/plans/figures-and-short-captions.md,
|
|
234
|
+
* decision 12). The body is the picture alone — a lab's tools sit in a tray
|
|
235
|
+
* outside it. A hydrated island in it (a lab's plot) becomes plain markup,
|
|
236
|
+
* so it never hydrates a second time inside the card: its plot is copied as
|
|
237
|
+
* drawn. A picture with nothing drawn yet — a plot the browser has not
|
|
238
|
+
* drawn, or one on a fetched page, where only the server markup exists —
|
|
239
|
+
* gives no copy, and the card shows the caption alone.
|
|
240
|
+
*/
|
|
241
|
+
function cloneFigureBody(target: HTMLElement): HTMLElement | null {
|
|
242
|
+
const clone = cloneBody(target, 'keyed-figure__body');
|
|
243
|
+
if (!clone) return null;
|
|
244
|
+
const islands = Array.from(clone.querySelectorAll('astro-island'));
|
|
245
|
+
for (const island of islands) {
|
|
246
|
+
const still = document.createElement('div');
|
|
247
|
+
still.className = 'float-ref-island';
|
|
248
|
+
still.append(...island.childNodes);
|
|
249
|
+
island.replaceWith(still);
|
|
250
|
+
}
|
|
251
|
+
clone.querySelectorAll('script, template, astro-slot').forEach((node) => node.remove());
|
|
252
|
+
const drawn = clone.querySelector('svg, img, picture, canvas, video, table');
|
|
253
|
+
if (islands.length > 0 && !drawn) return null;
|
|
254
|
+
for (const control of clone.querySelectorAll<HTMLElement>('input, select, textarea, button')) {
|
|
255
|
+
control.setAttribute('tabindex', '-1');
|
|
256
|
+
control.setAttribute('disabled', '');
|
|
257
|
+
}
|
|
258
|
+
isolateIds(clone, 'figure');
|
|
203
259
|
return clone;
|
|
204
260
|
}
|
|
205
261
|
|
|
206
|
-
|
|
262
|
+
/** A copy with no ids, so a preview never collides with the page. */
|
|
263
|
+
function importWithoutIds(node: Node): Node {
|
|
264
|
+
const clone = document.importNode(node, true);
|
|
265
|
+
if (clone instanceof Element) {
|
|
266
|
+
clone.removeAttribute('id');
|
|
267
|
+
clone.querySelectorAll('[id]').forEach((element) => element.removeAttribute('id'));
|
|
268
|
+
}
|
|
269
|
+
return clone;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The first paragraph of a heading's section. On this page a collapsible
|
|
274
|
+
* section's body sits in its `<details>` (`CollapsibleSections.astro`); on
|
|
275
|
+
* a fetched page, or an ordinary section, it is the paragraphs after the
|
|
276
|
+
* heading's wrapper, up to the next heading.
|
|
277
|
+
*/
|
|
278
|
+
function sectionLead(heading: HTMLElement): HTMLElement | null {
|
|
279
|
+
const body = heading
|
|
280
|
+
.closest('summary')
|
|
281
|
+
?.parentElement?.querySelector<HTMLElement>(':scope > .sl-section-body');
|
|
282
|
+
if (body) return body.querySelector<HTMLElement>(':scope > p');
|
|
283
|
+
let node = (heading.closest('.sl-heading-wrapper') ?? heading).nextElementSibling;
|
|
284
|
+
while (node && !node.matches('.sl-heading-wrapper, h1, h2, h3, h4, h5, h6')) {
|
|
285
|
+
if (node.tagName === 'P') return node as HTMLElement;
|
|
286
|
+
node = node.nextElementSibling;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* A knowledge check's or an example set's heading (`BlockHeading.astro`):
|
|
293
|
+
* its topical title — not the badge or the number, which the card's jump
|
|
294
|
+
* link already names — then `detail`, one line of what the block holds.
|
|
295
|
+
*/
|
|
296
|
+
function fillBlockPreview(target: HTMLElement, contentEl: HTMLElement, detail: string) {
|
|
297
|
+
const title = document.createElement('p');
|
|
298
|
+
title.className = 'float-ref-panel-section-title';
|
|
299
|
+
const source = target.querySelector('.block-heading__title') ?? target;
|
|
300
|
+
for (const child of source.childNodes) title.append(importWithoutIds(child));
|
|
301
|
+
const lead = document.createElement('p');
|
|
302
|
+
lead.className = 'float-ref-panel-section-lead';
|
|
303
|
+
lead.textContent = detail;
|
|
304
|
+
contentEl.replaceChildren(title, ...(detail ? [lead] : []));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const plural = (count: number, noun: string) =>
|
|
308
|
+
`${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
309
|
+
|
|
310
|
+
const KINDS: Record<
|
|
311
|
+
'equation' | 'table' | 'figure' | 'diagram' | 'section' | 'example' | 'check',
|
|
312
|
+
KindConfig
|
|
313
|
+
> = {
|
|
207
314
|
equation: {
|
|
208
315
|
triggerClass: 'equation-ref',
|
|
209
316
|
hrefPrefix: '#eq-',
|
|
@@ -211,6 +318,7 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
211
318
|
numberDataKey: 'eqNumber',
|
|
212
319
|
highlightClass: 'eq-ref-highlight',
|
|
213
320
|
jumpLabel: 'equation',
|
|
321
|
+
shortDataKey: 'eqShort',
|
|
214
322
|
revealOnPin: true,
|
|
215
323
|
fillContent(target, contentEl) {
|
|
216
324
|
contentEl.replaceChildren(cloneEquationFormula(target));
|
|
@@ -226,6 +334,7 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
226
334
|
numberDataKey: 'tabNumber',
|
|
227
335
|
highlightClass: 'tab-ref-highlight',
|
|
228
336
|
jumpLabel: 'table',
|
|
337
|
+
shortDataKey: 'tabShort',
|
|
229
338
|
revealOnPin: false,
|
|
230
339
|
fillContent(target, contentEl) {
|
|
231
340
|
const clone = cloneBody(target, 'keyed-table__body');
|
|
@@ -245,10 +354,11 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
245
354
|
numberDataKey: 'figNumber',
|
|
246
355
|
highlightClass: 'fig-ref-highlight',
|
|
247
356
|
jumpLabel: 'figure',
|
|
357
|
+
shortDataKey: 'figShort',
|
|
248
358
|
revealOnPin: false,
|
|
249
359
|
fillContent(target, contentEl) {
|
|
250
360
|
const previewable = target.dataset.figPreviewable !== 'false';
|
|
251
|
-
const clone = previewable ?
|
|
361
|
+
const clone = previewable ? cloneFigureBody(target) : null;
|
|
252
362
|
contentEl.replaceChildren(...(clone ? [clone] : []));
|
|
253
363
|
},
|
|
254
364
|
captionFor(target) {
|
|
@@ -264,6 +374,7 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
264
374
|
numberDataKey: 'diaNumber',
|
|
265
375
|
highlightClass: 'dia-ref-highlight',
|
|
266
376
|
jumpLabel: 'diagram',
|
|
377
|
+
shortDataKey: 'diaShort',
|
|
267
378
|
revealOnPin: false,
|
|
268
379
|
fillContent(target, contentEl) {
|
|
269
380
|
const clone = cloneDiagramBody(target);
|
|
@@ -273,6 +384,81 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
273
384
|
return target.querySelector('.diagram__caption-text')?.textContent ?? '';
|
|
274
385
|
},
|
|
275
386
|
},
|
|
387
|
+
section: {
|
|
388
|
+
triggerClass: 'section-ref',
|
|
389
|
+
// A section's anchor is its heading's slug, with no prefix of its own;
|
|
390
|
+
// only `a.section-ref` links are ever considered here.
|
|
391
|
+
hrefPrefix: '#',
|
|
392
|
+
numberClass: '',
|
|
393
|
+
numberDataKey: 'secNumber',
|
|
394
|
+
highlightClass: 'sec-ref-highlight',
|
|
395
|
+
jumpLabel: 'section',
|
|
396
|
+
revealOnPin: false,
|
|
397
|
+
fillContent(target, contentEl) {
|
|
398
|
+
const title = document.createElement('p');
|
|
399
|
+
title.className = 'float-ref-panel-section-title';
|
|
400
|
+
// The heading's own nodes, so a title with math keeps it rendered.
|
|
401
|
+
for (const child of target.childNodes) title.append(importWithoutIds(child));
|
|
402
|
+
const lead = sectionLead(target);
|
|
403
|
+
const leadClone = lead ? (importWithoutIds(lead) as HTMLElement) : null;
|
|
404
|
+
leadClone?.classList.add('float-ref-panel-section-lead');
|
|
405
|
+
contentEl.replaceChildren(title, ...(leadClone ? [leadClone] : []));
|
|
406
|
+
},
|
|
407
|
+
captionFor() {
|
|
408
|
+
return '';
|
|
409
|
+
},
|
|
410
|
+
numberLabel(number) {
|
|
411
|
+
return number ? `§\u00a0${number}` : '';
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
example: {
|
|
415
|
+
triggerClass: 'example-ref',
|
|
416
|
+
hrefPrefix: '#ex-',
|
|
417
|
+
numberClass: '',
|
|
418
|
+
numberDataKey: 'exNumber',
|
|
419
|
+
highlightClass: 'sec-ref-highlight',
|
|
420
|
+
jumpLabel: 'example',
|
|
421
|
+
revealOnPin: false,
|
|
422
|
+
fillContent(target, contentEl) {
|
|
423
|
+
// The examples' own names, as the set's tabs show them.
|
|
424
|
+
const labels = Array.from(
|
|
425
|
+
target.closest('compact-examples')?.querySelectorAll<HTMLElement>(
|
|
426
|
+
'[data-example-label]',
|
|
427
|
+
) ?? [],
|
|
428
|
+
(panel) => panel.dataset.exampleLabel ?? '',
|
|
429
|
+
).filter(Boolean);
|
|
430
|
+
fillBlockPreview(target, contentEl, labels.join(' · '));
|
|
431
|
+
},
|
|
432
|
+
captionFor() {
|
|
433
|
+
return '';
|
|
434
|
+
},
|
|
435
|
+
numberLabel(number) {
|
|
436
|
+
return number ? `Example\u00a0${number}` : '';
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
check: {
|
|
440
|
+
triggerClass: 'knowledge-check-ref',
|
|
441
|
+
hrefPrefix: '#check-',
|
|
442
|
+
numberClass: '',
|
|
443
|
+
numberDataKey: 'checkNumber',
|
|
444
|
+
highlightClass: 'sec-ref-highlight',
|
|
445
|
+
jumpLabel: 'knowledge check',
|
|
446
|
+
revealOnPin: false,
|
|
447
|
+
fillContent(target, contentEl) {
|
|
448
|
+
// The runner is labelled by the check's heading
|
|
449
|
+
// (`AssessmentRunner.tsx`), wherever the section has moved it.
|
|
450
|
+
const questions = target.ownerDocument
|
|
451
|
+
.querySelector(`.assessment-shell[aria-labelledby="${CSS.escape(target.id)}"]`)
|
|
452
|
+
?.querySelectorAll('.assessment-item').length;
|
|
453
|
+
fillBlockPreview(target, contentEl, questions ? plural(questions, 'question') : '');
|
|
454
|
+
},
|
|
455
|
+
captionFor() {
|
|
456
|
+
return '';
|
|
457
|
+
},
|
|
458
|
+
numberLabel(number) {
|
|
459
|
+
return number ? `Knowledge check\u00a0${number}` : '';
|
|
460
|
+
},
|
|
461
|
+
},
|
|
276
462
|
};
|
|
277
463
|
|
|
278
464
|
const TRIGGER_SELECTOR = Object.values(KINDS)
|
|
@@ -351,57 +537,34 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
351
537
|
if (link) document.head.appendChild(document.importNode(link, true));
|
|
352
538
|
}
|
|
353
539
|
|
|
540
|
+
const capitalized = (text: string) =>
|
|
541
|
+
`${text.charAt(0).toUpperCase()}${text.slice(1)}`;
|
|
542
|
+
|
|
543
|
+
/** The number link's text: `(2.1)` for an equation, else `Table 2.1`, `§ 1.1.2`, …; empty hides it. */
|
|
544
|
+
function numberText(kind: KindConfig, number: string): string {
|
|
545
|
+
if (kind.numberLabel) return kind.numberLabel(number);
|
|
546
|
+
return kind.jumpLabel === 'equation'
|
|
547
|
+
? `(${number})`
|
|
548
|
+
: `${capitalized(kind.jumpLabel)} ${number}`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** What the card's "Go to …" link names (C15): `equation (1.4)`, `table 2.1`, `the section`. */
|
|
552
|
+
function goToLabel(kind: KindConfig, number: string): string {
|
|
553
|
+
if (!number) return `the ${kind.jumpLabel}`;
|
|
554
|
+
return kind.jumpLabel === 'equation'
|
|
555
|
+
? `equation (${number})`
|
|
556
|
+
: `${kind.jumpLabel} ${number}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
354
559
|
function setup() {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// `position: fixed` sibling with no z-index of its own, and CSS still
|
|
362
|
-
// paints any positioned box ahead of a plain in-flow one regardless of
|
|
363
|
-
// z-index or DOM order. No z-index set here could ever win against that
|
|
364
|
-
// from inside `.main-pane`, so the panel is allowed to cover the rail
|
|
365
|
-
// (per its own design) by moving it out to `<body>` instead — the same
|
|
366
|
-
// fix the notation layer's popups already use for this exact trap (see
|
|
367
|
-
// `PopupStackConfig.container` in `popup-stack.ts`). `position: fixed`
|
|
368
|
-
// means the DOM parent doesn't affect its layout.
|
|
369
|
-
if (panel.parentElement !== document.body) {
|
|
370
|
-
document.body.appendChild(panel);
|
|
560
|
+
const template = document.getElementById('float-ref-panel');
|
|
561
|
+
if (
|
|
562
|
+
!(template instanceof HTMLTemplateElement) ||
|
|
563
|
+
template.dataset.cardSourceReady === 'true'
|
|
564
|
+
) {
|
|
565
|
+
return;
|
|
371
566
|
}
|
|
372
|
-
|
|
373
|
-
const refs = Array.from(
|
|
374
|
-
document.querySelectorAll<HTMLAnchorElement>(
|
|
375
|
-
Object.values(KINDS)
|
|
376
|
-
.map((kind) => `.sl-markdown-content a.${kind.triggerClass}`)
|
|
377
|
-
.join(', '),
|
|
378
|
-
),
|
|
379
|
-
).filter((a) => {
|
|
380
|
-
const kind = kindOf(a);
|
|
381
|
-
if (!kind) return false;
|
|
382
|
-
// Same-page (`#eq-…`) or a resolvable cross-page (`/lesson/#eq-…`) ref.
|
|
383
|
-
return (
|
|
384
|
-
(a.getAttribute('href') ?? '').startsWith(kind.hrefPrefix) ||
|
|
385
|
-
crossPageHref(a, kind) !== null
|
|
386
|
-
);
|
|
387
|
-
});
|
|
388
|
-
if (refs.length === 0) return;
|
|
389
|
-
panel.dataset.floatRefReady = 'true';
|
|
390
|
-
|
|
391
|
-
const refSet = new Set<HTMLElement>(refs);
|
|
392
|
-
const numberEl = panel.querySelector<HTMLAnchorElement>('[data-panel-number]')!;
|
|
393
|
-
const contentEl = panel.querySelector<HTMLElement>('[data-panel-content]')!;
|
|
394
|
-
const captionEl = panel.querySelector<HTMLElement>('[data-panel-caption]')!;
|
|
395
|
-
const jumpEl = panel.querySelector<HTMLAnchorElement>('[data-panel-jump]')!;
|
|
396
|
-
const jumpLabelEl = panel.querySelector<HTMLElement>('[data-panel-jump-label]')!;
|
|
397
|
-
const closeButton = panel.querySelector<HTMLElement>('[data-hover-panel-close]');
|
|
398
|
-
|
|
399
|
-
let activeTrigger: HTMLElement | null = null;
|
|
400
|
-
let activeTarget: HTMLElement | null = null;
|
|
401
|
-
let activeKind: KindConfig | null = null;
|
|
402
|
-
let mode: 'highlight' | 'panel' | null = null;
|
|
403
|
-
let pinned = false;
|
|
404
|
-
let leaveTimer: number | null = null;
|
|
567
|
+
const shell: HTMLTemplateElement = template;
|
|
405
568
|
|
|
406
569
|
const targetFor = (trigger: HTMLElement, kind: KindConfig) => {
|
|
407
570
|
const href = trigger.getAttribute('href') ?? '';
|
|
@@ -410,7 +573,51 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
410
573
|
: null;
|
|
411
574
|
};
|
|
412
575
|
|
|
576
|
+
// A reference this layer can preview: same-page with its target on this
|
|
577
|
+
// page, or a cross-page one. A card cloned from another page
|
|
578
|
+
// (`data-card-foreign`) holds references into that page, not this one.
|
|
579
|
+
const refFrom = (node: EventTarget | null) => {
|
|
580
|
+
const trigger = closestElement(node, TRIGGER_SELECTOR);
|
|
581
|
+
if (!trigger || trigger.closest('[data-card-foreign]')) return null;
|
|
582
|
+
const kind = kindOf(trigger);
|
|
583
|
+
if (!kind) return null;
|
|
584
|
+
return targetFor(trigger, kind) || crossPageHref(trigger, kind) ? trigger : null;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const refs = Array.from(document.querySelectorAll<HTMLElement>(TRIGGER_SELECTOR));
|
|
588
|
+
if (!refs.some((ref) => refFrom(ref))) return;
|
|
589
|
+
shell.dataset.cardSourceReady = 'true';
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The outermost fold hiding `el`: a closed memo or section (its
|
|
593
|
+
* `<details>` stays laid out while closed, clipped, so its content still
|
|
594
|
+
* has an on-screen box), any other closed `<details>`, or a `hidden`
|
|
595
|
+
* ancestor. A reference to something in there previews it in a card, as
|
|
596
|
+
* if it were off screen, and marks the fold's bar while the card is open.
|
|
597
|
+
*/
|
|
598
|
+
const foldHiding = (el: Element): HTMLElement | null => {
|
|
599
|
+
let hiding: HTMLElement | null = null;
|
|
600
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
601
|
+
if (node instanceof HTMLDetailsElement) {
|
|
602
|
+
const state = node.dataset.state;
|
|
603
|
+
if (!node.open || (state !== undefined && state !== 'open' && state !== 'peeking')) {
|
|
604
|
+
hiding = node;
|
|
605
|
+
}
|
|
606
|
+
} else if (node.hidden) {
|
|
607
|
+
hiding = node;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return hiding;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/** The part of a fold a reader sees while it is closed: its bar. */
|
|
614
|
+
const foldBar = (fold: HTMLElement): HTMLElement =>
|
|
615
|
+
(fold instanceof HTMLDetailsElement
|
|
616
|
+
? fold.querySelector<HTMLElement>(':scope > summary')
|
|
617
|
+
: null) ?? fold;
|
|
618
|
+
|
|
413
619
|
const isFullyVisible = (el: HTMLElement) => {
|
|
620
|
+
if (foldHiding(el)) return false;
|
|
414
621
|
const rect = el.getBoundingClientRect();
|
|
415
622
|
const headerBottom =
|
|
416
623
|
document
|
|
@@ -425,373 +632,182 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
425
632
|
);
|
|
426
633
|
};
|
|
427
634
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
635
|
+
// Highlight mode: a same-page float already fully on screen is lit up in
|
|
636
|
+
// place, and its reference stays a plain link (the manager leaves a
|
|
637
|
+
// trigger the source does not claim alone).
|
|
638
|
+
let highlighted: { target: HTMLElement; className: string } | null = null;
|
|
639
|
+
const clearHighlight = () => {
|
|
640
|
+
highlighted?.target.classList.remove(highlighted.className);
|
|
641
|
+
highlighted = null;
|
|
642
|
+
};
|
|
643
|
+
const highlight = (trigger: HTMLElement) => {
|
|
644
|
+
const kind = kindOf(trigger);
|
|
645
|
+
const target = kind ? targetFor(trigger, kind) : null;
|
|
646
|
+
if (!kind || !target || !isFullyVisible(target)) return;
|
|
647
|
+
clearHighlight();
|
|
648
|
+
target.classList.add(kind.highlightClass);
|
|
649
|
+
highlighted = { target, className: kind.highlightClass };
|
|
431
650
|
};
|
|
432
651
|
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const gap = 10;
|
|
462
|
-
const displayEquation = trigger.closest<HTMLElement>('.katex-display');
|
|
463
|
-
const safeRegion = displayEquation ?? trigger;
|
|
464
|
-
const regionRect = safeRegion.getBoundingClientRect();
|
|
465
|
-
const anchorRect = trigger.getBoundingClientRect();
|
|
466
|
-
|
|
467
|
-
// Set before any width-dependent measurement below so the card has
|
|
468
|
-
// already grown (or not) to its final width by the time its rect is
|
|
469
|
-
// read for placement.
|
|
470
|
-
const { start: horizontalStart, end: horizontalEnd } =
|
|
471
|
-
horizontalRegion(edge);
|
|
472
|
-
panel.style.setProperty(
|
|
473
|
-
'--float-ref-max-width',
|
|
474
|
-
`${Math.max(horizontalEnd - horizontalStart, 0)}px`,
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
const headerBottom =
|
|
478
|
-
document
|
|
479
|
-
.querySelector<HTMLElement>('.header')
|
|
480
|
-
?.getBoundingClientRect().bottom ?? 0;
|
|
481
|
-
const verticalStart = Math.max(edge, headerBottom + edge);
|
|
482
|
-
const verticalEnd = window.innerHeight - edge;
|
|
483
|
-
const spaceAbove = regionRect.top - gap - verticalStart;
|
|
484
|
-
const spaceBelow = verticalEnd - regionRect.bottom - gap;
|
|
485
|
-
|
|
486
|
-
panel.style.maxHeight = `${Math.max(spaceAbove, spaceBelow, 0)}px`;
|
|
487
|
-
let rect = panel.getBoundingClientRect();
|
|
488
|
-
const placeBelow = spaceBelow >= rect.height || spaceBelow >= spaceAbove;
|
|
489
|
-
panel.style.maxHeight = `${Math.max(placeBelow ? spaceBelow : spaceAbove, 0)}px`;
|
|
490
|
-
rect = panel.getBoundingClientRect();
|
|
491
|
-
|
|
492
|
-
let top = placeBelow
|
|
493
|
-
? regionRect.bottom + gap
|
|
494
|
-
: regionRect.top - gap - rect.height;
|
|
495
|
-
let left = anchorRect.left + anchorRect.width / 2 - rect.width / 2;
|
|
496
|
-
|
|
497
|
-
top = Math.max(verticalStart, Math.min(top, verticalEnd - rect.height));
|
|
498
|
-
left =
|
|
499
|
-
horizontalEnd - horizontalStart >= rect.width
|
|
500
|
-
? Math.max(horizontalStart, Math.min(left, horizontalEnd - rect.width))
|
|
501
|
-
: Math.max(edge, Math.min(left, window.innerWidth - rect.width - edge));
|
|
502
|
-
|
|
503
|
-
panel.style.top = `${Math.round(top)}px`;
|
|
504
|
-
panel.style.left = `${Math.round(left)}px`;
|
|
505
|
-
}
|
|
652
|
+
/** `lesson` names the page a cross-page reference lands on. */
|
|
653
|
+
const buildCard = (
|
|
654
|
+
trigger: HTMLElement,
|
|
655
|
+
target: HTMLElement,
|
|
656
|
+
kind: KindConfig,
|
|
657
|
+
lesson?: string,
|
|
658
|
+
) => {
|
|
659
|
+
const el = cloneCardShell(shell);
|
|
660
|
+
if (!el) return null;
|
|
661
|
+
const field = (name: string) => el.querySelector<HTMLElement>(`[data-panel-${name}]`);
|
|
662
|
+
const kindEl = field('kind');
|
|
663
|
+
const headingEl = field('heading');
|
|
664
|
+
const numberEl = el.querySelector<HTMLAnchorElement>('[data-panel-number]');
|
|
665
|
+
const contentEl = field('content');
|
|
666
|
+
const captionEl = field('caption');
|
|
667
|
+
const jumpEl = el.querySelector<HTMLAnchorElement>('[data-panel-jump]');
|
|
668
|
+
const jumpLabelEl = field('jump-label');
|
|
669
|
+
if (
|
|
670
|
+
!kindEl ||
|
|
671
|
+
!headingEl ||
|
|
672
|
+
!numberEl ||
|
|
673
|
+
!contentEl ||
|
|
674
|
+
!captionEl ||
|
|
675
|
+
!jumpEl ||
|
|
676
|
+
!jumpLabelEl
|
|
677
|
+
) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
506
680
|
|
|
507
|
-
|
|
508
|
-
const number = (target.dataset[kind.numberDataKey] ?? '') as string;
|
|
509
|
-
const label = kind.jumpLabel;
|
|
681
|
+
const number = target.dataset[kind.numberDataKey] ?? '';
|
|
510
682
|
const href = trigger.getAttribute('href') ?? '';
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
kind.
|
|
515
|
-
|
|
516
|
-
|
|
683
|
+
const goTo = goToLabel(kind, number);
|
|
684
|
+
kindEl.textContent = capitalized(kind.jumpLabel);
|
|
685
|
+
headingEl.textContent = kind.shortDataKey
|
|
686
|
+
? (target.dataset[kind.shortDataKey] ?? '')
|
|
687
|
+
: '';
|
|
688
|
+
headingEl.hidden = headingEl.textContent.length === 0;
|
|
689
|
+
// The number sits to the right of the content and is itself a link to
|
|
690
|
+
// the float — for an equation just "(2.1)", else the labelled form.
|
|
691
|
+
numberEl.textContent = numberText(kind, number);
|
|
692
|
+
numberEl.hidden = numberEl.textContent.length === 0;
|
|
517
693
|
numberEl.href = href;
|
|
518
|
-
numberEl.title = `
|
|
694
|
+
numberEl.title = `Go to the ${kind.jumpLabel}`;
|
|
519
695
|
kind.fillContent(target, contentEl);
|
|
696
|
+
// A term cloned from another page still carries its no-JS tooltip.
|
|
697
|
+
for (const term of contentEl.querySelectorAll('a.notation-term[title]')) {
|
|
698
|
+
term.removeAttribute('title');
|
|
699
|
+
}
|
|
520
700
|
captionEl.textContent = kind.captionFor(target);
|
|
521
701
|
captionEl.hidden = captionEl.textContent.length === 0;
|
|
522
|
-
jumpLabelEl.textContent =
|
|
702
|
+
jumpLabelEl.textContent = lesson ? `${goTo} in ${lesson}` : goTo;
|
|
523
703
|
jumpEl.href = href;
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
activeTarget.classList.remove(activeKind.highlightClass);
|
|
529
|
-
}
|
|
530
|
-
if (mode === 'panel') {
|
|
531
|
-
panel.hidden = true;
|
|
532
|
-
releaseActiveSurface(GROUP);
|
|
533
|
-
}
|
|
534
|
-
activeTrigger = null;
|
|
535
|
-
activeTarget = null;
|
|
536
|
-
activeKind = null;
|
|
537
|
-
mode = null;
|
|
538
|
-
pinned = false;
|
|
539
|
-
}
|
|
704
|
+
wireJumpArrow(jumpEl);
|
|
705
|
+
el.setAttribute('aria-label', `Preview of ${goTo}`);
|
|
706
|
+
return el;
|
|
707
|
+
};
|
|
540
708
|
|
|
541
709
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* card still closes on its "×", on an outside click, or on Escape.
|
|
710
|
+
* A same-page reference fills its card from this document. A cross-page
|
|
711
|
+
* one fetches and parses its page once (`fetchDoc` caches it), pulls the
|
|
712
|
+
* `#eq-…`/`#tab-…`/`#fig-…` node out of it, and fills the same card —
|
|
713
|
+
* cloned across documents by the `importNode`-based helpers above.
|
|
547
714
|
*/
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
reveal(target, kind);
|
|
551
|
-
releaseActiveSurface(GROUP);
|
|
552
|
-
}
|
|
715
|
+
// Fold bars marked for an open card, by the card's item.
|
|
716
|
+
const foldMarks = new Map<string, HTMLElement>();
|
|
553
717
|
|
|
554
|
-
|
|
718
|
+
const render = (item: string, trigger: HTMLElement) => {
|
|
555
719
|
const kind = kindOf(trigger);
|
|
556
|
-
if (!kind) return;
|
|
557
|
-
|
|
720
|
+
if (!kind) return null;
|
|
558
721
|
const crossPage = crossPageHref(trigger, kind);
|
|
559
|
-
if (crossPage) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (trigger === activeTrigger) {
|
|
571
|
-
if (pin) pinPanel(activeTarget, kind);
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
clearAll();
|
|
576
|
-
activeTrigger = trigger;
|
|
577
|
-
activeTarget = target;
|
|
578
|
-
activeKind = kind;
|
|
579
|
-
|
|
580
|
-
if (isFullyVisible(target)) {
|
|
581
|
-
// Never reached with `pin: true` — the click handler only calls
|
|
582
|
-
// `show(trigger, true)` once it has already checked the float is
|
|
583
|
-
// off screen; an on-screen reference is left to navigate natively.
|
|
584
|
-
mode = 'highlight';
|
|
585
|
-
target.classList.add(kind.highlightClass);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
mode = 'panel';
|
|
590
|
-
fillPanel(trigger, target, kind);
|
|
591
|
-
claimActiveSurface(GROUP, clearAll);
|
|
592
|
-
panel.style.visibility = 'hidden';
|
|
593
|
-
panel.hidden = false;
|
|
594
|
-
position(trigger);
|
|
595
|
-
panel.style.visibility = 'visible';
|
|
596
|
-
if (pin) pinPanel(target, kind);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* The cross-page counterpart of `show`: the target float lives on another
|
|
601
|
-
* lesson page, so there is nothing in this document to highlight in place.
|
|
602
|
-
* Fetch and parse that page once (`fetchDoc` caches it), pull the
|
|
603
|
-
* `#eq-…`/`#tab-…`/`#fig-…` node out of it, and fill the same card — the
|
|
604
|
-
* formula, table body, or (for a `previewable` figure) figure body, cloned
|
|
605
|
-
* across documents by `fillPanel`'s `importNode`-based helpers. A slow
|
|
606
|
-
* fetch that resolves after the pointer has moved on is discarded.
|
|
607
|
-
*/
|
|
608
|
-
async function showCrossPage(
|
|
609
|
-
trigger: HTMLElement,
|
|
610
|
-
kind: KindConfig,
|
|
611
|
-
ref: { page: string; id: string },
|
|
612
|
-
) {
|
|
613
|
-
if (pinned && trigger !== activeTrigger) return;
|
|
614
|
-
if (trigger === activeTrigger) return;
|
|
615
|
-
|
|
616
|
-
clearAll();
|
|
617
|
-
activeTrigger = trigger;
|
|
618
|
-
activeKind = kind;
|
|
619
|
-
|
|
620
|
-
let sourceDoc: Document;
|
|
621
|
-
try {
|
|
622
|
-
sourceDoc = await fetchDoc(ref.page);
|
|
623
|
-
} catch {
|
|
624
|
-
if (activeTrigger === trigger) clearAll();
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if (activeTrigger !== trigger) return;
|
|
628
|
-
|
|
629
|
-
const target = sourceDoc.getElementById(ref.id);
|
|
630
|
-
if (!target) {
|
|
631
|
-
clearAll();
|
|
632
|
-
return;
|
|
722
|
+
if (!crossPage) {
|
|
723
|
+
const target = targetFor(trigger, kind);
|
|
724
|
+
if (!target) return null;
|
|
725
|
+
const fold = foldHiding(target);
|
|
726
|
+
if (fold) {
|
|
727
|
+
const bar = foldBar(fold);
|
|
728
|
+
bar.classList.add('float-ref-fold-hint');
|
|
729
|
+
foldMarks.set(item, bar);
|
|
730
|
+
}
|
|
731
|
+
return buildCard(trigger, target, kind);
|
|
633
732
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
position(trigger);
|
|
643
|
-
panel.style.visibility = 'visible';
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function scheduleLeave() {
|
|
647
|
-
if (pinned) return;
|
|
648
|
-
if (leaveTimer !== null) window.cancelAnimationFrame(leaveTimer);
|
|
649
|
-
leaveTimer = window.requestAnimationFrame(() => {
|
|
650
|
-
leaveTimer = null;
|
|
651
|
-
if (pinned || !activeTrigger) return;
|
|
652
|
-
const stillActive =
|
|
653
|
-
activeTrigger.matches(':hover') ||
|
|
654
|
-
activeTrigger === document.activeElement ||
|
|
655
|
-
panel.matches(':hover') ||
|
|
656
|
-
panel.contains(document.activeElement);
|
|
657
|
-
if (!stillActive) clearAll();
|
|
733
|
+
return fetchDoc(crossPage.page).then((sourceDoc) => {
|
|
734
|
+
const target = sourceDoc.getElementById(crossPage.id);
|
|
735
|
+
if (!target) return null;
|
|
736
|
+
ensureKatexStylesheet(sourceDoc);
|
|
737
|
+
const lesson = sourceDoc.getElementById('_top')?.textContent?.trim() || undefined;
|
|
738
|
+
const el = buildCard(trigger, target, kind, lesson);
|
|
739
|
+
el?.setAttribute('data-card-foreign', '');
|
|
740
|
+
return el;
|
|
658
741
|
});
|
|
659
|
-
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
registerCardSource({
|
|
745
|
+
layer: 'reference',
|
|
746
|
+
triggerFrom: refFrom,
|
|
747
|
+
itemFor: (trigger) => {
|
|
748
|
+
const kind = kindOf(trigger);
|
|
749
|
+
if (!kind) return null;
|
|
750
|
+
const crossPage = crossPageHref(trigger, kind);
|
|
751
|
+
if (crossPage) return `${crossPage.page}#${crossPage.id}`;
|
|
752
|
+
return (trigger.getAttribute('href') ?? '').slice(1) || null;
|
|
753
|
+
},
|
|
754
|
+
claims: (trigger) => {
|
|
755
|
+
const kind = kindOf(trigger);
|
|
756
|
+
if (!kind) return false;
|
|
757
|
+
// The exporter rewrote a cross-page reference to the course's own
|
|
758
|
+
// URL; an offline copy must never fetch it.
|
|
759
|
+
if (crossPageHref(trigger, kind)) return !standalone;
|
|
760
|
+
const target = targetFor(trigger, kind);
|
|
761
|
+
return !!target && !isFullyVisible(target);
|
|
762
|
+
},
|
|
763
|
+
render,
|
|
764
|
+
onChange: (open) => {
|
|
765
|
+
for (const [item, bar] of foldMarks) {
|
|
766
|
+
if (open.has(item)) continue;
|
|
767
|
+
bar.classList.remove('float-ref-fold-hint');
|
|
768
|
+
foldMarks.delete(item);
|
|
769
|
+
}
|
|
770
|
+
// Keeping an equation's card, pinned or docked, reveals its faded
|
|
771
|
+
// number badge (F4).
|
|
772
|
+
for (const [item, mode] of open) {
|
|
773
|
+
if (!isKept(mode)) continue;
|
|
774
|
+
const kind = Object.values(KINDS).find(
|
|
775
|
+
(candidate) =>
|
|
776
|
+
candidate.revealOnPin && item.startsWith(candidate.hrefPrefix.slice(1)),
|
|
777
|
+
);
|
|
778
|
+
if (!kind) continue;
|
|
779
|
+
document
|
|
780
|
+
.getElementById(item)
|
|
781
|
+
?.querySelector(`.${kind.numberClass}`)
|
|
782
|
+
?.classList.add('eq-number-revealed');
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
});
|
|
660
786
|
|
|
661
787
|
document.addEventListener('pointerover', (event) => {
|
|
662
788
|
if (event.pointerType === 'touch') return;
|
|
663
|
-
const trigger =
|
|
664
|
-
if (!trigger
|
|
665
|
-
if (
|
|
666
|
-
|
|
789
|
+
const trigger = refFrom(event.target);
|
|
790
|
+
if (!trigger) return;
|
|
791
|
+
if (event.relatedTarget instanceof Node && trigger.contains(event.relatedTarget)) return;
|
|
792
|
+
highlight(trigger);
|
|
667
793
|
});
|
|
668
794
|
|
|
669
795
|
document.addEventListener('pointerout', (event) => {
|
|
670
796
|
if (event.pointerType === 'touch') return;
|
|
671
|
-
const trigger =
|
|
672
|
-
if (!trigger
|
|
673
|
-
if (
|
|
674
|
-
|
|
797
|
+
const trigger = refFrom(event.target);
|
|
798
|
+
if (!trigger) return;
|
|
799
|
+
if (event.relatedTarget instanceof Node && trigger.contains(event.relatedTarget)) return;
|
|
800
|
+
clearHighlight();
|
|
675
801
|
});
|
|
676
802
|
|
|
677
803
|
document.addEventListener('focusin', (event) => {
|
|
678
|
-
const trigger =
|
|
679
|
-
if (trigger
|
|
804
|
+
const trigger = refFrom(event.target);
|
|
805
|
+
if (trigger) highlight(trigger);
|
|
680
806
|
});
|
|
681
807
|
|
|
682
808
|
document.addEventListener('focusout', (event) => {
|
|
683
|
-
|
|
684
|
-
if (trigger && refSet.has(trigger)) scheduleLeave();
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
panel.addEventListener('pointerleave', scheduleLeave);
|
|
688
|
-
|
|
689
|
-
document.addEventListener('click', (event) => {
|
|
690
|
-
if (closeButton && closestElement(event.target, '[data-hover-panel-close]')) {
|
|
691
|
-
event.preventDefault();
|
|
692
|
-
clearAll();
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const trigger = closestElement(event.target, TRIGGER_SELECTOR);
|
|
697
|
-
if (trigger && refSet.has(trigger)) {
|
|
698
|
-
const kind = kindOf(trigger);
|
|
699
|
-
if (!kind) return;
|
|
700
|
-
|
|
701
|
-
const isCrossPage = crossPageHref(trigger, kind) !== null;
|
|
702
|
-
|
|
703
|
-
// A diagram reference remains an ordinary navigation link. Hover may
|
|
704
|
-
// preview an off-screen diagram, but clicking must dismiss that card
|
|
705
|
-
// and take the reader to the real diagram in one gesture.
|
|
706
|
-
if (kind.jumpLabel === 'diagram' && !isCrossPage) {
|
|
707
|
-
clearAll();
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// A card that is already open for this trigger — same-page *or*
|
|
712
|
-
// cross-page — pins in place instead of navigating. The only way to
|
|
713
|
-
// the float itself is now the card's own number/"Jump to …" link.
|
|
714
|
-
if (mode === 'panel' && activeTrigger === trigger) {
|
|
715
|
-
event.preventDefault();
|
|
716
|
-
pinPanel(isCrossPage ? null : targetFor(trigger, kind), kind);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (isCrossPage) {
|
|
721
|
-
// The exporter rewrites this to the canonical course URL. Leave it
|
|
722
|
-
// as an ordinary link; an offline hover/click must never fetch it.
|
|
723
|
-
if (standalone) {
|
|
724
|
-
clearAll();
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
// No card open yet (keyboard, or a click that beat the hover): open
|
|
728
|
-
// it — pinned — rather than leave the page.
|
|
729
|
-
event.preventDefault();
|
|
730
|
-
const ref = crossPageHref(trigger, kind)!;
|
|
731
|
-
void showCrossPage(trigger, kind, ref).then(() => {
|
|
732
|
-
if (activeTrigger === trigger && mode === 'panel') {
|
|
733
|
-
pinPanel(null, kind);
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const target = targetFor(trigger, kind);
|
|
740
|
-
if (target && !isFullyVisible(target)) {
|
|
741
|
-
event.preventDefault();
|
|
742
|
-
show(trigger, true);
|
|
743
|
-
}
|
|
744
|
-
// Already fully visible: let the native anchor navigation through —
|
|
745
|
-
// `KeyedFloatEnhancer.astro` picks up the `hashchange` it produces.
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (closestElement(event.target, '[data-panel-jump], [data-panel-number]')) {
|
|
750
|
-
// Real navigation to the float is the point of these links; just drop
|
|
751
|
-
// our own state so a stray pointerout doesn't fight it.
|
|
752
|
-
clearAll();
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (mode === 'panel' && event.target instanceof Node) {
|
|
757
|
-
// An outside click closes the card — but a click landing in a notation
|
|
758
|
-
// popup that a card symbol opened is "inside" as far as the reader is
|
|
759
|
-
// concerned.
|
|
760
|
-
if (
|
|
761
|
-
!panel.contains(event.target) &&
|
|
762
|
-
!closestElement(event.target, '.notation-popup')
|
|
763
|
-
) {
|
|
764
|
-
clearAll();
|
|
765
|
-
}
|
|
766
|
-
}
|
|
809
|
+
if (refFrom(event.target)) clearHighlight();
|
|
767
810
|
});
|
|
768
|
-
|
|
769
|
-
document.addEventListener('keydown', (event) => {
|
|
770
|
-
if (event.key === 'Escape' && mode) clearAll();
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
window.addEventListener(
|
|
774
|
-
'resize',
|
|
775
|
-
() => {
|
|
776
|
-
if (mode === 'panel' && activeTrigger) position(activeTrigger);
|
|
777
|
-
},
|
|
778
|
-
{ passive: true },
|
|
779
|
-
);
|
|
780
|
-
window.addEventListener(
|
|
781
|
-
'scroll',
|
|
782
|
-
() => {
|
|
783
|
-
if (mode !== 'panel' || !activeTrigger) return;
|
|
784
|
-
if (
|
|
785
|
-
activeTarget?.ownerDocument === document &&
|
|
786
|
-
isFullyVisible(activeTarget)
|
|
787
|
-
) {
|
|
788
|
-
clearAll();
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
position(activeTrigger);
|
|
792
|
-
},
|
|
793
|
-
{ passive: true, capture: true },
|
|
794
|
-
);
|
|
795
811
|
}
|
|
796
812
|
|
|
797
813
|
setup();
|
|
@@ -837,7 +853,11 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
837
853
|
text-decoration: underline;
|
|
838
854
|
}
|
|
839
855
|
|
|
856
|
+
/* The content takes the card's width: a figure's SVG sized by its
|
|
857
|
+
`viewBox` alone, or a table in its scroll clip, has no width of its
|
|
858
|
+
own to give a card that sizes itself to its content. */
|
|
840
859
|
.float-ref-panel-content {
|
|
860
|
+
flex: 1 1 auto;
|
|
841
861
|
min-width: 0;
|
|
842
862
|
overflow-x: auto;
|
|
843
863
|
/* `overflow-x: auto` alone makes the block axis compute to `auto` too
|
|
@@ -853,6 +873,10 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
853
873
|
display: none;
|
|
854
874
|
}
|
|
855
875
|
|
|
876
|
+
.float-ref-panel-content :global(.keyed-figure__body) {
|
|
877
|
+
min-inline-size: min(22rem, 70vw);
|
|
878
|
+
}
|
|
879
|
+
|
|
856
880
|
.float-ref-panel-content :global(.katex-display) {
|
|
857
881
|
margin-block: 0;
|
|
858
882
|
}
|
|
@@ -881,6 +905,44 @@ import HoverPanel from '../HoverPanel.astro';
|
|
|
881
905
|
font-size: var(--sl-text-xs);
|
|
882
906
|
}
|
|
883
907
|
|
|
908
|
+
/* The card's header: the kind of float, then its short caption when it
|
|
909
|
+
has one. */
|
|
910
|
+
.float-ref-panel-title {
|
|
911
|
+
display: flex;
|
|
912
|
+
flex-wrap: wrap;
|
|
913
|
+
align-items: baseline;
|
|
914
|
+
gap: 0.2rem 0.6rem;
|
|
915
|
+
margin: 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.float-ref-panel-kind {
|
|
919
|
+
color: var(--sl-color-gray-3);
|
|
920
|
+
font-size: var(--sl-text-xs);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.float-ref-panel-heading {
|
|
924
|
+
font-weight: 600;
|
|
925
|
+
font-size: var(--sl-text-sm);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* A section preview: its title, then the start of its first paragraph —
|
|
929
|
+
enough to recognise the passage, not to read it here. */
|
|
930
|
+
.float-ref-panel-content :global(.float-ref-panel-section-title) {
|
|
931
|
+
margin: 0;
|
|
932
|
+
font-weight: 600;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.float-ref-panel-content :global(.float-ref-panel-section-lead) {
|
|
936
|
+
display: -webkit-box;
|
|
937
|
+
margin: 0.35rem 0 0;
|
|
938
|
+
overflow: hidden;
|
|
939
|
+
-webkit-box-orient: vertical;
|
|
940
|
+
-webkit-line-clamp: 4;
|
|
941
|
+
line-clamp: 4;
|
|
942
|
+
color: var(--sl-color-gray-2);
|
|
943
|
+
font-size: var(--sl-text-sm);
|
|
944
|
+
}
|
|
945
|
+
|
|
884
946
|
.float-ref-panel-jump {
|
|
885
947
|
justify-self: start;
|
|
886
948
|
font-size: var(--sl-text-xs);
|