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