explico 0.1.1 → 0.2.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/dist/compiler/cli.d.ts +1 -0
- package/dist/compiler/collections.js +27 -0
- package/dist/compiler/manifest.d.ts +10 -1
- package/dist/compiler/manifest.js +18 -2
- package/dist/compiler/resolution-report.d.ts +1 -0
- package/dist/reference/consistency.d.ts +1 -1
- package/dist/reference/consistency.js +93 -2
- package/dist/reference/equations.d.mts +43 -16
- package/dist/reference/equations.mjs +75 -39
- package/dist/reference/gate-math.d.ts +5 -4
- package/dist/reference/gate-math.js +73 -2
- package/dist/reference/gloss.d.ts +87 -0
- package/dist/reference/gloss.js +59 -0
- package/dist/reference/registry.js +25 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-notation.mjs +30 -24
- package/dist/reference/types.d.ts +25 -2
- package/package.json +1 -1
- package/src/compiler/collections.ts +40 -0
- package/src/compiler/manifest.ts +31 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -0
- package/src/components/hover-panel.ts +71 -5
- package/src/components/notation/NotationFormula.astro +154 -0
- package/src/components/notation/NotationGlossary.astro +60 -0
- package/src/components/notation/NotationLayer.astro +248 -103
- package/src/components/notation/render-notation-math.ts +55 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/starlight/EquationEnhancer.astro +20 -5
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/LessonFooter.astro +4 -0
- package/src/content-config.ts +38 -0
- package/src/reference/consistency.ts +154 -1
- package/src/reference/equations.mjs +75 -39
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +30 -24
- package/src/reference/types.ts +26 -1
- package/src/styles/global.css +84 -2
|
@@ -58,6 +58,8 @@ export function buildNotationRegistry(
|
|
|
58
58
|
);
|
|
59
59
|
const availableDefinitionIds = new Set(definitions.keys());
|
|
60
60
|
|
|
61
|
+
validateGlossKeys(definitions, diagnostics);
|
|
62
|
+
|
|
61
63
|
// `notation.uses` is retired (D1 decision): a lesson pulls a shared key into
|
|
62
64
|
// scope by referencing it (`[[key]]`/`\explain`), and every reference resolves
|
|
63
65
|
// directly against page-local then shared definitions.
|
|
@@ -411,6 +413,43 @@ function normalizeLocalDefinition(
|
|
|
411
413
|
};
|
|
412
414
|
}
|
|
413
415
|
|
|
416
|
+
/**
|
|
417
|
+
* A gloss key is derived as `<card key>.<slug(name)>`, so it is entry-scoped
|
|
418
|
+
* and cannot clash with another card's glosses. It can still collide with a
|
|
419
|
+
* **card** whose own key happens to take that shape — and a colliding key
|
|
420
|
+
* would make the same string mean two things in the flat glyph table, which
|
|
421
|
+
* is exactly what the semantic-key design exists to prevent. Tier 1.
|
|
422
|
+
*
|
|
423
|
+
* This is also the promotion boundary: promoting a gloss means deleting the
|
|
424
|
+
* gloss line *and* adding the card. Leaving both in place fails here rather
|
|
425
|
+
* than resolving to whichever the table saw first.
|
|
426
|
+
*/
|
|
427
|
+
function validateGlossKeys(
|
|
428
|
+
definitions: ReadonlyMap<string, DefinitionCandidate>,
|
|
429
|
+
diagnostics: NotationDiagnostic[],
|
|
430
|
+
): void {
|
|
431
|
+
const cardKeys = new Set(
|
|
432
|
+
[...definitions.values()].map((candidate) => candidate.input.key),
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
for (const candidate of definitions.values()) {
|
|
436
|
+
for (const gloss of candidate.input.glosses) {
|
|
437
|
+
if (!cardKeys.has(gloss.key)) continue;
|
|
438
|
+
diagnostics.push(
|
|
439
|
+
diagnostic({
|
|
440
|
+
code: 'gloss-collides-with-card',
|
|
441
|
+
message: `${candidate.id} glosses ${JSON.stringify(gloss.label)} as ${gloss.key}, which is also a notation card; delete the gloss or rename it`,
|
|
442
|
+
key: gloss.key,
|
|
443
|
+
lessonId:
|
|
444
|
+
candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
|
|
445
|
+
definitionId: candidate.id,
|
|
446
|
+
source: candidate.input.source,
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
414
453
|
function validateReferenceKey(
|
|
415
454
|
reference: NotationReferenceInput,
|
|
416
455
|
owner: DefinitionCandidate,
|
|
@@ -503,6 +542,7 @@ function toDefinitionRecord(
|
|
|
503
542
|
status: input.status,
|
|
504
543
|
aiAssisted: input.aiAssisted,
|
|
505
544
|
body: input.body,
|
|
545
|
+
glosses: input.glosses,
|
|
506
546
|
source: input.source,
|
|
507
547
|
resolvedReferences: candidate.resolvedReferences,
|
|
508
548
|
...(input.formula === undefined ? {} : { formula: input.formula }),
|
|
@@ -525,6 +565,7 @@ function toDefinitionRecord(
|
|
|
525
565
|
seeAlso: input.seeAlso,
|
|
526
566
|
alignment: input.alignment,
|
|
527
567
|
status: candidate.lesson.status,
|
|
568
|
+
glosses: input.glosses,
|
|
528
569
|
source: input.source,
|
|
529
570
|
resolvedReferences: candidate.resolvedReferences,
|
|
530
571
|
...(input.formula === undefined ? {} : { formula: input.formula }),
|
|
@@ -570,6 +611,7 @@ function definitionFingerprint(candidate: DefinitionCandidate): string {
|
|
|
570
611
|
status: input.status,
|
|
571
612
|
aiAssisted: input.aiAssisted,
|
|
572
613
|
body: normalizeText(input.body),
|
|
614
|
+
glosses: input.glosses,
|
|
573
615
|
references,
|
|
574
616
|
});
|
|
575
617
|
}
|
|
@@ -586,6 +628,7 @@ function definitionFingerprint(candidate: DefinitionCandidate): string {
|
|
|
586
628
|
sources: [...input.sources].sort(),
|
|
587
629
|
seeAlso: [...input.seeAlso].sort(),
|
|
588
630
|
alignment,
|
|
631
|
+
glosses: input.glosses,
|
|
589
632
|
references,
|
|
590
633
|
});
|
|
591
634
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-linkable content headings (Step F3).
|
|
3
|
+
*
|
|
4
|
+
* The lesson pipeline uses a custom Markdown `processor` (see `astro.config.mjs`),
|
|
5
|
+
* which bypasses Starlight's own heading-link rehype pass. This plugin restores
|
|
6
|
+
* it with the *same* markup Starlight emits, so its bundled `anchor-links.css`
|
|
7
|
+
* (shipped globally because `markdown.headingLinks` defaults on) styles it for
|
|
8
|
+
* free — including the hover/focus reveal of an otherwise invisible anchor:
|
|
9
|
+
*
|
|
10
|
+
* <div class="sl-heading-wrapper level-h2">
|
|
11
|
+
* <h2 id="slug">Heading</h2>
|
|
12
|
+
* <a class="sl-anchor-link" href="#slug"> <icon> <sr-only label> </a>
|
|
13
|
+
* </div>
|
|
14
|
+
*
|
|
15
|
+
* The `:target` flash, sticky-header scroll offset, and the JS niceties
|
|
16
|
+
* (re-trigger on repeat activation, smooth scroll, `history.pushState`, brief
|
|
17
|
+
* section highlight) live in `global.css` + `HeadingAnchorEnhancer.astro`, the
|
|
18
|
+
* same split the keyed-equation feature uses.
|
|
19
|
+
*
|
|
20
|
+
* `rehypeHeadingIds` from `@astrojs/markdown-remark` normally runs *after* the
|
|
21
|
+
* user rehype plugins, so heading `id`s do not exist yet when this runs. It is
|
|
22
|
+
* idempotent (only assigns a missing `id`, and rebuilds `file.data.astro.headings`
|
|
23
|
+
* from an unchanged document order), so this plugin runs it first and Astro's
|
|
24
|
+
* later pass becomes a no-op for these headings.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
28
|
+
|
|
29
|
+
const HEADING_TAG = /^h([1-6])$/;
|
|
30
|
+
|
|
31
|
+
// Starlight's `link-alt` icon (`components-internals/Icons.ts`), the same glyph
|
|
32
|
+
// its own `rehypeAutolinkHeadings` uses.
|
|
33
|
+
const LINK_ICON_PATH =
|
|
34
|
+
'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42';
|
|
35
|
+
|
|
36
|
+
function headingText(node) {
|
|
37
|
+
let text = '';
|
|
38
|
+
const walk = (current) => {
|
|
39
|
+
if (!current || typeof current !== 'object') return;
|
|
40
|
+
if (current.type === 'text' || current.type === 'raw') {
|
|
41
|
+
text += String(current.value ?? '');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(current.children)) current.children.forEach(walk);
|
|
45
|
+
};
|
|
46
|
+
walk(node);
|
|
47
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isHeadingWrapper(node) {
|
|
51
|
+
const className = node?.properties?.className;
|
|
52
|
+
return (
|
|
53
|
+
node?.type === 'element' &&
|
|
54
|
+
Array.isArray(className) &&
|
|
55
|
+
className.includes('sl-heading-wrapper')
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function anchorLink(id, label) {
|
|
60
|
+
return {
|
|
61
|
+
type: 'element',
|
|
62
|
+
tagName: 'a',
|
|
63
|
+
properties: { className: ['sl-anchor-link'], href: `#${id}` },
|
|
64
|
+
children: [
|
|
65
|
+
{
|
|
66
|
+
type: 'element',
|
|
67
|
+
tagName: 'span',
|
|
68
|
+
properties: { ariaHidden: 'true', className: ['sl-anchor-icon'] },
|
|
69
|
+
children: [
|
|
70
|
+
{
|
|
71
|
+
type: 'element',
|
|
72
|
+
tagName: 'svg',
|
|
73
|
+
properties: {
|
|
74
|
+
width: 16,
|
|
75
|
+
height: 16,
|
|
76
|
+
viewBox: '0 0 24 24',
|
|
77
|
+
fill: 'currentColor',
|
|
78
|
+
},
|
|
79
|
+
children: [
|
|
80
|
+
{
|
|
81
|
+
type: 'element',
|
|
82
|
+
tagName: 'path',
|
|
83
|
+
properties: { d: LINK_ICON_PATH },
|
|
84
|
+
children: [],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'element',
|
|
92
|
+
tagName: 'span',
|
|
93
|
+
properties: { className: ['sr-only'], 'data-pagefind-ignore': 'true' },
|
|
94
|
+
children: [{ type: 'text', value: label }],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default function rehypeHeadingAnchors() {
|
|
101
|
+
const assignHeadingIds = rehypeHeadingIds();
|
|
102
|
+
|
|
103
|
+
return function transform(tree, file) {
|
|
104
|
+
assignHeadingIds(tree, file);
|
|
105
|
+
|
|
106
|
+
const walk = (node) => {
|
|
107
|
+
if (!node || typeof node !== 'object' || !Array.isArray(node.children)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (isHeadingWrapper(node)) return; // already processed
|
|
111
|
+
|
|
112
|
+
for (let index = 0; index < node.children.length; index++) {
|
|
113
|
+
const child = node.children[index];
|
|
114
|
+
const tag =
|
|
115
|
+
child?.type === 'element' && typeof child.tagName === 'string'
|
|
116
|
+
? HEADING_TAG.exec(child.tagName)
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
tag &&
|
|
121
|
+
tag[1] !== '1' &&
|
|
122
|
+
typeof child.properties?.id === 'string' &&
|
|
123
|
+
child.properties.id.length > 0
|
|
124
|
+
) {
|
|
125
|
+
const id = child.properties.id;
|
|
126
|
+
const label = `Section titled “${headingText(child)}”`;
|
|
127
|
+
node.children[index] = {
|
|
128
|
+
type: 'element',
|
|
129
|
+
tagName: 'div',
|
|
130
|
+
properties: {
|
|
131
|
+
className: ['sl-heading-wrapper', `level-${child.tagName}`],
|
|
132
|
+
},
|
|
133
|
+
children: [child, anchorLink(id, label)],
|
|
134
|
+
};
|
|
135
|
+
continue; // the heading is a leaf for our purposes
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
walk(child);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
walk(tree);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -36,7 +36,9 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
|
36
36
|
import { GlyphResolutionError, resolveMathGlyphs } from './math-glyphs.mjs';
|
|
37
37
|
import {
|
|
38
38
|
EQ_LABEL_PATTERN,
|
|
39
|
+
collectDisplayEquations,
|
|
39
40
|
collectEquationLabels,
|
|
41
|
+
equationAnchorId,
|
|
40
42
|
parseEquationRef,
|
|
41
43
|
stripEquationLabels,
|
|
42
44
|
} from './equations.mjs';
|
|
@@ -307,28 +309,31 @@ function defaultHref(definition, key, base = '') {
|
|
|
307
309
|
}
|
|
308
310
|
|
|
309
311
|
/**
|
|
310
|
-
* Wrap a
|
|
311
|
-
* (which splices the inner `math-display` element out).
|
|
312
|
-
* a
|
|
313
|
-
*
|
|
312
|
+
* Wrap a display `$$…$$` `math` node so the container survives `rehype-katex`
|
|
313
|
+
* (which splices the inner `math-display` element out). Every display equation
|
|
314
|
+
* in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
|
|
315
|
+
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
316
|
+
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
317
|
+
* screen readers name it "Equation 2.4".
|
|
314
318
|
*/
|
|
315
|
-
function
|
|
319
|
+
function equationNode(mathNode, { key, number }) {
|
|
320
|
+
const id = equationAnchorId({ key, number });
|
|
321
|
+
const hProperties = {
|
|
322
|
+
id,
|
|
323
|
+
className: ['keyed-equation'],
|
|
324
|
+
'data-eq-number': number,
|
|
325
|
+
};
|
|
326
|
+
if (key) hProperties['data-eq-key'] = key;
|
|
327
|
+
else hProperties['data-eq-auto'] = '';
|
|
328
|
+
|
|
316
329
|
return {
|
|
317
330
|
type: 'paragraph',
|
|
318
|
-
data: {
|
|
319
|
-
hName: 'div',
|
|
320
|
-
hProperties: {
|
|
321
|
-
id: `eq-${key}`,
|
|
322
|
-
className: ['keyed-equation'],
|
|
323
|
-
'data-eq-key': key,
|
|
324
|
-
'data-eq-number': number,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
331
|
+
data: { hName: 'div', hProperties },
|
|
327
332
|
children: [
|
|
328
333
|
mathNode,
|
|
329
334
|
{
|
|
330
335
|
type: 'link',
|
|
331
|
-
url:
|
|
336
|
+
url: `#${id}`,
|
|
332
337
|
children: [{ type: 'text', value: `(${number})` }],
|
|
333
338
|
data: {
|
|
334
339
|
hProperties: {
|
|
@@ -598,6 +603,14 @@ export default function remarkNotation(options = {}) {
|
|
|
598
603
|
crossPage: options.equations ?? {},
|
|
599
604
|
};
|
|
600
605
|
|
|
606
|
+
// Number and anchor every display equation on a lesson page, keyed or
|
|
607
|
+
// not (F3). Non-lesson pages (notation registry, stray docs) keep the
|
|
608
|
+
// keyed-only behaviour so their body math is left untouched.
|
|
609
|
+
const displayEquations = lessonId ? collectDisplayEquations(tree) : [];
|
|
610
|
+
const equationByNode = new Map(
|
|
611
|
+
displayEquations.map((equation) => [equation.node, equation]),
|
|
612
|
+
);
|
|
613
|
+
|
|
601
614
|
const resolve = (key, kind, node) => {
|
|
602
615
|
const context = {
|
|
603
616
|
file,
|
|
@@ -647,7 +660,6 @@ export default function remarkNotation(options = {}) {
|
|
|
647
660
|
node,
|
|
648
661
|
);
|
|
649
662
|
}
|
|
650
|
-
node.__eqKey = labelMatch[1];
|
|
651
663
|
value = stripEquationLabels(value);
|
|
652
664
|
replaceMathSource(node, value);
|
|
653
665
|
}
|
|
@@ -725,14 +737,8 @@ export default function remarkNotation(options = {}) {
|
|
|
725
737
|
continue;
|
|
726
738
|
}
|
|
727
739
|
visit(child, false);
|
|
728
|
-
if (child && child.
|
|
729
|
-
|
|
730
|
-
delete child.__eqKey;
|
|
731
|
-
node.children[index] = keyedEquationNode(
|
|
732
|
-
child,
|
|
733
|
-
key,
|
|
734
|
-
pageLabels.get(key),
|
|
735
|
-
);
|
|
740
|
+
if (child && child.type === 'math' && equationByNode.has(child)) {
|
|
741
|
+
node.children[index] = equationNode(child, equationByNode.get(child));
|
|
736
742
|
}
|
|
737
743
|
}
|
|
738
744
|
};
|
package/src/reference/types.ts
CHANGED
|
@@ -21,6 +21,20 @@ export type NotationAlignment =
|
|
|
21
21
|
|
|
22
22
|
export type NotationReferenceKind = 'prose' | 'math' | 'definition';
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A gloss resolved by the loader (D15). Declared on a card, keyed
|
|
26
|
+
* `<card key>.<slug(name)>`, and deliberately absent from the registry: a
|
|
27
|
+
* gloss is never a definition record, never a reference target, and never a
|
|
28
|
+
* curriculum node. See `reference/gloss.ts`.
|
|
29
|
+
*/
|
|
30
|
+
export interface NotationGlossEntry {
|
|
31
|
+
readonly key: string;
|
|
32
|
+
readonly notation: string;
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly units?: string;
|
|
35
|
+
readonly ownerKey: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
24
38
|
export interface NotationReferenceInput {
|
|
25
39
|
readonly key: string;
|
|
26
40
|
readonly kind: NotationReferenceKind;
|
|
@@ -44,6 +58,8 @@ export interface SharedNotationDefinitionInput {
|
|
|
44
58
|
readonly body: string;
|
|
45
59
|
/** References harvested from the Markdown body. */
|
|
46
60
|
readonly references: readonly NotationReferenceInput[];
|
|
61
|
+
/** Symbols this entry's `formula` names but does not make cards of. */
|
|
62
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
47
63
|
readonly source: SourceSpan;
|
|
48
64
|
}
|
|
49
65
|
|
|
@@ -59,6 +75,8 @@ export interface LocalNotationDefinitionInput {
|
|
|
59
75
|
readonly alignment: NotationAlignment;
|
|
60
76
|
/** References harvested from summary content. */
|
|
61
77
|
readonly references: readonly NotationReferenceInput[];
|
|
78
|
+
/** Symbols this entry's `formula` names but does not make cards of. */
|
|
79
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
62
80
|
readonly source: SourceSpan;
|
|
63
81
|
}
|
|
64
82
|
|
|
@@ -90,7 +108,8 @@ export type NotationDiagnosticCode =
|
|
|
90
108
|
| 'alignment-unknown-lesson'
|
|
91
109
|
| 'alignment-introduction'
|
|
92
110
|
| 'alignment-unavailable'
|
|
93
|
-
| 'alignment-review-state'
|
|
111
|
+
| 'alignment-review-state'
|
|
112
|
+
| 'gloss-collides-with-card';
|
|
94
113
|
|
|
95
114
|
export type DiagnosticSeverity = 'error' | 'warning';
|
|
96
115
|
|
|
@@ -120,6 +139,12 @@ export interface ResolvedNotationReference {
|
|
|
120
139
|
interface NotationDefinitionRecordBase {
|
|
121
140
|
readonly id: string;
|
|
122
141
|
readonly key: string;
|
|
142
|
+
/**
|
|
143
|
+
* Symbols this entry's `formula` names but does not make cards of (D15).
|
|
144
|
+
* Carried on the record so the manifest can build the entry-scoped formula
|
|
145
|
+
* table; deliberately **not** a definition, a reference target, or an edge.
|
|
146
|
+
*/
|
|
147
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
123
148
|
readonly scope: NotationDefinitionScope;
|
|
124
149
|
readonly notation: string;
|
|
125
150
|
readonly label: string;
|
package/src/styles/global.css
CHANGED
|
@@ -690,13 +690,42 @@
|
|
|
690
690
|
font-size: var(--sl-text-sm);
|
|
691
691
|
color: var(--sl-color-text-accent);
|
|
692
692
|
text-decoration: none;
|
|
693
|
+
/* Hardly visible until you look for it (F3): transparent by default, so it
|
|
694
|
+
never competes with the equation, and — being `opacity:0`, not `hidden` —
|
|
695
|
+
still announced to assistive tech and revealed on hover/focus/deep-link.
|
|
696
|
+
A partial opacity would fail the WCAG contrast check, so it is all or
|
|
697
|
+
nothing, mirroring Starlight's own heading anchor links. */
|
|
698
|
+
opacity: 0;
|
|
699
|
+
transition: opacity 120ms ease-out;
|
|
693
700
|
}
|
|
694
701
|
|
|
695
|
-
|
|
702
|
+
@media (hover: none) {
|
|
703
|
+
/* No hover to reveal it on touch, so keep the number legible there. */
|
|
704
|
+
.sl-markdown-content a.keyed-equation__number {
|
|
705
|
+
opacity: 1;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.sl-markdown-content .keyed-equation:hover a.keyed-equation__number,
|
|
710
|
+
.sl-markdown-content .keyed-equation:focus-within a.keyed-equation__number,
|
|
711
|
+
.sl-markdown-content .keyed-equation:target a.keyed-equation__number,
|
|
696
712
|
.sl-markdown-content a.keyed-equation__number:focus-visible {
|
|
713
|
+
opacity: 1;
|
|
697
714
|
text-decoration: underline;
|
|
698
715
|
}
|
|
699
716
|
|
|
717
|
+
@media (prefers-reduced-motion: reduce) {
|
|
718
|
+
.sl-markdown-content a.keyed-equation__number {
|
|
719
|
+
transition: none;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
@media (forced-colors: active) {
|
|
724
|
+
.sl-markdown-content a.keyed-equation__number {
|
|
725
|
+
opacity: 1;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
700
729
|
.sl-markdown-content a.equation-ref {
|
|
701
730
|
font-variant-numeric: tabular-nums;
|
|
702
731
|
font-weight: 600;
|
|
@@ -725,7 +754,7 @@
|
|
|
725
754
|
}
|
|
726
755
|
|
|
727
756
|
.sl-markdown-content .keyed-equation:target {
|
|
728
|
-
animation: eq-flash
|
|
757
|
+
animation: eq-flash 5s ease-out both;
|
|
729
758
|
}
|
|
730
759
|
|
|
731
760
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -761,3 +790,56 @@
|
|
|
761
790
|
padding-inline-end: 0;
|
|
762
791
|
}
|
|
763
792
|
}
|
|
793
|
+
|
|
794
|
+
/* Deep-linkable content headings (F3). The wrapper + anchor-link markup and its
|
|
795
|
+
hover/focus reveal come from Starlight's bundled `anchor-links.css`; these
|
|
796
|
+
rules add the sticky-header scroll offset and the `:target` flash, mirroring
|
|
797
|
+
the keyed-equation deep-link treatment. `HeadingAnchorEnhancer.astro` adds
|
|
798
|
+
the JS-only niceties (smooth scroll, repeat-activation re-trigger, and a
|
|
799
|
+
brief highlight of the section body). */
|
|
800
|
+
.sl-markdown-content :is(h1, h2, h3, h4, h5, h6),
|
|
801
|
+
.sl-markdown-content .sl-heading-wrapper {
|
|
802
|
+
scroll-margin-block-start: 5rem;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
@keyframes heading-flash {
|
|
806
|
+
from {
|
|
807
|
+
background: var(--sl-color-accent-low);
|
|
808
|
+
box-shadow: 0 0 0 0.3rem var(--sl-color-accent-low);
|
|
809
|
+
}
|
|
810
|
+
to {
|
|
811
|
+
background: transparent;
|
|
812
|
+
box-shadow: 0 0 0 0.3rem transparent;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.sl-markdown-content :is(h1, h2, h3, h4, h5, h6):target,
|
|
817
|
+
.sl-markdown-content .sl-heading-wrapper:has(> :target),
|
|
818
|
+
.sl-markdown-content .deep-link-section-flash {
|
|
819
|
+
border-radius: 0.3rem;
|
|
820
|
+
/* `HeadingAnchorEnhancer.astro`'s `FLASH_MS` cleanup timeout must stay
|
|
821
|
+
comfortably longer than this so it never clips the fade-out. */
|
|
822
|
+
animation: heading-flash 5s ease-out both;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
@media (prefers-reduced-motion: reduce) {
|
|
826
|
+
.sl-markdown-content :is(h1, h2, h3, h4, h5, h6):target,
|
|
827
|
+
.sl-markdown-content .sl-heading-wrapper:has(> :target) {
|
|
828
|
+
animation: none;
|
|
829
|
+
background: var(--sl-color-accent-low);
|
|
830
|
+
outline: 2px solid var(--sl-color-accent);
|
|
831
|
+
outline-offset: 0.2rem;
|
|
832
|
+
}
|
|
833
|
+
.sl-markdown-content .deep-link-section-flash {
|
|
834
|
+
animation: none;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
@media (forced-colors: active) {
|
|
839
|
+
.sl-markdown-content :is(h1, h2, h3, h4, h5, h6):target,
|
|
840
|
+
.sl-markdown-content .sl-heading-wrapper:has(> :target) {
|
|
841
|
+
animation: none;
|
|
842
|
+
outline: 2px solid Highlight;
|
|
843
|
+
outline-offset: 0.2rem;
|
|
844
|
+
}
|
|
845
|
+
}
|