react-cheminfo 0.8.0 → 0.9.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 (292) hide show
  1. package/README.md +60 -2
  2. package/bin/check-tokens.mjs +105 -0
  3. package/lib/about/core/about.d.ts +95 -0
  4. package/lib/about/core/about.d.ts.map +1 -0
  5. package/lib/about/core/about.js +78 -0
  6. package/lib/about/core/about.js.map +1 -0
  7. package/lib/about/core/index.d.ts +3 -0
  8. package/lib/about/core/index.d.ts.map +1 -0
  9. package/lib/about/core/index.js +2 -0
  10. package/lib/about/core/index.js.map +1 -0
  11. package/lib/about/ui/AboutCitations.d.ts +14 -0
  12. package/lib/about/ui/AboutCitations.d.ts.map +1 -0
  13. package/lib/about/ui/AboutCitations.js +40 -0
  14. package/lib/about/ui/AboutCitations.js.map +1 -0
  15. package/lib/about/ui/AboutPage.d.ts +32 -0
  16. package/lib/about/ui/AboutPage.d.ts.map +1 -0
  17. package/lib/about/ui/AboutPage.js +70 -0
  18. package/lib/about/ui/AboutPage.js.map +1 -0
  19. package/lib/about/ui/AboutSection.d.ts +23 -0
  20. package/lib/about/ui/AboutSection.d.ts.map +1 -0
  21. package/lib/about/ui/AboutSection.js +28 -0
  22. package/lib/about/ui/AboutSection.js.map +1 -0
  23. package/lib/about/ui/index.d.ts +7 -0
  24. package/lib/about/ui/index.d.ts.map +1 -0
  25. package/lib/about/ui/index.js +4 -0
  26. package/lib/about/ui/index.js.map +1 -0
  27. package/lib/chrome/ui/index.d.ts +1 -0
  28. package/lib/chrome/ui/index.d.ts.map +1 -1
  29. package/lib/chrome/ui/index.js +3 -0
  30. package/lib/chrome/ui/index.js.map +1 -1
  31. package/lib/clipboard/ui/CodeBlock.d.ts.map +1 -1
  32. package/lib/clipboard/ui/CodeBlock.js +4 -1
  33. package/lib/clipboard/ui/CodeBlock.js.map +1 -1
  34. package/lib/color/ui/ColorScaleLegend.js +1 -1
  35. package/lib/color/ui/ColorScaleLegend.js.map +1 -1
  36. package/lib/core.d.ts +3 -0
  37. package/lib/core.d.ts.map +1 -1
  38. package/lib/core.js +5 -0
  39. package/lib/core.js.map +1 -1
  40. package/lib/credits/core/credits.d.ts +36 -0
  41. package/lib/credits/core/credits.d.ts.map +1 -1
  42. package/lib/credits/core/credits.js +42 -0
  43. package/lib/credits/core/credits.js.map +1 -1
  44. package/lib/credits/ui/CreditsList.js +1 -1
  45. package/lib/credits/ui/CreditsList.js.map +1 -1
  46. package/lib/delimited/ui/DelimitedTextPanel.js +1 -1
  47. package/lib/delimited/ui/DelimitedTextPanel.js.map +1 -1
  48. package/lib/ecosystem/core/sites.d.ts +1 -1
  49. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  50. package/lib/ecosystem/core/sites.js +10 -0
  51. package/lib/ecosystem/core/sites.js.map +1 -1
  52. package/lib/ecosystem/core/tokens.d.ts.map +1 -1
  53. package/lib/ecosystem/core/tokens.js +1 -0
  54. package/lib/ecosystem/core/tokens.js.map +1 -1
  55. package/lib/ecosystem/ui/EcosystemLinks.js +2 -2
  56. package/lib/ecosystem/ui/EcosystemLinks.js.map +1 -1
  57. package/lib/ecosystem/ui/EcosystemMenu.js +1 -1
  58. package/lib/ecosystem/ui/EcosystemMenu.js.map +1 -1
  59. package/lib/ecosystem/ui/SiteTile.d.ts.map +1 -1
  60. package/lib/ecosystem/ui/SiteTile.js +5 -5
  61. package/lib/ecosystem/ui/SiteTile.js.map +1 -1
  62. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  63. package/lib/ecosystem/ui/glyphs.js +3 -0
  64. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  65. package/lib/orbital/core/index.d.ts +1 -1
  66. package/lib/orbital/core/index.d.ts.map +1 -1
  67. package/lib/orbital/core/index.js +1 -1
  68. package/lib/orbital/core/index.js.map +1 -1
  69. package/lib/orbital/core/realHarmonics.d.ts +5 -2
  70. package/lib/orbital/core/realHarmonics.d.ts.map +1 -1
  71. package/lib/orbital/core/realHarmonics.js.map +1 -1
  72. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +18 -0
  73. package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -1
  74. package/lib/orbital/ui/AtomicOrbitalCanvas.js +84 -7
  75. package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -1
  76. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +13 -0
  77. package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -1
  78. package/lib/orbital/ui/AtomicOrbitalViewer.js +1 -1
  79. package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -1
  80. package/lib/orbital/ui/camera.d.ts +15 -0
  81. package/lib/orbital/ui/camera.d.ts.map +1 -1
  82. package/lib/orbital/ui/camera.js +68 -0
  83. package/lib/orbital/ui/camera.js.map +1 -1
  84. package/lib/orbital/ui/renderVolume.d.ts.map +1 -1
  85. package/lib/orbital/ui/renderVolume.js +8 -1
  86. package/lib/orbital/ui/renderVolume.js.map +1 -1
  87. package/lib/orbital/ui/serial.d.ts +25 -0
  88. package/lib/orbital/ui/serial.d.ts.map +1 -0
  89. package/lib/orbital/ui/serial.js +25 -0
  90. package/lib/orbital/ui/serial.js.map +1 -0
  91. package/lib/orbital/ui/viewer.d.ts +23 -0
  92. package/lib/orbital/ui/viewer.d.ts.map +1 -1
  93. package/lib/orbital/ui/viewer.js +70 -17
  94. package/lib/orbital/ui/viewer.js.map +1 -1
  95. package/lib/pedagogy/ui/ExerciseProgressHeader.js +1 -1
  96. package/lib/pedagogy/ui/ExerciseProgressHeader.js.map +1 -1
  97. package/lib/pedagogy/ui/GlossaryText.js +2 -2
  98. package/lib/pedagogy/ui/GlossaryText.js.map +1 -1
  99. package/lib/pedagogy/ui/ReferenceSectionBlock.js +5 -2
  100. package/lib/pedagogy/ui/ReferenceSectionBlock.js.map +1 -1
  101. package/lib/pedagogy/ui/SyntaxTooltip.js +2 -2
  102. package/lib/pedagogy/ui/SyntaxTooltip.js.map +1 -1
  103. package/lib/pedagogy/ui/TutorialStepStrip.js +4 -1
  104. package/lib/pedagogy/ui/TutorialStepStrip.js.map +1 -1
  105. package/lib/periodic/ui/CategoryLegend.js +1 -1
  106. package/lib/periodic/ui/CategoryLegend.js.map +1 -1
  107. package/lib/periodic/ui/PeriodicTableChrome.js +2 -2
  108. package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
  109. package/lib/seo/vite/ogCard.d.ts.map +1 -1
  110. package/lib/seo/vite/ogCard.js +1 -0
  111. package/lib/seo/vite/ogCard.js.map +1 -1
  112. package/lib/share/ui/ShareDialogContent.d.ts.map +1 -1
  113. package/lib/share/ui/ShareDialogContent.js +5 -2
  114. package/lib/share/ui/ShareDialogContent.js.map +1 -1
  115. package/lib/share/ui/SharePartOptions.js +1 -1
  116. package/lib/share/ui/SharePartOptions.js.map +1 -1
  117. package/lib/slides/core/demoLinks.d.ts +43 -0
  118. package/lib/slides/core/demoLinks.d.ts.map +1 -0
  119. package/lib/slides/core/demoLinks.js +104 -0
  120. package/lib/slides/core/demoLinks.js.map +1 -0
  121. package/lib/slides/core/index.d.ts +13 -0
  122. package/lib/slides/core/index.d.ts.map +1 -0
  123. package/lib/slides/core/index.js +7 -0
  124. package/lib/slides/core/index.js.map +1 -0
  125. package/lib/slides/core/sectionHeading.d.ts +19 -0
  126. package/lib/slides/core/sectionHeading.d.ts.map +1 -0
  127. package/lib/slides/core/sectionHeading.js +19 -0
  128. package/lib/slides/core/sectionHeading.js.map +1 -0
  129. package/lib/slides/core/slideNavigation.d.ts +38 -0
  130. package/lib/slides/core/slideNavigation.d.ts.map +1 -0
  131. package/lib/slides/core/slideNavigation.js +65 -0
  132. package/lib/slides/core/slideNavigation.js.map +1 -0
  133. package/lib/slides/core/talk.d.ts +82 -0
  134. package/lib/slides/core/talk.d.ts.map +1 -0
  135. package/lib/slides/core/talk.js +134 -0
  136. package/lib/slides/core/talk.js.map +1 -0
  137. package/lib/slides/core/talkManifest.d.ts +90 -0
  138. package/lib/slides/core/talkManifest.d.ts.map +1 -0
  139. package/lib/slides/core/talkManifest.js +109 -0
  140. package/lib/slides/core/talkManifest.js.map +1 -0
  141. package/lib/slides/core/talkOrigin.d.ts +50 -0
  142. package/lib/slides/core/talkOrigin.d.ts.map +1 -0
  143. package/lib/slides/core/talkOrigin.js +62 -0
  144. package/lib/slides/core/talkOrigin.js.map +1 -0
  145. package/lib/slides/ui/BackToSlides.d.ts +26 -0
  146. package/lib/slides/ui/BackToSlides.d.ts.map +1 -0
  147. package/lib/slides/ui/BackToSlides.js +24 -0
  148. package/lib/slides/ui/BackToSlides.js.map +1 -0
  149. package/lib/slides/ui/DemoLink.d.ts +42 -0
  150. package/lib/slides/ui/DemoLink.d.ts.map +1 -0
  151. package/lib/slides/ui/DemoLink.js +17 -0
  152. package/lib/slides/ui/DemoLink.js.map +1 -0
  153. package/lib/slides/ui/EmbedSlide.d.ts +19 -0
  154. package/lib/slides/ui/EmbedSlide.d.ts.map +1 -0
  155. package/lib/slides/ui/EmbedSlide.js +56 -0
  156. package/lib/slides/ui/EmbedSlide.js.map +1 -0
  157. package/lib/slides/ui/SectionSlide.d.ts +18 -0
  158. package/lib/slides/ui/SectionSlide.d.ts.map +1 -0
  159. package/lib/slides/ui/SectionSlide.js +18 -0
  160. package/lib/slides/ui/SectionSlide.js.map +1 -0
  161. package/lib/slides/ui/Slide.d.ts +52 -0
  162. package/lib/slides/ui/Slide.d.ts.map +1 -0
  163. package/lib/slides/ui/Slide.js +58 -0
  164. package/lib/slides/ui/Slide.js.map +1 -0
  165. package/lib/slides/ui/SlideBody.d.ts +32 -0
  166. package/lib/slides/ui/SlideBody.d.ts.map +1 -0
  167. package/lib/slides/ui/SlideBody.js +27 -0
  168. package/lib/slides/ui/SlideBody.js.map +1 -0
  169. package/lib/slides/ui/Slideshow.d.ts +93 -0
  170. package/lib/slides/ui/Slideshow.d.ts.map +1 -0
  171. package/lib/slides/ui/Slideshow.js +122 -0
  172. package/lib/slides/ui/Slideshow.js.map +1 -0
  173. package/lib/slides/ui/SlideshowBar.d.ts +33 -0
  174. package/lib/slides/ui/SlideshowBar.d.ts.map +1 -0
  175. package/lib/slides/ui/SlideshowBar.js +20 -0
  176. package/lib/slides/ui/SlideshowBar.js.map +1 -0
  177. package/lib/slides/ui/TalkList.d.ts +37 -0
  178. package/lib/slides/ui/TalkList.d.ts.map +1 -0
  179. package/lib/slides/ui/TalkList.js +68 -0
  180. package/lib/slides/ui/TalkList.js.map +1 -0
  181. package/lib/slides/ui/index.d.ts +19 -0
  182. package/lib/slides/ui/index.d.ts.map +1 -0
  183. package/lib/slides/ui/index.js +10 -0
  184. package/lib/slides/ui/index.js.map +1 -0
  185. package/lib/slides/vite/index.d.ts +3 -0
  186. package/lib/slides/vite/index.d.ts.map +1 -0
  187. package/lib/slides/vite/index.js +2 -0
  188. package/lib/slides/vite/index.js.map +1 -0
  189. package/lib/slides/vite/talks.d.ts +50 -0
  190. package/lib/slides/vite/talks.d.ts.map +1 -0
  191. package/lib/slides/vite/talks.js +87 -0
  192. package/lib/slides/vite/talks.js.map +1 -0
  193. package/lib/slides.d.ts +3 -0
  194. package/lib/slides.d.ts.map +1 -0
  195. package/lib/slides.js +3 -0
  196. package/lib/slides.js.map +1 -0
  197. package/lib/structure/ui/StructureEditor.js +2 -2
  198. package/lib/structure/ui/StructureEditor.js.map +1 -1
  199. package/lib/structure/ui/StructurePlaceholder.js +1 -1
  200. package/lib/structure/ui/StructurePlaceholder.js.map +1 -1
  201. package/lib/tokens/core/checkTokens.d.ts +51 -0
  202. package/lib/tokens/core/checkTokens.d.ts.map +1 -0
  203. package/lib/tokens/core/checkTokens.js +188 -0
  204. package/lib/tokens/core/checkTokens.js.map +1 -0
  205. package/lib/tokens/core/colors.d.ts +42 -0
  206. package/lib/tokens/core/colors.d.ts.map +1 -0
  207. package/lib/tokens/core/colors.js +119 -0
  208. package/lib/tokens/core/colors.js.map +1 -0
  209. package/lib/tokens/core/index.d.ts +3 -0
  210. package/lib/tokens/core/index.d.ts.map +1 -0
  211. package/lib/tokens/core/index.js +2 -0
  212. package/lib/tokens/core/index.js.map +1 -0
  213. package/lib/tokens/core/rules.d.ts +23 -0
  214. package/lib/tokens/core/rules.d.ts.map +1 -0
  215. package/lib/tokens/core/rules.js +90 -0
  216. package/lib/tokens/core/rules.js.map +1 -0
  217. package/lib/ui.d.ts +1 -0
  218. package/lib/ui.d.ts.map +1 -1
  219. package/lib/ui.js +1 -0
  220. package/lib/ui.js.map +1 -1
  221. package/lib/vite.d.ts +1 -0
  222. package/lib/vite.d.ts.map +1 -1
  223. package/lib/vite.js +1 -0
  224. package/lib/vite.js.map +1 -1
  225. package/package.json +11 -3
  226. package/src/about/core/about.ts +176 -0
  227. package/src/about/core/index.ts +2 -0
  228. package/src/about/ui/AboutCitations.tsx +79 -0
  229. package/src/about/ui/AboutPage.tsx +177 -0
  230. package/src/about/ui/AboutSection.tsx +56 -0
  231. package/src/about/ui/index.ts +6 -0
  232. package/src/chrome/ui/index.ts +3 -0
  233. package/src/clipboard/ui/CodeBlock.tsx +4 -1
  234. package/src/color/ui/ColorScaleLegend.tsx +1 -1
  235. package/src/core.ts +5 -0
  236. package/src/credits/core/credits.ts +42 -0
  237. package/src/credits/ui/CreditsList.tsx +1 -1
  238. package/src/delimited/ui/DelimitedTextPanel.tsx +1 -1
  239. package/src/ecosystem/core/sites.ts +12 -0
  240. package/src/ecosystem/core/tokens.ts +1 -0
  241. package/src/ecosystem/ui/EcosystemLinks.tsx +2 -2
  242. package/src/ecosystem/ui/EcosystemMenu.tsx +1 -1
  243. package/src/ecosystem/ui/SiteTile.tsx +7 -5
  244. package/src/ecosystem/ui/glyphs.tsx +40 -0
  245. package/src/orbital/core/index.ts +4 -1
  246. package/src/orbital/core/realHarmonics.ts +5 -2
  247. package/src/orbital/ui/AtomicOrbitalCanvas.tsx +126 -4
  248. package/src/orbital/ui/AtomicOrbitalViewer.tsx +14 -1
  249. package/src/orbital/ui/camera.ts +81 -0
  250. package/src/orbital/ui/renderVolume.ts +8 -1
  251. package/src/orbital/ui/serial.ts +35 -0
  252. package/src/orbital/ui/viewer.ts +72 -13
  253. package/src/pedagogy/ui/ExerciseProgressHeader.tsx +1 -1
  254. package/src/pedagogy/ui/GlossaryText.tsx +2 -2
  255. package/src/pedagogy/ui/ReferenceSectionBlock.tsx +5 -2
  256. package/src/pedagogy/ui/SyntaxTooltip.tsx +2 -2
  257. package/src/pedagogy/ui/TutorialStepStrip.tsx +4 -1
  258. package/src/periodic/ui/CategoryLegend.tsx +1 -1
  259. package/src/periodic/ui/PeriodicTableChrome.tsx +2 -2
  260. package/src/seo/vite/ogCard.ts +1 -0
  261. package/src/share/ui/ShareDialogContent.tsx +5 -2
  262. package/src/share/ui/SharePartOptions.tsx +1 -1
  263. package/src/slides/core/demoLinks.ts +113 -0
  264. package/src/slides/core/index.ts +22 -0
  265. package/src/slides/core/sectionHeading.ts +29 -0
  266. package/src/slides/core/slideNavigation.ts +77 -0
  267. package/src/slides/core/talk.ts +203 -0
  268. package/src/slides/core/talkManifest.ts +158 -0
  269. package/src/slides/core/talkOrigin.ts +83 -0
  270. package/src/slides/ui/BackToSlides.tsx +49 -0
  271. package/src/slides/ui/DemoLink.tsx +67 -0
  272. package/src/slides/ui/EmbedSlide.tsx +85 -0
  273. package/src/slides/ui/SectionSlide.tsx +44 -0
  274. package/src/slides/ui/Slide.tsx +143 -0
  275. package/src/slides/ui/SlideBody.tsx +64 -0
  276. package/src/slides/ui/Slideshow.tsx +282 -0
  277. package/src/slides/ui/SlideshowBar.tsx +83 -0
  278. package/src/slides/ui/TalkList.tsx +146 -0
  279. package/src/slides/ui/index.ts +18 -0
  280. package/src/slides/vite/index.ts +2 -0
  281. package/src/slides/vite/talks.ts +126 -0
  282. package/src/slides.ts +2 -0
  283. package/src/structure/ui/StructureEditor.tsx +2 -2
  284. package/src/structure/ui/StructurePlaceholder.tsx +1 -1
  285. package/src/tokens/core/checkTokens.ts +242 -0
  286. package/src/tokens/core/colors.ts +122 -0
  287. package/src/tokens/core/index.ts +6 -0
  288. package/src/tokens/core/rules.ts +103 -0
  289. package/src/ui.ts +1 -0
  290. package/src/vite.ts +1 -0
  291. package/styles/chrome.css +1 -0
  292. package/styles/slides.css +667 -0
