react-cheminfo 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/lib/chrome/ui/NavLink.d.ts.map +1 -1
  2. package/lib/chrome/ui/NavLink.js +4 -3
  3. package/lib/chrome/ui/NavLink.js.map +1 -1
  4. package/lib/chrome/ui/SiteFooter.d.ts +8 -0
  5. package/lib/chrome/ui/SiteFooter.d.ts.map +1 -1
  6. package/lib/chrome/ui/SiteFooter.js +7 -2
  7. package/lib/chrome/ui/SiteFooter.js.map +1 -1
  8. package/lib/chrome/ui/SiteHeader.d.ts +11 -0
  9. package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
  10. package/lib/chrome/ui/SiteHeader.js +7 -2
  11. package/lib/chrome/ui/SiteHeader.js.map +1 -1
  12. package/lib/chrome/ui/navItem.d.ts +12 -0
  13. package/lib/chrome/ui/navItem.d.ts.map +1 -1
  14. package/lib/chrome/ui/navItem.js.map +1 -1
  15. package/lib/core.d.ts +1 -0
  16. package/lib/core.d.ts.map +1 -1
  17. package/lib/core.js +1 -0
  18. package/lib/core.js.map +1 -1
  19. package/lib/download/core/figureLegend.d.ts +90 -0
  20. package/lib/download/core/figureLegend.d.ts.map +1 -0
  21. package/lib/download/core/figureLegend.js +77 -0
  22. package/lib/download/core/figureLegend.js.map +1 -0
  23. package/lib/download/core/figureSvg.d.ts.map +1 -1
  24. package/lib/download/core/figureSvg.js +71 -1
  25. package/lib/download/core/figureSvg.js.map +1 -1
  26. package/lib/download/core/figureSvgDocument.d.ts +9 -2
  27. package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
  28. package/lib/download/core/figureSvgDocument.js +4 -1
  29. package/lib/download/core/figureSvgDocument.js.map +1 -1
  30. package/lib/download/core/figureTarget.d.ts +11 -0
  31. package/lib/download/core/figureTarget.d.ts.map +1 -1
  32. package/lib/download/core/figureTarget.js +19 -0
  33. package/lib/download/core/figureTarget.js.map +1 -1
  34. package/lib/download/core/index.d.ts +3 -1
  35. package/lib/download/core/index.d.ts.map +1 -1
  36. package/lib/download/core/index.js +2 -1
  37. package/lib/download/core/index.js.map +1 -1
  38. package/lib/ecosystem/core/sites.d.ts +1 -1
  39. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  40. package/lib/ecosystem/core/sites.js +13 -0
  41. package/lib/ecosystem/core/sites.js.map +1 -1
  42. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  43. package/lib/ecosystem/ui/glyphs.js +5 -0
  44. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  45. package/lib/overlay/ui/OverlayLegend.d.ts.map +1 -1
  46. package/lib/overlay/ui/OverlayLegend.js +1 -1
  47. package/lib/overlay/ui/OverlayLegend.js.map +1 -1
  48. package/lib/projection/core/projectionCopy.d.ts +4 -0
  49. package/lib/projection/core/projectionCopy.d.ts.map +1 -1
  50. package/lib/projection/core/projectionCopy.js +3 -0
  51. package/lib/projection/core/projectionCopy.js.map +1 -1
  52. package/lib/projection/core/projectionOptions.d.ts +12 -0
  53. package/lib/projection/core/projectionOptions.d.ts.map +1 -1
  54. package/lib/projection/core/projectionOptions.js +27 -1
  55. package/lib/projection/core/projectionOptions.js.map +1 -1
  56. package/lib/projection/core/projectionStrings.d.ts +4 -0
  57. package/lib/projection/core/projectionStrings.d.ts.map +1 -1
  58. package/lib/projection/core/projectionStrings.js +14 -0
  59. package/lib/projection/core/projectionStrings.js.map +1 -1
  60. package/lib/projection/core/projectionTabDefaults.d.ts.map +1 -1
  61. package/lib/projection/core/projectionTabDefaults.js +12 -0
  62. package/lib/projection/core/projectionTabDefaults.js.map +1 -1
  63. package/lib/projection/core/projectionTabs.d.ts +1 -1
  64. package/lib/projection/core/projectionTabs.d.ts.map +1 -1
  65. package/lib/projection/core/projectionTabs.js +9 -0
  66. package/lib/projection/core/projectionTabs.js.map +1 -1
  67. package/lib/projection/ui/ProjectionMapTab.d.ts +6 -0
  68. package/lib/projection/ui/ProjectionMapTab.d.ts.map +1 -1
  69. package/lib/projection/ui/ProjectionMapTab.js +2 -1
  70. package/lib/projection/ui/ProjectionMapTab.js.map +1 -1
  71. package/lib/projection/ui/ProjectionPanel.d.ts +6 -0
  72. package/lib/projection/ui/ProjectionPanel.d.ts.map +1 -1
  73. package/lib/projection/ui/ProjectionPanel.js +15 -1
  74. package/lib/projection/ui/ProjectionPanel.js.map +1 -1
  75. package/lib/projection/ui/ProjectionReadout.d.ts +7 -0
  76. package/lib/projection/ui/ProjectionReadout.d.ts.map +1 -1
  77. package/lib/projection/ui/ProjectionReadout.js +5 -4
  78. package/lib/projection/ui/ProjectionReadout.js.map +1 -1
  79. package/lib/projection/ui/ProjectionSpaceControls.d.ts +51 -0
  80. package/lib/projection/ui/ProjectionSpaceControls.d.ts.map +1 -0
  81. package/lib/projection/ui/ProjectionSpaceControls.js +71 -0
  82. package/lib/projection/ui/ProjectionSpaceControls.js.map +1 -0
  83. package/lib/projection/ui/ProjectionSpaceMore.d.ts +32 -0
  84. package/lib/projection/ui/ProjectionSpaceMore.d.ts.map +1 -0
  85. package/lib/projection/ui/ProjectionSpaceMore.js +57 -0
  86. package/lib/projection/ui/ProjectionSpaceMore.js.map +1 -0
  87. package/lib/projection/ui/ProjectionSpaceTab.d.ts +98 -0
  88. package/lib/projection/ui/ProjectionSpaceTab.d.ts.map +1 -0
  89. package/lib/projection/ui/ProjectionSpaceTab.js +66 -0
  90. package/lib/projection/ui/ProjectionSpaceTab.js.map +1 -0
  91. package/lib/projection/ui/ProjectionViewer.d.ts +1 -0
  92. package/lib/projection/ui/ProjectionViewer.d.ts.map +1 -1
  93. package/lib/projection/ui/ProjectionViewer.js +2 -1
  94. package/lib/projection/ui/ProjectionViewer.js.map +1 -1
  95. package/lib/projection/ui/index.d.ts +1 -0
  96. package/lib/projection/ui/index.d.ts.map +1 -1
  97. package/lib/projection/ui/index.js.map +1 -1
  98. package/lib/projection/ui/projectionBarModel.d.ts.map +1 -1
  99. package/lib/projection/ui/projectionBarModel.js +3 -2
  100. package/lib/projection/ui/projectionBarModel.js.map +1 -1
  101. package/lib/projection/ui/projectionBarReadings.d.ts.map +1 -1
  102. package/lib/projection/ui/projectionBarReadings.js +17 -0
  103. package/lib/projection/ui/projectionBarReadings.js.map +1 -1
  104. package/lib/projection/ui/projectionBarSlots.d.ts.map +1 -1
  105. package/lib/projection/ui/projectionBarSlots.js +17 -0
  106. package/lib/projection/ui/projectionBarSlots.js.map +1 -1
  107. package/lib/projection/ui/projectionCloudChoices.d.ts +18 -0
  108. package/lib/projection/ui/projectionCloudChoices.d.ts.map +1 -0
  109. package/lib/projection/ui/projectionCloudChoices.js +19 -0
  110. package/lib/projection/ui/projectionCloudChoices.js.map +1 -0
  111. package/lib/projection/ui/projectionEllipsoid.d.ts +38 -0
  112. package/lib/projection/ui/projectionEllipsoid.d.ts.map +1 -0
  113. package/lib/projection/ui/projectionEllipsoid.js +71 -0
  114. package/lib/projection/ui/projectionEllipsoid.js.map +1 -0
  115. package/lib/projection/ui/projectionMapChrome.d.ts +8 -0
  116. package/lib/projection/ui/projectionMapChrome.d.ts.map +1 -1
  117. package/lib/projection/ui/projectionMapChrome.js +10 -6
  118. package/lib/projection/ui/projectionMapChrome.js.map +1 -1
  119. package/lib/projection/ui/projectionMapView.d.ts +13 -0
  120. package/lib/projection/ui/projectionMapView.d.ts.map +1 -1
  121. package/lib/projection/ui/projectionMapView.js +26 -0
  122. package/lib/projection/ui/projectionMapView.js.map +1 -1
  123. package/lib/projection/ui/projectionSpaceModel.d.ts +14 -0
  124. package/lib/projection/ui/projectionSpaceModel.d.ts.map +1 -0
  125. package/lib/projection/ui/projectionSpaceModel.js +14 -0
  126. package/lib/projection/ui/projectionSpaceModel.js.map +1 -0
  127. package/lib/projection/ui/projectionViewerProps.d.ts +34 -0
  128. package/lib/projection/ui/projectionViewerProps.d.ts.map +1 -1
  129. package/lib/scatter/ui/ScatterPlot.d.ts +1 -1
  130. package/lib/scatter/ui/ScatterPlot.d.ts.map +1 -1
  131. package/lib/scatter/ui/ScatterPlot.js +18 -2
  132. package/lib/scatter/ui/ScatterPlot.js.map +1 -1
  133. package/lib/scatter/ui/lassoGesture.d.ts +14 -2
  134. package/lib/scatter/ui/lassoGesture.d.ts.map +1 -1
  135. package/lib/scatter/ui/lassoGesture.js +4 -1
  136. package/lib/scatter/ui/lassoGesture.js.map +1 -1
  137. package/lib/scatter/ui/scatterPlotProps.d.ts +34 -0
  138. package/lib/scatter/ui/scatterPlotProps.d.ts.map +1 -1
  139. package/lib/scatter/ui/useScatterInteraction.d.ts +9 -0
  140. package/lib/scatter/ui/useScatterInteraction.d.ts.map +1 -1
  141. package/lib/scatter/ui/useScatterInteraction.js +9 -1
  142. package/lib/scatter/ui/useScatterInteraction.js.map +1 -1
  143. package/lib/scatter3d/core/cloudGesture.d.ts +17 -0
  144. package/lib/scatter3d/core/cloudGesture.d.ts.map +1 -0
  145. package/lib/scatter3d/core/cloudGesture.js +2 -0
  146. package/lib/scatter3d/core/cloudGesture.js.map +1 -0
  147. package/lib/scatter3d/core/confidenceEllipsoid.d.ts +63 -0
  148. package/lib/scatter3d/core/confidenceEllipsoid.d.ts.map +1 -0
  149. package/lib/scatter3d/core/confidenceEllipsoid.js +128 -0
  150. package/lib/scatter3d/core/confidenceEllipsoid.js.map +1 -0
  151. package/lib/scatter3d/core/cubeFrame.d.ts +40 -0
  152. package/lib/scatter3d/core/cubeFrame.d.ts.map +1 -0
  153. package/lib/scatter3d/core/cubeFrame.js +58 -0
  154. package/lib/scatter3d/core/cubeFrame.js.map +1 -0
  155. package/lib/scatter3d/core/cubePoints.d.ts +35 -0
  156. package/lib/scatter3d/core/cubePoints.d.ts.map +1 -0
  157. package/lib/scatter3d/core/cubePoints.js +50 -0
  158. package/lib/scatter3d/core/cubePoints.js.map +1 -0
  159. package/lib/scatter3d/core/ellipsoidCoverage.d.ts +32 -0
  160. package/lib/scatter3d/core/ellipsoidCoverage.d.ts.map +1 -0
  161. package/lib/scatter3d/core/ellipsoidCoverage.js +83 -0
  162. package/lib/scatter3d/core/ellipsoidCoverage.js.map +1 -0
  163. package/lib/scatter3d/core/ellipsoidSilhouette.d.ts +42 -0
  164. package/lib/scatter3d/core/ellipsoidSilhouette.d.ts.map +1 -0
  165. package/lib/scatter3d/core/ellipsoidSilhouette.js +52 -0
  166. package/lib/scatter3d/core/ellipsoidSilhouette.js.map +1 -0
  167. package/lib/scatter3d/core/index.d.ts +29 -0
  168. package/lib/scatter3d/core/index.d.ts.map +1 -0
  169. package/lib/scatter3d/core/index.js +22 -0
  170. package/lib/scatter3d/core/index.js.map +1 -0
  171. package/lib/scatter3d/core/orbitCamera.d.ts +113 -0
  172. package/lib/scatter3d/core/orbitCamera.d.ts.map +1 -0
  173. package/lib/scatter3d/core/orbitCamera.js +104 -0
  174. package/lib/scatter3d/core/orbitCamera.js.map +1 -0
  175. package/lib/scatter3d/core/symmetricEigen.d.ts +47 -0
  176. package/lib/scatter3d/core/symmetricEigen.d.ts.map +1 -0
  177. package/lib/scatter3d/core/symmetricEigen.js +219 -0
  178. package/lib/scatter3d/core/symmetricEigen.js.map +1 -0
  179. package/lib/scatter3d/ui/CloudFrameLayer.d.ts +33 -0
  180. package/lib/scatter3d/ui/CloudFrameLayer.d.ts.map +1 -0
  181. package/lib/scatter3d/ui/CloudFrameLayer.js +37 -0
  182. package/lib/scatter3d/ui/CloudFrameLayer.js.map +1 -0
  183. package/lib/scatter3d/ui/CloudPointLayer.d.ts +62 -0
  184. package/lib/scatter3d/ui/CloudPointLayer.d.ts.map +1 -0
  185. package/lib/scatter3d/ui/CloudPointLayer.js +67 -0
  186. package/lib/scatter3d/ui/CloudPointLayer.js.map +1 -0
  187. package/lib/scatter3d/ui/CloudShellLayer.d.ts +52 -0
  188. package/lib/scatter3d/ui/CloudShellLayer.d.ts.map +1 -0
  189. package/lib/scatter3d/ui/CloudShellLayer.js +40 -0
  190. package/lib/scatter3d/ui/CloudShellLayer.js.map +1 -0
  191. package/lib/scatter3d/ui/ScatterCloud.d.ts +24 -0
  192. package/lib/scatter3d/ui/ScatterCloud.d.ts.map +1 -0
  193. package/lib/scatter3d/ui/ScatterCloud.js +189 -0
  194. package/lib/scatter3d/ui/ScatterCloud.js.map +1 -0
  195. package/lib/scatter3d/ui/cloudGeometry.d.ts +108 -0
  196. package/lib/scatter3d/ui/cloudGeometry.d.ts.map +1 -0
  197. package/lib/scatter3d/ui/cloudGeometry.js +122 -0
  198. package/lib/scatter3d/ui/cloudGeometry.js.map +1 -0
  199. package/lib/scatter3d/ui/cloudShellStyles.d.ts +36 -0
  200. package/lib/scatter3d/ui/cloudShellStyles.d.ts.map +1 -0
  201. package/lib/scatter3d/ui/cloudShellStyles.js +62 -0
  202. package/lib/scatter3d/ui/cloudShellStyles.js.map +1 -0
  203. package/lib/scatter3d/ui/cloudZoom.d.ts +21 -0
  204. package/lib/scatter3d/ui/cloudZoom.d.ts.map +1 -0
  205. package/lib/scatter3d/ui/cloudZoom.js +22 -0
  206. package/lib/scatter3d/ui/cloudZoom.js.map +1 -0
  207. package/lib/scatter3d/ui/index.d.ts +29 -0
  208. package/lib/scatter3d/ui/index.d.ts.map +1 -0
  209. package/lib/scatter3d/ui/index.js +21 -0
  210. package/lib/scatter3d/ui/index.js.map +1 -0
  211. package/lib/scatter3d/ui/scatterCloudModel.d.ts +60 -0
  212. package/lib/scatter3d/ui/scatterCloudModel.d.ts.map +1 -0
  213. package/lib/scatter3d/ui/scatterCloudModel.js +66 -0
  214. package/lib/scatter3d/ui/scatterCloudModel.js.map +1 -0
  215. package/lib/scatter3d/ui/scatterCloudProps.d.ts +185 -0
  216. package/lib/scatter3d/ui/scatterCloudProps.d.ts.map +1 -0
  217. package/lib/scatter3d/ui/scatterCloudProps.js +9 -0
  218. package/lib/scatter3d/ui/scatterCloudProps.js.map +1 -0
  219. package/lib/scatter3d/ui/useOrbitDrag.d.ts +60 -0
  220. package/lib/scatter3d/ui/useOrbitDrag.d.ts.map +1 -0
  221. package/lib/scatter3d/ui/useOrbitDrag.js +129 -0
  222. package/lib/scatter3d/ui/useOrbitDrag.js.map +1 -0
  223. package/lib/ui.d.ts +1 -0
  224. package/lib/ui.d.ts.map +1 -1
  225. package/lib/ui.js +1 -0
  226. package/lib/ui.js.map +1 -1
  227. package/package.json +1 -1
  228. package/src/chrome/ui/NavLink.tsx +5 -3
  229. package/src/chrome/ui/SiteFooter.tsx +16 -1
  230. package/src/chrome/ui/SiteHeader.tsx +19 -1
  231. package/src/chrome/ui/navItem.ts +12 -0
  232. package/src/core.ts +1 -0
  233. package/src/download/core/figureLegend.ts +167 -0
  234. package/src/download/core/figureSvg.ts +81 -1
  235. package/src/download/core/figureSvgDocument.ts +13 -3
  236. package/src/download/core/figureTarget.ts +21 -0
  237. package/src/download/core/index.ts +8 -0
  238. package/src/ecosystem/core/sites.ts +14 -0
  239. package/src/ecosystem/ui/glyphs.tsx +26 -0
  240. package/src/overlay/ui/OverlayLegend.tsx +7 -1
  241. package/src/projection/core/projectionCopy.ts +9 -0
  242. package/src/projection/core/projectionOptions.ts +48 -3
  243. package/src/projection/core/projectionStrings.ts +16 -0
  244. package/src/projection/core/projectionTabDefaults.ts +12 -0
  245. package/src/projection/core/projectionTabs.ts +10 -1
  246. package/src/projection/ui/ProjectionMapTab.tsx +8 -0
  247. package/src/projection/ui/ProjectionPanel.tsx +63 -0
  248. package/src/projection/ui/ProjectionReadout.tsx +16 -6
  249. package/src/projection/ui/ProjectionSpaceControls.tsx +219 -0
  250. package/src/projection/ui/ProjectionSpaceMore.tsx +169 -0
  251. package/src/projection/ui/ProjectionSpaceTab.tsx +217 -0
  252. package/src/projection/ui/ProjectionViewer.tsx +3 -0
  253. package/src/projection/ui/index.ts +1 -0
  254. package/src/projection/ui/projectionBarModel.ts +3 -2
  255. package/src/projection/ui/projectionBarReadings.ts +18 -0
  256. package/src/projection/ui/projectionBarSlots.tsx +19 -0
  257. package/src/projection/ui/projectionCloudChoices.ts +23 -0
  258. package/src/projection/ui/projectionEllipsoid.ts +78 -0
  259. package/src/projection/ui/projectionMapChrome.ts +26 -5
  260. package/src/projection/ui/projectionMapView.ts +45 -0
  261. package/src/projection/ui/projectionSpaceModel.ts +14 -0
  262. package/src/projection/ui/projectionViewerProps.ts +35 -0
  263. package/src/scatter/ui/ScatterPlot.tsx +21 -2
  264. package/src/scatter/ui/lassoGesture.ts +15 -2
  265. package/src/scatter/ui/scatterPlotProps.ts +35 -0
  266. package/src/scatter/ui/useScatterInteraction.ts +22 -1
  267. package/src/scatter3d/core/cloudGesture.ts +16 -0
  268. package/src/scatter3d/core/confidenceEllipsoid.ts +177 -0
  269. package/src/scatter3d/core/cubeFrame.ts +72 -0
  270. package/src/scatter3d/core/cubePoints.ts +75 -0
  271. package/src/scatter3d/core/ellipsoidCoverage.ts +86 -0
  272. package/src/scatter3d/core/ellipsoidSilhouette.ts +76 -0
  273. package/src/scatter3d/core/index.ts +52 -0
  274. package/src/scatter3d/core/orbitCamera.ts +159 -0
  275. package/src/scatter3d/core/symmetricEigen.ts +273 -0
  276. package/src/scatter3d/ui/CloudFrameLayer.tsx +97 -0
  277. package/src/scatter3d/ui/CloudPointLayer.tsx +132 -0
  278. package/src/scatter3d/ui/CloudShellLayer.tsx +107 -0
  279. package/src/scatter3d/ui/ScatterCloud.tsx +312 -0
  280. package/src/scatter3d/ui/cloudGeometry.ts +200 -0
  281. package/src/scatter3d/ui/cloudShellStyles.ts +74 -0
  282. package/src/scatter3d/ui/cloudZoom.ts +22 -0
  283. package/src/scatter3d/ui/index.ts +41 -0
  284. package/src/scatter3d/ui/scatterCloudModel.ts +98 -0
  285. package/src/scatter3d/ui/scatterCloudProps.ts +191 -0
  286. package/src/scatter3d/ui/useOrbitDrag.ts +220 -0
  287. package/src/ui.ts +1 -0
  288. package/styles/chrome.css +29 -4
