explico 0.4.0 → 0.6.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 (150) hide show
  1. package/CHANGELOG.md +382 -1
  2. package/README.md +103 -8
  3. package/dist/compiler/astro.d.ts +9 -1
  4. package/dist/compiler/astro.js +182 -3
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +28 -4
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +88 -1
  10. package/dist/compiler/course-config.js +35 -1
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/manifest.d.ts +98 -6
  14. package/dist/compiler/manifest.js +154 -17
  15. package/dist/compiler/resolution-report.d.ts +4 -0
  16. package/dist/reference/base-abbreviations.d.mts +30 -0
  17. package/dist/reference/base-abbreviations.mjs +60 -0
  18. package/dist/reference/base-operators.d.mts +15 -0
  19. package/dist/reference/base-operators.mjs +40 -0
  20. package/dist/reference/base-roman-labels.d.mts +35 -0
  21. package/dist/reference/base-roman-labels.mjs +43 -0
  22. package/dist/reference/consistency.d.ts +8 -2
  23. package/dist/reference/consistency.js +50 -3
  24. package/dist/reference/diagrams-validate.d.ts +21 -0
  25. package/dist/reference/diagrams-validate.js +13 -0
  26. package/dist/reference/equations.d.mts +10 -15
  27. package/dist/reference/equations.mjs +67 -14
  28. package/dist/reference/gate-abbreviations.d.ts +17 -0
  29. package/dist/reference/gate-abbreviations.js +64 -0
  30. package/dist/reference/gate-heading-length.d.ts +12 -0
  31. package/dist/reference/gate-heading-length.js +79 -0
  32. package/dist/reference/gate-math.d.ts +32 -5
  33. package/dist/reference/gate-math.js +6 -2
  34. package/dist/reference/gate-prose-notation.d.ts +60 -0
  35. package/dist/reference/gate-prose-notation.js +125 -0
  36. package/dist/reference/keyed-float-validate.d.ts +5 -5
  37. package/dist/reference/keyed-jsx-floats.d.mts +7 -12
  38. package/dist/reference/keyed-jsx-floats.mjs +50 -25
  39. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  40. package/dist/reference/knowledge-checks-validate.js +78 -0
  41. package/dist/reference/knowledge-checks.d.mts +25 -0
  42. package/dist/reference/knowledge-checks.mjs +45 -0
  43. package/dist/reference/lab-math-scope.d.ts +9 -1
  44. package/dist/reference/lab-math-scope.js +32 -4
  45. package/dist/reference/math-glyphs.d.mts +42 -0
  46. package/dist/reference/math-glyphs.mjs +548 -40
  47. package/dist/reference/mdx-attributes.d.mts +13 -0
  48. package/dist/reference/mdx-attributes.mjs +38 -0
  49. package/dist/reference/mixed-content.d.ts +17 -0
  50. package/dist/reference/mixed-content.js +29 -0
  51. package/dist/reference/page-notation.d.ts +52 -0
  52. package/dist/reference/page-notation.js +171 -0
  53. package/dist/reference/raw-floats-validate.d.ts +18 -0
  54. package/dist/reference/raw-floats-validate.js +53 -0
  55. package/dist/reference/raw-floats.d.mts +18 -0
  56. package/dist/reference/raw-floats.mjs +129 -0
  57. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  58. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  59. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  60. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  61. package/dist/reference/remark-abbreviations.d.mts +7 -0
  62. package/dist/reference/remark-abbreviations.mjs +174 -0
  63. package/dist/reference/remark-citation.d.mts +5 -1
  64. package/dist/reference/remark-citation.mjs +0 -0
  65. package/dist/reference/remark-memo.d.mts +16 -0
  66. package/dist/reference/remark-memo.mjs +339 -0
  67. package/dist/reference/remark-notation.d.mts +57 -9
  68. package/dist/reference/remark-notation.mjs +354 -42
  69. package/dist/reference/remark-short-note.d.mts +4 -0
  70. package/dist/reference/remark-short-note.mjs +237 -0
  71. package/dist/reference/types.d.ts +16 -1
  72. package/dist/reference/units-vocab.js +4 -0
  73. package/docs/VERSIONING.md +53 -4
  74. package/docs/authoring/README.md +10 -0
  75. package/docs/authoring/agents.md +17 -2
  76. package/docs/authoring/components.md +138 -0
  77. package/docs/authoring/migrating.md +275 -7
  78. package/docs/authoring/notation.md +615 -1
  79. package/docs/authoring/structure-rules.md +69 -0
  80. package/docs/authoring/structure.md +120 -8
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +208 -3
  84. package/src/compiler/cli.ts +29 -2
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +132 -0
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/manifest.ts +281 -22
  89. package/src/components/HoverPanel.astro +45 -4
  90. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  91. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  92. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  93. package/src/components/assessments/AssessmentSet.astro +98 -14
  94. package/src/components/assessments/RenderedContent.tsx +55 -0
  95. package/src/components/assessments/assessment-randomization.ts +16 -0
  96. package/src/components/diagram/Diagram.astro +409 -103
  97. package/src/components/disclosure/hover-preview.ts +202 -0
  98. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  99. package/src/components/examples/CompactExamples.astro +2 -0
  100. package/src/components/figure/Figure.astro +279 -10
  101. package/src/components/memo/Memo.astro +453 -0
  102. package/src/components/memo/ShortNote.astro +223 -0
  103. package/src/components/notation/NotationFormula.astro +18 -1
  104. package/src/components/notation/NotationLayer.astro +260 -83
  105. package/src/components/notation/render-notation-math.ts +39 -2
  106. package/src/components/notation/safe-math-to-react.ts +50 -0
  107. package/src/components/reference/FloatRefLayer.astro +233 -15
  108. package/src/components/starlight/CollapsibleSections.astro +461 -0
  109. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  110. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  111. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  112. package/src/components/starlight/LayoutHeader.astro +50 -0
  113. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  114. package/src/components/starlight/LessonExportActions.astro +436 -0
  115. package/src/components/starlight/LessonFooter.astro +30 -3
  116. package/src/components/starlight/section-parts.ts +31 -0
  117. package/src/components/table/Table.astro +293 -7
  118. package/src/components/table/TableOverflowExpander.astro +489 -0
  119. package/src/content-config.ts +36 -1
  120. package/src/reference/base-abbreviations.mjs +60 -0
  121. package/src/reference/base-operators.mjs +40 -0
  122. package/src/reference/base-roman-labels.mjs +43 -0
  123. package/src/reference/consistency.ts +73 -2
  124. package/src/reference/diagrams-validate.ts +40 -0
  125. package/src/reference/equations.mjs +67 -14
  126. package/src/reference/gate-abbreviations.ts +128 -0
  127. package/src/reference/gate-heading-length.ts +131 -0
  128. package/src/reference/gate-math.ts +32 -7
  129. package/src/reference/gate-prose-notation.ts +224 -0
  130. package/src/reference/keyed-float-validate.ts +5 -5
  131. package/src/reference/keyed-jsx-floats.mjs +50 -25
  132. package/src/reference/knowledge-checks-validate.ts +130 -0
  133. package/src/reference/knowledge-checks.mjs +45 -0
  134. package/src/reference/lab-math-scope.ts +45 -3
  135. package/src/reference/math-glyphs.mjs +548 -40
  136. package/src/reference/mdx-attributes.mjs +38 -0
  137. package/src/reference/mixed-content.ts +69 -0
  138. package/src/reference/page-notation.ts +252 -0
  139. package/src/reference/raw-floats-validate.ts +103 -0
  140. package/src/reference/raw-floats.mjs +129 -0
  141. package/src/reference/rehype-heading-anchors.mjs +107 -1
  142. package/src/reference/rehype-table-header-labels.mjs +152 -0
  143. package/src/reference/remark-abbreviations.mjs +174 -0
  144. package/src/reference/remark-citation.mjs +0 -0
  145. package/src/reference/remark-memo.mjs +339 -0
  146. package/src/reference/remark-notation.mjs +354 -42
  147. package/src/reference/remark-short-note.mjs +237 -0
  148. package/src/reference/types.ts +16 -0
  149. package/src/reference/units-vocab.ts +4 -0
  150. package/src/styles/global.css +996 -50
