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
@@ -1,24 +1,26 @@
1
1
  import { Button, HTMLSelect } from '@blueprintjs/core';
2
2
  import type { CSSProperties, ReactElement } from 'react';
3
+ import { useEffect, useRef, useState } from 'react';
3
4
 
4
- import { formatDecimal } from '../../format/core/numbers.ts';
5
5
  import { useChromeT } from '../../i18n/ui/useT.ts';
6
+ import { NumberInput } from '../../number/ui/NumberInput.tsx';
6
7
  import { normalizeHexColor } from '../core/hex.ts';
7
8
  import type {
8
9
  ColorInterpolation,
9
10
  ColorScale,
10
11
  ColorStop,
11
12
  } from '../core/interpolate.ts';
12
- import { colorAt } from '../core/interpolate.ts';
13
- import { MAXIMUM_CUSTOM_STOPS } from '../core/scaleText.ts';
13
+ import {
14
+ MINIMUM_CUSTOM_STOPS,
15
+ moveColorStop,
16
+ recolorColorStop,
17
+ removeColorStop,
18
+ } from '../core/stops.ts';
14
19
 
15
- import { ColorScaleBar } from './ColorScaleBar.tsx';
20
+ import { ColorScaleTone } from './ColorScaleTone.tsx';
21
+ import { ColorScaleTrack } from './ColorScaleTrack.tsx';
16
22
 
17
- const MINIMUM_STOPS = 2;
18
23
  const POSITION_STEP = 0.01;
19
- const PREVIEW_HEIGHT = 16;
20
- const PREVIEW_SAMPLES = 48;
21
- const POSITION_DECIMALS = 2;
22
24
  // A `type="color"` input only takes six hex digits; an anchor that is not a
23
25
  // hex colour is shown as black until it is picked again.
24
26
  const UNREADABLE_COLOR = '#000000';
@@ -44,88 +46,108 @@ export interface ColorScaleEditorProps {
44
46
  }
45
47
 
46
48
  /**
47
- * A colour scale of the reader's own: the colours it passes through, where
48
- * each one sits, and the path it takes between them.
49
+ * A colour scale of the reader's own, edited on the strip it draws.
49
50
  *
50
- * A colour is anchored rather than typed into a list, so two anchors and the
51
- * HSV path already describe a whole rainbow, and an anchor never crosses its
52
- * neighbours — the scale it draws is always the one the strip above shows.
51
+ * A click on the strip adds an anchor in the colour already there, an anchor
52
+ * is dragged along it, and an anchor pointed at goes with Backspace. The
53
+ * anchor last touched shows its colour and position below, to set exactly.
53
54
  * @param props - See {@link ColorScaleEditorProps}.
54
- * @returns The preview, the anchors, and the picker of the path between them.
55
+ * @returns The strip, the selected anchor, and the picker of the path between anchors.
55
56
  */
