react-cheminfo 0.49.0 → 0.51.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 (269) hide show
  1. package/README.md +23 -0
  2. package/bin/check-seo.mjs +27 -2
  3. package/lib/clipboard/core/index.d.ts +0 -1
  4. package/lib/clipboard/core/index.d.ts.map +1 -1
  5. package/lib/clipboard/core/index.js +0 -1
  6. package/lib/clipboard/core/index.js.map +1 -1
  7. package/lib/clipboard/ui/CopyButton.d.ts +6 -0
  8. package/lib/clipboard/ui/CopyButton.d.ts.map +1 -1
  9. package/lib/clipboard/ui/CopyButton.js +5 -2
  10. package/lib/clipboard/ui/CopyButton.js.map +1 -1
  11. package/lib/color/core/index.d.ts +3 -0
  12. package/lib/color/core/index.d.ts.map +1 -1
  13. package/lib/color/core/index.js +2 -0
  14. package/lib/color/core/index.js.map +1 -1
  15. package/lib/color/core/interpolate.d.ts +4 -2
  16. package/lib/color/core/interpolate.d.ts.map +1 -1
  17. package/lib/color/core/interpolate.js +34 -12
  18. package/lib/color/core/interpolate.js.map +1 -1
  19. package/lib/color/core/stops.d.ts +43 -0
  20. package/lib/color/core/stops.d.ts.map +1 -0
  21. package/lib/color/core/stops.js +66 -0
  22. package/lib/color/core/stops.js.map +1 -0
  23. package/lib/color/core/tone.d.ts +25 -0
  24. package/lib/color/core/tone.d.ts.map +1 -0
  25. package/lib/color/core/tone.js +67 -0
  26. package/lib/color/core/tone.js.map +1 -0
  27. package/lib/color/core/uniform.d.ts +36 -0
  28. package/lib/color/core/uniform.d.ts.map +1 -0
  29. package/lib/color/core/uniform.js +61 -0
  30. package/lib/color/core/uniform.js.map +1 -0
  31. package/lib/color/ui/ColorScaleEditor.d.ts +5 -6
  32. package/lib/color/ui/ColorScaleEditor.d.ts.map +1 -1
  33. package/lib/color/ui/ColorScaleEditor.js +74 -84
  34. package/lib/color/ui/ColorScaleEditor.js.map +1 -1
  35. package/lib/color/ui/ColorScaleSelect.d.ts +16 -0
  36. package/lib/color/ui/ColorScaleSelect.d.ts.map +1 -1
  37. package/lib/color/ui/ColorScaleSelect.js +69 -17
  38. package/lib/color/ui/ColorScaleSelect.js.map +1 -1
  39. package/lib/color/ui/ColorScaleTone.d.ts +17 -0
  40. package/lib/color/ui/ColorScaleTone.d.ts.map +1 -0
  41. package/lib/color/ui/ColorScaleTone.js +55 -0
  42. package/lib/color/ui/ColorScaleTone.js.map +1 -0
  43. package/lib/color/ui/ColorScaleTrack.d.ts +29 -0
  44. package/lib/color/ui/ColorScaleTrack.d.ts.map +1 -0
  45. package/lib/color/ui/ColorScaleTrack.js +143 -0
  46. package/lib/color/ui/ColorScaleTrack.js.map +1 -0
  47. package/lib/download/core/copyFigure.d.ts +14 -0
  48. package/lib/download/core/copyFigure.d.ts.map +1 -0
  49. package/lib/download/core/copyFigure.js +18 -0
  50. package/lib/download/core/copyFigure.js.map +1 -0
  51. package/lib/download/core/figureHtml.js +1 -0
  52. package/lib/download/core/figureHtml.js.map +1 -1
  53. package/lib/download/core/figureHtmlMarkup.d.ts +10 -0
  54. package/lib/download/core/figureHtmlMarkup.d.ts.map +1 -1
  55. package/lib/download/core/figureHtmlMarkup.js +7 -1
  56. package/lib/download/core/figureHtmlMarkup.js.map +1 -1
  57. package/lib/download/core/index.d.ts +2 -0
  58. package/lib/download/core/index.d.ts.map +1 -1
  59. package/lib/download/core/index.js +1 -0
  60. package/lib/download/core/index.js.map +1 -1
  61. package/lib/download/ui/FigureDownloadPanel.d.ts +2 -3
  62. package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
  63. package/lib/download/ui/FigureDownloadPanel.js +1 -1
  64. package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
  65. package/lib/download/ui/figureDownloadHint.d.ts +1 -1
  66. package/lib/download/ui/figureDownloadHint.d.ts.map +1 -1
  67. package/lib/download/ui/figureDownloadHint.js.map +1 -1
  68. package/lib/download/ui/index.d.ts +0 -1
  69. package/lib/download/ui/index.d.ts.map +1 -1
  70. package/lib/download/ui/useFigureActions.d.ts +2 -11
  71. package/lib/download/ui/useFigureActions.d.ts.map +1 -1
  72. package/lib/download/ui/useFigureActions.js +4 -23
  73. package/lib/download/ui/useFigureActions.js.map +1 -1
  74. package/lib/download/ui/useFigureTask.d.ts +21 -0
  75. package/lib/download/ui/useFigureTask.d.ts.map +1 -0
  76. package/lib/download/ui/useFigureTask.js +27 -0
  77. package/lib/download/ui/useFigureTask.js.map +1 -0
  78. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  79. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  80. package/lib/locales/de.json +28 -2
  81. package/lib/locales/en.json +28 -2
  82. package/lib/locales/es.json +28 -2
  83. package/lib/locales/fr.json +28 -2
  84. package/lib/locales/it.json +28 -2
  85. package/lib/molecule3d/core/index.d.ts +1 -1
  86. package/lib/molecule3d/core/index.d.ts.map +1 -1
  87. package/lib/molecule3d/core/index.js +1 -1
  88. package/lib/molecule3d/core/index.js.map +1 -1
  89. package/lib/molecule3d/core/settings.d.ts +15 -0
  90. package/lib/molecule3d/core/settings.d.ts.map +1 -1
  91. package/lib/molecule3d/core/settings.js +25 -0
  92. package/lib/molecule3d/core/settings.js.map +1 -1
  93. package/lib/molecule3d/ui/Molecule3DExport.d.ts +3 -0
  94. package/lib/molecule3d/ui/Molecule3DExport.d.ts.map +1 -1
  95. package/lib/molecule3d/ui/Molecule3DExport.js +10 -16
  96. package/lib/molecule3d/ui/Molecule3DExport.js.map +1 -1
  97. package/lib/molecule3d/ui/Molecule3DOptions.d.ts +3 -1
  98. package/lib/molecule3d/ui/Molecule3DOptions.d.ts.map +1 -1
  99. package/lib/molecule3d/ui/Molecule3DOptions.js +22 -6
  100. package/lib/molecule3d/ui/Molecule3DOptions.js.map +1 -1
  101. package/lib/molecule3d/ui/MoleculeCanvas3D.d.ts.map +1 -1
  102. package/lib/molecule3d/ui/MoleculeCanvas3D.js +4 -3
  103. package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
  104. package/lib/molecule3d/ui/exportMoleculeImage.d.ts +9 -0
  105. package/lib/molecule3d/ui/exportMoleculeImage.d.ts.map +1 -1
  106. package/lib/molecule3d/ui/exportMoleculeImage.js +15 -0
  107. package/lib/molecule3d/ui/exportMoleculeImage.js.map +1 -1
  108. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts +2 -1
  109. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts.map +1 -1
  110. package/lib/molecule3d/ui/useImageExport.d.ts +7 -1
  111. package/lib/molecule3d/ui/useImageExport.d.ts.map +1 -1
  112. package/lib/molecule3d/ui/useImageExport.js +12 -3
  113. package/lib/molecule3d/ui/useImageExport.js.map +1 -1
  114. package/lib/overlay/ui/FigureBar.d.ts +41 -0
  115. package/lib/overlay/ui/FigureBar.d.ts.map +1 -0
  116. package/lib/overlay/ui/FigureBar.js +43 -0
  117. package/lib/overlay/ui/FigureBar.js.map +1 -0
  118. package/lib/overlay/ui/OverlayBar.d.ts +13 -4
  119. package/lib/overlay/ui/OverlayBar.d.ts.map +1 -1
  120. package/lib/overlay/ui/OverlayBar.js +2 -2
  121. package/lib/overlay/ui/OverlayBar.js.map +1 -1
  122. package/lib/overlay/ui/OverlayBarStrip.d.ts +2 -1
  123. package/lib/overlay/ui/OverlayBarStrip.d.ts.map +1 -1
  124. package/lib/overlay/ui/OverlayBarStrip.js +9 -5
  125. package/lib/overlay/ui/OverlayBarStrip.js.map +1 -1
  126. package/lib/overlay/ui/OverlayIconButton.d.ts +8 -2
  127. package/lib/overlay/ui/OverlayIconButton.d.ts.map +1 -1
  128. package/lib/overlay/ui/OverlayIconButton.js +2 -1
  129. package/lib/overlay/ui/OverlayIconButton.js.map +1 -1
  130. package/lib/overlay/ui/index.d.ts +4 -0
  131. package/lib/overlay/ui/index.d.ts.map +1 -1
  132. package/lib/overlay/ui/index.js +2 -0
  133. package/lib/overlay/ui/index.js.map +1 -1
  134. package/lib/overlay/ui/overlayBarShape.d.ts +18 -0
  135. package/lib/overlay/ui/overlayBarShape.d.ts.map +1 -1
  136. package/lib/overlay/ui/overlayStyles.d.ts +30 -0
  137. package/lib/overlay/ui/overlayStyles.d.ts.map +1 -1
  138. package/lib/overlay/ui/overlayStyles.js +52 -0
  139. package/lib/overlay/ui/overlayStyles.js.map +1 -1
  140. package/lib/overlay/ui/useFigureBarAwake.d.ts +12 -0
  141. package/lib/overlay/ui/useFigureBarAwake.d.ts.map +1 -0
  142. package/lib/overlay/ui/useFigureBarAwake.js +51 -0
  143. package/lib/overlay/ui/useFigureBarAwake.js.map +1 -0
  144. package/lib/pages.d.ts +3 -0
  145. package/lib/pages.d.ts.map +1 -0
  146. package/lib/pages.js +2 -0
  147. package/lib/pages.js.map +1 -0
  148. package/lib/periodic/core/elements.d.ts +6 -3
  149. package/lib/periodic/core/elements.d.ts.map +1 -1
  150. package/lib/periodic/core/elements.js +9 -3
  151. package/lib/periodic/core/elements.js.map +1 -1
  152. package/lib/periodic/core/index.d.ts +2 -2
  153. package/lib/periodic/core/index.d.ts.map +1 -1
  154. package/lib/periodic/core/index.js +1 -1
  155. package/lib/periodic/core/index.js.map +1 -1
  156. package/lib/periodic/core/layout.d.ts +33 -3
  157. package/lib/periodic/core/layout.d.ts.map +1 -1
  158. package/lib/periodic/core/layout.js +14 -0
  159. package/lib/periodic/core/layout.js.map +1 -1
  160. package/lib/periodic/ui/BlockStrip.d.ts +20 -0
  161. package/lib/periodic/ui/BlockStrip.d.ts.map +1 -0
  162. package/lib/periodic/ui/BlockStrip.js +48 -0
  163. package/lib/periodic/ui/BlockStrip.js.map +1 -0
  164. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  165. package/lib/periodic/ui/ElementCell.js +9 -8
  166. package/lib/periodic/ui/ElementCell.js.map +1 -1
  167. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  168. package/lib/periodic/ui/PeriodicTable.js +3 -1
  169. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  170. package/lib/periodic/ui/PeriodicTableHelp.d.ts +33 -0
  171. package/lib/periodic/ui/PeriodicTableHelp.d.ts.map +1 -0
  172. package/lib/periodic/ui/PeriodicTableHelp.js +107 -0
  173. package/lib/periodic/ui/PeriodicTableHelp.js.map +1 -0
  174. package/lib/periodic/ui/cellStyles.d.ts +3 -2
  175. package/lib/periodic/ui/cellStyles.d.ts.map +1 -1
  176. package/lib/periodic/ui/cellStyles.js +3 -2
  177. package/lib/periodic/ui/cellStyles.js.map +1 -1
  178. package/lib/periodic/ui/index.d.ts +2 -0
  179. package/lib/periodic/ui/index.d.ts.map +1 -1
  180. package/lib/periodic/ui/index.js +1 -0
  181. package/lib/periodic/ui/index.js.map +1 -1
  182. package/lib/periodic/ui/periodicTableProps.d.ts +5 -4
  183. package/lib/periodic/ui/periodicTableProps.d.ts.map +1 -1
  184. package/lib/periodic/ui/tableLayout.d.ts +3 -1
  185. package/lib/periodic/ui/tableLayout.d.ts.map +1 -1
  186. package/lib/periodic/ui/tableLayout.js +8 -3
  187. package/lib/periodic/ui/tableLayout.js.map +1 -1
  188. package/lib/seo/core/trackingMiddleware.d.ts +27 -0
  189. package/lib/seo/core/trackingMiddleware.d.ts.map +1 -0
  190. package/lib/seo/core/trackingMiddleware.js +33 -0
  191. package/lib/seo/core/trackingMiddleware.js.map +1 -0
  192. package/lib/share/ui/ShareDialogContent.d.ts +3 -2
  193. package/lib/share/ui/ShareDialogContent.d.ts.map +1 -1
  194. package/lib/share/ui/ShareDialogContent.js +5 -6
  195. package/lib/share/ui/ShareDialogContent.js.map +1 -1
  196. package/lib/share/ui/ShareLinkBar.d.ts +8 -2
  197. package/lib/share/ui/ShareLinkBar.d.ts.map +1 -1
  198. package/lib/share/ui/ShareLinkBar.js +6 -5
  199. package/lib/share/ui/ShareLinkBar.js.map +1 -1
  200. package/lib/share/ui/SharePreview.d.ts.map +1 -1
  201. package/lib/share/ui/SharePreview.js +2 -2
  202. package/lib/share/ui/SharePreview.js.map +1 -1
  203. package/lib/structure/ui/StructurePicturePane.js +3 -3
  204. package/lib/structure/ui/StructurePicturePane.js.map +1 -1
  205. package/package.json +2 -1
  206. package/src/clipboard/core/index.ts +0 -1
  207. package/src/clipboard/ui/CopyButton.tsx +12 -1
  208. package/src/color/core/index.ts +9 -0
  209. package/src/color/core/interpolate.ts +51 -22
  210. package/src/color/core/stops.ts +98 -0
  211. package/src/color/core/tone.ts +79 -0
  212. package/src/color/core/uniform.ts +67 -0
  213. package/src/color/ui/ColorScaleEditor.tsx +115 -141
  214. package/src/color/ui/ColorScaleSelect.tsx +160 -36
  215. package/src/color/ui/ColorScaleTone.tsx +103 -0
  216. package/src/color/ui/ColorScaleTrack.tsx +239 -0
  217. package/src/download/core/copyFigure.ts +20 -0
  218. package/src/download/core/figureHtml.ts +1 -0
  219. package/src/download/core/figureHtmlMarkup.ts +14 -1
  220. package/src/download/core/index.ts +2 -0
  221. package/src/download/ui/FigureDownloadPanel.tsx +8 -11
  222. package/src/download/ui/figureDownloadHint.ts +1 -2
  223. package/src/download/ui/index.ts +0 -1
  224. package/src/download/ui/useFigureActions.ts +6 -42
  225. package/src/download/ui/useFigureTask.ts +46 -0
  226. package/src/locales/de.json +28 -2
  227. package/src/locales/en.json +28 -2
  228. package/src/locales/es.json +28 -2
  229. package/src/locales/fr.json +28 -2
  230. package/src/locales/it.json +28 -2
  231. package/src/molecule3d/core/index.ts +2 -0
  232. package/src/molecule3d/core/settings.ts +35 -0
  233. package/src/molecule3d/ui/Molecule3DExport.tsx +18 -17
  234. package/src/molecule3d/ui/Molecule3DOptions.tsx +51 -3
  235. package/src/molecule3d/ui/MoleculeCanvas3D.tsx +11 -3
  236. package/src/molecule3d/ui/exportMoleculeImage.ts +20 -0
  237. package/src/molecule3d/ui/moleculeViewer3DProps.ts +2 -1
  238. package/src/molecule3d/ui/useImageExport.ts +25 -3
  239. package/src/overlay/ui/FigureBar.tsx +96 -0
  240. package/src/overlay/ui/OverlayBar.tsx +15 -5
  241. package/src/overlay/ui/OverlayBarStrip.tsx +34 -22
  242. package/src/overlay/ui/OverlayIconButton.tsx +10 -2
  243. package/src/overlay/ui/index.ts +4 -0
  244. package/src/overlay/ui/overlayBarShape.ts +19 -0
  245. package/src/overlay/ui/overlayStyles.ts +61 -0
  246. package/src/overlay/ui/useFigureBarAwake.ts +56 -0
  247. package/src/pages.ts +2 -0
  248. package/src/periodic/core/elements.ts +8 -3
  249. package/src/periodic/core/index.ts +8 -1
  250. package/src/periodic/core/layout.ts +45 -3
  251. package/src/periodic/ui/BlockStrip.tsx +81 -0
  252. package/src/periodic/ui/ElementCell.tsx +9 -8
  253. package/src/periodic/ui/PeriodicTable.tsx +4 -1
  254. package/src/periodic/ui/PeriodicTableHelp.tsx +205 -0
  255. package/src/periodic/ui/cellStyles.ts +3 -2
  256. package/src/periodic/ui/index.ts +5 -0
  257. package/src/periodic/ui/periodicTableProps.ts +5 -4
  258. package/src/periodic/ui/tableLayout.ts +9 -3
  259. package/src/seo/core/trackingMiddleware.ts +48 -0
  260. package/src/share/ui/ShareDialogContent.tsx +9 -13
  261. package/src/share/ui/ShareLinkBar.tsx +16 -4
  262. package/src/share/ui/SharePreview.tsx +6 -2
  263. package/src/structure/ui/StructurePicturePane.tsx +3 -3
  264. package/styles/chrome.css +43 -32
  265. package/lib/clipboard/core/copyPng.d.ts +0 -13
  266. package/lib/clipboard/core/copyPng.d.ts.map +0 -1
  267. package/lib/clipboard/core/copyPng.js +0 -19
  268. package/lib/clipboard/core/copyPng.js.map +0 -1
  269. package/src/clipboard/core/copyPng.ts +0 -20
