react-cheminfo 0.34.0 → 0.35.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 (171) hide show
  1. package/README.md +28 -28
  2. package/lib/about/core/about.d.ts +1 -30
  3. package/lib/about/core/about.d.ts.map +1 -1
  4. package/lib/about/core/about.js +0 -16
  5. package/lib/about/core/about.js.map +1 -1
  6. package/lib/about/core/index.d.ts +0 -1
  7. package/lib/about/core/index.d.ts.map +1 -1
  8. package/lib/about/core/index.js +0 -1
  9. package/lib/about/core/index.js.map +1 -1
  10. package/lib/about/ui/AboutBuild.d.ts +2 -5
  11. package/lib/about/ui/AboutBuild.d.ts.map +1 -1
  12. package/lib/about/ui/AboutBuild.js +7 -19
  13. package/lib/about/ui/AboutBuild.js.map +1 -1
  14. package/lib/about/ui/AboutPage.d.ts +2 -2
  15. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  16. package/lib/about/ui/AboutPage.js +3 -14
  17. package/lib/about/ui/AboutPage.js.map +1 -1
  18. package/lib/about/ui/AboutVersion.d.ts +0 -4
  19. package/lib/about/ui/AboutVersion.d.ts.map +1 -1
  20. package/lib/about/ui/AboutVersion.js +3 -31
  21. package/lib/about/ui/AboutVersion.js.map +1 -1
  22. package/lib/download/core/index.d.ts +0 -5
  23. package/lib/download/core/index.d.ts.map +1 -1
  24. package/lib/download/core/index.js +0 -3
  25. package/lib/download/core/index.js.map +1 -1
  26. package/lib/download/ui/FigureDownload.d.ts +4 -55
  27. package/lib/download/ui/FigureDownload.d.ts.map +1 -1
  28. package/lib/download/ui/FigureDownload.js +6 -66
  29. package/lib/download/ui/FigureDownload.js.map +1 -1
  30. package/lib/download/ui/FigureDownloadPanel.d.ts +16 -2
  31. package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
  32. package/lib/download/ui/FigureDownloadPanel.js +11 -30
  33. package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
  34. package/lib/download/ui/figureDownloadHint.d.ts +30 -0
  35. package/lib/download/ui/figureDownloadHint.d.ts.map +1 -0
  36. package/lib/download/ui/figureDownloadHint.js +33 -0
  37. package/lib/download/ui/figureDownloadHint.js.map +1 -0
  38. package/lib/download/ui/index.d.ts +3 -10
  39. package/lib/download/ui/index.d.ts.map +1 -1
  40. package/lib/download/ui/index.js +1 -5
  41. package/lib/download/ui/index.js.map +1 -1
  42. package/lib/download/ui/useFigureActions.d.ts +50 -0
  43. package/lib/download/ui/useFigureActions.d.ts.map +1 -0
  44. package/lib/download/ui/useFigureActions.js +58 -0
  45. package/lib/download/ui/useFigureActions.js.map +1 -0
  46. package/lib/download/ui/useFigureDownload.d.ts +85 -0
  47. package/lib/download/ui/useFigureDownload.d.ts.map +1 -0
  48. package/lib/download/ui/useFigureDownload.js +75 -0
  49. package/lib/download/ui/useFigureDownload.js.map +1 -0
  50. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  51. package/lib/ecosystem/core/sites.js +0 -40
  52. package/lib/ecosystem/core/sites.js.map +1 -1
  53. package/lib/ecosystem/core/types.d.ts +0 -10
  54. package/lib/ecosystem/core/types.d.ts.map +1 -1
  55. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  56. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  57. package/lib/ir/core/irCommands.d.ts +2 -2
  58. package/lib/ir/core/irCommands.js +2 -2
  59. package/lib/ir/core/irCommands.js.map +1 -1
  60. package/lib/ir/ui/IrAboutDialog.d.ts.map +1 -1
  61. package/lib/ir/ui/IrAboutDialog.js +1 -1
  62. package/lib/ir/ui/IrAboutDialog.js.map +1 -1
  63. package/lib/ir/ui/IrCanvas.d.ts +6 -0
  64. package/lib/ir/ui/IrCanvas.d.ts.map +1 -1
  65. package/lib/ir/ui/IrCanvas.js +2 -2
  66. package/lib/ir/ui/IrCanvas.js.map +1 -1
  67. package/lib/ir/ui/IrShell.d.ts.map +1 -1
  68. package/lib/ir/ui/IrShell.js +16 -11
  69. package/lib/ir/ui/IrShell.js.map +1 -1
  70. package/lib/ir/ui/IrToolbar.d.ts +11 -1
  71. package/lib/ir/ui/IrToolbar.d.ts.map +1 -1
  72. package/lib/ir/ui/IrToolbar.js +12 -2
  73. package/lib/ir/ui/IrToolbar.js.map +1 -1
  74. package/lib/ir/ui/index.d.ts +1 -0
  75. package/lib/ir/ui/index.d.ts.map +1 -1
  76. package/lib/ir/ui/index.js.map +1 -1
  77. package/lib/locales/de.json +2 -4
  78. package/lib/locales/en.json +2 -4
  79. package/lib/locales/es.json +2 -4
  80. package/lib/locales/fr.json +2 -4
  81. package/lib/locales/it.json +2 -4
  82. package/lib/overlay/ui/OverlayPanel.d.ts +6 -0
  83. package/lib/overlay/ui/OverlayPanel.d.ts.map +1 -1
  84. package/lib/overlay/ui/OverlayPanel.js +2 -2
  85. package/lib/overlay/ui/OverlayPanel.js.map +1 -1
  86. package/lib/overlay/ui/overlayPanelStyles.d.ts +2 -1
  87. package/lib/overlay/ui/overlayPanelStyles.d.ts.map +1 -1
  88. package/lib/overlay/ui/overlayPanelStyles.js +3 -1
  89. package/lib/overlay/ui/overlayPanelStyles.js.map +1 -1
  90. package/lib/periodic/ui/ElementCell.d.ts +0 -6
  91. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  92. package/lib/periodic/ui/ElementCell.js +34 -30
  93. package/lib/periodic/ui/ElementCell.js.map +1 -1
  94. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  95. package/lib/periodic/ui/PeriodicTable.js +12 -15
  96. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  97. package/package.json +1 -1
  98. package/src/about/core/about.ts +1 -51
  99. package/src/about/core/index.ts +0 -1
  100. package/src/about/ui/AboutBuild.tsx +8 -37
  101. package/src/about/ui/AboutPage.tsx +8 -64
  102. package/src/about/ui/AboutVersion.tsx +5 -45
  103. package/src/download/core/index.ts +0 -25
  104. package/src/download/ui/FigureDownload.tsx +11 -148
  105. package/src/download/ui/FigureDownloadPanel.tsx +46 -45
  106. package/src/download/ui/figureDownloadHint.ts +52 -0
  107. package/src/download/ui/index.ts +6 -10
  108. package/src/download/ui/useFigureActions.ts +113 -0
  109. package/src/download/ui/useFigureDownload.tsx +191 -0
  110. package/src/ecosystem/core/sites.ts +0 -40
  111. package/src/ecosystem/core/types.ts +0 -10
  112. package/src/ir/core/irCommands.ts +2 -2
  113. package/src/ir/ui/IrAboutDialog.tsx +0 -13
  114. package/src/ir/ui/IrCanvas.tsx +8 -2
  115. package/src/ir/ui/IrShell.tsx +23 -25
  116. package/src/ir/ui/IrToolbar.tsx +36 -6
  117. package/src/ir/ui/index.ts +1 -0
  118. package/src/locales/de.json +2 -4
  119. package/src/locales/en.json +2 -4
  120. package/src/locales/es.json +2 -4
  121. package/src/locales/fr.json +2 -4
  122. package/src/locales/it.json +2 -4
  123. package/src/overlay/ui/OverlayPanel.tsx +8 -2
  124. package/src/overlay/ui/overlayPanelStyles.ts +3 -0
  125. package/src/periodic/ui/ElementCell.tsx +34 -37
  126. package/src/periodic/ui/PeriodicTable.tsx +12 -17
  127. package/lib/about/core/repository.d.ts +0 -14
  128. package/lib/about/core/repository.d.ts.map +0 -1
  129. package/lib/about/core/repository.js +0 -21
  130. package/lib/about/core/repository.js.map +0 -1
  131. package/lib/download/core/exportStyles.d.ts +0 -22
  132. package/lib/download/core/exportStyles.d.ts.map +0 -1
  133. package/lib/download/core/exportStyles.js +0 -19
  134. package/lib/download/core/exportStyles.js.map +0 -1
  135. package/lib/download/core/imageResolution.d.ts +0 -88
  136. package/lib/download/core/imageResolution.d.ts.map +0 -1
  137. package/lib/download/core/imageResolution.js +0 -77
  138. package/lib/download/core/imageResolution.js.map +0 -1
  139. package/lib/download/core/pictureExport.d.ts +0 -93
  140. package/lib/download/core/pictureExport.d.ts.map +0 -1
  141. package/lib/download/core/pictureExport.js +0 -116
  142. package/lib/download/core/pictureExport.js.map +0 -1
  143. package/lib/download/ui/ExportDialogFrame.d.ts +0 -46
  144. package/lib/download/ui/ExportDialogFrame.d.ts.map +0 -1
  145. package/lib/download/ui/ExportDialogFrame.js +0 -24
  146. package/lib/download/ui/ExportDialogFrame.js.map +0 -1
  147. package/lib/download/ui/ExportImageDialog.d.ts +0 -47
  148. package/lib/download/ui/ExportImageDialog.d.ts.map +0 -1
  149. package/lib/download/ui/ExportImageDialog.js +0 -25
  150. package/lib/download/ui/ExportImageDialog.js.map +0 -1
  151. package/lib/download/ui/ExportNameField.d.ts +0 -29
  152. package/lib/download/ui/ExportNameField.d.ts.map +0 -1
  153. package/lib/download/ui/ExportNameField.js +0 -99
  154. package/lib/download/ui/ExportNameField.js.map +0 -1
  155. package/lib/download/ui/ExportPictureSection.d.ts +0 -38
  156. package/lib/download/ui/ExportPictureSection.d.ts.map +0 -1
  157. package/lib/download/ui/ExportPictureSection.js +0 -48
  158. package/lib/download/ui/ExportPictureSection.js.map +0 -1
  159. package/lib/download/ui/useDrawingBox.d.ts +0 -23
  160. package/lib/download/ui/useDrawingBox.d.ts.map +0 -1
  161. package/lib/download/ui/useDrawingBox.js +0 -21
  162. package/lib/download/ui/useDrawingBox.js.map +0 -1
  163. package/src/about/core/repository.ts +0 -22
  164. package/src/download/core/exportStyles.ts +0 -20
  165. package/src/download/core/imageResolution.ts +0 -118
  166. package/src/download/core/pictureExport.ts +0 -207
  167. package/src/download/ui/ExportDialogFrame.tsx +0 -86
  168. package/src/download/ui/ExportImageDialog.tsx +0 -91
  169. package/src/download/ui/ExportNameField.tsx +0 -148
  170. package/src/download/ui/ExportPictureSection.tsx +0 -142
  171. package/src/download/ui/useDrawingBox.ts +0 -37