56
57
  export function ColorScaleEditor(props: ColorScaleEditorProps): ReactElement {
57
58
  const { className, value, onChange } = props;
58
59
  const t = useChromeT();
59
60
  const stops = value.stops;
60
-
61
- function write(next: readonly ColorStop[]): void {
61
+ const [picked, setPicked] = useState(0);
62
+ const [hovered, setHovered] = useState<number | null>(null);
63
+ const colorInput = useRef<HTMLInputElement>(null);
64
+ const selected = Math.min(picked, stops.length - 1);
65
+ const stop = stops[selected];
66
+ const removable = stops.length > MINIMUM_CUSTOM_STOPS;
67
+
68
+ function write(next: ColorStop[]): void {
62
69
  onChange({ stops: next, interpolation: value.interpolation });
63
70
  }
64
71
 
72
+ function remove(index: number): void {
73
+ if (!removable) return;
74
+ write(removeColorStop(stops, index));
75
+ setHovered(null);
76
+ setPicked(Math.max(0, index - 1));
77
+ }
78
+
79
+ useEffect(() => {
80
+ if (hovered === null || !removable) return;
81
+ const index = hovered;
82
+ function onKeyDown(event: globalThis.KeyboardEvent): void {
83
+ if (event.key !== 'Backspace' && event.key !== 'Delete') return;
84
+ if (isEditable(event.target)) return;
85
+ event.preventDefault();
86
+ onChange({
87
+ stops: removeColorStop(stops, index),
88
+ interpolation: value.interpolation,
89
+ });
90
+ setHovered(null);
91
+ setPicked(Math.max(0, index - 1));
92
+ }
93
+ globalThis.addEventListener('keydown', onKeyDown);
94
+ return () => {
95
+ globalThis.removeEventListener('keydown', onKeyDown);
96
+ };
97
+ }, [hovered, removable, stops, value.interpolation, onChange]);
98
+
65
99
  return (
66
100
  <div className={className} style={PANEL_STYLE}>
67
- <ColorScaleBar
101
+ <ColorScaleTrack
68
102
  scale={value}
69
- height={PREVIEW_HEIGHT}
70
- samples={PREVIEW_SAMPLES}
71
- label={t('color.scaleBeingEdited')}
103
+ selected={selected}
104
+ hovered={hovered}
105
+ onSelect={setPicked}
106
+ onHover={setHovered}
107
+ onChange={write}
108
+ onRemove={remove}
109
+ onPick={() => {
110
+ colorInput.current?.showPicker();
111
+ }}
72
112
  />
73
113
 
74
- <div style={ROWS_STYLE}>
75
- {stops.map((stop, index) => (
76
- <div key={`${String(index)}-${stop.color}`} style={ROW_STYLE}>
77
- <input
78
- type="color"
79
- aria-label={t('color.anchorColour', { index: index + 1 })}
80
- value={normalizeHexColor(stop.color) ?? UNREADABLE_COLOR}
81
- style={SWATCH_STYLE}
82
- onChange={(event) => {
83
- write(replace(stops, index, { color: event.target.value }));
84
- }}
85
- />
86
- <input
87
- type="range"
88
- aria-label={t('color.anchorPosition', { index: index + 1 })}
89
- min={0}
90
- max={1}
91
- step={POSITION_STEP}
92
- value={stop.position}
93
- style={SLIDER_STYLE}
94
- onChange={(event) => {
95
- write(
96
- replace(stops, index, {
97
- position: hold(stops, index, event.target.valueAsNumber),
98
- }),
99
- );
100
- }}
101
- />
102
- <span style={POSITION_STYLE}>
103
- {formatDecimal(stop.position, POSITION_DECIMALS)}
104
- </span>
105
- <Button
106
- icon="cross"
107
- variant="minimal"
108
- size="small"
109
- aria-label={t('color.removeAnchor', { index: index + 1 })}
110
- disabled={stops.length <= MINIMUM_STOPS}
111
- onClick={() => {
112
- write(stops.filter((_, at) => at !== index));
113
- }}
114
- />
115
- </div>
116
- ))}
117
- </div>
118
-
119
- <div style={FOOTER_STYLE}>
114
+ <div style={ROW_STYLE}>
115
+ <input
116
+ ref={colorInput}
117
+ type="color"
118
+ aria-label={t('color.anchorColour', { index: selected + 1 })}
119
+ value={normalizeHexColor(stop?.color ?? '') ?? UNREADABLE_COLOR}
120
+ style={SWATCH_STYLE}
121
+ onChange={(event) => {
122
+ write(recolorColorStop(stops, selected, event.target.value));
123
+ }}
124
+ />
125
+ <NumberInput
126
+ ariaLabel={t('color.anchorPosition', { index: selected + 1 })}
127
+ value={stop?.position ?? 0}
128
+ min={0}
129
+ max={1}
130
+ step={POSITION_STEP}
131
+ size="small"
132
+ buttons={false}
133
+ style={POSITION_STYLE}
134
+ onChange={(position) => {
135
+ const moved = moveColorStop(stops, selected, position);
136
+ write(moved.stops);
137
+ setPicked(moved.index);
138
+ }}
139
+ />
120
140
  <Button
121
- icon="plus"
141
+ icon="trash"
142
+ variant="minimal"
122
143
  size="small"
123
- text={t('color.addColour')}
124
- disabled={stops.length >= MAXIMUM_CUSTOM_STOPS}
144
+ aria-label={t('color.removeAnchor', { index: selected + 1 })}
145
+ disabled={!removable}
125
146
  onClick={() => {
126
- write(withAddedStop(value));
147
+ remove(selected);
127
148
  }}
128
149
  />
150
+ <span style={SPACER_STYLE} />
129
151
  <HTMLSelect
130
152
  aria-label={t('color.path')}
131
153
  value={value.interpolation}
@@ -141,59 +163,22 @@ export function ColorScaleEditor(props: ColorScaleEditorProps): ReactElement {
141
163
  }}
142
164
  />
143
165
  </div>
144
- </div>
145
- );
146
- }
147
166
 
