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
@@ -0,0 +1,143 @@
1
+ import type { ComponentType, ReactElement } from 'react';
2
+ import { Fragment } from 'react';
3
+
4
+ import type {
5
+ Slide as SlideData,
6
+ TalkMeta,
7
+ TalkOrigin,
8
+ } from '../core/index.ts';
9
+ import { demoLabel, splitDemoLinks } from '../core/index.ts';
10
+
11
+ import type { RenderSlideLink } from './DemoLink.tsx';
12
+ import { DemoLink } from './DemoLink.tsx';
13
+ import { EmbedSlide } from './EmbedSlide.tsx';
14
+ import { SectionSlide } from './SectionSlide.tsx';
15
+ import { SlideBody } from './SlideBody.tsx';
16
+
17
+ /** What every layout — the generic ones and a site's own — is handed. */
18
+ export interface SlideLayoutProps {
19
+ /** The slide to draw. */
20
+ slide: SlideData;
21
+ /** How the publishing site names the talk in its addresses. */
22
+ talkId: string;
23
+ /** Which slide of the talk this is, zero-based. */
24
+ slideIndex: number;
25
+ /**
26
+ * Which site published the deck, written into the way back.
27
+ * @default undefined — the site playing it
28
+ */
29
+ site?: string;
30
+ /**
31
+ * Where the publishing site lives, which a relative address in the deck is
32
+ * resolved against.
33
+ * @default undefined — the addresses are this site's own
34
+ */
35
+ talkOrigin?: string;
36
+ /** What the talk's front-matter said, for a layout that draws from it. */
37
+ meta: TalkMeta;
38
+ /**
39
+ * The site's own link, used for the in-app links of the slide.
40
+ * @default undefined — a plain anchor
41
+ */
42
+ renderLink?: RenderSlideLink;
43
+ }
44
+
45
+ /** A slide, and the layouts available to draw it. */
46
+ export interface SlideViewProps extends SlideLayoutProps {
47
+ /**
48
+ * The layouts this site adds, by the name a slide writes in its
49
+ * `<!-- layout: … -->` marker. An entry here also replaces a generic layout
50
+ * of the same name.
51
+ * @default undefined
52
+ */
53
+ layouts?: Record<string, ComponentType<SlideLayoutProps>>;
54
+ }
55
+
56
+ /**
57
+ * Draw one slide with the layout it asks for.
58
+ *
59
+ * The layout name is not a closed list — a deck is written before the site
60
+ * knows how to draw it — so a name neither the site nor the player recognises
61
+ * falls back to the plain content layout. A talk must never fail to open
62
+ * because one of its slides asks for something that is not there yet.
63
+ * @param props - See {@link SlideViewProps}.
64
+ * @returns The slide.
65
+ */
66
+ export function SlideView(props: SlideViewProps): ReactElement {
67
+ const { layouts, ...layoutProps } = props;
68
+ const { slide } = layoutProps;
69
+
70
+ const Custom = layouts?.[slide.layout];
71
+ if (Custom !== undefined) return <Custom {...layoutProps} />;
72
+ if (slide.layout === 'section') return <SectionSlide body={slide.body} />;
73
+ if (slide.layout === 'embed') return <EmbedSlide slide={slide} />;
74
+
75
+ return <GenericSlide {...layoutProps} />;
76
+ }
77
+
78
+ /** The layouts the player draws itself, for any deck on any site. */
79
+ const GENERIC_LAYOUTS = new Set(['content', 'title', 'quote', 'thanks']);
80
+
81
+ function GenericSlide(props: SlideLayoutProps): ReactElement {
82
+ const { slide, talkId, slideIndex, meta, renderLink, site, talkOrigin } =
83
+ props;
84
+ const layout = GENERIC_LAYOUTS.has(slide.layout) ? slide.layout : 'content';
85
+ const { body, demos } = splitDemoLinks(slide.body);
86
+ const origin: TalkOrigin =
87
+ site === undefined
88
+ ? { talkId, slide: slideIndex }
89
+ : { talkId, slide: slideIndex, site };
90
+ const logos = layout === 'title' ? (meta.logos ?? []) : [];
91
+
92
+ return (
93
+ <div className={`slide slide--${layout}`}>
94
+ <div className="slide-content">
95
+ <SlideBody body={body} origin={origin} renderLink={renderLink} />
96
+ {logos.length === 0 ? null : (
97
+ <div className="slide-logos">
98
+ {logos.map((logo) => (
99
+ <img key={logo} className="slide-logo" src={logo} alt="" />
100
+ ))}
101
+ </div>
102
+ )}
103
+ {demos.length === 0 ? null : (
104
+ <div className="slide-demos">
105
+ <span className="slide-demos-label">Live demos:</span>
106
+ {demos.map((demo, index) => (
107
+ <Fragment key={demo.href}>
108
+ {index === 0 ? null : (
109
+ <span className="slide-demos-separator" aria-hidden="true">
110
+ |
111
+ </span>
112
+ )}
113
+ <DemoLink
114
+ href={resolveDemoHref(demo.href, talkOrigin)}
115
+ origin={origin}
116
+ renderLink={renderLink}
117
+ >
118
+ {demoLabel(demo.label)}
119
+ </DemoLink>
120
+ </Fragment>
121
+ ))}
122
+ </div>
123
+ )}
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Where a demo link actually points.
131
+ *
132
+ * A deck writes `/mf-finder?mass=300`, meaning its own site. Played there it is
133
+ * exactly that; played on another site of the family it has to be resolved
134
+ * against the site that wrote it, or the link lands on a page that does not
135
+ * exist.
136
+ * @param href - The address the slide wrote.
137
+ * @param talkOrigin - Where the publishing site lives, when it is not this one.
138
+ * @returns The address to open.
139
+ */
140
+ function resolveDemoHref(href: string, talkOrigin?: string): string {
141
+ if (talkOrigin === undefined || !href.startsWith('/')) return href;
142
+ return `${talkOrigin.replace(/\/$/, '')}${href}`;
143
+ }
@@ -0,0 +1,64 @@
1
+ import type { ReactElement } from 'react';
2
+ import Markdown from 'react-markdown';
3
+ import rehypeRaw from 'rehype-raw';
4
+
5
+ import type { TalkOrigin } from '../core/index.ts';
6
+
7
+ import type { RenderSlideLink } from './DemoLink.tsx';
8
+ import { DemoLink } from './DemoLink.tsx';
9
+
10
+ /** The Markdown of a slide, and where its links go. */
11
+ export interface SlideBodyProps {
12
+ /** The Markdown source of the slide, or of one part of it. */
13
+ body: string;
14
+ /**
15
+ * Which slide the body belongs to, carried by every in-app link it holds so
16
+ * the tool a demo opens can offer the way back.
17
+ * @default undefined
18
+ */
19
+ origin?: TalkOrigin;
20
+ /**
21
+ * The site's own link, used for the in-app links of the body.
22
+ * @default undefined — a plain anchor
23
+ */
24
+ renderLink?: RenderSlideLink;
25
+ }
26
+
27
+ /**
28
+ * Render a slide's Markdown.
29
+ *
30
+ * A talk is written as prose, so raw HTML in the source is kept: a slide
31
+ * routinely holds a `<sub>`, a `<br>` or a figure the Markdown syntax cannot
32
+ * express. A link to an in-app route becomes a demo link; every other link
33
+ * leaves the deck in a tab of its own, so a click during a talk never loses the
34
+ * slide it was made from.
35
+ * @param props - See {@link SlideBodyProps}.
36
+ * @returns The rendered body.
37
+ */
38
+ export function SlideBody(props: SlideBodyProps): ReactElement {
39
+ const { body, origin, renderLink } = props;
40
+
41
+ return (
42
+ <Markdown
43
+ rehypePlugins={[rehypeRaw]}
44
+ components={{
45
+ a({ href, children }) {
46
+ if (href?.startsWith('/') === true) {
47
+ return (
48
+ <DemoLink href={href} origin={origin} renderLink={renderLink}>
49
+ {children}
50
+ </DemoLink>
51
+ );
52
+ }
53
+ return (
54
+ <a href={href} target="_blank" rel="noreferrer">
55
+ {children}
56
+ </a>
57
+ );
58
+ },
59
+ }}
60
+ >
61
+ {body}
62
+ </Markdown>
63
+ );
64
+ }
@@ -0,0 +1,282 @@
1
+ import type {
2
+ CSSProperties,
3
+ ComponentType,
4
+ MouseEvent,
5
+ ReactElement,
6
+ } from 'react';
7
+ import { useCallback, useEffect, useRef, useState } from 'react';
8
+
9
+ import { useContainerSize } from '../../hooks/ui/useContainerSize.ts';
10
+ import type { Talk } from '../core/index.ts';
11
+ import { clampSlideIndex, slideActionForKey } from '../core/index.ts';
12
+
13
+ import type { RenderSlideLink } from './DemoLink.tsx';
14
+ import type { SlideLayoutProps } from './Slide.tsx';
15
+ import { SlideView } from './Slide.tsx';
16
+ import { SlideshowBar } from './SlideshowBar.tsx';
17
+
18
+ /** The talk being presented, and everything the site keeps a hand on. */
19
+ export interface SlideshowProps {
20
+ /** The parsed talk. */
21
+ talk: Talk;
22
+ /** Which slide is showing, zero-based; clamped into the talk before drawing. */
23
+ index: number;
24
+ /** Called with the slide to show; the site writes it wherever it keeps it. */
25
+ onIndex: (index: number) => void;
26
+ /**
27
+ * Called when the presenter leaves the talk. The bar carries no Exit button
28
+ * when it is left out.
29
+ * @default undefined
30
+ */
31
+ onExit?: () => void;
32
+ /**
33
+ * The layouts this site adds, by the name a slide writes in its
34
+ * `<!-- layout: … -->` marker.
35
+ * @default undefined
36
+ */
37
+ layouts?: Record<string, ComponentType<SlideLayoutProps>>;
38
+ /**
39
+ * The site's own link, used for the in-app links the slides hold.
40
+ * @default undefined — a plain anchor
41
+ */
42
+ renderLink?: RenderSlideLink;
43
+ /**
44
+ * How the publishing site names this talk in its addresses, carried by every
45
+ * demo link so the tool it opens can offer the way back.
46
+ * @default '' — the demo links carry no talk
47
+ */
48
+ talkId?: string;
49
+ /**
50
+ * Which site published the deck. A deck played somewhere else — as
51
+ * learn.cheminfo.org plays the family's — says so in the way back, so the
52
+ * link returns to the player it came from rather than to this one.
53
+ * @default undefined — the deck belongs to the site playing it
54
+ */
55
+ site?: string;
56
+ /**
57
+ * Where the publishing site lives, e.g. `https://www.chemcalc.org`. A deck
58
+ * written for one site holds addresses relative to it, so playing it
59
+ * elsewhere has to resolve them against their own origin rather than against
60
+ * the page.
61
+ * @default undefined — the addresses are this site's own
62
+ */
63
+ talkOrigin?: string;
64
+ /**
65
+ * What the bar reads.
66
+ * @default the talk's own title
67
+ */
68
+ title?: string;
69
+ /**
70
+ * Class the player carries, in addition to `slideshow`.
71
+ * @default undefined
72
+ */
73
+ className?: string;
74
+ /**
75
+ * Fullscreen owned by the site rather than by the player.
76
+ *
77
+ * A deck that links into the live tool wants the whole app fullscreen, not
78
+ * the player: the player unmounts on the way to the demo, and the browser
79
+ * would leave fullscreen with it. A site in that shape passes what it
80
+ * fullscreens and how to toggle it, and the player's key and button drive
81
+ * that instead of their own element.
82
+ * @default undefined — the player fullscreens itself
83
+ */
84
+ fullscreen?: {
85
+ /** Whether the site is presenting. */
86
+ isFullscreen: boolean;
87
+ /** Called when `f` or the bar's button asks to change that. */
88
+ onToggle: () => void;
89
+ };
90
+ }
91
+
92
+ /** Slides are authored on this canvas, and it is scaled to whatever it is played on. */
93
+ const SLIDE_WIDTH = 1920;
94
+ const SLIDE_HEIGHT = 1080;
95
+
96
+ /**
97
+ * The conference player: one talk, one slide at a time, on a canvas scaled to
98
+ * fill whatever screen it is given.
99
+ *
100
+ * The index is controlled, because the site owns the address — the slide lives
101
+ * in a query parameter so a demo link can return to the exact one — and the
102
+ * player therefore never touches history itself.
103
+ *
104
+ * Arrow keys, space and PageUp/PageDown navigate, `Home` and `End` jump to the
105
+ * ends, `f` toggles fullscreen, `b` and `w` blank the screen to black and to
106
+ * white the way a projector remote does, and `n` shows the presenter's notes.
107
+ * @param props - See {@link SlideshowProps}.
108
+ * @returns The player.
109
+ */
110
+ export function Slideshow(props: SlideshowProps): ReactElement {
111
+ const {
112
+ talk,
113
+ index,
114
+ onIndex,
115
+ onExit,
116
+ layouts,
117
+ renderLink,
118
+ talkId = '',
119
+ site,
120
+ talkOrigin,
121
+ fullscreen,
122
+ title,
123
+ className,
124
+ } = props;
125
+
126
+ const total = talk.slides.length;
127
+ const current = clampSlideIndex(index, total);
128
+ const slide = talk.slides[current];
129
+
130
+ const playerRef = useRef<HTMLDivElement>(null);
131
+ const stageRef = useRef<HTMLDivElement>(null);
132
+ const { width, height } = useContainerSize(stageRef);
133
+ const scale =
134
+ width === 0 || height === 0
135
+ ? 1
136
+ : Math.min(width / SLIDE_WIDTH, height / SLIDE_HEIGHT);
137
+
138
+ const [blank, setBlank] = useState<'black' | 'white' | null>(null);
139
+ const [showNotes, setShowNotes] = useState(false);
140
+ const [isFullscreen, setIsFullscreen] = useState(false);
141
+
142
+ const goTo = useCallback(
143
+ (next: number) => {
144
+ setBlank(null);
145
+ onIndex(clampSlideIndex(next, total));
146
+ },
147
+ [onIndex, total],
148
+ );
149
+
150
+ const ownFullscreen = fullscreen === undefined;
151
+ const toggleOwn = useCallback(() => {
152
+ const element = playerRef.current;
153
+ if (element === null) return;
154
+ if (document.fullscreenElement === element) {
155
+ void document.exitFullscreen().catch(() => undefined);
156
+ } else {
157
+ void element.requestFullscreen().catch(() => undefined);
158
+ }
159
+ }, []);
160
+ const toggleFullscreen = fullscreen?.onToggle ?? toggleOwn;
161
+
162
+ useEffect(() => {
163
+ if (!ownFullscreen) return;
164
+ function onFullscreenChange(): void {
165
+ setIsFullscreen(document.fullscreenElement === playerRef.current);
166
+ }
167
+ document.addEventListener('fullscreenchange', onFullscreenChange);
168
+ return () => {
169
+ document.removeEventListener('fullscreenchange', onFullscreenChange);
170
+ };
171
+ }, [ownFullscreen]);
172
+
173
+ useEffect(() => {
174
+ function onKeyDown(event: KeyboardEvent): void {
175
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
176
+ // A player embedded in a page shares the document with its fields, and
177
+ // space is both "next slide" and a character.
178
+ if (isTextEntry(event.target)) return;
179
+ const action = slideActionForKey(event.key, current, total);
180
+ if (action === null) return;
181
+ event.preventDefault();
182
+ switch (action.kind) {
183
+ case 'go':
184
+ goTo(action.index);
185
+ break;
186
+ case 'blank':
187
+ setBlank((shown) => (shown === action.color ? null : action.color));
188
+ break;
189
+ case 'fullscreen':
190
+ toggleFullscreen();
191
+ break;
192
+ case 'notes':
193
+ setShowNotes((shown) => !shown);
194
+ break;
195
+ default:
196
+ break;
197
+ }
198
+ }
199
+ document.addEventListener('keydown', onKeyDown);
200
+ return () => {
201
+ document.removeEventListener('keydown', onKeyDown);
202
+ };
203
+ }, [current, total, goTo, toggleFullscreen]);
204
+
205
+ function onStageClick(event: MouseEvent<HTMLDivElement>): void {
206
+ // A demo link or a control handles its own click without also stepping on.
207
+ const target = event.target as Element | null;
208
+ if (target !== null && target.closest('a, button') !== null) return;
209
+ goTo(current + 1);
210
+ }
211
+
212
+ const progress = total > 1 ? (current / (total - 1)) * 100 : 100;
213
+
214
+ return (
215
+ <div
216
+ ref={playerRef}
217
+ className={
218
+ className === undefined ? 'slideshow' : `slideshow ${className}`
219
+ }
220
+ >
221
+ {/* Clicking the stage steps on, the way a projector remote does; the
222
+ keyboard equivalent is the arrow keys the player already listens for. */}
223
+ <div className="slideshow-stage" ref={stageRef} onClick={onStageClick}>
224
+ <div className="slide-canvas" style={{ transform: `scale(${scale})` }}>
225
+ {slide === undefined ? null : (
226
+ // Keyed on the index so an animated slide replays from its start
227
+ // every time it is navigated to.
228
+ <SlideView
229
+ key={current}
230
+ slide={slide}
231
+ talkId={talkId}
232
+ site={site}
233
+ talkOrigin={talkOrigin}
234
+ slideIndex={current}
235
+ meta={talk.meta}
236
+ layouts={layouts}
237
+ renderLink={renderLink}
238
+ />
239
+ )}
240
+ </div>
241
+ {blank === null ? null : (
242
+ // Clicking the blank screen brings the slide back, as does pressing
243
+ // the key that blanked it.
244
+ <div
245
+ className={`slideshow-blank slideshow-blank--${blank}`}
246
+ onClick={(event) => {
247
+ event.stopPropagation();
248
+ setBlank(null);
249
+ }}
250
+ />
251
+ )}
252
+ </div>
253
+ {showNotes && slide?.notes !== undefined ? (
254
+ <div className="slideshow-notes no-print">{slide.notes}</div>
255
+ ) : null}
256
+ <SlideshowBar
257
+ title={title ?? talk.meta.title}
258
+ index={current}
259
+ total={total}
260
+ isFullscreen={fullscreen?.isFullscreen ?? isFullscreen}
261
+ onGo={goTo}
262
+ onToggleFullscreen={toggleFullscreen}
263
+ onExit={onExit}
264
+ />
265
+ <div
266
+ className="slideshow-progress"
267
+ style={{ '--progress': `${progress}%` } as CSSProperties}
268
+ />
269
+ </div>
270
+ );
271
+ }
272
+
273
+ const TEXT_ENTRY_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
274
+
275
+ function isTextEntry(target: EventTarget | null): boolean {
276
+ if (target === null) return false;
277
+ const element = target as { tagName?: unknown; isContentEditable?: unknown };
278
+ if (element.isContentEditable === true) return true;
279
+ return (
280
+ typeof element.tagName === 'string' && TEXT_ENTRY_TAGS.has(element.tagName)
281
+ );
282
+ }
@@ -0,0 +1,83 @@
1
+ import { Button } from '@blueprintjs/core';
2
+ import type { ReactElement } from 'react';
3
+
4
+ /** Where the talk stands, and what the presenter may do about it. */
5
+ export interface SlideshowBarProps {
6
+ /** What the bar reads. */
7
+ title: string;
8
+ /** Which slide is showing, zero-based. */
9
+ index: number;
10
+ /** How many slides the talk holds. */
11
+ total: number;
12
+ /** Whether the player is presenting on the whole screen. */
13
+ isFullscreen: boolean;
14
+ /** Called with the slide to show. */
15
+ onGo: (index: number) => void;
16
+ /** Called to enter or leave fullscreen. */
17
+ onToggleFullscreen: () => void;
18
+ /**
19
+ * Called when the presenter leaves the talk. The bar carries no Exit button
20
+ * when it is left out.
21
+ * @default undefined
22
+ */
23
+ onExit?: () => void;
24
+ }
25
+
26
+ /**
27
+ * The bar under the stage: the way out, the talk's name, and where in it the
28
+ * presenter stands.
29
+ *
30
+ * It is chrome rather than the talk, so it is dropped from a printed handout
31
+ * along with the rest of the site's.
32
+ * @param props - See {@link SlideshowBarProps}.
33
+ * @returns The bar.
34
+ */
35
+ export function SlideshowBar(props: SlideshowBarProps): ReactElement {
36
+ const {
37
+ title,
38
+ index,
39
+ total,
40
+ isFullscreen,
41
+ onGo,
42
+ onToggleFullscreen,
43
+ onExit,
44
+ } = props;
45
+
46
+ return (
47
+ <div className="slideshow-bar no-print">
48
+ {onExit === undefined ? null : (
49
+ <Button variant="minimal" icon="cross" text="Exit" onClick={onExit} />
50
+ )}
51
+ <div className="slideshow-title">{title}</div>
52
+ <div className="slideshow-controls">
53
+ <Button
54
+ variant="minimal"
55
+ icon="chevron-left"
56
+ aria-label="Previous slide"
57
+ disabled={index === 0}
58
+ onClick={() => {
59
+ onGo(index - 1);
60
+ }}
61
+ />
62
+ <span className="slideshow-counter">
63
+ {`${total === 0 ? 0 : index + 1} / ${total}`}
64
+ </span>
65
+ <Button
66
+ variant="minimal"
67
+ icon="chevron-right"
68
+ aria-label="Next slide"
69
+ disabled={index >= total - 1}
70
+ onClick={() => {
71
+ onGo(index + 1);
72
+ }}
73
+ />
74
+ <Button
75
+ variant="minimal"
76
+ icon={isFullscreen ? 'minimize' : 'fullscreen'}
77
+ aria-label="Toggle fullscreen"
78
+ onClick={onToggleFullscreen}
79
+ />
80
+ </div>
81
+ </div>
82
+ );
83
+ }