@@ -0,0 +1,191 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import { useCallback, useState } from 'react';
3
+
4
+ import { useChromeT } from '../../i18n/ui/useT.ts';
5
+ import type { FigureFormat } from '../core/downloadFigure.ts';
6
+ import type { FigureLayout } from '../core/figureLayout.ts';
7
+ import { figureLayoutRedraws, figureLayoutSize } from '../core/figureLayout.ts';
8
+ import type { FigurePixels } from '../core/figureScale.ts';
9
+ import {
10
+ DEFAULT_FIGURE_SCALE,
11
+ FIGURE_SCALES,
12
+ defaultFigureScale,
13
+ } from '../core/figureScale.ts';
14
+ import { figureSize } from '../core/figureTarget.ts';
15
+
16
+ import { FigureDownloadPanel } from './FigureDownloadPanel.tsx';
17
+ import { useFigureActions } from './useFigureActions.ts';
18
+ import type { FigureRenderer } from './useFigureRedraw.tsx';
19
+ import { useFigureRedraw } from './useFigureRedraw.tsx';
20
+
21
+ /** What the "Save figure" panel is set up with. */
22
+ export interface FigureDownloadOptions {
23
+ /**
24
+ * The `id` of the box the figure is mounted in. Everything drawn inside it
25
+ * is saved — sixteen charts of a pair grid as readily as one scatter plot —
26
+ * and the controls floating over it are left behind.
27
+ */
28
+ targetId: string;
29
+ /**
30
+ * What the saved file is called, without its extension. Name it after the
31
+ * data rather than after the tool: a reader with four of these in a
32
+ * downloads folder cannot tell four `figure.png` apart.
33
+ * @default 'figure'
34
+ */
35
+ fileName?: string;
36
+ /**
37
+ * The format it opens on.
38
+ * @default 'png'
39
+ */
40
+ defaultFormat?: FigureFormat;
41
+ /**
42
+ * The resolution a PNG opens on, as a multiple of the figure on screen. An
43
+ * SVG opens at 1×, the size it has on screen, and each format keeps the
44
+ * multiple the reader last picked for it.
45
+ * @default 2
46
+ */
47
+ defaultScale?: number;
48
+ /**
49
+ * The resolutions offered.
50
+ * @default [1, 2, 3, 4]
51
+ */
52
+ scales?: readonly number[];
53
+ /**
54
+ * What is painted under the figure. `transparent` leaves it unpainted, for a
55
+ * figure going onto a coloured slide.
56
+ * @default the surface the figure is drawn on
57
+ */
58
+ background?: string;
59
+ /**
60
+ * What the panel is called.
61
+ * @default the chrome's own line, in the language of the page
62
+ */
63
+ title?: string;
64
+ /**
65
+ * Draws the figure at a size, for a file. When it is given the panel offers
66
+ * a size — as shown, 4:3, 16:9, a journal column, or typed — and a figure
67
+ * saved at a size other than its own is drawn again at that size, off the
68
+ * page, so its axes and labels are laid out for the new shape rather than
69
+ * stretched into it. Draw exactly what is on screen, with the same data,
70
+ * zoom and colours; only the box changes.
71
+ * @default undefined — the figure is saved at the size it has on screen
72
+ */
73
+ renderFigure?: FigureRenderer;
74
+ }
75
+
76
+ /** What {@link useFigureDownload} hands back. */
77
+ export interface FigureDownloadState {
78
+ /** Whether the panel is showing. */
79
+ isOpen: boolean;
80
+ /**
81
+ * Show or hide the panel. Showing it measures the figure, so hand this to
82
+ * the popover's `onInteraction` rather than a setter of your own.
83
+ */
84
+ setOpen: (open: boolean) => void;
85
+ /** The panel, for the popover's `content`. */
86
+ panel: ReactElement;
87
+ /** The format picked, for a trigger that shows it. */
88
+ format: FigureFormat;
89
+ /**
90
+ * The copy drawn off the page while a figure is saved at another size, to
91
+ * be rendered anywhere in the caller's tree.
92
+ */
93
+ portal: ReactNode;
94
+ }
95
+
96
+ /**
97
+ * The "Save figure" panel — save as PNG or SVG, or copy as PNG — for a caller
98
+ * that brings its own trigger, such as a button in an editor's toolbar.
99
+ * `FigureDownload` is the same panel behind the family's own glyph.
100
+ * @param options - See {@link FigureDownloadOptions}.
101
+ * @returns See {@link FigureDownloadState}.
102
+ */
103
+ export function useFigureDownload(
104
+ options: FigureDownloadOptions,
105
+ ): FigureDownloadState {
106
+ const { targetId, fileName, background, scales = FIGURE_SCALES } = options;
107
+ const { defaultFormat = 'png', defaultScale = DEFAULT_FIGURE_SCALE } =
108
+ options;
109
+ const { title, renderFigure } = options;
110
+
111
+ const t = useChromeT();
112
+ const [isOpen, setIsOpen] = useState(false);
113
+ const [format, setFormat] = useState<FigureFormat>(defaultFormat);
114
+ const [picked, setPicked] = useState<Record<FigureFormat, number>>(() => ({
115
+ png: defaultScale,
116
+ svg: defaultFigureScale('svg'),
117
+ }));
118
+ const scale = picked[format];
119
+ const [size, setSize] = useState<FigurePixels | null>(null);
120
+ const [layout, setLayout] = useState<FigureLayout>('screen');
121
+ const [custom, setCustom] = useState<FigurePixels | null>(null);
122
+ const { redraw, portal } = useFigureRedraw(renderFigure);
123
+ const redrawAt =
124
+ renderFigure !== undefined &&
125
+ size !== null &&
126
+ figureLayoutRedraws(layout, size, custom ?? size)
127
+ ? figureLayoutSize(layout, size, custom ?? size)
128
+ : null;
129
+ const { save, copy, busy, failure, notice, clear } = useFigureActions({
130
+ targetId,
131
+ format,
132
+ scale,
133
+ fileName,
134
+ background,
135
+ redrawAt,
136
+ redraw,
137
+ });
138
+
139
+ // The panel is held open from here rather than left to itself, because the
140
+ // figure has to be measured at the moment it opens: its size is what the
141
+ // panel writes out, and reading a bounding box on every render would cost a
142
+ // layout pass on a bar that is re-rendered on every pointer move.
143
+ const setOpen = useCallback(
144
+ (next: boolean) => {
145
+ setIsOpen(next);
146
+ if (!next) return;
147
+ setSize(figureSize(targetId));
148
+ clear();
149
+ },
150
+ [targetId, clear],
151
+ );
152
+
153
+ // What the last copy came to only describes the choices it was made with.
154
+ function changed<T>(apply: (value: T) => void): (value: T) => void {
155
+ return (value) => {
156
+ clear();
157
+ apply(value);
158
+ };
159
+ }
160
+
161
+ const panel = (
162
+ <FigureDownloadPanel
163
+ title={title ?? t('download.saveFigure')}
164
+ format={format}
165
+ scale={scale}
166
+ scales={scales}
167
+ size={size}
168
+ failure={failure}
169
+ notice={notice}
170
+ saving={busy}
171
+ onFormatChange={changed(setFormat)}
172
+ onScaleChange={changed((next: number) =>
173
+ setPicked((current) => ({ ...current, [format]: next })),
174
+ )}
175
+ onSave={save}
176
+ onCopy={copy}
177
+ sizing={
178
+ renderFigure === undefined
179
+ ? undefined
180
+ : {
181
+ layout,
182
+ custom,
183
+ onLayoutChange: changed(setLayout),
184
+ onCustomChange: changed(setCustom),
185
+ }
186
+ }
187
+ />
188
+ );
189
+
190
+ return { isOpen, setOpen, panel, format, portal };
191
+ }
@@ -20,7 +20,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
20
20
  id: 'learn',
