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
|
@@ -42,6 +42,11 @@ import {
|
|
|
42
42
|
parseEquationRef,
|
|
43
43
|
stripEquationLabels,
|
|
44
44
|
} from './equations.mjs';
|
|
45
|
+
import {
|
|
46
|
+
collectKeyedJsxFloats,
|
|
47
|
+
keyedFloatAnchorId,
|
|
48
|
+
parseKeyedFloatRef,
|
|
49
|
+
} from './keyed-jsx-floats.mjs';
|
|
45
50
|
|
|
46
51
|
const PROSE_SKIP_TYPES = new Set([
|
|
47
52
|
'code',
|
|
@@ -340,6 +345,8 @@ function equationNode(mathNode, { key, number }) {
|
|
|
340
345
|
className: ['keyed-equation__number'],
|
|
341
346
|
'data-eq-number': number,
|
|
342
347
|
'aria-label': `Equation ${number}`,
|
|
348
|
+
// Plain hover tooltip — the number is a permalink to this equation.
|
|
349
|
+
title: 'Anchor to this equation',
|
|
343
350
|
},
|
|
344
351
|
},
|
|
345
352
|
},
|
|
@@ -347,6 +354,32 @@ function equationNode(mathNode, { key, number }) {
|
|
|
347
354
|
};
|
|
348
355
|
}
|
|
349
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Give a `<Table>`/`<Figure>` `mdxJsxFlowElement` its computed `number` and
|
|
359
|
+
* `anchorId` as real JSX attributes, so the actual Astro component (which
|
|
360
|
+
* renders the numbered anchor, unlike `equationNode`'s synthesized hast div)
|
|
361
|
+
* receives them as props. The compiler is the single source of truth for
|
|
362
|
+
* both — the component only renders what it's given.
|
|
363
|
+
*/
|
|
364
|
+
function injectFloatNumberAttributes(node, float, prefix) {
|
|
365
|
+
const anchorId = keyedFloatAnchorId({
|
|
366
|
+
key: float.key,
|
|
367
|
+
number: float.number,
|
|
368
|
+
prefix,
|
|
369
|
+
});
|
|
370
|
+
node.attributes = (node.attributes ?? []).filter(
|
|
371
|
+
(attribute) =>
|
|
372
|
+
!(
|
|
373
|
+
attribute?.type === 'mdxJsxAttribute' &&
|
|
374
|
+
(attribute.name === 'number' || attribute.name === 'anchorId')
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
node.attributes.push(
|
|
378
|
+
{ type: 'mdxJsxAttribute', name: 'number', value: float.number },
|
|
379
|
+
{ type: 'mdxJsxAttribute', name: 'anchorId', value: anchorId },
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
350
383
|
/** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
|
|
351
384
|
function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
352
385
|
if (eqRef.lesson) {
|
|
@@ -374,7 +407,47 @@ function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
|
374
407
|
return { number, href: `#eq-${eqRef.key}` };
|
|
375
408
|
}
|
|
376
409
|
|
|
377
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Resolve a `[[<prefix>-key]]` / `[[slug#<prefix>-key]]` reference (a
|
|
412
|
+
* `<Table>`/`<Figure>` float) to `{ number, href }` — the table/figure
|
|
413
|
+
* equivalent of `resolveEquationRef`.
|
|
414
|
+
*/
|
|
415
|
+
function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
|
|
416
|
+
if (floatRef.lesson) {
|
|
417
|
+
const number = ctx.crossPage?.[floatRef.lesson]?.[floatRef.key];
|
|
418
|
+
if (number === undefined) {
|
|
419
|
+
fail(
|
|
420
|
+
file,
|
|
421
|
+
`Unknown ${tagName} reference [[${floatRef.lesson}#${prefix}-${floatRef.key}]]: lesson ${floatRef.lesson} has no <${tagName}> with id="${floatRef.key}".`,
|
|
422
|
+
node,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
number,
|
|
427
|
+
href: `${ctx.base.replace(/\/$/, '')}/${floatRef.lesson}/#${prefix}-${floatRef.key}`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const number = ctx.pageLabels.get(floatRef.key);
|
|
431
|
+
if (number === undefined) {
|
|
432
|
+
fail(
|
|
433
|
+
file,
|
|
434
|
+
`Unknown ${tagName} reference [[${prefix}-${floatRef.key}]]: no <${tagName}> on this page carries id="${floatRef.key}".`,
|
|
435
|
+
node,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
return { number, href: `#${prefix}-${floatRef.key}` };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function proseNodes(
|
|
442
|
+
value,
|
|
443
|
+
resolve,
|
|
444
|
+
file,
|
|
445
|
+
node,
|
|
446
|
+
references,
|
|
447
|
+
eqCtx,
|
|
448
|
+
tableCtx,
|
|
449
|
+
figureCtx,
|
|
450
|
+
) {
|
|
378
451
|
const base = eqCtx?.base ?? '';
|
|
379
452
|
const strayExplain = firstUnescaped(value, EXPLAIN);
|
|
380
453
|
if (strayExplain !== -1) {
|
|
@@ -395,7 +468,18 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
|
|
|
395
468
|
|
|
396
469
|
const key = match[1].trim();
|
|
397
470
|
const eqRef = eqCtx ? parseEquationRef(key) : undefined;
|
|
398
|
-
|
|
471
|
+
const tableRef =
|
|
472
|
+
!eqRef && tableCtx ? parseKeyedFloatRef(key, { prefix: 'tab' }) : undefined;
|
|
473
|
+
const figureRef =
|
|
474
|
+
!eqRef && !tableRef && figureCtx
|
|
475
|
+
? parseKeyedFloatRef(key, { prefix: 'fig' })
|
|
476
|
+
: undefined;
|
|
477
|
+
if (
|
|
478
|
+
!eqRef &&
|
|
479
|
+
!tableRef &&
|
|
480
|
+
!figureRef &&
|
|
481
|
+
!NOTATION_KEY_PATTERN.test(key)
|
|
482
|
+
) {
|
|
399
483
|
fail(file, `Invalid notation key "${key}" in [[...]].`, node);
|
|
400
484
|
}
|
|
401
485
|
|
|
@@ -426,9 +510,46 @@ function proseNodes(value, resolve, file, node, references, eqCtx) {
|
|
|
426
510
|
continue;
|
|
427
511
|
}
|
|
428
512
|
|
|
513
|
+
const floatRef = tableRef ?? figureRef;
|
|
514
|
+
if (floatRef) {
|
|
515
|
+
const kind = tableRef ? 'table' : 'figure';
|
|
516
|
+
const prefix = tableRef ? 'tab' : 'fig';
|
|
517
|
+
const tagName = tableRef ? 'Table' : 'Figure';
|
|
518
|
+
const { number, href } = resolveKeyedFloatRef(
|
|
519
|
+
floatRef,
|
|
520
|
+
tableRef ? tableCtx : figureCtx,
|
|
521
|
+
{ prefix, tagName },
|
|
522
|
+
file,
|
|
523
|
+
node,
|
|
524
|
+
);
|
|
525
|
+
output.push({
|
|
526
|
+
type: 'link',
|
|
527
|
+
url: href,
|
|
528
|
+
children: [{ type: 'text', value: `${tagName} ${number}` }],
|
|
529
|
+
data: {
|
|
530
|
+
hProperties: {
|
|
531
|
+
className: [`${kind}-ref`],
|
|
532
|
+
[`data-${prefix}-ref`]: floatRef.key,
|
|
533
|
+
...(floatRef.lesson ? { [`data-${prefix}-lesson`]: floatRef.lesson } : {}),
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
references.push({
|
|
538
|
+
key: floatRef.key,
|
|
539
|
+
kind,
|
|
540
|
+
...(floatRef.lesson ? { lesson: floatRef.lesson } : {}),
|
|
541
|
+
});
|
|
542
|
+
cursor = match.index + match[0].length;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
429
546
|
const definition = resolve(key, 'prose', node);
|
|
430
|
-
|
|
431
|
-
|
|
547
|
+
// `title`/`summary` are the pre-C1b field names, kept as the first
|
|
548
|
+
// fallback for a caller still supplying them; real content authors
|
|
549
|
+
// `label`/`meaning` (see `content-config.ts`), which raw frontmatter
|
|
550
|
+
// (`manifest.notation.raw`, a page's own `notation.local`) carries as-is.
|
|
551
|
+
const title = definition?.title ?? definition?.label ?? key;
|
|
552
|
+
const summary = definition?.summary ?? definition?.meaning;
|
|
432
553
|
const href = defaultHref(definition, key, base);
|
|
433
554
|
|
|
434
555
|
const properties = {
|
|
@@ -557,7 +678,16 @@ function inspectExplainCalls(value, resolve, file, node, references) {
|
|
|
557
678
|
* unknown?: 'error' | 'warn' | 'ignore',
|
|
558
679
|
* base?: string,
|
|
559
680
|
* equations?: Record<string, Record<string, string>>,
|
|
681
|
+
* tables?: Record<string, Record<string, string>>,
|
|
682
|
+
* figures?: Record<string, Record<string, string>>,
|
|
683
|
+
* chapters?: Record<string, number>,
|
|
560
684
|
* }} [options]
|
|
685
|
+
*
|
|
686
|
+
* `chapters` maps a lesson slug to its 1-based course ordinal. When the
|
|
687
|
+
* current page has one, every display equation / `<Table>` / `<Figure>` is
|
|
688
|
+
* numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
|
|
689
|
+
* `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
|
|
690
|
+
* which is what feeds `equations`/`tables`/`figures` above.
|
|
561
691
|
*/
|
|
562
692
|
export default function remarkNotation(options = {}) {
|
|
563
693
|
const unknown = options.unknown ?? 'error';
|
|
@@ -573,6 +703,10 @@ export default function remarkNotation(options = {}) {
|
|
|
573
703
|
);
|
|
574
704
|
const localDefinitions = localDefinitionsFromFile(file);
|
|
575
705
|
const lessonId = lessonIdFrom(file);
|
|
706
|
+
// The lesson's course ordinal (see the `chapters` option). Falsy / missing
|
|
707
|
+
// ⇒ the legacy `##`-section numbering, so `0` and `undefined` behave alike.
|
|
708
|
+
const chapter =
|
|
709
|
+
(lessonId && options.chapters?.[lessonId.replace(/\./g, '/')]) || undefined;
|
|
576
710
|
const registryFile = isNotationRegistryFile(file);
|
|
577
711
|
const mathScope = pageGlyphScope(
|
|
578
712
|
localDefinitions,
|
|
@@ -582,10 +716,10 @@ export default function remarkNotation(options = {}) {
|
|
|
582
716
|
const references = [];
|
|
583
717
|
const resolved = new Map();
|
|
584
718
|
|
|
585
|
-
// Equation identity (D7): assign every `\label{eq:…}` its
|
|
586
|
-
//
|
|
587
|
-
// the
|
|
588
|
-
const equationLabels = collectEquationLabels(tree);
|
|
719
|
+
// Equation identity (D7): assign every `\label{eq:…}` its number before any
|
|
720
|
+
// math node is rewritten, so a `[[eq-key]]` earlier in the page still
|
|
721
|
+
// resolves. `chapter` picks the `‹chapter›.‹n›` vs `##`-section scheme.
|
|
722
|
+
const equationLabels = collectEquationLabels(tree, chapter);
|
|
589
723
|
const pageLabels = new Map();
|
|
590
724
|
for (const label of equationLabels) {
|
|
591
725
|
if (pageLabels.has(label.key)) {
|
|
@@ -606,11 +740,50 @@ export default function remarkNotation(options = {}) {
|
|
|
606
740
|
// Number and anchor every display equation on a lesson page, keyed or
|
|
607
741
|
// not (F3). Non-lesson pages (notation registry, stray docs) keep the
|
|
608
742
|
// keyed-only behaviour so their body math is left untouched.
|
|
609
|
-
const displayEquations = lessonId
|
|
743
|
+
const displayEquations = lessonId
|
|
744
|
+
? collectDisplayEquations(tree, chapter)
|
|
745
|
+
: [];
|
|
610
746
|
const equationByNode = new Map(
|
|
611
747
|
displayEquations.map((equation) => [equation.node, equation]),
|
|
612
748
|
);
|
|
613
749
|
|
|
750
|
+
// Keyed floats (F-tables-figures): every `<Table>`/`<Figure>` on a lesson
|
|
751
|
+
// page is numbered and anchored, keyed or not — same rule as equations,
|
|
752
|
+
// via the shared engine in `keyed-jsx-floats.mjs`.
|
|
753
|
+
const makeFloatCtx = (tagName, crossPage) => {
|
|
754
|
+
const floats = lessonId
|
|
755
|
+
? collectKeyedJsxFloats(tree, { tagName, chapter })
|
|
756
|
+
: [];
|
|
757
|
+
const pageFloatLabels = new Map();
|
|
758
|
+
for (const float of floats) {
|
|
759
|
+
if (float.key == null) continue;
|
|
760
|
+
if (pageFloatLabels.has(float.key)) {
|
|
761
|
+
fail(
|
|
762
|
+
file,
|
|
763
|
+
`Duplicate <${tagName} id="${float.key}"> on this page; an id is unique per lesson.`,
|
|
764
|
+
float.node,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
pageFloatLabels.set(float.key, float.number);
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
ctx: {
|
|
771
|
+
base: options.base ?? '',
|
|
772
|
+
pageLabels: pageFloatLabels,
|
|
773
|
+
crossPage: crossPage ?? {},
|
|
774
|
+
},
|
|
775
|
+
byNode: new Map(floats.map((float) => [float.node, float])),
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
const { ctx: tableCtx, byNode: tableByNode } = makeFloatCtx(
|
|
779
|
+
'Table',
|
|
780
|
+
options.tables,
|
|
781
|
+
);
|
|
782
|
+
const { ctx: figureCtx, byNode: figureByNode } = makeFloatCtx(
|
|
783
|
+
'Figure',
|
|
784
|
+
options.figures,
|
|
785
|
+
);
|
|
786
|
+
|
|
614
787
|
const resolve = (key, kind, node) => {
|
|
615
788
|
const context = {
|
|
616
789
|
file,
|
|
@@ -729,6 +902,8 @@ export default function remarkNotation(options = {}) {
|
|
|
729
902
|
child,
|
|
730
903
|
references,
|
|
731
904
|
eqCtx,
|
|
905
|
+
tableCtx,
|
|
906
|
+
figureCtx,
|
|
732
907
|
);
|
|
733
908
|
if (replacement) {
|
|
734
909
|
node.children.splice(index, 1, ...replacement);
|
|
@@ -739,6 +914,10 @@ export default function remarkNotation(options = {}) {
|
|
|
739
914
|
visit(child, false);
|
|
740
915
|
if (child && child.type === 'math' && equationByNode.has(child)) {
|
|
741
916
|
node.children[index] = equationNode(child, equationByNode.get(child));
|
|
917
|
+
} else if (child?.type === 'mdxJsxFlowElement' && tableByNode.has(child)) {
|
|
918
|
+
injectFloatNumberAttributes(child, tableByNode.get(child), 'tab');
|
|
919
|
+
} else if (child?.type === 'mdxJsxFlowElement' && figureByNode.has(child)) {
|
|
920
|
+
injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
|
|
742
921
|
}
|
|
743
922
|
}
|
|
744
923
|
};
|
|
@@ -755,5 +934,16 @@ export default function remarkNotation(options = {}) {
|
|
|
755
934
|
indexInSection,
|
|
756
935
|
}),
|
|
757
936
|
);
|
|
937
|
+
const floatLabelData = (byNode) =>
|
|
938
|
+
[...byNode.values()]
|
|
939
|
+
.filter((float) => float.key != null)
|
|
940
|
+
.map(({ key, number, section, indexInSection }) => ({
|
|
941
|
+
key,
|
|
942
|
+
number,
|
|
943
|
+
section,
|
|
944
|
+
indexInSection,
|
|
945
|
+
}));
|
|
946
|
+
file.data.tableLabels = floatLabelData(tableByNode);
|
|
947
|
+
file.data.figureLabels = floatLabelData(figureByNode);
|
|
758
948
|
};
|
|
759
949
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
validateKeyedFloats,
|
|
5
|
+
type KeyedFloatLabelRecord,
|
|
6
|
+
type KeyedFloatLessonInput,
|
|
7
|
+
type KeyedFloatValidation,
|
|
8
|
+
} from './keyed-float-validate';
|
|
9
|
+
|
|
10
|
+
export type TableDiagnosticCode =
|
|
11
|
+
| 'tab-invalid-key'
|
|
12
|
+
| 'tab-duplicate-key'
|
|
13
|
+
| 'tab-ref-resolves'
|
|
14
|
+
| 'tab-key-unused';
|
|
15
|
+
|
|
16
|
+
export interface TableDiagnostic {
|
|
17
|
+
readonly code: TableDiagnosticCode;
|
|
18
|
+
readonly severity: 'error' | 'warning';
|
|
19
|
+
readonly message: string;
|
|
20
|
+
readonly file?: string;
|
|
21
|
+
readonly lessonId?: string;
|
|
22
|
+
readonly key?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TableLabelRecord = KeyedFloatLabelRecord;
|
|
26
|
+
export type TableLessonInput = KeyedFloatLessonInput;
|
|
27
|
+
|
|
28
|
+
export interface TableValidation extends KeyedFloatValidation {
|
|
29
|
+
readonly diagnostics: readonly TableDiagnostic[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateTables(
|
|
33
|
+
lessons: readonly TableLessonInput[],
|
|
34
|
+
): TableValidation {
|
|
35
|
+
return validateKeyedFloats(lessons, {
|
|
36
|
+
tagName: 'Table',
|
|
37
|
+
prefix: 'tab',
|
|
38
|
+
noun: 'table',
|
|
39
|
+
}) as TableValidation;
|
|
40
|
+
}
|
|
@@ -118,6 +118,24 @@ export const UNITS_VOCAB: ReadonlySet<string> = new Set([
|
|
|
118
118
|
'quoted',
|
|
119
119
|
'explicitly',
|
|
120
120
|
'calculations',
|
|
121
|
+
// motion / physical quantities (playground corpus)
|
|
122
|
+
'meter',
|
|
123
|
+
'meters',
|
|
124
|
+
'second',
|
|
125
|
+
'seconds',
|
|
126
|
+
'speed',
|
|
127
|
+
'velocity',
|
|
128
|
+
'acceleration',
|
|
129
|
+
'gravitational',
|
|
130
|
+
'launch',
|
|
131
|
+
'angle',
|
|
132
|
+
'degrees',
|
|
133
|
+
'horizontal',
|
|
134
|
+
'vertical',
|
|
135
|
+
'level',
|
|
136
|
+
'ground',
|
|
137
|
+
'squared',
|
|
138
|
+
'above',
|
|
121
139
|
]);
|
|
122
140
|
|
|
123
141
|
/** Lowercase word tokens of a `units` string (split on whitespace and hyphens). */
|
package/src/styles/global.css
CHANGED
|
@@ -668,7 +668,7 @@
|
|
|
668
668
|
`.keyed-equation` with an `id="eq-<key>"`, a `data-eq-number`, and a
|
|
669
669
|
focusable number link. Prose refers to it with `[[eq:key]]` →
|
|
670
670
|
`a.equation-ref`. The deep-link highlight is a pure-CSS `:target` animation
|
|
671
|
-
with a reduced-motion fallback; `
|
|
671
|
+
with a reduced-motion fallback; `KeyedFloatEnhancer.astro` only re-triggers it
|
|
672
672
|
on a repeated activation and moves focus. */
|
|
673
673
|
.sl-markdown-content .keyed-equation {
|
|
674
674
|
position: relative;
|
|
@@ -690,12 +690,11 @@
|
|
|
690
690
|
font-size: var(--sl-text-sm);
|
|
691
691
|
color: var(--sl-color-text-accent);
|
|
692
692
|
text-decoration: none;
|
|
693
|
-
/*
|
|
694
|
-
|
|
695
|
-
still
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
opacity: 0;
|
|
693
|
+
/* Present but quiet by default (F4): partially visible so a reader
|
|
694
|
+
scanning the page can always find the number without hunting, but it
|
|
695
|
+
still doesn't compete with the equation itself. Hover, focus, and a
|
|
696
|
+
deep-link (`:target`) push it to full opacity, same as before. */
|
|
697
|
+
opacity: 0.45;
|
|
699
698
|
transition: opacity 120ms ease-out;
|
|
700
699
|
}
|
|
701
700
|
|
|
@@ -726,6 +725,41 @@
|
|
|
726
725
|
}
|
|
727
726
|
}
|
|
728
727
|
|
|
728
|
+
/* Once a reader has actually arrived at (or clicked through to) an equation —
|
|
729
|
+
a deep-link on load, an in-page `[[eq:key]]` navigation, or pinning its
|
|
730
|
+
preview card (`FloatRefLayer.astro`) — its number stays fully visible
|
|
731
|
+
for the rest of the page's life, not just while `:target` or `:hover`
|
|
732
|
+
holds. `KeyedFloatEnhancer.astro` and `FloatRefLayer.astro` add this class
|
|
733
|
+
in JS; it is never present in the static markup. One extra class over the
|
|
734
|
+
base selector's specificity, so this always wins regardless of rule order. */
|
|
735
|
+
.sl-markdown-content a.keyed-equation__number.eq-number-revealed {
|
|
736
|
+
opacity: 1;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* A referencing `[[eq:key]]` link elsewhere in the prose was hovered/focused
|
|
740
|
+
and the equation it points to is already fully on screen: highlight it in
|
|
741
|
+
place instead of opening a preview card (`FloatRefLayer.astro` decides
|
|
742
|
+
which). Mirrors the reduced-motion `:target` treatment below — steady, no
|
|
743
|
+
keyframe — since this fires on every hover, not just a rare deep-link. */
|
|
744
|
+
.sl-markdown-content .keyed-equation.eq-ref-highlight {
|
|
745
|
+
background: var(--sl-color-accent-low);
|
|
746
|
+
outline: 2px solid var(--sl-color-accent);
|
|
747
|
+
outline-offset: 0.2rem;
|
|
748
|
+
transition:
|
|
749
|
+
background 120ms ease-out,
|
|
750
|
+
outline-color 120ms ease-out;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.sl-markdown-content .keyed-equation.eq-ref-highlight a.keyed-equation__number {
|
|
754
|
+
opacity: 1;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
@media (forced-colors: active) {
|
|
758
|
+
.sl-markdown-content .keyed-equation.eq-ref-highlight {
|
|
759
|
+
outline-color: Highlight;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
729
763
|
.sl-markdown-content a.equation-ref {
|
|
730
764
|
font-variant-numeric: tabular-nums;
|
|
731
765
|
font-weight: 600;
|
|
@@ -785,12 +819,86 @@
|
|
|
785
819
|
transform: none;
|
|
786
820
|
text-align: end;
|
|
787
821
|
color: inherit;
|
|
822
|
+
opacity: 1;
|
|
788
823
|
}
|
|
789
824
|
.sl-markdown-content .keyed-equation {
|
|
790
825
|
padding-inline-end: 0;
|
|
791
826
|
}
|
|
792
827
|
}
|
|
793
828
|
|
|
829
|
+
/* `<Table>`/`<Figure>` keyed floats (F-tables-figures) — the same deep-link
|
|
830
|
+
and hover-highlight treatment as equations above, via `KeyedFloatEnhancer`
|
|
831
|
+
and `FloatRefLayer.astro`. Unlike an equation's number (quiet until
|
|
832
|
+
hovered, F4), a table/figure number lives in its always-visible caption,
|
|
833
|
+
so there is no opacity dance to mirror here — only the `:target` flash
|
|
834
|
+
(reusing the `eq-flash` keyframe above) and the ref-highlight. */
|
|
835
|
+
.sl-markdown-content a.table-ref,
|
|
836
|
+
.sl-markdown-content a.figure-ref {
|
|
837
|
+
font-variant-numeric: tabular-nums;
|
|
838
|
+
font-weight: 600;
|
|
839
|
+
text-decoration: underline;
|
|
840
|
+
text-underline-offset: 0.15em;
|
|
841
|
+
padding-inline: 0.1em;
|
|
842
|
+
border-radius: 0.2em;
|
|
843
|
+
color: var(--sl-color-text-accent);
|
|
844
|
+
white-space: nowrap;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.sl-markdown-content a.table-ref:hover,
|
|
848
|
+
.sl-markdown-content a.table-ref:focus-visible,
|
|
849
|
+
.sl-markdown-content a.figure-ref:hover,
|
|
850
|
+
.sl-markdown-content a.figure-ref:focus-visible {
|
|
851
|
+
background: var(--sl-color-accent-low);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.sl-markdown-content .keyed-table:target,
|
|
855
|
+
.sl-markdown-content .keyed-figure:target {
|
|
856
|
+
animation: eq-flash 5s ease-out both;
|
|
857
|
+
border-radius: 0.3rem;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
@media (prefers-reduced-motion: reduce) {
|
|
861
|
+
.sl-markdown-content .keyed-table:target,
|
|
862
|
+
.sl-markdown-content .keyed-figure:target {
|
|
863
|
+
animation: none;
|
|
864
|
+
background: var(--sl-color-accent-low);
|
|
865
|
+
outline: 2px solid var(--sl-color-accent);
|
|
866
|
+
outline-offset: 0.2rem;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* A referencing `[[tab-key]]`/`[[fig-key]]` link elsewhere in the prose was
|
|
871
|
+
hovered/focused and the float it points to is already fully on screen:
|
|
872
|
+
highlight it in place instead of opening a preview card
|
|
873
|
+
(`FloatRefLayer.astro` decides which) — mirrors `.eq-ref-highlight`. */
|
|
874
|
+
.sl-markdown-content .keyed-table.tab-ref-highlight,
|
|
875
|
+
.sl-markdown-content .keyed-figure.fig-ref-highlight {
|
|
876
|
+
background: var(--sl-color-accent-low);
|
|
877
|
+
outline: 2px solid var(--sl-color-accent);
|
|
878
|
+
outline-offset: 0.2rem;
|
|
879
|
+
border-radius: 0.3rem;
|
|
880
|
+
transition:
|
|
881
|
+
background 120ms ease-out,
|
|
882
|
+
outline-color 120ms ease-out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
@media (forced-colors: active) {
|
|
886
|
+
.sl-markdown-content .keyed-table:target,
|
|
887
|
+
.sl-markdown-content .keyed-figure:target,
|
|
888
|
+
.sl-markdown-content .keyed-table.tab-ref-highlight,
|
|
889
|
+
.sl-markdown-content .keyed-figure.fig-ref-highlight {
|
|
890
|
+
animation: none;
|
|
891
|
+
outline: 2px solid Highlight;
|
|
892
|
+
outline-offset: 0.2rem;
|
|
893
|
+
}
|
|
894
|
+
.sl-markdown-content a.table-ref:hover,
|
|
895
|
+
.sl-markdown-content a.table-ref:focus-visible,
|
|
896
|
+
.sl-markdown-content a.figure-ref:hover,
|
|
897
|
+
.sl-markdown-content a.figure-ref:focus-visible {
|
|
898
|
+
outline: 2px solid Highlight;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
794
902
|
/* Deep-linkable content headings (F3). The wrapper + anchor-link markup and its
|
|
795
903
|
hover/focus reveal come from Starlight's bundled `anchor-links.css`; these
|
|
796
904
|
rules add the sticky-header scroll offset and the `:target` flash, mirroring
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Progressive enhancement for keyed equations (Step D7).
|
|
4
|
-
*
|
|
5
|
-
* The no-JS baseline is complete: `remark-notation` emits a real `id="eq-<key>"`
|
|
6
|
-
* container, a focusable number `<a>`, and `[[eq:key]]` renders a plain anchor;
|
|
7
|
-
* `global.css` does the `:target` deep-link highlight (with a
|
|
8
|
-
* `prefers-reduced-motion` fallback) and the sticky-header scroll offset via
|
|
9
|
-
* `scroll-margin`.
|
|
10
|
-
*
|
|
11
|
-
* This script only adds what CSS and anchors cannot:
|
|
12
|
-
* - a smooth, header-aware scroll on an in-page jump;
|
|
13
|
-
* - re-triggering the highlight when the same `#eq-…` is activated again —
|
|
14
|
-
* the one case a plain anchor click does nothing for, since the browser
|
|
15
|
-
* won't navigate to a fragment that's already current;
|
|
16
|
-
* - moving keyboard focus onto the equation so screen-reader users land on it.
|
|
17
|
-
*
|
|
18
|
-
* It deliberately does *not* intercept an ordinary click that changes the
|
|
19
|
-
* hash. `:target` is set only by a genuine same-document fragment
|
|
20
|
-
* navigation — never by `history.pushState` (a common trap: pushState
|
|
21
|
-
* updates the URL but leaves `:target`, and thus `eq-flash`, dark). Letting
|
|
22
|
-
* the click through and reacting to the `hashchange` it produces is what
|
|
23
|
-
* actually plays the highlight.
|
|
24
|
-
*
|
|
25
|
-
* Everything here is gated on `prefers-reduced-motion` and degrades to the
|
|
26
|
-
* native anchor behaviour if it throws.
|
|
27
|
-
*/
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
<script>
|
|
31
|
-
(() => {
|
|
32
|
-
const root = document.querySelector('.sl-markdown-content');
|
|
33
|
-
if (!root) return;
|
|
34
|
-
|
|
35
|
-
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
36
|
-
|
|
37
|
-
const activate = (target: HTMLElement | null, doScroll: boolean) => {
|
|
38
|
-
if (!target) return;
|
|
39
|
-
if (!reduce.matches) {
|
|
40
|
-
// Restart the CSS `:target` animation even when the hash is unchanged.
|
|
41
|
-
target.style.animation = 'none';
|
|
42
|
-
void target.offsetWidth;
|
|
43
|
-
target.style.removeProperty('animation');
|
|
44
|
-
}
|
|
45
|
-
if (doScroll) {
|
|
46
|
-
target.scrollIntoView({
|
|
47
|
-
behavior: reduce.matches ? 'auto' : 'smooth',
|
|
48
|
-
block: 'start',
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
target.setAttribute('tabindex', '-1');
|
|
52
|
-
try {
|
|
53
|
-
target.focus({ preventScroll: true });
|
|
54
|
-
} catch {
|
|
55
|
-
target.focus();
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
root.addEventListener('click', (event) => {
|
|
60
|
-
const origin = event.target instanceof Element ? event.target : null;
|
|
61
|
-
const link = origin?.closest('a.equation-ref, a.keyed-equation__number');
|
|
62
|
-
if (!link) return;
|
|
63
|
-
const href = link.getAttribute('href') || '';
|
|
64
|
-
if (!href.startsWith('#eq-')) return; // cross-page: let the browser navigate
|
|
65
|
-
const target = document.getElementById(href.slice(1));
|
|
66
|
-
if (!target) return;
|
|
67
|
-
// Only the already-on-this-anchor click needs handling here — the
|
|
68
|
-
// browser has nothing new to navigate to, so it won't fire `hashchange`
|
|
69
|
-
// and nothing else will re-trigger the flash. Every other click is left
|
|
70
|
-
// alone: its native navigation sets `:target` and fires `hashchange`,
|
|
71
|
-
// which the listener below turns into the same `activate()`.
|
|
72
|
-
if (location.hash === href) {
|
|
73
|
-
event.preventDefault();
|
|
74
|
-
activate(target, true);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
window.addEventListener('hashchange', () => {
|
|
79
|
-
if (!location.hash.startsWith('#eq-')) return;
|
|
80
|
-
activate(document.getElementById(location.hash.slice(1)), true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (location.hash.startsWith('#eq-')) {
|
|
84
|
-
activate(document.getElementById(location.hash.slice(1)), true);
|
|
85
|
-
}
|
|
86
|
-
})();
|
|
87
|
-
</script>
|