react-cheminfo 0.33.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 (248) 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/axis/core/axisMarks.d.ts +54 -0
  23. package/lib/axis/core/axisMarks.d.ts.map +1 -0
  24. package/lib/axis/core/axisMarks.js +69 -0
  25. package/lib/axis/core/axisMarks.js.map +1 -0
  26. package/lib/axis/core/axisScale.d.ts +31 -0
  27. package/lib/axis/core/axisScale.d.ts.map +1 -0
  28. package/lib/axis/core/axisScale.js +31 -0
  29. package/lib/axis/core/axisScale.js.map +1 -0
  30. package/lib/axis/core/index.d.ts +16 -0
  31. package/lib/axis/core/index.d.ts.map +1 -0
  32. package/lib/axis/core/index.js +13 -0
  33. package/lib/axis/core/index.js.map +1 -0
  34. package/lib/axis/core/logAxisBounds.d.ts +78 -0
  35. package/lib/axis/core/logAxisBounds.d.ts.map +1 -0
  36. package/lib/axis/core/logAxisBounds.js +129 -0
  37. package/lib/axis/core/logAxisBounds.js.map +1 -0
  38. package/lib/axis/ui/AxisScaleControl.d.ts +66 -0
  39. package/lib/axis/ui/AxisScaleControl.d.ts.map +1 -0
  40. package/lib/axis/ui/AxisScaleControl.js +26 -0
  41. package/lib/axis/ui/AxisScaleControl.js.map +1 -0
  42. package/lib/axis/ui/index.d.ts +6 -0
  43. package/lib/axis/ui/index.d.ts.map +1 -0
  44. package/lib/axis/ui/index.js +5 -0
  45. package/lib/axis/ui/index.js.map +1 -0
  46. package/lib/core.d.ts +1 -0
  47. package/lib/core.d.ts.map +1 -1
  48. package/lib/core.js +1 -0
  49. package/lib/core.js.map +1 -1
  50. package/lib/delimited/core/index.d.ts +2 -0
  51. package/lib/delimited/core/index.d.ts.map +1 -1
  52. package/lib/delimited/core/index.js +1 -0
  53. package/lib/delimited/core/index.js.map +1 -1
  54. package/lib/delimited/core/tableRows.d.ts +49 -0
  55. package/lib/delimited/core/tableRows.d.ts.map +1 -0
  56. package/lib/delimited/core/tableRows.js +63 -0
  57. package/lib/delimited/core/tableRows.js.map +1 -0
  58. package/lib/delimited/ui/DelimitedTextPanel.d.ts +9 -0
  59. package/lib/delimited/ui/DelimitedTextPanel.d.ts.map +1 -1
  60. package/lib/delimited/ui/DelimitedTextPanel.js +3 -2
  61. package/lib/delimited/ui/DelimitedTextPanel.js.map +1 -1
  62. package/lib/delimited/ui/TableDataButton.d.ts +96 -0
  63. package/lib/delimited/ui/TableDataButton.d.ts.map +1 -0
  64. package/lib/delimited/ui/TableDataButton.js +39 -0
  65. package/lib/delimited/ui/TableDataButton.js.map +1 -0
  66. package/lib/delimited/ui/index.d.ts +2 -0
  67. package/lib/delimited/ui/index.d.ts.map +1 -1
  68. package/lib/delimited/ui/index.js +1 -0
  69. package/lib/delimited/ui/index.js.map +1 -1
  70. package/lib/download/core/index.d.ts +0 -5
  71. package/lib/download/core/index.d.ts.map +1 -1
  72. package/lib/download/core/index.js +0 -3
  73. package/lib/download/core/index.js.map +1 -1
  74. package/lib/download/ui/FigureDownload.d.ts +4 -55
  75. package/lib/download/ui/FigureDownload.d.ts.map +1 -1
  76. package/lib/download/ui/FigureDownload.js +6 -66
  77. package/lib/download/ui/FigureDownload.js.map +1 -1
  78. package/lib/download/ui/FigureDownloadPanel.d.ts +16 -2
  79. package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
  80. package/lib/download/ui/FigureDownloadPanel.js +11 -30
  81. package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
  82. package/lib/download/ui/figureDownloadHint.d.ts +30 -0
  83. package/lib/download/ui/figureDownloadHint.d.ts.map +1 -0
  84. package/lib/download/ui/figureDownloadHint.js +33 -0
  85. package/lib/download/ui/figureDownloadHint.js.map +1 -0
  86. package/lib/download/ui/index.d.ts +3 -10
  87. package/lib/download/ui/index.d.ts.map +1 -1
  88. package/lib/download/ui/index.js +1 -5
  89. package/lib/download/ui/index.js.map +1 -1
  90. package/lib/download/ui/useFigureActions.d.ts +50 -0
  91. package/lib/download/ui/useFigureActions.d.ts.map +1 -0
  92. package/lib/download/ui/useFigureActions.js +58 -0
  93. package/lib/download/ui/useFigureActions.js.map +1 -0
  94. package/lib/download/ui/useFigureDownload.d.ts +85 -0
  95. package/lib/download/ui/useFigureDownload.d.ts.map +1 -0
  96. package/lib/download/ui/useFigureDownload.js +75 -0
  97. package/lib/download/ui/useFigureDownload.js.map +1 -0
  98. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  99. package/lib/ecosystem/core/sites.js +0 -40
  100. package/lib/ecosystem/core/sites.js.map +1 -1
  101. package/lib/ecosystem/core/types.d.ts +0 -10
  102. package/lib/ecosystem/core/types.d.ts.map +1 -1
  103. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  104. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  105. package/lib/ir/core/irCommands.d.ts +2 -2
  106. package/lib/ir/core/irCommands.js +2 -2
  107. package/lib/ir/core/irCommands.js.map +1 -1
  108. package/lib/ir/ui/IrAboutDialog.d.ts.map +1 -1
  109. package/lib/ir/ui/IrAboutDialog.js +1 -1
  110. package/lib/ir/ui/IrAboutDialog.js.map +1 -1
  111. package/lib/ir/ui/IrCanvas.d.ts +6 -0
  112. package/lib/ir/ui/IrCanvas.d.ts.map +1 -1
  113. package/lib/ir/ui/IrCanvas.js +2 -2
  114. package/lib/ir/ui/IrCanvas.js.map +1 -1
  115. package/lib/ir/ui/IrShell.d.ts.map +1 -1
  116. package/lib/ir/ui/IrShell.js +16 -11
  117. package/lib/ir/ui/IrShell.js.map +1 -1
  118. package/lib/ir/ui/IrToolbar.d.ts +11 -1
  119. package/lib/ir/ui/IrToolbar.d.ts.map +1 -1
  120. package/lib/ir/ui/IrToolbar.js +12 -2
  121. package/lib/ir/ui/IrToolbar.js.map +1 -1
  122. package/lib/ir/ui/index.d.ts +1 -0
  123. package/lib/ir/ui/index.d.ts.map +1 -1
  124. package/lib/ir/ui/index.js.map +1 -1
  125. package/lib/locales/de.json +8 -4
  126. package/lib/locales/en.json +8 -4
  127. package/lib/locales/es.json +8 -4
  128. package/lib/locales/fr.json +8 -4
  129. package/lib/locales/it.json +8 -4
  130. package/lib/overlay/ui/OverlayPanel.d.ts +6 -0
  131. package/lib/overlay/ui/OverlayPanel.d.ts.map +1 -1
  132. package/lib/overlay/ui/OverlayPanel.js +2 -2
  133. package/lib/overlay/ui/OverlayPanel.js.map +1 -1
  134. package/lib/overlay/ui/overlayPanelStyles.d.ts +2 -1
  135. package/lib/overlay/ui/overlayPanelStyles.d.ts.map +1 -1
  136. package/lib/overlay/ui/overlayPanelStyles.js +3 -1
  137. package/lib/overlay/ui/overlayPanelStyles.js.map +1 -1
  138. package/lib/periodic/core/index.d.ts +1 -1
  139. package/lib/periodic/core/index.d.ts.map +1 -1
  140. package/lib/periodic/core/index.js +1 -1
  141. package/lib/periodic/core/index.js.map +1 -1
  142. package/lib/periodic/core/layout.d.ts +18 -0
  143. package/lib/periodic/core/layout.d.ts.map +1 -1
  144. package/lib/periodic/core/layout.js +9 -0
  145. package/lib/periodic/core/layout.js.map +1 -1
  146. package/lib/periodic/ui/ElementCell.d.ts +0 -6
  147. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  148. package/lib/periodic/ui/ElementCell.js +35 -19
  149. package/lib/periodic/ui/ElementCell.js.map +1 -1
  150. package/lib/periodic/ui/PeriodicTable.d.ts +12 -1
  151. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  152. package/lib/periodic/ui/PeriodicTable.js +34 -15
  153. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  154. package/lib/ui.d.ts +1 -0
  155. package/lib/ui.d.ts.map +1 -1
  156. package/lib/ui.js +1 -0
  157. package/lib/ui.js.map +1 -1
  158. package/package.json +1 -1
  159. package/src/about/core/about.ts +1 -51
  160. package/src/about/core/index.ts +0 -1
  161. package/src/about/ui/AboutBuild.tsx +8 -37
  162. package/src/about/ui/AboutPage.tsx +8 -64
  163. package/src/about/ui/AboutVersion.tsx +5 -45
  164. package/src/axis/core/axisMarks.ts +114 -0
  165. package/src/axis/core/axisScale.ts +41 -0
  166. package/src/axis/core/index.ts +20 -0
  167. package/src/axis/core/logAxisBounds.ts +198 -0
  168. package/src/axis/ui/AxisScaleControl.tsx +103 -0
  169. package/src/axis/ui/index.ts +9 -0
  170. package/src/core.ts +1 -0
  171. package/src/delimited/core/index.ts +2 -0
  172. package/src/delimited/core/tableRows.ts +93 -0
  173. package/src/delimited/ui/DelimitedTextPanel.tsx +25 -9
  174. package/src/delimited/ui/TableDataButton.tsx +162 -0
  175. package/src/delimited/ui/index.ts +2 -0
  176. package/src/download/core/index.ts +0 -25
  177. package/src/download/ui/FigureDownload.tsx +11 -148
  178. package/src/download/ui/FigureDownloadPanel.tsx +46 -45
  179. package/src/download/ui/figureDownloadHint.ts +52 -0
  180. package/src/download/ui/index.ts +6 -10
  181. package/src/download/ui/useFigureActions.ts +113 -0
  182. package/src/download/ui/useFigureDownload.tsx +191 -0
  183. package/src/ecosystem/core/sites.ts +0 -40
  184. package/src/ecosystem/core/types.ts +0 -10
  185. package/src/ir/core/irCommands.ts +2 -2
  186. package/src/ir/ui/IrAboutDialog.tsx +0 -13
  187. package/src/ir/ui/IrCanvas.tsx +8 -2
  188. package/src/ir/ui/IrShell.tsx +23 -25
  189. package/src/ir/ui/IrToolbar.tsx +36 -6
  190. package/src/ir/ui/index.ts +1 -0
  191. package/src/locales/de.json +8 -4
  192. package/src/locales/en.json +8 -4
  193. package/src/locales/es.json +8 -4
  194. package/src/locales/fr.json +8 -4
  195. package/src/locales/it.json +8 -4
  196. package/src/overlay/ui/OverlayPanel.tsx +8 -2
  197. package/src/overlay/ui/overlayPanelStyles.ts +3 -0
  198. package/src/periodic/core/index.ts +1 -0
  199. package/src/periodic/core/layout.ts +19 -0
  200. package/src/periodic/ui/ElementCell.tsx +35 -26
  201. package/src/periodic/ui/PeriodicTable.tsx +55 -16
  202. package/src/ui.ts +1 -0
  203. package/styles/chrome.css +71 -0
  204. package/lib/about/core/repository.d.ts +0 -14
  205. package/lib/about/core/repository.d.ts.map +0 -1
  206. package/lib/about/core/repository.js +0 -21
  207. package/lib/about/core/repository.js.map +0 -1
  208. package/lib/download/core/exportStyles.d.ts +0 -22
  209. package/lib/download/core/exportStyles.d.ts.map +0 -1
  210. package/lib/download/core/exportStyles.js +0 -19
  211. package/lib/download/core/exportStyles.js.map +0 -1
  212. package/lib/download/core/imageResolution.d.ts +0 -88
  213. package/lib/download/core/imageResolution.d.ts.map +0 -1
  214. package/lib/download/core/imageResolution.js +0 -77
  215. package/lib/download/core/imageResolution.js.map +0 -1
  216. package/lib/download/core/pictureExport.d.ts +0 -93
  217. package/lib/download/core/pictureExport.d.ts.map +0 -1
  218. package/lib/download/core/pictureExport.js +0 -116
  219. package/lib/download/core/pictureExport.js.map +0 -1
  220. package/lib/download/ui/ExportDialogFrame.d.ts +0 -46
  221. package/lib/download/ui/ExportDialogFrame.d.ts.map +0 -1
  222. package/lib/download/ui/ExportDialogFrame.js +0 -24
  223. package/lib/download/ui/ExportDialogFrame.js.map +0 -1
  224. package/lib/download/ui/ExportImageDialog.d.ts +0 -47
  225. package/lib/download/ui/ExportImageDialog.d.ts.map +0 -1
  226. package/lib/download/ui/ExportImageDialog.js +0 -25
  227. package/lib/download/ui/ExportImageDialog.js.map +0 -1
  228. package/lib/download/ui/ExportNameField.d.ts +0 -29
  229. package/lib/download/ui/ExportNameField.d.ts.map +0 -1
  230. package/lib/download/ui/ExportNameField.js +0 -99
  231. package/lib/download/ui/ExportNameField.js.map +0 -1
  232. package/lib/download/ui/ExportPictureSection.d.ts +0 -38
  233. package/lib/download/ui/ExportPictureSection.d.ts.map +0 -1
  234. package/lib/download/ui/ExportPictureSection.js +0 -48
  235. package/lib/download/ui/ExportPictureSection.js.map +0 -1
  236. package/lib/download/ui/useDrawingBox.d.ts +0 -23
  237. package/lib/download/ui/useDrawingBox.d.ts.map +0 -1
  238. package/lib/download/ui/useDrawingBox.js +0 -21
  239. package/lib/download/ui/useDrawingBox.js.map +0 -1
  240. package/src/about/core/repository.ts +0 -22
  241. package/src/download/core/exportStyles.ts +0 -20
  242. package/src/download/core/imageResolution.ts +0 -118
  243. package/src/download/core/pictureExport.ts +0 -207
  244. package/src/download/ui/ExportDialogFrame.tsx +0 -86
  245. package/src/download/ui/ExportImageDialog.tsx +0 -91
  246. package/src/download/ui/ExportNameField.tsx +0 -148
  247. package/src/download/ui/ExportPictureSection.tsx +0 -142
  248. package/src/download/ui/useDrawingBox.ts +0 -37