@@ -0,0 +1,436 @@
1
+ ---
2
+ interface Props {
3
+ standaloneDownload?: {
4
+ readonly href: string;
5
+ readonly filename: string;
6
+ };
7
+ print: boolean;
8
+ canonicalBaseUrl?: string;
9
+ siteBaseUrl: string;
10
+ }
11
+
12
+ const {
13
+ standaloneDownload,
14
+ print,
15
+ canonicalBaseUrl,
16
+ siteBaseUrl,
17
+ } = Astro.props;
18
+ ---
19
+
20
+ <nav
21
+ class="lesson-export-actions not-content"
22
+ aria-label="Lesson exports"
23
+ data-explico-export-chrome="lesson-actions"
24
+ data-explico-print-controller={print ? '' : undefined}
25
+ data-canonical-base-url={canonicalBaseUrl}
26
+ data-site-base-url={siteBaseUrl}
27
+ >
28
+ {
29
+ standaloneDownload && (
30
+ <a
31
+ href={standaloneDownload.href}
32
+ download={standaloneDownload.filename}
33
+ aria-label="Download this lesson as standalone HTML"
34
+ title="Download this lesson as standalone HTML"
35
+ >
36
+ <svg viewBox="0 0 24 24" aria-hidden="true">
37
+ <path d="M12 3v12m0 0 4-4m-4 4-4-4M5 19h14" />
38
+ </svg>
39
+ </a>
40
+ )
41
+ }
42
+ {
43
+ print && (
44
+ <button
45
+ type="button"
46
+ data-explico-print-action
47
+ aria-label="Print or save this lesson as PDF"
48
+ title="Print or save this lesson as PDF"
49
+ >
50
+ <svg viewBox="0 0 24 24" aria-hidden="true">
51
+ <path d="M7 9V3h10v6M7 17H5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2M7 14h10v7H7z" />
52
+ </svg>
53
+ </button>
54
+ )
55
+ }
56
+ </nav>
57
+
58
+ <style>
59
+ .lesson-export-actions {
60
+ display: inline-flex;
61
+ }
62
+
63
+ .lesson-export-actions a,
64
+ .lesson-export-actions button {
65
+ align-items: center;
66
+ background: transparent;
67
+ border: 1px solid var(--sl-color-gray-5);
68
+ border-radius: 999px;
69
+ color: var(--sl-color-gray-1);
70
+ cursor: pointer;
71
+ display: inline-flex;
72
+ font: inherit;
73
+ height: 1.75rem;
74
+ justify-content: center;
75
+ padding: 0;
76
+ text-decoration: none;
77
+ width: 1.75rem;
78
+ }
79
+
80
+ .lesson-export-actions svg {
81
+ fill: none;
82
+ height: 1rem;
83
+ stroke: currentColor;
84
+ stroke-linecap: round;
85
+ stroke-linejoin: round;
86
+ stroke-width: 1.75;
87
+ width: 1rem;
88
+ }
89
+
90
+ .lesson-export-actions :is(a, button):hover,
91
+ .lesson-export-actions :is(a, button):focus-visible {
92
+ border-color: var(--sl-color-accent);
93
+ color: var(--sl-color-text-accent);
94
+ }
95
+
96
+ .lesson-export-actions :is(a, button):focus-visible {
97
+ outline: 2px solid var(--sl-color-accent);
98
+ outline-offset: 2px;
99
+ }
100
+ </style>
101
+
102
+ <script>
103
+ type SavedDetails = {
104
+ readonly element: HTMLDetailsElement;
105
+ readonly open: boolean;
106
+ readonly state: string | undefined;
107
+ readonly style: string | null;
108
+ readonly body: HTMLElement | null;
109
+ readonly bodyStyle: string | null;
110
+ readonly temporaryMarker: string | null;
111
+ };
112
+
113
+ type SavedLink = {
114
+ readonly element: HTMLAnchorElement;
115
+ readonly href: string | null;
116
+ };
117
+
118
+ type SavedHidden = {
119
+ readonly element: HTMLElement;
120
+ readonly hidden: boolean;
121
+ };
122
+
123
+ type StaticExportMode = 'print' | 'markdown';
124
+
125
+ type PreparedStaticExport = {
126
+ readonly mode: StaticExportMode;
127
+ readonly details: readonly SavedDetails[];
128
+ readonly hidden: readonly SavedHidden[];
129
+ readonly links: readonly SavedLink[];
130
+ };
131
+
132
+ const root = document.documentElement;
133
+ let prepared: PreparedStaticExport | null = null;
134
+ let pendingMarkerCleanup: (() => void) | null = null;
135
+
136
+ function controller(): HTMLElement | null {
137
+ return document.querySelector<HTMLElement>('[data-explico-print-controller]');
138
+ }
139
+
140
+ function canonicalPrintHref(
141
+ link: HTMLAnchorElement,
142
+ canonicalBaseUrl: string | undefined,
143
+ siteBaseUrl: string,
144
+ ): string | undefined {
145
+ const rawHref = link.getAttribute('href');
146
+ if (!rawHref || rawHref.startsWith('#') || link.hasAttribute('download')) {
147
+ return undefined;
148
+ }
149
+
150
+ let target: URL;
151
+ try {
152
+ target = new URL(rawHref, location.href);
153
+ } catch {
154
+ return undefined;
155
+ }
156
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') return undefined;
157
+
158
+ const current = new URL(location.href);
159
+ if (
160
+ target.origin === current.origin &&
161
+ target.pathname === current.pathname &&
162
+ target.search === current.search
163
+ ) {
164
+ return undefined;
165
+ }
166
+
167
+ let result = target.href;
168
+ if (canonicalBaseUrl && target.origin === current.origin) {
169
+ const normalizedSiteBase = `/${siteBaseUrl.replace(/^\/+|\/+$/gu, '')}/`;
170
+ const relativePath = target.pathname.startsWith(normalizedSiteBase)
171
+ ? target.pathname.slice(normalizedSiteBase.length)
172
+ : target.pathname.replace(/^\/+/, '');
173
+ result = new URL(
174
+ `${relativePath}${target.search}${target.hash}`,
175
+ canonicalBaseUrl,
176
+ ).href;
177
+ }
178
+
179
+ return result;
180
+ }
181
+
182
+ function prepareStaticExport(mode: StaticExportMode): void {
183
+ if (prepared) return;
184
+ const printController = controller();
185
+ if (!printController) return;
186
+ pendingMarkerCleanup?.();
187
+
188
+ root.dataset.explicoStaticExport = mode;
189
+
190
+ const details = Array.from(
191
+ document.querySelectorAll<HTMLDetailsElement>('main details'),
192
+ (element): SavedDetails => {
193
+ const body = element.querySelector<HTMLElement>(
194
+ ':scope > .sl-section-body, :scope > .diagram__body',
195
+ );
196
+ const saved = {
197
+ element,
198
+ open: element.open,
199
+ state: element.dataset.state,
200
+ style: element.getAttribute('style'),
201
+ body,
202
+ bodyStyle: body?.getAttribute('style') ?? null,
203
+ temporaryMarker: element.getAttribute('data-explico-temporary-open'),
204
+ };
205
+ element.dataset.explicoTemporaryOpen = mode;
206
+ element.open = true;
207
+ if (element.dataset.state !== undefined) element.dataset.state = 'open';
208
+ body?.style.removeProperty('max-height');
209
+ body?.style.removeProperty('overflow');
210
+ body?.style.removeProperty('transition');
211
+ return saved;
212
+ },
213
+ );
214
+
215
+ const hidden = Array.from(
216
+ document.querySelectorAll<HTMLElement>(
217
+ 'main :is(.assessment-panel, .short-note, .compact-example, .keyed-table__body tr:not(.keyed-table__more-row))[hidden]',
218
+ ),
219
+ (element): SavedHidden => {
220
+ const saved = { element, hidden: element.hidden };
221
+ element.hidden = false;
222
+ return saved;
223
+ },
224
+ );
225
+
226
+ const canonicalBaseUrl = printController.dataset.canonicalBaseUrl;
227
+ const siteBaseUrl = printController.dataset.siteBaseUrl ?? '/';
228
+ const links =
229
+ mode === 'print'
230
+ ? Array.from(
231
+ document.querySelectorAll<HTMLAnchorElement>('main a[href]'),
232
+ ).flatMap((element): SavedLink[] => {
233
+ const printHref = canonicalPrintHref(
234
+ element,
235
+ canonicalBaseUrl,
236
+ siteBaseUrl,
237
+ );
238
+ if (!printHref) return [];
239
+ const saved = {
240
+ element,
241
+ href: element.getAttribute('href'),
242
+ };
243
+ // PDF viewers retain anchors as clickable annotations. Point the
244
+ // anchor itself at the deployed lesson while printing; no visible
245
+ // URL suffix is needed in the document text.
246
+ element.setAttribute('href', printHref);
247
+ return [saved];
248
+ })
249
+ : [];
250
+
251
+ prepared = { mode, details, hidden, links };
252
+ if (mode === 'print') root.dataset.explicoPrintReady = '';
253
+ }
254
+
255
+ function restoreStaticExport(): void {
256
+ const saved = prepared;
257
+ if (!saved) return;
258
+ prepared = null;
259
+
260
+ for (const state of saved.details) {
261
+ state.element.open = state.open;
262
+ if (state.state === undefined) delete state.element.dataset.state;
263
+ else state.element.dataset.state = state.state;
264
+ if (state.style === null) state.element.removeAttribute('style');
265
+ else state.element.setAttribute('style', state.style);
266
+ if (state.body) {
267
+ if (state.bodyStyle === null) state.body.removeAttribute('style');
268
+ else state.body.setAttribute('style', state.bodyStyle);
269
+ }
270
+ }
271
+ for (const state of saved.hidden) state.element.hidden = state.hidden;
272
+ for (const state of saved.links) {
273
+ if (state.href === null) state.element.removeAttribute('href');
274
+ else state.element.setAttribute('href', state.href);
275
+ }
276
+ delete root.dataset.explicoPrintReady;
277
+
278
+ // Native `toggle` is queued after changing `.open`. Keep the temporary
279
+ // marker through that task so disclosure persistence ignores both the
280
+ // prepare and restore mutations.
281
+ const cleanupMarkers = () => {
282
+ if (pendingMarkerCleanup !== cleanupMarkers) return;
283
+ for (const state of saved.details) {
284
+ if (state.temporaryMarker === null) {
285
+ state.element.removeAttribute('data-explico-temporary-open');
286
+ } else {
287
+ state.element.setAttribute(
288
+ 'data-explico-temporary-open',
289
+ state.temporaryMarker,
290
+ );
291
+ }
292
+ }
293
+ delete root.dataset.explicoStaticExport;
294
+ pendingMarkerCleanup = null;
295
+ };
296
+ pendingMarkerCleanup = cleanupMarkers;
297
+ window.setTimeout(cleanupMarkers, 0);
298
+ }
299
+
300
+ function afterNextPaint(): Promise<void> {
301
+ return new Promise((resolve) => {
302
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
303
+ });
304
+ }
305
+
306
+ function waitForIslandHydration(island: HTMLElement): Promise<void> {
307
+ if (!island.hasAttribute('ssr')) return Promise.resolve();
308
+
309
+ return new Promise((resolve) => {
310
+ let timeout = 0;
311
+ const finish = () => {
312
+ window.clearTimeout(timeout);
313
+ island.removeEventListener('astro:hydrate', finish);
314
+ island.removeEventListener('astro:hydration-error', finish);
315
+ resolve();
316
+ };
317
+ island.addEventListener('astro:hydrate', finish, { once: true });
318
+ island.addEventListener('astro:hydration-error', finish, { once: true });
319
+ timeout = window.setTimeout(finish, 10_000);
320
+ requestAnimationFrame(() => {
321
+ if (!island.hasAttribute('ssr')) finish();
322
+ });
323
+ });
324
+ }
325
+
326
+ async function hydrateVisibleIslandsForPrint(): Promise<void> {
327
+ const islands = Array.from(
328
+ document.querySelectorAll<HTMLElement>('main astro-island[ssr][client="visible"]'),
329
+ );
330
+ if (islands.length === 0) return;
331
+
332
+ const scrollX = window.scrollX;
333
+ const scrollY = window.scrollY;
334
+ const scrollBehavior = root.style.scrollBehavior;
335
+ root.style.scrollBehavior = 'auto';
336
+ try {
337
+ // `client:visible` deliberately does not hydrate an off-screen plot.
338
+ // Visit each island briefly so its normal Astro directive runs before
339
+ // the browser snapshots the document for printing.
340
+ for (const island of islands) {
341
+ if (!island.isConnected || !island.hasAttribute('ssr')) continue;
342
+ const hydrated = waitForIslandHydration(island);
343
+ // Astro's `client:visible` directive observes an island's children,
344
+ // while the custom-element wrapper itself may have no layout box.
345
+ (island.firstElementChild ?? island).scrollIntoView({
346
+ block: 'center',
347
+ inline: 'nearest',
348
+ });
349
+ await hydrated;
350
+ }
351
+ } finally {
352
+ window.scrollTo({ left: scrollX, top: scrollY, behavior: 'auto' });
353
+ root.style.scrollBehavior = scrollBehavior;
354
+ }
355
+ }
356
+
357
+ async function waitForPrintVisuals(): Promise<void> {
358
+ await hydrateVisibleIslandsForPrint();
359
+ await document.fonts?.ready;
360
+ await Promise.all(
361
+ Array.from(document.querySelectorAll<HTMLImageElement>('main img'), (image) => {
362
+ if (!image.complete) {
363
+ return new Promise<void>((resolve) => {
364
+ image.addEventListener('load', () => resolve(), { once: true });
365
+ image.addEventListener('error', () => resolve(), { once: true });
366
+ });
367
+ }
368
+ return typeof image.decode === 'function'
369
+ ? image.decode().catch(() => undefined)
370
+ : Promise.resolve();
371
+ }),
372
+ );
373
+ // React chart effects and SVG layout settle after island hydration.
374
+ await afterNextPaint();
375
+ }
376
+
377
+ function wirePrintActions(): void {
378
+ document
379
+ .querySelectorAll<HTMLButtonElement>(
380
+ '[data-explico-print-action]:not([data-explico-print-wired])',
381
+ )
382
+ .forEach((button) => {
383
+ button.dataset.explicoPrintWired = '';
384
+ button.addEventListener('click', async () => {
385
+ button.disabled = true;
386
+ button.setAttribute('aria-busy', 'true');
387
+ window.dispatchEvent(
388
+ new CustomEvent('explico:prepare-static-export', {
389
+ detail: { mode: 'print' },
390
+ }),
391
+ );
392
+ try {
393
+ await waitForPrintVisuals();
394
+ window.print();
395
+ } finally {
396
+ button.disabled = false;
397
+ button.removeAttribute('aria-busy');
398
+ }
399
+ });
400
+ });
401
+ }
402
+
403
+ wirePrintActions();
404
+ document.addEventListener('astro:page-load', wirePrintActions);
405
+
406
+ if (!root.hasAttribute('data-explico-print-controller-wired')) {
407
+ root.dataset.explicoPrintControllerWired = '';
408
+ const printMedia = window.matchMedia('print');
409
+ window.addEventListener('explico:prepare-static-export', (event) => {
410
+ const mode = (event as CustomEvent<{ mode?: unknown }>).detail?.mode;
411
+ prepareStaticExport(mode === 'markdown' ? 'markdown' : 'print');
412
+ });
413
+ window.addEventListener('explico:restore-static-export', restoreStaticExport);
414
+ const requestPrintPreparation = () => {
415
+ window.dispatchEvent(
416
+ new CustomEvent('explico:prepare-static-export', {
417
+ detail: { mode: 'print' },
418
+ }),
419
+ );
420
+ };
421
+ const requestPrintRestore = () => {
422
+ window.dispatchEvent(
423
+ new CustomEvent('explico:restore-static-export', {
424
+ detail: { mode: 'print' },
425
+ }),
426
+ );
427
+ };
428
+ window.addEventListener('beforeprint', requestPrintPreparation);
429
+ window.addEventListener('afterprint', requestPrintRestore);
430
+ printMedia.addEventListener('change', (event) => {
431
+ if (event.matches) requestPrintPreparation();
432
+ else requestPrintRestore();
433
+ });
434
+ if (printMedia.matches) requestPrintPreparation();
435
+ }
436
+ </script>
@@ -19,9 +19,17 @@ import NotationLayer, {
19
19
  type NotationLayerDefinition,
20
20
  } from '../notation/NotationLayer.astro';