@@ -0,0 +1,22 @@
1
+ /**
2
+ * How far the reader may stand back from the box, and how near they may get.
3
+ *
4
+ * A cloud without a zoom cannot be read where its groups overlap, which on a
5
+ * projection is exactly where the reader is looking. The range is kept modest
6
+ * at both ends: past four the frame is off the picture and there is nothing
7
+ * left to say which way is which, and below four tenths the cloud is a smudge
8
+ * in the middle of an empty box.
9
+ */
10
+
11
+ /** How far the wheel may zoom out and in. */
12
+ export const CLOUD_ZOOM_RANGE = { min: 0.4, max: 4 } as const;
13
+
14
+ /**
15
+ * A zoom held inside what the box can usefully be seen at.
16
+ * @param zoom - The zoom asked for.
17
+ * @returns The zoom to draw at; `1` for anything that is not a number.
18
+ */
19
+ export function clampCloudZoom(zoom: number): number {
20
+ if (!Number.isFinite(zoom)) return 1;
21
+ return Math.min(CLOUD_ZOOM_RANGE.max, Math.max(CLOUD_ZOOM_RANGE.min, zoom));
22
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A cloud of samples in a box the reader can turn.
3
+ *
4
+ * It is the `scatter` domain's twin, and deliberately so: the same selection,
5
+ * the same lasso, the same hover card, the same labels and the same group
6
+ * colours, so that a reader who has learned the map has learned this too. What
7
+ * is new is everything the third axis forces — a box to say which way is up, a
8
+ * drag that turns it, glass shells instead of outlines, and one scale across
9
+ * all three axes, because a solid seen from an angle has nowhere to write
10
+ * three sets of tick labels.
11
+ */
12
+
13
+ export type { CloudFrameLayerProps } from './CloudFrameLayer.tsx';
14
+ export { CloudFrameLayer } from './CloudFrameLayer.tsx';
15
+ export type { CloudPointLayerProps } from './CloudPointLayer.tsx';
16
+ export { CloudPointLayer } from './CloudPointLayer.tsx';
17
+ export type { CloudShell, CloudShellLayerProps } from './CloudShellLayer.tsx';
18
+ export { CloudShellLayer } from './CloudShellLayer.tsx';
19
+ export type {
20
+ CloudCloud,
21
+ CloudOutline,
22
+ CloudSegment,
23
+ CloudView,
24
+ } from './cloudGeometry.ts';
25
+ export {
26
+ CLOUD_FILL,
27
+ CLOUD_MARGIN,
28
+ cloudView,
29
+ projectCloud,
30
+ projectSegments,
31
+ projectShellOutline,
32
+ } from './cloudGeometry.ts';
33
+ export type { ShellGlassStop } from './cloudShellStyles.ts';
34
+ export { SHELL_FILL_OPACITY, shellGlassStops } from './cloudShellStyles.ts';
35
+ export { CLOUD_ZOOM_RANGE, clampCloudZoom } from './cloudZoom.ts';
36
+ export type { CloudShellsInput } from './scatterCloudModel.ts';
37
+ export { cloudLabel, cloudShells } from './scatterCloudModel.ts';
38
+ export type { CloudGesture, ScatterCloudProps } from './scatterCloudProps.ts';
39
+ export { ScatterCloud } from './ScatterCloud.tsx';
40
+ export type { OrbitDrag, OrbitDragOptions } from './useOrbitDrag.ts';
41
+ export { useOrbitDrag } from './useOrbitDrag.ts';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * What a cloud is made of before any of it is drawn: the shell around each
3
+ * group, and the sentence a screen reader is given instead of the picture.
4
+ *
5
+ * It is kept out of the component for the same reason the flat scatter's model
6
+ * is — a shell has to be cut before there is anything to project, and cutting
7
+ * it inside the render callback would cut it again on every turn.
8
+ */
9
+
10
+ import type { EllipseSize } from '../../scatter/core/confidenceEllipse.ts';
11
+ import type { ScatterGroupInk } from '../../scatter/ui/scatterPlotModel.ts';
12
+ import type { ScatterGroup } from '../../scatter/ui/scatterPlotProps.ts';
13
+ import { confidenceEllipsoid } from '../core/confidenceEllipsoid.ts';
14
+ import type { Vector3 } from '../core/orbitCamera.ts';
15
+
16
+ import type { CloudShell } from './CloudShellLayer.tsx';
17
+
18
+ /** What {@link cloudShells} needs to fit one shell per group. */
19
+ export interface CloudShellsInput {
20
+ /** Every sample, in cube units. */
21
+ cube: readonly Vector3[];
22
+ /** Which group each sample belongs to, as an index into `groups`. */
23
+ groupOf: ArrayLike<number>;
24
+ /** The groups, in the order they are coloured. */
25
+ groups: readonly ScatterGroup[];
26
+ /** The colour and the strength each group is drawn at. */
27
+ ink: ScatterGroupInk;
28
+ /** How large the shells are. */
29
+ size: EllipseSize;
30
+ /**
31
+ * How many samples a group needs before it is given a shell.
32
+ * @default 4
33
+ */
34
+ minimumPoints?: number;
35
+ }
36
+
37
+ /**
38
+ * One shell per group that has enough samples to have a shape.
39
+ *
40
+ * The shells are fitted in cube units rather than in the data's own, because
41
+ * the three axes were stretched to one cube and a shell fitted before that
42
+ * stretch would not sit on the cloud the reader is looking at. It is the same
43
+ * reasoning the map uses when it fits its outlines in pixels.
44
+ *
45
+ * A group the reader has switched off in the legend keeps its shell and loses
46
+ * its strength, so the shape it was making is still visible behind the groups
47
+ * that are left — which is the whole point of switching one off.
48
+ * @param input - See {@link CloudShellsInput}.
49
+ * @returns The shells, in group order. A group too small for a shape is left out rather than given a shell that describes its handful of samples.
50
+ */
51
+ export function cloudShells(input: CloudShellsInput): CloudShell[] {
52
+ const { cube, groupOf, groups, ink, size, minimumPoints } = input;
53
+
54
+ const held: Vector3[][] = groups.map(() => []);
55
+ const count = Math.min(cube.length, groupOf.length);
56
+ for (let index = 0; index < count; index++) {
57
+ const group = groupOf[index];
58
+ if (group === undefined || group < 0 || group >= groups.length) continue;
59
+ (held[group] as Vector3[]).push(cube[index] as Vector3);
60
+ }
61
+
62
+ const shells: CloudShell[] = [];
63
+ for (const [group, entry] of groups.entries()) {
64
+ const ellipsoid = confidenceEllipsoid(held[group] as Vector3[], {
65
+ size,
66
+ minimumPoints,
67
+ });
68
+ if (ellipsoid === null) continue;
69
+ shells.push({
70
+ id: entry.id,
71
+ color: ink.colors[group] ?? entry.color,
72
+ ellipsoid,
73
+ opacity: ink.opacities[group] ?? 1,
74
+ });
75
+ }
76
+ return shells;
77
+ }
78
+
79
+ /**
80
+ * What a screen reader is told the figure shows.
81
+ *
82
+ * It names all three axes, because a reader who cannot see the box has no
83
+ * frame to read them off, and the frame is where a sighted reader learns which
84
+ * axis is which.
85
+ * @param xLabel - What the axis running left to right is called.
86
+ * @param yLabel - What the axis running bottom to top is called.
87
+ * @param zLabel - What the axis running away from the reader is called.
88
+ * @param count - How many samples there are.
89
+ * @returns The sentence.
90
+ */
91
+ export function cloudLabel(
92
+ xLabel: string,
93
+ yLabel: string,
94
+ zLabel: string,
95
+ count: number,
96
+ ): string {
97
+ return `A three-dimensional scatter plot of ${count} samples: ${xLabel} across, ${yLabel} up, and ${zLabel} into the picture.`;
98
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * What a caller hands a `ScatterCloud`.
3
+ *
4
+ * The types live beside the component rather than inside it because the
5
+ * component's own file would otherwise be mostly prose, exactly as the flat
6
+ * scatter's are.
7
+ */
8
+
9
+ import type { ReactNode } from 'react';
10
+
11
+ import type { EllipseSize } from '../../scatter/core/confidenceEllipse.ts';
12
+ import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
13
+ import type {
14
+ ScatterGroup,
15
+ ScatterPointOpen,
16
+ } from '../../scatter/ui/scatterPlotProps.ts';
17
+ import type { SelectionChange } from '../../scatter/ui/useScatterSelection.ts';
18
+ import type { CloudGesture } from '../core/cloudGesture.ts';
19
+ import type { OrbitCamera } from '../core/orbitCamera.ts';
20
+
21
+ export type { CloudGesture } from '../core/cloudGesture.ts';
22
+
23
+ /** What a `ScatterCloud` needs. */
24
+ export interface ScatterCloudProps {
25
+ /** Where every sample sits along the axis that runs left to right, in data units. */
26
+ x: ArrayLike<number>;
27
+ /** Along the axis that runs bottom to top, in the same order. */
28
+ y: ArrayLike<number>;
29
+ /** Along the axis that runs away from the reader. */
30
+ z: ArrayLike<number>;
31
+ /** Total width, in pixels, from `useContainerSize`. */
32
+ width: number;
33
+ /** Total height. */
34
+ height: number;
35
+ /** What the axis running left to right is called, written on the frame. */
36
+ xLabel: string;
37
+ /** What the axis running bottom to top is called. */
38
+ yLabel: string;
39
+ /** What the axis running away from the reader is called. */
40
+ zLabel: string;
41
+ /**
42
+ * Which group each sample belongs to, as an index into `groups`. A sample
43
+ * with `-1`, or an index outside the range, is drawn in the muted ink and
44
+ * left out of every shell.
45
+ * @default undefined — every sample is one crowd
46
+ */
47
+ groupOf?: ArrayLike<number>;
48
+ /**
49
+ * The groups, in the order they are coloured and listed.
50
+ * @default undefined
51
+ */
52
+ groups?: readonly ScatterGroup[];
53
+ /**
54
+ * Which groups are drawn faint, by id — what a legend entry switches.
55
+ * @default undefined — every group is drawn in full
56
+ */
57
+ mutedGroups?: ReadonlySet<string>;
58
+ /**
59
+ * How large the group shells are, or `null` for none. A share is read in
60
+ * three dimensions here, so the same share gives a wider shell than the same
61
+ * call gives on the map — which is the point of asking for a share rather
62
+ * than for a distance.
63
+ * @default null
64
+ */
65
+ ellipsoid?: EllipseSize | null;
66
+ /**
67
+ * How many samples a group needs before it is given a shell. Below four a
68
+ * group has no volume to measure, so anything less is read as four.
69
+ * @default 4
70
+ */
71
+ ellipsoidMinimumPoints?: number;
72
+ /**
73
+ * How solid the glass is; `0` draws no shells at all.
74
+ * @default SHELL_FILL_OPACITY
75
+ */
76
+ ellipsoidFillOpacity?: number;
77
+ /**
78
+ * Whether each group's name is written once, over the middle of its own
79
+ * shell. It needs `groups`, since it is their names it writes.
80
+ * @default false
81
+ */
82
+ showGroupLabels?: boolean;
83
+ /**
84
+ * What each sample is called, written beside its own dot. A sample whose
85
+ * entry is `undefined` is left unnamed.
86
+ * @default undefined — no sample is named
87
+ */
88
+ pointLabels?: ReadonlyArray<string | undefined>;
89
+ /**
90
+ * Radius of a dot at the middle of the box, in pixels. One at the front is
91
+ * drawn larger and one at the back smaller, about this.
92
+ * @default 3.5
93
+ */
94
+ pointRadius?: number;
95
+ /**
96
+ * The index from which samples are drawn as outlines rather than filled,
97
+ * which is how one the model was fitted on is told from one placed into it
98
+ * afterwards.
99
+ * @default undefined — every sample is filled
100
+ */
101
+ outlinedFrom?: number;
102
+ /**
103
+ * What a drag does. The cloud never changes it on its own, because the
104
+ * control belongs in the bar above the figure rather than on top of the
105
+ * picture, so it is a plain setting rather than a controlled value.
106
+ * @default 'turn'
107
+ */
108
+ gesture?: CloudGesture;
109
+ /**
110
+ * Where the reader is standing. Present, the caller owns the camera, which
111
+ * is how a site puts a viewpoint in a share link.
112
+ * @default undefined — the cloud keeps its own
113
+ */
114
+ camera?: OrbitCamera;
115
+ /**
116
+ * Called with the camera every frame of a turn leaves behind.
117
+ * @default undefined
118
+ */
119
+ onCameraChange?: (camera: OrbitCamera) => void;
120
+ /**
121
+ * How far in the reader has zoomed; `1` is the resting size. Present, the
122
+ * caller owns it.
123
+ * @default undefined — the cloud keeps its own
124
+ */
125
+ zoom?: number;
126
+ /**
127
+ * Called with the zoom a wheel asks for.
128
+ * @default undefined
129
+ */
130
+ onZoomChange?: (zoom: number) => void;
131
+ /**
132
+ * Whether the wheel zooms the box. On, because a cloud that cannot be zoomed
133
+ * cannot be read where its groups overlap; a figure set in a column of prose
134
+ * should turn it off so a reader on their way past keeps their scroll.
135
+ * @default true
136
+ */
137
+ wheelZoom?: boolean;
138
+ /**
139
+ * The selected rows. Present, the caller owns the selection.
140
+ * @default undefined — the cloud keeps its own
141
+ */
142
+ selected?: readonly number[];
143
+ /**
144
+ * The rows selected before the reader touches anything.
145
+ * @default undefined
146
+ */
147
+ defaultSelected?: readonly number[];
148
+ /**
149
+ * Called when a lasso is released or a dot is tapped — never while a lasso
150
+ * is being drawn, and never while the box is being turned.
151
+ * @default undefined
152
+ */
153
+ onSelectionChange?: (change: SelectionChange) => void;
154
+ /**
155
+ * What a gesture with no modifier held does to the selection. Shift always
156
+ * adds and Alt always removes, exactly as on the map.
157
+ * @default 'replace'
158
+ */
159
+ selectMode?: ScatterSelectionMode;
160
+ /**
161
+ * Called with the row under the pointer, or `-1`.
162
+ * @default undefined
163
+ */
164
+ onHoverChange?: (index: number) => void;
165
+ /**
166
+ * Called when the reader double-clicks a sample, exactly as on the flat map:
167
+ * the gesture for "tell me more about this one", or "let me change it".
168
+ *
169
+ * It fires whichever gesture a drag is set to, because a double click is not
170
+ * a drag — a reader turning the box must not have to switch to Select to
171
+ * open a sample. The sample is selected first, since a double click is also
172
+ * two clicks; the map says why, and the two behave alike on purpose.
173
+ * @default undefined
174
+ */
175
+ onPointDoubleClick?: (point: ScatterPointOpen) => void;
176
+ /**
177
+ * What floats over the figure — a bar, a key, a caption, a card.
178
+ * @default undefined
179
+ */
180
+ overlay?: ReactNode;
181
+ /**
182
+ * What a screen reader is told the figure shows.
183
+ * @default a sentence built from the three axis names and the sample count
184
+ */
185
+ label?: string;
186
+ /**
187
+ * Value of the `data-testid` attribute of the wrapper.
188
+ * @default undefined
189
+ */
190
+ testId?: string;
191
+ }
@@ -0,0 +1,220 @@
1
+ import type { PointerEvent as ReactPointerEvent } from 'react';
2
+ import { useCallback, useMemo, useRef, useState } from 'react';
3
+
4
+ import type { ScatterSelectionMode } from '../../scatter/core/scatterSelection.ts';
5
+ import type { ScatterSurfaceProps } from '../../scatter/ui/lassoGesture.ts';
6
+ import {
7
+ capturePointer,
8
+ dropFrame,
9
+ modifierMode,
10
+ scheduleFrame,
11
+ surfacePosition,
12
+ } from '../../scatter/ui/lassoGesture.ts';
13
+ import type { OrbitCamera } from '../core/orbitCamera.ts';
14
+ import { orbitByDrag } from '../core/orbitCamera.ts';
15
+
16
+ /** What {@link useOrbitDrag} needs. */
17
+ export interface OrbitDragOptions {
18
+ /**
19
+ * Whether a drag turns the box at all. Off, the surface still reports a tap,
20
+ * so the reader can pick a sample while the drag belongs to something else.
21
+ * @default true
22
+ */
23
+ enabled?: boolean;
24
+ /** Called with the camera each frame of a drag leaves behind. */
25
+ onOrbit: (turn: (camera: OrbitCamera) => OrbitCamera) => void;
26
+ /**
27
+ * Called when a press and release happened in one place — a click, however
28
+ * the reader's hand shook.
29
+ * @default undefined
30
+ */
31
+ onTap?: (x: number, y: number, mode: ScatterSelectionMode) => void;
32
+ /**
33
+ * Called with the pointer's position on every frame it moves without
34
+ * turning, which is what raises the card under it.
35
+ * @default undefined
36
+ */
37
+ onMove?: (x: number, y: number) => void;
38
+ /**
39
+ * Called when the pointer leaves the surface without a drag in progress.
40
+ * @default undefined
41
+ */
42
+ onLeave?: () => void;
43
+ /**
44
+ * What a tap with no modifier held does to the selection.
45
+ * @default 'replace'
46
+ */
47
+ mode?: ScatterSelectionMode;
48
+ }
49
+
50
+ /** A drag that turns the box, and the taps it lets through. */
51
+ export interface OrbitDrag {
52
+ /** Whether the box is being turned right now. */
53
+ turning: boolean;
54
+ /** Spread onto the transparent rectangle over the figure. */
55
+ surface: ScatterSurfaceProps;
56
+ }
57
+
58
+ /**
59
+ * Turning the box with the pointer.
60
+ *
61
+ * It is the lasso's twin and is built the same way — the pointer is captured
62
+ * on the way down so a drag that wanders off the figure keeps turning it, all
63
+ * three ways a capture can end lead to one function, and moves are coalesced
64
+ * to one a frame because a pointer emits many per frame and each one that
65
+ * reaches the caller repaints every dot and every face.
66
+ *
67
+ * A press that goes nowhere is a tap rather than a turn of no degrees, because
68
+ * nobody presses a mouse button without moving it a pixel or two and a reader
69
+ * who meant to pick a sample must not have to hold their hand still to do it.
70
+ * @param options - See {@link OrbitDragOptions}.
71
+ * @returns The drag. See {@link OrbitDrag}.
72
+ */
73
+ export function useOrbitDrag(options: OrbitDragOptions): OrbitDrag {
74
+ const {
75
+ enabled = true,
76
+ onOrbit,
77
+ onTap,
78
+ onMove,
79
+ onLeave,
80
+ mode: restingMode = 'replace',
81
+ } = options;
82
+
83
+ const pointerRef = useRef<number | null>(null);
84
+ const lastRef = useRef({ x: 0, y: 0 });
85
+ const travelRef = useRef(0);
86
+ const frameRef = useRef<number | null>(null);
87
+ const pendingRef = useRef<PendingTurn | null>(null);
88
+ const [turning, setTurning] = useState(false);
89
+
90
+ const flush = useCallback(() => {
91
+ frameRef.current = null;
92
+ const next = pendingRef.current;
93
+ pendingRef.current = null;
94
+ if (next === null) return;
95
+ if (pointerRef.current === null) {
96
+ onMove?.(next.x, next.y);
97
+ return;
98
+ }
99
+ onOrbit((camera) => orbitByDrag(camera, next.dx, next.dy));
100
+ }, [onMove, onOrbit]);
101
+
102
+ const end = useCallback(
103
+ (event: ReactPointerEvent<Element>, commit: boolean) => {
104
+ if (event.pointerId !== pointerRef.current) return;
105
+ pointerRef.current = null;
106
+ pendingRef.current = null;
107
+ dropFrame(frameRef);
108
+ setTurning(false);
109
+ if (!commit || travelRef.current > TAP_SLACK) return;
110
+ const at = surfacePosition(event, 0, 0);
111
+ onTap?.(at.x, at.y, modifierMode(event, restingMode));
112
+ },
113
+ [onTap, restingMode],
114
+ );
115
+
116
+ const handleDown = useCallback((event: ReactPointerEvent<Element>) => {
117
+ if (pointerRef.current !== null || event.button > 0) return;
118
+ pointerRef.current = event.pointerId;
119
+ lastRef.current = { x: event.clientX, y: event.clientY };
120
+ travelRef.current = 0;
121
+ setTurning(true);
122
+ capturePointer(event);
123
+ }, []);
124
+
125
+ const handleMove = useCallback(
126
+ (event: ReactPointerEvent<Element>) => {
127
+ const active = pointerRef.current;
128
+ if (active === null) {
129
+ const at = surfacePosition(event, 0, 0);
130
+ pendingRef.current = { x: at.x, y: at.y, dx: 0, dy: 0 };
131
+ scheduleFrame(frameRef, flush);
132
+ return;
133
+ }
134
+ if (event.pointerId !== active) return;
135
+ const last = lastRef.current;
136
+ const dx = event.clientX - last.x;
137
+ const dy = event.clientY - last.y;
138
+ lastRef.current = { x: event.clientX, y: event.clientY };
139
+ travelRef.current += Math.abs(dx) + Math.abs(dy);
140
+ if (!enabled) return;
141
+ const waiting = pendingRef.current;
142
+ pendingRef.current = {
143
+ x: 0,
144
+ y: 0,
145
+ dx: (waiting?.dx ?? 0) + dx,
146
+ dy: (waiting?.dy ?? 0) + dy,
147
+ };
148
+ scheduleFrame(frameRef, flush);
149
+ },
150
+ [enabled, flush],
151
+ );
152
+
153
+ const handleUp = useCallback(
154
+ (event: ReactPointerEvent<Element>) => {
155
+ end(event, true);
156
+ },
157
+ [end],
158
+ );
159
+
160
+ const handleAbandon = useCallback(
161
+ (event: ReactPointerEvent<Element>) => {
162
+ end(event, false);
163
+ },
164
+ [end],
165
+ );
166
+
167
+ const handleLeave = useCallback(() => {
168
+ if (pointerRef.current !== null) return;
169
+ pendingRef.current = null;
170
+ onLeave?.();
171
+ }, [onLeave]);
172
+
173
+ const surface = useMemo<ScatterSurfaceProps>(
174
+ () => ({
175
+ onPointerDown: handleDown,
176
+ onPointerMove: handleMove,
177
+ onPointerUp: handleUp,
178
+ onPointerCancel: handleAbandon,
179
+ onLostPointerCapture: handleAbandon,
180
+ onPointerLeave: handleLeave,
181
+ style: {
182
+ cursor: enabled ? (turning ? 'grabbing' : 'grab') : 'default',
183
+ touchAction: 'none',
184
+ },
185
+ }),
186
+ [
187
+ enabled,
188
+ handleAbandon,
189
+ handleDown,
190
+ handleLeave,
191
+ handleMove,
192
+ handleUp,
193
+ turning,
194
+ ],
195
+ );
196
+
197
+ return { turning, surface };
198
+ }
199
+
200
+ /**
201
+ * How far a press may travel and still be a tap, in pixels of total travel.
202
+ *
203
+ * Generous on purpose: a mouse moves a pixel or two under the click of its own
204
+ * button, and a finger moves rather more, so a stricter number turns "pick
205
+ * this sample" into "turn the box by nothing at all" for anybody whose hand is
206
+ * not steady.
207
+ */
208
+ const TAP_SLACK = 5;
209
+
210
+ /** What the pointer has done since the last frame, waiting for the next. */
211
+ interface PendingTurn {
212
+ /** Where it is, for a move that is not turning anything. */
213
+ x: number;
214
+ /** Where it is. */
215
+ y: number;
216
+ /** How far it has moved across since the last frame. */
217
+ dx: number;
218
+ /** How far it has moved down. */
219
+ dy: number;
220
+ }
package/src/ui.ts CHANGED
@@ -18,6 +18,7 @@ export * from './pedagogy/ui/index.ts';
18
18
  export * from './periodic/ui/index.ts';
19
19
  export * from './projection/ui/index.ts';
20
20
  export * from './scatter/ui/index.ts';
21
+ export * from './scatter3d/ui/index.ts';
21
22
  export * from './share/ui/index.ts';
22
23
  export * from './shared/ui/index.ts';
23
24
  export * from './spectra/ui/index.ts';
package/styles/chrome.css CHANGED
@@ -29,13 +29,13 @@
29
29
  --shadow-md: 0 4px 14px rgb(16 32 48 / 10%);
30
30
  --shadow-lg: 0 12px 32px rgb(16 32 48 / 16%);
31
31
 
32
- --header-height: 3.5rem;
32
+ --header-height: 3rem;
33
33
  --page-max: 84rem;
34
34
  }
35
35
 
36
36
  /* The size goes on `body`, never on `html`: `rem` has to stay the browser's
37
37
  16px, or every measurement written here shrinks by 6.25% and the bar renders
38
- 52.5px tall on one site and 56px on its sibling. */
38
+ 45px tall on one site and 48px on its sibling. */
39
39
  body {
40
40
  font-family:
41
41
  -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
@@ -75,6 +75,14 @@ button {
75
75
  gap: 1rem;
76
76
  }
77
77
 
78
+ /* A bar over something that fills the window rather than over a column of
79
+ content. The cap exists so the bar lines up with the page under it; on a
80
+ workbench there is no such column, and the centred bar reads as a mistake —
81
+ the brand floats inwards while the tool below runs to both edges. */
82
+ .app-header__inner--full {
83
+ max-width: none;
84
+ }
85
+
78
86
  .brand {
79
87
  display: flex;
80
88
  align-items: center;
@@ -148,7 +156,7 @@ button {
148
156
  .nav-link {
149
157
  display: inline-flex;
150
158
  align-items: center;
151
- padding: 0.4rem 0.7rem;
159
+ padding: 0.3rem 0.7rem;
152
160
  border: 0;
153
161
  border-radius: 8px;
154
162
  background: none;
@@ -175,6 +183,16 @@ button {
175
183
  color: var(--accent);
176
184
  }
177
185
 
186
+ /* A page the site cannot open yet: still listed, so the reader learns it is
187
+ there, but plainly not pressable. It never takes the hover wash, or it would
188
+ read as reachable under the pointer. */
189
+ .nav-link--disabled,
190
+ .nav-link--disabled:hover {
191
+ background: none;
192
+ color: var(--text-faint);
193
+ cursor: default;
194
+ }
195
+
178
196
  .nav-link svg {
179
197
  width: 14px;
180
198
  height: 14px;
@@ -184,7 +202,8 @@ button {
184
202
  type is brought to the one the links beside them are set in. */
185
203
  .citation-button .bp6-button,
186
204
  .ecosystem-button .bp6-button {
187
- padding: 0.4rem 0.7rem;
205
+ min-height: 0;
206
+ padding: 0.3rem 0.7rem;
188
207
  border-radius: 8px;
189
208
  color: var(--text-muted);
190
209
  font-size: 0.875rem;
@@ -214,6 +233,12 @@ button {
214
233
  margin: 0 auto;
215
234
  }
216
235
 
236
+ /* Under a tool that fills the window, for the reason the bar runs full width
237
+ above it: the two edges of the chrome answer the two edges of the page. */
238
+ .app-footer__inner--full {
239
+ max-width: none;
240
+ }
241
+
217
242
  /* ── Printing ────────────────────────────────────────────────────────────── */
218
243
 
219
244
  /* Students print the cheatsheet. Let them, without the chrome. */