explico 0.3.0 → 0.5.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 +344 -1
- package/README.md +117 -3
- package/dist/compiler/astro.d.ts +38 -0
- package/dist/compiler/astro.js +258 -0
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +32 -12
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +86 -4
- package/dist/compiler/course-config.js +39 -0
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/lesson-derivation.d.ts +1 -1
- package/dist/compiler/lesson-derivation.js +2 -2
- package/dist/compiler/manifest.d.ts +112 -7
- package/dist/compiler/manifest.js +205 -40
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations-validate.d.ts +8 -3
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +66 -35
- package/dist/reference/equations.mjs +142 -40
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-math.d.ts +28 -5
- package/dist/reference/gate-math.js +5 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +14 -8
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/keyed-jsx-floats.d.mts +24 -21
- package/dist/reference/keyed-jsx-floats.mjs +90 -41
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +30 -0
- package/dist/reference/math-glyphs.mjs +367 -0
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +102 -5
- package/dist/reference/remark-notation.mjs +506 -50
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +33 -4
- package/docs/authoring/README.md +12 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +135 -0
- package/docs/authoring/migrating.md +154 -7
- package/docs/authoring/notation.md +568 -8
- package/docs/authoring/structure.md +251 -0
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +332 -0
- package/src/compiler/cli.ts +33 -10
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +131 -3
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/lesson-derivation.ts +2 -2
- package/src/compiler/manifest.ts +367 -49
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +444 -83
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +116 -8
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSidebar.astro +62 -1
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +38 -2
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +296 -9
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +20 -0
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations-validate.ts +6 -4
- package/src/reference/equations.mjs +142 -40
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-math.ts +27 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +12 -9
- package/src/reference/keyed-jsx-floats.mjs +90 -41
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +367 -0
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +506 -50
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +1125 -50
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Progressive behavior for `<Diagram>`:
|
|
4
|
+
*
|
|
5
|
+
* - a persistent, animated title disclosure;
|
|
6
|
+
* - a compact horizontal expand control shown only when the canvas overflows;
|
|
7
|
+
* - an in-page expanded surface that moves the real figure (never a clone) to
|
|
8
|
+
* `<body>`, overlays its placeholder, and follows that placeholder while the
|
|
9
|
+
* page scrolls — the same spatial model as `TableOverflowExpander.astro`.
|
|
10
|
+
*
|
|
11
|
+
* Expansion is deliberately non-modal: there is no backdrop, page scrolling
|
|
12
|
+
* remains available, and the diagram closes only through its own shrink button
|
|
13
|
+
* or Escape. Tall expanded content scrolls vertically inside its viewport-
|
|
14
|
+
* capped surface.
|
|
15
|
+
*/
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
import {
|
|
20
|
+
savePersistedOpenState,
|
|
21
|
+
wirePersistedDetails,
|
|
22
|
+
} from '../disclosure/persisted-disclosure';
|
|
23
|
+
import { claimActiveSurface, releaseActiveSurface } from '../hover-panel';
|
|
24
|
+
|
|
25
|
+
const GROUP = 'diagram-overflow';
|
|
26
|
+
const EDGE_PX = 16;
|
|
27
|
+
const ANIMATION_MS = 220;
|
|
28
|
+
|
|
29
|
+
interface Expansion {
|
|
30
|
+
readonly figure: HTMLElement;
|
|
31
|
+
readonly placeholder: HTMLElement;
|
|
32
|
+
readonly spoofWrapper: HTMLElement;
|
|
33
|
+
readonly button: HTMLButtonElement;
|
|
34
|
+
readonly scroll: HTMLElement;
|
|
35
|
+
readonly reposition: () => void;
|
|
36
|
+
readonly evaluate: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let active: Expansion | null = null;
|
|
40
|
+
|
|
41
|
+
function sidebarLeftBound(): number {
|
|
42
|
+
const sidebar = document.querySelector<HTMLElement>('.sidebar-pane');
|
|
43
|
+
const visible = !!sidebar && getComputedStyle(sidebar).visibility !== 'hidden';
|
|
44
|
+
return visible ? sidebar!.getBoundingClientRect().right + EDGE_PX : EDGE_PX;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tocLeftBound(): number | null {
|
|
48
|
+
const toc = document.querySelector<HTMLElement>('.right-sidebar');
|
|
49
|
+
if (!toc) return null;
|
|
50
|
+
if (document.documentElement.hasAttribute('data-page-sidebar-hidden')) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!window.matchMedia('(min-width: 72rem)').matches) return null;
|
|
54
|
+
return toc.getBoundingClientRect().left;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function captionFor(figure: HTMLElement): string {
|
|
58
|
+
return (
|
|
59
|
+
figure.querySelector<HTMLElement>('.diagram__summary')?.textContent?.trim() ??
|
|
60
|
+
'this diagram'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function updateExpandButton(
|
|
65
|
+
figure: HTMLElement,
|
|
66
|
+
button: HTMLButtonElement,
|
|
67
|
+
expanded: boolean,
|
|
68
|
+
): void {
|
|
69
|
+
button.setAttribute('aria-expanded', String(expanded));
|
|
70
|
+
button.setAttribute(
|
|
71
|
+
'aria-label',
|
|
72
|
+
expanded
|
|
73
|
+
? `Shrink ${captionFor(figure)}`
|
|
74
|
+
: `Expand ${captionFor(figure)} horizontally`,
|
|
75
|
+
);
|
|
76
|
+
button.title = expanded ? 'Shrink diagram' : 'Expand diagram horizontally';
|
|
77
|
+
const srText = button.querySelector<HTMLElement>('.sr-only');
|
|
78
|
+
if (srText) srText.textContent = expanded ? 'Shrink diagram' : 'Expand diagram';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function updateScrollFocusability(scroll: HTMLElement, figure: HTMLElement): void {
|
|
82
|
+
if (scroll.scrollWidth > scroll.clientWidth + 1) {
|
|
83
|
+
scroll.tabIndex = 0;
|
|
84
|
+
scroll.setAttribute('aria-label', `Scrollable ${captionFor(figure)}`);
|
|
85
|
+
} else {
|
|
86
|
+
scroll.removeAttribute('tabindex');
|
|
87
|
+
scroll.removeAttribute('aria-label');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function evaluateOverflow(
|
|
92
|
+
figure: HTMLElement,
|
|
93
|
+
details: HTMLDetailsElement,
|
|
94
|
+
toolbar: HTMLElement,
|
|
95
|
+
button: HTMLButtonElement,
|
|
96
|
+
scroll: HTMLElement,
|
|
97
|
+
): void {
|
|
98
|
+
if (active?.figure === figure) return;
|
|
99
|
+
const overflows = details.open && scroll.scrollWidth > scroll.clientWidth + 1;
|
|
100
|
+
figure.dataset.diagramOverflow = String(overflows);
|
|
101
|
+
toolbar.hidden = !overflows;
|
|
102
|
+
button.hidden = !overflows;
|
|
103
|
+
updateScrollFocusability(scroll, figure);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collapseExpansion({ restoreFocus = true } = {}): void {
|
|
107
|
+
if (!active) return;
|
|
108
|
+
const state = active;
|
|
109
|
+
active = null;
|
|
110
|
+
window.removeEventListener('scroll', state.reposition, true);
|
|
111
|
+
window.removeEventListener('resize', state.reposition);
|
|
112
|
+
|
|
113
|
+
state.figure.classList.remove('diagram-is-expanded');
|
|
114
|
+
state.figure.style.cssText = '';
|
|
115
|
+
state.placeholder.replaceWith(state.figure);
|
|
116
|
+
state.spoofWrapper.remove();
|
|
117
|
+
updateExpandButton(state.figure, state.button, false);
|
|
118
|
+
state.evaluate();
|
|
119
|
+
if (restoreFocus) state.button.focus({ preventScroll: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function expand(
|
|
123
|
+
figure: HTMLElement,
|
|
124
|
+
button: HTMLButtonElement,
|
|
125
|
+
scroll: HTMLElement,
|
|
126
|
+
evaluate: () => void,
|
|
127
|
+
): void {
|
|
128
|
+
// An expanded diagram is dismissed only by its own shrink control or
|
|
129
|
+
// Escape. Ignore another diagram's expand control until the current one is
|
|
130
|
+
// restored.
|
|
131
|
+
if (active && active.figure !== figure) return;
|
|
132
|
+
|
|
133
|
+
claimActiveSurface(GROUP, () => {});
|
|
134
|
+
releaseActiveSurface(GROUP);
|
|
135
|
+
|
|
136
|
+
const neededWidth = scroll.scrollWidth;
|
|
137
|
+
const rect = figure.getBoundingClientRect();
|
|
138
|
+
const placeholder = document.createElement('div');
|
|
139
|
+
placeholder.className = 'diagram-expand-placeholder';
|
|
140
|
+
placeholder.style.height = `${rect.height}px`;
|
|
141
|
+
figure.replaceWith(placeholder);
|
|
142
|
+
|
|
143
|
+
// Retain every `.sl-markdown-content …` descendant rule after moving the
|
|
144
|
+
// original figure to `<body>`, without creating a layout box of its own.
|
|
145
|
+
const spoofWrapper = document.createElement('div');
|
|
146
|
+
spoofWrapper.className = 'sl-markdown-content';
|
|
147
|
+
spoofWrapper.style.display = 'contents';
|
|
148
|
+
document.body.append(spoofWrapper);
|
|
149
|
+
spoofWrapper.append(figure);
|
|
150
|
+
figure.classList.add('diagram-is-expanded');
|
|
151
|
+
|
|
152
|
+
const reposition = () => {
|
|
153
|
+
const headerBottom =
|
|
154
|
+
document.querySelector<HTMLElement>('.header')?.getBoundingClientRect()
|
|
155
|
+
.bottom ?? 0;
|
|
156
|
+
const verticalStart = Math.max(EDGE_PX, headerBottom + EDGE_PX);
|
|
157
|
+
const placeholderRect = placeholder.getBoundingClientRect();
|
|
158
|
+
const sidebarEdge = sidebarLeftBound();
|
|
159
|
+
const viewportEdge = window.innerWidth - EDGE_PX;
|
|
160
|
+
const tocBoundary = tocLeftBound();
|
|
161
|
+
const rightBound = Math.min(
|
|
162
|
+
viewportEdge,
|
|
163
|
+
Math.max(
|
|
164
|
+
tocBoundary !== null ? tocBoundary - EDGE_PX : viewportEdge,
|
|
165
|
+
sidebarEdge + neededWidth,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const availableWidth = Math.max(rightBound - sidebarEdge, 80);
|
|
169
|
+
const width = Math.min(neededWidth, availableWidth);
|
|
170
|
+
const left = sidebarEdge + (availableWidth - width) / 2;
|
|
171
|
+
|
|
172
|
+
// Exact placeholder tracking makes this an overlay at the diagram's
|
|
173
|
+
// document position, not a sticky/modal surface. It naturally moves
|
|
174
|
+
// above or below the viewport as the reader scrolls past it.
|
|
175
|
+
const top = placeholderRect.top;
|
|
176
|
+
figure.style.position = 'fixed';
|
|
177
|
+
figure.style.top = `${top}px`;
|
|
178
|
+
figure.style.left = `${left}px`;
|
|
179
|
+
figure.style.width = `${width}px`;
|
|
180
|
+
const visibleTop = Math.max(verticalStart, top);
|
|
181
|
+
const maxHeight = Math.max(
|
|
182
|
+
window.innerHeight - visibleTop - EDGE_PX,
|
|
183
|
+
80,
|
|
184
|
+
);
|
|
185
|
+
figure.style.maxHeight = `${maxHeight}px`;
|
|
186
|
+
const titleHeight =
|
|
187
|
+
figure
|
|
188
|
+
.querySelector<HTMLElement>('.diagram__summary')
|
|
189
|
+
?.getBoundingClientRect().height ?? 0;
|
|
190
|
+
figure.style.setProperty(
|
|
191
|
+
'--diagram-expanded-body-height',
|
|
192
|
+
`${Math.max(maxHeight - titleHeight, 40)}px`,
|
|
193
|
+
);
|
|
194
|
+
updateScrollFocusability(scroll, figure);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
active = {
|
|
198
|
+
figure,
|
|
199
|
+
placeholder,
|
|
200
|
+
spoofWrapper,
|
|
201
|
+
button,
|
|
202
|
+
scroll,
|
|
203
|
+
reposition,
|
|
204
|
+
evaluate,
|
|
205
|
+
};
|
|
206
|
+
updateExpandButton(figure, button, true);
|
|
207
|
+
reposition();
|
|
208
|
+
window.addEventListener('scroll', reposition, {
|
|
209
|
+
passive: true,
|
|
210
|
+
capture: true,
|
|
211
|
+
});
|
|
212
|
+
window.addEventListener('resize', reposition, { passive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function wireDisclosure(
|
|
216
|
+
figure: HTMLElement,
|
|
217
|
+
details: HTMLDetailsElement,
|
|
218
|
+
body: HTMLElement,
|
|
219
|
+
evaluate: () => void,
|
|
220
|
+
): void {
|
|
221
|
+
const summary = details.querySelector<HTMLElement>(':scope > summary');
|
|
222
|
+
if (!summary) return;
|
|
223
|
+
const defaultOpen = details.open;
|
|
224
|
+
const remember = details.dataset.rememberState === 'true';
|
|
225
|
+
if (remember) {
|
|
226
|
+
wirePersistedDetails(details, `diagram:${figure.id}`, defaultOpen);
|
|
227
|
+
}
|
|
228
|
+
details.dataset.state = details.open ? 'open' : 'closed';
|
|
229
|
+
|
|
230
|
+
// An external/bookmarked deep link reveals the complete diagram. Later
|
|
231
|
+
// in-page jumps retain the reader's persisted minimized state.
|
|
232
|
+
if (location.hash === `#${figure.id}`) {
|
|
233
|
+
details.open = true;
|
|
234
|
+
details.dataset.state = 'open';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
238
|
+
let finishTimer: number | null = null;
|
|
239
|
+
let transitionEnd: ((event: TransitionEvent) => void) | null = null;
|
|
240
|
+
|
|
241
|
+
const cancelAnimation = () => {
|
|
242
|
+
if (finishTimer !== null) window.clearTimeout(finishTimer);
|
|
243
|
+
finishTimer = null;
|
|
244
|
+
if (transitionEnd) body.removeEventListener('transitionend', transitionEnd);
|
|
245
|
+
transitionEnd = null;
|
|
246
|
+
body.style.removeProperty('transition');
|
|
247
|
+
body.style.removeProperty('overflow');
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const setOpen = (open: boolean) => {
|
|
251
|
+
cancelAnimation();
|
|
252
|
+
const current = body.getBoundingClientRect().height;
|
|
253
|
+
if (open) details.open = true;
|
|
254
|
+
details.dataset.state = open ? 'open' : 'closed';
|
|
255
|
+
if (remember) savePersistedOpenState(`diagram:${figure.id}`, open);
|
|
256
|
+
|
|
257
|
+
if (reduceMotion.matches) {
|
|
258
|
+
details.open = open;
|
|
259
|
+
body.style.maxHeight = open ? '' : '0px';
|
|
260
|
+
if (active?.figure === figure) {
|
|
261
|
+
active.placeholder.style.height = `${figure.getBoundingClientRect().height}px`;
|
|
262
|
+
}
|
|
263
|
+
evaluate();
|
|
264
|
+
active?.reposition();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
body.style.transition = 'none';
|
|
269
|
+
body.style.maxHeight = `${current}px`;
|
|
270
|
+
body.style.overflow = 'hidden';
|
|
271
|
+
void body.offsetHeight;
|
|
272
|
+
body.style.removeProperty('transition');
|
|
273
|
+
|
|
274
|
+
const expandedLimit = Number.parseFloat(
|
|
275
|
+
getComputedStyle(figure).getPropertyValue(
|
|
276
|
+
'--diagram-expanded-body-height',
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
const target = open
|
|
280
|
+
? Math.min(
|
|
281
|
+
body.scrollHeight,
|
|
282
|
+
Number.isFinite(expandedLimit) ? expandedLimit : Number.POSITIVE_INFINITY,
|
|
283
|
+
)
|
|
284
|
+
: 0;
|
|
285
|
+
const finish = () => {
|
|
286
|
+
cancelAnimation();
|
|
287
|
+
details.open = open;
|
|
288
|
+
body.style.maxHeight = open ? '' : '0px';
|
|
289
|
+
if (active?.figure === figure) {
|
|
290
|
+
active.placeholder.style.height = `${figure.getBoundingClientRect().height}px`;
|
|
291
|
+
}
|
|
292
|
+
evaluate();
|
|
293
|
+
active?.reposition();
|
|
294
|
+
};
|
|
295
|
+
transitionEnd = (event) => {
|
|
296
|
+
if (event.target === body && event.propertyName === 'max-height') finish();
|
|
297
|
+
};
|
|
298
|
+
body.addEventListener('transitionend', transitionEnd);
|
|
299
|
+
finishTimer = window.setTimeout(finish, ANIMATION_MS + 60);
|
|
300
|
+
requestAnimationFrame(() => {
|
|
301
|
+
body.style.maxHeight = `${target}px`;
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
summary.addEventListener('click', (event) => {
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
setOpen(details.dataset.state !== 'open');
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function setup(): void {
|
|
312
|
+
for (const figure of document.querySelectorAll<HTMLElement>(
|
|
313
|
+
'.diagram[data-diagram-expandable]',
|
|
314
|
+
)) {
|
|
315
|
+
if (figure.dataset.diagramExpandReady === 'true') continue;
|
|
316
|
+
const details = figure.querySelector<HTMLDetailsElement>(
|
|
317
|
+
'[data-diagram-details]',
|
|
318
|
+
);
|
|
319
|
+
const body = figure.querySelector<HTMLElement>('[data-diagram-body]');
|
|
320
|
+
const toolbar = figure.querySelector<HTMLElement>('[data-diagram-toolbar]');
|
|
321
|
+
const button = figure.querySelector<HTMLButtonElement>('[data-diagram-expand]');
|
|
322
|
+
const scroll = figure.querySelector<HTMLElement>('.diagram-scroll');
|
|
323
|
+
if (!details || !body || !toolbar || !button || !scroll) continue;
|
|
324
|
+
figure.dataset.diagramExpandReady = 'true';
|
|
325
|
+
|
|
326
|
+
const evaluate = () =>
|
|
327
|
+
evaluateOverflow(figure, details, toolbar, button, scroll);
|
|
328
|
+
button.addEventListener('click', () => {
|
|
329
|
+
if (active?.figure === figure) collapseExpansion();
|
|
330
|
+
else expand(figure, button, scroll, evaluate);
|
|
331
|
+
});
|
|
332
|
+
new ResizeObserver(evaluate).observe(scroll);
|
|
333
|
+
details.addEventListener('toggle', evaluate);
|
|
334
|
+
wireDisclosure(figure, details, body, evaluate);
|
|
335
|
+
updateExpandButton(figure, button, false);
|
|
336
|
+
evaluate();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setup();
|
|
341
|
+
window.addEventListener('explico:prepare-static-export', () => {
|
|
342
|
+
collapseExpansion({ restoreFocus: false });
|
|
343
|
+
});
|
|
344
|
+
document.addEventListener('astro:page-load', setup);
|
|
345
|
+
document.addEventListener('keydown', (event) => {
|
|
346
|
+
if (event.key === 'Escape' && active) collapseExpansion();
|
|
347
|
+
});
|
|
348
|
+
</script>
|
|
349
|
+
|
|
350
|
+
<style is:global>
|
|
351
|
+
.diagram-is-expanded {
|
|
352
|
+
/* Above the side rails, but below Starlight's sticky site header. */
|
|
353
|
+
z-index: calc(var(--sl-z-index-navbar) - 1);
|
|
354
|
+
margin: 0 !important;
|
|
355
|
+
max-width: none !important;
|
|
356
|
+
contain: none !important;
|
|
357
|
+
overflow-y: hidden;
|
|
358
|
+
background: var(--sl-color-bg);
|
|
359
|
+
box-shadow: var(--sl-shadow-lg);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.diagram-is-expanded .diagram__titlebar,
|
|
363
|
+
.diagram-is-expanded .diagram__details {
|
|
364
|
+
max-height: inherit;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.diagram-is-expanded .diagram__details {
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.diagram-is-expanded .diagram__body {
|
|
373
|
+
min-height: 0;
|
|
374
|
+
max-height: var(--diagram-expanded-body-height);
|
|
375
|
+
flex: 1 1 auto;
|
|
376
|
+
overflow-x: hidden;
|
|
377
|
+
overflow-y: auto;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@media print {
|
|
381
|
+
.diagram-expand-placeholder {
|
|
382
|
+
display: none;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
</style>
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* won't navigate to a fragment that's already current;
|
|
20
20
|
* - moving keyboard focus onto the heading so screen-reader users land on it.
|
|
21
21
|
*
|
|
22
|
+
* `sectionParts()` (the section-boundary walk) lives in `./section-parts.ts`,
|
|
23
|
+
* shared with `CollapsibleSections.astro`.
|
|
24
|
+
*
|
|
22
25
|
* It deliberately does *not* intercept an ordinary click that changes the
|
|
23
26
|
* hash. `:target` is set only by a genuine same-document fragment
|
|
24
27
|
* navigation — never by `history.pushState` (a common trap: pushState
|
|
@@ -33,11 +36,12 @@
|
|
|
33
36
|
---
|
|
34
37
|
|
|
35
38
|
<script>
|
|
39
|
+
import { HEADINGS, sectionParts } from './section-parts';
|
|
40
|
+
|
|
36
41
|
(() => {
|
|
37
42
|
const root = document.querySelector('.sl-markdown-content');
|
|
38
43
|
if (!root) return;
|
|
39
44
|
|
|
40
|
-
const HEADINGS = 'h1, h2, h3, h4, h5, h6';
|
|
41
45
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
42
46
|
|
|
43
47
|
const isContentHeading = (el: Element | null): el is HTMLElement =>
|
|
@@ -70,22 +74,6 @@
|
|
|
70
74
|
);
|
|
71
75
|
};
|
|
72
76
|
|
|
73
|
-
const sectionParts = (heading: HTMLElement): Element[] => {
|
|
74
|
-
const wrapper = heading.closest('.sl-heading-wrapper') ?? heading;
|
|
75
|
-
const rank = Number(heading.tagName[1]);
|
|
76
|
-
const parts: Element[] = [wrapper];
|
|
77
|
-
let node = wrapper.nextElementSibling;
|
|
78
|
-
while (node) {
|
|
79
|
-
const inner = node.matches(HEADINGS)
|
|
80
|
-
? node
|
|
81
|
-
: node.querySelector(HEADINGS);
|
|
82
|
-
if (inner && Number(inner.tagName[1]) <= rank) break;
|
|
83
|
-
parts.push(node);
|
|
84
|
-
node = node.nextElementSibling;
|
|
85
|
-
}
|
|
86
|
-
return parts;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
77
|
const activate = (heading: HTMLElement | null, doScroll: boolean) => {
|
|
90
78
|
if (!heading) return;
|
|
91
79
|
const parts = sectionParts(heading);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* Progressive enhancement for keyed floats — equations (Step D7), extended
|
|
4
|
-
* to `<Table>`/`<Figure
|
|
4
|
+
* to `<Table>`/`<Figure>`/`<Diagram>`. Generalizes the original
|
|
5
5
|
* equation-only `EquationEnhancer`: the scroll/focus/flash mechanics are
|
|
6
6
|
* unchanged, parameterized over `KINDS` below so a deep-link to `#eq-`,
|
|
7
|
-
* `#tab-`, or `#
|
|
7
|
+
* `#tab-`, `#fig-`, or `#dia-` is handled the same way.
|
|
8
8
|
*
|
|
9
9
|
* The no-JS baseline is complete: `remark-notation` emits a real
|
|
10
|
-
* `id="eq-<key>"` / `id="tab-<key>"` / `id="fig-<key>"`
|
|
10
|
+
* `id="eq-<key>"` / `id="tab-<key>"` / `id="fig-<key>"` /
|
|
11
|
+
* `id="dia-<key>"` container, a
|
|
11
12
|
* focusable number `<a>`, and a `[[…]]` reference renders a plain anchor;
|
|
12
13
|
* `global.css` does the `:target` deep-link highlight (with a
|
|
13
14
|
* `prefers-reduced-motion` fallback) and the sticky-header scroll offset via
|
|
@@ -69,6 +70,12 @@
|
|
|
69
70
|
numberSelector: '.keyed-figure__number',
|
|
70
71
|
reveal: false,
|
|
71
72
|
},
|
|
73
|
+
{
|
|
74
|
+
prefix: '#dia-',
|
|
75
|
+
triggerSelector: 'a.diagram-ref, a.diagram__title-anchor',
|
|
76
|
+
numberSelector: '.diagram__number',
|
|
77
|
+
reveal: false,
|
|
78
|
+
},
|
|
72
79
|
];
|
|
73
80
|
const TRIGGER_SELECTOR = KINDS.map((kind) => kind.triggerSelector).join(', ');
|
|
74
81
|
const kindForHash = (hash: string) =>
|
|
@@ -123,6 +130,77 @@
|
|
|
123
130
|
}
|
|
124
131
|
});
|
|
125
132
|
|
|
133
|
+
// "Copy LaTeX" for an equation, two ways: its own button (only a numbered
|
|
134
|
+
// display equation has one — `data-eq-copy`, `remark-notation.mjs`'s
|
|
135
|
+
// `equationNode`), or a click anywhere else in the equation that isn't a
|
|
136
|
+
// symbol with its own notation card (an operator, a bare number, a
|
|
137
|
+
// parenthesis, …) — the same gesture works on an inline equation too
|
|
138
|
+
// (`inlineEquationNode`), which has no room for a persistent button of
|
|
139
|
+
// its own without breaking the reading line. A notation-registry symbol
|
|
140
|
+
// keeps its existing click-to-pin behavior either way
|
|
141
|
+
// (`NotationLayer.astro`), so this skips anything under `[data-notation-key]`.
|
|
142
|
+
root.addEventListener('click', (event) => {
|
|
143
|
+
const origin = event.target instanceof Element ? event.target : null;
|
|
144
|
+
if (!origin) return;
|
|
145
|
+
if (origin.closest('a.keyed-equation__number, [data-notation-key]')) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ownButton = origin.closest<HTMLButtonElement>('[data-eq-copy]');
|
|
150
|
+
const container = ownButton
|
|
151
|
+
? null
|
|
152
|
+
: origin.closest<HTMLElement>('.keyed-equation, .inline-equation');
|
|
153
|
+
if (!ownButton && (!container || !origin.closest('.katex-html'))) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const feedbackTarget =
|
|
158
|
+
ownButton ??
|
|
159
|
+
container?.querySelector<HTMLElement>('.keyed-equation__copy') ??
|
|
160
|
+
container;
|
|
161
|
+
const latex =
|
|
162
|
+
ownButton?.dataset.latex ?? feedbackTarget?.dataset.latex ?? '';
|
|
163
|
+
if (!feedbackTarget || latex.length === 0 || !navigator.clipboard) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
navigator.clipboard.writeText(latex).then(
|
|
168
|
+
() => {
|
|
169
|
+
feedbackTarget.dataset.copied = 'true';
|
|
170
|
+
window.setTimeout(() => {
|
|
171
|
+
delete feedbackTarget.dataset.copied;
|
|
172
|
+
}, 1200);
|
|
173
|
+
},
|
|
174
|
+
() => {
|
|
175
|
+
/* Clipboard blocked: nothing more we can do here. */
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// A tooltip for the gesture above, and only for the part of the equation
|
|
181
|
+
// it actually applies to — an explained symbol has its own hover card
|
|
182
|
+
// (`NotationLayer.astro`) and needs no tooltip layered on top of it.
|
|
183
|
+
// `mouseover`/`mouseout` (not `mouseenter`/`mouseleave`) because they
|
|
184
|
+
// bubble, which delegation on `root` needs.
|
|
185
|
+
root.addEventListener('mouseover', (event) => {
|
|
186
|
+
const origin = event.target instanceof Element ? event.target : null;
|
|
187
|
+
if (!origin || origin.getAttribute('title') === 'Copy LaTeX') return;
|
|
188
|
+
if (origin.closest('[data-notation-key], [data-eq-copy]')) return;
|
|
189
|
+
if (
|
|
190
|
+
!origin.closest('.keyed-equation .katex-html, .inline-equation .katex-html')
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
origin.setAttribute('title', 'Copy LaTeX');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
root.addEventListener('mouseout', (event) => {
|
|
198
|
+
const origin = event.target instanceof Element ? event.target : null;
|
|
199
|
+
if (origin?.getAttribute('title') === 'Copy LaTeX') {
|
|
200
|
+
origin.removeAttribute('title');
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
126
204
|
window.addEventListener('hashchange', () => {
|
|
127
205
|
const kind = kindForHash(location.hash);
|
|
128
206
|
if (!kind) return;
|
|
@@ -40,6 +40,39 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
40
40
|
|
|
41
41
|
const root = document.documentElement;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* `.main-pane`'s width formula (`global.css`) caps itself against
|
|
45
|
+
* `var(--toc-actual-width, ...)` so it never grows into the "On this
|
|
46
|
+
* page" rail — `.right-sidebar` is edge-anchored (`inset-inline-end: 0`)
|
|
47
|
+
* rather than positioned relative to `.main-pane`, so nothing else
|
|
48
|
+
* guarantees that any more. A worst-case CSS estimate alone would have
|
|
49
|
+
* to assume the rail's own 80ch cap at all times, which is far wider
|
|
50
|
+
* than any actual page's headings need and would cap `.main-pane`
|
|
51
|
+
* well short of where it could safely go. Measuring the rail's real
|
|
52
|
+
* rendered width here and writing it to `--toc-actual-width` lets the
|
|
53
|
+
* cap match reality instead — on a typical page of short headings this
|
|
54
|
+
* ends up nowhere near binding, so `.main-pane` keeps the same width
|
|
55
|
+
* Starlight's own formula already gave it.
|
|
56
|
+
*
|
|
57
|
+
* 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, not
|
|
59
|
+
* against how much room `.main-pane` leaves it (that dependency is
|
|
60
|
+
* exactly what the edge anchor removed), so measuring it here doesn't
|
|
61
|
+
* need to react to anything this function itself changes.
|
|
62
|
+
*/
|
|
63
|
+
function updateTocReservation(): void {
|
|
64
|
+
const panel = document.querySelector<HTMLElement>('.right-sidebar');
|
|
65
|
+
if (!panel) return;
|
|
66
|
+
// Only meaningful for the open, two-column desktop layout — collapsed
|
|
67
|
+
// (`data-page-sidebar-hidden`) uses its own edge-rail width formula
|
|
68
|
+
// that never reads this variable, and below the breakpoint there's no
|
|
69
|
+
// `.right-sidebar` column to cap `.main-pane` against at all.
|
|
70
|
+
if (root.hasAttribute('data-page-sidebar-hidden')) return;
|
|
71
|
+
if (!window.matchMedia('(min-width: 72rem)').matches) return;
|
|
72
|
+
const width = panel.getBoundingClientRect().width;
|
|
73
|
+
if (width > 0) root.style.setProperty('--toc-actual-width', `${width}px`);
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
// The one seam for per-viewer UI preferences (Phase H1). This component never
|
|
44
77
|
// touches browser storage directly; a future SSR host swaps in a different
|
|
45
78
|
// PreferenceStore and nothing here changes.
|
|
@@ -80,6 +113,10 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
80
113
|
const hidden = !root.hasAttribute(settings.attribute);
|
|
81
114
|
applyState(button, region, hidden);
|
|
82
115
|
writePreference(settings.storageKey, hidden);
|
|
116
|
+
// The initial measurement below skips a rail that starts collapsed
|
|
117
|
+
// (there's nothing on screen yet to measure); catch it here the
|
|
118
|
+
// first time it actually opens.
|
|
119
|
+
if (region === 'contents' && !hidden) updateTocReservation();
|
|
83
120
|
|
|
84
121
|
// A pointer click should leave a newly collapsed rail collapsed even
|
|
85
122
|
// though the pointer was inside the full-width panel when clicked.
|
|
@@ -99,4 +136,17 @@ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
|
|
|
99
136
|
}
|
|
100
137
|
});
|
|
101
138
|
});
|
|
139
|
+
|
|
140
|
+
updateTocReservation();
|
|
141
|
+
let tocReservationResizeTimer: number | null = null;
|
|
142
|
+
window.addEventListener(
|
|
143
|
+
'resize',
|
|
144
|
+
() => {
|
|
145
|
+
if (tocReservationResizeTimer !== null) {
|
|
146
|
+
window.clearTimeout(tocReservationResizeTimer);
|
|
147
|
+
}
|
|
148
|
+
tocReservationResizeTimer = window.setTimeout(updateTocReservation, 150);
|
|
149
|
+
},
|
|
150
|
+
{ passive: true },
|
|
151
|
+
);
|
|
102
152
|
</script>
|
|
@@ -23,7 +23,68 @@ import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
|
|
|
23
23
|
size="1rem"
|
|
24
24
|
/>
|
|
25
25
|
</button>
|
|
26
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
id="starlight__navigation-content"
|
|
28
|
+
class="layout-navigation-content layout-compact-sidebar"
|
|
29
|
+
>
|
|
27
30
|
<DefaultSidebar />
|
|
31
|
+
<script>
|
|
32
|
+
import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
|
|
33
|
+
|
|
34
|
+
(() => {
|
|
35
|
+
if (!matchMedia('(min-width: 50rem)').matches) return;
|
|
36
|
+
const root = document.querySelector('#starlight__navigation-content');
|
|
37
|
+
const stateRoot = root?.querySelector('sl-sidebar-state-persist');
|
|
38
|
+
if (!stateRoot) return;
|
|
39
|
+
|
|
40
|
+
const groups = [
|
|
41
|
+
...stateRoot.querySelectorAll(':scope > ul > li > details'),
|
|
42
|
+
];
|
|
43
|
+
const storageKey = 'explico-sidebar-groups';
|
|
44
|
+
const preferences = createBrowserPreferenceStore();
|
|
45
|
+
let compact = groups.map(() => true);
|
|
46
|
+
try {
|
|
47
|
+
const saved = JSON.parse(preferences.get(storageKey) || 'null');
|
|
48
|
+
if (
|
|
49
|
+
saved?.hash === stateRoot.dataset.hash &&
|
|
50
|
+
Array.isArray(saved.compact)
|
|
51
|
+
) {
|
|
52
|
+
compact = groups.map((_, index) => saved.compact[index] !== false);
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
const save = () => {
|
|
57
|
+
try {
|
|
58
|
+
preferences.set(
|
|
59
|
+
storageKey,
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
hash: stateRoot.dataset.hash,
|
|
62
|
+
compact: groups.map((group) => group.hasAttribute('data-compact')),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
} catch {}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
groups.forEach((group, index) => {
|
|
69
|
+
// The links remain rendered and operable in compact mode; only their
|
|
70
|
+
// labels collapse. Keep <details> open so this stays true for assistive
|
|
71
|
+
// technology as well as pointer users.
|
|
72
|
+
group.setAttribute('open', '');
|
|
73
|
+
group.toggleAttribute('data-compact', compact[index]);
|
|
74
|
+
const summary = group.querySelector(':scope > summary');
|
|
75
|
+
summary?.setAttribute('aria-expanded', String(!compact[index]));
|
|
76
|
+
summary?.addEventListener('click', (event) => {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
group.toggleAttribute('data-compact');
|
|
79
|
+
group.setAttribute('open', '');
|
|
80
|
+
summary.setAttribute(
|
|
81
|
+
'aria-expanded',
|
|
82
|
+
String(!group.hasAttribute('data-compact')),
|
|
83
|
+
);
|
|
84
|
+
save();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
})();
|
|
88
|
+
</script>
|
|
28
89
|
</div>
|
|
29
90
|
</div>
|