21
21
  import CitationLayer from '../citation/CitationLayer.astro';
22
+ import AbbreviationLayer from '../abbreviation/AbbreviationLayer.astro';
22
23
  import KeyedFloatEnhancer from './KeyedFloatEnhancer.astro';
23
24
  import FloatRefLayer from '../reference/FloatRefLayer.astro';
24
25
  import HeadingAnchorEnhancer from './HeadingAnchorEnhancer.astro';
26
+ import CollapsibleSections from './CollapsibleSections.astro';
27
+ import TableOverflowExpander from '../table/TableOverflowExpander.astro';
28
+ import DiagramEnhancer from './DiagramEnhancer.astro';
29
+ import {
30
+ notationDefinitionsForRenderedPage,
31
+ pageNotationDependencies,
32
+ } from '../../reference/page-notation';
25
33
 
26
34
  // `lessonId` is no longer authored (Phase C2); derive it from the route.
27
35
  const routeSlug = Astro.locals.starlightRoute.entry.id;
@@ -32,8 +40,10 @@ const lessonId = isLessonSlug(routeSlug)
32
40
  const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
33
41
 
34
42
  const definitions: NotationLayerDefinition[] = [];
43
+ let notationDefaultOpen = false;
35
44
  if (lessonId) {
36
45
  const manifest = await loadManifest();
46
+ notationDefaultOpen = manifest.disclosureDefaults.notation;
37
47
  const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
38
48
  const routeData = Astro.locals.starlightRoute.entry.data as {
39
49
  notation?: { local?: readonly { key?: unknown }[] };
@@ -45,7 +55,12 @@ if (lessonId) {
45
55
  .filter((definition) => definition.scope === 'local')
46
56
  .map((definition) => definition.key);
47
57
  assertLocalNotationParity(lessonId, authoredLocalKeys, renderedLocalKeys);
48
- for (const definition of lesson?.notation.definitions ?? []) {
58
+ const renderedDefinitions = notationDefinitionsForRenderedPage(
59
+ manifest,
60
+ lessonId,
61
+ pageNotationDependencies(Astro.locals),
62
+ );
63
+ for (const definition of renderedDefinitions) {
49
64
  definitions.push({
50
65
  key: definition.key,
51
66
  notation: definition.notation,
@@ -67,9 +82,21 @@ if (lessonId) {
67
82
  }
68
83
  ---
69
84
 
70
- <NotationLayer definitions={definitions} />
85
+ <NotationLayer definitions={definitions} defaultOpen={notationDefaultOpen} />
71
86
  {lessonId && <CitationLayer />}
87
+ {lessonId && <AbbreviationLayer />}
72
88
  {lessonId && <KeyedFloatEnhancer />}
73
89
  {lessonId && <FloatRefLayer />}
90
+ {lessonId && <CollapsibleSections />}
91
+ {lessonId && <TableOverflowExpander />}
92
+ <DiagramEnhancer />
74
93
  {lessonId && <HeadingAnchorEnhancer />}
75
- <DefaultFooter />
94
+ <div class="explico-site-footer" data-explico-export-chrome="site-footer">
95
+ <DefaultFooter />
96
+ </div>
97
+
98
+ <style>
99
+ .explico-site-footer {
100
+ display: contents;
101
+ }
102
+ </style>
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared boundary-finding for a heading's "section": its `.sl-heading-wrapper`
3
+ * (see `rehype-heading-anchors.mjs`) plus every following sibling up to (not
4
+ * including) the next heading of equal or higher rank.
5
+ *
6
+ * Extracted so `HeadingAnchorEnhancer.astro` (deep-link flash) and
7
+ * `CollapsibleSections.astro` (Part 3 of
8
+ * `docs/plans/collapsible-disclosure.md`) share one algorithm instead of two
9
+ * copies drifting apart. Works unchanged when a section's body already
10
+ * contains a nested, already-wrapped `<details class="sl-section-collapsible">`
11
+ * for a deeper heading: `querySelector` finds the nested heading regardless of
12
+ * depth, so `CollapsibleSections.astro` can process headings deepest-rank
13
+ * first and have each parent's walk see its already-wrapped children as
14
+ * ordinary siblings.
15
+ */
16
+
17
+ export const HEADINGS = 'h1, h2, h3, h4, h5, h6';
18
+
19
+ export function sectionParts(heading: HTMLElement): Element[] {
20
+ const wrapper = heading.closest('.sl-heading-wrapper') ?? heading;
21
+ const rank = Number(heading.tagName[1]);
22
+ const parts: Element[] = [wrapper];
23
+ let node = wrapper.nextElementSibling;
24
+ while (node) {
25
+ const inner = node.matches(HEADINGS) ? node : node.querySelector(HEADINGS);
26
+ if (inner && Number(inner.tagName[1]) <= rank) break;
27
+ parts.push(node);
28
+ node = node.nextElementSibling;
29
+ }
30
+ return parts;
31
+ }