21
21
  name: { lead: 'learn', alt: 'cheminfo', dot: true },
22
22
  host: 'learn.cheminfo.org',
23
- repository: 'https://github.com/cheminfo/learn.cheminfo.org',
24
23
  tagline: 'Arrange the tools into a course and hand out its link.',
25
24
  group: 'computing',
26
25
  brand: '#0d9488',
@@ -31,7 +30,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
31
30
  id: 'inchi',
32
31
  name: { lead: 'inchi', alt: 'cheminfo', dot: true },
33
32
  host: 'inchi.cheminfo.org',
34
- repository: 'https://github.com/cheminfo/inchi.cheminfo.org',
35
33
  tagline: 'InChI and InChIKey from a structure, in the browser.',
36
34
  group: 'structures',
37
35
  brand: '#5b21b6',
@@ -42,7 +40,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
42
40
  id: 'vcl',
43
41
  name: { lead: 'vcl', alt: 'cheminfo', dot: true },
44
42
  host: 'vcl.cheminfo.org',
45
- repository: 'https://github.com/cheminfo/vcl.cheminfo.org',
46
43
  tagline: 'Combine a core and fragments into a screened library.',
47
44
  group: 'research',
48
45
  brand: '#2d72d2',
@@ -53,7 +50,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
53
50
  id: 'smiles',
