explico 0.7.0 → 0.11.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 (236) hide show
  1. package/CHANGELOG.md +1409 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +54 -3
  5. package/dist/compiler/cli.js +80 -6
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +84 -8
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +14 -1
  15. package/dist/compiler/logging.js +6 -2
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +21 -11
  27. package/dist/reference/delimiter-size.js +15 -39
  28. package/dist/reference/diagrams-validate.d.ts +7 -2
  29. package/dist/reference/diagrams-validate.js +9 -1
  30. package/dist/reference/equations-validate.d.ts +8 -5
  31. package/dist/reference/equations-validate.js +1 -0
  32. package/dist/reference/equations.d.mts +69 -34
  33. package/dist/reference/equations.mjs +90 -44
  34. package/dist/reference/examples-validate.d.ts +31 -0
  35. package/dist/reference/examples-validate.js +48 -0
  36. package/dist/reference/figures-validate.d.ts +1 -1
  37. package/dist/reference/figures-validate.js +4 -1
  38. package/dist/reference/folds-validate.d.ts +33 -0
  39. package/dist/reference/folds-validate.js +32 -0
  40. package/dist/reference/gate-abbreviations.d.ts +3 -1
  41. package/dist/reference/gate-abbreviations.js +3 -2
  42. package/dist/reference/gate-course-rules.d.ts +32 -0
  43. package/dist/reference/gate-course-rules.js +85 -0
  44. package/dist/reference/gate-heading-length.d.ts +5 -2
  45. package/dist/reference/gate-heading-length.js +47 -23
  46. package/dist/reference/gate-heading-structure.d.ts +41 -0
  47. package/dist/reference/gate-heading-structure.js +113 -0
  48. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  49. package/dist/reference/gate-math-punctuation.js +251 -0
  50. package/dist/reference/gate-math.d.ts +44 -6
  51. package/dist/reference/gate-math.js +63 -10
  52. package/dist/reference/gate-part-pages.d.ts +50 -0
  53. package/dist/reference/gate-part-pages.js +118 -0
  54. package/dist/reference/gate-prose-notation.d.ts +32 -11
  55. package/dist/reference/gate-prose-notation.js +30 -8
  56. package/dist/reference/katex-delimiters.d.mts +15 -0
  57. package/dist/reference/katex-delimiters.mjs +138 -0
  58. package/dist/reference/katex-options.mjs +4 -0
  59. package/dist/reference/keyed-float-validate.d.ts +27 -10
  60. package/dist/reference/keyed-float-validate.js +15 -1
  61. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  62. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  63. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  64. package/dist/reference/knowledge-checks.d.mts +3 -1
  65. package/dist/reference/knowledge-checks.mjs +9 -4
  66. package/dist/reference/lab-math-scope.d.ts +1 -0
  67. package/dist/reference/lab-math-slot.d.ts +34 -0
  68. package/dist/reference/lab-math-slot.js +6 -0
  69. package/dist/reference/math-glyphs.d.mts +80 -4
  70. package/dist/reference/math-glyphs.mjs +588 -39
  71. package/dist/reference/page-notation.d.ts +6 -1
  72. package/dist/reference/raw-floats-validate.js +1 -1
  73. package/dist/reference/raw-floats.mjs +1 -1
  74. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  75. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  76. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  77. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  78. package/dist/reference/remark-asides.d.mts +3 -0
  79. package/dist/reference/remark-asides.mjs +180 -0
  80. package/dist/reference/remark-memo.d.mts +3 -1
  81. package/dist/reference/remark-memo.mjs +42 -11
  82. package/dist/reference/remark-notation.d.mts +60 -10
  83. package/dist/reference/remark-notation.mjs +387 -36
  84. package/dist/reference/render-lab-math.d.ts +4 -26
  85. package/dist/reference/render-lab-math.js +5 -5
  86. package/dist/reference/sections-validate.d.ts +27 -0
  87. package/dist/reference/sections-validate.js +41 -0
  88. package/dist/reference/sections.d.mts +97 -0
  89. package/dist/reference/sections.mjs +306 -0
  90. package/dist/reference/tables-validate.d.ts +1 -1
  91. package/dist/reference/types.d.ts +24 -1
  92. package/dist/reference/unit-words.d.ts +37 -0
  93. package/dist/reference/unit-words.js +170 -0
  94. package/dist/rules/catalog.d.ts +176 -0
  95. package/dist/rules/catalog.js +983 -0
  96. package/dist/rules/course-rules.d.ts +115 -0
  97. package/dist/rules/course-rules.js +292 -0
  98. package/dist/rules/course-rules.schema.json +138 -0
  99. package/dist/rules/in-effect.d.ts +53 -0
  100. package/dist/rules/in-effect.js +142 -0
  101. package/dist/rules/rules.schema.json +1072 -0
  102. package/dist/rules/settings.d.ts +3 -0
  103. package/dist/rules/settings.js +290 -0
  104. package/dist/rules/waivers.d.ts +53 -0
  105. package/dist/rules/waivers.js +132 -0
  106. package/dist/session/PreferenceStore.d.ts +5 -0
  107. package/dist/session/PreferenceStore.js +9 -0
  108. package/docs/VERSIONING.md +2 -2
  109. package/docs/authoring/README.md +232 -40
  110. package/docs/authoring/agents.md +55 -9
  111. package/docs/authoring/components.md +178 -22
  112. package/docs/authoring/migrating.md +1324 -2
  113. package/docs/authoring/notation.md +616 -344
  114. package/docs/authoring/prose.md +551 -0
  115. package/docs/authoring/retired.md +35 -0
  116. package/docs/authoring/structure-rules.md +422 -40
  117. package/docs/authoring/structure.md +445 -62
  118. package/docs/authoring/versioning.md +20 -28
  119. package/docs/authoring/visualizations.md +142 -20
  120. package/docs/katex-bracket-scaling.md +191 -0
  121. package/package.json +3 -2
  122. package/src/compiler/cli.ts +139 -6
  123. package/src/compiler/collections.ts +144 -10
  124. package/src/compiler/course-config.ts +273 -34
  125. package/src/compiler/knowledge-version.ts +1 -1
  126. package/src/compiler/lesson-derivation.ts +54 -15
  127. package/src/compiler/logging.ts +21 -4
  128. package/src/compiler/manifest.ts +573 -104
  129. package/src/components/BlockHeading.astro +113 -0
  130. package/src/components/CourseContents.astro +221 -0
  131. package/src/components/CurriculumMap.astro +5 -6
  132. package/src/components/FloatIndex.astro +200 -0
  133. package/src/components/HoverPanel.astro +431 -79
  134. package/src/components/PartBridge.astro +27 -0
  135. package/src/components/PartOverview.astro +401 -0
  136. package/src/components/PartScope.astro +27 -0
  137. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  138. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  139. package/src/components/assessments/AssessmentSet.astro +23 -26
  140. package/src/components/block-heading.ts +146 -0
  141. package/src/components/card-arrow.ts +243 -0
  142. package/src/components/card-drag.ts +631 -0
  143. package/src/components/card-jump.ts +45 -0
  144. package/src/components/card-manager.ts +1589 -0
  145. package/src/components/card-memory.ts +177 -0
  146. package/src/components/card-placement.ts +504 -0
  147. package/src/components/card-side.ts +336 -0
  148. package/src/components/card-travel.ts +197 -0
  149. package/src/components/citation/CitationLayer.astro +77 -76
  150. package/src/components/diagram/Diagram.astro +161 -316
  151. package/src/components/disclosure/course-disclosure.ts +24 -0
  152. package/src/components/disclosure/hover-preview.ts +122 -42
  153. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  154. package/src/components/disclosure/print-as-shown.ts +252 -0
  155. package/src/components/examples/CompactExample.astro +3 -1
  156. package/src/components/examples/CompactExamples.astro +101 -94
  157. package/src/components/figure/Figure.astro +174 -274
  158. package/src/components/figure/FloatCaption.astro +87 -0
  159. package/src/components/figure/figure-state.ts +58 -0
  160. package/src/components/figure/figure-tools.ts +87 -0
  161. package/src/components/float-name.ts +101 -0
  162. package/src/components/fold/Fold.astro +125 -0
  163. package/src/components/fold/fold-id.ts +40 -0
  164. package/src/components/memo/Memo.astro +162 -231
  165. package/src/components/memo/ShortNote.astro +6 -6
  166. package/src/components/notation/NotationFormula.astro +5 -1
  167. package/src/components/notation/NotationGlossary.astro +24 -14
  168. package/src/components/notation/NotationLayer.astro +169 -162
  169. package/src/components/notation/NotationMath.astro +10 -1
  170. package/src/components/notation/render-notation-math.ts +111 -10
  171. package/src/components/reference/FloatRefLayer.astro +485 -423
  172. package/src/components/starlight/AboutPanel.astro +22 -6
  173. package/src/components/starlight/CollapsibleSections.astro +207 -93
  174. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  175. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  176. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  177. package/src/components/starlight/LayoutHeader.astro +298 -19
  178. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  179. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  180. package/src/components/starlight/LayoutSidebar.astro +229 -11
  181. package/src/components/starlight/LessonExportActions.astro +53 -72
  182. package/src/components/starlight/LessonFooter.astro +26 -4
  183. package/src/components/starlight/section-parts.ts +15 -2
  184. package/src/components/starlight/section-summary.ts +181 -0
  185. package/src/components/table/Table.astro +95 -76
  186. package/src/components/table/TableOverflowExpander.astro +65 -28
  187. package/src/content-config.ts +40 -4
  188. package/src/reference/base-accents.mjs +31 -0
  189. package/src/reference/base-roman-labels.mjs +2 -2
  190. package/src/reference/consistency.ts +176 -5
  191. package/src/reference/delimiter-size.ts +40 -61
  192. package/src/reference/diagrams-validate.ts +17 -1
  193. package/src/reference/equations-validate.ts +10 -5
  194. package/src/reference/equations.mjs +90 -44
  195. package/src/reference/examples-validate.ts +97 -0
  196. package/src/reference/figures-validate.ts +6 -1
  197. package/src/reference/folds-validate.ts +65 -0
  198. package/src/reference/gate-abbreviations.ts +3 -2
  199. package/src/reference/gate-course-rules.ts +134 -0
  200. package/src/reference/gate-heading-length.ts +68 -37
  201. package/src/reference/gate-heading-structure.ts +193 -0
  202. package/src/reference/gate-math-punctuation.ts +381 -0
  203. package/src/reference/gate-math.ts +119 -12
  204. package/src/reference/gate-part-pages.ts +217 -0
  205. package/src/reference/gate-prose-notation.ts +74 -13
  206. package/src/reference/katex-delimiters.mjs +138 -0
  207. package/src/reference/katex-options.mjs +4 -0
  208. package/src/reference/keyed-float-validate.ts +45 -11
  209. package/src/reference/keyed-jsx-floats.mjs +73 -20
  210. package/src/reference/knowledge-checks-validate.ts +1 -1
  211. package/src/reference/knowledge-checks.mjs +9 -4
  212. package/src/reference/lab-math-slot.ts +36 -0
  213. package/src/reference/math-glyphs.mjs +588 -39
  214. package/src/reference/raw-floats-validate.ts +1 -1
  215. package/src/reference/raw-floats.mjs +1 -1
  216. package/src/reference/rehype-heading-anchors.mjs +116 -17
  217. package/src/reference/rehype-table-header-labels.mjs +21 -3
  218. package/src/reference/remark-asides.mjs +180 -0
  219. package/src/reference/remark-memo.mjs +42 -11
  220. package/src/reference/remark-notation.mjs +387 -36
  221. package/src/reference/render-lab-math.ts +17 -32
  222. package/src/reference/sections-validate.ts +81 -0
  223. package/src/reference/sections.mjs +306 -0
  224. package/src/reference/tables-validate.ts +2 -1
  225. package/src/reference/types.ts +24 -0
  226. package/src/reference/unit-words.ts +233 -0
  227. package/src/rules/catalog.ts +1298 -0
  228. package/src/rules/course-rules.ts +456 -0
  229. package/src/rules/in-effect.ts +235 -0
  230. package/src/rules/settings.ts +448 -0
  231. package/src/rules/waivers.ts +249 -0
  232. package/src/session/PreferenceStore.ts +15 -0
  233. package/src/styles/global.css +1471 -163
  234. package/src/styles/surfaces.css +182 -0
  235. package/src/components/hover-panel.ts +0 -339
  236. package/src/components/popup-stack.ts +0 -345