@@ -0,0 +1,103 @@
1
+ import { Slider } from '@blueprintjs/core';
2
+ import type { CSSProperties, ReactElement } from 'react';
3
+ import { useRef } from 'react';
4
+
5
+ import { formatPercent } from '../../format/core/numbers.ts';
6
+ import { useChromeT } from '../../i18n/ui/useT.ts';
7
+ import type { ColorStop } from '../core/interpolate.ts';
8
+ import type { ScaleTone } from '../core/tone.ts';
9
+ import { scaleTone, setScaleTone } from '../core/tone.ts';
10
+
11
+ const STEP = 0.01;
12
+ // Above zero: a grey or a black anchor has no hue left to bring back.
13
+ const MINIMUM_SATURATION = 0.1;
14
+ const MINIMUM_VALUE = 0.2;
15
+
16
+ /** What {@link ColorScaleTone} edits. */
17
+ export interface ColorScaleToneProps {
18
+ /** The anchors of the scale being edited. */
19
+ stops: readonly ColorStop[];
20
+ /** Called with the recoloured anchors on every move of either slider. */
21
+ onChange: (stops: ColorStop[]) => void;
22
+ }
23
+
24
+ /**
25
+ * Two sliders setting the saturation and the brightness of every anchor at
26
+ * once, each anchor keeping its hue.
27
+ * @param props - See {@link ColorScaleToneProps}.
28
+ * @returns The two sliders, each with its value.
29
+ */
30
+ export function ColorScaleTone(props: ColorScaleToneProps): ReactElement {
31
+ const { stops, onChange } = props;
32
+ const t = useChromeT();
33
+ const tone = scaleTone(stops);
34
+ // The anchors as a drag found them, so their hues are read once per drag
35
+ // rather than off colours already rounded by the previous move.
36
+ const base = useRef<readonly ColorStop[] | null>(null);
37
+
38
+ function set(next: Partial<ScaleTone>): void {
39
+ base.current ??= stops;
40
+ onChange(setScaleTone(base.current, next));
41
+ }
42
+
43
+ function release(): void {
44
+ base.current = null;
45
+ }
46
+
47
+ return (
48
+ <div style={GRID_STYLE} title={t('color.toneHelp')}>
49
+ <span style={LABEL_STYLE}>{t('color.saturation')}</span>
50
+ <span style={SLIDER_STYLE}>
51
+ <Slider
52
+ min={MINIMUM_SATURATION}
53
+ max={1}
54
+ stepSize={STEP}
55
+ labelRenderer={false}
56
+ value={Math.max(MINIMUM_SATURATION, tone.saturation)}
57
+ onChange={(saturation) => {
58
+ set({ saturation });
59
+ }}
60
+ onRelease={release}
61
+ />
62
+ </span>
63
+ <span style={VALUE_STYLE}>{formatPercent(tone.saturation, 0)}</span>
64
+ <span style={LABEL_STYLE}>{t('color.brightness')}</span>
65
+ <span style={SLIDER_STYLE}>
66
+ <Slider
67
+ min={MINIMUM_VALUE}
68
+ max={1}
69
+ stepSize={STEP}
70
+ labelRenderer={false}
71
+ value={Math.max(MINIMUM_VALUE, tone.value)}
72
+ onChange={(value) => {
73
+ set({ value });
74
+ }}
75
+ onRelease={release}
76
+ />
77
+ </span>
78
+ <span style={VALUE_STYLE}>{formatPercent(tone.value, 0)}</span>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ const GRID_STYLE = {
84
+ display: 'grid',
85
+ gridTemplateColumns: 'max-content minmax(0, 1fr) 3.5em',
86
+ alignItems: 'center',
87
+ columnGap: 10,
88
+ rowGap: 4,
89
+ } as const satisfies CSSProperties;
90
+
91
+ const LABEL_STYLE = { fontSize: 12 } as const satisfies CSSProperties;
92
+
93
+ /** Room on both sides, so the handle is never clipped at either end. */
94
+ const SLIDER_STYLE = {
95
+ display: 'block',
96
+ paddingInline: 8,
97
+ } as const satisfies CSSProperties;
98
+
99
+ const VALUE_STYLE = {
100
+ fontSize: 12,
101
+ fontVariantNumeric: 'tabular-nums',
102
+ textAlign: 'right',
103
+ } as const satisfies CSSProperties;
@@ -0,0 +1,239 @@
1
+ import type {
2
+ CSSProperties,
3
+ KeyboardEvent,
4
+ PointerEvent,
5
+ ReactElement,
6
+ } from 'react';
7
+ import { useRef } from 'react';
8
+
9
+ import { useChromeT } from '../../i18n/ui/useT.ts';
10
+ import type { ColorScale, ColorStop } from '../core/interpolate.ts';
11
+ import { addColorStop, moveColorStop } from '../core/stops.ts';
12
+
13
+ import { ColorScaleBar } from './ColorScaleBar.tsx';
14
+
15
+ const STRIP_HEIGHT = 28;
16
+ const STRIP_SAMPLES = 64;
17
+ const SWATCH_SIZE = 14;
18
+ const KEY_STEP = 0.01;
19
+ const KEY_STEP_LARGE = 0.1;
20
+
21
+ /** What {@link ColorScaleTrack} draws and hands back. */
22
+ export interface ColorScaleTrackProps {
23
+ /** The scale being edited. */
24
+ scale: ColorScale;
25
+ /** The anchor whose colour and position the editor shows. */
26
+ selected: number;
27
+ /** The anchor pointed at, which Backspace would remove. */
28
+ hovered: number | null;
29
+ /** Called when an anchor is clicked, focused, dragged or added. */
30
+ onSelect: (index: number) => void;
31
+ /** Called when an anchor is pointed at, and with `null` when it is left. */
32
+ onHover: (index: number | null) => void;
33
+ /** Called with the anchors after every add or move. */
34
+ onChange: (stops: ColorStop[]) => void;
35
+ /** Called when an anchor asks to be removed, from the keyboard. */
36
+ onRemove: (index: number) => void;
37
+ /** Called when an anchor is double-clicked, to recolour it. */
38
+ onPick: () => void;
39
+ }
40
+
41
+ /**
42
+ * The scale as one strip with its anchors on it: a click on the strip adds an
43
+ * anchor there, and an anchor is dragged along it.
44
+ * @param props - See {@link ColorScaleTrackProps}.
45
+ * @returns The strip and its anchors.
46
+ */
47
+ export function ColorScaleTrack(props: ColorScaleTrackProps): ReactElement {
48
+ const {
49
+ scale,
50
+ selected,
51
+ hovered,
52
+ onSelect,
53
+ onHover,
54
+ onChange,
55
+ onRemove,
56
+ onPick,
57
+ } = props;
58
+ const t = useChromeT();
59
+ // The anchors as the drag left them: a pointer can move twice before the
60
+ // page renders the first move, and the second must not undo it.
61
+ const drag = useRef<{ index: number; stops: ColorStop[] } | null>(null);
62
+
63
+ function start(event: PointerEvent<HTMLDivElement>): void {
64
+ if (event.button !== 0) return;
65
+ const anchor = (event.target as HTMLElement).closest<HTMLElement>(
66
+ '[data-anchor]',
67
+ );
68
+ let index = Number(anchor?.dataset.anchor);
69
+ let stops = [...scale.stops];
70
+ if (anchor === null) {
71
+ const added = addColorStop(scale, positionOf(event));
72
+ if (added === null) return;
73
+ ({ index, stops } = added);
74
+ onChange(stops);
75
+ }
76
+ event.preventDefault();
77
+ event.currentTarget.setPointerCapture(event.pointerId);
78
+ anchor?.focus({ focusVisible: false });
79
+ drag.current = { index, stops };
80
+ onSelect(index);
81
+ }
82
+
83
+ function move(event: PointerEvent<HTMLDivElement>): void {
84
+ if (drag.current === null) return;
85
+ const moved = moveColorStop(
86
+ drag.current.stops,
87
+ drag.current.index,
88
+ positionOf(event),
89
+ );
90
+ drag.current = moved;
91
+ onChange(moved.stops);
92
+ onSelect(moved.index);
93
+ onHover(moved.index);
94
+ }
95
+
96
+ function stop(): void {
97
+ drag.current = null;
98
+ }
99
+
100
+ function nudge(event: KeyboardEvent<HTMLSpanElement>, index: number): void {
101
+ if (event.key === 'Backspace' || event.key === 'Delete') {
102
+ event.preventDefault();
103
+ event.stopPropagation();
104
+ onRemove(index);
105
+ return;
106
+ }
107
+ const step = event.shiftKey ? KEY_STEP_LARGE : KEY_STEP;
108
+ const delta =
109
+ event.key === 'ArrowRight' || event.key === 'ArrowUp'
110
+ ? step
111
+ : event.key === 'ArrowLeft' || event.key === 'ArrowDown'
112
+ ? -step
113
+ : 0;
114
+ if (delta === 0) return;
115
+ event.preventDefault();
116
+ // From the keyboard an anchor stops at its neighbours, so focus stays on
117
+ // the anchor being moved.
118
+ const low = scale.stops[index - 1]?.position ?? 0;
119
+ const high = scale.stops[index + 1]?.position ?? 1;
120
+ const position = (scale.stops[index]?.position ?? 0) + delta;
121
+ const moved = moveColorStop(
122
+ scale.stops,
123
+ index,
124
+ Math.min(high, Math.max(low, position)),
125
+ );
126
+ onChange(moved.stops);
127
+ }
128
+
129
+ return (
130
+ <div
131
+ style={TRACK_STYLE}
132
+ onPointerDown={start}
133
+ onPointerMove={move}
134
+ onPointerUp={stop}
135
+ onPointerCancel={stop}
136
+ >
137
+ <ColorScaleBar
138
+ scale={scale}
139
+ height={STRIP_HEIGHT}
140
+ samples={STRIP_SAMPLES}
141
+ label={t('color.scaleBeingEdited')}
142
+ />
143
+ {scale.stops.map((anchor, index) => (
144
+ <span
145
+ // An anchor has no identity but its rank, and keeping the element
146
+ // keeps the focus on it while it is dragged past a neighbour.
147
+ key={`anchor-${String(index)}`}
148
+ data-anchor={index}
149
+ role="slider"
150
+ tabIndex={0}
151
+ aria-label={t('color.anchorPosition', { index: index + 1 })}
152
+ aria-valuemin={0}
153
+ aria-valuemax={1}
154
+ aria-valuenow={anchor.position}
155
+ title={anchor.color}
156
+ style={{
157
+ ...ANCHOR_STYLE,
158
+ left: `${String(anchor.position * 100)}%`,
159
+ zIndex: index === selected ? 2 : 1,
160
+ }}
161
+ onFocus={() => {
162
+ onSelect(index);
163
+ }}
164
+ onKeyDown={(event) => {
165
+ nudge(event, index);
166
+ }}
167
+ onPointerEnter={() => {
168
+ if (drag.current === null) onHover(index);
169
+ }}
170
+ onPointerLeave={() => {
171
+ if (drag.current === null) onHover(null);
172
+ }}
173
+ onDoubleClick={onPick}
174
+ >
175
+ <span style={NEEDLE_STYLE} />
176
+ <span
177
+ style={{
178
+ ...SWATCH_STYLE,
179
+ background: anchor.color,
180
+ boxShadow:
181
+ index === selected
182
+ ? SELECTED_RING
183
+ : index === hovered
184
+ ? HOVERED_RING
185
+ : SWATCH_STYLE.boxShadow,
186
+ }}
187
+ />
188
+ </span>
189
+ ))}
190
+ </div>
191
+ );
192
+ }
193
+
194
+ function positionOf(event: PointerEvent<HTMLDivElement>): number {
195
+ const box = event.currentTarget.getBoundingClientRect();
196
+ return box.width === 0 ? 0 : (event.clientX - box.left) / box.width;
197
+ }
198
+
199
+ const SELECTED_RING = '0 0 0 2px var(--accent)';
200
+ const HOVERED_RING = '0 0 0 2px var(--text-muted)';
201
+
202
+ const TRACK_STYLE = {
203
+ position: 'relative',
204
+ margin: `0 ${String(SWATCH_SIZE / 2 + 2)}px`,
205
+ paddingBottom: SWATCH_SIZE + 4,
206
+ cursor: 'copy',
207
+ touchAction: 'none',
208
+ userSelect: 'none',
209
+ } as const satisfies CSSProperties;
210
+
211
+ const ANCHOR_STYLE = {
212
+ position: 'absolute',
213
+ top: -3,
214
+ bottom: 0,
215
+ display: 'flex',
216
+ width: SWATCH_SIZE + 4,
217
+ flexDirection: 'column',
218
+ alignItems: 'center',
219
+ cursor: 'ew-resize',
220
+ outline: 'none',
221
+ transform: 'translateX(-50%)',
222
+ } as const satisfies CSSProperties;
223
+
224
+ const NEEDLE_STYLE = {
225
+ width: 3,
226
+ height: STRIP_HEIGHT + 6,
227
+ border: '1px solid var(--text)',
228
+ borderRadius: 2,
229
+ background: 'var(--surface)',
230
+ } as const satisfies CSSProperties;
231
+
232
+ const SWATCH_STYLE = {
233
+ width: SWATCH_SIZE,
234
+ height: SWATCH_SIZE,
235
+ marginTop: -2,
236
+ border: '2px solid var(--surface)',
237
+ borderRadius: 4,
238
+ boxShadow: '0 0 0 1px var(--border-strong)',
239
+ } as const satisfies CSSProperties;
@@ -0,0 +1,20 @@
1
+ import { writeBlobToClipboard } from '../../clipboard/core/writeBlobToClipboard.ts';
2
+
3
+ /** What the last copy came to, when it is worth a line of its own. */
4
+ export type FigureNotice = 'copied' | 'copyUnsupported';
5
+
6
+ /**
7
+ * Put a figure on the clipboard as a PNG.
8
+ *
9
+ * Call it from the click itself, with the picture still being painted: Safari
10
+ * only allows a clipboard write the click started.
11
+ * @param png - The picture being painted.
12
+ * @returns `copied`, or `copyUnsupported` where the clipboard takes no picture.
13
+ * @throws {Error} The painting's own error, when the picture could not be
14
+ * painted — a failure to report, not a browser that cannot copy.
15
+ */
16
+ export async function copyFigure(png: Promise<Blob>): Promise<FigureNotice> {
17
+ if (await writeBlobToClipboard(png)) return 'copied';
18
+ await png;
19
+ return 'copyUnsupported';
20
+ }
@@ -70,6 +70,7 @@ function paintElement(
70
70
  markup: drawingMarkup(element),
71
71
  x: box.left - context.origin.left,
72
72
  y: box.top - context.origin.top,
73
+ size: { width: box.width, height: box.height },
73
74
  });
74
75
  return;
75
76
  }
