explico 0.2.0 → 0.3.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 +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +16 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +4 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +77 -2
- package/dist/compiler/manifest.js +115 -18
- 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 +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +42 -11
- package/dist/reference/equations.mjs +58 -17
- 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 +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -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 +12 -0
- package/dist/reference/remark-notation.mjs +199 -9
- 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 +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +12 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +239 -17
- 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/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/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +4 -2
- package/src/components/table/Table.astro +104 -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 +6 -1
- package/src/reference/equations.mjs +58 -17
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/remark-notation.mjs +199 -9
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +115 -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>
|
|
@@ -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 />
|
|
@@ -18,7 +18,8 @@ import NotationLayer, {
|
|
|
18
18
|
type NotationLayerDefinition,
|
|
19
19
|
} from '../notation/NotationLayer.astro';
|
|
20
20
|
import CitationLayer from '../citation/CitationLayer.astro';
|
|
21
|
-
import
|
|
21
|
+
import KeyedFloatEnhancer from './KeyedFloatEnhancer.astro';
|
|
22
|
+
import FloatRefLayer from '../reference/FloatRefLayer.astro';
|
|
22
23
|
import HeadingAnchorEnhancer from './HeadingAnchorEnhancer.astro';
|
|
23
24
|
|
|
24
25
|
// `lessonId` is no longer authored (Phase C2); derive it from the route.
|
|
@@ -57,6 +58,7 @@ if (lessonId) {
|
|
|
57
58
|
|
|
58
59
|
<NotationLayer definitions={definitions} />
|
|
59
60
|
{lessonId && <CitationLayer />}
|
|
60
|
-
{lessonId && <
|
|
61
|
+
{lessonId && <KeyedFloatEnhancer />}
|
|
62
|
+
{lessonId && <FloatRefLayer />}
|
|
61
63
|
{lessonId && <HeadingAnchorEnhancer />}
|
|
62
64
|
<DefaultFooter />
|
|
@@ -0,0 +1,104 @@
|
|
|
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 `‹chapter›.‹n›` (chapter = the lesson's course ordinal, `n`
|
|
6
|
+
* sequential over the lesson), keyed or not, and injects the resulting
|
|
7
|
+
* `number`/`anchorId` as props; this component only renders what it's given
|
|
8
|
+
* — it never computes its own number, same as `equationNode()`'s output for
|
|
9
|
+
* equations.
|
|
10
|
+
*
|
|
11
|
+
* An author sets `id` to make the table referenceable by name with
|
|
12
|
+
* `[[tab-<id>]]` (same page) or `[[lesson-slug#tab-<id>]]` (cross page); a
|
|
13
|
+
* table without `id` is still numbered and deep-linkable, just not
|
|
14
|
+
* referenceable by name — the same rule an unkeyed equation follows.
|
|
15
|
+
*/
|
|
16
|
+
export interface Props {
|
|
17
|
+
/** Stable key, e.g. `id="kinematics-summary"`. Omit for a positional-only table. */
|
|
18
|
+
id?: string;
|
|
19
|
+
/** Visible caption, shown below the table and in the hover-preview card. */
|
|
20
|
+
caption: string;
|
|
21
|
+
/** Computed by `remark-notation.mjs` — never author this directly. */
|
|
22
|
+
number?: string;
|
|
23
|
+
/** Computed by `remark-notation.mjs` — never author this directly. */
|
|
24
|
+
anchorId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { id, caption, number, anchorId } = Astro.props;
|
|
28
|
+
|
|
29
|
+
if (typeof caption !== 'string' || !caption.trim()) {
|
|
30
|
+
throw new Error('Table requires a non-empty `caption` prop.');
|
|
31
|
+
}
|
|
32
|
+
if (!number || !anchorId) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Table is missing its computed `number`/`anchorId` — this page must run remark-notation with a `tables` cross-page table configured (see astro.config.mjs).',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<figure
|
|
40
|
+
class="keyed-table"
|
|
41
|
+
id={anchorId}
|
|
42
|
+
data-tab-number={number}
|
|
43
|
+
data-tab-key={id}
|
|
44
|
+
>
|
|
45
|
+
<div class="keyed-table__body">
|
|
46
|
+
<slot />
|
|
47
|
+
</div>
|
|
48
|
+
<figcaption class="keyed-table__caption">
|
|
49
|
+
<a
|
|
50
|
+
class="keyed-table__number"
|
|
51
|
+
href={`#${anchorId}`}
|
|
52
|
+
aria-label={`Table ${number}`}>Table {number}</a
|
|
53
|
+
>
|
|
54
|
+
<span class="keyed-table__caption-text">{caption}</span>
|
|
55
|
+
</figcaption>
|
|
56
|
+
</figure>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.keyed-table {
|
|
60
|
+
margin-block: 1.5rem;
|
|
61
|
+
padding: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.keyed-table__body {
|
|
65
|
+
overflow-x: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.keyed-table__body :global(table) {
|
|
69
|
+
margin: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.keyed-table__caption {
|
|
73
|
+
margin-block-start: 0.5rem;
|
|
74
|
+
font-size: var(--sl-text-sm);
|
|
75
|
+
color: var(--sl-color-gray-3);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.keyed-table__number {
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
font-variant-numeric: tabular-nums;
|
|
81
|
+
color: var(--sl-color-text-accent);
|
|
82
|
+
text-decoration: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.keyed-table__number:hover,
|
|
86
|
+
.keyed-table__number:focus-visible {
|
|
87
|
+
text-decoration: underline;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.keyed-table__number:focus-visible {
|
|
91
|
+
outline: 2px solid var(--sl-color-accent);
|
|
92
|
+
outline-offset: 0.15rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.keyed-table__caption-text::before {
|
|
96
|
+
content: ' — ';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@media print {
|
|
100
|
+
.keyed-table__number {
|
|
101
|
+
color: inherit;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base operator library: the named mathematical operators every reader is
|
|
3
|
+
* assumed to know, so a lesson never has to introduce them with a notation
|
|
4
|
+
* entry.
|
|
5
|
+
*
|
|
6
|
+
* Rationale (parallel to `base-library.mjs`): the completeness gate has no
|
|
7
|
+
* opinion about operators — KaTeX parses `\exp`, `\int`, `\sin`, … as operator
|
|
8
|
+
* nodes, and `\operatorname{Var}` as an `operatorname` node, none of which the
|
|
9
|
+
* glyph resolver treats as a bindable symbol. The `operator-explained`
|
|
10
|
+
* consistency check (rule N8) fills that gap: an operator a lesson uses is
|
|
11
|
+
* either on this list (or the course's additions to it) or it carries a
|
|
12
|
+
* notation entry the reader can hover. This set is the "everyone knows these"
|
|
13
|
+
* baseline — trig, the exponential and logarithms, limits, extrema, and the
|
|
14
|
+
* big operators — kept greppable and small like the base library.
|
|
15
|
+
*
|
|
16
|
+
* A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
|
|
17
|
+
* written `\operatorname{…}` and is NOT here: it earns a card, a
|
|
18
|
+
* `notation.local` entry, or an explicit line in `course.config.ts`
|
|
19
|
+
* (`operators.allow`).
|
|
20
|
+
*
|
|
21
|
+
* Entries are the KaTeX command without its backslash (`exp`, not `\exp`) for
|
|
22
|
+
* `op` nodes, and the verbatim body for `\operatorname{…}` nodes (`Var`).
|
|
23
|
+
* Matching is case-sensitive.
|
|
24
|
+
*/
|
|
25
|
+
export const BASE_OPERATORS = Object.freeze(
|
|
26
|
+
new Set([
|
|
27
|
+
// exponential / logarithms
|
|
28
|
+
'exp',
|
|
29
|
+
'log',
|
|
30
|
+
'ln',
|
|
31
|
+
'lg',
|
|
32
|
+
// trigonometric
|
|
33
|
+
'sin',
|
|
34
|
+
'cos',
|
|
35
|
+
'tan',
|
|
36
|
+
'cot',
|
|
37
|
+
'sec',
|
|
38
|
+
'csc',
|
|
39
|
+
// inverse trigonometric
|
|
40
|
+
'arcsin',
|
|
41
|
+
'arccos',
|
|
42
|
+
'arctan',
|
|
43
|
+
// hyperbolic
|
|
44
|
+
'sinh',
|
|
45
|
+
'cosh',
|
|
46
|
+
'tanh',
|
|
47
|
+
'coth',
|
|
48
|
+
// limits
|
|
49
|
+
'lim',
|
|
50
|
+
'limsup',
|
|
51
|
+
'liminf',
|
|
52
|
+
'injlim',
|
|
53
|
+
'projlim',
|
|
54
|
+
'varlimsup',
|
|
55
|
+
'varliminf',
|
|
56
|
+
'varinjlim',
|
|
57
|
+
'varprojlim',
|
|
58
|
+
// extrema
|
|
59
|
+
'max',
|
|
60
|
+
'min',
|
|
61
|
+
'sup',
|
|
62
|
+
'inf',
|
|
63
|
+
// linear algebra / number theory / misc
|
|
64
|
+
'arg',
|
|
65
|
+
'deg',
|
|
66
|
+
'det',
|
|
67
|
+
'dim',
|
|
68
|
+
'gcd',
|
|
69
|
+
'hom',
|
|
70
|
+
'ker',
|
|
71
|
+
'Pr',
|
|
72
|
+
'mod',
|
|
73
|
+
// big operators
|
|
74
|
+
'sum',
|
|
75
|
+
'prod',
|
|
76
|
+
'coprod',
|
|
77
|
+
'int',
|
|
78
|
+
'iint',
|
|
79
|
+
'iiint',
|
|
80
|
+
'oint',
|
|
81
|
+
]),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The operators a course treats as common, given its `course.config.ts`
|
|
86
|
+
* `operators` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
87
|
+
*
|
|
88
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} operators
|
|
89
|
+
* @returns {Set<string>}
|
|
90
|
+
*/
|
|
91
|
+
export function effectiveOperatorAllowlist(operators) {
|
|
92
|
+
const allow = operators?.allow ?? [];
|
|
93
|
+
const deny = new Set(operators?.deny ?? []);
|
|
94
|
+
const effective = new Set(BASE_OPERATORS);
|
|
95
|
+
for (const name of allow) effective.add(name);
|
|
96
|
+
for (const name of deny) effective.delete(name);
|
|
97
|
+
return effective;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True when `name` needs no notation entry for this course. */
|
|
101
|
+
export function isBaseOperator(name, operators) {
|
|
102
|
+
return effectiveOperatorAllowlist(operators).has(name);
|
|
103
|
+
}
|