explico 0.2.0 → 0.4.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 +173 -0
- package/README.md +67 -7
- package/dist/compiler/astro.d.ts +30 -0
- package/dist/compiler/astro.js +79 -0
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +43 -9
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +80 -4
- package/dist/compiler/course-config.js +12 -0
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/lesson-derivation.d.ts +1 -1
- package/dist/compiler/lesson-derivation.js +2 -2
- package/dist/compiler/manifest.d.ts +98 -2
- package/dist/compiler/manifest.js +153 -24
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +51 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +10 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +80 -13
- package/dist/reference/equations.mjs +109 -19
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/keyed-float-validate.d.ts +73 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +64 -0
- package/dist/reference/keyed-jsx-floats.mjs +191 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/remark-notation.d.mts +61 -0
- package/dist/reference/remark-notation.mjs +344 -10
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +100 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +169 -0
- package/docs/authoring/notation.md +297 -0
- package/docs/authoring/structure.md +139 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/astro.ts +127 -0
- package/src/compiler/cli.ts +52 -7
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +93 -3
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/lesson-derivation.ts +2 -2
- package/src/compiler/manifest.ts +320 -26
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/notation/NotationLayer.astro +11 -0
- package/src/components/notation/render-notation-math.ts +13 -2
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSidebar.astro +62 -1
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +15 -2
- package/src/components/table/Table.astro +105 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +83 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +8 -1
- package/src/reference/equations.mjs +109 -19
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +210 -0
- package/src/reference/keyed-jsx-floats.mjs +191 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +344 -10
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +244 -7
- package/src/components/starlight/EquationEnhancer.astro +0 -87
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The `[?]` "about this material" control, rendered top-right in the header next
|
|
4
|
+
* to the social icons (`LayoutSocialIcons.astro` composes both). It appears on
|
|
5
|
+
* every route — landing page and study-material pages alike.
|
|
6
|
+
*
|
|
7
|
+
* All of its data comes from the one compiler manifest (`manifest.about`): the
|
|
8
|
+
* engine identity from `explico`'s own `package.json`, the course's reader-facing
|
|
9
|
+
* `about` block, and the knowledge version resolved against
|
|
10
|
+
* `content/CHANGELOG.md`. This component never re-reads the filesystem.
|
|
11
|
+
*
|
|
12
|
+
* It is a plain `<details>` disclosure: the browser gives keyboard toggle and
|
|
13
|
+
* `aria-expanded` for free; the inline script only adds Escape-to-close and
|
|
14
|
+
* outside-click-to-close.
|
|
15
|
+
*/
|
|
16
|
+
import { loadManifest } from '../../compiler/manifest';
|
|
17
|
+
|
|
18
|
+
const { about } = await loadManifest();
|
|
19
|
+
const { engine, material, knowledge, authors, disclaimer, changelogHref } =
|
|
20
|
+
about;
|
|
21
|
+
|
|
22
|
+
/** `mailto:` an email, pass through an http(s) URL, plain text otherwise. */
|
|
23
|
+
function contactHref(contact: string): string | undefined {
|
|
24
|
+
if (/^https?:\/\//i.test(contact)) return contact;
|
|
25
|
+
if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(contact)) return `mailto:${contact}`;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const authorLabel = authors.length > 1 ? 'Authors' : 'Author';
|
|
30
|
+
const knowledgeLine = knowledge.date
|
|
31
|
+
? `${knowledge.version} · ${knowledge.date}`
|
|
32
|
+
: knowledge.version;
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
<details class="about" data-about-panel>
|
|
36
|
+
<summary
|
|
37
|
+
class="about-toggle"
|
|
38
|
+
aria-label={`About ${material.name}`}
|
|
39
|
+
title={`About ${material.name}`}
|
|
40
|
+
>
|
|
41
|
+
<span aria-hidden="true">?</span>
|
|
42
|
+
</summary>
|
|
43
|
+
<div class="about-body">
|
|
44
|
+
<p class="about-name">{material.name}</p>
|
|
45
|
+
{material.summary && <p class="about-summary">{material.summary}</p>}
|
|
46
|
+
|
|
47
|
+
<dl class="about-facts">
|
|
48
|
+
<div>
|
|
49
|
+
<dt>Knowledge version</dt>
|
|
50
|
+
<dd>{knowledgeLine}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<dt>Engine</dt>
|
|
54
|
+
<dd>
|
|
55
|
+
{
|
|
56
|
+
engine.homepage ? (
|
|
57
|
+
<a href={engine.homepage}>
|
|
58
|
+
{engine.name} {engine.version}
|
|
59
|
+
</a>
|
|
60
|
+
) : (
|
|
61
|
+
<span>
|
|
62
|
+
{engine.name} {engine.version}
|
|
63
|
+
</span>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
</dd>
|
|
67
|
+
</div>
|
|
68
|
+
{
|
|
69
|
+
authors.length > 0 && (
|
|
70
|
+
<div>
|
|
71
|
+
<dt>{authorLabel}</dt>
|
|
72
|
+
<dd>
|
|
73
|
+
{authors.map((author, index) => {
|
|
74
|
+
const href = author.contact
|
|
75
|
+
? contactHref(author.contact)
|
|
76
|
+
: undefined;
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
{index > 0 && ', '}
|
|
80
|
+
{author.name}
|
|
81
|
+
{author.contact && (
|
|
82
|
+
<>
|
|
83
|
+
{' '}
|
|
84
|
+
(
|
|
85
|
+
{href ? (
|
|
86
|
+
<a href={href}>{author.contact}</a>
|
|
87
|
+
) : (
|
|
88
|
+
author.contact
|
|
89
|
+
)}
|
|
90
|
+
)
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
})}
|
|
96
|
+
</dd>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
</dl>
|
|
101
|
+
|
|
102
|
+
{disclaimer && <p class="about-disclaimer">{disclaimer}</p>}
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
changelogHref && (
|
|
106
|
+
<p class="about-links">
|
|
107
|
+
<a href={changelogHref}>What changed in this material</a>
|
|
108
|
+
</p>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
</div>
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
@layer starlight.components {
|
|
116
|
+
.about {
|
|
117
|
+
position: relative;
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.about-toggle {
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
width: 1.75rem;
|
|
126
|
+
height: 1.75rem;
|
|
127
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
128
|
+
border-radius: 999px;
|
|
129
|
+
color: var(--sl-color-gray-1);
|
|
130
|
+
background: var(--sl-color-bg);
|
|
131
|
+
font-size: var(--sl-text-sm);
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
line-height: 1;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
user-select: none;
|
|
136
|
+
list-style: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.about-toggle::-webkit-details-marker {
|
|
140
|
+
display: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.about-toggle:hover {
|
|
144
|
+
color: var(--sl-color-text);
|
|
145
|
+
border-color: var(--sl-color-gray-4);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.about[open] .about-toggle {
|
|
149
|
+
color: var(--sl-color-text);
|
|
150
|
+
border-color: var(--sl-color-accent);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.about-toggle:focus-visible {
|
|
154
|
+
outline: 2px solid var(--sl-color-accent);
|
|
155
|
+
outline-offset: 2px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.about-body {
|
|
159
|
+
position: absolute;
|
|
160
|
+
inset-inline-end: 0;
|
|
161
|
+
top: calc(100% + 0.5rem);
|
|
162
|
+
z-index: 90;
|
|
163
|
+
width: min(21rem, calc(100vw - 1.5rem));
|
|
164
|
+
max-height: min(70vh, 34rem);
|
|
165
|
+
overflow-y: auto;
|
|
166
|
+
padding: 1rem;
|
|
167
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
168
|
+
border-top: 0.25rem solid var(--sl-color-accent);
|
|
169
|
+
border-radius: 0.65rem;
|
|
170
|
+
color: var(--sl-color-text);
|
|
171
|
+
background: var(--sl-color-bg);
|
|
172
|
+
box-shadow: var(--sl-shadow-lg);
|
|
173
|
+
font-size: var(--sl-text-xs);
|
|
174
|
+
line-height: 1.5;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.about-name {
|
|
178
|
+
margin: 0;
|
|
179
|
+
font-size: var(--sl-text-sm);
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.about-summary {
|
|
184
|
+
margin: 0.35rem 0 0;
|
|
185
|
+
color: var(--sl-color-gray-2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.about-facts {
|
|
189
|
+
margin: 0.75rem 0 0;
|
|
190
|
+
display: grid;
|
|
191
|
+
gap: 0.4rem;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.about-facts > div {
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-columns: 8rem 1fr;
|
|
197
|
+
gap: 0.5rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.about-facts dt {
|
|
201
|
+
color: var(--sl-color-gray-3);
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
letter-spacing: 0.03em;
|
|
204
|
+
font-size: 0.65rem;
|
|
205
|
+
line-height: 1.6;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.about-facts dd {
|
|
209
|
+
margin: 0;
|
|
210
|
+
overflow-wrap: anywhere;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.about-disclaimer {
|
|
214
|
+
margin: 0.85rem 0 0;
|
|
215
|
+
padding-top: 0.75rem;
|
|
216
|
+
border-top: 1px solid var(--sl-color-gray-6);
|
|
217
|
+
color: var(--sl-color-gray-2);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.about-links {
|
|
221
|
+
margin: 0.75rem 0 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.about-body a {
|
|
225
|
+
color: var(--sl-color-text-accent);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
|
|
230
|
+
<script>
|
|
231
|
+
(() => {
|
|
232
|
+
const panels = Array.from(
|
|
233
|
+
document.querySelectorAll<HTMLDetailsElement>('details[data-about-panel]'),
|
|
234
|
+
);
|
|
235
|
+
if (panels.length === 0) return;
|
|
236
|
+
|
|
237
|
+
document.addEventListener('keydown', (event) => {
|
|
238
|
+
if (event.key !== 'Escape') return;
|
|
239
|
+
for (const details of panels) {
|
|
240
|
+
if (!details.open) continue;
|
|
241
|
+
details.open = false;
|
|
242
|
+
details.querySelector<HTMLElement>('summary')?.focus();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
document.addEventListener('pointerdown', (event) => {
|
|
247
|
+
const target = event.target instanceof Node ? event.target : null;
|
|
248
|
+
for (const details of panels) {
|
|
249
|
+
if (details.open && (!target || !details.contains(target))) {
|
|
250
|
+
details.open = false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
})();
|
|
255
|
+
</script>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* Progressive enhancement for content-heading deep links (Step F3), the sibling
|
|
4
|
-
* of `
|
|
4
|
+
* of `KeyedFloatEnhancer.astro`.
|
|
5
5
|
*
|
|
6
6
|
* The no-JS baseline is complete: `rehype-heading-anchors` emits Starlight's
|
|
7
7
|
* `.sl-heading-wrapper` + `a.sl-anchor-link` markup, its bundled
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* navigation — never by `history.pushState` (a common trap: pushState
|
|
25
25
|
* updates the URL but leaves `:target`, and thus `heading-flash`, dark).
|
|
26
26
|
* Letting the click through and reacting to the `hashchange` it produces is
|
|
27
|
-
* what actually plays the highlight — see `
|
|
27
|
+
* what actually plays the highlight — see `KeyedFloatEnhancer.astro`, which
|
|
28
28
|
* this mirrors.
|
|
29
29
|
*
|
|
30
30
|
* Everything here is gated on `prefers-reduced-motion` and degrades to the
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
const heading = headingForId(href.slice(1));
|
|
114
114
|
if (!heading) return;
|
|
115
115
|
// Only the already-on-this-anchor click needs handling here — see
|
|
116
|
-
// `
|
|
116
|
+
// `KeyedFloatEnhancer.astro`. Every other click is left to the browser's
|
|
117
117
|
// own fragment navigation, picked up below via `hashchange`.
|
|
118
118
|
if (location.hash === href) {
|
|
119
119
|
event.preventDefault();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Progressive enhancement for keyed floats — equations (Step D7), extended
|
|
4
|
+
* to `<Table>`/`<Figure>` (F-tables-figures). Generalizes the original
|
|
5
|
+
* equation-only `EquationEnhancer`: the scroll/focus/flash mechanics are
|
|
6
|
+
* unchanged, parameterized over `KINDS` below so a deep-link to `#eq-`,
|
|
7
|
+
* `#tab-`, or `#fig-` is handled the same way.
|
|
8
|
+
*
|
|
9
|
+
* The no-JS baseline is complete: `remark-notation` emits a real
|
|
10
|
+
* `id="eq-<key>"` / `id="tab-<key>"` / `id="fig-<key>"` container, a
|
|
11
|
+
* focusable number `<a>`, and a `[[…]]` reference renders a plain anchor;
|
|
12
|
+
* `global.css` does the `:target` deep-link highlight (with a
|
|
13
|
+
* `prefers-reduced-motion` fallback) and the sticky-header scroll offset via
|
|
14
|
+
* `scroll-margin`.
|
|
15
|
+
*
|
|
16
|
+
* This script only adds what CSS and anchors cannot:
|
|
17
|
+
* - a smooth, header-aware scroll on an in-page jump;
|
|
18
|
+
* - re-triggering the highlight when the same `#eq-…`/`#tab-…`/`#fig-…` is
|
|
19
|
+
* activated again — the one case a plain anchor click does nothing for,
|
|
20
|
+
* since the browser won't navigate to a fragment that's already current;
|
|
21
|
+
* - moving keyboard focus onto the float so screen-reader users land on it;
|
|
22
|
+
* - for an equation only, marking its number permanently visible
|
|
23
|
+
* (`.eq-number-revealed`, styled in `global.css`) once the reader has
|
|
24
|
+
* actually arrived at it, so it doesn't fade back down once the
|
|
25
|
+
* `:target`/`:hover` state ends — a table/figure number isn't faded to
|
|
26
|
+
* begin with (it's part of the always-visible caption), so it has
|
|
27
|
+
* nothing to reveal.
|
|
28
|
+
*
|
|
29
|
+
* It deliberately does *not* intercept an ordinary click that changes the
|
|
30
|
+
* hash. `:target` is set only by a genuine same-document fragment
|
|
31
|
+
* navigation — never by `history.pushState` (a common trap: pushState
|
|
32
|
+
* updates the URL but leaves `:target`, and thus the flash, dark). Letting
|
|
33
|
+
* the click through and reacting to the `hashchange` it produces is what
|
|
34
|
+
* actually plays the highlight.
|
|
35
|
+
*
|
|
36
|
+
* Everything here is gated on `prefers-reduced-motion` and degrades to the
|
|
37
|
+
* native anchor behaviour if it throws.
|
|
38
|
+
*/
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
(() => {
|
|
43
|
+
const root = document.querySelector('.sl-markdown-content');
|
|
44
|
+
if (!root) return;
|
|
45
|
+
|
|
46
|
+
interface Kind {
|
|
47
|
+
readonly prefix: string;
|
|
48
|
+
readonly triggerSelector: string;
|
|
49
|
+
readonly numberSelector: string;
|
|
50
|
+
readonly reveal: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const KINDS: readonly Kind[] = [
|
|
54
|
+
{
|
|
55
|
+
prefix: '#eq-',
|
|
56
|
+
triggerSelector: 'a.equation-ref, a.keyed-equation__number',
|
|
57
|
+
numberSelector: '.keyed-equation__number',
|
|
58
|
+
reveal: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
prefix: '#tab-',
|
|
62
|
+
triggerSelector: 'a.table-ref, a.keyed-table__number',
|
|
63
|
+
numberSelector: '.keyed-table__number',
|
|
64
|
+
reveal: false,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
prefix: '#fig-',
|
|
68
|
+
triggerSelector: 'a.figure-ref, a.keyed-figure__number',
|
|
69
|
+
numberSelector: '.keyed-figure__number',
|
|
70
|
+
reveal: false,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
const TRIGGER_SELECTOR = KINDS.map((kind) => kind.triggerSelector).join(', ');
|
|
74
|
+
const kindForHash = (hash: string) =>
|
|
75
|
+
KINDS.find((kind) => hash.startsWith(kind.prefix));
|
|
76
|
+
|
|
77
|
+
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
78
|
+
|
|
79
|
+
const activate = (target: HTMLElement | null, doScroll: boolean, kind: Kind) => {
|
|
80
|
+
if (!target) return;
|
|
81
|
+
if (kind.reveal) {
|
|
82
|
+
target
|
|
83
|
+
.querySelector(kind.numberSelector)
|
|
84
|
+
?.classList.add('eq-number-revealed');
|
|
85
|
+
}
|
|
86
|
+
if (!reduce.matches) {
|
|
87
|
+
// Restart the CSS `:target` animation even when the hash is unchanged.
|
|
88
|
+
target.style.animation = 'none';
|
|
89
|
+
void target.offsetWidth;
|
|
90
|
+
target.style.removeProperty('animation');
|
|
91
|
+
}
|
|
92
|
+
if (doScroll) {
|
|
93
|
+
target.scrollIntoView({
|
|
94
|
+
behavior: reduce.matches ? 'auto' : 'smooth',
|
|
95
|
+
block: 'start',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
target.setAttribute('tabindex', '-1');
|
|
99
|
+
try {
|
|
100
|
+
target.focus({ preventScroll: true });
|
|
101
|
+
} catch {
|
|
102
|
+
target.focus();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
root.addEventListener('click', (event) => {
|
|
107
|
+
const origin = event.target instanceof Element ? event.target : null;
|
|
108
|
+
const link = origin?.closest(TRIGGER_SELECTOR);
|
|
109
|
+
if (!link) return;
|
|
110
|
+
const href = link.getAttribute('href') || '';
|
|
111
|
+
const kind = kindForHash(href);
|
|
112
|
+
if (!kind) return; // cross-page or unrecognized: let the browser navigate
|
|
113
|
+
const target = document.getElementById(href.slice(1));
|
|
114
|
+
if (!target) return;
|
|
115
|
+
// Only the already-on-this-anchor click needs handling here — the
|
|
116
|
+
// browser has nothing new to navigate to, so it won't fire `hashchange`
|
|
117
|
+
// and nothing else will re-trigger the flash. Every other click is left
|
|
118
|
+
// alone: its native navigation sets `:target` and fires `hashchange`,
|
|
119
|
+
// which the listener below turns into the same `activate()`.
|
|
120
|
+
if (location.hash === href) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
activate(target, true, kind);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
window.addEventListener('hashchange', () => {
|
|
127
|
+
const kind = kindForHash(location.hash);
|
|
128
|
+
if (!kind) return;
|
|
129
|
+
activate(document.getElementById(location.hash.slice(1)), true, kind);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const initialKind = kindForHash(location.hash);
|
|
133
|
+
if (initialKind) {
|
|
134
|
+
activate(document.getElementById(location.hash.slice(1)), true, initialKind);
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
</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>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Starlight allows one `SocialIcons` override, so this composes the stock
|
|
4
|
+
* social-link row with the `[?]` About control (`AboutPanel.astro`). Both sit in
|
|
5
|
+
* the header's right cluster; the About panel is present on every route.
|
|
6
|
+
*/
|
|
7
|
+
import DefaultSocialIcons from '@astrojs/starlight/components/SocialIcons.astro';
|
|
8
|
+
import AboutPanel from './AboutPanel.astro';
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<DefaultSocialIcons />
|
|
12
|
+
<AboutPanel />
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* D5); this component never rebuilds the registry.
|
|
10
10
|
*/
|
|
11
11
|
import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
|
|
12
|
+
import { assertLocalNotationParity } from '../../compiler/astro';
|
|
12
13
|
import { loadManifest } from '../../compiler/manifest';
|
|
13
14
|
import {
|
|
14
15
|
isLessonSlug,
|
|
@@ -18,7 +19,8 @@ import NotationLayer, {
|
|
|
18
19
|
type NotationLayerDefinition,
|
|
19
20
|
} from '../notation/NotationLayer.astro';
|
|
20
21
|
import CitationLayer from '../citation/CitationLayer.astro';
|
|
21
|
-
import
|
|
22
|
+
import KeyedFloatEnhancer from './KeyedFloatEnhancer.astro';
|
|
23
|
+
import FloatRefLayer from '../reference/FloatRefLayer.astro';
|
|
22
24
|
import HeadingAnchorEnhancer from './HeadingAnchorEnhancer.astro';
|
|
23
25
|
|
|
24
26
|
// `lessonId` is no longer authored (Phase C2); derive it from the route.
|
|
@@ -33,6 +35,16 @@ const definitions: NotationLayerDefinition[] = [];
|
|
|
33
35
|
if (lessonId) {
|
|
34
36
|
const manifest = await loadManifest();
|
|
35
37
|
const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
|
|
38
|
+
const routeData = Astro.locals.starlightRoute.entry.data as {
|
|
39
|
+
notation?: { local?: readonly { key?: unknown }[] };
|
|
40
|
+
};
|
|
41
|
+
const authoredLocalKeys = (routeData.notation?.local ?? [])
|
|
42
|
+
.map((entry) => entry.key)
|
|
43
|
+
.filter((key): key is string => typeof key === 'string');
|
|
44
|
+
const renderedLocalKeys = (lesson?.notation.definitions ?? [])
|
|
45
|
+
.filter((definition) => definition.scope === 'local')
|
|
46
|
+
.map((definition) => definition.key);
|
|
47
|
+
assertLocalNotationParity(lessonId, authoredLocalKeys, renderedLocalKeys);
|
|
36
48
|
for (const definition of lesson?.notation.definitions ?? []) {
|
|
37
49
|
definitions.push({
|
|
38
50
|
key: definition.key,
|
|
@@ -57,6 +69,7 @@ if (lessonId) {
|
|
|
57
69
|
|
|
58
70
|
<NotationLayer definitions={definitions} />
|
|
59
71
|
{lessonId && <CitationLayer />}
|
|
60
|
-
{lessonId && <
|
|
72
|
+
{lessonId && <KeyedFloatEnhancer />}
|
|
73
|
+
{lessonId && <FloatRefLayer />}
|
|
61
74
|
{lessonId && <HeadingAnchorEnhancer />}
|
|
62
75
|
<DefaultFooter />
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A referenceable table (F-tables-figures) — the table equivalent of a
|
|
4
|
+
* keyed display equation. `remark-notation.mjs` numbers every `<Table>` on a
|
|
5
|
+
* lesson page `‹part›.‹n›` (the part is the sidebar group's ordinal, `n`
|
|
6
|
+
* sequential over every `<Table>` in that part — see
|
|
7
|
+
* `docs/authoring/structure.md`), keyed or not, and injects the resulting
|
|
8
|
+
* `number`/`anchorId` as props; this component only renders what it's given
|
|
9
|
+
* — it never computes its own number, same as `equationNode()`'s output for
|
|
10
|
+
* equations.
|
|
11
|
+
*
|
|
12
|
+
* An author sets `id` to make the table referenceable by name with
|
|
13
|
+
* `[[tab-<id>]]` (same page) or `[[lesson-slug#tab-<id>]]` (cross page); a
|
|
14
|
+
* table without `id` is still numbered and deep-linkable, just not
|
|
15
|
+
* referenceable by name — the same rule an unkeyed equation follows.
|
|
16
|
+
*/
|
|
17
|
+
export interface Props {
|
|
18
|
+
/** Stable key, e.g. `id="kinematics-summary"`. Omit for a positional-only table. */
|
|
19
|
+
id?: string;
|
|
20
|
+
/** Visible caption, shown below the table and in the hover-preview card. */
|
|
21
|
+
caption: string;
|
|
22
|
+
/** Computed by `remark-notation.mjs` — never author this directly. */
|
|
23
|
+
number?: string;
|
|
24
|
+
/** Computed by `remark-notation.mjs` — never author this directly. */
|
|
25
|
+
anchorId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { id, caption, number, anchorId } = Astro.props;
|
|
29
|
+
|
|
30
|
+
if (typeof caption !== 'string' || !caption.trim()) {
|
|
31
|
+
throw new Error('Table requires a non-empty `caption` prop.');
|
|
32
|
+
}
|
|
33
|
+
if (!number || !anchorId) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'Table is missing its computed `number`/`anchorId` — this page must run remark-notation with a `tables` cross-page table configured (see astro.config.mjs).',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<figure
|
|
41
|
+
class="keyed-table"
|
|
42
|
+
id={anchorId}
|
|
43
|
+
data-tab-number={number}
|
|
44
|
+
data-tab-key={id}
|
|
45
|
+
>
|
|
46
|
+
<div class="keyed-table__body">
|
|
47
|
+
<slot />
|
|
48
|
+
</div>
|
|
49
|
+
<figcaption class="keyed-table__caption">
|
|
50
|
+
<a
|
|
51
|
+
class="keyed-table__number"
|
|
52
|
+
href={`#${anchorId}`}
|
|
53
|
+
aria-label={`Table ${number}`}>Table {number}</a
|
|
54
|
+
>
|
|
55
|
+
<span class="keyed-table__caption-text">{caption}</span>
|
|
56
|
+
</figcaption>
|
|
57
|
+
</figure>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.keyed-table {
|
|
61
|
+
margin-block: 1.5rem;
|
|
62
|
+
padding: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.keyed-table__body {
|
|
66
|
+
overflow-x: auto;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.keyed-table__body :global(table) {
|
|
70
|
+
margin: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.keyed-table__caption {
|
|
74
|
+
margin-block-start: 0.5rem;
|
|
75
|
+
font-size: var(--sl-text-sm);
|
|
76
|
+
color: var(--sl-color-gray-3);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.keyed-table__number {
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
font-variant-numeric: tabular-nums;
|
|
82
|
+
color: var(--sl-color-text-accent);
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.keyed-table__number:hover,
|
|
87
|
+
.keyed-table__number:focus-visible {
|
|
88
|
+
text-decoration: underline;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.keyed-table__number:focus-visible {
|
|
92
|
+
outline: 2px solid var(--sl-color-accent);
|
|
93
|
+
outline-offset: 0.15rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.keyed-table__caption-text::before {
|
|
97
|
+
content: ' — ';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@media print {
|
|
101
|
+
.keyed-table__number {
|
|
102
|
+
color: inherit;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|