@@ -89,6 +89,13 @@ export interface HtmlPaintDrawing {
89
89
  x: number;
90
90
  /** Its top edge, in pixels from the top of the part. */
91
91
  y: number;
92
+ /**
93
+ * Its width and height as the browser drew it. A drawing sized by CSS
94
+ * carries no size of its own, and an `<svg>` without one fills the whole
95
+ * file.
96
+ * @default undefined — the markup is placed as it is
97
+ */
98
+ size?: { width: number; height: number };
92
99
  }
93
100
 
94
101
  /** Everything inside an element the page drew faded. */
@@ -123,7 +130,7 @@ function paintMarkup(paint: HtmlPaint): string {
123
130
  case 'text':
124
131
  return textMarkup(paint);
125
132
  case 'drawing':
126
- return `<g transform="translate(${round(paint.x)} ${round(paint.y)})">${paint.markup}</g>`;
133
+ return `<g transform="translate(${round(paint.x)} ${round(paint.y)})">${sizedDrawing(paint)}</g>`;
127
134
  case 'group':
128
135
  return `<g opacity="${round(paint.opacity)}">${figureHtmlMarkup(paint.children)}</g>`;
129
136
  default:
@@ -131,6 +138,12 @@ function paintMarkup(paint: HtmlPaint): string {
131
138
  }
132
139
  }
