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.
Files changed (73) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +46 -5
  3. package/dist/compiler/cli.d.ts +16 -0
  4. package/dist/compiler/cli.js +41 -3
  5. package/dist/compiler/collections.js +4 -1
  6. package/dist/compiler/course-config.d.ts +52 -1
  7. package/dist/compiler/knowledge-version.d.ts +58 -0
  8. package/dist/compiler/knowledge-version.js +80 -0
  9. package/dist/compiler/manifest.d.ts +77 -2
  10. package/dist/compiler/manifest.js +115 -18
  11. package/dist/compiler/resolution-report.d.ts +4 -0
  12. package/dist/reference/base-operators.d.mts +38 -0
  13. package/dist/reference/base-operators.mjs +103 -0
  14. package/dist/reference/consistency.d.ts +7 -2
  15. package/dist/reference/consistency.js +51 -2
  16. package/dist/reference/diagram-layout.d.ts +63 -0
  17. package/dist/reference/diagram-layout.js +122 -0
  18. package/dist/reference/equations-validate.d.ts +5 -0
  19. package/dist/reference/equations-validate.js +1 -1
  20. package/dist/reference/equations.d.mts +42 -11
  21. package/dist/reference/equations.mjs +58 -17
  22. package/dist/reference/figures-validate.d.ts +21 -0
  23. package/dist/reference/figures-validate.js +13 -0
  24. package/dist/reference/keyed-float-validate.d.ts +67 -0
  25. package/dist/reference/keyed-float-validate.js +110 -0
  26. package/dist/reference/keyed-jsx-floats.d.mts +56 -0
  27. package/dist/reference/keyed-jsx-floats.mjs +167 -0
  28. package/dist/reference/math-glyphs.d.mts +18 -0
  29. package/dist/reference/math-glyphs.mjs +92 -0
  30. package/dist/reference/remark-notation.d.mts +12 -0
  31. package/dist/reference/remark-notation.mjs +199 -9
  32. package/dist/reference/tables-validate.d.ts +21 -0
  33. package/dist/reference/tables-validate.js +13 -0
  34. package/dist/reference/units-vocab.js +19 -1
  35. package/docs/VERSIONING.md +66 -0
  36. package/docs/authoring/README.md +96 -0
  37. package/docs/authoring/agents.md +63 -0
  38. package/docs/authoring/migrating.md +107 -0
  39. package/docs/authoring/notation.md +296 -0
  40. package/docs/authoring/versioning.md +116 -0
  41. package/docs/authoring/visualizations.md +195 -0
  42. package/package.json +20 -5
  43. package/src/compiler/cli.ts +49 -0
  44. package/src/compiler/collections.ts +12 -3
  45. package/src/compiler/course-config.ts +54 -0
  46. package/src/compiler/knowledge-version.ts +157 -0
  47. package/src/compiler/manifest.ts +239 -17
  48. package/src/components/CurriculumMap.astro +27 -0
  49. package/src/components/diagram/Diagram.astro +317 -0
  50. package/src/components/examples/CompactExamples.astro +1 -1
  51. package/src/components/figure/Figure.astro +104 -0
  52. package/src/components/notation/render-notation-math.ts +13 -2
  53. package/src/components/reference/FloatRefLayer.astro +671 -0
  54. package/src/components/starlight/AboutPanel.astro +255 -0
  55. package/src/components/starlight/HeadingAnchorEnhancer.astro +3 -3
  56. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  57. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  58. package/src/components/starlight/LessonFooter.astro +4 -2
  59. package/src/components/table/Table.astro +104 -0
  60. package/src/reference/base-operators.mjs +103 -0
  61. package/src/reference/consistency.ts +83 -2
  62. package/src/reference/diagram-layout.ts +246 -0
  63. package/src/reference/equations-validate.ts +6 -1
  64. package/src/reference/equations.mjs +58 -17
  65. package/src/reference/figures-validate.ts +40 -0
  66. package/src/reference/keyed-float-validate.ts +207 -0
  67. package/src/reference/keyed-jsx-floats.mjs +167 -0
  68. package/src/reference/math-glyphs.mjs +92 -0
  69. package/src/reference/remark-notation.mjs +199 -9
  70. package/src/reference/tables-validate.ts +40 -0
  71. package/src/reference/units-vocab.ts +18 -0
  72. package/src/styles/global.css +115 -7
  73. 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 `EquationEnhancer.astro`.
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 `EquationEnhancer.astro`, which
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
- // `EquationEnhancer.astro`. Every other click is left to the browser's
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 EquationEnhancer from './EquationEnhancer.astro';
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 && <EquationEnhancer />}
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
+ }