@@ -54,6 +54,15 @@ export interface DelimitedTextPanelProps {
54
54
  * @default 320
55
55
  */
56
56
  height?: number;
57
+ /**
58
+ * What is shown in place of the text area. A caller with more rows than a
59
+ * text area can hold — a spectrum of a hundred thousand points — passes its
60
+ * own virtualized table here; what is copied and what is saved stay the
61
+ * serialized text either way, so the preview can shorten a number the file
62
+ * keeps in full.
63
+ * @default undefined — the text itself, in a read-only text area
64
+ */
65
+ preview?: ReactNode;
57
66
  /**
58
67
  * Class names added to the root element, after the component's own.
59
68
  * @default undefined
@@ -84,6 +93,7 @@ export function DelimitedTextPanel(
84
93
  downloadable = true,
85
94
  label,
86
95
  height = 320,
96
+ preview,
87
97
  } = props;
88
98
  const t = useChromeT();
89
99
  const [delimiterId, setDelimiterId] = useState<string>(defaultDelimiter);
@@ -138,15 +148,19 @@ export function DelimitedTextPanel(
138
148
  small
139
149
  />
140
150
  </div>
141
- <textarea
142
- readOnly
143
- value={text}
144
- spellCheck={false}
145
- aria-label={label ?? t('delimited.tableAsText')}
146
- className={Classes.INPUT}
147
- style={{ ...TEXT_STYLE, height }}
148
- onFocus={(event) => event.currentTarget.select()}
149
- />
151
+ {preview === undefined ? (
152
+ <textarea
153
+ readOnly
154
+ value={text}
155
+ spellCheck={false}
156
+ aria-label={label ?? t('delimited.tableAsText')}
157
+ className={Classes.INPUT}
158
+ style={{ ...TEXT_STYLE, height }}
159
+ onFocus={(event) => event.currentTarget.select()}
160
+ />
161
+ ) : (
162
+ <div style={PREVIEW_STYLE}>{preview}</div>
163
+ )}
150
164
  </div>
151
165
  );
152
166
  }
@@ -166,6 +180,8 @@ const LABEL_STYLE = {
166
180
 
167
181
  const SPACER_STYLE = { flex: 1 } as const satisfies CSSProperties;
168
182
 
183
+ const PREVIEW_STYLE = { marginTop: 12 } as const satisfies CSSProperties;
184
+
169
185
  const TEXT_STYLE = {
170
186
  width: '100%',
171
187
  marginTop: 12,
@@ -0,0 +1,162 @@
1
+ import type { IconName } from '@blueprintjs/core';
2
+ import { Button } from '@blueprintjs/core';
3
+ import type { ReactElement, ReactNode } from 'react';
4
+ import { useState } from 'react';
5
+
6
+ import { useChromeT } from '../../i18n/ui/useT.ts';
7
+
8
+ import type { DelimitedTextDialogProps } from './DelimitedTextDialog.tsx';
9
+ import { DelimitedTextDialog } from './DelimitedTextDialog.tsx';
10
+
11
+ /** The cells of a table, one array per line. */
12
+ type TableRows = ReadonlyArray<readonly string[]>;
13
+
14
+ /** What the shut dialog is handed, so it holds no copy of the last table. */
15
+ const EMPTY: TableRows = [];
16
+
17
+ /** What {@link TableDataButton} hands over, and how the button reads. */
18
+ export interface TableDataButtonProps extends Omit<
19
+ DelimitedTextDialogProps,
20
+ 'isOpen' | 'onClose' | 'rows'
21
+ > {
22
+ /**
23
+ * The cells, one array per line. A function is called when the dialog opens
24
+ * and not before, which is what a table built out of a computation has to
25
+ * be: formatting a thousand points into strings on every render, for a
26
+ * button nobody may press, is a page that stutters while it is used.
27
+ */
28
+ rows: TableRows | (() => TableRows);
29
+ /**
30
+ * Text of the button. Left out for an icon-only button, which is what a
31
+ * dense toolbar over a table wants.
32
+ * @default undefined — the button is reduced to its icon
33
+ */
34
+ text?: string;
35
+ /**
36
+ * Glyph on the button.
37
+ * @default 'th'
38
+ */
39
+ icon?: IconName;
40
+ /**
41
+ * Whether the button drops its background, for a toolbar or a card header.
42
+ * @default true
43
+ */
44
+ minimal?: boolean;
45
+ /**
46
+ * Whether the button is the small size.
47
+ * @default false
48
+ */
49
+ small?: boolean;
50
+ /**
51
+ * What the pointer and a screen reader are told. The dialog's own `title`
52
+ * names the table — `Titration curve as a table` — while this names the
53
+ * affordance, and the two are deliberately not the same string: the heading
54
+ * is the site's to write, the button's name is the family's, so the control
55
+ * reads the same on every tool a visitor opens that week.
56
+ * @default the chrome's own line, in the language of the page
57
+ */
58
+ tooltip?: string;
59
+ /**
60
+ * Whether there is nothing to hand over. A table that has not been computed
61
+ * yet says so by being greyed rather than by opening an empty dialog.
62
+ * @default whether there are no rows — and `false` where `rows` is a
63
+ * function, which is not called to find out
64
+ */
65
+ disabled?: boolean;
66
+ /**
67
+ * Class the button carries, so a site can reach it from its stylesheet — a
68
+ * terminal whose green the chrome's muted grey is unreadable against, a
69
+ * toolbar with its own metrics.
70
+ * @default undefined
71
+ */
72
+ buttonClassName?: string;
73
+ /**
74
+ * Value of the `data-testid` attribute of the button.
75
+ * @default undefined
76
+ */
77
+ testId?: string;
78
+ /**
79
+ * The control in place of the Blueprint button, for a page whose chrome is
80
+ * not Blueprint's — a `react-science` toolbar, a site's own toolbar button.
81
+ * It is handed the function that opens the dialog.
82
+ *
83
+ * It exists so the dialog, the separator, the copy, the save and the wording
84
+ * are shared even where the button cannot be: a site that keeps its own
85
+ * trigger should not also be keeping its own dialog.
86
+ * @default a Blueprint button carrying the glyph and the text
87
+ */
88
+ trigger?: (open: () => void) => ReactNode;
89
+ }
90
+
91
+ /**
92
+ * The one control that takes a table off the page: copy it, or save it.
93
+ *
94
+ * It is a button and a dialog together rather than a dialog a site has to hold
95
+ * open itself, because the state behind it is the same three lines in every
96
+ * site and because the wording is not a site's to choose. An audit in
97
+ * September 2026 found the same affordance spelled `Export as TSV` on one
98
+ * site, `Copy as TSV` on another and `Copy or download data` on a third, which
99
+ * reads as three different features to anybody who uses two of our tools in a
100
+ * week.
101
+ *
102
+ * It is also deliberately not named after a separator. The dialog behind it
103
+ * writes tab-, comma- or semicolon-separated text, so a button that says TSV
104
+ * is wrong two times out of three — and a reader looking for CSV concludes the
105
+ * tool cannot give it to them.
106
+ * @param props - See {@link TableDataButtonProps}.
107
+ * @returns The button and its dialog.
108
+ */
109
+ export function TableDataButton(props: TableDataButtonProps): ReactElement {
110
+ const {
111
+ rows,
112
+ text,
113
+ icon = 'th',
114
+ minimal = true,
115
+ small = false,
116
+ tooltip,
117
+ disabled,
118
+ testId,
119
+ trigger,
120
+ buttonClassName,
121
+ ...dialog
122
+ } = props;
123
+ const t = useChromeT();
124
+ // The cells are held rather than read on every render, so a `rows` function
125
+ // is called once per opening and the closed button costs nothing at all.
126
+ const [shown, setShown] = useState<TableRows | null>(null);
127
+
128
+ const told = tooltip ?? t('delimited.copyOrDownload');
129
+ const nothing =
130
+ disabled ?? (typeof rows === 'function' ? false : rows.length === 0);
131
+
132
+ function show(): void {
133
+ setShown(typeof rows === 'function' ? rows() : rows);
134
+ }
135
+
136
+ return (
137
+ <>
138
+ {trigger ? (
139
+ trigger(show)
140
+ ) : (
141
+ <Button
142
+ className={buttonClassName}
143
+ icon={icon}
144
+ text={text}
145
+ variant={minimal ? 'minimal' : 'solid'}
146
+ size={small ? 'small' : 'medium'}
147
+ disabled={nothing}
148
+ title={told}
149
+ aria-label={text ?? told}
150
+ data-testid={testId}
151
+ onClick={show}
152
+ />
153
+ )}
154
+ <DelimitedTextDialog
155
+ {...dialog}
156
+ rows={shown ?? EMPTY}
157
+ isOpen={shown !== null}
158
+ onClose={() => setShown(null)}
159
+ />
160
+ </>
161
+ );
162
+ }
@@ -2,3 +2,5 @@ export type { DelimitedTextDialogProps } from './DelimitedTextDialog.tsx';
2
2
  export { DelimitedTextDialog } from './DelimitedTextDialog.tsx';
3
3
  export type { DelimitedTextPanelProps } from './DelimitedTextPanel.tsx';
4
4
  export { DelimitedTextPanel } from './DelimitedTextPanel.tsx';
5
+ export type { TableDataButtonProps } from './TableDataButton.tsx';
6
+ export { TableDataButton } from './TableDataButton.tsx';
@@ -51,31 +51,6 @@ export {
51
51
  } from './figureTarget.ts';
52
52
  export { sanitizeFileName } from './sanitizeFileName.ts';
53
53
 
54
- export {
55
- exportButtonsStyle,
56
- exportNoteStyle,
57
- exportSectionStyle,
58
- } from './exportStyles.ts';
59
- export type {
60
- ImageResolution,
61
- ImageResolutionId,
62
- ImageSize,
63
- } from './imageResolution.ts';
64
- export {
65
- DEFAULT_IMAGE_RESOLUTION,
66
- IMAGE_RESOLUTIONS,
67
- formatImageSize,
68
- imageResolution,
69
- imageSize,
70
- } from './imageResolution.ts';
71
- export type { PictureFrame, PictureOptions } from './pictureExport.ts';
72
- export {
73
- PICTURE_PADDING,
74
- copyPicture,
75
- pictureSize,
76
- savePngPicture,
77
- saveSvgPicture,
78
- } from './pictureExport.ts';
79
54
  export { drawingIn, stackDrawings } from './stackedDrawing.ts';
80
55
  export type { SvgBox } from './svgBounds.ts';
81
56
  export { CONTENT_ATTRIBUTE, contentBox, elementBox } from './svgBounds.ts';
@@ -1,74 +1,20 @@
1
1
  import type { IconName } from '@blueprintjs/core';
2
2
  import { PopoverNext } from '@blueprintjs/core';
3
3
  import type { ReactElement } from 'react';
4
- import { useState } from 'react';
5
4
 
6
5
  import { useChromeT } from '../../i18n/ui/useT.ts';
7
6
  import { OverlayIconButton } from '../../overlay/ui/OverlayIconButton.tsx';
8
- import type { FigureFormat } from '../core/downloadFigure.ts';
9
- import { downloadFigure } from '../core/downloadFigure.ts';
10
- import type { FigureLayout } from '../core/figureLayout.ts';
11
- import { figureLayoutRedraws, figureLayoutSize } from '../core/figureLayout.ts';
12
- import type { FigurePixels } from '../core/figureScale.ts';
13
- import {
14
- DEFAULT_FIGURE_SCALE,
15
- FIGURE_SCALES,
16
- defaultFigureScale,
17
- } from '../core/figureScale.ts';
18
- import { figureSize } from '../core/figureTarget.ts';
19
7
 
20
- import { FigureDownloadPanel } from './FigureDownloadPanel.tsx';
21
- import type { FigureRenderer } from './useFigureRedraw.tsx';
22
- import { useFigureRedraw } from './useFigureRedraw.tsx';
8
+ import type { FigureDownloadOptions } from './useFigureDownload.tsx';
9
+ import { useFigureDownload } from './useFigureDownload.tsx';
23
10
 
24
11
  /** What {@link FigureDownload} needs. */
25
- export interface FigureDownloadProps {
26
- /**
27
- * The `id` of the box the figure is mounted in. Everything drawn inside it
28
- * is saved — sixteen charts of a pair grid as readily as one scatter plot —
29
- * and the controls floating over it are left behind.
30
- */
31
- targetId: string;
32
- /**
33
- * What the saved file is called, without its extension. Name it after the
34
- * data rather than after the tool: a reader with four of these in a
35
- * downloads folder cannot tell four `figure.png` apart.
36
- * @default 'figure'
37
- */
38
- fileName?: string;
39
- /**
40
- * The format it opens on.
41
- * @default 'png'
42
- */
43
- defaultFormat?: FigureFormat;
44
- /**
45
- * The resolution a PNG opens on, as a multiple of the figure on screen. An
46
- * SVG opens at 1×, the size it has on screen, and each format keeps the
47
- * multiple the reader last picked for it.
48
- * @default 2
49
- */
50
- defaultScale?: number;
51
- /**
52
- * The resolutions offered.
53
- * @default [1, 2, 3, 4]
54
- */
55
- scales?: readonly number[];
56
- /**
57
- * What is painted under the figure. `transparent` leaves it unpainted, for a
58
- * figure going onto a coloured slide.
59
- * @default the surface the figure is drawn on
60
- */
61
- background?: string;
12
+ export interface FigureDownloadProps extends FigureDownloadOptions {
62
13
  /**
63
14
  * What the glyph is called, for the pointer and for a screen reader.
64
15
  * @default the chrome's own line, in the language of the page
65
16
  */
66
17
  label?: string;
67
- /**
68
- * What the panel behind it is called.
69
- * @default the chrome's own line, in the language of the page
70
- */
71
- title?: string;
72
18
  /**
73
19
  * Its glyph.
74
20
  * @default 'download'
@@ -79,20 +25,11 @@ export interface FigureDownloadProps {
79
25
  * @default undefined
80
26
  */
81
27
  testId?: string;
82
- /**
83
- * Draws the figure at a size, for a file. When it is given the panel offers
84
- * a size — as shown, 4:3, 16:9, a journal column, or typed — and a figure
85
- * saved at a size other than its own is drawn again at that size, off the
86
- * page, so its axes and labels are laid out for the new shape rather than
87
- * stretched into it. Draw exactly what is on screen, with the same data,
88
- * zoom and colours; only the box changes.
89
- * @default undefined — the figure is saved at the size it has on screen
90
- */
91
- renderFigure?: FigureRenderer;
92
28
  }
93
29
 
94
30
  /**
95
- * The glyph that takes the figure off the page as a file.
31
+ * The glyph that takes the figure off the page, as a file or onto the
32
+ * clipboard.
96
33
  *
97
34
  * It works from the `id` of the box the figure is mounted in rather than from
98
35
  * the figure itself, so the control is free to sit in the bar above the
@@ -112,97 +49,23 @@ export interface FigureDownloadProps {
112
49
  * @returns The glyph and its panel.
113
50
  */
114
51
  export function FigureDownload(props: FigureDownloadProps): ReactElement {
115
- const { targetId, fileName, background, scales = FIGURE_SCALES } = props;
116
- const { defaultFormat = 'png', defaultScale = DEFAULT_FIGURE_SCALE } = props;
117
- const { label, title } = props;
118
- const { icon = 'download', testId, renderFigure } = props;
119
-
52
+ const { label, icon = 'download', testId, ...options } = props;
120
53
  const t = useChromeT();
121
- const [open, setOpen] = useState(false);
122
- const [format, setFormat] = useState<FigureFormat>(defaultFormat);
123
- const [picked, setPicked] = useState<Record<FigureFormat, number>>(() => ({
124
- png: defaultScale,
125
- svg: defaultFigureScale('svg'),
126
- }));
127
- const scale = picked[format];
128
- const [size, setSize] = useState<FigurePixels | null>(null);
129
- const [failure, setFailure] = useState<string | null>(null);
130
- const [saving, setSaving] = useState(false);
131
- const [layout, setLayout] = useState<FigureLayout>('screen');
132
- const [custom, setCustom] = useState<FigurePixels | null>(null);
133
- const { redraw, portal } = useFigureRedraw(renderFigure);
134
-
135
- // The panel is held open from here rather than left to itself, because the
136
- // figure has to be measured at the moment it opens: its size is what the
137
- // panel writes out, and reading a bounding box on every render would cost a
138
- // layout pass on a bar that is re-rendered on every pointer move.
139
- function interact(next: boolean): void {
140
- setOpen(next);
141
- if (!next) return;
142
- setSize(figureSize(targetId));
143
- setFailure(null);
144
- }
145
-
146
- async function save(): Promise<void> {
147
- setSaving(true);
148
- try {
149
- const options = { format, scale, fileName, background };
150
- if (
151
- renderFigure !== undefined &&
152
- size !== null &&
153
- figureLayoutRedraws(layout, size, custom ?? size)
154
- ) {
155
- const drawn = figureLayoutSize(layout, size, custom ?? size);
156
- await redraw(targetId, drawn, (box) => downloadFigure(box, options));
157
- } else {
158
- await downloadFigure(targetId, options);
159
- }
160
- setFailure(null);
161
- } catch (error) {
162
- setFailure(error instanceof Error ? error.message : String(error));
163
- } finally {
164
- setSaving(false);
165
- }
166
- }
54
+ const { isOpen, setOpen, panel, format, portal } = useFigureDownload(options);
167
55
 
168
56
  return (
169
57
  <>
170
58
  <PopoverNext
171
- isOpen={open}
59
+ isOpen={isOpen}
172
60
  placement="bottom-end"
173
- onInteraction={interact}
174
- content={
175
- <FigureDownloadPanel
176
- title={title ?? t('download.saveFigure')}
177
- format={format}
178
- scale={scale}
179
- scales={scales}
180
- size={size}
181
- failure={failure}
182
- saving={saving}
183
- onFormatChange={setFormat}
184
- onScaleChange={(next) =>
185
- setPicked((current) => ({ ...current, [format]: next }))
186
- }
187
- onSave={() => void save()}
188
- sizing={
189
- renderFigure === undefined
190
- ? undefined
191
- : {
192
- layout,
193
- custom,
194
- onLayoutChange: setLayout,
195
- onCustomChange: setCustom,
196
- }
197
- }
198
- />
199
- }
61
+ onInteraction={setOpen}
62
+ content={panel}
200
63
  >
201
64
  <OverlayIconButton
202
65
  icon={icon}
203
66
  label={label ?? t('download.saveThisFigure')}
204
67
  value={format.toUpperCase()}
205
- active={open}
68
+ active={isOpen}
206
69
  testId={testId}
207
70
  opensMenu
208
71
  />
@@ -1,7 +1,5 @@
1
1
  import type { ReactElement } from 'react';
2
2
 
3
- import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
4
- import type { Translate } from '../../i18n/ui/useT.ts';
5
3
  import { useChromeT } from '../../i18n/ui/useT.ts';
6
4
  import { OverlayAction } from '../../overlay/ui/OverlayAction.tsx';
7
5
  import { OverlayPanel } from '../../overlay/ui/OverlayPanel.tsx';
@@ -19,6 +17,8 @@ import {
19
17
 
20
18
  import type { FigureSizing } from './FigureSizeRows.tsx';
21
19
  import { FigureSizeRows } from './FigureSizeRows.tsx';
20
+ import { hintOf } from './figureDownloadHint.ts';
21
+ import type { FigureNotice } from './useFigureActions.ts';
22
22
 
23
23
  /** What {@link FigureDownloadPanel} is drawn from. */
24
24
  export interface FigureDownloadPanelProps {
@@ -34,7 +34,13 @@ export interface FigureDownloadPanelProps {
34
34
  size: FigurePixels | null;
35
35
  /** What went wrong the last time save was pressed, if anything. */
36
36
  failure: string | null;
37
- /** Whether a file is being written right now. */
37
+ /**
38
+ * What the last copy came to, while the choices it was made with still
39
+ * stand.
40
+ * @default null
41
+ */
42
+ notice?: FigureNotice | null;
43
+ /** Whether a file or a copy is being written right now. */
38
44
  saving: boolean;
39
45
  /** Called with the format the reader picked. */
40
46
  onFormatChange: (format: FigureFormat) => void;
@@ -42,6 +48,12 @@ export interface FigureDownloadPanelProps {
42
48
  onScaleChange: (scale: number) => void;
43
49
  /** Called when they press save. */
44
50
  onSave: () => void;
51
+ /**
52
+ * Called when they press copy, which puts a PNG at the resolution picked on
53
+ * the clipboard.
54
+ * @default undefined — no copy is offered
55
+ */
56
+ onCopy?: () => void;
45
57
  /**
46
58
  * Whether the SVG holds a rendered picture rather than vector drawings, as
47
59
  * for a WebGL scene: its resolution is then the pixels of that picture, it
@@ -59,7 +71,8 @@ export interface FigureDownloadPanelProps {
59
71
  }
60
72
 
61
73
  /**
62
- * The choices behind the save glyph, and the button that writes the file.
74
+ * The choices behind the save glyph, and the buttons that write the file and
75
+ * copy the picture.
63
76
  *
64
77
  * The resolution applies to both formats, and means what the reader needs it
65
78
  * to: a PNG is painted with that many more pixels, and an SVG, drawn the same,
@@ -81,7 +94,7 @@ export function FigureDownloadPanel(
81
94
  ): ReactElement {
82
95
  const { title, format, scale, scales, size, failure, saving } = props;
83
96
  const { onFormatChange, onScaleChange, onSave, rasterSvg = false } = props;
84
- const { sizing } = props;
97
+ const { sizing, onCopy, notice = null } = props;
85
98
  const t = useChromeT();
86
99
 
87
100
  // What the file is drawn at: the figure on screen, or the shape picked.
@@ -95,15 +108,29 @@ export function FigureDownloadPanel(
95
108
  return (
96
109
  <OverlayPanel
97
110
  title={title}
98
- hint={hintOf(format, drawn, scale, failure, rasterSvg, t)}
111
+ width={PANEL_WIDTH}
112
+ hint={hintOf(
113
+ { format, size: drawn, scale, failure, notice, rasterSvg },
114
+ t,
115
+ )}
99
116
  actions={
100
- <OverlayAction
101
- text={t('download.save')}
102
- icon="download"
103
- intent="primary"
104
- disabled={saving || size === null}
105
- onClick={onSave}
106
- />
117
+ <>
118
+ <OverlayAction
119
+ text={t('download.save')}
120
+ icon="download"
121
+ intent="primary"
122
+ disabled={saving || size === null}
123
+ onClick={onSave}
124
+ />
125
+ {onCopy === undefined ? null : (
126
+ <OverlayAction
127
+ text={t('clipboard.copy')}
128
+ icon="clipboard"
129
+ disabled={saving || size === null}
130
+ onClick={onCopy}
131
+ />
132
+ )}
133
+ </>
107
134
  }
108
135
  >
109
136
  <OverlaySegmented<FigureFormat>
@@ -137,6 +164,12 @@ export function FigureDownloadPanel(
137
164
  );
138
165
  }
139
166
 
167
+ /**
168
+ * Wide enough for the longest hint on one line in English, so switching
169
+ * between PNG and SVG — whose hints differ in length — never resizes the panel.
170
+ */
171
+ const PANEL_WIDTH = '27em';
172
+
140
173
  /** The two files a figure can leave the page as. */
141
174
  const FORMAT_CHOICES: ReadonlyArray<OverlayOption<FigureFormat>> = [
142
175
  { value: 'png', label: 'PNG' },
@@ -171,35 +204,3 @@ function scaleChoices(
171
204
  }
172
205
  return choices;
173
206
  }
174
-
175
- /**
176
- * The line at the foot of the panel: what pressing save is about to do.
177
- * @param format - Which file the reader is about to write.
178
- * @param size - How big the figure is drawn for the file.
179
- * @param scale - The multiple it is saved at.
180
- * @param failure - What went wrong last time, if anything.
181
- * @param rasterSvg - Whether the SVG embeds a rendered picture.
182
- * @param t - The chrome's formatter, so the sentence is in the language of
183
- * the page.
184
- * @returns The sentence.
185
- */
186
- function hintOf(
187
- format: FigureFormat,
188
- size: FigurePixels | null,
189
- scale: number,
190
- failure: string | null,
191
- rasterSvg: boolean,
192
- t: Translate<ChromeKey>,
193
- ): string {
194
- if (failure !== null) return failure;
195
- if (size === null) return t('download.noFigure');
196
- if (format === 'svg' && rasterSvg) {
197
- return t('download.hintRasterSvg', {
198
- pixels: formatFigurePixels(figurePixels(size, scale)),
199
- });
200
- }
201
- const pixels = formatFigurePixels(figurePixels(size, scale));
202
- return format === 'svg'
203
- ? t('download.hintSvg', { pixels })
204
- : t('download.hintPng', { pixels });
205
- }
@@ -0,0 +1,52 @@
1
+ import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
2
+ import type { Translate } from '../../i18n/ui/useT.ts';
3
+ import type { FigureFormat } from '../core/downloadFigure.ts';
4
+ import type { FigurePixels } from '../core/figureScale.ts';
5
+ import { figurePixels, formatFigurePixels } from '../core/figureScale.ts';
6
+
7
+ import type { FigureNotice } from './useFigureActions.ts';
8
+
9
+ /** What the line at the foot of the panel is written from. */
10
+ export interface HintState {
11
+ /** Which file the reader is about to write. */
12
+ format: FigureFormat;
13
+ /** How big the figure is drawn for the file. */
14
+ size: FigurePixels | null;
15
+ /** The multiple it is saved at. */
16
+ scale: number;
17
+ /** What went wrong last time, if anything. */
18
+ failure: string | null;
19
+ /** What the last copy came to, if anything. */
20
+ notice: FigureNotice | null;
21
+ /** Whether the SVG embeds a rendered picture. */
22
+ rasterSvg: boolean;
23
+ }
24
+
25
+ /**
26
+ * The line at the foot of the panel: what pressing save is about to do, or
27
+ * what the last copy did.
28
+ * @param state - See {@link HintState}.
29
+ * @param t - The chrome's formatter, so the sentence is in the language of
30
+ * the page.
31
+ * @returns The sentence.
32
+ */
33
+ export function hintOf(state: HintState, t: Translate<ChromeKey>): string {
34
+ const { format, size, scale, failure, notice, rasterSvg } = state;
35
+ if (failure !== null) return failure;
36
+ if (size === null) return t('download.noFigure');
37
+ if (notice === 'copyUnsupported') return t('download.copyUnsupported');
38
+ if (notice === 'copied') {
39
+ return t('download.hintCopied', {
40
+ pixels: formatFigurePixels(figurePixels(size, scale)),
41
+ });
42
+ }
43
+ if (format === 'svg' && rasterSvg) {
44
+ return t('download.hintRasterSvg', {
45
+ pixels: formatFigurePixels(figurePixels(size, scale)),
46
+ });
47
+ }
48
+ const pixels = formatFigurePixels(figurePixels(size, scale));
49
+ return format === 'svg'
50
+ ? t('download.hintSvg', { pixels })
51
+ : t('download.hintPng', { pixels });
52
+ }
@@ -14,13 +14,9 @@ export { FigureDownloadPanel } from './FigureDownloadPanel.tsx';
14
14
  export type { FigureSizing } from './FigureSizeRows.tsx';
15
15
  export type { FigureRenderer } from './useFigureRedraw.tsx';
16
16
 
17
- export type { ExportDialogFrameProps } from './ExportDialogFrame.tsx';
18
- export { ExportDialogFrame } from './ExportDialogFrame.tsx';
19
- export type { ExportImageDialogProps } from './ExportImageDialog.tsx';
20
- export { ExportImageDialog } from './ExportImageDialog.tsx';
21
- export type { ExportNameFieldProps } from './ExportNameField.tsx';
22
- export { ExportNameField } from './ExportNameField.tsx';
23
- export type { ExportPictureSectionProps } from './ExportPictureSection.tsx';
24
- export { ExportPictureSection } from './ExportPictureSection.tsx';
25
- export type { DrawingBox } from './useDrawingBox.ts';
26
- export { useDrawingBox } from './useDrawingBox.ts';
17
+ export type {
18
+ FigureDownloadOptions,
19
+ FigureDownloadState,
20
+ } from './useFigureDownload.tsx';
21
+ export { useFigureDownload } from './useFigureDownload.tsx';
22
+ export type { FigureNotice } from './useFigureActions.ts';