133
140
 
141
+ function sizedDrawing(drawing: HtmlPaintDrawing): string {
142
+ const { markup, size } = drawing;
143
+ if (size === undefined) return markup;
144
+ return `<svg width="${round(size.width)}" height="${round(size.height)}" overflow="visible">${markup}</svg>`;
145
+ }
146
+
134
147
  function boxMarkup(box: HtmlPaintBox): string {
135
148
  const { x, y, width, height, fill, stroke, dash } = box;
136
149
  const { radius = 0, strokeWidth = 1 } = box;
@@ -1,3 +1,5 @@
1
+ export type { FigureNotice } from './copyFigure.ts';
2
+ export { copyFigure } from './copyFigure.ts';
1
3
  export { cssTokenNames, resolveCssTokens } from './cssTokens.ts';
2
4
  export { downloadBlob } from './downloadBlob.ts';
3
5
  export type { DownloadFigureOptions, FigureFormat } from './downloadFigure.ts';
@@ -5,6 +5,7 @@ import { OverlayAction } from '../../overlay/ui/OverlayAction.tsx';
5
5
  import { OverlayPanel } from '../../overlay/ui/OverlayPanel.tsx';
6
6
  import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
7
7
  import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
8
+ import type { FigureNotice } from '../core/copyFigure.ts';
8
9
  import type { FigureFormat } from '../core/downloadFigure.ts';
9
10
  import { figureLayoutSize } from '../core/figureLayout.ts';
10
11
  import type { FigurePixels } from '../core/figureScale.ts';
@@ -18,7 +19,6 @@ import {
18
19
  import type { FigureSizing } from './FigureSizeRows.tsx';
19
20
  import { FigureSizeRows } from './FigureSizeRows.tsx';
20
21
  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 {
@@ -51,9 +51,8 @@ export interface FigureDownloadPanelProps {
51
51
  /**
52
52
  * Called when they press copy, which puts a PNG at the resolution picked on
53
53
  * the clipboard.
54
- * @default undefined — no copy is offered
55
54
  */
56
- onCopy?: () => void;
55
+ onCopy: () => void;
57
56
  /**
58
57
  * Whether the SVG holds a rendered picture rather than vector drawings, as
59
58
  * for a WebGL scene: its resolution is then the pixels of that picture, it
@@ -122,14 +121,12 @@ export function FigureDownloadPanel(
122
121
  disabled={saving || size === null}
123
122
  onClick={onSave}
124
123
  />
125
- {onCopy === undefined ? null : (
126
- <OverlayAction
127
- text={t('clipboard.copy')}
128
- icon="clipboard"
129
- disabled={saving || size === null}
130
- onClick={onCopy}
131
- />
132
- )}
124
+ <OverlayAction
125
+ text={t('clipboard.copy')}
126
+ icon="clipboard"
127
+ disabled={saving || size === null}
128
+ onClick={onCopy}
129
+ />
133
130
  </>
134
131
  }
135
132
  >
@@ -1,11 +1,10 @@
1
1
  import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
2
2
  import type { Translate } from '../../i18n/ui/useT.ts';
3
+ import type { FigureNotice } from '../core/copyFigure.ts';
3
4
  import type { FigureFormat } from '../core/downloadFigure.ts';
4
5
  import type { FigurePixels } from '../core/figureScale.ts';
5
6
  import { figurePixels, formatFigurePixels } from '../core/figureScale.ts';
6
7
 
7
- import type { FigureNotice } from './useFigureActions.ts';
8
-
9
8
  /** What the line at the foot of the panel is written from. */
10
9
  export interface HintState {
11
10
  /** Which file the reader is about to write. */
@@ -19,4 +19,3 @@ export type {
19
19
  FigureDownloadState,
20
20
  } from './useFigureDownload.tsx';
21
21
  export { useFigureDownload } from './useFigureDownload.tsx';
22
- export type { FigureNotice } from './useFigureActions.ts';
@@ -1,6 +1,4 @@
1
- import { useCallback, useState } from 'react';
2
-
3
- import { writeImageToClipboard } from '../../clipboard/core/copyPng.ts';
1
+ import { copyFigure } from '../core/copyFigure.ts';
4
2
  import type { FigureFormat } from '../core/downloadFigure.ts';
5
3
  import { downloadFigure } from '../core/downloadFigure.ts';
6
4
  import { figurePng } from '../core/figurePng.ts';
@@ -8,9 +6,8 @@ import type { FigurePixels } from '../core/figureScale.ts';
8
6
  import { figureSvg } from '../core/figureSvg.ts';
9
7
 
10
8
  import type { FigureRedraw } from './useFigureRedraw.tsx';
11
-
12
- /** What the last copy came to, when it is worth a line of its own. */
13
- export type FigureNotice = 'copied' | 'copyUnsupported';
9
+ import type { FigureTask } from './useFigureTask.ts';
10
+ import { useFigureTask } from './useFigureTask.ts';
14
11
 
15
12
  /** What {@link useFigureActions} works from. */
16
13
  export interface FigureActionsOptions {
@@ -34,19 +31,11 @@ export interface FigureActionsOptions {
34
31
  }
35
32
 
36
33
  /** What {@link useFigureActions} hands back. */
37
- export interface FigureActions {
34
+ export interface FigureActions extends Omit<FigureTask, 'run'> {
38
35
  /** Write the figure to a file, in the format picked. */
39
36
  save: () => void;
40
37
  /** Put the figure on the clipboard, as a PNG at the resolution picked. */
41
38
  copy: () => void;
42
- /** Whether a file or a copy is being written right now. */
43
- busy: boolean;
44
- /** What went wrong the last time, if anything. */
45
- failure: string | null;
46
- /** What the last copy came to, if it is still what the panel describes. */
47
- notice: FigureNotice | null;
48
- /** Forget the last outcome, once the choices it answered have changed. */
49
- clear: () => void;
50
39
  }
51
40
 
52
41
  /**
@@ -60,9 +49,7 @@ export interface FigureActions {
60
49
  export function useFigureActions(options: FigureActionsOptions): FigureActions {
61
50
  const { targetId, format, scale, fileName, background } = options;
62
51
  const { redrawAt, redraw } = options;
63
- const [busy, setBusy] = useState(false);
64
- const [failure, setFailure] = useState<string | null>(null);
65
- const [notice, setNotice] = useState<FigureNotice | null>(null);
52
+ const { run, busy, failure, notice, clear } = useFigureTask();
66
53
 
67
54
  function withFigure(write: (box: string | Element) => Promise<void>) {
68
55
  return redrawAt === null
@@ -70,17 +57,6 @@ export function useFigureActions(options: FigureActionsOptions): FigureActions {
70
57
  : redraw(targetId, redrawAt, write);
71
58
  }
72
59
 
73
- function run(task: Promise<FigureNotice | null>): void {
74
- setBusy(true);
75
- setFailure(null);
76
- setNotice(null);
77
- void task
78
- .then(setNotice, (error: unknown) => {
79
- setFailure(error instanceof Error ? error.message : String(error));
80
- })
81
- .finally(() => setBusy(false));
82
- }
83
-
84
60
  function save(): void {
85
61
  const options = { format, scale, fileName, background };
86
62
  run(withFigure((box) => downloadFigure(box, options)).then(() => null));
@@ -94,20 +70,8 @@ export function useFigureActions(options: FigureActionsOptions): FigureActions {
94
70
  resolve(await figurePng(figureSvg(box, { background }), scale));
95
71
  }).catch(reject);
96
72
  });
97
- // A browser whose clipboard takes text only never reads the picture, so
98
- // its failure would otherwise go unhandled.
99
- png.catch(() => undefined);
100
- run(
101
- writeImageToClipboard(png).then((copied) =>
102
- copied ? 'copied' : 'copyUnsupported',
103
- ),
104
- );
73
+ run(copyFigure(png));
105
74
  }
106
75
 
107
- const clear = useCallback(() => {
108
- setFailure(null);
109
- setNotice(null);
110
- }, []);
111
-
112
76
  return { save, copy, busy, failure, notice, clear };
113
77
  }
@@ -0,0 +1,46 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import type { FigureNotice } from '../core/copyFigure.ts';
4
+
5
+ /** What {@link useFigureTask} hands back. */
6
+ export interface FigureTask {
7
+ /** Run a save or a copy, keeping what it came to. */
8
+ run: (task: Promise<FigureNotice | null>) => void;
9
+ /** Whether a file or a copy is being written right now. */
10
+ busy: boolean;
11
+ /** What went wrong the last time, if anything. */
12
+ failure: string | null;
13
+ /** What the last copy came to, if it is still what the panel describes. */
14
+ notice: FigureNotice | null;
15
+ /** Forget the last outcome, once the choices it answered have changed. */
16
+ clear: () => void;
17
+ }
18
+
19
+ /**
20
+ * The state behind the figure panel's save and copy buttons, whatever the
21
+ * figure is drawn with: whether one is running, and what the last one came to.
22
+ * @returns See {@link FigureTask}.
23
+ */
24
+ export function useFigureTask(): FigureTask {
25
+ const [busy, setBusy] = useState(false);
26
+ const [failure, setFailure] = useState<string | null>(null);
27
+ const [notice, setNotice] = useState<FigureNotice | null>(null);
28
+
29
+ const run = useCallback((task: Promise<FigureNotice | null>) => {
30
+ setBusy(true);
31
+ setFailure(null);
32
+ setNotice(null);
33
+ void task
34
+ .then(setNotice, (error: unknown) => {
35
+ setFailure(error instanceof Error ? error.message : String(error));
36
+ })
37
+ .finally(() => setBusy(false));
38
+ }, []);
39
+
40
+ const clear = useCallback(() => {
41
+ setFailure(null);
42
+ setNotice(null);
43
+ }, []);
44
+
45
+ return { run, busy, failure, notice, clear };
46
+ }