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,146 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { isModifiedClick } from '../../chrome/ui/navItem.ts';
4
+ import { pluralize } from '../../format/core/index.ts';
5
+ import type { TalkManifest, TalkSummary } from '../core/index.ts';
6
+
7
+ /** The talks to list, and what opening one means. */
8
+ export interface TalkListProps {
9
+ /** One manifest per publishing site. */
10
+ manifests: readonly TalkManifest[];
11
+ /** Called when a talk is picked. */
12
+ onOpen: (manifest: TalkManifest, talk: TalkSummary) => void;
13
+ /**
14
+ * The address a talk sits at, which keeps the card a real link — a middle
15
+ * click opens a tab of its own, and a crawler walks the list.
16
+ * @default undefined — the card is a link with no address
17
+ */
18
+ renderHref?: (manifest: TalkManifest, talk: TalkSummary) => string;
19
+ /**
20
+ * How a site id is written above its talks.
21
+ * @default the id itself
22
+ */
23
+ siteName?: (site: string) => string;
24
+ /**
25
+ * What an empty list says. A list that draws nothing at all reads as a
26
+ * broken page rather than as a site that has published no deck yet.
27
+ * @default 'No talk yet.'
28
+ */
29
+ emptyNote?: string;
30
+ }
31
+
32
+ /**
33
+ * The talks on offer: a site's own, or the whole family's.
34
+ *
35
+ * The site heading appears only once there is more than one site to tell
36
+ * apart, so the same component draws a single site's `/talks` page and the
37
+ * family-wide listing without either looking like the other one's leftovers.
38
+ * @param props - See {@link TalkListProps}.
39
+ * @returns The listing.
40
+ */
41
+ export function TalkList(props: TalkListProps): ReactElement {
42
+ const {
43
+ manifests,
44
+ onOpen,
45
+ renderHref,
46
+ siteName,
47
+ emptyNote = 'No talk yet.',
48
+ } = props;
49
+ const groups = groupBySite(manifests);
50
+ const showSite = groups.length > 1;
51
+
52
+ if (groups.length === 0) {
53
+ return <p className="talk-list-empty">{emptyNote}</p>;
54
+ }
55
+
56
+ return (
57
+ <div className="talk-list">
58
+ {groups.map((group) => (
59
+ <section key={group.site} className="talk-list-group">
60
+ {showSite ? (
61
+ <h2 className="talk-list-site">
62
+ {siteName === undefined ? group.site : siteName(group.site)}
63
+ </h2>
64
+ ) : null}
65
+ <div className="talk-grid">
66
+ {group.entries.map(({ manifest, talk }) => (
67
+ <a
68
+ key={`${manifest.site}:${talk.id}`}
69
+ className="talk-card"
70
+ href={renderHref?.(manifest, talk)}
71
+ onClick={(event) => {
72
+ if (isModifiedClick(event)) return;
73
+ event.preventDefault();
74
+ onOpen(manifest, talk);
75
+ }}
76
+ >
77
+ <div className="talk-card-title">{talk.title}</div>
78
+ {talk.subtitle === undefined ? null : (
79
+ <div className="talk-card-subtitle">{talk.subtitle}</div>
80
+ )}
81
+ <div className="talk-card-meta">{occasion(talk)}</div>
82
+ <div className="talk-card-count">
83
+ {`${talk.slideCount} ${pluralize(talk.slideCount, 'slide')}`}
84
+ </div>
85
+ </a>
86
+ ))}
87
+ </div>
88
+ </section>
89
+ ))}
90
+ </div>
91
+ );
92
+ }
93
+
94
+ /** One talk, together with the manifest that published it. */
95
+ interface TalkEntry {
96
+ /** The manifest the talk was listed in. */
97
+ manifest: TalkManifest;
98
+ /** The talk itself. */
99
+ talk: TalkSummary;
100
+ }
101
+
102
+ /** Every talk one site published, most recent first. */
103
+ interface TalkGroup {
104
+ /** The `ECOSYSTEM_SITES` id of the publishing site. */
105
+ site: string;
106
+ /** Its talks. */
107
+ entries: TalkEntry[];
108
+ }
109
+
110
+ function groupBySite(manifests: readonly TalkManifest[]): TalkGroup[] {
111
+ const bySite = new Map<string, TalkEntry[]>();
112
+ for (const manifest of manifests) {
113
+ let entries = bySite.get(manifest.site);
114
+ if (entries === undefined) {
115
+ entries = [];
116
+ bySite.set(manifest.site, entries);
117
+ }
118
+ for (const talk of manifest.talks) entries.push({ manifest, talk });
119
+ }
120
+
121
+ const groups: TalkGroup[] = [];
122
+ for (const [site, entries] of bySite) {
123
+ // A site that has published nothing is not a heading over an empty row.
124
+ if (entries.length === 0) continue;
125
+ groups.push({ site, entries: entries.toSorted(byMostRecent) });
126
+ }
127
+ return groups;
128
+ }
129
+
130
+ // The dates are ISO, so they order as text; a talk with no date sorts last
131
+ // rather than ahead of every dated one.
132
+ function byMostRecent(left: TalkEntry, right: TalkEntry): number {
133
+ const before = left.talk.date ?? '';
134
+ const after = right.talk.date ?? '';
135
+ if (before === after) return 0;
136
+ if (before === '') return 1;
137
+ if (after === '') return -1;
138
+ return before > after ? -1 : 1;
139
+ }
140
+
141
+ function occasion(talk: TalkSummary): string {
142
+ const parts: string[] = [];
143
+ if (talk.event !== undefined) parts.push(talk.event);
144
+ if (talk.date !== undefined) parts.push(talk.date);
145
+ return parts.join(' · ');
146
+ }
@@ -0,0 +1,18 @@
1
+ export type { BackToSlidesProps } from './BackToSlides.tsx';
2
+ export { BackToSlides } from './BackToSlides.tsx';
3
+ export type { DemoLinkProps, RenderSlideLink } from './DemoLink.tsx';
4
+ export { DemoLink } from './DemoLink.tsx';
5
+ export type { EmbedSlideProps } from './EmbedSlide.tsx';
6
+ export { EmbedSlide } from './EmbedSlide.tsx';
7
+ export type { SectionSlideProps } from './SectionSlide.tsx';
8
+ export { SectionSlide } from './SectionSlide.tsx';
9
+ export type { SlideLayoutProps, SlideViewProps } from './Slide.tsx';
10
+ export { SlideView } from './Slide.tsx';
11
+ export type { SlideBodyProps } from './SlideBody.tsx';
12
+ export { SlideBody } from './SlideBody.tsx';
13
+ export type { SlideshowProps } from './Slideshow.tsx';
14
+ export { Slideshow } from './Slideshow.tsx';
15
+ export type { SlideshowBarProps } from './SlideshowBar.tsx';
16
+ export { SlideshowBar } from './SlideshowBar.tsx';
17
+ export type { TalkListProps } from './TalkList.tsx';
18
+ export { TalkList } from './TalkList.tsx';
@@ -0,0 +1,2 @@
1
+ export type { LoadedTalk, TalksOptions } from './talks.ts';
2
+ export { cheminfoTalks, loadTalksFromDisk } from './talks.ts';
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Publish a site's talks so another site can list and play them.
3
+ *
4
+ * A deck is Markdown in the repository of the site whose subject it teaches,
5
+ * which is what keeps a talk reviewable. For learn.cheminfo.org to gather the
6
+ * family's talks it needs two static files per site and no API: a manifest of
7
+ * what exists, and the source of each deck. Both are written at build time and
8
+ * served with permissive CORS, because public teaching material is fetched from
9
+ * a page that is not ours.
10
+ */
11
+
12
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
13
+ import { join, resolve } from 'node:path';
14
+
15
+ import type { Plugin } from 'vite';
16
+
17
+ import { siteById, siteUrl } from '../../ecosystem/core/index.ts';
18
+ import type { SiteId } from '../../ecosystem/core/sites.ts';
19
+ import type { Talk } from '../core/talk.ts';
20
+ import { parseTalk } from '../core/talk.ts';
21
+ import { buildTalkManifest } from '../core/talkManifest.ts';
22
+
23
+ /** What the build needs to know to publish the decks. */
24
+ export interface TalksOptions {
25
+ /** The site the talks belong to. */
26
+ site: SiteId;
27
+ /**
28
+ * Where the sources live, relative to the project root.
29
+ * @default 'src/talks'
30
+ */
31
+ dir?: string;
32
+ /**
33
+ * Where the site is served, used as the manifest's origin.
34
+ * @default `https://<the site's host>`
35
+ */
36
+ origin?: string;
37
+ }
38
+
39
+ /** One talk on disk: its id, its source, and what the source parses into. */
40
+ export interface LoadedTalk {
41
+ /** The file name without extension, or the folder name for `<id>/index.md`. */
42
+ id: string;
43
+ /** The Markdown as written, which is what other sites fetch. */
44
+ source: string;
45
+ /** The parsed talk, for the manifest. */
46
+ talk: Talk;
47
+ }
48
+
49
+ /**
50
+ * Write `talks.json` and `talks/<id>.md` into the build.
51
+ * @param options - Which site, and where its decks are.
52
+ * @returns The Vite plugin.
53
+ */
54
+ export function cheminfoTalks(options: TalksOptions): Plugin {
55
+ const { site, dir = 'src/talks' } = options;
56
+ let root = process.cwd();
57
+ let outDir = 'dist';
58
+
59
+ return {
60
+ name: 'cheminfo-talks',
61
+ apply: 'build',
62
+ configResolved(config) {
63
+ root = config.root;
64
+ outDir = config.build.outDir;
65
+ },
66
+ closeBundle() {
67
+ const talks = loadTalksFromDisk(join(root, dir));
68
+ const origin =
69
+ options.origin ?? siteUrl(siteById(site)).replace(/\/$/, '');
70
+ const manifest = buildTalkManifest({
71
+ site,
72
+ origin,
73
+ talks: talks.map((entry) => ({ id: entry.id, ...entry.talk })),
74
+ });
75
+
76
+ const target = resolve(root, outDir);
77
+ mkdirSync(join(target, 'talks'), { recursive: true });
78
+ writeFileSync(
79
+ join(target, 'talks.json'),
80
+ `${JSON.stringify(manifest, null, 2)}\n`,
81
+ );
82
+ for (const entry of talks) {
83
+ writeFileSync(join(target, 'talks', `${entry.id}.md`), entry.source);
84
+ }
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Read every deck of a directory, in either of the two shapes a talk takes.
91
+ * @param directory - Absolute path of the talks directory.
92
+ * @returns The talks, sorted by id so a build is reproducible.
93
+ */
94
+ export function loadTalksFromDisk(directory: string): LoadedTalk[] {
95
+ const loaded: LoadedTalk[] = [];
96
+ let entries: string[] = [];
97
+ try {
98
+ entries = readdirSync(directory);
99
+ } catch {
100
+ // A site with no talks yet still builds; it simply publishes an empty list.
101
+ return loaded;
102
+ }
103
+
104
+ for (const entry of entries) {
105
+ const path = join(directory, entry);
106
+ if (entry.endsWith('.md')) {
107
+ const source = readFileSync(path, 'utf8');
108
+ loaded.push({
109
+ id: entry.slice(0, -3),
110
+ source,
111
+ talk: parseTalk(source),
112
+ });
113
+ continue;
114
+ }
115
+ const index = join(path, 'index.md');
116
+ let source: string;
117
+ try {
118
+ source = readFileSync(index, 'utf8');
119
+ } catch {
120
+ continue;
121
+ }
122
+ loaded.push({ id: entry, source, talk: parseTalk(source) });
123
+ }
124
+
125
+ return loaded.toSorted((left, right) => left.id.localeCompare(right.id));
126
+ }
package/src/slides.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './slides/core/index.ts';
2
+ export * from './slides/ui/index.ts';
@@ -179,7 +179,7 @@ const ROOT_STYLE: CSSProperties = {
179
179
  position: 'relative',
180
180
  overflow: 'hidden',
181
181
  boxSizing: 'border-box',
182
- border: '1px solid #d3d8de',
182
+ border: '1px solid var(--border, #d3d8de)',
183
183
  borderRadius: 6,
184
184
  background: '#fff',
185
185
  };
@@ -191,6 +191,6 @@ const LOADING_STYLE: CSSProperties = {
191
191
  display: 'flex',
192
192
  alignItems: 'center',
193
193
  justifyContent: 'center',
194
- color: '#8a96a3',
194
+ color: 'var(--text-faint, #8a96a3)',
195
195
  fontSize: 13,
196
196
  };
@@ -43,6 +43,6 @@ const PLACEHOLDER_STYLE: CSSProperties = {
43
43
  display: 'inline-flex',
44
44
  alignItems: 'center',
45
45
  justifyContent: 'center',
46
- color: '#8a96a3',
46
+ color: 'var(--text-faint, #8a96a3)',
47
47
  fontSize: '0.75rem',
48
48
  };
@@ -0,0 +1,242 @@
1
+ // tokens-ok: file — the checker names the colours it looks for.
2
+ import { normalizeColor, readHexColor, readRgbColor } from './colors.ts';
3
+ import { BANNED_COLORS } from './rules.ts';
4
+
5
+ /** What a site got wrong where a token was called for. */
6
+ export type TokenViolationKind =
7
+ 'blueprint-grey' | 'blueprint-blue' | 'retyped-token' | 'redeclared-brand';
8
+
9
+ /** One place a site writes a colour that is not its own to write. */
10
+ export interface TokenViolation {
11
+ /** The file it is written in, named as the caller named it. */
12
+ file: string;
13
+ /** Which line, counting from 1. */
14
+ line: number;
15
+ /** Which column, counting from 1. */
16
+ column: number;
17
+ /** What kind of mistake it is. */
18
+ kind: TokenViolationKind;
19
+ /** The text as written — the hex, the `rgb()` call, the property name. */
20
+ text: string;
21
+ /** The token to use instead, in one line. */
22
+ hint: string;
23
+ }
24
+
25
+ /** How forgiving a scan is. */
26
+ export interface CheckTokensOptions {
27
+ /**
28
+ * Colours the project legitimately owns — a subject-matter palette, a logo
29
+ * of somebody else.
30
+ * @default []
31
+ */
32
+ allow?: readonly string[];
33
+ /**
34
+ * Lines carrying this marker are skipped, for the rare deliberate literal.
35
+ * @default 'tokens-ok'
36
+ */
37
+ ignoreMarker?: string;
38
+ }
39
+
40
+ /**
41
+ * Every colour a set of files writes that belongs to the family rather than to
42
+ * the site.
43
+ *
44
+ * The neutrals, the geometry and the type are shared, and the two colours a
45
+ * site owns come from `<SiteTheme siteId>`; a site that re-types either — a
46
+ * Blueprint grey because a component used it, `#f5f7fa` because that is what
47
+ * the token holds today, `--brand` because the palette was declared by hand —
48
+ * leaves its page unable to follow the family. Both spellings of one colour
49
+ * are caught, `#5f6b7c` and `rgb(95 107 124)` alike, and reading a token
50
+ * through `var()` is never a violation.
51
+ * @param files - The files to read, each with the path a report names it by.
52
+ * @param options - The colours the project owns, and the marker waiving a line.
53
+ * @returns Every violation, in file order and then in reading order.
54
+ */
55
+ export function findTokenViolations(
56
+ files: ReadonlyArray<{ path: string; text: string }>,
57
+ options: CheckTokensOptions = {},
58
+ ): TokenViolation[] {
59
+ const marker = (options.ignoreMarker ?? DEFAULT_IGNORE_MARKER).toLowerCase();
60
+ const allowed = new Set<string>();
61
+ for (const entry of options.allow ?? []) {
62
+ const color = normalizeColor(entry);
63
+ if (color !== null) allowed.add(color);
64
+ }
65
+
66
+ const violations: TokenViolation[] = [];
67
+ for (const file of files) {
68
+ // A file whose opening lines carry the marker defines the palette rather
69
+ // than consuming it — the token block itself, the registry of banned
70
+ // colours, an image generated outside any page.
71
+ if (waivesWholeFile(file.text, marker)) continue;
72
+ const scan = scanFile(file.text, marker);
73
+ for (const candidate of scan.candidates) {
74
+ if (scan.markedLines.has(candidate.line)) continue;
75
+ if (candidate.color !== null && allowed.has(candidate.color)) continue;
76
+ violations.push({
77
+ file: file.path,
78
+ line: candidate.line,
79
+ column: candidate.column,
80
+ kind: candidate.kind,
81
+ text: candidate.text,
82
+ hint: candidate.hint,
83
+ });
84
+ }
85
+ }
86
+ return violations;
87
+ }
88
+
89
+ const DEFAULT_IGNORE_MARKER = 'tokens-ok';
90
+
91
+ // How far into a file the whole-file waiver may sit: a licence header, an
92
+ // import or two, and the comment saying why this file writes colours.
93
+ const WAIVER_LINES = 6;
94
+
95
+ function waivesWholeFile(text: string, marker: string): boolean {
96
+ if (marker.length === 0) return false;
97
+ const head = text.split('\n', WAIVER_LINES).join('\n').toLowerCase();
98
+ return head.includes(`${marker}: file`);
99
+ }
100
+
101
+ // Longest first, so `--brand-alt` is never read as `--brand`.
102
+ const BRAND_PROPERTIES = ['--brand-alt', '--brand', '--accent'] as const;
103
+
104
+ interface Candidate extends Omit<TokenViolation, 'file'> {
105
+ /** The colour written, or `null` for a property that should not be declared. */
106
+ color: string | null;
107
+ }
108
+
109
+ interface FileScan {
110
+ candidates: Candidate[];
111
+ markedLines: Set<number>;
112
+ }
113
+
114
+ // One pass over the file: the marker, the block structure and every colour are
115
+ // all read from the same walk, so nothing is matched a second time per line.
116
+ function scanFile(text: string, marker: string): FileScan {
117
+ const lower = text.toLowerCase();
118
+ const candidates: Candidate[] = [];
119
+ const markedLines = new Set<number>();
120
+ const rootBlocks: boolean[] = [];
121
+ // Which parentheses are open, and whether each is a `var(` past its comma —
122
+ // a colour there is a fallback for the token named beside it.
123
+ const parens: Array<{ isVar: boolean; sawComma: boolean }> = [];
124
+ let selectorStart = 0;
125
+ let line = 1;
126
+ let lineStart = 0;
127
+
128
+ for (let index = 0; index < lower.length; index++) {
129
+ const char = lower[index];
130
+ if (char === '\n') {
131
+ line++;
132
+ lineStart = index + 1;
133
+ continue;
134
+ }
135
+ if (
136
+ marker.length > 0 &&
137
+ char === marker[0] &&
138
+ lower.startsWith(marker, index)
139
+ ) {
140
+ markedLines.add(line);
141
+ index += marker.length - 1;
142
+ continue;
143
+ }
144
+ if (char === '(') {
145
+ parens.push({
146
+ isVar: lower.startsWith('var(', index - 3),
147
+ sawComma: false,
148
+ });
149
+ continue;
150
+ }
151
+ if (char === ')') {
152
+ parens.pop();
153
+ continue;
154
+ }
155
+ if (char === ',') {
156
+ const open = parens.at(-1);
157
+ if (open !== undefined) open.sawComma = true;
158
+ continue;
159
+ }
160
+ if (char === '{') {
161
+ rootBlocks.push(lower.slice(selectorStart, index).includes(':root'));
162
+ selectorStart = index + 1;
163
+ continue;
164
+ }
165
+ if (char === '}' || char === ';') {
166
+ if (char === '}') rootBlocks.pop();
167
+ selectorStart = index + 1;
168
+ continue;
169
+ }
170
+
171
+ if (char === '#' || char === 'r') {
172
+ const match =
173
+ char === '#' ? readHexColor(lower, index) : readRgbColor(lower, index);
174
+ if (match === null) continue;
175
+ const rule = BANNED_COLORS.get(match.color);
176
+ const open = parens.at(-1);
177
+ const isFallback = open !== undefined && open.isVar && open.sawComma;
178
+ if (rule !== undefined && !isFallback) {
179
+ candidates.push({
180
+ line,
181
+ column: index - lineStart + 1,
182
+ kind: rule.kind,
183
+ text: text.slice(index, match.end),
184
+ hint: rule.hint,
185
+ color: match.color,
186
+ });
187
+ }
188
+ index = match.end - 1;
189
+ continue;
190
+ }
191
+
192
+ if (char === '-') {
193
+ const property = readBrandDeclaration(lower, index);
194
+ if (property === null) continue;
195
+ if (rootBlocks.includes(true)) {
196
+ candidates.push({
197
+ line,
198
+ column: index - lineStart + 1,
199
+ kind: 'redeclared-brand',
200
+ text: property.name,
201
+ hint: `render <SiteTheme siteId> instead of declaring ${property.name} in :root.`,
202
+ color: null,
203
+ });
204
+ }
205
+ index = property.end - 1;
206
+ }
207
+ }
208
+ return { candidates, markedLines };
209
+ }
210
+
211
+ // A declaration of one of the palette properties. Reading one — `var(--brand)`
212
+ // — has no colon after the name, so it never lands here.
213
+ function readBrandDeclaration(
214
+ lower: string,
215
+ start: number,
216
+ ): { name: string; end: number } | null {
217
+ for (const name of BRAND_PROPERTIES) {
218
+ if (!lower.startsWith(name, start)) continue;
219
+ const after = start + name.length;
220
+ // `--brand-alt-text` is its own property, not `--brand-alt`.
221
+ if (isNameChar(lower[after])) return null;
222
+ let index = after;
223
+ while (isSpace(lower[index])) index++;
224
+ if (lower[index] !== ':') return null;
225
+ return { name, end: after };
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function isNameChar(char: string | undefined): boolean {
231
+ if (char === undefined) return false;
232
+ return (
233
+ (char >= '0' && char <= '9') ||
234
+ (char >= 'a' && char <= 'z') ||
235
+ char === '-' ||
236
+ char === '_'
237
+ );
238
+ }
239
+
240
+ function isSpace(char: string | undefined): boolean {
241
+ return char === ' ' || char === '\t';
242
+ }
@@ -0,0 +1,122 @@
1
+ // tokens-ok: file — the colour reader is tested against literals.
2
+ /**
3
+ * Reading a colour out of source text, in both spellings the same value is
4
+ * written in: `#5f6b7c` and `rgb(95 107 124)`. Both come back as the same six
5
+ * lowercase hex digits, so a table of banned colours holds one entry per
6
+ * colour rather than one per way of typing it.
7
+ */
8
+
9
+ /** A colour found in the text, and where it ends. */
10
+ export interface ColorMatch {
11
+ /** The value, as six lowercase hex digits, without its `#`. */
12
+ color: string;
13
+ /** The index just past the last character the colour is written with. */
14
+ end: number;
15
+ }
16
+
17
+ /**
18
+ * The colour a hex literal starts at, in `#rgb`, `#rgba`, `#rrggbb` or
19
+ * `#rrggbbaa` form. The alpha, when there is one, says nothing about the
20
+ * colour and is dropped.
21
+ * @param lower - The text being scanned, already lowercased.
22
+ * @param start - The index of the `#`.
23
+ * @returns The colour and its end, or `null` when it is not a hex colour.
24
+ */
25
+ export function readHexColor(lower: string, start: number): ColorMatch | null {
26
+ let end = start + 1;
27
+ while (isHexDigit(lower[end])) end++;
28
+
29
+ // A run of hex digits running straight into a word is an identifier — a
30
+ // private field, an anchor — rather than a colour.
31
+ if (isWordChar(lower[end])) return null;
32
+
33
+ const digits = end - start - 1;
34
+ if (digits === 3 || digits === 4) {
35
+ const red = lower[start + 1];
36
+ const green = lower[start + 2];
37
+ const blue = lower[start + 3];
38
+ return { color: `${red}${red}${green}${green}${blue}${blue}`, end };
39
+ }
40
+ if (digits === 6 || digits === 8) {
41
+ return { color: lower.slice(start + 1, start + 7), end };
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * The colour an `rgb()` or `rgba()` call starts at, in either the comma or the
48
+ * space separated spelling.
49
+ *
50
+ * Only a plain byte triple is read: a percentage, a decimal or a variable
51
+ * inside the call means the value cannot be compared with a hex literal, and
52
+ * is left alone rather than guessed at.
53
+ * @param lower - The text being scanned, already lowercased.
54
+ * @param start - The index of the `r`.
55
+ * @returns The colour and its end, or `null` when it is not one.
56
+ */
57
+ export function readRgbColor(lower: string, start: number): ColorMatch | null {
58
+ let open = -1;
59
+ if (lower.startsWith('rgb(', start)) open = start + 3;
60
+ else if (lower.startsWith('rgba(', start)) open = start + 4;
61
+ if (open === -1) return null;
62
+
63
+ const close = lower.indexOf(')', open);
64
+ if (close === -1) return null;
65
+
66
+ const components: number[] = [];
67
+ let value = -1;
68
+ for (let index = open + 1; index < close; index++) {
69
+ const char = lower[index];
70
+ if (char !== undefined && char >= '0' && char <= '9') {
71
+ value = Math.max(value, 0) * 10 + Number(char);
72
+ continue;
73
+ }
74
+ if (value >= 0) {
75
+ components.push(value);
76
+ value = -1;
77
+ }
78
+ // The alpha of the modern syntax, which the colour does not depend on.
79
+ if (char === '/') break;
80
+ if (char !== ' ' && char !== ',' && char !== '\t' && char !== '\n') {
81
+ return null;
82
+ }
83
+ }
84
+ if (value >= 0) components.push(value);
85
+ if (components.length < 3) return null;
86
+
87
+ let color = '';
88
+ for (let index = 0; index < 3; index++) {
89
+ const component = components[index];
90
+ if (component === undefined || component > 255) return null;
91
+ color += component.toString(16).padStart(2, '0');
92
+ }
93
+ return { color, end: close + 1 };
94
+ }
95
+
96
+ /**
97
+ * The `rrggbb` form of a colour written by hand — in an allow list, on a
98
+ * command line — in any of the spellings the scanner itself reads.
99
+ * @param value - The colour, with or without its `#`.
100
+ * @returns The six lowercase hex digits, or `null` when it is not a colour.
101
+ */
102
+ export function normalizeColor(value: string): string | null {
103
+ const text = value.trim().toLowerCase();
104
+ if (text === '') return null;
105
+ let match: ColorMatch | null = null;
106
+ if (text.startsWith('#')) match = readHexColor(text, 0);
107
+ else if (text.startsWith('rgb')) match = readRgbColor(text, 0);
108
+ else match = readHexColor(`#${text}`, 0);
109
+ return match === null ? null : match.color;
110
+ }
111
+
112
+ function isHexDigit(char: string | undefined): boolean {
113
+ if (char === undefined) return false;
114
+ return (char >= '0' && char <= '9') || (char >= 'a' && char <= 'f');
115
+ }
116
+
117
+ function isWordChar(char: string | undefined): boolean {
118
+ if (char === undefined) return false;
119
+ return (
120
+ (char >= '0' && char <= '9') || (char >= 'a' && char <= 'z') || char === '_'
121
+ );
122
+ }