explico 0.7.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- 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 +14 -1
- package/dist/compiler/logging.js +6 -2
- 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 +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- 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 +69 -34
- package/dist/reference/equations.mjs +90 -44
- 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 +3 -2
- 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 +47 -23
- 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 +44 -6
- package/dist/reference/gate-math.js +63 -10
- 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 +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +5 -5
- 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 +24 -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 +2 -2
- 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 +1324 -2
- package/docs/authoring/notation.md +616 -344
- 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/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- 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 +21 -4
- 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 +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- 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 +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- 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 +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +17 -32
- 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 +24 -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 +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -14,27 +14,53 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
14
14
|
{
|
|
15
15
|
attribute: string;
|
|
16
16
|
storageKey: string;
|
|
17
|
+
legacyStorageKey: string;
|
|
18
|
+
widthStorageKey: string;
|
|
19
|
+
widthProperty: string;
|
|
20
|
+
resizedAttribute: string;
|
|
17
21
|
peekSuppressedAttribute: string;
|
|
22
|
+
peekArmedAttribute: string;
|
|
18
23
|
panelSelector: string;
|
|
24
|
+
/** The layout this panel's width belongs to; outside it, nothing to set. */
|
|
25
|
+
query: string;
|
|
26
|
+
/** Which way a drag widens the panel, before the page's own direction. */
|
|
27
|
+
grows: 1 | -1;
|
|
19
28
|
shownLabel: string;
|
|
20
29
|
hiddenLabel: string;
|
|
30
|
+
widthLabel: string;
|
|
21
31
|
}
|
|
22
32
|
> = {
|
|
23
33
|
navigation: {
|
|
24
34
|
attribute: 'data-navigation-hidden',
|
|
25
|
-
storageKey: '
|
|
35
|
+
storageKey: 'explico:layout:navigation-hidden',
|
|
36
|
+
legacyStorageKey: 'credit-playground:layout:navigation-hidden',
|
|
37
|
+
widthStorageKey: 'explico:layout:navigation-width',
|
|
38
|
+
widthProperty: '--sl-sidebar-width',
|
|
39
|
+
resizedAttribute: 'data-navigation-resized',
|
|
26
40
|
peekSuppressedAttribute: 'data-navigation-peek-suppressed',
|
|
41
|
+
peekArmedAttribute: 'data-navigation-peek-armed',
|
|
27
42
|
panelSelector: '.sidebar-pane',
|
|
43
|
+
query: '(min-width: 50rem)',
|
|
44
|
+
grows: 1,
|
|
28
45
|
shownLabel: 'Hide navigation',
|
|
29
46
|
hiddenLabel: 'Show navigation',
|
|
47
|
+
widthLabel: 'Navigation width',
|
|
30
48
|
},
|
|
31
49
|
contents: {
|
|
32
50
|
attribute: 'data-page-sidebar-hidden',
|
|
33
|
-
storageKey: '
|
|
51
|
+
storageKey: 'explico:layout:contents-hidden',
|
|
52
|
+
legacyStorageKey: 'credit-playground:layout:contents-hidden',
|
|
53
|
+
widthStorageKey: 'explico:layout:contents-width',
|
|
54
|
+
widthProperty: '--layout-contents-width',
|
|
55
|
+
resizedAttribute: 'data-contents-resized',
|
|
34
56
|
peekSuppressedAttribute: 'data-page-sidebar-peek-suppressed',
|
|
57
|
+
peekArmedAttribute: 'data-page-sidebar-peek-armed',
|
|
35
58
|
panelSelector: '.right-sidebar',
|
|
59
|
+
query: '(min-width: 72rem)',
|
|
60
|
+
grows: -1,
|
|
36
61
|
shownLabel: 'Hide page contents',
|
|
37
62
|
hiddenLabel: 'Show page contents',
|
|
63
|
+
widthLabel: 'Page contents width',
|
|
38
64
|
},
|
|
39
65
|
};
|
|
40
66
|
|
|
@@ -55,10 +81,11 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
55
81
|
* Starlight's own formula already gave it.
|
|
56
82
|
*
|
|
57
83
|
* This doesn't close a loop back on itself: `.right-sidebar`'s own width
|
|
58
|
-
* is edge-anchored and shrink-to-fit against its own content,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* need to react to anything
|
|
84
|
+
* is edge-anchored and shrink-to-fit against its own content (or, once the
|
|
85
|
+
* reader has set one, exactly the width they set), not against how much
|
|
86
|
+
* room `.main-pane` leaves it — that dependency is exactly what the edge
|
|
87
|
+
* anchor removed — so measuring it here doesn't need to react to anything
|
|
88
|
+
* this function itself changes.
|
|
62
89
|
*/
|
|
63
90
|
function updateTocReservation(): void {
|
|
64
91
|
const panel = document.querySelector<HTMLElement>('.right-sidebar');
|
|
@@ -78,14 +105,22 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
78
105
|
// PreferenceStore and nothing here changes.
|
|
79
106
|
const preferences = createBrowserPreferenceStore();
|
|
80
107
|
|
|
81
|
-
function readPreference(
|
|
82
|
-
|
|
108
|
+
function readPreference(region: LayoutRegion): boolean {
|
|
109
|
+
const settings = regions[region];
|
|
110
|
+
const stored =
|
|
111
|
+
preferences.get(settings.storageKey) ??
|
|
112
|
+
// Written under a name this engine has outgrown; honour it once so a
|
|
113
|
+
// returning reader keeps the layout they left.
|
|
114
|
+
preferences.get(settings.legacyStorageKey);
|
|
115
|
+
return stored === 'true';
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
function writePreference(storageKey: string, hidden: boolean): void {
|
|
86
119
|
preferences.set(storageKey, String(hidden));
|
|
87
120
|
}
|
|
88
121
|
|
|
122
|
+
const toggles = new Map<LayoutRegion, HTMLButtonElement>();
|
|
123
|
+
|
|
89
124
|
function applyState(
|
|
90
125
|
button: HTMLButtonElement,
|
|
91
126
|
region: LayoutRegion,
|
|
@@ -100,14 +135,40 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
100
135
|
button.title = label;
|
|
101
136
|
}
|
|
102
137
|
|
|
138
|
+
function setHidden(region: LayoutRegion, hidden: boolean): void {
|
|
139
|
+
const settings = regions[region];
|
|
140
|
+
if (root.hasAttribute(settings.attribute) === hidden) return;
|
|
141
|
+
const button = toggles.get(region);
|
|
142
|
+
if (button) applyState(button, region, hidden);
|
|
143
|
+
else root.toggleAttribute(settings.attribute, hidden);
|
|
144
|
+
writePreference(settings.storageKey, hidden);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A panel collapsed by the pointer would otherwise peek straight back open,
|
|
149
|
+
* because the pointer that collapsed it is still inside the rail it left
|
|
150
|
+
* behind. Hold the peek off until the pointer actually leaves.
|
|
151
|
+
*/
|
|
152
|
+
function suppressPeek(region: LayoutRegion): void {
|
|
153
|
+
const settings = regions[region];
|
|
154
|
+
root.setAttribute(settings.peekSuppressedAttribute, '');
|
|
155
|
+
const panel = document.querySelector(settings.panelSelector);
|
|
156
|
+
const clear = () => root.removeAttribute(settings.peekSuppressedAttribute);
|
|
157
|
+
panel?.addEventListener('pointerleave', clear, { once: true });
|
|
158
|
+
window.setTimeout(() => {
|
|
159
|
+
if (!panel?.matches(':hover')) clear();
|
|
160
|
+
}, 180);
|
|
161
|
+
}
|
|
162
|
+
|
|
103
163
|
document
|
|
104
164
|
.querySelectorAll<HTMLButtonElement>('[data-layout-toggle]')
|
|
105
165
|
.forEach((button) => {
|
|
106
166
|
const region = button.dataset.layoutToggle as LayoutRegion;
|
|
107
167
|
const settings = regions[region];
|
|
108
168
|
if (!settings) return;
|
|
169
|
+
toggles.set(region, button);
|
|
109
170
|
|
|
110
|
-
applyState(button, region, readPreference(
|
|
171
|
+
applyState(button, region, readPreference(region));
|
|
111
172
|
button.removeAttribute('hidden');
|
|
112
173
|
button.addEventListener('click', (event) => {
|
|
113
174
|
const hidden = !root.hasAttribute(settings.attribute);
|
|
@@ -121,20 +182,238 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
121
182
|
// A pointer click should leave a newly collapsed rail collapsed even
|
|
122
183
|
// though the pointer was inside the full-width panel when clicked.
|
|
123
184
|
if (hidden && event.detail > 0) {
|
|
124
|
-
root.setAttribute(settings.peekSuppressedAttribute, '');
|
|
125
185
|
button.blur();
|
|
186
|
+
suppressPeek(region);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Hover intent for the peek.
|
|
193
|
+
*
|
|
194
|
+
* A collapsed panel peeks open when the pointer rests on its rail, and the
|
|
195
|
+
* rails sit on the two edges of the window — a pointer on its way to the
|
|
196
|
+
* scrollbar, the tab strip, or the other side of the article crosses one
|
|
197
|
+
* without meaning anything by it, and the panel would swing open behind it
|
|
198
|
+
* every time. Waiting out `--layout-peek-delay` before arming the peek
|
|
199
|
+
* makes crossing the rail cost nothing while resting on it still opens the
|
|
200
|
+
* panel about as fast as the eye registers that it moved.
|
|
201
|
+
*
|
|
202
|
+
* The CSS keeps `:hover` as the trigger and reads this attribute as the
|
|
203
|
+
* gate in front of it, so there is one description of what peeking looks
|
|
204
|
+
* like, not two. Nothing is lost without JavaScript: a panel is only ever
|
|
205
|
+
* collapsed because this same script collapsed it.
|
|
206
|
+
*/
|
|
207
|
+
const PEEK_DELAY_FALLBACK = 40;
|
|
208
|
+
|
|
209
|
+
function peekDelay(): number {
|
|
210
|
+
const declared = getComputedStyle(root)
|
|
211
|
+
.getPropertyValue('--layout-peek-delay')
|
|
212
|
+
.trim();
|
|
213
|
+
const value = Number.parseFloat(declared);
|
|
214
|
+
if (!Number.isFinite(value) || value < 0) return PEEK_DELAY_FALLBACK;
|
|
215
|
+
// A bare number is read as milliseconds; `s` is the only other unit CSS
|
|
216
|
+
// gives a <time>.
|
|
217
|
+
return declared.endsWith('ms') || !declared.endsWith('s')
|
|
218
|
+
? value
|
|
219
|
+
: value * 1000;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
(Object.keys(regions) as LayoutRegion[]).forEach((region) => {
|
|
223
|
+
const settings = regions[region];
|
|
224
|
+
const panel = document.querySelector<HTMLElement>(settings.panelSelector);
|
|
225
|
+
if (!panel) return;
|
|
226
|
+
let timer: number | null = null;
|
|
227
|
+
|
|
228
|
+
panel.addEventListener('pointerenter', () => {
|
|
229
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
230
|
+
timer = window.setTimeout(() => {
|
|
231
|
+
timer = null;
|
|
232
|
+
root.setAttribute(settings.peekArmedAttribute, '');
|
|
233
|
+
}, peekDelay());
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
panel.addEventListener('pointerleave', () => {
|
|
237
|
+
if (timer !== null) window.clearTimeout(timer);
|
|
238
|
+
timer = null;
|
|
239
|
+
root.removeAttribute(settings.peekArmedAttribute);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reader-set panel widths.
|
|
245
|
+
*
|
|
246
|
+
* A panel's width is the reader's, not the theme's: `--sl-sidebar-width` is
|
|
247
|
+
* Starlight's own token, so setting it moves the header's title column, the
|
|
248
|
+
* article's inline start and the `.main-pane` formula together, with no
|
|
249
|
+
* second source of truth to keep in step. The "On this page" rail has no
|
|
250
|
+
* such token — it shrink-wraps its own content — so a reader-set width goes
|
|
251
|
+
* to `--layout-contents-width`, which `global.css` only honours once
|
|
252
|
+
* `data-contents-resized` says the reader has set one.
|
|
253
|
+
*
|
|
254
|
+
* Dragging a panel below three characters of width is how a reader collapses
|
|
255
|
+
* it by hand: there is no useful panel narrower than that, and treating it
|
|
256
|
+
* as "closed" beats leaving a sliver that can only be reopened by dragging
|
|
257
|
+
* a handle that has nowhere left to sit. `3ch` is measured in the panel's
|
|
258
|
+
* own font rather than assumed, so a course that changes `--sl-font` gets
|
|
259
|
+
* the threshold its own text implies.
|
|
260
|
+
*/
|
|
261
|
+
const MAX_PANEL_WIDTH = 640;
|
|
262
|
+
const pageIsRtl = getComputedStyle(root).direction === 'rtl';
|
|
263
|
+
|
|
264
|
+
function threeCharacters(panel: HTMLElement): number {
|
|
265
|
+
const probe = document.createElement('div');
|
|
266
|
+
probe.style.cssText = 'position:absolute;visibility:hidden;width:3ch';
|
|
267
|
+
panel.appendChild(probe);
|
|
268
|
+
const width = probe.getBoundingClientRect().width;
|
|
269
|
+
probe.remove();
|
|
270
|
+
return width || 24;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function maxPanelWidth(): number {
|
|
274
|
+
return Math.min(window.innerWidth / 2, MAX_PANEL_WIDTH);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function restoreWidth(region: LayoutRegion, matches: boolean): void {
|
|
278
|
+
const settings = regions[region];
|
|
279
|
+
const stored = Number(preferences.get(settings.widthStorageKey));
|
|
280
|
+
if (matches && Number.isFinite(stored) && stored > 0) {
|
|
281
|
+
root.style.setProperty(settings.widthProperty, `${Math.round(stored)}px`);
|
|
282
|
+
root.setAttribute(settings.resizedAttribute, '');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Outside this panel's layout the token belongs to the theme again: a
|
|
286
|
+
// desktop width left on `:root` would follow the page down to a phone,
|
|
287
|
+
// where Starlight expects its own value.
|
|
288
|
+
root.style.removeProperty(settings.widthProperty);
|
|
289
|
+
root.removeAttribute(settings.resizedAttribute);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
document
|
|
293
|
+
.querySelectorAll<HTMLElement>('[data-layout-resizer]')
|
|
294
|
+
.forEach((handle) => {
|
|
295
|
+
const region = handle.dataset.layoutResizer as LayoutRegion;
|
|
296
|
+
const settings = regions[region];
|
|
297
|
+
if (!settings) return;
|
|
298
|
+
const media = window.matchMedia(settings.query);
|
|
299
|
+
const grows = settings.grows * (pageIsRtl ? -1 : 1);
|
|
300
|
+
let floor = 24;
|
|
301
|
+
|
|
302
|
+
const panel = () =>
|
|
303
|
+
document.querySelector<HTMLElement>(settings.panelSelector);
|
|
304
|
+
const currentWidth = () => panel()?.getBoundingClientRect().width ?? 0;
|
|
305
|
+
|
|
306
|
+
const describe = (width: number) => {
|
|
307
|
+
handle.setAttribute('aria-valuemin', '0');
|
|
308
|
+
handle.setAttribute('aria-valuemax', String(Math.round(maxPanelWidth())));
|
|
309
|
+
handle.setAttribute('aria-valuenow', String(Math.round(width)));
|
|
310
|
+
handle.setAttribute(
|
|
311
|
+
'aria-valuetext',
|
|
312
|
+
width > 0 ? `${Math.round(width)} pixels` : 'collapsed',
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const applyWidth = (width: number) => {
|
|
317
|
+
root.style.setProperty(settings.widthProperty, `${Math.round(width)}px`);
|
|
318
|
+
root.setAttribute(settings.resizedAttribute, '');
|
|
319
|
+
if (region === 'contents') updateTocReservation();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* A width the reader passed through on the way somewhere else is not a
|
|
324
|
+
* width they chose, so nothing is written until the gesture ends. That
|
|
325
|
+
* also keeps the width a collapsing drag sweeps through — 40px, 30px,
|
|
326
|
+
* whatever the last frame above the threshold happened to be — from
|
|
327
|
+
* becoming the width the panel reopens at.
|
|
328
|
+
*/
|
|
329
|
+
const persist = () => {
|
|
330
|
+
const current = Number.parseFloat(
|
|
331
|
+
root.style.getPropertyValue(settings.widthProperty),
|
|
332
|
+
);
|
|
333
|
+
if (Number.isFinite(current) && current > 0) {
|
|
334
|
+
preferences.set(settings.widthStorageKey, String(Math.round(current)));
|
|
335
|
+
}
|
|
336
|
+
};
|
|
126
337
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
panel
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}, 180);
|
|
338
|
+
const resize = (width: number, reopenAt: number) => {
|
|
339
|
+
if (width < floor) {
|
|
340
|
+
setHidden(region, true);
|
|
341
|
+
// Collapsed, the panel's own width is the rail's; this is the width
|
|
342
|
+
// its control will bring back.
|
|
343
|
+
applyWidth(reopenAt);
|
|
344
|
+
describe(0);
|
|
345
|
+
return;
|
|
136
346
|
}
|
|
347
|
+
setHidden(region, false);
|
|
348
|
+
const next = Math.round(
|
|
349
|
+
Math.min(Math.max(width, floor), maxPanelWidth()),
|
|
350
|
+
);
|
|
351
|
+
applyWidth(next);
|
|
352
|
+
describe(next);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const measureFloor = () => {
|
|
356
|
+
const element = panel();
|
|
357
|
+
floor = element ? threeCharacters(element) : 24;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
handle.addEventListener('pointerdown', (event) => {
|
|
361
|
+
if (!media.matches) return;
|
|
362
|
+
const startX = event.clientX;
|
|
363
|
+
const startWidth = currentWidth();
|
|
364
|
+
measureFloor();
|
|
365
|
+
handle.setAttribute('data-resizing', '');
|
|
366
|
+
handle.setPointerCapture(event.pointerId);
|
|
367
|
+
|
|
368
|
+
const move = (moveEvent: PointerEvent) => {
|
|
369
|
+
resize(startWidth + (moveEvent.clientX - startX) * grows, startWidth);
|
|
370
|
+
};
|
|
371
|
+
const end = () => {
|
|
372
|
+
handle.removeAttribute('data-resizing');
|
|
373
|
+
persist();
|
|
374
|
+
// The pointer that dragged the panel away is still inside the rail
|
|
375
|
+
// it left behind, which would peek it straight back open.
|
|
376
|
+
if (root.hasAttribute(settings.attribute)) suppressPeek(region);
|
|
377
|
+
window.removeEventListener('pointermove', move);
|
|
378
|
+
window.removeEventListener('pointerup', end);
|
|
379
|
+
window.removeEventListener('pointercancel', end);
|
|
380
|
+
};
|
|
381
|
+
// Captured events still bubble on through the handle's ancestors, and
|
|
382
|
+
// listening up here keeps the drag alive across the moment a panel
|
|
383
|
+
// dragged past the threshold collapses and takes its handle off the
|
|
384
|
+
// layout — the reader can drag straight back out of it.
|
|
385
|
+
window.addEventListener('pointermove', move);
|
|
386
|
+
window.addEventListener('pointerup', end);
|
|
387
|
+
window.addEventListener('pointercancel', end);
|
|
388
|
+
event.preventDefault();
|
|
137
389
|
});
|
|
390
|
+
|
|
391
|
+
handle.addEventListener('keydown', (event) => {
|
|
392
|
+
if (!media.matches) return;
|
|
393
|
+
const step = event.shiftKey ? 64 : 16;
|
|
394
|
+
measureFloor();
|
|
395
|
+
const from = currentWidth();
|
|
396
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
|
397
|
+
const direction = event.key === 'ArrowRight' ? 1 : -1;
|
|
398
|
+
resize(from + direction * grows * step, from);
|
|
399
|
+
} else if (event.key === 'Home') {
|
|
400
|
+
resize(0, from);
|
|
401
|
+
} else if (event.key === 'End') {
|
|
402
|
+
resize(maxPanelWidth(), from);
|
|
403
|
+
} else {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
persist();
|
|
407
|
+
event.preventDefault();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const applyMedia = () => {
|
|
411
|
+
restoreWidth(region, media.matches);
|
|
412
|
+
handle.toggleAttribute('hidden', !media.matches);
|
|
413
|
+
if (media.matches) describe(currentWidth());
|
|
414
|
+
};
|
|
415
|
+
applyMedia();
|
|
416
|
+
media.addEventListener('change', applyMedia);
|
|
138
417
|
});
|
|
139
418
|
|
|
140
419
|
updateTocReservation();
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { Icon } from '@astrojs/starlight/components';
|
|
3
2
|
import DefaultPageSidebar from '@astrojs/starlight/components/PageSidebar.astro';
|
|
3
|
+
import { isLessonSlug, lessonIdFromSlug } from '../../compiler/lesson-derivation';
|
|
4
|
+
import { titledBlocksOf, tocWithTitledBlocks } from '../block-heading';
|
|
5
|
+
|
|
6
|
+
// "On this page" lists the `<h2>` a knowledge check and a worked-example set
|
|
7
|
+
// draw (docs/plans/titles-and-containers.md Part 6): a component renders
|
|
8
|
+
// them, so Astro's heading list — Starlight's source for the list — never
|
|
9
|
+
// had them. The manifest's `titledBlocks` says where each one stands; the
|
|
10
|
+
// list is rebuilt with them in before Starlight's own component (desktop and
|
|
11
|
+
// mobile alike) renders it. Each is listed by its title, as a section is;
|
|
12
|
+
// `data-titled-blocks` tells the script below which entries they are, so
|
|
13
|
+
// `global.css` draws the badge's icon before them.
|
|
14
|
+
const route = Astro.locals.starlightRoute;
|
|
15
|
+
const routeSlug = route.entry.id;
|
|
16
|
+
let blockKinds: Record<string, string> | undefined;
|
|
17
|
+
if (route.toc && isLessonSlug(routeSlug)) {
|
|
18
|
+
const blocks = await titledBlocksOf(lessonIdFromSlug(routeSlug));
|
|
19
|
+
if (blocks.length > 0) {
|
|
20
|
+
route.toc = {
|
|
21
|
+
...route.toc,
|
|
22
|
+
items: tocWithTitledBlocks(route.toc, route.headings, blocks),
|
|
23
|
+
};
|
|
24
|
+
blockKinds = Object.fromEntries(blocks.map((block) => [block.anchor, block.kind]));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
---
|
|
5
28
|
|
|
6
29
|
<div
|
|
@@ -16,18 +39,82 @@ import DefaultPageSidebar from '@astrojs/starlight/components/PageSidebar.astro'
|
|
|
16
39
|
data-layout-toggle="contents"
|
|
17
40
|
hidden
|
|
18
41
|
>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
name="left-caret"
|
|
26
|
-
class="layout-edge-icon layout-edge-icon-restore"
|
|
27
|
-
size="1rem"
|
|
28
|
-
/>
|
|
42
|
+
<span class="layout-edge-glyph layout-edge-glyph-hide" aria-hidden="true"
|
|
43
|
+
>>>></span
|
|
44
|
+
>
|
|
45
|
+
<span class="layout-edge-glyph layout-edge-glyph-restore" aria-hidden="true"
|
|
46
|
+
><<<</span
|
|
47
|
+
>
|
|
29
48
|
</button>
|
|
30
|
-
<div
|
|
49
|
+
<div
|
|
50
|
+
class="layout-edge-resizer layout-contents-resizer"
|
|
51
|
+
role="separator"
|
|
52
|
+
aria-orientation="vertical"
|
|
53
|
+
aria-controls="starlight__page-sidebar-content"
|
|
54
|
+
aria-label="Page contents width"
|
|
55
|
+
tabindex="0"
|
|
56
|
+
data-layout-resizer="contents"
|
|
57
|
+
hidden
|
|
58
|
+
>
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
id="starlight__page-sidebar-content"
|
|
62
|
+
class="layout-page-sidebar-content"
|
|
63
|
+
data-titled-blocks={blockKinds && JSON.stringify(blockKinds)}
|
|
64
|
+
>
|
|
31
65
|
<DefaultPageSidebar />
|
|
66
|
+
<script>
|
|
67
|
+
// A knowledge check's or an example set's entry: `data-block-kind` on
|
|
68
|
+
// its link, which `global.css` gives the badge's icon.
|
|
69
|
+
(() => {
|
|
70
|
+
const rail = document.querySelector<HTMLElement>('#starlight__page-sidebar-content');
|
|
71
|
+
const kinds = JSON.parse(rail?.dataset.titledBlocks ?? '{}') as Record<string, string>;
|
|
72
|
+
for (const link of rail?.querySelectorAll<HTMLAnchorElement>('a[href^="#"]') ?? []) {
|
|
73
|
+
const kind = kinds[decodeURIComponent(link.hash.slice(1))];
|
|
74
|
+
if (kind) link.dataset.blockKind = kind;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
</script>
|
|
78
|
+
<script>
|
|
79
|
+
/**
|
|
80
|
+
* The same progress the navigation rail shows across the course, shown
|
|
81
|
+
* here across the page: the headings above the reader's position are
|
|
82
|
+
* marked, the one they are in is marked as current, and the rest stay
|
|
83
|
+
* plain.
|
|
84
|
+
*
|
|
85
|
+
* Starlight's own scroll-spy moves `aria-current="true"` from link to
|
|
86
|
+
* link as the reader scrolls, so this watches for that attribute
|
|
87
|
+
* instead of re-implementing the intersection logic, and re-marks from
|
|
88
|
+
* whichever link holds it.
|
|
89
|
+
*/
|
|
90
|
+
(() => {
|
|
91
|
+
const rail = document.querySelector('#starlight__page-sidebar-content');
|
|
92
|
+
const toc = rail?.querySelector('starlight-toc');
|
|
93
|
+
if (!toc) return;
|
|
94
|
+
|
|
95
|
+
const mark = () => {
|
|
96
|
+
const links = [...toc.querySelectorAll('a')];
|
|
97
|
+
const current = links.findIndex(
|
|
98
|
+
(link) => link.getAttribute('aria-current') === 'true',
|
|
99
|
+
);
|
|
100
|
+
links.forEach((link, index) => {
|
|
101
|
+
const item = link.parentElement;
|
|
102
|
+
if (!item) return;
|
|
103
|
+
item.toggleAttribute(
|
|
104
|
+
'data-covered',
|
|
105
|
+
current > 0 && index < current,
|
|
106
|
+
);
|
|
107
|
+
item.toggleAttribute('data-current', index === current);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
mark();
|
|
112
|
+
new MutationObserver(mark).observe(toc, {
|
|
113
|
+
attributes: true,
|
|
114
|
+
attributeFilter: ['aria-current'],
|
|
115
|
+
subtree: true,
|
|
116
|
+
});
|
|
117
|
+
})();
|
|
118
|
+
</script>
|
|
32
119
|
</div>
|
|
33
120
|
</div>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The page title, preceded by the part it belongs to.
|
|
4
|
+
*
|
|
5
|
+
* A reader who arrives from a search result, a shared link, or a print export
|
|
6
|
+
* has no sidebar state to read their position from, and the sidebar itself
|
|
7
|
+
* names a part without linking it. One line above the title answers both: it
|
|
8
|
+
* says which part this chapter sits in, and it is the route to that part's own
|
|
9
|
+
* page — the surface that does not depend on a hover, a caret, or a rail being
|
|
10
|
+
* open at all.
|
|
11
|
+
*
|
|
12
|
+
* The trail stops at the part on purpose: the `<h1>` immediately below is the
|
|
13
|
+
* current page, and repeating it costs a screen reader the same words twice.
|
|
14
|
+
*/
|
|
15
|
+
import DefaultPageTitle from '@astrojs/starlight/components/PageTitle.astro';
|
|
16
|
+
import { loadManifest } from '../../compiler/manifest';
|
|
17
|
+
import { isLessonSlug } from '../../compiler/lesson-derivation';
|
|
18
|
+
|
|
19
|
+
const route = Astro.locals.starlightRoute;
|
|
20
|
+
const routeSlug = route.entry.id;
|
|
21
|
+
const manifest = await loadManifest();
|
|
22
|
+
|
|
23
|
+
// `<dir>/<name>` is a chapter. A part's own page is `<dir>/index.mdx`, whose
|
|
24
|
+
// entry id the glob loader shortens to the directory itself — so a slug with
|
|
25
|
+
// no `/` is a part page when it names a configured part, and an ordinary
|
|
26
|
+
// top-level page (`glossary`, `curriculum-map`) when it does not.
|
|
27
|
+
const partDir = isLessonSlug(routeSlug)
|
|
28
|
+
? routeSlug.slice(0, routeSlug.indexOf('/'))
|
|
29
|
+
: routeSlug.replace(/\/index$/, '');
|
|
30
|
+
const part = manifest.parts.find((entry) => entry.dir === partDir);
|
|
31
|
+
const isPartPage = part !== undefined && !isLessonSlug(routeSlug);
|
|
32
|
+
|
|
33
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
34
|
+
// A part is only a link when there is a page behind its directory URL.
|
|
35
|
+
const partHref =
|
|
36
|
+
part?.overview && !isPartPage ? `${base}${part.link}` : undefined;
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
part && (
|
|
41
|
+
<nav class="lesson-breadcrumb not-content" aria-label="Breadcrumb">
|
|
42
|
+
<ol>
|
|
43
|
+
<li>
|
|
44
|
+
<a href={route.siteTitleHref}>{route.siteTitle}</a>
|
|
45
|
+
</li>
|
|
46
|
+
<li>
|
|
47
|
+
{partHref ? (
|
|
48
|
+
<a href={partHref}>{part.label}</a>
|
|
49
|
+
) : (
|
|
50
|
+
<span aria-current={isPartPage ? 'page' : undefined}>
|
|
51
|
+
{part.label}
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</li>
|
|
55
|
+
</ol>
|
|
56
|
+
</nav>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
<DefaultPageTitle />
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.lesson-breadcrumb {
|
|
63
|
+
margin-block-start: 1rem;
|
|
64
|
+
/* The title's own `margin-top: 1rem` would otherwise open a second gap of
|
|
65
|
+
the same size between the trail and the heading it introduces. */
|
|
66
|
+
margin-block-end: -0.75rem;
|
|
67
|
+
font-size: var(--sl-text-xs);
|
|
68
|
+
line-height: 1.4;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.lesson-breadcrumb ol {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-wrap: wrap;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 0.25rem 0.4rem;
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 0;
|
|
78
|
+
list-style: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.lesson-breadcrumb li {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 0.4rem;
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 0;
|
|
87
|
+
color: var(--sl-color-gray-3);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.lesson-breadcrumb li + li::before {
|
|
91
|
+
content: '\203A';
|
|
92
|
+
color: var(--sl-color-gray-4);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.lesson-breadcrumb a {
|
|
96
|
+
color: var(--sl-color-gray-3);
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.lesson-breadcrumb a:hover,
|
|
101
|
+
.lesson-breadcrumb a:focus-visible {
|
|
102
|
+
color: var(--sl-color-text-accent);
|
|
103
|
+
text-decoration: underline;
|
|
104
|
+
text-underline-offset: 0.2em;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[dir='rtl'] .lesson-breadcrumb li + li::before {
|
|
108
|
+
content: '\2039';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media print {
|
|
112
|
+
.lesson-breadcrumb {
|
|
113
|
+
margin-block-end: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.lesson-breadcrumb a {
|
|
117
|
+
text-decoration: none;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
</style>
|