54
51
  name: { lead: 'smiles', alt: 'cheminfo', dot: true },
55
52
  host: 'smiles.cheminfo.org',
56
- repository: 'https://github.com/cheminfo/smiles.cheminfo.org',
57
53
  tagline: 'Draw a structure, read its SMILES, learn the notation.',
58
54
  group: 'structures',
59
55
  brand: '#1c6e42',
@@ -64,8 +60,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
64
60
  id: 'openbabel',
65
61
  name: { lead: 'openbabel', alt: 'cheminfo', dot: true },
66
62
  host: 'openbabel.cheminfo.org',
67
- repository: 'https://github.com/cheminfo/openbabel.cheminfo.org',
68
- publicRepository: true,
69
63
  tagline: 'Convert a structure between any pair of chemical file formats.',
70
64
  group: 'structures',
71
65
  brand: '#0f766e',
@@ -76,7 +70,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
76
70
  id: 'chemcalc',
77
71
  name: { lead: 'Chem', alt: 'Calc' },
78
72
  host: 'www.chemcalc.org',
79
- repository: 'https://github.com/cheminfo/chemcalc.org',
80
73
  tagline: 'Molecular formula, exact mass and isotopic distribution.',
81
74
  group: 'basics',
82
75
  brand: '#5b52e0',
