explico 0.1.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/README.md +108 -0
- package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
- package/dist/analytics/AnalyticsEmitter.js +5 -0
- package/dist/compiler/cli.d.ts +189 -0
- package/dist/compiler/cli.js +552 -0
- package/dist/compiler/collections.d.ts +70 -0
- package/dist/compiler/collections.js +383 -0
- package/dist/compiler/course-config.d.ts +31 -0
- package/dist/compiler/course-config.js +0 -0
- package/dist/compiler/lesson-derivation.d.ts +89 -0
- package/dist/compiler/lesson-derivation.js +91 -0
- package/dist/compiler/manifest.d.ts +178 -0
- package/dist/compiler/manifest.js +345 -0
- package/dist/compiler/resolution-report.d.ts +45 -0
- package/dist/compiler/resolution-report.js +97 -0
- package/dist/curriculum/validation.d.ts +76 -0
- package/dist/curriculum/validation.js +320 -0
- package/dist/progress/ProgressRepository.d.ts +53 -0
- package/dist/progress/ProgressRepository.js +12 -0
- package/dist/reference/base-library.mjs +27 -0
- package/dist/reference/citation-format.mjs +133 -0
- package/dist/reference/consistency.d.ts +64 -0
- package/dist/reference/consistency.js +234 -0
- package/dist/reference/curriculum-alignment.d.ts +7 -0
- package/dist/reference/curriculum-alignment.js +166 -0
- package/dist/reference/equations-validate.d.ts +50 -0
- package/dist/reference/equations-validate.js +90 -0
- package/dist/reference/equations.mjs +144 -0
- package/dist/reference/gate-math.d.ts +41 -0
- package/dist/reference/gate-math.js +151 -0
- package/dist/reference/katex-options.mjs +91 -0
- package/dist/reference/lab-math-scope.d.ts +21 -0
- package/dist/reference/lab-math-scope.js +28 -0
- package/dist/reference/label.d.ts +11 -0
- package/dist/reference/label.js +38 -0
- package/dist/reference/lint-ignore.d.ts +32 -0
- package/dist/reference/lint-ignore.js +39 -0
- package/dist/reference/math-glyphs.mjs +830 -0
- package/dist/reference/prose.d.ts +15 -0
- package/dist/reference/prose.js +12 -0
- package/dist/reference/references.d.ts +17 -0
- package/dist/reference/references.js +42 -0
- package/dist/reference/registry.d.ts +6 -0
- package/dist/reference/registry.js +548 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +753 -0
- package/dist/reference/render-lab-math.d.ts +21 -0
- package/dist/reference/render-lab-math.js +58 -0
- package/dist/reference/types.d.ts +151 -0
- package/dist/reference/types.js +0 -0
- package/dist/reference/units-vocab.d.ts +26 -0
- package/dist/reference/units-vocab.js +119 -0
- package/dist/session/PreferenceStore.d.ts +28 -0
- package/dist/session/PreferenceStore.js +30 -0
- package/dist/session/user-context.d.ts +7 -0
- package/dist/session/user-context.js +10 -0
- package/dist/session/user.d.ts +21 -0
- package/dist/session/user.js +8 -0
- package/package.json +53 -0
- package/src/analytics/AnalyticsEmitter.ts +23 -0
- package/src/compiler/cli.ts +866 -0
- package/src/compiler/collections.ts +554 -0
- package/src/compiler/course-config.ts +32 -0
- package/src/compiler/lesson-derivation.ts +186 -0
- package/src/compiler/manifest.ts +601 -0
- package/src/compiler/resolution-report.ts +150 -0
- package/src/components/CurriculumMap.astro +208 -0
- package/src/components/HoverPanel.astro +67 -0
- package/src/components/assessments/AssessmentRunner.tsx +320 -0
- package/src/components/assessments/AssessmentSet.astro +54 -0
- package/src/components/citation/CitationLayer.astro +173 -0
- package/src/components/examples/CompactExample.astro +34 -0
- package/src/components/examples/CompactExamples.astro +253 -0
- package/src/components/hover-panel.ts +273 -0
- package/src/components/notation/NotationGlossary.astro +412 -0
- package/src/components/notation/NotationLayer.astro +594 -0
- package/src/components/notation/NotationMath.astro +24 -0
- package/src/components/notation/SafeMathNode.astro +23 -0
- package/src/components/notation/render-notation-math.ts +223 -0
- package/src/components/starlight/EquationEnhancer.astro +72 -0
- package/src/components/starlight/LayoutHeader.astro +102 -0
- package/src/components/starlight/LayoutPageSidebar.astro +33 -0
- package/src/components/starlight/LayoutSidebar.astro +29 -0
- package/src/components/starlight/LessonFooter.astro +58 -0
- package/src/content-config.ts +259 -0
- package/src/curriculum/validation.ts +481 -0
- package/src/progress/ProgressRepository.ts +64 -0
- package/src/reference/base-library.mjs +27 -0
- package/src/reference/citation-format.mjs +133 -0
- package/src/reference/consistency.ts +375 -0
- package/src/reference/curriculum-alignment.ts +228 -0
- package/src/reference/equations-validate.ts +167 -0
- package/src/reference/equations.mjs +144 -0
- package/src/reference/gate-math.ts +250 -0
- package/src/reference/katex-options.mjs +91 -0
- package/src/reference/lab-math-scope.ts +54 -0
- package/src/reference/label.ts +47 -0
- package/src/reference/lint-ignore.ts +75 -0
- package/src/reference/math-glyphs.mjs +830 -0
- package/src/reference/prose.ts +21 -0
- package/src/reference/references.ts +82 -0
- package/src/reference/registry.ts +723 -0
- package/src/reference/rehype-fail-katex-errors.mjs +55 -0
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +753 -0
- package/src/reference/render-lab-math.ts +99 -0
- package/src/reference/types.ts +182 -0
- package/src/reference/units-vocab.ts +143 -0
- package/src/session/PreferenceStore.ts +54 -0
- package/src/session/user-context.ts +18 -0
- package/src/session/user.ts +24 -0
- package/src/styles/global.css +763 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The committed per-lesson resolution report (Step D6 (d)).
|
|
3
|
+
*
|
|
4
|
+
* `compileManifest()` already resolves every `[[key]]` / `\explain{key}`
|
|
5
|
+
* occurrence to a definition, scope, and source span
|
|
6
|
+
* (`manifest.lessons[].notation.resolution`). That lives in the git-ignored
|
|
7
|
+
* `.generated/manifest.json`, so this module projects it into
|
|
8
|
+
* `resolution/<lessonId>.notation.json` — one small, diff-checkable file per
|
|
9
|
+
* lesson — plus `resolution/consistency-report.json` for the Tier-3 corpus
|
|
10
|
+
* checks. `pnpm validate:content` regenerates them and **fails if a committed
|
|
11
|
+
* file is stale**, so a drift between content and the checked-in report cannot
|
|
12
|
+
* pass CI unnoticed.
|
|
13
|
+
*
|
|
14
|
+
* The reports are deterministic: keys are sorted recursively, rows follow the
|
|
15
|
+
* manifest's stable ordering, and nothing records a timestamp or a content
|
|
16
|
+
* hash — a file changes only when a resolution or a finding changes.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
consistencyCounts,
|
|
31
|
+
type ConsistencyDiagnostic,
|
|
32
|
+
} from '../reference/consistency';
|
|
33
|
+
import { MANIFEST_SCHEMA_VERSION, type Manifest } from './manifest';
|
|
34
|
+
|
|
35
|
+
const REPOSITORY_ROOT = process.cwd();
|
|
36
|
+
const RESOLUTION_DIR = join(REPOSITORY_ROOT, 'build', 'resolution');
|
|
37
|
+
/** Repo-relative prefix for the map keys and the stale-path messages (Phase F). */
|
|
38
|
+
const RESOLUTION_PREFIX = 'build/resolution/';
|
|
39
|
+
const CONSISTENCY_FILE = 'consistency-report.json';
|
|
40
|
+
|
|
41
|
+
/** JSON with recursively sorted object keys and a trailing newline. */
|
|
42
|
+
function stableJson(value: unknown): string {
|
|
43
|
+
const sortKeys = (node: unknown): unknown => {
|
|
44
|
+
if (Array.isArray(node)) return node.map(sortKeys);
|
|
45
|
+
if (node && typeof node === 'object') {
|
|
46
|
+
return Object.fromEntries(
|
|
47
|
+
Object.keys(node as Record<string, unknown>)
|
|
48
|
+
.sort()
|
|
49
|
+
.map((key) => [
|
|
50
|
+
key,
|
|
51
|
+
sortKeys((node as Record<string, unknown>)[key]),
|
|
52
|
+
]),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return node;
|
|
56
|
+
};
|
|
57
|
+
return `${JSON.stringify(sortKeys(value), null, 2)}\n`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `{ 'build/resolution/foo.notation.json': '…', … }` — the full expected tree. */
|
|
61
|
+
export function buildResolutionReports(
|
|
62
|
+
manifest: Manifest,
|
|
63
|
+
): Map<string, string> {
|
|
64
|
+
const files = new Map<string, string>();
|
|
65
|
+
|
|
66
|
+
const localKeysByLesson = new Map<string, string[]>();
|
|
67
|
+
for (const definition of manifest.notation.definitions) {
|
|
68
|
+
if (definition.kind !== 'local') continue;
|
|
69
|
+
const list = localKeysByLesson.get(definition.lessonId) ?? [];
|
|
70
|
+
list.push(definition.key);
|
|
71
|
+
localKeysByLesson.set(definition.lessonId, list);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const lesson of manifest.lessons) {
|
|
75
|
+
files.set(
|
|
76
|
+
`${RESOLUTION_PREFIX}${lesson.id}.notation.json`,
|
|
77
|
+
stableJson({
|
|
78
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
79
|
+
lesson: lesson.id,
|
|
80
|
+
file: lesson.file,
|
|
81
|
+
localKeys: [...(localKeysByLesson.get(lesson.id) ?? [])].sort(),
|
|
82
|
+
references: lesson.notation.resolution,
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
files.set(
|
|
88
|
+
`${RESOLUTION_PREFIX}${CONSISTENCY_FILE}`,
|
|
89
|
+
stableJson({
|
|
90
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
91
|
+
generatedBy: 'pnpm validate:content',
|
|
92
|
+
counts: consistencyCounts(manifest.diagnostics.consistency),
|
|
93
|
+
diagnostics: manifest.diagnostics
|
|
94
|
+
.consistency as readonly ConsistencyDiagnostic[],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return files;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ResolutionReportSync {
|
|
102
|
+
readonly written: number;
|
|
103
|
+
/** Repo-relative paths that were out of date (created, changed, or removed). */
|
|
104
|
+
readonly stale: readonly string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Write every expected report and prune orphans. Returns the paths that did not
|
|
109
|
+
* already match on disk — the caller turns a non-empty list into a build
|
|
110
|
+
* failure so the checked-in reports stay honest.
|
|
111
|
+
*/
|
|
112
|
+
export function syncResolutionReports(
|
|
113
|
+
manifest: Manifest,
|
|
114
|
+
): ResolutionReportSync {
|
|
115
|
+
const expected = buildResolutionReports(manifest);
|
|
116
|
+
mkdirSync(RESOLUTION_DIR, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const onDisk = new Set(
|
|
119
|
+
existsSync(RESOLUTION_DIR)
|
|
120
|
+
? readdirSync(RESOLUTION_DIR).filter((name) => name.endsWith('.json'))
|
|
121
|
+
: [],
|
|
122
|
+
);
|
|
123
|
+
const stale: string[] = [];
|
|
124
|
+
|
|
125
|
+
for (const [relPath, content] of [...expected].sort(([a], [b]) =>
|
|
126
|
+
a.localeCompare(b),
|
|
127
|
+
)) {
|
|
128
|
+
const name = relPath.slice(RESOLUTION_PREFIX.length);
|
|
129
|
+
const absolute = join(RESOLUTION_DIR, name);
|
|
130
|
+
onDisk.delete(name);
|
|
131
|
+
const current = existsSync(absolute)
|
|
132
|
+
? readFileSync(absolute, 'utf8')
|
|
133
|
+
: undefined;
|
|
134
|
+
if (current !== content) {
|
|
135
|
+
writeFileSync(absolute, content);
|
|
136
|
+
stale.push(
|
|
137
|
+
`${relPath} (${current === undefined ? 'missing' : 'changed'})`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const orphan of [...onDisk].sort()) {
|
|
143
|
+
rmSync(join(RESOLUTION_DIR, orphan));
|
|
144
|
+
stale.push(`${RESOLUTION_PREFIX}${orphan} (stale, removed)`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { written: expected.size, stale };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { RESOLUTION_DIR };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { loadManifest } from '../compiler/manifest';
|
|
4
|
+
import { isLessonSlug, lessonIdFromSlug } from '../compiler/lesson-derivation';
|
|
5
|
+
|
|
6
|
+
const [tracks, docs, competencies, manifest] = await Promise.all([
|
|
7
|
+
getCollection('tracks'),
|
|
8
|
+
getCollection('docs'),
|
|
9
|
+
getCollection('competencies'),
|
|
10
|
+
loadManifest(),
|
|
11
|
+
]);
|
|
12
|
+
// `requires` / `assessments` are derived (Phase C2); read them from the one
|
|
13
|
+
// compiler manifest, and the doc entry for title / description / slug.
|
|
14
|
+
const docEntryById = new Map(
|
|
15
|
+
docs
|
|
16
|
+
.filter((entry) => isLessonSlug(entry.id))
|
|
17
|
+
.map((entry) => [lessonIdFromSlug(entry.id), entry]),
|
|
18
|
+
);
|
|
19
|
+
const lessonById = new Map(
|
|
20
|
+
manifest.lessons.map((lesson) => [lesson.id, lesson]),
|
|
21
|
+
);
|
|
22
|
+
const competencyById = new Map(
|
|
23
|
+
competencies.map((entry) => [entry.data.id, entry.data]),
|
|
24
|
+
);
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
tracks.map(({ data: track }) => (
|
|
29
|
+
<section class="track" aria-labelledby={`track-${track.id}`}>
|
|
30
|
+
<header>
|
|
31
|
+
<h2 id={`track-${track.id}`}>{track.title}</h2>
|
|
32
|
+
<p>{track.description}</p>
|
|
33
|
+
<p>
|
|
34
|
+
<strong>Audience:</strong> {track.audience}
|
|
35
|
+
</p>
|
|
36
|
+
<div class="entry-assumptions">
|
|
37
|
+
<strong>Entry assumptions:</strong>
|
|
38
|
+
<ul>
|
|
39
|
+
{track.entryAssumptions.map((assumption) => (
|
|
40
|
+
<li>{assumption}</li>
|
|
41
|
+
))}
|
|
42
|
+
</ul>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<ol class="lesson-sequence">
|
|
47
|
+
{track.lessons.map((lessonId, index) => {
|
|
48
|
+
const lesson = lessonById.get(lessonId);
|
|
49
|
+
const entry = docEntryById.get(lessonId);
|
|
50
|
+
if (!lesson || !entry) return null;
|
|
51
|
+
return (
|
|
52
|
+
<li>
|
|
53
|
+
<article>
|
|
54
|
+
<p class="step">Lesson {index + 1}</p>
|
|
55
|
+
<h3>
|
|
56
|
+
<a
|
|
57
|
+
href={`${import.meta.env.BASE_URL.replace(/\/$/, '')}/${entry.id}/`}
|
|
58
|
+
>
|
|
59
|
+
{entry.data.title}
|
|
60
|
+
</a>
|
|
61
|
+
</h3>
|
|
62
|
+
<code>{lessonId}</code>
|
|
63
|
+
<p>{entry.data.description}</p>
|
|
64
|
+
|
|
65
|
+
<dl>
|
|
66
|
+
<div>
|
|
67
|
+
<dt>Requires</dt>
|
|
68
|
+
<dd>
|
|
69
|
+
{lesson.requires.length > 0
|
|
70
|
+
? lesson.requires.map((id) => (
|
|
71
|
+
<span title={competencyById.get(id)?.title}>
|
|
72
|
+
<code>{id}</code>
|
|
73
|
+
</span>
|
|
74
|
+
))
|
|
75
|
+
: 'Track entry assumptions only'}
|
|
76
|
+
</dd>
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<dt>Teaches</dt>
|
|
80
|
+
<dd>
|
|
81
|
+
{lesson.teaches.map((id) => (
|
|
82
|
+
<span title={competencyById.get(id)?.title}>
|
|
83
|
+
<code>{id}</code>
|
|
84
|
+
</span>
|
|
85
|
+
))}
|
|
86
|
+
</dd>
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<dt>Evidence</dt>
|
|
90
|
+
<dd>
|
|
91
|
+
{lesson.assessments.map((id) => (
|
|
92
|
+
<code>{id}</code>
|
|
93
|
+
))}
|
|
94
|
+
</dd>
|
|
95
|
+
</div>
|
|
96
|
+
</dl>
|
|
97
|
+
</article>
|
|
98
|
+
</li>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</ol>
|
|
102
|
+
</section>
|
|
103
|
+
))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
<section aria-labelledby="competency-catalog">
|
|
107
|
+
<h2 id="competency-catalog">Atomic competency catalog</h2>
|
|
108
|
+
<p>
|
|
109
|
+
Each competency has an explicit prerequisite list and an evidence policy.
|
|
110
|
+
The build rejects unknown references, cycles, invalid lesson ordering, and
|
|
111
|
+
insufficient assessment coverage.
|
|
112
|
+
</p>
|
|
113
|
+
<ol class="competency-map">
|
|
114
|
+
{
|
|
115
|
+
competencies
|
|
116
|
+
.sort((left, right) => left.data.id.localeCompare(right.data.id))
|
|
117
|
+
.map(({ data }) => (
|
|
118
|
+
<li>
|
|
119
|
+
<strong>{data.title}</strong>
|
|
120
|
+
<br />
|
|
121
|
+
<code>{data.id}</code>
|
|
122
|
+
<p>{data.outcome}</p>
|
|
123
|
+
{data.prerequisites.length > 0 ? (
|
|
124
|
+
<small>
|
|
125
|
+
Requires: <code>{data.prerequisites.join(', ')}</code>
|
|
126
|
+
</small>
|
|
127
|
+
) : (
|
|
128
|
+
<small>Entry competency: no declared prerequisites.</small>
|
|
129
|
+
)}
|
|
130
|
+
</li>
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
</ol>
|
|
134
|
+
</section>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
.track > header {
|
|
138
|
+
margin-block-end: 1.25rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.entry-assumptions ul {
|
|
142
|
+
margin-block: 0.35rem 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.lesson-sequence,
|
|
146
|
+
.competency-map {
|
|
147
|
+
display: grid;
|
|
148
|
+
gap: 1rem;
|
|
149
|
+
padding-inline-start: 1.5rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.lesson-sequence > li::marker {
|
|
153
|
+
color: var(--sl-color-accent-high);
|
|
154
|
+
font-weight: 700;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.lesson-sequence article,
|
|
158
|
+
.competency-map li {
|
|
159
|
+
padding: 1rem;
|
|
160
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
161
|
+
border-radius: 0.55rem;
|
|
162
|
+
background: var(--sl-color-gray-7);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.lesson-sequence h3,
|
|
166
|
+
.lesson-sequence p,
|
|
167
|
+
.competency-map p {
|
|
168
|
+
margin-block: 0.4rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.step {
|
|
172
|
+
color: var(--sl-color-gray-3);
|
|
173
|
+
font-size: var(--sl-text-sm);
|
|
174
|
+
font-weight: 600;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
letter-spacing: 0.04em;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.lesson-sequence dl {
|
|
180
|
+
display: grid;
|
|
181
|
+
gap: 0.7rem;
|
|
182
|
+
margin-block: 1rem 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.lesson-sequence dl div {
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: minmax(5rem, 0.25fr) 1fr;
|
|
188
|
+
gap: 0.75rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.lesson-sequence dt {
|
|
192
|
+
font-weight: 700;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.lesson-sequence dd {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-wrap: wrap;
|
|
198
|
+
gap: 0.35rem 0.6rem;
|
|
199
|
+
margin: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@media (max-width: 36rem) {
|
|
203
|
+
.lesson-sequence dl div {
|
|
204
|
+
grid-template-columns: 1fr;
|
|
205
|
+
gap: 0.1rem;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Shared hover/pin panel shell for the notation and citation layers.
|
|
4
|
+
*
|
|
5
|
+
* This component contributes only the fixed-position `<aside>` container and
|
|
6
|
+
* its base styling. The interaction engine — side selection, the sticky-header
|
|
7
|
+
* and article-column clamps, pin, Escape, and outside-close — lives once in
|
|
8
|
+
* `./hover-panel.ts` and is wired from each layer's island script. The panel's
|
|
9
|
+
* inner fields are slotted by the layer that owns them.
|
|
10
|
+
*
|
|
11
|
+
* Extra attributes (e.g. `data-notation-panel`, `data-citation-panel`) are
|
|
12
|
+
* forwarded to the `<aside>` so each layer keeps the hook its tests and pin
|
|
13
|
+
* controls already reference.
|
|
14
|
+
*/
|
|
15
|
+
export interface Props {
|
|
16
|
+
/** Element id — pin controls point `aria-controls` at it. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Accessible name for the region. */
|
|
19
|
+
label: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { id, label, class: className, ...rest } = Astro.props;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<aside
|
|
27
|
+
class:list={['hover-panel', className]}
|
|
28
|
+
id={id}
|
|
29
|
+
role="region"
|
|
30
|
+
aria-label={label}
|
|
31
|
+
aria-live="polite"
|
|
32
|
+
aria-atomic="true"
|
|
33
|
+
hidden
|
|
34
|
+
{...rest}
|
|
35
|
+
>
|
|
36
|
+
<slot />
|
|
37
|
+
</aside>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
@layer starlight.components {
|
|
41
|
+
.hover-panel {
|
|
42
|
+
position: fixed;
|
|
43
|
+
z-index: 100;
|
|
44
|
+
/* The panel is an <aside> in the content flow; a stray cascade margin
|
|
45
|
+
would offset a position:fixed box from its computed `top`, so the
|
|
46
|
+
JS placement (which clears display equations) must start from zero. */
|
|
47
|
+
margin: 0;
|
|
48
|
+
width: min(23rem, calc(100vw - 1.5rem));
|
|
49
|
+
overflow: auto;
|
|
50
|
+
padding: 1rem;
|
|
51
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
52
|
+
border-top: 0.25rem solid var(--sl-color-accent);
|
|
53
|
+
border-radius: 0.65rem;
|
|
54
|
+
color: var(--sl-color-text);
|
|
55
|
+
background: var(--sl-color-bg);
|
|
56
|
+
box-shadow: var(--sl-shadow-lg);
|
|
57
|
+
font-size: var(--sl-text-sm);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Slotted fields carry the owning layer's scope, not this component's, so
|
|
61
|
+
the descendant match must be global. */
|
|
62
|
+
.hover-panel :global(:focus-visible) {
|
|
63
|
+
outline: 3px solid var(--sl-color-accent);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</style>
|