@@ -6,65 +6,53 @@
6
6
  * mirrors exactly (separate counter, same `[[fig-<id>]]` / cross-page
7
7
  * reference grammar, `number`/`anchorId` computed by `remark-notation.mjs`).
8
8
  *
9
+ * **One caption pair** (docs/plans/titles-and-containers.md Part 3): a figure
10
+ * is named by `caption`, the long caption shown below it with its number,
11
+ * and an optional `short` caption, the name the `[[fig-…]]` preview card
12
+ * heads with (and rule S1 measures). There is no title bar: a lab inside the
13
+ * figure carries no title or caption of its own either — the figure's
14
+ * caption names the whole.
15
+ *
9
16
  * `previewable` (default `true`) controls what the hover-preview card
10
17
  * (`FloatRefLayer.astro`) shows for a `[[fig-<id>]]` reference elsewhere in
11
- * the prose: `true` clones this figure's visual into the card alongside its
12
- * caption; `false` shows the caption and number only. Set it `false` for
13
- * interactive content — a hydrated `client:*` island cloned into a floating
14
- * card would be an inert, non-hydrated, id-colliding duplicate, not a
15
- * working preview.
16
- *
17
- * Collapsible + persistent (docs/plans/collapsible-disclosure.md Part 1): the
18
- * figure body sits in a native `<details>` whose `<summary>` is a persistent
19
- * title bar showing `title` (a short label, distinct from the longer
20
- * `caption`) — reading as an OS-window minimize/restore control while
21
- * staying entirely native `<details>` semantics (`aria-expanded`, keyboard
22
- * toggle, no bespoke ARIA needed). The rotating chevron itself isn't
23
- * authored here: `@astrojs/starlight`'s own `markdown.css` already gives
24
- * every `.sl-markdown-content summary` a disclosure-triangle `::before`
25
- * marker that rotates 90° on `[open]` — reproducing that ourselves would
26
- * just draw two.
18
+ * the prose: `true` clones the figure's picture — the default slot, never
19
+ * its tools — into the card, as a still copy; `false` shows the caption and
20
+ * number only.
27
21
  *