@@ -87,8 +80,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
87
80
  id: 'dbe',
88
81
  name: { lead: 'dbe', alt: 'cheminfo', dot: true },
89
82
  host: 'dbe.cheminfo.org',
90
- repository: 'https://github.com/cheminfo/dbe.cheminfo.org',
91
- publicRepository: true,
92
83
  tagline: 'Count rings and double bonds from a formula or a structure.',
93
84
  group: 'basics',
94
85
  brand: '#63123f',
@@ -101,8 +92,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
101
92
  id: 'nmrium',
102
93
  name: { lead: 'NMR', alt: 'ium' },
103
94
  host: 'www.nmrium.org',
104
- repository: 'https://github.com/cheminfo/nmrium',
105
- publicRepository: true,
106
95
  tagline: 'Process and assign NMR spectra in the browser.',
107
96
  group: 'spectra',
108
97
  brand: '#ea580c',
@@ -116,7 +105,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
116
105
  // of the cheminfo tools.
117
106
  name: { lead: 'metabo', alt: 'nmrium', dot: true },
118
107
  host: 'metabo.nmrium.com',
119
- repository: 'https://github.com/cheminfo/metabo',
120
108
  tagline: 'Turn a batch of NMR spectra into a map of your samples.',
121
109
  group: 'spectra',
122
110
  brand: '#4d7c0f',
@@ -129,7 +117,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
129
117
  id: 'derepflow',
130
118
  name: { lead: 'Derep', alt: 'Flow', ink: 'lead' },
131
119
  host: 'derepflow.cheminfo.org',
132
- repository: 'https://github.com/cheminfo/derepflow',
133
120
  tagline: 'Identify the constituents of a mixture from its ¹³C spectrum.',
134
121
  group: 'spectra',
135
122
  brand: '#127ba3',
@@ -140,8 +127,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
140
127
  id: 'surge',
141
128
  name: { lead: 'surge', alt: 'cheminfo', dot: true },
142
129
  host: 'surge.cheminfo.org',
143
- repository: 'https://github.com/cheminfo/surge.cheminfo.org',
144
- publicRepository: true,
145
130
  tagline: 'Every constitutional isomer of a molecular formula.',
146
131
  group: 'structures',
147
132
  brand: '#4338ca',
@@ -152,8 +137,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
152
137
  id: 'tex',
153
138
  name: { lead: 'tex', alt: 'cheminfo', dot: true },
154
139
  host: 'tex.cheminfo.org',
155
- repository: 'https://github.com/cheminfo/tex.cheminfo.org',
156
- publicRepository: true,
157
140
  tagline: 'LaTeX formulas rendered to SVG or PNG from a URL.',
158
141
  group: 'computing',
159
142
  brand: '#a21caf',
@@ -164,7 +147,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
164
147
  id: 'lcao',
165
148
  name: { lead: 'lcao', alt: 'cheminfo', dot: true },
166
149
  host: 'lcao.cheminfo.org',
167
- repository: 'https://github.com/cheminfo/lcao.cheminfo.org',
168
150
  tagline: 'Combine atomic orbitals into molecular ones.',
169
151
  group: 'basics',
170
152
  brand: '#1565c0',
@@ -175,8 +157,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
175
157
  id: 'regexp',
176
158
  name: { lead: 'regexp', alt: 'cheminfo', dot: true },
177
159
  host: 'regexp.cheminfo.org',
178
- repository: 'https://github.com/cheminfo/regexp.cheminfo.org',
179
- publicRepository: true,
180
160
  tagline: 'Learn regular expressions in a live playground.',
181
161
  group: 'computing',
182
162
  brand: '#1e3a8a',
@@ -187,8 +167,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
187
167
  id: 'pdb',
188
168
  name: { lead: 'pdb', alt: 'cheminfo', dot: true },
189
169
  host: 'pdb.cheminfo.org',
190
- repository: 'https://github.com/cheminfo/pdb.cheminfo.org',
191
- publicRepository: true,
192
170
  tagline: 'A fast look at any Protein Data Bank entry.',
193
171
  group: 'research',
194
172
  brand: '#2563eb',
@@ -199,8 +177,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
199
177
  id: 'elucidation',
200
178
  name: { lead: 'elucidation', alt: 'cheminfo', dot: true },
201
179
  host: 'elucidation.cheminfo.org',
202
- repository: 'https://github.com/cheminfo/elucidation.cheminfo.org',
203
- publicRepository: true,
204
180
  tagline: 'A structure from a 1H NMR spectrum and a formula.',
205
181
  group: 'spectra',
206
182
  brand: '#7e22ce',
@@ -211,7 +187,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
211
187
  id: 'equilibrium',
212
188
  name: { lead: 'Equi', alt: 'Librium' },
213
189
  host: 'equilibrium.cheminfo.org',
214
- repository: 'https://github.com/cheminfo/equilibrium.cheminfo.org',
215
190
  tagline: 'Chemical equilibria: pH, speciation and titration curves.',
216
191
  group: 'basics',
217
192
  brand: '#0b5754',
@@ -222,8 +197,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
222
197
  id: 'polycarp',
223
198
  name: { lead: 'Poly', alt: 'Carp' },
224
199
  host: 'polycarp.cheminfo.org',