@@ -26,12 +26,14 @@ import {
26
26
  DEFAULT_CAMERA_DURATION,
27
27
  DEFAULT_SPIN_SPEED,
28
28
  frameOrbital,
29
+ refitOrbital,
29
30
  setSpin,
30
31
  } from './camera.ts';
31
32
  import type { AxesStyle } from './renderAxes.ts';
32
33
  import { clearOrbitalAxes, renderOrbitalAxes } from './renderAxes.ts';
33
34
  import type { VolumeStyle } from './renderVolume.ts';
34
35
  import { clearSampledVolume, renderSampledVolume } from './renderVolume.ts';
36
+ import { createSerialRunner } from './serial.ts';
35
37
 
36
38
  /** Settings fixed for the life of a viewer. */
37
39
  export interface OrbitalViewerOptions {
@@ -64,7 +66,12 @@ export function createOrbitalViewer(
64
66
  */
65
67
  export class OrbitalViewer {
66
68
  readonly #model: PluginViewModel;
69
+ // One canvas, so one operation at a time — see `serial.ts` for what
70
+ // interleaving two of them does to the scene.
71
+ readonly #serial = createSerialRunner();
67
72
  #disposed = false;
73
+ /** Extent the camera was last fitted to; `null` before it has been placed. */
74
+ #fittedRadius: number | null = null;
68
75
 
69
76
  /** Resolves once the canvas exists; every method awaits it internally. */
70
77
  readonly ready: Promise<void>;
@@ -78,7 +85,16 @@ export class OrbitalViewer {
78
85
  canvas3d: {
79
86
  ...spec.canvas3d,
80
87
  renderer: { backgroundColor: Color.fromHexStyle(background) },
81
- camera: { helper: { axes: { name: 'off', params: {} } } },
88
+ camera: {
89
+ helper: { axes: { name: 'off', params: {} } },
90
+ // The camera is ours alone. Molstar reframes itself whenever a
91
+ // scene commit leaves the renderable count at zero, which is what
92
+ // replacing one orbital by another does — the old surfaces are
93
+ // removed and committed before the new ones exist — and it frames
94
+ // the whole sampled box rather than the isosurface inside it,
95
+ // which leaves the orbital a fifth of the frame wide.
96
+ manualReset: true,
97
+ },
82
98
  },
83
99
  },
84
100
  });
@@ -159,9 +175,49 @@ export class OrbitalViewer {
159
175
  ): Promise<void> {
160
176
  return this.#run((plugin) => {
161
177
  frameOrbital(plugin, orbitalRadius, durationMs);
178
+ this.#fittedRadius = orbitalRadius ?? null;
162
179
  });