28
- * The caption + permalink number live in a `<figcaption>` *inside*
29
- * `<details>`, after `.keyed-figure__body` — the pre-collapsible reading
30
- * position (caption below the figure), but now part of the disclosure
31
- * content: a native closed `<details>` hides it along with the body, since
32
- * hovering the (collapsed) summary already surfaces the caption via the
33
- * hover-peek card — showing it twice would be redundant. `textContent`
34
- * reads on a `display: none` element still work, so `FloatRefLayer`'s
35
- * preview card (`captionFor` in `FloatRefLayer.astro`) can read it
36
- * regardless of the source figure's collapse state.
22
+ * **Laid out as LaTeX lays it out, in every state**
23
+ * (docs/plans/figures-and-short-captions.md, decision 1): the picture, then
24
+ * the caption line (`FloatCaption.astro`). A figure never hides or compacts
25
+ * by itself; to make one fully collapsible, wrap it in a `<Fold>` or put it
26
+ * in a `### … [collapsible]` / `[card]` section.
37
27
  *
38
- * The title-bar anchor link (`.keyed-figure__title-anchor`) sits next to the
39
- * title text but, like the permalink number, is a DOM sibling of `<summary>`
40
- * rather than a child of it — positioned onto the same line with CSS
41
- * (mirroring Starlight's own heading-wrapper anchor trick in
42
- * `anchor-links.css`, adapted since our "heading" is an interactive
43
- * `<summary>` rather than inert text). A focusable `<a>` nested *inside* a
44
- * `<summary>` trips axe's `nested-interactive` check (two independently
45
- * activatable controls, unclear which one wins on click/Enter) — the
46
- * `figures.spec.ts` axe test enforces this. Keeping it a sibling, wrapped
47
- * together in `.keyed-figure__titlebar` with `position: relative` +
48
- * `position: absolute`, avoids that while still reading as "next to the
49
- * title".
50
- *
51
- * `defaultOpen` sets the first-visit state; `rememberState` (default `true`)
52
- * persists the reader's later choice per-page via
53
- * `../disclosure/persisted-disclosure`.
28
+ * **A lab is picture → caption → tools** (decisions 3–6, 8). The default
29
+ * slot is the picture (a lab's plot); the named slot `tools` holds its
30
+ * controls — an instruction line, the sliders and fields, the live equation —
31
+ * and renders after the caption line, in a tray inside a `<details>`. Only
32
+ * the tools fold: the fold button ⌃ / ⌄ ends the caption line, and folded
33
+ * tools leave a "Hidden here: 2 sliders, 1 equation" line that unfolds them
34
+ * (`./figure-tools.ts`). Their first state is `defaultOpen`, else the
35
+ * course's `disclosure.figures`; `rememberState` (default `true`) keeps the
36
+ * reader's choice per page. The caption line has a live end the lab writes
37
+ * the case it shows into (`./figure-state.ts`). Print and export show the
38
+ * figure as LaTeX would — picture and caption, live clause included; the
39
+ * tools never print. A figure with no `tools` slot has no button and no tray.
54
40
  */
41
+ import FloatCaption from './FloatCaption.astro';
42
+ import { courseDisclosure } from '../disclosure/course-disclosure';
55
43
 
56
44
  export interface Props {
57
45
  /** Stable key, e.g. `id="projectile-trajectory"`. Omit for a positional-only figure. */
58
46
  id?: string;
59
- /** Short label for the title bar / anchor. Falls back to `caption` when omitted. */
60
- title?: string;
61
- /** Full caption, shown below the figure (with its number) and in the hover-preview card. */
47
+ /** The long caption, shown below the figure with its number, and in the hover-preview card. */
62
48
  caption: string;
63
- /** `false` for interactive content the hover-preview must not clone. */
49
+ /** Optional short caption: heads the `[[fig-…]]` preview card; rule S1 measures it. */
50
+ short?: string;
51
+ /** `false` to keep the hover-preview card to the caption and number. */
64
52
  previewable?: boolean;
65
- /** Author-chosen state for a reader with nothing saved yet. Default: `true`. */
53
+ /** Whether the `tools` start open for a reader with nothing saved yet. Default: the course's `disclosure.figures`. */
66
54
  defaultOpen?: boolean;
67
- /** Persist the reader's open/closed choice across visits, per page. Default: `true`. */
55
+ /** Persist the reader's open/folded choice for the `tools` across visits, per page. Default: `true`. */
68
56
  rememberState?: boolean;
69
57
  /** Computed by `remark-notation.mjs` — never author this directly. */
70
58
  number?: string;
@@ -74,25 +62,36 @@ export interface Props {
74
62
 
75
63
  const {
76
64
  id,
77
- title,
78
65
  caption,
66
+ short,
79
67
  previewable = true,
80
- defaultOpen = true,
68
+ defaultOpen,
81
69
  rememberState = true,
82
70
  number,
83
71
  anchorId,
84
72
  } = Astro.props;
85
73
 
74
+ if ('title' in Astro.props) {
75
+ throw new Error(
76
+ `<Figure${id ? ` id="${id}"` : ''}> has no \`title\` prop: a figure is named by its \`caption\` (the long caption, shown below it with its number) and an optional \`short\` caption. Move a short title to \`short\`.`,
77
+ );
78
+ }
86
79
  if (typeof caption !== 'string' || !caption.trim()) {
87
80
  throw new Error('Figure requires a non-empty `caption` prop.');
88
81
  }
82
+ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
83
+ throw new Error('Figure `short` must be a non-empty string when given.');
84
+ }
89
85
  if (!number || !anchorId) {
90
86
  throw new Error(
91
87
  'Figure is missing its computed `number`/`anchorId` — this page must run remark-notation with a `figures` cross-page table configured (see astro.config.mjs).',
92
88
  );
93
89
  }
94
90
 
95
- const titleText = typeof title === 'string' && title.trim() ? title : caption;
91
+ const hasTools = Astro.slots.has('tools');
92
+ const toolsId = `${anchorId}-tools`;
93
+ const toolsOpen = hasTools && (defaultOpen ?? (await courseDisclosure()).figures.defaultOpen);
94
+ const name = `Figure ${number}`;
96
95
  ---
97
96
 
98
97
  <figure
@@ -100,46 +99,70 @@ const titleText = typeof title === 'string' && title.trim() ? title : caption;
100
99
  id={anchorId}
101
100
  data-fig-number={number}
102
101
  data-fig-key={id}
102
+ data-fig-short={short}
103
103
  data-fig-previewable={String(previewable)}
104
+ data-fig-tools={hasTools ? '' : undefined}
104
105
  >
105
- <div class="keyed-figure__titlebar">
106
- <details
107
- class="keyed-figure__details"
108
- data-remember-state={String(rememberState)}
109
- open={defaultOpen}
110
- >
111
- <summary class="keyed-figure__summary">
112
- <span class="keyed-figure__title-text">{titleText}</span>
113
- </summary>
114
- <div class="keyed-figure__body">
115
- <slot />
116
- </div>
117
- <figcaption class="keyed-figure__caption">
118
- <a
119
- class="keyed-figure__number"
120
- href={`#${anchorId}`}
121
- aria-label={`Figure ${number}`}>Figure {number}</a
106
+ <div class="keyed-figure__body">
107
+ <slot />
108
+ </div>
109
+ <FloatCaption
110
+ kind="Figure"
111
+ prefix="keyed-figure"
112
+ caption={caption}
113
+ short={short}
114
+ anchorId={anchorId}
115
+ number={number}
116
+ live
117
+ >
118
+ {
119
+ hasTools && (
120
+ <button
121
+ type="button"
122
+ class="float-tool keyed-figure__tools-toggle"
123
+ data-figure-tools-toggle
124
+ aria-controls={toolsId}
125
+ aria-expanded={String(toolsOpen)}
126
+ aria-label={`${toolsOpen ? 'Hide' : 'Show'} the controls of ${name}`}
127
+ title={toolsOpen ? 'Hide the controls' : 'Show the controls'}
122
128
  >
123
- <span class="keyed-figure__caption-text">{caption}</span>
124
- </figcaption>
125
- </details>
126
- <a
127
- class="keyed-figure__title-anchor"
128
- href={`#${anchorId}`}
129
- aria-label={`Figure ${number} titled “${titleText}”`}
130
- >
131
- <svg
132
- aria-hidden="true"
133
- width="16"
134
- height="16"
135
- viewBox="0 0 24 24"
136
- fill="currentColor"
137
- ><path
138
- 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"
139
- ></path></svg
129
+ <svg viewBox="0 0 24 24" aria-hidden="true">
130
+ <path class="float-tool__fold" d="M6 15l6-6 6 6" />
131
+ <path class="float-tool__unfold" d="M6 9l6 6 6-6" />
132
+ </svg>
133
+ </button>
134
+ )
135
+ }
136
+ </FloatCaption>
137
+ {
138
+ hasTools && (
139
+ <button
140
+ type="button"
141
+ class="keyed-figure__tools-stub"
142
+ data-figure-tools-stub
143
+ aria-controls={toolsId}
144
+ hidden={toolsOpen}
140
145
  >
141
- </a>
142
- </div>
146
+ Hidden here: the controls
147
+ </button>
148
+ )
149
+ }
150
+ {
151
+ hasTools && (
152
+ <details
153
+ class="keyed-figure__tools-fold"
154
+ id={toolsId}
155
+ data-remember-state={String(rememberState)}
156
+ data-print-omit
157
+ open={toolsOpen}
158
+ >
159
+ <summary class="keyed-figure__tools-summary">Controls of {name}</summary>
160
+ <div class="keyed-figure__tools">
161
+ <slot name="tools" />
162
+ </div>
163
+ </details>
164
+ )
165
+ }
143
166
  </figure>
144
167
 
145
168
  <style>
@@ -148,226 +171,103 @@ const titleText = typeof title === 'string' && title.trim() ? title : caption;
148
171
  padding: 0;
149
172
  }
150
173
 
151
- /* One card for the whole figure — title bar and body together — rather
152
- than a separate title strip on top of a second bordered box (the
153
- `.lab-shell` a wrapped lab already draws around itself): the title now
154
- lives *inside* this card, so `.lab-shell` gets its own border/background
155
- stripped back to plain spacing when nested here (see `global.css`,
156
- `.keyed-figure__body .lab-shell`) to avoid drawing that second box.
157
- Same treatment as `.lab-shell` itself, for one consistent "card"
158
- language sitewide — overrides Starlight's own default `<details>`
159
- accent rule (`markdown.css`, a rule down the left side) entirely. */
160
- .keyed-figure__details {
161
- border: 1px solid var(--sl-color-gray-5);
162
- border-block-start: 4px solid var(--sl-color-accent);
163
- border-radius: 0.2rem;
164
- background: color-mix(in srgb, var(--sl-color-bg) 94%, var(--sl-color-accent) 6%);
165
- box-shadow: 0 1rem 2.6rem rgba(0, 0, 0, 0.13);
166
- padding: clamp(1rem, 3vw, 1.5rem);
167
- }
168
-
169
- .keyed-figure__titlebar {
170
- position: relative;
171
- /* A custom property, not `font-size` itself: `font-size` is inherited
172
- CSS, and `.keyed-figure__body` (the slotted figure content) is a
173
- *descendant* of this wrapper too — setting `font-size` here directly
174
- would inflate the figure's real content, not just the title. Read
175
- this back explicitly on just the summary and the anchor icon below. */
176
- --keyed-figure-title-size: var(--sl-text-h3);
177
- }
178
-
179
- .keyed-figure__summary {
180
- /* This is a title, not a caption — size it like the headings labs
181
- already use internally (e.g. LaunchAngleExplorer's own <h3>), not
182
- like the muted `.keyed-figure__caption-text` below. */
183
- font-size: var(--keyed-figure-title-size);
184
- line-height: var(--sl-line-height-headings, 1.2);
185
- /* Room for the absolutely-positioned anchor icon so title text never
186
- runs under it. */
187
- padding-inline-end: 1.5em;
188
- }
189
-
190
- .keyed-figure__title-text {
191
- font-weight: 600;
192
- color: var(--sl-color-white);
193
- }
194
-
195
- .keyed-figure__title-anchor {
196
- position: absolute;
197
- top: 0.15em;
198
- inset-inline-end: 0;
199
- display: inline-flex;
200
- align-items: center;
201
- justify-content: center;
202
- font-size: var(--keyed-figure-title-size);
203
- width: 1em;
204
- height: 1em;
205
- color: var(--sl-color-gray-3);
206
- text-decoration: none;
207
- /* Prevent double/triple clicks from selecting the icon's a11y text. */
208
- -webkit-user-select: none;
209
- user-select: none;
210
- }
211
-
212
- .keyed-figure__title-anchor svg {
213
- width: 0.7em;
214
- height: 0.7em;
174
+ /* A figure is book material: the plain tier (`surfaces.css`), no card —
175
+ no border, tint, shadow or top rule (docs/plans/titles-and-containers.md
176
+ Q8). A figure that should read as set apart goes in a `### … [card]`
177
+ section. The caption line is styled once for every float, in
178
+ `global.css` (`.float-caption`). */
179
+ .keyed-figure__body {
180
+ overflow-x: auto;
215
181
  }
216
182
 
217
- .keyed-figure__title-anchor:hover,
218
- .keyed-figure__title-anchor:focus-visible {
219
- color: var(--sl-color-text-accent);
183
+ /* A lab's tools: a tray of their own after the caption line — a thin
184
+ border and the meta tier's radius, no tint, so it reads as controls
185
+ beside the figure rather than as part of the picture. */
186
+ .keyed-figure__tools-fold {
187
+ margin-block-start: 0.75rem;
188
+ padding: 0;
189
+ border: 0;
220
190
  }
221
191
 
222
- .keyed-figure__title-anchor:focus-visible {
223
- outline: 2px solid var(--sl-color-accent);
224
- outline-offset: 0.15rem;
192
+ .keyed-figure__tools {
193
+ padding: 0.75rem 1rem;
194
+ border: 1px solid var(--sl-color-gray-5);
195
+ border-radius: var(--ex-radius-meta, 0.5rem);
225
196
  }
226
197
 
227
- /* Hidden at rest, revealed on hover/focus of the title bar — same
228
- "hover/focus to reveal" treatment as Starlight's own heading anchors
229
- (`anchor-links.css`). */
230
- @media (hover: hover) {
231
- .keyed-figure__title-anchor {
232
- opacity: 0;
233
- }
234
-
235
- .keyed-figure__title-anchor:focus-visible,
236
- .keyed-figure__titlebar:hover .keyed-figure__title-anchor {
237
- opacity: 1;
238
- }
198
+ .keyed-figure__tools > :global(:first-child) {
199
+ margin-block-start: 0;
239
200
  }
240
201
 
241
- .keyed-figure__body {
242
- overflow-x: auto;
202
+ /* The native summary is for a reader without scripting; with it, the fold
203
+ button at the end of the caption line and the "Hidden here" line do the
204
+ work. */
205
+ .keyed-figure__tools-summary {
206
+ display: none;
243
207
  }
244
208
 
245
- .keyed-figure__caption {
246
- margin-block-start: 0.5rem;
209
+ /* Folded tools leave a line in the section stub's look (`global.css`
210
+ `.sl-section-stub`), a button that unfolds them. */
211
+ .keyed-figure__tools-stub {
212
+ display: block;
213
+ width: 100%;
214
+ margin: 0.3rem 0 0;
215
+ padding: 0 0 0.45rem;
216
+ border: 0;
217
+ border-block-end: 1px dashed var(--sl-color-gray-5);
218
+ background: none;
219
+ font: inherit;
247
220
  font-size: var(--sl-text-sm);
221
+ line-height: 1.5;
248
222
  color: var(--sl-color-gray-3);
223
+ text-align: start;
224
+ cursor: pointer;
249
225
  }
250
226
 
251
- .keyed-figure__number {
252
- font-weight: 600;
253
- font-variant-numeric: tabular-nums;
254
- color: var(--sl-color-text-accent);
255
- text-decoration: none;
227
+ .keyed-figure__tools-stub[hidden] {
228
+ display: none;
256
229
  }
257
230
 
258
- .keyed-figure__number:hover,
259
- .keyed-figure__number:focus-visible {
260
- text-decoration: underline;
231
+ .keyed-figure__tools-stub:hover {
232
+ color: var(--sl-color-text-accent);
261
233
  }
262
234
 
263
- .keyed-figure__number:focus-visible {
235
+ .keyed-figure__tools-stub:focus-visible {
264
236
  outline: 2px solid var(--sl-color-accent);
265
- outline-offset: 0.15rem;
266
- }
267
-
268
- .keyed-figure__caption-text::before {
269
- content: ' — ';
237
+ outline-offset: 2px;
270
238
  }
271
239
 
272
- @media print {
273
- .keyed-figure__number {
274
- color: inherit;
275
- }
276
-
277
- .keyed-figure__title-anchor {
278
- display: none;
240
+ @media (scripting: none) {
241
+ .keyed-figure__tools-summary {
242
+ display: list-item;
243
+ font-size: var(--sl-text-sm);
244
+ font-weight: 600;
279
245
  }
280
246
 
281
- /* The disclosure triangle (Starlight's summary::before) is meaningless
282
- once printed content is forced open below. */
283
- .keyed-figure__summary::before {
247
+ .keyed-figure__tools-stub,
248
+ :global(.keyed-figure__tools-toggle) {
284
249
  display: none !important;
285
250
  }
251
+ }
286
252
 
287
- /* A closed <details> hides its non-summary children via the UA
288
- stylesheet; force the body and caption visible regardless of
289
- on-screen state so a collapsed figure still prints its content. */
290
- .keyed-figure__body,
291
- .keyed-figure__caption {
292
- display: block !important;
253
+ /* Print and export show a lab as its LaTeX figure: the picture and the
254
+ caption, live clause included. The tools never print
255
+ (`../disclosure/print-as-shown.ts` leaves their `<details>` alone). */
256
+ @media print {
257
+ .keyed-figure__tools-fold,
258
+ .keyed-figure__tools-stub {
259
+ display: none !important;
293
260
  }
294
261
  }
295
262
  </style>
296
263
 
297
264
  <script>
298
- import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
299
- import { attachHoverPeek, closeHoverPeek } from '../disclosure/hover-preview';
265
+ import { wireFigureTools } from './figure-tools';
300
266
 
301
267
  document
302
- .querySelectorAll<HTMLDetailsElement>(
303
- '.keyed-figure__details:not([data-figure-wired])',
304
- )
305
- .forEach((details) => {
306
- details.dataset.figureWired = 'true';
307
- const figure = details.closest<HTMLElement>('.keyed-figure');
308
- if (!figure || !figure.id) return;
309
-
310
- // The SSR-rendered `open` attribute already carries the author's
311
- // `defaultOpen` prop verbatim — read it before anything below mutates
312
- // `.open`, rather than threading the prop value through a second data
313
- // attribute.
314
- const defaultOpen = details.open;
315
- if (details.dataset.rememberState === 'true') {
316
- wirePersistedDetails(details, `figure:${figure.id}`, defaultOpen);
317
- }
318
-
319
- const summary = details.querySelector<HTMLElement>(
320
- ':scope > summary',
321
- );
322
-
323
- // Deep-linking to this figure (from a `[[fig-key]]` jump or a shared
324
- // URL) must reveal it even if a reader had left it collapsed — the
325
- // figure's id lives on the always-visible <figure>, not on the
326
- // <details> itself, so the browser's native :target reveal doesn't
327
- // apply here and this has to be done by hand.
328
- const openIfTargeted = () => {
329
- if (location.hash === `#${figure.id}`) details.open = true;
330
- };
331
- openIfTargeted();
332
- window.addEventListener('hashchange', openIfTargeted);
333
-
334
- if (!summary) return;
335
-
336
- // The peek is only useful while collapsed — once expanded, the real
337
- // content is already on screen right below the title bar, and duping
338
- // it into a floating box would just be noise. `attachHoverPeek` itself
339
- // doesn't know about "collapsed", so gate it here: a capture-order
340
- // listener added *before* the wiring call runs first on the same
341
- // element and can suppress the event before `attachHoverPeek`'s own
342
- // handler sees it.
343
- const suppressWhenOpen = (event: Event) => {
344
- if (details.open) event.stopImmediatePropagation();
345
- };
346
- summary.addEventListener('pointerenter', suppressWhenOpen);
347
- summary.addEventListener('focus', suppressWhenOpen);
348
-
349
- // Expanding while the peek is still showing (hover, then click to
350
- // open without the pointer ever leaving) would otherwise leave a now-
351
- // redundant preview floating over the real content that just
352
- // appeared underneath it.
353
- details.addEventListener('toggle', () => {
354
- if (details.open) closeHoverPeek(summary);
355
- });
356
-
357
- // The caption now lives in the <figcaption> after <details>, not
358
- // inside the summary — query it from the figure, not the summary.
359
- const captionText =
360
- figure.querySelector('.keyed-figure__caption-text')?.textContent ??
361
- '';
362
- attachHoverPeek(summary, {
363
- label: `Figure ${figure.dataset.figNumber ?? ''} preview`,
364
- getContent: () => {
365
- const wrap = document.createElement('div');
366
- const strong = document.createElement('strong');
367
- strong.textContent = `Figure ${figure.dataset.figNumber ?? ''}`;
368
- wrap.append(strong, document.createTextNode(` — ${captionText}`));
369
- return wrap;
370
- },
371
- });
268
+ .querySelectorAll<HTMLElement>('.keyed-figure[data-fig-tools]:not([data-fig-tools-wired])')
269
+ .forEach((figure) => {
270
+ figure.dataset.figToolsWired = 'true';
271
+ wireFigureTools(figure);
372
272
  });
373
273
  </script>
@@ -0,0 +1,87 @@
1
+ ---
2
+ /**
3
+ * The caption line of a `<Figure>` or a `<Diagram>`
4
+ * (docs/plans/figures-and-short-captions.md, decisions 1 and 4). One
5
+ * anatomy, below the picture, the LaTeX figure convention:
6
+ *
7
+ * Figure 2.2.1 [chain] **‹short›.** ‹caption› ‹live end› [buttons]
8
+ *
9
+ * The number links to the float, the chain icon copies that link
10
+ * (`HeadingAnchorEnhancer.astro` reads `data-copy-link`) and then navigates
11
+ * the same way, and the caption is the float's one long caption. A float
12
+ * with a short caption opens the caption with it as a bold title,
13
+ * `b.float-caption__title` (decision 9.1), outside `.float-caption__text`, so
14
+ * a reader of the long caption (a preview card, a fold's bar) still reads
15
+ * it alone. The line
16
+ * closes the figure and is never the thing that hides: a figure is always
17
+ * its picture and then this line. To fold a float away completely, wrap it
18
+ * in a `<Fold>` or put it in a collapsible section.
19
+ *
20
+ * A host's buttons (a diagram's widen ↔, the fold of a lab's tools) go in
21
+ * the default slot, rendered inside a `.float-tools` span at the end of the
22
+ * line — the boxed row a table's caption has (`global.css`). A float with no
23
+ * buttons has no row, even when the host passes an empty slot.
24
+ *
25
+ * `live` adds the line's live end: an empty
26
+ * `span.float-caption__state[data-figure-state]` after the caption, outside
27
+ * `.float-caption__text`, where a lab states the case its plot shows
28
+ * (`./figure-state.ts`).
29
+ */
30
+ import { captionTitle } from '../float-name';
31
+
32
+ export interface Props {
33
+ /** The reader-facing kind word: `Figure`, `Diagram`. */
34
+ kind: string;
35
+ /** Class prefix of the host component, e.g. `keyed-figure` → `keyed-figure__number`. */
36
+ prefix: string;
37
+ /** The float's long caption. */
38
+ caption: string;
39
+ /** Its short caption, shown as the line's bold title. */
40
+ short?: string;
41
+ /** Its anchor id (`fig-…`, `dia-…`). */
42
+ anchorId: string;
43
+ /** Its number; an unnumbered float shows none. */
44
+ number?: string;
45
+ /** An id for the caption text, for a control inside the body to be named by. */
46
+ textId?: string;
47
+ /** Give the line a live end a lab writes into (`./figure-state.ts`). */
48
+ live?: boolean;
49
+ }
50
+
51
+ const { kind, prefix, caption, short, anchorId, number, textId, live = false } = Astro.props;
52
+ const name = number ? `${kind} ${number}` : kind.toLowerCase();
53
+ const tools = Astro.slots.has('default') ? (await Astro.slots.render('default')).trim() : '';
54
+ ---
55
+
56
+ <figcaption class:list={['float-caption', `${prefix}__caption`]} data-float-name={name}>
57
+ {
58
+ number && (
59
+ <a
60
+ class:list={['float-caption__number', `${prefix}__number`]}
61
+ href={`#${anchorId}`}
62
+ aria-label={name}
63
+ >
64
+ {name}
65
+ </a>
66
+ )
67
+ }
68
+ <a
69
+ class="float-caption__anchor"
70
+ href={`#${anchorId}`}
71
+ aria-label={`Copy the link to ${name}`}
72
+ title="Copy link"
73
+ data-copy-link={name}
74
+ >
75
+ <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
76
+ ><path
77
+ 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"
78
+ ></path></svg
79
+ >
80
+ </a>
81
+ {short && <b class="float-caption__title">{captionTitle(short)}</b>}{short && ' '}<span
82
+ class:list={['float-caption__text', `${prefix}__caption-text`]}
83
+ id={textId}
84
+ >{caption}</span
85
+ >{live && <span class="float-caption__state" data-figure-state />}
86
+ {tools && <span class="float-tools" set:html={tools} />}
87
+ </figcaption>