225
- repository: 'https://github.com/cheminfo/polycarp.cheminfo.org',
226
- publicRepository: true,
227
200
  tagline: 'Predict the microstructure of a radical copolymerisation.',
228
201
  group: 'research',
229
202
  brand: '#701a75',
@@ -234,7 +207,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
234
207
  id: '3d',
235
208
  name: { lead: '3d', alt: 'cheminfo', dot: true },
236
209
  host: '3d.cheminfo.org',
237
- repository: 'https://github.com/cheminfo/3d.cheminfo.org',
238
210
  tagline: 'Conformers in 3D from a structure you draw.',
239
211
  group: 'structures',
240
212
  brand: '#155e75',
@@ -245,7 +217,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
245
217
  id: 'periodic-table',
246
218
  name: { lead: 'Periodic', alt: 'Table' },
247
219
  host: 'periodic-table.cheminfo.org',
248
- repository: 'https://github.com/cheminfo/periodic-table.cheminfo.org',
249
220
  tagline: 'The periodic table, coloured, plotted and taken apart.',
250
221
  group: 'basics',
251
222
  brand: '#15803d',
@@ -256,8 +227,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
256
227
  id: 'database',
257
228
  name: { lead: 'database', alt: 'cheminfo', dot: true },
258
229
  host: 'database.cheminfo.org',
259
- repository: 'https://github.com/cheminfo/database.cheminfo.org',
260
- publicRepository: true,
261
230
  tagline: 'Query a real chemical dataset in SQL and in Mango.',
262
231
  group: 'research',
263
232
  brand: '#7c2d12',
@@ -268,8 +237,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
268
237
  id: 'symmetry',
269
238
  name: { lead: 'Symme', alt: 'Try' },
270
239
  host: 'symmetry.cheminfo.org',
271
- repository: 'https://github.com/cheminfo/symmetry.cheminfo.org',
272
- publicRepository: true,
273
240
  tagline: 'Find the symmetry of a molecule, a crystal or a pattern.',
274
241
  group: 'basics',
275
242
  brand: '#9f1239',
@@ -282,8 +249,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
282
249
  id: 'osiris',
283
250
  name: { lead: 'osiris', alt: 'cheminfo', dot: true },
284
251
  host: 'osiris.cheminfo.org',
285
- repository: 'https://github.com/cheminfo/osiris.cheminfo.org',
286
- publicRepository: true,
287
252
  tagline: 'Predicted toxicity risks and properties, one molecule or many.',
288
253
  group: 'research',
289
254
  brand: '#854d0e',
@@ -297,8 +262,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
297
262
  id: 'atoms',
298
263
  name: { lead: 'atoms', alt: 'cheminfo', dot: true },
299
264
  host: 'atoms.cheminfo.org',
300
- repository: 'https://github.com/cheminfo/atoms.cheminfo.org',
301
- publicRepository: true,
302
265
  tagline: 'Electrons, neutrons, Lewis structures and oxidation states.',
303
266
  group: 'practice',
304
267
  brand: '#b91c1c',
@@ -311,8 +274,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
311
274
  id: 'moles',
312
275
  name: { lead: 'moles', alt: 'cheminfo', dot: true },
313
276
  host: 'moles.cheminfo.org',
314
- repository: 'https://github.com/cheminfo/moles.cheminfo.org',
315
- publicRepository: true,
316
277
  tagline: 'Balance a reaction, and work out its mass composition.',
317
278
  group: 'practice',
318
279
  brand: '#334155',
@@ -325,7 +286,6 @@ export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
325
286
  id: 'inorganic',
326
287
  name: { lead: 'inorganic', alt: 'cheminfo', dot: true },
327
288
  host: 'inorganic.cheminfo.org',
328
- repository: 'https://github.com/cheminfo/inorganic.cheminfo.org',
329
289
  tagline: 'Name an inorganic compound, and write its formula.',
330
290
  group: 'practice',
331
291
  brand: '#a3620a',
@@ -65,16 +65,6 @@ export interface SiteRecord {
65
65
  name: SiteName;
66
66
  /** Where the site lives, written the way it is typed. */
67
67
  host: string;
68
- /** Where the sources live, and what a Source link opens. */
69
- repository: string;
70
- /**
71
- * Whether a visitor can open that repository. A private one is named
72
- * nowhere: no licence-and-source section, no link from the version, no issue
73
- * tracker a reader cannot reach. It is stated rather than assumed, so a site
74
- * whose record forgets it advertises nothing rather than a dead link.
75
- * @default false
76
- */
77
- publicRepository?: boolean;
78
68
  /** One line on what the site does. */
79
69
  tagline: string;
80
70
  /**
@@ -120,10 +120,10 @@ export const irCommands = {
120
120
  'Writes what each labelled band might be under its wavenumber. Worth turning off once the chart is crowded.',
121
121
  },
122
122
  exportImage: {
123
- title: 'Export as an image…',
123
+ title: 'Save figure…',
124
124
  shortcut: { keys: ['x'] },
125
125
  description:
126
- 'The chart as it stands, at a chosen resolution — PNG, or SVG, which is drawn again at whatever size it is printed rather than enlarged.',
126
+ 'The chart as it stands, saved as a PNG or an SVG at a chosen resolution, or copied as a PNG.',
127
127
  },
128
128
  clear: {
129
129
  title: 'Close every spectrum',
@@ -82,19 +82,6 @@ export function IrAboutDialog(props: IrAboutDialogProps) {
82
82
  reader with a colour vision deficiency.
83
83
  </li>
84
84
  </ul>
85
-
86
- <h4 style={headingStyle}>Licence</h4>
87
- <p style={paragraphStyle}>
88
- MIT, in the{' '}
89
- <a
90
- href="https://github.com/cheminfo/glycans"
91
- target="_blank"
92
- rel="noopener noreferrer"
93
- >
94
- cheminfo/glycans
95
- </a>{' '}
96
- monorepo.
97
- </p>
98
85
  </DialogBody>
99
86
  </Dialog>
100
87
  );
@@ -26,6 +26,12 @@ export interface IrCanvasProps {
26
26
  * @default undefined
27
27
  */
28
28
  attachChart?: (element: HTMLDivElement | null) => void;
29
+ /**
30
+ * The `id` put on the box the chart is drawn in, which the "Save figure"
31
+ * panel is pointed at.
32
+ * @default undefined
33
+ */
34
+ chartId?: string;
29
35
  }
