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.
- package/CHANGELOG.md +382 -1
- package/README.md +103 -8
- package/dist/compiler/astro.d.ts +9 -1
- package/dist/compiler/astro.js +182 -3
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +28 -4
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +88 -1
- package/dist/compiler/course-config.js +35 -1
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/manifest.d.ts +98 -6
- package/dist/compiler/manifest.js +154 -17
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations.d.mts +10 -15
- package/dist/reference/equations.mjs +67 -14
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-heading-length.d.ts +12 -0
- package/dist/reference/gate-heading-length.js +79 -0
- package/dist/reference/gate-math.d.ts +32 -5
- package/dist/reference/gate-math.js +6 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +5 -5
- package/dist/reference/keyed-jsx-floats.d.mts +7 -12
- package/dist/reference/keyed-jsx-floats.mjs +50 -25
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +42 -0
- package/dist/reference/math-glyphs.mjs +548 -40
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +57 -9
- package/dist/reference/remark-notation.mjs +354 -42
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +53 -4
- package/docs/authoring/README.md +10 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +138 -0
- package/docs/authoring/migrating.md +275 -7
- package/docs/authoring/notation.md +615 -1
- package/docs/authoring/structure-rules.md +69 -0
- package/docs/authoring/structure.md +120 -8
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +208 -3
- package/src/compiler/cli.ts +29 -2
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +132 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +281 -22
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +409 -103
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +260 -83
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +30 -3
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +293 -7
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +36 -1
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations.mjs +67 -14
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +32 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +5 -5
- package/src/reference/keyed-jsx-floats.mjs +50 -25
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +548 -40
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +354 -42
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +996 -50
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A collapsible side-note authored inline and moved between the surrounding
|
|
4
|
+
* paragraphs by `remark-memo.mjs`. The remark adapter owns `number` and
|
|
5
|
+
* `anchorId`; this component owns the block, persistence, hover preview, and
|
|
6
|
+
* click/deep-link behavior.
|
|
7
|
+
*
|
|
8
|
+
* The numbered permalink and chain-icon permalink are siblings of `<details>`,
|
|
9
|
+
* positioned into its title row. They cannot be children of `<summary>`:
|
|
10
|
+
* nesting a focusable link inside that disclosure control is invalid and trips
|
|
11
|
+
* axe's `nested-interactive` rule.
|
|
12
|
+
*/
|
|
13
|
+
export interface Props {
|
|
14
|
+
/** Stable lowercase/hyphenated key used by the marker and persistence. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Short summary shown on the disclosure itself. */
|
|
17
|
+
title: string;
|
|
18
|
+
/** Required context shown before the body in the marker's hover preview. */
|
|
19
|
+
description: string;
|
|
20
|
+
/** Optional prose label for the marker; omission gives the numbered `[n]` form. */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Persist the reader's open/closed choice across visits, per page. Default: `true`. */
|
|
23
|
+
rememberState?: boolean;
|
|
24
|
+
/** Computed by `remark-memo.mjs` — never author this directly. */
|
|
25
|
+
number?: string;
|
|
26
|
+
/** Computed by `remark-memo.mjs` — never author this directly. */
|
|
27
|
+
anchorId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
id,
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
rememberState = true,
|
|
35
|
+
number,
|
|
36
|
+
anchorId,
|
|
37
|
+
} = Astro.props;
|
|
38
|
+
|
|
39
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
40
|
+
throw new Error('Memo requires a non-empty `id` prop.');
|
|
41
|
+
}
|
|
42
|
+
if (typeof title !== 'string' || !title.trim()) {
|
|
43
|
+
throw new Error('Memo requires a non-empty `title` prop.');
|
|
44
|
+
}
|
|
45
|
+
if (typeof description !== 'string' || !description.trim()) {
|
|
46
|
+
throw new Error('Memo requires a non-empty `description` prop.');
|
|
47
|
+
}
|
|
48
|
+
if (!number || !anchorId) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Memo is missing its computed `number`/`anchorId` — register remark-memo after remark-mdx in the Markdown remark pipeline.',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<div class="memo-block-frame">
|
|
56
|
+
<a
|
|
57
|
+
class="memo-block__anchor"
|
|
58
|
+
href={`#${anchorId}`}
|
|
59
|
+
aria-label={`Link to memo ${number}: ${title}`}
|
|
60
|
+
data-memo-self-link
|
|
61
|
+
>[{number}]</a>
|
|
62
|
+
<details
|
|
63
|
+
class="memo-block"
|
|
64
|
+
id={anchorId}
|
|
65
|
+
data-memo-key={id}
|
|
66
|
+
data-memo-number={number}
|
|
67
|
+
data-memo-description={description}
|
|
68
|
+
data-remember-state={String(rememberState)}
|
|
69
|
+
>
|
|
70
|
+
<summary class="memo-block__summary">{title}</summary>
|
|
71
|
+
<div class="memo-block__body">
|
|
72
|
+
<slot />
|
|
73
|
+
</div>
|
|
74
|
+
</details>
|
|
75
|
+
<a
|
|
76
|
+
class="memo-block__title-anchor"
|
|
77
|
+
href={`#${anchorId}`}
|
|
78
|
+
aria-label={`Link to memo ${number} titled “${title}”`}
|
|
79
|
+
data-memo-anchor-link
|
|
80
|
+
>
|
|
81
|
+
<svg
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
width="16"
|
|
84
|
+
height="16"
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
fill="currentColor"
|
|
87
|
+
><path
|
|
88
|
+
d="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"
|
|
89
|
+
></path></svg
|
|
90
|
+
>
|
|
91
|
+
</a>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
.memo-block-frame {
|
|
96
|
+
position: relative;
|
|
97
|
+
margin-block: 1rem;
|
|
98
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
99
|
+
border-inline-start: 4px solid var(--sl-color-accent);
|
|
100
|
+
border-radius: 0.35rem;
|
|
101
|
+
background: color-mix(in srgb, var(--sl-color-bg) 95%, var(--sl-color-accent) 5%);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.memo-block {
|
|
105
|
+
margin: 0;
|
|
106
|
+
padding: 0;
|
|
107
|
+
scroll-margin-block-start: 5rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.memo-block__summary {
|
|
111
|
+
padding: 0.7rem 2.75rem 0.7rem 3.25rem;
|
|
112
|
+
color: var(--sl-color-white);
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.memo-block__anchor {
|
|
117
|
+
position: absolute;
|
|
118
|
+
z-index: 1;
|
|
119
|
+
inset-block-start: 0.7rem;
|
|
120
|
+
inset-inline-start: 0.9rem;
|
|
121
|
+
color: var(--sl-color-text-accent);
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
text-decoration: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.memo-block__anchor:hover,
|
|
127
|
+
.memo-block__anchor:focus-visible {
|
|
128
|
+
text-decoration: underline;
|
|
129
|
+
text-underline-offset: 0.15em;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.memo-block__title-anchor {
|
|
133
|
+
position: absolute;
|
|
134
|
+
z-index: 1;
|
|
135
|
+
inset-block-start: 0.55rem;
|
|
136
|
+
inset-inline-end: 0.65rem;
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
width: 1.75rem;
|
|
141
|
+
height: 1.75rem;
|
|
142
|
+
color: var(--sl-color-gray-3);
|
|
143
|
+
text-decoration: none;
|
|
144
|
+
-webkit-user-select: none;
|
|
145
|
+
user-select: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.memo-block__title-anchor svg {
|
|
149
|
+
width: 1rem;
|
|
150
|
+
height: 1rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.memo-block__title-anchor:hover,
|
|
154
|
+
.memo-block__title-anchor:focus-visible {
|
|
155
|
+
color: var(--sl-color-text-accent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.memo-block__title-anchor:focus-visible {
|
|
159
|
+
outline: 2px solid var(--sl-color-accent);
|
|
160
|
+
outline-offset: 0.1rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@media (hover: hover) {
|
|
164
|
+
.memo-block__title-anchor {
|
|
165
|
+
opacity: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.memo-block__title-anchor:focus-visible,
|
|
169
|
+
.memo-block-frame:hover .memo-block__title-anchor {
|
|
170
|
+
opacity: 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.memo-block__body {
|
|
175
|
+
padding: 0 0.9rem 0.9rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.memo-block__body > :first-child {
|
|
179
|
+
margin-block-start: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
:global(.sl-markdown-content .memo-marker) {
|
|
183
|
+
border-radius: 0.2em;
|
|
184
|
+
color: var(--sl-color-text-accent);
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
text-decoration: underline;
|
|
187
|
+
text-underline-offset: 0.15em;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
:global(.sl-markdown-content .memo-marker:hover),
|
|
191
|
+
:global(.sl-markdown-content .memo-marker:focus-visible) {
|
|
192
|
+
background: var(--sl-color-accent-low);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:global(.sl-markdown-content .memo-marker:focus-visible) {
|
|
196
|
+
outline: 2px solid var(--sl-color-accent);
|
|
197
|
+
outline-offset: 0.15rem;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
:global(.sl-markdown-content .memo-marker-wrap) {
|
|
201
|
+
line-height: 0;
|
|
202
|
+
vertical-align: super;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
:global(.memo-preview__description) {
|
|
206
|
+
margin: 0 0 0.65rem;
|
|
207
|
+
color: var(--sl-color-gray-2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
:global(.memo-preview__body) {
|
|
211
|
+
max-height: 20em;
|
|
212
|
+
overflow: hidden;
|
|
213
|
+
border-block-start: 1px solid var(--sl-color-gray-5);
|
|
214
|
+
padding-block-start: 0.65rem;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@media print {
|
|
218
|
+
.memo-block-frame {
|
|
219
|
+
border-color: currentColor;
|
|
220
|
+
background: transparent;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.memo-block__summary::before {
|
|
224
|
+
display: none !important;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.memo-block__title-anchor {
|
|
228
|
+
display: none;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* The script opens the native disclosure while print media is active.
|
|
232
|
+
Keep the body explicit in case author CSS would otherwise hide it. */
|
|
233
|
+
.memo-block__body {
|
|
234
|
+
display: block !important;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
238
|
+
|
|
239
|
+
<script>
|
|
240
|
+
import { attachHoverPeek, closeHoverPeek } from '../disclosure/hover-preview';
|
|
241
|
+
import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
|
|
242
|
+
|
|
243
|
+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
244
|
+
let memoPreviewSerial = 0;
|
|
245
|
+
|
|
246
|
+
function cloneMemoBody(body: HTMLElement): HTMLElement {
|
|
247
|
+
const clone = document.importNode(body, true);
|
|
248
|
+
clone.classList.add('memo-preview__body');
|
|
249
|
+
|
|
250
|
+
// The shared panel temporarily joins the live document. Isolate every
|
|
251
|
+
// cloned id and rewrite both SVG fragment URLs and HTML/ARIA IDREFs. Merely
|
|
252
|
+
// deleting ids would break arrow markers, masks, labels, and controls in a
|
|
253
|
+
// nested Diagram/Figure preview.
|
|
254
|
+
const prefix = `memo-preview-${++memoPreviewSerial}-`;
|
|
255
|
+
const ids = new Map<string, string>();
|
|
256
|
+
for (const element of clone.querySelectorAll<Element>('[id]')) {
|
|
257
|
+
const oldId = element.getAttribute('id');
|
|
258
|
+
if (!oldId) continue;
|
|
259
|
+
const nextId = `${prefix}${oldId}`;
|
|
260
|
+
ids.set(oldId, nextId);
|
|
261
|
+
element.setAttribute('id', nextId);
|
|
262
|
+
}
|
|
263
|
+
const fragmentUrlAttributes = [
|
|
264
|
+
'marker-start',
|
|
265
|
+
'marker-mid',
|
|
266
|
+
'marker-end',
|
|
267
|
+
'clip-path',
|
|
268
|
+
'mask',
|
|
269
|
+
'filter',
|
|
270
|
+
'fill',
|
|
271
|
+
'stroke',
|
|
272
|
+
'style',
|
|
273
|
+
];
|
|
274
|
+
const idrefAttributes = [
|
|
275
|
+
'aria-activedescendant',
|
|
276
|
+
'aria-controls',
|
|
277
|
+
'aria-describedby',
|
|
278
|
+
'aria-details',
|
|
279
|
+
'aria-errormessage',
|
|
280
|
+
'aria-flowto',
|
|
281
|
+
'aria-labelledby',
|
|
282
|
+
'aria-owns',
|
|
283
|
+
'for',
|
|
284
|
+
'form',
|
|
285
|
+
'headers',
|
|
286
|
+
'list',
|
|
287
|
+
];
|
|
288
|
+
for (const element of clone.querySelectorAll<Element>('*')) {
|
|
289
|
+
for (const name of fragmentUrlAttributes) {
|
|
290
|
+
const value = element.getAttribute(name);
|
|
291
|
+
if (!value) continue;
|
|
292
|
+
const rewritten = value.replace(
|
|
293
|
+
/url\(\s*(['"]?)#([^)'"\s]+)\1\s*\)/g,
|
|
294
|
+
(match, quote: string, oldId: string) => {
|
|
295
|
+
const nextId = ids.get(oldId);
|
|
296
|
+
return nextId ? `url(${quote}#${nextId}${quote})` : match;
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
if (rewritten !== value) element.setAttribute(name, rewritten);
|
|
300
|
+
}
|
|
301
|
+
for (const name of ['href', 'xlink:href']) {
|
|
302
|
+
const value = element.getAttribute(name);
|
|
303
|
+
if (!value?.startsWith('#')) continue;
|
|
304
|
+
const nextId = ids.get(value.slice(1));
|
|
305
|
+
if (nextId) element.setAttribute(name, `#${nextId}`);
|
|
306
|
+
}
|
|
307
|
+
for (const name of idrefAttributes) {
|
|
308
|
+
const value = element.getAttribute(name);
|
|
309
|
+
if (!value) continue;
|
|
310
|
+
const rewritten = value
|
|
311
|
+
.split(/\s+/)
|
|
312
|
+
.map((id) => ids.get(id) ?? id)
|
|
313
|
+
.join(' ');
|
|
314
|
+
if (rewritten !== value) element.setAttribute(name, rewritten);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return clone;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function targetInside(details: HTMLDetailsElement): boolean {
|
|
321
|
+
if (!location.hash) return false;
|
|
322
|
+
let id = location.hash.slice(1);
|
|
323
|
+
try {
|
|
324
|
+
id = decodeURIComponent(id);
|
|
325
|
+
} catch {
|
|
326
|
+
/* keep the raw fragment */
|
|
327
|
+
}
|
|
328
|
+
const target = document.getElementById(id);
|
|
329
|
+
return target === details || (!!target && details.contains(target));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
document
|
|
333
|
+
.querySelectorAll<HTMLDetailsElement>('.memo-block:not([data-memo-wired])')
|
|
334
|
+
.forEach((details) => {
|
|
335
|
+
details.dataset.memoWired = 'true';
|
|
336
|
+
const key = details.dataset.memoKey;
|
|
337
|
+
const number = details.dataset.memoNumber;
|
|
338
|
+
const body = details.querySelector<HTMLElement>(':scope > .memo-block__body');
|
|
339
|
+
const summary = details.querySelector<HTMLElement>(
|
|
340
|
+
':scope > .memo-block__summary',
|
|
341
|
+
);
|
|
342
|
+
const selfLinks = Array.from(
|
|
343
|
+
details.parentElement?.querySelectorAll<HTMLAnchorElement>(
|
|
344
|
+
':scope > [data-memo-self-link], :scope > [data-memo-anchor-link]',
|
|
345
|
+
) ?? [],
|
|
346
|
+
);
|
|
347
|
+
if (!key || !number || !body || !summary || selfLinks.length === 0) return;
|
|
348
|
+
|
|
349
|
+
if (details.dataset.rememberState === 'true') {
|
|
350
|
+
wirePersistedDetails(details, `memo:${key}`, false);
|
|
351
|
+
} else {
|
|
352
|
+
details.open = false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const markers = Array.from(
|
|
356
|
+
document.querySelectorAll<HTMLAnchorElement>(
|
|
357
|
+
`.memo-marker[data-memo-key="${key}"]`,
|
|
358
|
+
),
|
|
359
|
+
).filter(
|
|
360
|
+
(marker) => marker.getAttribute('aria-controls') === details.id,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const syncMarkers = () => {
|
|
364
|
+
for (const marker of markers) {
|
|
365
|
+
marker.setAttribute('aria-expanded', String(details.open));
|
|
366
|
+
if (details.open) closeHoverPeek(marker);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
details.addEventListener('toggle', syncMarkers);
|
|
370
|
+
syncMarkers();
|
|
371
|
+
|
|
372
|
+
const revealForHash = () => {
|
|
373
|
+
if (targetInside(details)) details.open = true;
|
|
374
|
+
};
|
|
375
|
+
revealForHash();
|
|
376
|
+
window.addEventListener('hashchange', revealForHash);
|
|
377
|
+
|
|
378
|
+
for (const selfLink of selfLinks) {
|
|
379
|
+
selfLink.addEventListener('click', (event) => {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
event.stopPropagation();
|
|
382
|
+
details.open = true;
|
|
383
|
+
if (location.hash !== `#${details.id}`) {
|
|
384
|
+
history.pushState(null, '', `#${details.id}`);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// A closed native <details> suppresses its body at the UA rendering
|
|
390
|
+
// layer, beyond what an author stylesheet can reliably override. Open it
|
|
391
|
+
// while print media is active, then put the reader's exact screen state
|
|
392
|
+
// back when the print dialog/export ends (the same approach used by
|
|
393
|
+
// CollapsibleSections for its native disclosures).
|
|
394
|
+
const printMedia = window.matchMedia('print');
|
|
395
|
+
let screenOpen: boolean | null = null;
|
|
396
|
+
const syncPrintState = () => {
|
|
397
|
+
if (document.querySelector('[data-explico-print-controller]')) return;
|
|
398
|
+
if (printMedia.matches) {
|
|
399
|
+
if (screenOpen === null) screenOpen = details.open;
|
|
400
|
+
details.open = true;
|
|
401
|
+
} else if (screenOpen !== null) {
|
|
402
|
+
details.open = screenOpen;
|
|
403
|
+
screenOpen = null;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
printMedia.addEventListener('change', syncPrintState);
|
|
407
|
+
window.addEventListener('beforeprint', syncPrintState);
|
|
408
|
+
window.addEventListener('afterprint', syncPrintState);
|
|
409
|
+
syncPrintState();
|
|
410
|
+
|
|
411
|
+
for (const marker of markers) {
|
|
412
|
+
const suppressWhenOpen = (event: Event) => {
|
|
413
|
+
if (details.open) event.stopImmediatePropagation();
|
|
414
|
+
};
|
|
415
|
+
marker.addEventListener('pointerenter', suppressWhenOpen);
|
|
416
|
+
marker.addEventListener('focus', suppressWhenOpen);
|
|
417
|
+
|
|
418
|
+
attachHoverPeek(marker, {
|
|
419
|
+
label: `Memo ${number} preview`,
|
|
420
|
+
// Keep the description visible above up to 20em of body content.
|
|
421
|
+
maxHeightEm: 24,
|
|
422
|
+
getContent: () => {
|
|
423
|
+
const wrap = document.createElement('div');
|
|
424
|
+
const heading = document.createElement('strong');
|
|
425
|
+
heading.textContent = summary.textContent?.trim() || `Memo ${number}`;
|
|
426
|
+
const description = document.createElement('p');
|
|
427
|
+
description.className = 'memo-preview__description';
|
|
428
|
+
description.textContent = details.dataset.memoDescription ?? '';
|
|
429
|
+
wrap.append(heading, description, cloneMemoBody(body));
|
|
430
|
+
return wrap;
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
marker.addEventListener('click', (event) => {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
const opening = !details.open;
|
|
437
|
+
details.open = opening;
|
|
438
|
+
if (!opening) return;
|
|
439
|
+
|
|
440
|
+
closeHoverPeek(marker);
|
|
441
|
+
if (location.hash !== `#${details.id}`) {
|
|
442
|
+
history.pushState(null, '', `#${details.id}`);
|
|
443
|
+
}
|
|
444
|
+
requestAnimationFrame(() => {
|
|
445
|
+
details.scrollIntoView({
|
|
446
|
+
behavior: reduceMotion.matches ? 'auto' : 'smooth',
|
|
447
|
+
block: 'start',
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
</script>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
/** A reveal-in-place note whose collapsed trigger remains part of its sentence. */
|
|
3
|
+
export interface Props {
|
|
4
|
+
/** Stable lowercase/hyphenated key used by the trigger and URL fragment. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Exact inline text shown while the note is collapsed. */
|
|
7
|
+
title: string;
|
|
8
|
+
/** Accessible explanation of what expanding the note will reveal. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Computed by `remark-short-note.mjs` — never author this directly. */
|
|
11
|
+
anchorId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { id, title, description, anchorId } = Astro.props;
|
|
15
|
+
|
|
16
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
17
|
+
throw new Error('ShortNote requires a non-empty `id` prop.');
|
|
18
|
+
}
|
|
19
|
+
if (typeof title !== 'string' || !title.trim()) {
|
|
20
|
+
throw new Error('ShortNote requires a non-empty `title` prop.');
|
|
21
|
+
}
|
|
22
|
+
if (typeof description !== 'string' || !description.trim()) {
|
|
23
|
+
throw new Error('ShortNote requires a non-empty `description` prop.');
|
|
24
|
+
}
|
|
25
|
+
if (!anchorId) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'ShortNote is missing its computed `anchorId` — register remark-short-note before remark-memo in the Markdown remark pipeline.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<aside
|
|
33
|
+
class="short-note"
|
|
34
|
+
id={anchorId}
|
|
35
|
+
data-short-note-key={id}
|
|
36
|
+
aria-label={description}
|
|
37
|
+
hidden
|
|
38
|
+
>
|
|
39
|
+
<div class="short-note__body">
|
|
40
|
+
<slot />
|
|
41
|
+
</div>
|
|
42
|
+
<button class="short-note__collapse" type="button">Show less</button>
|
|
43
|
+
</aside>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.short-note {
|
|
47
|
+
margin-block: 0.65rem;
|
|
48
|
+
padding-inline-start: 0.9rem;
|
|
49
|
+
scroll-margin-block-start: 5rem;
|
|
50
|
+
border-inline-start: 2px solid var(--sl-color-accent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.short-note[hidden] {
|
|
54
|
+
display: none !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* A fragment link still reveals the note when client JavaScript is absent. */
|
|
58
|
+
.short-note:target {
|
|
59
|
+
display: block !important;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.short-note__body > :first-child {
|
|
63
|
+
margin-block-start: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.short-note__body > :last-child {
|
|
67
|
+
margin-block-end: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.short-note__collapse {
|
|
71
|
+
margin-block-start: 0.45rem;
|
|
72
|
+
padding: 0;
|
|
73
|
+
border: 0;
|
|
74
|
+
background: transparent;
|
|
75
|
+
color: var(--sl-color-text-accent);
|
|
76
|
+
font: inherit;
|
|
77
|
+
font-size: var(--sl-text-sm);
|
|
78
|
+
text-decoration: underline;
|
|
79
|
+
text-underline-offset: 0.15em;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
:global(.sl-markdown-content .short-note-marker) {
|
|
84
|
+
border-radius: 0.2em;
|
|
85
|
+
color: var(--sl-color-text-accent);
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
text-decoration-line: underline;
|
|
88
|
+
text-decoration-style: dashed;
|
|
89
|
+
text-underline-offset: 0.18em;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:global(.sl-markdown-content .short-note-marker:hover),
|
|
93
|
+
:global(.sl-markdown-content .short-note-marker:focus-visible) {
|
|
94
|
+
background: var(--sl-color-accent-low);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:global(.sl-markdown-content .short-note-marker:focus-visible),
|
|
98
|
+
.short-note__collapse:focus-visible {
|
|
99
|
+
outline: 2px solid var(--sl-color-accent);
|
|
100
|
+
outline-offset: 0.15rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:global(.short-note__continuation) {
|
|
104
|
+
margin-block-start: 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@media print {
|
|
108
|
+
.short-note[hidden] {
|
|
109
|
+
display: block !important;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.short-note__collapse {
|
|
113
|
+
display: none;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
120
|
+
|
|
121
|
+
document
|
|
122
|
+
.querySelectorAll<HTMLElement>('.short-note:not([data-short-note-wired])')
|
|
123
|
+
.forEach((note) => {
|
|
124
|
+
note.dataset.shortNoteWired = 'true';
|
|
125
|
+
const key = note.dataset.shortNoteKey;
|
|
126
|
+
const collapseButton = note.querySelector<HTMLButtonElement>(
|
|
127
|
+
':scope > .short-note__collapse',
|
|
128
|
+
);
|
|
129
|
+
if (!key || !collapseButton) return;
|
|
130
|
+
|
|
131
|
+
const markers = Array.from(
|
|
132
|
+
document.querySelectorAll<HTMLAnchorElement>(
|
|
133
|
+
`.short-note-marker[data-short-note-key="${key}"]`,
|
|
134
|
+
),
|
|
135
|
+
).filter((marker) => marker.getAttribute('aria-controls') === note.id);
|
|
136
|
+
if (markers.length === 0) return;
|
|
137
|
+
|
|
138
|
+
const storage = document.createComment(`short-note:${key}`);
|
|
139
|
+
note.before(storage);
|
|
140
|
+
let activeMarker: HTMLAnchorElement | null = null;
|
|
141
|
+
let paragraph: HTMLParagraphElement | null = null;
|
|
142
|
+
let continuation: HTMLParagraphElement | null = null;
|
|
143
|
+
|
|
144
|
+
const setExpanded = (expanded: boolean) => {
|
|
145
|
+
for (const marker of markers) {
|
|
146
|
+
marker.setAttribute('aria-expanded', String(expanded));
|
|
147
|
+
marker.hidden = expanded && marker === activeMarker;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const collapse = (restoreFocus: boolean) => {
|
|
152
|
+
if (note.hidden) return;
|
|
153
|
+
note.hidden = true;
|
|
154
|
+
if (paragraph && continuation) {
|
|
155
|
+
while (continuation.firstChild) paragraph.append(continuation.firstChild);
|
|
156
|
+
continuation.remove();
|
|
157
|
+
}
|
|
158
|
+
storage.after(note);
|
|
159
|
+
const marker = activeMarker;
|
|
160
|
+
activeMarker = null;
|
|
161
|
+
paragraph = null;
|
|
162
|
+
continuation = null;
|
|
163
|
+
setExpanded(false);
|
|
164
|
+
if (location.hash === `#${note.id}`) {
|
|
165
|
+
history.replaceState(null, '', `${location.pathname}${location.search}`);
|
|
166
|
+
}
|
|
167
|
+
if (restoreFocus) marker?.focus();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const expand = (marker: HTMLAnchorElement, updateUrl = true) => {
|
|
171
|
+
window.dispatchEvent(
|
|
172
|
+
new CustomEvent('explico:short-note-open', { detail: note }),
|
|
173
|
+
);
|
|
174
|
+
activeMarker = marker;
|
|
175
|
+
paragraph = marker.closest('p');
|
|
176
|
+
continuation = document.createElement('p');
|
|
177
|
+
continuation.className = 'short-note__continuation';
|
|
178
|
+
while (marker.nextSibling) continuation.append(marker.nextSibling);
|
|
179
|
+
|
|
180
|
+
note.hidden = false;
|
|
181
|
+
paragraph?.after(note);
|
|
182
|
+
if (continuation.hasChildNodes()) note.after(continuation);
|
|
183
|
+
setExpanded(true);
|
|
184
|
+
if (updateUrl && location.hash !== `#${note.id}`) {
|
|
185
|
+
history.pushState(null, '', `#${note.id}`);
|
|
186
|
+
}
|
|
187
|
+
requestAnimationFrame(() => {
|
|
188
|
+
note.scrollIntoView({
|
|
189
|
+
behavior: reduceMotion.matches ? 'auto' : 'smooth',
|
|
190
|
+
block: 'nearest',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
window.addEventListener('explico:short-note-open', (event) => {
|
|
196
|
+
if ((event as CustomEvent).detail !== note) collapse(false);
|
|
197
|
+
});
|
|
198
|
+
collapseButton.addEventListener('click', () => collapse(true));
|
|
199
|
+
for (const marker of markers) {
|
|
200
|
+
marker.addEventListener('click', (event) => {
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
if (!note.hidden) collapse(false);
|
|
203
|
+
expand(marker);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const revealForHash = () => {
|
|
208
|
+
if (!location.hash || !note.hidden) return;
|
|
209
|
+
let id = location.hash.slice(1);
|
|
210
|
+
try {
|
|
211
|
+
id = decodeURIComponent(id);
|
|
212
|
+
} catch {
|
|
213
|
+
/* keep the raw fragment */
|
|
214
|
+
}
|
|
215
|
+
const target = document.getElementById(id);
|
|
216
|
+
if (target === note || (!!target && note.contains(target))) {
|
|
217
|
+
expand(markers[0], false);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
window.addEventListener('hashchange', revealForHash);
|
|
221
|
+
revealForHash();
|
|
222
|
+
});
|
|
223
|
+
</script>
|