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
@@ -26,6 +26,10 @@ import { renderNotationFormula } from '../notation/render-notation-math';
26
26
  import type { SafeMathNode as SafeMathNodeType } from '../notation/render-notation-math';
27
27
  import SafeMathNode from '../notation/SafeMathNode.astro';
28
28
  import type { GlyphScopeEntry } from '../../reference/gloss';
29
+ import {
30
+ notationKeysInMathNodes,
31
+ registerPageNotationDependencies,
32
+ } from '../../reference/page-notation';
29
33
 
30
34
  export interface DiagramNode {
31
35
  readonly id: string;
@@ -53,12 +57,19 @@ export interface Props {
53
57
  /** Glyph scope `formula` resolves against — omit if no node uses `formula`. */
54
58
  scope?: readonly GlyphScopeEntry[];
55
59
  direction?: 'TB' | 'LR';
60
+ /** Computed by `remark-notation.mjs` for a top-level lesson diagram. */
61
+ number?: string;
62
+ /** Computed by `remark-notation.mjs` for a top-level lesson diagram. */
63
+ anchorId?: string;
64
+ /** Author-chosen state for a reader with nothing saved yet. Default: `true`. */
65
+ defaultOpen?: boolean;
66
+ /** Persist the reader's open/closed choice across visits. Default: `true`. */
67
+ rememberState?: boolean;
56
68
  /**
57
- * Prose text alternative for the SVG. When omitted, the fallback is
58
- * generated from the node labels and the edge chain — readable, but a
59
- * hand-written sentence usually reads better for a screen-reader user.
69
+ * Emit a build warning when the laid-out SVG is wider than this many pixels.
70
+ * Set `false` only when horizontal overflow is deliberate. Default: `800`.
60
71
  */
61
- description?: string;
72
+ warnAboveWidth?: number | false;
62
73
  }
63
74
 
64
75
  const {
@@ -68,7 +79,11 @@ const {
68
79
  edges,
69
80
  scope = [],
70
81
  direction = 'TB',
71
- description,
82
+ number,
83
+ anchorId,
84
+ defaultOpen = true,
85
+ rememberState = true,
86
+ warnAboveWidth = 800,
72
87
  } = Astro.props;
73
88
 
74
89
  const idPattern = /^[A-Za-z][A-Za-z0-9_-]*$/;
@@ -78,6 +93,17 @@ if (!idPattern.test(id)) {
78
93
  if (nodes.length === 0) {
79
94
  throw new Error(`Diagram "${id}" has no nodes.`);
80
95
  }
96
+ if ((number && !anchorId) || (!number && anchorId)) {
97
+ throw new Error(
98
+ `Diagram "${id}" must receive both computed \`number\` and \`anchorId\`, or neither.`,
99
+ );
100
+ }
101
+ if (
102
+ warnAboveWidth !== false &&
103
+ (!Number.isFinite(warnAboveWidth) || warnAboveWidth <= 0)
104
+ ) {
105
+ throw new Error(`Diagram "${id}" requires a positive \`warnAboveWidth\`.`);
106
+ }
81
107
 
82
108
  interface RenderedNode extends DiagramNode {
83
109
  readonly mathNodes?: readonly SafeMathNodeType[];
@@ -114,6 +140,15 @@ const rendered: RenderedNode[] = nodes.map((node) => {
114
140
  return { ...node, mathNodes };
115
141
  });
116
142
 
143
+ for (const node of rendered) {
144
+ if (!node.mathNodes) continue;
145
+ registerPageNotationDependencies(
146
+ Astro.locals,
147
+ notationKeysInMathNodes(node.mathNodes),
148
+ `Diagram "${id}" node "${node.id}" formula "${node.formula}"`,
149
+ );
150
+ }
151
+
117
152
  const sized = rendered.map((node) => ({
118
153
  node,
119
154
  box:
@@ -134,11 +169,18 @@ const layout = layoutDiagram(
134
169
  const nodeById = new Map(rendered.map((node) => [node.id, node]));
135
170
  const laidOutById = new Map(layout.nodes.map((node) => [node.id, node]));
136
171
  const markerId = `diagram-arrow-${id}`;
172
+ const legacyAnchorId = `diagram-${id}`;
173
+ const renderedAnchorId = anchorId ?? legacyAnchorId;
174
+ const viewportId = `${legacyAnchorId}-viewport`;
137
175
 
138
- /** A node's human label for the text fallback — never the internal id. */
139
- function fallbackLabel(nodeId: string): string {
140
- const node = nodeById.get(nodeId);
141
- return node?.text ?? node?.formula ?? nodeId;
176
+ if (warnAboveWidth !== false && layout.width > warnAboveWidth) {
177
+ console.warn(
178
+ `[explico/Diagram] "${id}" lays out at ${layout.width}px wide, above the ` +
179
+ `${warnAboveWidth}px authoring threshold. It will scroll and can be expanded ` +
180
+ `by readers, but consider recreating the graph with direction="TB", shorter ` +
181
+ `labels, or explicit node sizes. Set warnAboveWidth={false} only when the ` +
182
+ `horizontal overflow is intentional.`,
183
+ );
142
184
  }
143
185
 
144
186
  function pathFrom(points: readonly { x: number; y: number }[]): string {
@@ -148,100 +190,334 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
148
190
  }
149
191
  ---
150
192
 
151
- <figure class="diagram" id={`diagram-${id}`}>
152
- <figcaption id={`${id}-caption`}>{label}</figcaption>
153
- <div class="diagram-scroll" tabindex="0" role="group" aria-labelledby={`${id}-caption`}>
154
- <svg
155
- viewBox={`0 0 ${layout.width} ${layout.height}`}
156
- width={layout.width}
157
- height={layout.height}
158
- >
159
- <defs>
160
- <marker
161
- id={markerId}
162
- viewBox="0 0 10 10"
163
- refX="9"
164
- refY="5"
165
- markerWidth="7"
166
- markerHeight="7"
167
- orient="auto-start-reverse"
193
+ <figure
194
+ class="diagram"
195
+ id={renderedAnchorId}
196
+ aria-labelledby={`${id}-caption`}
197
+ data-dia-number={number}
198
+ data-dia-key={number ? id : undefined}
199
+ data-diagram-width={layout.width}
200
+ data-diagram-expandable
201
+ >
202
+ <div class="diagram__titlebar">
203
+ <details
204
+ class="diagram__details"
205
+ data-diagram-details
206
+ data-remember-state={String(rememberState)}
207
+ data-state={defaultOpen ? 'open' : 'closed'}
208
+ open={defaultOpen}
209
+ >
210
+ <summary class="diagram__summary" id={`${id}-caption`}>
211
+ {number && <span class="diagram__number">Diagram {number}</span>}
212
+ <span class="diagram__caption-text">{label}</span>
213
+ </summary>
214
+ <div class="diagram__body" data-diagram-body>
215
+ <div
216
+ class="diagram-stage"
217
+ id={renderedAnchorId === legacyAnchorId ? undefined : legacyAnchorId}
218
+ >
219
+ <div
220
+ id={viewportId}
221
+ class="diagram-scroll"
222
+ tabindex="0"
223
+ role="group"
224
+ aria-labelledby={`${id}-caption`}
225
+ >
226
+ <div
227
+ class="diagram-canvas"
228
+ style={`--diagram-print-ratio: ${layout.width} / ${layout.height}; width: ${layout.width}px; height: ${layout.height}px;`}
229
+ >
230
+ <svg
231
+ viewBox={`0 0 ${layout.width} ${layout.height}`}
232
+ width="100%"
233
+ height="100%"
234
+ >
235
+ <defs>
236
+ <marker
237
+ id={markerId}
238
+ viewBox="0 0 10 10"
239
+ refX="9"
240
+ refY="5"
241
+ markerWidth="7"
242
+ markerHeight="7"
243
+ orient="auto-start-reverse"
244
+ >
245
+ <path
246
+ d="M 0 0 L 10 5 L 0 10 z"
247
+ class="diagram-arrowhead"
248
+ />
249
+ </marker>
250
+ </defs>
251
+
252
+ {
253
+ layout.edges.map((edge) => (
254
+ <path
255
+ d={pathFrom(edge.points)}
256
+ class="diagram-edge"
257
+ marker-end={`url(#${markerId})`}
258
+ />
259
+ ))
260
+ }
261
+
262
+ {
263
+ layout.nodes.map((position) => {
264
+ const node = nodeById.get(position.id)!;
265
+ return (
266
+ <g
267
+ transform={`translate(${position.x}, ${position.y})`}
268
+ class="diagram-node"
269
+ >
270
+ <rect
271
+ width={position.width}
272
+ height={position.height}
273
+ rx="8"
274
+ ry="8"
275
+ />
276
+ <foreignObject
277
+ width={position.width}
278
+ height={position.height}
279
+ >
280
+ <div
281
+ xmlns="http://www.w3.org/1999/xhtml"
282
+ class="diagram-node-content"
283
+ >
284
+ {
285
+ node.text && (
286
+ <span class="diagram-node-text">{node.text}</span>
287
+ )
288
+ }
289
+ {
290
+ node.mathNodes && (
291
+ <span class="diagram-node-formula notation-math">
292
+ {node.mathNodes.map((mathNode) => (
293
+ <SafeMathNode node={mathNode} />
294
+ ))}
295
+ </span>
296
+ )
297
+ }
298
+ </div>
299
+ </foreignObject>
300
+ </g>
301
+ );
302
+ })
303
+ }
304
+ </svg>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </details>
310
+
311
+ <a
312
+ class="diagram__title-anchor"
313
+ href={`#${renderedAnchorId}`}
314
+ aria-label={`${number ? `Diagram ${number}: ` : ''}${label}`}
315
+ >
316
+ <svg
317
+ aria-hidden="true"
318
+ width="16"
319
+ height="16"
320
+ viewBox="0 0 24 24"
321
+ fill="currentColor"
322
+ ><path
323
+ 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"
324
+ ></path></svg
168
325
  >
169
- <path d="M 0 0 L 10 5 L 0 10 z" class="diagram-arrowhead" />
170
- </marker>
171
- </defs>
172
-
173
- {
174
- layout.edges.map((edge) => (
175
- <path
176
- d={pathFrom(edge.points)}
177
- class="diagram-edge"
178
- marker-end={`url(#${markerId})`}
179
- />
180
- ))
181
- }
182
-
183
- {
184
- layout.nodes.map((position) => {
185
- const node = nodeById.get(position.id)!;
186
- return (
187
- <g transform={`translate(${position.x}, ${position.y})`} class="diagram-node">
188
- <rect width={position.width} height={position.height} rx="8" ry="8" />
189
- <foreignObject width={position.width} height={position.height}>
190
- <div xmlns="http://www.w3.org/1999/xhtml" class="diagram-node-content">
191
- {node.text && <span class="diagram-node-text">{node.text}</span>}
192
- {node.mathNodes && (
193
- <span class="diagram-node-formula notation-math">
194
- {node.mathNodes.map((mathNode) => (
195
- <SafeMathNode node={mathNode} />
196
- ))}
197
- </span>
198
- )}
199
- </div>
200
- </foreignObject>
201
- </g>
202
- );
203
- })
204
- }
205
- </svg>
206
- </div>
326
+ </a>
207
327
 
208
- <details class="diagram-fallback">
209
- <summary>Diagram description</summary>
210
- {
211
- description ? (
212
- <p>{description}</p>
213
- ) : (
214
- <Fragment>
215
- <ol>
216
- {rendered.map((node) => (
217
- <li>{fallbackLabel(node.id)}</li>
218
- ))}
219
- </ol>
220
- {edges.length > 0 && (
221
- <p class="diagram-fallback-flow">
222
- {edges.map((edge, index) => (
223
- <Fragment>
224
- {index > 0 && '; '}
225
- {fallbackLabel(edge.from)} → {fallbackLabel(edge.to)}
226
- </Fragment>
227
- ))}
228
- </p>
229
- )}
230
- </Fragment>
231
- )
232
- }
233
- </details>
328
+ <div class="diagram-toolbar" data-diagram-toolbar hidden>
329
+ <button
330
+ type="button"
331
+ class="diagram-expand-button"
332
+ data-diagram-expand
333
+ aria-controls={viewportId}
334
+ aria-expanded="false"
335
+ title="Expand diagram horizontally"
336
+ >
337
+ <svg aria-hidden="true" viewBox="0 0 24 24">
338
+ <path class="diagram-expand-icon" d="M3 12h18M8 7l-5 5 5 5M16 7l5 5-5 5" />
339
+ <path
340
+ class="diagram-shrink-icon"
341
+ d="M3 12h7M7 9l3 3-3 3M21 12h-7M17 9l-3 3 3 3"
342
+ />
343
+ </svg>
344
+ <span class="sr-only">Expand {label}</span>
345
+ </button>
346
+ </div>
347
+ </div>
234
348
  </figure>
235
349
 
236
350
  <style>
237
351
  @layer starlight.components {
238
352
  .diagram {
353
+ position: relative;
239
354
  margin: 1.5rem 0;
355
+ max-width: 100%;
356
+ contain: inline-size;
357
+ }
358
+
359
+ .diagram__titlebar {
360
+ position: relative;
240
361
  }
241
362
 
242
- .diagram figcaption {
243
- margin-block-end: 0.5rem;
363
+ .diagram__details {
364
+ border-inline-start: 0;
365
+ padding-inline-start: 0;
366
+ }
367
+
368
+ .diagram__summary {
369
+ position: relative;
370
+ min-height: 1.75rem;
371
+ padding-inline-end: 4rem;
244
372
  font-weight: 600;
373
+ cursor: pointer;
374
+ }
375
+
376
+ .diagram__summary::before {
377
+ position: absolute;
378
+ top: 50%;
379
+ left: -1.35em;
380
+ margin: 0;
381
+ transform: translateY(-50%);
382
+ opacity: 0;
383
+ transition:
384
+ opacity 150ms ease 700ms,
385
+ transform 150ms ease;
386
+ }
387
+
388
+ .diagram__details[data-state='open'] > .diagram__summary::before {
389
+ transform: translateY(-50%) rotateZ(90deg);
390
+ }
391
+
392
+ .diagram__details[data-state='closed'] > .diagram__summary::before {
393
+ opacity: 1;
394
+ transition: none;
395
+ }
396
+
397
+ .diagram__summary:hover::before,
398
+ .diagram__summary:focus-visible::before {
399
+ opacity: 1;
400
+ transition: opacity 100ms ease;
401
+ }
402
+
403
+ .diagram__body {
404
+ overflow: hidden;
405
+ transition: max-height 220ms ease;
406
+ }
407
+
408
+ .diagram__number {
409
+ color: var(--sl-color-text-accent);
410
+ }
411
+
412
+ .diagram__caption-text::before {
413
+ content: ' — ';
414
+ }
415
+
416
+ .diagram__summary > .diagram__caption-text:first-child::before {
417
+ content: none;
418
+ }
419
+
420
+ .diagram__title-anchor {
421
+ position: absolute;
422
+ top: 0.1rem;
423
+ inset-inline-end: 0;
424
+ display: inline-flex;
425
+ width: 1.5rem;
426
+ height: 1.5rem;
427
+ align-items: center;
428
+ justify-content: center;
429
+ color: var(--sl-color-gray-3);
430
+ text-decoration: none;
431
+ opacity: 1;
432
+ }
433
+
434
+ .diagram[data-diagram-overflow='true'] .diagram__title-anchor {
435
+ inset-inline-end: 2rem;
436
+ }
437
+
438
+ .diagram__title-anchor svg {
439
+ width: 0.9rem;
440
+ height: 0.9rem;
441
+ }
442
+
443
+ .diagram__title-anchor:hover,
444
+ .diagram__title-anchor:focus-visible {
445
+ color: var(--sl-color-text-accent);
446
+ }
447
+
448
+ .diagram__title-anchor:focus-visible {
449
+ outline: 2px solid var(--sl-color-accent);
450
+ outline-offset: 0.1rem;
451
+ }
452
+
453
+ @media (hover: hover) {
454
+ .diagram__title-anchor {
455
+ opacity: 0;
456
+ }
457
+
458
+ .diagram__details:has(> .diagram__summary:hover) ~ .diagram__title-anchor,
459
+ .diagram__details:has(> .diagram__summary:focus-visible)
460
+ ~ .diagram__title-anchor,
461
+ .diagram__title-anchor:hover,
462
+ .diagram__title-anchor:focus-visible {
463
+ opacity: 1;
464
+ }
465
+ }
466
+
467
+ .diagram-stage {
468
+ min-width: 0;
469
+ max-width: 100%;
470
+ overflow: hidden;
471
+ }
472
+
473
+ .diagram-toolbar {
474
+ position: absolute;
475
+ z-index: 2;
476
+ top: -0.25rem;
477
+ inset-inline-end: 0;
478
+ }
479
+
480
+ .diagram-toolbar[hidden] {
481
+ display: none;
482
+ }
483
+
484
+ .diagram-expand-button {
485
+ display: inline-flex;
486
+ width: 1.75rem;
487
+ height: 1.75rem;
488
+ margin: 0;
489
+ align-items: center;
490
+ justify-content: center;
491
+ color: var(--sl-color-gray-2);
492
+ border: 0;
493
+ border-radius: 0.25rem;
494
+ background: transparent;
495
+ cursor: pointer;
496
+ }
497
+
498
+ .diagram-expand-button svg {
499
+ width: 1.1rem;
500
+ height: 1.1rem;
501
+ fill: none;
502
+ stroke: currentColor;
503
+ stroke-width: 1.8;
504
+ stroke-linecap: round;
505
+ stroke-linejoin: round;
506
+ }
507
+
508
+ .diagram-expand-button[aria-expanded='false'] .diagram-shrink-icon,
509
+ .diagram-expand-button[aria-expanded='true'] .diagram-expand-icon {
510
+ display: none;
511
+ }
512
+
513
+ .diagram-expand-button:hover {
514
+ color: var(--sl-color-text);
515
+ background: var(--sl-color-accent-low);
516
+ }
517
+
518
+ .diagram-expand-button:focus-visible {
519
+ outline: 2px solid var(--sl-color-accent);
520
+ outline-offset: 1px;
245
521
  }
246
522
 
247
523
  /* A wide graph scrolls inside its own box rather than shrinking to
@@ -250,6 +526,10 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
250
526
  that: a scrollable region a keyboard user cannot reach is unreachable
251
527
  content, not just an awkward one. */
252
528
  .diagram-scroll {
529
+ width: 100%;
530
+ min-width: 0;
531
+ max-width: 100%;
532
+ flex: 1 1 auto;
253
533
  overflow-x: auto;
254
534
  padding-block: 0.15rem;
255
535
  }
@@ -259,11 +539,27 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
259
539
  outline-offset: 2px;
260
540
  }
261
541
 
262
- .diagram svg {
542
+ .diagram-canvas > svg {
263
543
  display: block;
544
+ /* Starlight globally constrains SVGs to the content width. That is safe
545
+ for an all-SVG illustration, but this diagram embeds HTML/KaTeX in
546
+ `foreignObject` nodes. WebKit on iOS can scale the SVG geometry while
547
+ leaving that HTML at its unscaled size, which puts formulas outside
548
+ their boxes. Keep the build-time dimensions and let the keyboard-
549
+ reachable wrapper above provide the intended horizontal scroll. */
550
+ max-width: none;
264
551
  overflow: visible;
265
552
  }
266
553
 
554
+ .diagram-canvas {
555
+ flex: none;
556
+ }
557
+
558
+ .diagram.dia-ref-highlight {
559
+ outline: 3px solid var(--sl-color-accent);
560
+ outline-offset: 0.25rem;
561
+ }
562
+
267
563
  .diagram-node rect {
268
564
  fill: var(--sl-color-gray-7);
269
565
  stroke: var(--sl-color-gray-5);
@@ -299,19 +595,29 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
299
595
  fill: var(--sl-color-gray-4);
300
596
  }
301
597
 
302
- .diagram-fallback {
303
- margin-block-start: 0.5rem;
304
- color: var(--sl-color-gray-3);
305
- font-size: var(--sl-text-sm);
306
- }
598
+ @media print {
599
+ .diagram-toolbar,
600
+ .diagram__title-anchor,
601
+ .diagram__summary::before {
602
+ display: none;
603
+ }
604
+
605
+ .diagram__number {
606
+ color: inherit;
607
+ }
307
608
 
308
- .diagram-fallback ol,
309
- .diagram-fallback p {
310
- margin: 0.35rem 0 0;
609
+ .diagram__body {
610
+ display: block !important;
611
+ max-height: none !important;
612
+ overflow: visible !important;
613
+ }
311
614
  }
312
615
 
313
- .diagram-fallback ol {
314
- padding-inline-start: 1.25rem;
616
+ @media (prefers-reduced-motion: reduce) {
617
+ .diagram__summary::before,
618
+ .diagram__body {
619
+ transition: none;
620
+ }
315
621
  }
316
622
  }
317
623
  </style>