148
- function replace(
149
- stops: readonly ColorStop[],
150
- index: number,
151
- patch: Partial<ColorStop>,
152
- ): ColorStop[] {
153
- return stops.map((stop, at) => (at === index ? { ...stop, ...patch } : stop));
154
- }
167
+ <ColorScaleTone stops={stops} onChange={write} />
155
168
 
156
- /**
157
- * A position an anchor may take: never past either of its neighbours, so the
158
- * rows never reorder under a dragging finger.
159
- * @param stops - The anchors as they stand.
160
- * @param index - Which of them is being moved.
161
- * @param position - Where it is being moved to.
162
- * @returns The position it is allowed to take.
163
- */
164
- function hold(
165
- stops: readonly ColorStop[],
166
- index: number,
167
- position: number,
168
- ): number {
169
- if (!Number.isFinite(position)) return stops[index]?.position ?? 0;
170
- const low = stops[index - 1]?.position ?? 0;
171
- const high = stops[index + 1]?.position ?? 1;
172
- return Math.min(high, Math.max(low, position));
169
+ <p style={HINT_STYLE}>{t('color.editorHint')}</p>
170
+ </div>
171
+ );
173
172
  }
174
173
 
175
- /**
176
- * The anchors with one more, dropped in the widest gap and taking the colour
177
- * the scale already has there — so adding one changes nothing until it is moved.
178
- * @param scale - The scale being edited.
179
- * @returns Its anchors, with the new one among them.
180
- */
181
- function withAddedStop(scale: ColorScale): ColorStop[] {
182
- const stops = [...scale.stops];
183
- let widest = 0;
184
- let at = 1;
185
- for (let index = 1; index < stops.length; index++) {
186
- const span =
187
- (stops[index]?.position ?? 0) - (stops[index - 1]?.position ?? 0);
188
- if (span > widest) {
189
- widest = span;
190
- at = index;
191
- }
192
- }
193
- const position =
194
- ((stops[at]?.position ?? 1) + (stops[at - 1]?.position ?? 0)) / 2;
195
- stops.splice(at, 0, { position, color: colorAt(scale, position) });
196
- return stops;
174
+ function isEditable(target: EventTarget | null): boolean {
175
+ if (!(target instanceof HTMLElement)) return false;
176
+ return (
177
+ target.isContentEditable ||
178
+ target instanceof HTMLInputElement ||
179
+ target instanceof HTMLTextAreaElement ||
180
+ target instanceof HTMLSelectElement
181
+ );
197
182
  }
198
183
 
199
184
  const PANEL_STYLE = {
@@ -201,18 +186,13 @@ const PANEL_STYLE = {
201
186
  flexDirection: 'column',
202
187
  gap: 10,
203
188
  padding: 12,
204
- minWidth: 280,
205
- } as const satisfies CSSProperties;
206
-
207
- const ROWS_STYLE = {
208
- display: 'flex',
209
- flexDirection: 'column',
210
- gap: 4,
189
+ minWidth: 320,
211
190
  } as const satisfies CSSProperties;
212
191
 
213
192
  const ROW_STYLE = {
214
193
  display: 'flex',
215
194
  alignItems: 'center',
195
+ flexWrap: 'wrap',
216
196
  gap: 8,
217
197
  } as const satisfies CSSProperties;
218
198
 
@@ -225,23 +205,17 @@ const SWATCH_STYLE = {
225
205
  cursor: 'pointer',
226
206
  } as const satisfies CSSProperties;
227
207
 
228
- const SLIDER_STYLE = {
229
- flex: '1 1 auto',
230
- minWidth: 90,
231
- accentColor: 'var(--accent, currentColor)',
208
+ const POSITION_STYLE = {
209
+ width: 64,
232
210
  } as const satisfies CSSProperties;
233
211
 
234
- const POSITION_STYLE = {
235
- fontSize: 12,
236
- fontVariantNumeric: 'tabular-nums',
237
- width: 30,
238
- textAlign: 'right',
212
+ const SPACER_STYLE = {
213
+ flex: '1 1 auto',
239
214
  } as const satisfies CSSProperties;
240
215
 
241
- const FOOTER_STYLE = {
242
- display: 'flex',
243
- alignItems: 'center',
244
- flexWrap: 'wrap',
245
- gap: 8,
246
- justifyContent: 'space-between',
216
+ const HINT_STYLE = {
217
+ margin: 0,
218
+ color: 'var(--text-muted)',
219
+ fontSize: 12,
220
+ lineHeight: 1.4,
247
221
  } as const satisfies CSSProperties;
@@ -9,15 +9,28 @@ import type { CSSProperties, ReactElement } from 'react';
9
9
  import { useState } from 'react';
10
10
 
11
11
  import { useChromeT } from '../../i18n/ui/useT.ts';
12
+ import { OverlayValueButton } from '../../overlay/ui/OverlayValueButton.tsx';
12
13
  import { TOKEN } from '../../tokens/core/familyTokens.ts';
14
+ import { normalizeHexColor } from '../core/hex.ts';
15
+ import { sampleScale } from '../core/interpolate.ts';
13
16
  import { formatColorScale, resolveColorScale } from '../core/scaleText.ts';
14
17
  import { COLOR_SCALES } from '../core/scales.ts';
18
+ import {
19
+ UNIFORM_COLOR_SCALE_ID,
20
+ formatUniformColorScale,
21
+ isUniformColorScale,
22
+ uniformSwatch,
23
+ } from '../core/uniform.ts';
15
24
 
16
25
  import { ColorScaleBar } from './ColorScaleBar.tsx';
17
26
  import { ColorScaleEditor } from './ColorScaleEditor.tsx';
18
27
  import { ColorScaleEntry } from './ColorScaleEntry.tsx';
19
28
 
20
29
  const BUTTON_BAR_WIDTH = 72;
30
+ // Its two ends and its middle: the most dots `OverlaySwatchIcon` draws.
31
+ const BAR_SWATCHES = 3;
32
+ // A `type="color"` input only takes six hex digits.
33
+ const UNREADABLE_COLOR = '#000000';
21
34
 
22
35
  /** What {@link ColorScaleSelect} chooses. */
23
36
  export interface ColorScaleSelectProps {
@@ -38,6 +51,22 @@ export interface ColorScaleSelectProps {
38
51
  * @default true
39
52
  */
40
53
  allowCustom?: boolean;
54
+ /**
55
+ * Whether the menu opens on one colour for every value, which a figure that
56
+ * needs no key — a plain table to print — is drawn with. Chosen, it writes
57
+ * {@link UNIFORM_COLOR_SCALE_ID}; its swatch picks the colour, written as
58
+ * `formatUniformColorScale` does.
59
+ * @default false
60
+ */
61
+ allowUniform?: boolean;
62
+ /**
63
+ * Whether it is a form field — a caption over a full-width button — or a
64
+ * setting on a figure's bar, writing its name and its scale on one compact
65
+ * line as `OverlayValueButton` does. The bar form needs an `OverlayLayer` or
66
+ * a `FigureBar` above it for its measurements.
67
+ * @default 'field'
68
+ */
69
+ appearance?: 'field' | 'bar';
41
70
  /**
42
71
  * Value of the `data-testid` attribute of the button, for the end-to-end tests.
43
72
  * @default undefined
@@ -67,11 +96,15 @@ export function ColorScaleSelect(props: ColorScaleSelectProps): ReactElement {
67
96
  onChange,
68
97
  label = '',
69
98
  allowCustom = true,
99
+ allowUniform = false,
100
+ appearance = 'field',
70
101
  testId,
71
102
  } = props;
72
103
  const t = useChromeT();
73
104
  const [isOpen, setOpen] = useState(false);
74
105
  const [isEditing, setEditing] = useState(false);
106
+ const uniform = allowUniform && isUniformColorScale(value);
107
+ const oneColor = uniformSwatch(uniform ? value : undefined).background;
75
108
  const resolved = resolveColorScale(value);
76
109
 
77
110
  const content = isEditing ? (
@@ -104,6 +137,36 @@ export function ColorScaleSelect(props: ColorScaleSelectProps): ReactElement {
104
137
  </div>
105
138
  ) : (
106
139
  <Menu style={MENU_STYLE}>
140
+ {allowUniform ? (
141
+ <MenuItem
142
+ roleStructure="listoption"
143
+ selected={uniform}
144
+ htmlTitle={t('color.uniformDescription')}
145
+ text={
146
+ <span style={UNIFORM_ITEM_STYLE}>
147
+ <span>{t('color.uniform')}</span>
148
+ <input
149
+ type="color"
150
+ aria-label={t('color.uniformColour')}
151
+ title={t('color.uniformColour')}
152
+ value={normalizeHexColor(oneColor) ?? UNREADABLE_COLOR}
153
+ style={UNIFORM_INPUT_STYLE}
154
+ // The swatch picks the colour; the rest of the row chooses
155
+ // one colour and closes the menu.
156
+ onClick={(event) => {
157
+ event.stopPropagation();
158
+ }}
159
+ onChange={(event) => {
160
+ onChange(formatUniformColorScale(event.target.value));
161
+ }}
162
+ />
163
+ </span>
164
+ }
165
+ onClick={() => {
166
+ onChange(uniform ? value : UNIFORM_COLOR_SCALE_ID);
167
+ }}
168
+ />
169
+ ) : null}
107
170
  {COLOR_SCALES.map((entry, index) => (
108
171
  <ColorScaleEntry
109
172
  key={entry.id}
@@ -112,7 +175,7 @@ export function ColorScaleSelect(props: ColorScaleSelectProps): ReactElement {
112
175
  kind={entry.kind}
113
176
  description={entry.description}
114
177
  previous={COLOR_SCALES[index - 1]?.kind}
115
- selected={entry.id === resolved.id}
178
+ selected={!uniform && entry.id === resolved.id}
116
179
  onChange={onChange}
117
180
  />
118
181
  ))}
@@ -122,7 +185,9 @@ export function ColorScaleSelect(props: ColorScaleSelectProps): ReactElement {
122
185
  <MenuItem
123
186
  icon="edit"
124
187
  text={t('color.customEllipsis')}
125
- label={resolved.id === null ? t('color.inUse') : undefined}
188
+ label={
189
+ resolved.id === null && !uniform ? t('color.inUse') : undefined
190
+ }
126
191
  shouldDismissPopover={false}
127
192
  onClick={() => {
128
193
  setEditing(true);
@@ -133,44 +198,79 @@ export function ColorScaleSelect(props: ColorScaleSelectProps): ReactElement {
133
198
  </Menu>
134
199
  );
135
200
 
136
- return (
137
- <label className={className} style={FIELD_STYLE}>
138
- {label === '' ? null : <span style={CAPTION_STYLE}>{label}</span>}
139
- <PopoverNext
140
- // A fade, like the pickers a site puts beside this one: the default
141
- // grows the panel from a third of its size over about a second, which
142
- // reads as the row behaving differently control by control.
143
- animation="minimal"
144
- arrow={false}
145
- placement="bottom-start"
146
- isOpen={isOpen}
147
- content={content}
148
- onInteraction={(next) => {
149
- setOpen(next);
150
- if (!next) setEditing(false);
151
- }}
152
- >
153
- <Button
154
- alignText="start"
155
- aria-label={label === '' ? t('color.colourScale') : label}
156
- data-testid={testId}
157
- endIcon="caret-down"
158
- fill
159
- text={
160
- <span style={BUTTON_TEXT_STYLE}>
161
- <span>
162
- {resolved.id === null
163
- ? t('color.custom')
164
- : t.or(`color.scale.${resolved.id}.label`, resolved.label)}
165
- </span>
201
+ const scaleName = uniform
202
+ ? t('color.uniform')
203
+ : resolved.id === null
204
+ ? t('color.custom')
205
+ : t.or(`color.scale.${resolved.id}.label`, resolved.label);
206
+ const name = label === '' ? t('color.colourScale') : label;
207
+
208
+ const target =
209
+ appearance === 'bar' ? (
210
+ <OverlayValueButton
211
+ label={name}
212
+ value={scaleName}
213
+ swatches={
214
+ uniform ? [oneColor] : sampleScale(resolved.scale, BAR_SWATCHES)
215
+ }
216
+ active={isOpen}
217
+ opensMenu
218
+ testId={testId}
219
+ />
220
+ ) : (
221
+ <Button
222
+ alignText="start"
223
+ aria-label={name}
224
+ data-testid={testId}
225
+ endIcon="caret-down"
226
+ fill
227
+ text={
228
+ <span style={BUTTON_TEXT_STYLE}>
229
+ <span>{scaleName}</span>
230
+ {uniform ? (
231
+ <span
232
+ style={{
233
+ ...UNIFORM_BAR_STYLE,
234
+ width: BUTTON_BAR_WIDTH,
235
+ background: oneColor,
236
+ }}
237
+ />
238
+ ) : (
166
239
  <ColorScaleBar
167
240
  scale={resolved.scale}
168
241
  style={{ width: BUTTON_BAR_WIDTH }}
169
242
  />
170
- </span>
171
- }
172
- />
173
- </PopoverNext>
243
+ )}
244
+ </span>
245
+ }
246
+ />
247
+ );
248
+
249
+ const popover = (
250
+ <PopoverNext
251
+ // A fade, like the pickers a site puts beside this one: the default
252
+ // grows the panel from a third of its size over about a second, which
253
+ // reads as the row behaving differently control by control.
254
+ animation="minimal"
255
+ arrow={false}
256
+ placement="bottom-start"
257
+ isOpen={isOpen}
258
+ content={content}
259
+ onInteraction={(next) => {
260
+ setOpen(next);
261
+ if (!next) setEditing(false);
262
+ }}
263
+ >
264
+ {target}
265
+ </PopoverNext>
266
+ );
267
+
268
+ if (appearance === 'bar') return popover;
269
+
270
+ return (
271
+ <label className={className} style={FIELD_STYLE}>
272
+ {label === '' ? null : <span style={CAPTION_STYLE}>{label}</span>}
273
+ {popover}
174
274
  </label>
175
275
  );
176
276
  }
@@ -197,6 +297,30 @@ const BUTTON_TEXT_STYLE = {
197
297
  width: '100%',
198
298
  } as const satisfies CSSProperties;
199
299
 
300
+ // The lines of the menu, so one colour reads as one more choice among them.
301
+ const UNIFORM_ITEM_STYLE = {
302
+ display: 'grid',
303
+ gridTemplateColumns: '7rem 1fr',
304
+ alignItems: 'center',
305
+ gap: 10,
306
+ minWidth: 220,
307
+ } as const satisfies CSSProperties;
308
+
309
+ const UNIFORM_BAR_STYLE = {
310
+ display: 'inline-block',
311
+ height: 12,
312
+ borderRadius: 2,
313
+ } as const satisfies CSSProperties;
314
+
315
+ const UNIFORM_INPUT_STYLE = {
316
+ width: '100%',
317
+ height: 18,
318
+ padding: 0,
319
+ border: 0,
320
+ background: 'none',
321
+ cursor: 'pointer',
322
+ } as const satisfies CSSProperties;
323
+
200
324
  const MENU_STYLE = {
201
325
  maxHeight: '60vh',
202
326
  overflowY: 'auto',