30
36
 
31
37
  /**
@@ -44,7 +50,7 @@ export interface IrCanvasProps {
44
50
  * @returns The canvas.
45
51
  */
46
52
  export function IrCanvas(props: IrCanvasProps) {
47
- const { highlight = null, onHighlight, attachChart } = props;
53
+ const { highlight = null, onHighlight, attachChart, chartId } = props;
48
54
  const { state, visibleSpectra, assigned } = useIrEditorState();
49
55
  const actions = useIrActions();
50
56
  const { domain, tool, selectedId } = state.view;
@@ -59,7 +65,7 @@ export function IrCanvas(props: IrCanvasProps) {
59
65
 
60
66
  return (
61
67
  <div style={rootStyle}>
62
- <div ref={attachChart} style={chartStyle}>
68
+ <div ref={attachChart} id={chartId} style={chartStyle}>
63
69
  <IrChart
64
70
  spectra={visibleSpectra}
65
71
  mode={mode}
@@ -1,10 +1,15 @@
1
1
  import type { Ref } from 'react';
2
- import { useCallback, useImperativeHandle, useMemo, useState } from 'react';
2
+ import {
3
+ useCallback,
4
+ useId,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useState,
8
+ } from 'react';
3
9
  import { SplitPane, useFullscreen } from 'react-science/ui';
4
10
 
5
11
  import { sanitizeFileName } from '../../download/core/sanitizeFileName.ts';
6
- import { ExportImageDialog } from '../../download/ui/ExportImageDialog.tsx';
7
- import { useDrawingBox } from '../../download/ui/useDrawingBox.ts';
12
+ import { useFigureDownload } from '../../download/ui/useFigureDownload.tsx';
8
13
  import { PanelRail } from '../../panel/ui/PanelRail.tsx';
9
14
  import type { IrBand } from '../core/irBand.ts';
10
15
  import type { IrCommandHandlers } from '../core/irCommands.ts';
@@ -58,12 +63,16 @@ export function IrShell(props: IrShellProps) {
58
63
 
59
64
  const [isAboutOpen, setIsAboutOpen] = useState(false);
60
65
  const [isDocumentationOpen, setIsDocumentationOpen] = useState(false);
61
- const [isExportImageOpen, setIsExportImageOpen] = useState(false);
62
66
 
63
- // The chart is the picture, so the box it is drawn in is what the export
64
- // dialog is pointed at: the viewer around it is full of SVG — every icon in
65
- // every toolbar is one — and the first found there would be a magnifier.
66
- const chart = useDrawingBox();
67
+ // The chart is the picture, so the box it is drawn in is what the figure
68
+ // panel is pointed at: the viewer around it is full of SVG — every icon in
69
+ // every toolbar is one.
70
+ const chartId = useId();
71
+ const figure = useFigureDownload({
72
+ targetId: chartId,
73
+ fileName: pictureName(visibleSpectra, selectedSpectrum),
74
+ });
75
+ const { isOpen: isFigureOpen, setOpen: setFigureOpen } = figure;
67
76
  // The highlight is held here rather than in the reducer: what the pointer is
68
77
  // resting on is not part of the document and changes on every move across the
69
78
  // chart, so putting it through the reducer would run every panel's memo sixty
@@ -106,7 +115,7 @@ export function IrShell(props: IrShellProps) {
106
115
  togglePicking: () => actions.setSettings({ pickBands: !pickBands }),
107
116
  toggleAssignments: () =>
108
117
  actions.setSettings({ showAssignments: !showAssignments }),
109
- exportImage: () => setIsExportImageOpen(true),
118
+ exportImage: () => setFigureOpen(!isFigureOpen),
110
119
  clear: () => actions.clear(),
111
120
  }),
112
121
  [
@@ -117,6 +126,8 @@ export function IrShell(props: IrShellProps) {
117
126
  showAssignments,
118
127
  scaleValueAxis,
119
128
  toggleFullScreen,
129
+ isFigureOpen,
130
+ setFigureOpen,
120
131
  ],
121
132
  );
122
133
 
@@ -176,7 +187,7 @@ export function IrShell(props: IrShellProps) {
176
187
  default: in a narrow viewer the row would take the width it is
177
188
  short of out of them, and half an icon is what that looks like. */}
178
189
  <div style={railStyle}>
179
- <IrToolbar />
190
+ <IrToolbar figure={figure} />
180
191
  </div>
181
192
 
182
193
  <SplitPane
@@ -189,7 +200,7 @@ export function IrShell(props: IrShellProps) {
189
200
  <IrCanvas
190
201
  highlight={highlight}
191
202
  onHighlight={setHighlight}
192
- attachChart={chart.attach}
203
+ chartId={chartId}
193
204
  />
194
205
 
195
206
  <IrSidePanel highlight={highlight} onHighlight={setHighlight} />
@@ -215,19 +226,6 @@ export function IrShell(props: IrShellProps) {
215
226
  isOpen={isDocumentationOpen}
216
227
  onClose={() => setIsDocumentationOpen(false)}
217
228
  />
218
-
219
- {/* `element`, not `content`: the chart is drawn to the box it was given,
220
- so the box is the picture — measuring the marks inside it would crop
221
- the frame to wherever the trace happens to reach and leave the axes
222
- hanging. */}
223
- <ExportImageDialog
224
- isOpen={isExportImageOpen}
225
- getDrawing={chart.getDrawing}
226
- frame="element"
227
- filename={pictureName(visibleSpectra, selectedSpectrum)}
228
- label="Infrared spectrum"
229
- onClose={() => setIsExportImageOpen(false)}
230
- />
231
229
  </IrCommandProvider>
232
230
  );
233
231
  }
@@ -245,7 +243,7 @@ const ZOOM_STEP = 1.2;
245
243
  const DEFAULT_FRAME_WIDTH = 200;
246
244
 
247
245
  /**
248
- * What a picture of the chart is called before anything is typed in the box.
246
+ * What a picture of the chart is called.
249
247
  *
250
248
  * The spectrum being looked at, since a figure of the chart is a figure of that
251
249
  * spectrum, and a chemist who has one run open should not have to work out
@@ -1,10 +1,20 @@
1
1
  import { Toolbar } from 'react-science/ui';
2
2
 
3
+ import type { FigureDownloadState } from '../../download/ui/useFigureDownload.tsx';
3
4
  import { boxZoomOnly } from '../core/irMode.ts';
4
5
 
5
6
  import { useIrCommandProps } from './irCommandContext.tsx';
6
7
  import { useIrEditorState } from './irStateContext.ts';
7
8
 
9
+ /** What {@link IrToolbar} needs. */
10
+ export interface IrToolbarProps {
11
+ /**
12
+ * The "Save figure" panel the picture button opens.
13
+ * @default undefined — the button only runs the `exportImage` command
14
+ */
15
+ figure?: FigureDownloadState;
16
+ }
17
+
8
18
  /**
9
19
  * The tools, down the left of the chart.
10
20
  *
@@ -19,9 +29,11 @@ import { useIrEditorState } from './irStateContext.ts';
19
29
  * drag on that axis is a rectangle, so both buttons go dead with the square zoom
20
30
  * lit. Showing `read` as available there would offer a gesture the chart does not
21
31
  * make, and showing it as *active* would name the wrong one.
32
+ * @param props - Component props.
22
33
  * @returns The toolbar.
23
34
  */
24
- export function IrToolbar() {
35
+ export function IrToolbar(props: IrToolbarProps) {
36
+ const { figure } = props;
25
37
  const { state, selectedSpectrum } = useIrEditorState();
26
38
  const command = useIrCommandProps();
27
39
  const { spectra } = state.data;
@@ -29,6 +41,8 @@ export function IrToolbar() {
29
41
  const { mode, pickBands, showAssignments } = state.settings;
30
42
 
31
43
  const isEmpty = spectra.length === 0;
44
+ // The popover opens the panel itself, so the command's own click is left out.
45
+ const exportCommand = command('exportImage');
32
46
  const settled = boxZoomOnly(mode);
33
47
 
34
48
  return (
@@ -92,11 +106,27 @@ export function IrToolbar() {
92
106
  window it was dragged to, the way up it is being read, the
93
107
  assignments written under the bands — so the picture is taken here
94
108
  rather than rebuilt from the spectra in a panel. */}
95
- <Toolbar.Item
96
- icon="media"
97
- {...command('exportImage')}
98
- disabled={isEmpty}
99
- />
109
+ {figure === undefined ? (
110
+ <Toolbar.Item
111
+ icon="media"
112
+ {...command('exportImage')}
113
+ disabled={isEmpty}
114
+ />
115
+ ) : (
116
+ <Toolbar.PopoverItem
117
+ isOpen={figure.isOpen}
118
+ onInteraction={figure.setOpen}
119
+ content={figure.panel}
120
+ disabled={isEmpty}
121
+ itemProps={{
122
+ icon: 'media',
123
+ tooltip: exportCommand.tooltip,
124
+ 'aria-label': exportCommand['aria-label'],
125
+ active: figure.isOpen,
126
+ disabled: isEmpty,
127
+ }}
128
+ />
129
+ )}
100
130
  </Toolbar>
101
131
  );
102
132
  }
@@ -34,6 +34,7 @@ export { IrSidePanel } from './IrSidePanel.tsx';
34
34
  export type { IrStateProviderProps } from './IrStateProvider.tsx';
35
35
  export { IrStateProvider } from './IrStateProvider.tsx';
36
36
  export { IrStatusBar } from './IrStatusBar.tsx';
37
+ export type { IrToolbarProps } from './IrToolbar.tsx';
37
38
  export { IrToolbar } from './IrToolbar.tsx';
38
39
  export type { IrTracesProps } from './IrTraces.tsx';
39
40
  export { IrTraces } from './IrTraces.tsx';