163
180
  }
164
181
 
182
+ /**
183
+ * Fit a newly drawn orbital without disturbing the view.
184
+ *
185
+ * The first call frames the orbital, since there is no view to keep yet.
186
+ * Later ones leave the rotation and the zoom the student set and only rescale
187
+ * them when the drawn extent changed — clicking through a shell must not
188
+ * throw their view away.
189
+ * @param orbitalRadius - Extent of the drawn surface, as `showOrbital`
190
+ * returned it.
191
+ * @param durationMs - Transition length; 0 jumps.
192
+ * @returns Nothing, once the camera has been set.
193
+ */
194
+ refit(
195
+ orbitalRadius?: number,
196
+ durationMs = DEFAULT_CAMERA_DURATION,
197
+ ): Promise<void> {
198
+ const fitted = this.#fittedRadius;
199
+ if (fitted === null || orbitalRadius === undefined) {
200
+ return this.frame(orbitalRadius, durationMs);
201
+ }
202
+ return this.#run((plugin) => {
203
+ refitOrbital(plugin, fitted, orbitalRadius, durationMs);
204
+ this.#fittedRadius = orbitalRadius;
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Put the camera back where the orbital was first framed from.
210
+ *
211
+ * The counterpart of `refit`: since a view is now kept across a change of
212
+ * orbital, a student who has turned the atom into an unreadable angle needs
213
+ * one way back.
214
+ * @param durationMs - Transition length; 0 jumps.
215
+ * @returns Nothing, once the camera has been set.
216
+ */
217
+ resetView(durationMs = DEFAULT_CAMERA_DURATION): Promise<void> {
218
+ return this.frame(this.#fittedRadius ?? undefined, durationMs);
219
+ }
220
+
165
221
  /**
166
222
  * Turn the automatic spin on or off.
167
223
  * @param spinning - Whether the scene should keep turning.
@@ -199,23 +255,26 @@ export class OrbitalViewer {
199
255
  }
200
256
 
201
257
  /**
202
- * Wait for initialisation, then run `action` on the plugin. Resolves to
203
- * `undefined` when the viewer was disposed, before the call or while `action`
204
- * was still running; an initialisation failure still reaches the caller.
258
+ * Wait for the operations queued before this one and for initialisation,
259
+ * then run `action` on the plugin. Resolves to `undefined` when the viewer
260
+ * was disposed, before the call or while `action` was still running; an
261
+ * initialisation failure still reaches the caller.
205
262
  * @param action - What to run once the canvas is ready.
206
263
  * @returns What `action` returned, or `undefined` when disposed.
207
264
  */
208
- async #run<Result>(
265
+ #run<Result>(
209
266
  action: (plugin: PluginContext) => Result | Promise<Result>,
210
267
  ): Promise<Result | undefined> {
211
- if (this.#disposed) return undefined;
212
- await this.ready;
213
- if (this.#disposed) return undefined;
214
- try {
215
- return await action(this.#model.plugin);
216
- } catch (error) {
268
+ return this.#serial.run(async () => {
217
269
  if (this.#disposed) return undefined;
218
- throw error;
219
- }
270
+ await this.ready;
271
+ if (this.#disposed) return undefined;
272
+ try {
273
+ return await action(this.#model.plugin);
274
+ } catch (error) {
275
+ if (this.#disposed) return undefined;
276
+ throw error;
277
+ }
278
+ });
220
279
  }
221
280
  }
@@ -129,7 +129,7 @@ const ROW_STYLE: CSSProperties = {
129
129
  const COUNT_STYLE: CSSProperties = { fontWeight: 600 };
130
130
 
131
131
  const PERCENT_STYLE: CSSProperties = {
132
- color: '#5b6875',
132
+ color: 'var(--text-muted, #5b6875)',
133
133
  flex: '1 1 auto',
134
134
  fontSize: 12,
135
135
  };
@@ -140,7 +140,7 @@ const TITLE_STYLE: CSSProperties = { fontWeight: 600, fontSize: 13 };
140
140
  const SUMMARY_STYLE: CSSProperties = {
141
141
  fontSize: 12,
142
142
  lineHeight: 1.45,
143
- color: '#d3d8de',
143
+ color: 'var(--border, #d3d8de)',
144
144
  };
145
145
 
146
146
  const LIST_STYLE: CSSProperties = {
@@ -159,6 +159,6 @@ const CODE_STYLE: CSSProperties = {
159
159
  color: '#8abbff',
160
160
  };
161
161
 
162
- const INPUT_STYLE: CSSProperties = { color: '#f6f7f9' };
162
+ const INPUT_STYLE: CSSProperties = { color: 'var(--surface-sunken, #f6f7f9)' };
163
163
 
164
164
  const NOTE_STYLE: CSSProperties = { fontStyle: 'italic', color: '#abb3bf' };
@@ -140,7 +140,10 @@ function syntaxStyle(
140
140
  fontWeight: 600,
141
141
  color: '#1c2127',
142
142
  ...(interactive
143
- ? { borderBottom: '1px dotted #5b6875', alignSelf: 'flex-start' }
143
+ ? {
144
+ borderBottom: '1px dotted var(--text-muted, #5b6875)',
145
+ alignSelf: 'flex-start',
146
+ }
144
147
  : {}),
145
148
  };
146
149
  }
@@ -153,7 +156,7 @@ const SECTION_STYLE: CSSProperties = {
153
156
 
154
157
  const INTRO_STYLE: CSSProperties = {
155
158
  margin: '0 0 4px',
156
- color: '#5b6875',
159
+ color: 'var(--text-muted, #5b6875)',
157
160
  fontSize: 12,
158
161
  };
159
162
 
@@ -191,7 +191,7 @@ const TAG_STYLE: CSSProperties = { color: '#abb3bf', fontSize: 11 };
191
191
  const SUMMARY_STYLE: CSSProperties = { fontSize: 12, lineHeight: 1.4 };
192
192
 
193
193
  const DETAIL_STYLE: CSSProperties = {
194
- color: '#d3d8de',
194
+ color: 'var(--border, #d3d8de)',
195
195
  fontSize: 12,
196
196
  lineHeight: 1.45,
197
197
  };
@@ -209,7 +209,7 @@ const ROW_STYLE: CSSProperties = { display: 'flex', gap: 6, fontSize: 12 };
209
209
  const LABEL_STYLE: CSSProperties = { color: '#abb3bf', flex: '0 0 68px' };
210
210
 
211
211
  const VALUE_STYLE: CSSProperties = {
212
- color: '#f6f7f9',
212
+ color: 'var(--surface-sunken, #f6f7f9)',
213
213
  fontFamily: MONOSPACE,
214
214
  overflowWrap: 'anywhere',
215
215
  };
@@ -253,4 +253,7 @@ const PAGER_STYLE: CSSProperties = {
253
253
  paddingTop: 2,
254
254
  };
255
255
 
256
- const HINT_STYLE: CSSProperties = { color: '#5b6875', fontSize: 11 };
256
+ const HINT_STYLE: CSSProperties = {
257
+ color: 'var(--text-muted, #5b6875)',
258
+ fontSize: 11,
259
+ };
@@ -74,7 +74,7 @@ export function CategoryLegend(props: CategoryLegendProps): ReactElement {
74
74
  }
75
75
 
76
76
  const legendStyle = {
77
- color: 'rgb(95 107 124)',
77
+ color: 'var(--text-muted, rgb(95 107 124))',
78
78
  display: 'flex',
79
79
  flexWrap: 'wrap',
80
80
  fontSize: 11,
@@ -145,7 +145,7 @@ function HeaderCell(props: HeaderCellProps): ReactElement {
145
145
 
146
146
  const headerStyle = {
147
147
  alignItems: 'center',
148
- color: 'rgb(95 107 124)',
148
+ color: 'var(--text-muted, rgb(95 107 124))',
149
149
  display: 'flex',
150
150
  fontSize: 'clamp(0.4rem, 1.9cqw, 0.62rem)',
151
151
  justifyContent: 'center',
@@ -163,7 +163,7 @@ const markerStyle = {
163
163
  alignItems: 'center',
164
164
  border: '1px dashed rgb(182 191 204)',
165
165
  borderRadius: 3,
166
- color: 'rgb(95 107 124)',
166
+ color: 'var(--text-muted, rgb(95 107 124))',
167
167
  display: 'flex',
168
168
  fontSize: 'clamp(0.36rem, 1.7cqw, 0.55rem)',
169
169
  justifyContent: 'center',
@@ -1,3 +1,4 @@
1
+ // tokens-ok: file — the card is an image built outside any page, so it has no tokens to read.
1
2
  /**
2
3
  * The 1200×630 card a link to a site unfurls into, as a page to screenshot.
3
4
  *
@@ -28,7 +28,10 @@ import type { ShareDialogProps, ShareDraft } from './ShareDialog.tsx';
28
28
  import { SharePartOptions } from './SharePartOptions.tsx';
29
29
  import { withPart } from './draft.ts';
30
30
 
31
- const LEAD_STYLE: CSSProperties = { marginTop: 0, color: '#5b6875' };
31
+ const LEAD_STYLE: CSSProperties = {
32
+ marginTop: 0,
33
+ color: 'var(--text-muted, #5b6875)',
34
+ };
32
35
  const SECTION_STYLE: CSSProperties = { marginBottom: 18 };
33
36
  const ACTIONS_STYLE: CSSProperties = {
34
37
  display: 'flex',
@@ -39,7 +42,7 @@ const ACTIONS_STYLE: CSSProperties = {
39
42
  const HINT_STYLE: CSSProperties = {
40
43
  display: 'block',
41
44
  marginLeft: 26,
42
- color: '#5b6875',
45
+ color: 'var(--text-muted, #5b6875)',
43
46
  fontSize: 12,
44
47
  };
45
48
 
@@ -8,7 +8,7 @@ const OPTION_STYLE: CSSProperties = { marginBottom: 8 };
8
8
  const DESCRIPTION_STYLE: CSSProperties = {
9
9
  display: 'block',
10
10
  marginLeft: 26,
11
- color: '#5b6875',
11
+ color: 'var(--text-muted, #5b6875)',
12
12
  fontSize: 12,
13
13
  };
14
14
 
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The links a slide hands over to the live tool.
3
+ *
4
+ * A demo link is a paragraph that is exactly one Markdown link to an in-app
5
+ * route. It is parsed by hand rather than by a Markdown library because the
6
+ * labels are chemistry — `[M+H]⁺` opens a bracket the label closes itself, and
7
+ * a formula such as `H(OCH2CH2)10` puts parentheses inside the URL — and both
8
+ * defeat a naive `\[(.*)\]\((.*)\)` pattern.
9
+ */
10
+
11
+ /** One link a slide offers as a live demo. */
12
+ export interface DemoLinkSpec {
13
+ /** The link text, which may itself contain balanced brackets. */
14
+ label: string;
15
+ /** Where it goes; a demo link always targets an in-app route. */
16
+ href: string;
17
+ }
18
+
19
+ /**
20
+ * Read a paragraph that is exactly one Markdown link `[label](url)` and nothing
21
+ * else, counting brackets so a label may hold its own pair, and accepting the
22
+ * angle-bracketed URL form `[label](<url>)` for a URL holding parentheses.
23
+ * @param text - The paragraph text.
24
+ * @returns The link, or `null` when the paragraph is anything else.
25
+ */
26
+ export function parseSingleLink(text: string): DemoLinkSpec | null {
27
+ const source = text.trim();
28
+ if (!source.startsWith('[')) return null;
29
+
30
+ let depth = 0;
31
+ let labelEnd = -1;
32
+ for (let index = 0; index < source.length; index++) {
33
+ const char = source[index];
34
+ if (char === '[') {
35
+ depth++;
36
+ } else if (char === ']') {
37
+ depth--;
38
+ if (depth === 0) {
39
+ labelEnd = index;
40
+ break;
41
+ }
42
+ }
43
+ }
44
+ if (labelEnd === -1 || source[labelEnd + 1] !== '(') return null;
45
+
46
+ const label = source.slice(1, labelEnd);
47
+ let rest = source.slice(labelEnd + 2);
48
+ let href: string;
49
+
50
+ if (rest.startsWith('<')) {
51
+ const close = rest.indexOf('>');
52
+ if (close === -1 || rest[close + 1] !== ')') return null;
53
+ href = rest.slice(1, close);
54
+ rest = rest.slice(close + 2);
55
+ } else {
56
+ let parenDepth = 1;
57
+ let urlEnd = -1;
58
+ for (let index = 0; index < rest.length; index++) {
59
+ const char = rest[index];
60
+ if (char === '(') {
61
+ parenDepth++;
62
+ } else if (char === ')') {
63
+ parenDepth--;
64
+ if (parenDepth === 0) {
65
+ urlEnd = index;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ if (urlEnd === -1) return null;
71
+ href = rest.slice(0, urlEnd);
72
+ rest = rest.slice(urlEnd + 1);
73
+ }
74
+
75
+ if (rest.trim() !== '') return null;
76
+ return { label, href };
77
+ }
78
+
79
+ /**
80
+ * Split a slide body into its Markdown and the run of demo links that closes
81
+ * it. External links and ordinary paragraphs stay in the body, so a slide that
82
+ * merely cites a URL keeps it where the author wrote it.
83
+ * @param body - The slide Markdown body.
84
+ * @returns The body without its trailing demo links, and those links in order.
85
+ */
86
+ export function splitDemoLinks(body: string): {
87
+ body: string;
88
+ demos: DemoLinkSpec[];
89
+ } {
90
+ const blocks = body.split(/\n{2,}/);
91
+ let firstDemo = blocks.length;
92
+ const demos: DemoLinkSpec[] = [];
93
+ for (let index = blocks.length - 1; index >= 0; index--) {
94
+ const link = parseSingleLink(blocks[index] ?? '');
95
+ if (!link?.href.startsWith('/')) break;
96
+ demos.unshift(link);
97
+ firstDemo = index;
98
+ }
99
+ return { body: blocks.slice(0, firstDemo).join('\n\n').trim(), demos };
100
+ }
101
+
102
+ /**
103
+ * The displayed form of a demo link label: the `Demo:` prefix an author writes
104
+ * to mark the paragraph is dropped, and the first character is uppercased.
105
+ * @param label - The raw link label.
106
+ * @returns What the button shows.
107
+ */
108
+ export function demoLabel(label: string): string {
109
+ const text = label.replace(DEMO_PREFIX, '');
110
+ return text.charAt(0).toUpperCase() + text.slice(1);
111
+ }
112
+
113
+ const DEMO_PREFIX = /^Demo:\s*/i;
@@ -0,0 +1,22 @@
1
+ export type { DemoLinkSpec } from './demoLinks.ts';
2
+ export { demoLabel, parseSingleLink, splitDemoLinks } from './demoLinks.ts';
3
+ export type { SectionHeading } from './sectionHeading.ts';
4
+ export { parseSectionHeading } from './sectionHeading.ts';
5
+ export type { SlideAction } from './slideNavigation.ts';
6
+ export { clampSlideIndex, slideActionForKey } from './slideNavigation.ts';
7
+ export type { Slide, Talk, TalkMeta } from './talk.ts';
8
+ export { parseTalk } from './talk.ts';
9
+ export type { TalkManifest, TalkSummary } from './talkManifest.ts';
10
+ export {
11
+ buildTalkManifest,
12
+ parseTalkManifest,
13
+ talkSourceUrl,
14
+ talkSummary,
15
+ } from './talkManifest.ts';
16
+ export type { TalkOrigin } from './talkOrigin.ts';
17
+ export {
18
+ TALK_ORIGIN_PARAM,
19
+ formatTalkOrigin,
20
+ parseTalkOrigin,
21
+ withTalkOrigin,
22
+ } from './talkOrigin.ts';
@@ -0,0 +1,29 @@
1
+ /** The parts of a section divider's heading. */
2
+ export interface SectionHeading {
3
+ /** The section number, as written; empty when the heading carries none. */
4
+ number: string;
5
+ /** The section title, without its number; empty when the heading carries none. */
6
+ title: string;
7
+ /** Whatever the body holds besides that heading. */
8
+ rest: string;
9
+ }
10
+
11
+ /**
12
+ * Split a section-divider body into its leading number, its title and the rest
13
+ * of the Markdown, so a template can set the number apart from the title.
14
+ * `·`, `.`, `:`, `—` and `-` all separate the two.
15
+ * @param body - Markdown body, normally starting with `# N · Title`.
16
+ * @returns The parsed heading; a body with no numbered heading keeps all of
17
+ * itself in `rest`, leaving `number` and `title` empty.
18
+ */
19
+ export function parseSectionHeading(body: string): SectionHeading {
20
+ const match = HEAD_RE.exec(body);
21
+ const number = match?.groups?.number;
22
+ const title = match?.groups?.title;
23
+ if (match === null || number === undefined || title === undefined) {
24
+ return { number: '', title: '', rest: body.trim() };
25
+ }
26
+ return { number, title, rest: body.replace(match[0], '').trim() };
27
+ }
28
+
29
+ const HEAD_RE = /^#\s+(?<number>\d+)\s*[.:·—-]\s*(?<title>.+?)\s*$/m;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * What a key does during a presentation, decided without a DOM.
3
+ *
4
+ * The current slide lives in the address, so it arrives as whatever a link
5
+ * carried — a missing number, a negative one, one past the end of a talk that
6
+ * has since been shortened. Every move is therefore clamped here rather than
7
+ * trusted, and the player only ever renders a slide that exists.
8
+ */
9
+
10
+ /**
11
+ * The slide a requested index resolves to.
12
+ * @param index - The index asked for, from a link or a keystroke.
13
+ * @param total - How many slides the talk holds.
14
+ * @returns A zero-based index that exists, or `0` for a talk with no slides.
15
+ */
16
+ export function clampSlideIndex(index: number, total: number): number {
17
+ if (!Number.isFinite(total) || total < 1) return 0;
18
+ if (Number.isNaN(index) || index < 0) return 0;
19
+ const last = total - 1;
20
+ return index > last ? last : Math.trunc(index);
21
+ }
22
+
23
+ /** What a key press asks the player to do. */
24
+ export type SlideAction =
25
+ | { kind: 'go'; index: number }
26
+ | { kind: 'blank'; color: 'black' | 'white' }
27
+ | { kind: 'fullscreen' }
28
+ | { kind: 'notes' }
29
+ | null;
30
+
31
+ /**
32
+ * The action a key press means, with every move already clamped: stepping past
33
+ * the last slide, or back from the first, stays on the slide it is on rather
34
+ * than leaving the talk.
35
+ * @param key - The `key` of the keyboard event.
36
+ * @param index - Which slide is showing.
37
+ * @param total - How many slides the talk holds.
38
+ * @returns The action, or `null` when the key means nothing here.
39
+ */
40
+ export function slideActionForKey(
41
+ key: string,
42
+ index: number,
43
+ total: number,
44
+ ): SlideAction {
45
+ switch (key) {
46
+ case 'ArrowRight':
47
+ case 'ArrowDown':
48
+ case 'PageDown':
49
+ case ' ':
50
+ return { kind: 'go', index: clampSlideIndex(index + 1, total) };
51
+ case 'ArrowLeft':
52
+ case 'ArrowUp':
53
+ case 'PageUp':
54
+ return { kind: 'go', index: clampSlideIndex(index - 1, total) };
55
+ case 'Home':
56
+ return { kind: 'go', index: 0 };
57
+ case 'End':
58
+ return { kind: 'go', index: clampSlideIndex(total - 1, total) };
59
+ default:
60
+ return letterAction(key);
61
+ }
62
+ }
63
+
64
+ function letterAction(key: string): SlideAction {
65
+ switch (key.toLowerCase()) {
66
+ case 'f':
67
+ return { kind: 'fullscreen' };
68
+ case 'b':
69
+ return { kind: 'blank', color: 'black' };
70
+ case 'w':
71
+ return { kind: 'blank', color: 'white' };
72
+ case 'n':
73
+ return { kind: 'notes' };
74
+ default:
75
+ return null;
76
+ }
77
+ }