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
@@ -143,7 +143,7 @@ const CONTROLS_STYLE = {
143
143
  } as const satisfies CSSProperties;
144
144
 
145
145
  const LABEL_STYLE = {
146
- color: '#5f6b7c',
146
+ color: 'var(--text-muted, #5f6b7c)',
147
147
  fontSize: 12,
148
148
  } as const satisfies CSSProperties;
149
149
 
@@ -1,5 +1,7 @@
1
+ // tokens-ok: file — every site's two colours are declared here.
1
2
  /** The sites the menu links to, in the order they are listed. */
2
3
  export type SiteId =
4
+ | 'learn'
3
5
  | 'inchi'
4
6
  | 'vcl'
5
7
  | 'smiles'
@@ -70,6 +72,16 @@ export interface SiteMarkColors {
70
72
  * go with them live in `marks.tsx`.
71
73
  */
72
74
  export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
75
+ {
76
+ id: 'learn',
77
+ name: { lead: 'learn', alt: 'cheminfo', dot: true },
78
+ host: 'learn.cheminfo.org',
79
+ repository: 'https://github.com/cheminfo/learn.cheminfo.org',
80
+ tagline: 'Arrange the tools into a course and hand out its link.',
81
+ brand: '#0d9488',
82
+ brandAlt: '#b45309',
83
+ mark: { plate: '#0d9488', accent: '#f59e0b' },
84
+ },
73
85
  {
74
86
  id: 'inchi',
75
87
  name: { lead: 'inchi', alt: 'cheminfo', dot: true },
@@ -1,3 +1,4 @@
1
+ // tokens-ok: file — the block a site renders is written here.
1
2
  import { siteById } from './lookup.ts';
2
3
  import type { EcosystemSite, SiteId } from './sites.ts';
3
4
 
@@ -17,7 +17,7 @@ const SECTION_STYLE: CSSProperties = {
17
17
  };
18
18
  const HEADING_STYLE: CSSProperties = {
19
19
  margin: 0,
20
- color: '#5b6875',
20
+ color: 'var(--text-muted, #5b6875)',
21
21
  fontSize: '0.75rem',
22
22
  fontWeight: 600,
23
23
  };
@@ -40,7 +40,7 @@ const LINK_STYLE: CSSProperties = {
40
40
  };
41
41
  const CURRENT_STYLE: CSSProperties = {
42
42
  ...LINK_STYLE,
43
- color: '#8a96a3',
43
+ color: 'var(--text-faint, #8a96a3)',
44
44
  cursor: 'default',
45
45
  };
46
46
 
@@ -14,7 +14,7 @@ const PANEL_STYLE: CSSProperties = {
14
14
  gap: 8,
15
15
  };
16
16
  const HEADING_STYLE: CSSProperties = {
17
- color: '#5b6875',
17
+ color: 'var(--text-muted, #5b6875)',
18
18
  fontSize: '0.75rem',
19
19
  fontWeight: 600,
20
20
  };
@@ -11,7 +11,7 @@ const TILE_STYLE: CSSProperties = {
11
11
  padding: '7px 8px',
12
12
  border: '1px solid transparent',
13
13
  borderRadius: 10,
14
- color: '#16202c',
14
+ color: 'var(--text, #16202c)',
15
15
  gap: 10,
16
16
  textDecoration: 'none',
17
17
  transition: 'background 120ms, border-color 120ms, transform 120ms',
@@ -37,7 +37,7 @@ const HERE_STYLE: CSSProperties = {
37
37
  textTransform: 'uppercase',
38
38
  };
39
39
  const TAGLINE_STYLE: CSSProperties = {
40
- color: '#5b6875',
40
+ color: 'var(--text-muted, #5b6875)',
41
41
  fontSize: '0.75rem',
42
42
  lineHeight: 1.35,
43
43
  };
@@ -91,7 +91,9 @@ export function SiteTile(props: SiteTileProps): ReactElement {
91
91
  <div style={NAME_STYLE}>
92
92
  <span>
93
93
  <span style={{ color: site.brand }}>{site.name.lead}</span>
94
- {site.name.dot ? <span style={{ color: '#8a96a3' }}>.</span> : null}
94
+ {site.name.dot ? (
95
+ <span style={{ color: 'var(--text-faint, #8a96a3)' }}>.</span>
96
+ ) : null}
95
97
  <span style={{ color: site.brandAlt }}>{site.name.alt}</span>
96
98
  </span>
97
99
  {isCurrent ? (
@@ -134,7 +136,7 @@ export function SiteTile(props: SiteTileProps): ReactElement {
134
136
  // A tile lights up in the colour of the site it opens, never in ours: the
135
137
  // pointer moving down the grid is what makes the pairs of colours read.
136
138
  function tint(site: EcosystemSite, isCurrent: boolean, lit: boolean): string {
137
- if (isCurrent) return '#f5f7fa';
139
+ if (isCurrent) return 'var(--surface-sunken, #f5f7fa)';
138
140
  return lit ? `color-mix(in oklab, ${site.brand} 9%, white)` : 'transparent';
139
141
  }
140
142
 
@@ -143,6 +145,6 @@ function borderOf(
143
145
  isCurrent: boolean,
144
146
  lit: boolean,
145
147
  ): string {
146
- if (isCurrent) return '#dfe3e8';
148
+ if (isCurrent) return 'var(--border, #dfe3e8)';
147
149
  return lit ? `color-mix(in oklab, ${site.brand} 32%, white)` : 'transparent';
148
150
  }
@@ -16,6 +16,46 @@ import type { SiteId } from '../core/sites.ts';
16
16
 
17
17
  /** Every mark, keyed by site, drawn on top of its plate. */
18
18
  export const GLYPHS: Record<SiteId, (alt: string) => ReactNode> = {
19
+ // Six tiles standing where the six atoms of a benzene ring stand: the
20
+ // arrangement of tiles is the product and the ring is the subject.
21
+ learn: (alt) => (
22
+ <g>
23
+ <rect x="12.85" y="4.15" width="6.3" height="6.3" rx="1.9" fill={alt} />
24
+ <rect
25
+ x="20.39"
26
+ y="8.5"
27
+ width="6.3"
28
+ height="6.3"
29
+ rx="1.9"
30
+ fill="#ffffff"
31
+ />
32
+ <rect
33
+ x="20.39"
34
+ y="17.2"
35
+ width="6.3"
36
+ height="6.3"
37
+ rx="1.9"
38
+ fill="#ffffff"
39
+ />
40
+ <rect
41
+ x="12.85"
42
+ y="21.55"
43
+ width="6.3"
44
+ height="6.3"
45
+ rx="1.9"
46
+ fill="#ffffff"
47
+ />
48
+ <rect
49
+ x="5.31"
50
+ y="17.2"
51
+ width="6.3"
52
+ height="6.3"
53
+ rx="1.9"
54
+ fill="#ffffff"
55
+ />
56
+ <rect x="5.31" y="8.5" width="6.3" height="6.3" rx="1.9" fill="#ffffff" />
57
+ </g>
58
+ ),
19
59
  // A hexagon with a tail: the structure, and the string it hashes into.
20
60
  inchi: (alt) => (
21
61
  <g
@@ -1,5 +1,8 @@
1
1
  export type { ResolutionLimits } from './atomicGrid.ts';
2
- export { atomicGridResolution } from './atomicGrid.ts';
2
+ export {
3
+ atomicGridResolution,
4
+ createAtomicOrbitalEvaluator,
5
+ } from './atomicGrid.ts';
3
6
  export type { AtomicOrbital, AtomicOrbitalOptions } from './atomicOrbitals.ts';
4
7
  export {
5
8
  atomicOrbitalsOf,
@@ -32,8 +32,11 @@ export interface RealHarmonic {
32
32
  * @param y - Offset from the nucleus along y, ångström.
33
33
  * @param z - Offset from the nucleus along z, ångström.
34
34
  * @param radius - Length of `(x, y, z)`, passed in because the caller has
35
- * already computed it for the radial part.
36
- * @returns The dimensionless amplitude; 0 at the nucleus for ℓ > 0.
35
+ * already computed it for the radial part. Never 0: a harmonic names a
36
+ * direction and the nucleus has none, so every ℓ > 0 divides by it. Build
37
+ * `ψ` with `createAtomicOrbitalEvaluator`, which answers that one point
38
+ * itself rather than paying for a branch on every sample.
39
+ * @returns The dimensionless amplitude.
37
40
  */
38
41
  evaluate: (x: number, y: number, z: number, radius: number) => number;
39
42
  }
@@ -6,9 +6,12 @@
6
6
  * reason the two components are separate files.
7
7
  */
8
8
 
9
+ import { Button } from '@blueprintjs/core';
9
10
  import type { CSSProperties, ReactElement } from 'react';
10
11
  import { useEffect, useRef, useState } from 'react';
11
12
 
13
+ import type { HelpContent } from '../../help/ui/HelpBody.tsx';
14
+ import { HelpTooltip } from '../../help/ui/HelpTooltip.tsx';
12
15
  import type { ResolutionLimits } from '../core/atomicGrid.ts';
13
16
  import type { PhasePalette } from '../core/palette.ts';
14
17
  import { PHASE_PALETTES } from '../core/palette.ts';
@@ -39,6 +42,24 @@ export interface AtomicOrbitalCanvasProps {
39
42
  * @default 56
40
43
  */
41
44
  resolution?: number | ResolutionLimits;
45
+ /**
46
+ * Whether the labelled x, y, z frame is drawn through the nucleus when the
47
+ * canvas opens. A `3d_xz` is only `3d_xz` because of where its lobes sit
48
+ * against those axes, and a lone isosurface says nothing about that, so the
49
+ * frame is on unless a site says otherwise.
50
+ *
51
+ * The canvas owns the state from then on: the button in its corner flips it,
52
+ * and {@link AtomicOrbitalCanvasProps.onAxesChange} reports the new value to
53
+ * a site that wants to remember it for the next mount.
54
+ * @default true
55
+ */
56
+ axes?: boolean;
57
+ /**
58
+ * Called when the student flips the frame, with its new state — so a site can
59
+ * persist the choice and pass it back as `axes`.
60
+ * @default undefined
61
+ */
62
+ onAxesChange?: (axes: boolean) => void;
42
63
  /**
43
64
  * Whether the scene turns on its own, which is what makes a still screenshot
44
65
  * of a 3D shape readable.
@@ -82,9 +103,11 @@ export function AtomicOrbitalCanvas(
82
103
  orbitalId,
83
104
  palette = PHASE_PALETTES.textbook,
84
105
  resolution = DEFAULT_RESOLUTION,
106
+ axes: initialAxes = true,
85
107
  spinning = false,
86
108
  spinSpeed = DEFAULT_SPIN_SPEED,
87
109
  sample = sampleInProcess,
110
+ onAxesChange,
88
111
  onNodeRadii,
89
112
  onError,
90
113
  } = props;
@@ -93,6 +116,15 @@ export function AtomicOrbitalCanvas(
93
116
  const viewerRef = useRef<OrbitalViewer | null>(null);
94
117
  const [drawn, setDrawn] = useState<string | null>(null);
95
118
 
119
+ // How far the drawn surface reaches, and whether the frame is wanted around
120
+ // it. Both are read by the sampling effect, which finishes long after the
121
+ // render that started it; keeping them in refs is what lets the frame be
122
+ // switched on without re-sampling the orbital.
123
+ const reachRef = useRef<number | null>(null);
124
+ const axesRef = useRef(initialAxes);
125
+
126
+ const [axes, setAxes] = useState(initialAxes);
127
+
96
128
  // What the canvas is being asked to show. Comparing it with what it *is*
97
129
  // showing gives the progress note without a state write on every prop change.
98
130
  const wanted = `${atomicNumber}|${orbitalId}|${resolutionKey(resolution)}|${palette.id}`;
@@ -100,9 +132,9 @@ export function AtomicOrbitalCanvas(
100
132
 
101
133
  // Callbacks are read through refs so a caller passing an inline arrow does
102
134
  // not re-sample the orbital on every render of its parent.
103
- const callbacks = useRef({ onNodeRadii, onError });
135
+ const callbacks = useRef({ onAxesChange, onNodeRadii, onError });
104
136
  useEffect(() => {
105
- callbacks.current = { onNodeRadii, onError };
137
+ callbacks.current = { onAxesChange, onNodeRadii, onError };
106
138
  });
107
139
 
108
140
  // Created and disposed once per mount. React 19 runs this twice in
@@ -136,7 +168,8 @@ export function AtomicOrbitalCanvas(
136
168
  positiveColour: palette.positive,
137
169
  negativeColour: palette.negative,
138
170
  });
139
- await viewer.frame(reach);
171
+ reachRef.current = reach ?? null;
172
+ await viewer.refit(await fitAxes(viewer, reach, axesRef.current));
140
173
  if (!cancelled) setDrawn(wanted);
141
174
  })
142
175
  .catch((error: unknown) => {
@@ -150,6 +183,16 @@ export function AtomicOrbitalCanvas(
150
183
  };
151
184
  }, [atomicNumber, orbitalId, resolution, palette, sample, wanted]);
152
185
 
186
+ // Toggling the frame on an orbital already on screen: the field it was drawn
187
+ // from is unchanged, so nothing is re-sampled.
188
+ useEffect(() => {
189
+ axesRef.current = axes;
190
+ const viewer = viewerRef.current;
191
+ const reach = reachRef.current;
192
+ if (viewer === null || reach === null) return;
193
+ void fitAxes(viewer, reach, axes).then((framed) => viewer.refit(framed));
194
+ }, [axes]);
195
+
153
196
  useEffect(() => {
154
197
  void viewerRef.current?.setSpin(spinning, spinSpeed);
155
198
  }, [spinning, spinSpeed]);
@@ -157,10 +200,75 @@ export function AtomicOrbitalCanvas(
157
200
  return (
158
201
  <div ref={containerRef} style={CANVAS_STYLE}>
159
202
  {busy && <div style={BUSY_STYLE}>Sampling…</div>}
203
+ <div style={CONTROLS_STYLE}>
204
+ <HelpTooltip content={AXES_HELP} placement="bottom">
205
+ <Button
206
+ variant="minimal"
207
+ size="small"
208
+ icon="grid"
209
+ active={axes}
210
+ aria-label="Show the x, y, z axes"
211
+ aria-pressed={axes}
212
+ onClick={() => {
213
+ setAxes(!axes);
214
+ callbacks.current.onAxesChange?.(!axes);
215
+ }}
216
+ />
217
+ </HelpTooltip>
218
+ <HelpTooltip content={RESET_HELP} placement="bottom">
219
+ <Button
220
+ variant="minimal"
221
+ size="small"
222
+ icon="zoom-to-fit"
223
+ aria-label="Reset the view"
224
+ onClick={() => {
225
+ void viewerRef.current?.resetView();
226
+ }}
227
+ />
228
+ </HelpTooltip>
229
+ </div>
160
230
  </div>
161
231
  );
162
232
  }
163
233
 
234
+ /**
235
+ * Draw the cartesian frame around the orbital, or remove it.
236
+ * @param viewer - The viewer holding the canvas.
237
+ * @param reach - How far the drawn surface reaches, as `showOrbital` returned
238
+ * it.
239
+ * @param axes - Whether the frame is wanted.
240
+ * @returns What the camera has to fit: the frame reaches past the surface, so
241
+ * turning it on has to zoom out or the labels sit off screen.
242
+ */
243
+ async function fitAxes(
244
+ viewer: OrbitalViewer,
245
+ reach: number | undefined,
246
+ axes: boolean,
247
+ ): Promise<number | undefined> {
248
+ if (!axes) {
249
+ await viewer.hideAxes();
250
+ return reach;
251
+ }
252
+ if (reach === undefined) return undefined;
253
+ return (await viewer.showAxes(reach)) ?? reach;
254
+ }
255
+
256
+ /** What the frame button says it is for. */
257
+ const AXES_HELP: HelpContent = {
258
+ title: 'Cartesian axes',
259
+ body: 'Draw x, y and z through the nucleus. The label names an orbital by where its lobes sit against them.',
260
+ example: {
261
+ code: '3d_yz',
262
+ note: 'four lobes between the y and z axes, none on either.',
263
+ },
264
+ };
265
+
266
+ /** What the reset button says it is for. */
267
+ const RESET_HELP: HelpContent = {
268
+ title: 'Reset the view',
269
+ body: 'Back to the angle and zoom the orbital opened on. A change of orbital keeps whatever view you have turned it to.',
270
+ };
271
+
164
272
  /** Samples per edge; 56 resolves the radial node of a 3s in about 25 ms. */
165
273
  const DEFAULT_RESOLUTION = 56;
166
274
 
@@ -191,6 +299,20 @@ const CANVAS_STYLE: CSSProperties = {
191
299
  overflow: 'hidden',
192
300
  };
193
301
 
302
+ /**
303
+ * The two controls the canvas owns: the frame, and the way back to the framing
304
+ * the orbital opened on — a change of orbital now keeps whatever angle and zoom
305
+ * the student is on.
306
+ */
307
+ const CONTROLS_STYLE: CSSProperties = {
308
+ position: 'absolute',
309
+ top: 4,
310
+ right: 4,
311
+ zIndex: 1,
312
+ display: 'flex',
313
+ gap: 2,
314
+ };
315
+
194
316
  const BUSY_STYLE: CSSProperties = {
195
317
  position: 'absolute',
196
318
  top: 8,
@@ -199,6 +321,6 @@ const BUSY_STYLE: CSSProperties = {
199
321
  padding: '3px 8px',
200
322
  borderRadius: 3,
201
323
  background: 'rgba(255, 255, 255, 0.85)',
202
- color: '#5f6b7c',
324
+ color: 'var(--text-muted, #5f6b7c)',
203
325
  fontSize: 11,
204
326
  };
@@ -51,6 +51,19 @@ export interface AtomicOrbitalViewerProps {
51
51
  * @default 56
52
52
  */
53
53
  resolution?: number | ResolutionLimits;
54
+ /**
55
+ * Whether the labelled x, y, z frame is drawn through the nucleus when the
56
+ * canvas opens — it is what tells a `3d_xz` from a `3d_yz`. The button in the
57
+ * canvas's corner flips it from there.
58
+ * @default true
59
+ */
60
+ axes?: boolean;
61
+ /**
62
+ * Called when the student flips the frame, with its new state — so a site can
63
+ * persist the choice and pass it back as `axes`.
64
+ * @default undefined
65
+ */
66
+ onAxesChange?: (axes: boolean) => void;
54
67
  /**
55
68
  * Whether the scene turns on its own.
56
69
  * @default false
@@ -145,7 +158,7 @@ const NOTE_STYLE: CSSProperties = {
145
158
  padding: 12,
146
159
  borderRadius: 3,
147
160
  background: 'rgb(241 245 249)',
148
- color: '#5f6b7c',
161
+ color: 'var(--text-muted, #5f6b7c)',
149
162
  fontSize: 13,
150
163
  textAlign: 'center',
151
164
  };
@@ -107,11 +107,92 @@ export function frameOrbital(
107
107
  position: Vec3.add(Vec3.zero(), centre, offset),
108
108
  up: Vec3.clone(ORBITAL_VIEW_UP),
109
109
  radius,
110
+ ...radiusMaxOf(canvas3d),
110
111
  },
111
112
  durationMs,
112
113
  );
113
114
  }
114
115
 
116
+ /**
117
+ * Keep the view the student has set, and rescale it for an orbital of a
118
+ * different extent.
119
+ *
120
+ * Every atomic orbital is drawn at one canonical size, so the two extents are
121
+ * normally equal and the camera does not move at all: clicking through a shell
122
+ * keeps the rotation and the zoom instead of throwing them away. When they do
123
+ * differ, the offset from the target and the radius are multiplied by the same
124
+ * factor, which keeps the zoom *relative* to the orbital.
125
+ * @param plugin - The molstar context.
126
+ * @param previousRadius - Extent the camera is currently fitted to.
127
+ * @param orbitalRadius - Extent to fit now.
128
+ * @param durationMs - Transition length. Pass 0 for an instant jump.
129
+ */
130
+ export function refitOrbital(
131
+ plugin: PluginContext,
132
+ previousRadius: number,
133
+ orbitalRadius: number,
134
+ durationMs = DEFAULT_CAMERA_DURATION,
135
+ ): void {
136
+ const canvas3d = plugin.canvas3d;
137
+ if (canvas3d === undefined) return;
138
+ const scale = orbitalRadius / previousRadius;
139
+ if (!Number.isFinite(scale) || scale <= 0) {
140
+ frameOrbital(plugin, orbitalRadius, durationMs);
141
+ return;
142
+ }
143
+ // The box a sampled orbital lives in changes with the orbital, and molstar
144
+ // only tracks that for a camera it also reframes, which this one is not.
145
+ const radiusMax = radiusMaxOf(canvas3d);
146
+ if (scale === 1) {
147
+ canvas3d.camera.setState(radiusMax, 0);
148
+ return;
149
+ }
150
+ const { target, position, radius } = canvas3d.camera.state;
151
+ const offset = Vec3.sub(Vec3.zero(), position, target);
152
+ Vec3.scale(offset, offset, scale);
153
+ canvas3d.camera.setState(
154
+ {
155
+ position: Vec3.add(Vec3.zero(), target, offset),
156
+ radius: radius * scale,
157
+ ...radiusMax,
158
+ },
159
+ durationMs,
160
+ );
161
+ }
162
+
163
+ /**
164
+ * The far plane's share of a camera state, left out when the scene cannot be
165
+ * measured yet.
166
+ * @param canvas3d - The canvas whose scene to measure.
167
+ * @returns `{ radiusMax }`, or an empty object to keep the camera's own value.
168
+ */
169
+ function radiusMaxOf(canvas3d: PluginContext['canvas3d'] & object): {
170
+ radiusMax?: number;
171
+ } {
172
+ const radiusMax = sceneRadius(canvas3d);
173
+ return radiusMax > 0 ? { radiusMax } : {};
174
+ }
175
+
176
+ /**
177
+ * How far the whole scene reaches, as molstar measures it for its near and far
178
+ * planes.
179
+ *
180
+ * `boundingSphere` covers the whole scene but is only recomputed on a draw, so
181
+ * in the moment after a representation was added it still reads 0 — and a zero
182
+ * `radiusMax` collapses the near and far planes onto the target, leaving the
183
+ * orbital sliced into a few thin arcs. The visible sphere is current there, so
184
+ * it stands in.
185
+ * @param canvas3d - The canvas whose scene to measure.
186
+ * @returns The radius, in scene units; 0 when neither sphere is ready.
187
+ */
188
+ function sceneRadius(canvas3d: PluginContext['canvas3d'] & object): number {
189
+ const radius =
190
+ canvas3d.boundingSphere.radius > 0
191
+ ? canvas3d.boundingSphere.radius
192
+ : canvas3d.boundingSphereVisible.radius;
193
+ return radius * canvas3d.props.sceneRadiusFactor;
194
+ }
195
+
115
196
  /**
116
197
  * Turn the automatic spin on or off.
117
198
  * @param plugin - The molstar context.
@@ -108,7 +108,14 @@ export async function renderSampledVolume(
108
108
  const drawn = surfaces.filter(
109
109
  (surface): surface is Representation.Any => surface !== null,
110
110
  );
111
- representations.set(plugin, drawn);
111
+ // Added to whatever is already recorded rather than replacing it, so the
112
+ // record is every surface on the canvas and the next clear can take them all
113
+ // away. Overwriting it would strand any pair that reached the canvas without
114
+ // being the last one recorded, and a stranded pair is on screen for good.
115
+ representations.set(plugin, [
116
+ ...(representations.get(plugin) ?? []),
117
+ ...drawn,
118
+ ]);
112
119
  for (const representation of drawn) canvas3d.add(representation);
113
120
  // add() only queues; nothing appears until the queue is committed.
114
121
  canvas3d.commit();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Running asynchronous work one piece at a time.
3
+ *
4
+ * Everything the viewer does mutates one canvas, and it does it across an
5
+ * `await`: replacing an orbital removes the surfaces on screen before it has
6
+ * finished building the new ones. Two of those interleaved both add their own
7
+ * pair and only one of them is remembered, so the atom ends up wearing several
8
+ * orbitals at once and the ones nobody recorded can never be removed again.
9
+ */
10
+
11
+ /** Runs what it is given after everything handed to it before. */
12
+ export interface SerialRunner {
13
+ /**
14
+ * Queue `task`, and resolve to what it returns.
15
+ * @param task - The work to run once the queue reaches it.
16
+ * @returns What `task` resolved to; its rejection reaches this caller alone.
17
+ */
18
+ run: <Result>(task: () => Promise<Result>) => Promise<Result>;
19
+ }
20
+
21
+ /**
22
+ * A queue of one.
23
+ * @returns The runner. A task that throws is reported to whoever queued it and
24
+ * never stalls the tasks behind it.
25
+ */
26
+ export function createSerialRunner(): SerialRunner {
27
+ let tail: Promise<unknown> = Promise.resolve();
28
+ return {
29
+ run: <Result>(task: () => Promise<Result>): Promise<Result> => {
30
+ const result = tail.then(task);
31
+ tail = result.catch(() => undefined);
32
+ return result;
33
+ },
34
+ };
35
+ }