react-cheminfo 0.50.0 → 0.52.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 (314) hide show
  1. package/README.md +71 -16
  2. package/bin/check-seo.mjs +0 -0
  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/host/core/connectHost.d.ts +75 -0
  79. package/lib/host/core/connectHost.d.ts.map +1 -0
  80. package/lib/host/core/connectHost.js +171 -0
  81. package/lib/host/core/connectHost.js.map +1 -0
  82. package/lib/host/core/hostStructure.d.ts +52 -0
  83. package/lib/host/core/hostStructure.d.ts.map +1 -0
  84. package/lib/host/core/hostStructure.js +106 -0
  85. package/lib/host/core/hostStructure.js.map +1 -0
  86. package/lib/host/core/index.d.ts +9 -0
  87. package/lib/host/core/index.d.ts.map +1 -0
  88. package/lib/host/core/index.js +5 -0
  89. package/lib/host/core/index.js.map +1 -0
  90. package/lib/host/core/kinds.d.ts +73 -0
  91. package/lib/host/core/kinds.d.ts.map +1 -0
  92. package/lib/host/core/kinds.js +88 -0
  93. package/lib/host/core/kinds.js.map +1 -0
  94. package/lib/host/core/protocol.d.ts +134 -0
  95. package/lib/host/core/protocol.d.ts.map +1 -0
  96. package/lib/host/core/protocol.js +92 -0
  97. package/lib/host/core/protocol.js.map +1 -0
  98. package/lib/host/testing/sciPeaks.d.ts +21 -0
  99. package/lib/host/testing/sciPeaks.d.ts.map +1 -0
  100. package/lib/host/testing/sciPeaks.js +62 -0
  101. package/lib/host/testing/sciPeaks.js.map +1 -0
  102. package/lib/host/testing/standInHost.d.ts +47 -0
  103. package/lib/host/testing/standInHost.d.ts.map +1 -0
  104. package/lib/host/testing/standInHost.js +85 -0
  105. package/lib/host/testing/standInHost.js.map +1 -0
  106. package/lib/host/ui/index.d.ts +3 -0
  107. package/lib/host/ui/index.d.ts.map +1 -0
  108. package/lib/host/ui/index.js +2 -0
  109. package/lib/host/ui/index.js.map +1 -0
  110. package/lib/host/ui/useHostConnection.d.ts +36 -0
  111. package/lib/host/ui/useHostConnection.d.ts.map +1 -0
  112. package/lib/host/ui/useHostConnection.js +62 -0
  113. package/lib/host/ui/useHostConnection.js.map +1 -0
  114. package/lib/host-testing.d.ts +8 -0
  115. package/lib/host-testing.d.ts.map +1 -0
  116. package/lib/host-testing.js +7 -0
  117. package/lib/host-testing.js.map +1 -0
  118. package/lib/host.d.ts +11 -0
  119. package/lib/host.d.ts.map +1 -0
  120. package/lib/host.js +11 -0
  121. package/lib/host.js.map +1 -0
  122. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  123. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  124. package/lib/locales/de.json +28 -2
  125. package/lib/locales/en.json +28 -2
  126. package/lib/locales/es.json +28 -2
  127. package/lib/locales/fr.json +28 -2
  128. package/lib/locales/it.json +28 -2
  129. package/lib/molecule3d/core/index.d.ts +1 -1
  130. package/lib/molecule3d/core/index.d.ts.map +1 -1
  131. package/lib/molecule3d/core/index.js +1 -1
  132. package/lib/molecule3d/core/index.js.map +1 -1
  133. package/lib/molecule3d/core/settings.d.ts +15 -0
  134. package/lib/molecule3d/core/settings.d.ts.map +1 -1
  135. package/lib/molecule3d/core/settings.js +25 -0
  136. package/lib/molecule3d/core/settings.js.map +1 -1
  137. package/lib/molecule3d/ui/Molecule3DExport.d.ts +3 -0
  138. package/lib/molecule3d/ui/Molecule3DExport.d.ts.map +1 -1
  139. package/lib/molecule3d/ui/Molecule3DExport.js +10 -16
  140. package/lib/molecule3d/ui/Molecule3DExport.js.map +1 -1
  141. package/lib/molecule3d/ui/Molecule3DOptions.d.ts +3 -1
  142. package/lib/molecule3d/ui/Molecule3DOptions.d.ts.map +1 -1
  143. package/lib/molecule3d/ui/Molecule3DOptions.js +22 -6
  144. package/lib/molecule3d/ui/Molecule3DOptions.js.map +1 -1
  145. package/lib/molecule3d/ui/MoleculeCanvas3D.d.ts.map +1 -1
  146. package/lib/molecule3d/ui/MoleculeCanvas3D.js +4 -3
  147. package/lib/molecule3d/ui/MoleculeCanvas3D.js.map +1 -1
  148. package/lib/molecule3d/ui/exportMoleculeImage.d.ts +9 -0
  149. package/lib/molecule3d/ui/exportMoleculeImage.d.ts.map +1 -1
  150. package/lib/molecule3d/ui/exportMoleculeImage.js +15 -0
  151. package/lib/molecule3d/ui/exportMoleculeImage.js.map +1 -1
  152. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts +2 -1
  153. package/lib/molecule3d/ui/moleculeViewer3DProps.d.ts.map +1 -1
  154. package/lib/molecule3d/ui/useImageExport.d.ts +7 -1
  155. package/lib/molecule3d/ui/useImageExport.d.ts.map +1 -1
  156. package/lib/molecule3d/ui/useImageExport.js +12 -3
  157. package/lib/molecule3d/ui/useImageExport.js.map +1 -1
  158. package/lib/overlay/ui/FigureBar.d.ts +41 -0
  159. package/lib/overlay/ui/FigureBar.d.ts.map +1 -0
  160. package/lib/overlay/ui/FigureBar.js +43 -0
  161. package/lib/overlay/ui/FigureBar.js.map +1 -0
  162. package/lib/overlay/ui/OverlayBar.d.ts +13 -4
  163. package/lib/overlay/ui/OverlayBar.d.ts.map +1 -1
  164. package/lib/overlay/ui/OverlayBar.js +2 -2
  165. package/lib/overlay/ui/OverlayBar.js.map +1 -1
  166. package/lib/overlay/ui/OverlayBarStrip.d.ts +2 -1
  167. package/lib/overlay/ui/OverlayBarStrip.d.ts.map +1 -1
  168. package/lib/overlay/ui/OverlayBarStrip.js +9 -5
  169. package/lib/overlay/ui/OverlayBarStrip.js.map +1 -1
  170. package/lib/overlay/ui/OverlayIconButton.d.ts +8 -2
  171. package/lib/overlay/ui/OverlayIconButton.d.ts.map +1 -1
  172. package/lib/overlay/ui/OverlayIconButton.js +2 -1
  173. package/lib/overlay/ui/OverlayIconButton.js.map +1 -1
  174. package/lib/overlay/ui/index.d.ts +4 -0
  175. package/lib/overlay/ui/index.d.ts.map +1 -1
  176. package/lib/overlay/ui/index.js +2 -0
  177. package/lib/overlay/ui/index.js.map +1 -1
  178. package/lib/overlay/ui/overlayBarShape.d.ts +18 -0
  179. package/lib/overlay/ui/overlayBarShape.d.ts.map +1 -1
  180. package/lib/overlay/ui/overlayStyles.d.ts +30 -0
  181. package/lib/overlay/ui/overlayStyles.d.ts.map +1 -1
  182. package/lib/overlay/ui/overlayStyles.js +52 -0
  183. package/lib/overlay/ui/overlayStyles.js.map +1 -1
  184. package/lib/overlay/ui/useFigureBarAwake.d.ts +12 -0
  185. package/lib/overlay/ui/useFigureBarAwake.d.ts.map +1 -0
  186. package/lib/overlay/ui/useFigureBarAwake.js +51 -0
  187. package/lib/overlay/ui/useFigureBarAwake.js.map +1 -0
  188. package/lib/periodic/core/elements.d.ts +6 -3
  189. package/lib/periodic/core/elements.d.ts.map +1 -1
  190. package/lib/periodic/core/elements.js +9 -3
  191. package/lib/periodic/core/elements.js.map +1 -1
  192. package/lib/periodic/core/index.d.ts +2 -2
  193. package/lib/periodic/core/index.d.ts.map +1 -1
  194. package/lib/periodic/core/index.js +1 -1
  195. package/lib/periodic/core/index.js.map +1 -1
  196. package/lib/periodic/core/layout.d.ts +33 -3
  197. package/lib/periodic/core/layout.d.ts.map +1 -1
  198. package/lib/periodic/core/layout.js +14 -0
  199. package/lib/periodic/core/layout.js.map +1 -1
  200. package/lib/periodic/ui/BlockStrip.d.ts +20 -0
  201. package/lib/periodic/ui/BlockStrip.d.ts.map +1 -0
  202. package/lib/periodic/ui/BlockStrip.js +48 -0
  203. package/lib/periodic/ui/BlockStrip.js.map +1 -0
  204. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  205. package/lib/periodic/ui/ElementCell.js +9 -8
  206. package/lib/periodic/ui/ElementCell.js.map +1 -1
  207. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  208. package/lib/periodic/ui/PeriodicTable.js +3 -1
  209. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  210. package/lib/periodic/ui/PeriodicTableHelp.d.ts +33 -0
  211. package/lib/periodic/ui/PeriodicTableHelp.d.ts.map +1 -0
  212. package/lib/periodic/ui/PeriodicTableHelp.js +107 -0
  213. package/lib/periodic/ui/PeriodicTableHelp.js.map +1 -0
  214. package/lib/periodic/ui/cellStyles.d.ts +3 -2
  215. package/lib/periodic/ui/cellStyles.d.ts.map +1 -1
  216. package/lib/periodic/ui/cellStyles.js +3 -2
  217. package/lib/periodic/ui/cellStyles.js.map +1 -1
  218. package/lib/periodic/ui/index.d.ts +2 -0
  219. package/lib/periodic/ui/index.d.ts.map +1 -1
  220. package/lib/periodic/ui/index.js +1 -0
  221. package/lib/periodic/ui/index.js.map +1 -1
  222. package/lib/periodic/ui/periodicTableProps.d.ts +5 -4
  223. package/lib/periodic/ui/periodicTableProps.d.ts.map +1 -1
  224. package/lib/periodic/ui/tableLayout.d.ts +3 -1
  225. package/lib/periodic/ui/tableLayout.d.ts.map +1 -1
  226. package/lib/periodic/ui/tableLayout.js +8 -3
  227. package/lib/periodic/ui/tableLayout.js.map +1 -1
  228. package/lib/share/ui/ShareDialogContent.d.ts +3 -2
  229. package/lib/share/ui/ShareDialogContent.d.ts.map +1 -1
  230. package/lib/share/ui/ShareDialogContent.js +5 -6
  231. package/lib/share/ui/ShareDialogContent.js.map +1 -1
  232. package/lib/share/ui/ShareLinkBar.d.ts +8 -2
  233. package/lib/share/ui/ShareLinkBar.d.ts.map +1 -1
  234. package/lib/share/ui/ShareLinkBar.js +6 -5
  235. package/lib/share/ui/ShareLinkBar.js.map +1 -1
  236. package/lib/share/ui/SharePreview.d.ts.map +1 -1
  237. package/lib/share/ui/SharePreview.js +2 -2
  238. package/lib/share/ui/SharePreview.js.map +1 -1
  239. package/lib/structure/ui/StructurePicturePane.js +3 -3
  240. package/lib/structure/ui/StructurePicturePane.js.map +1 -1
  241. package/package.json +4 -1
  242. package/src/clipboard/core/index.ts +0 -1
  243. package/src/clipboard/ui/CopyButton.tsx +12 -1
  244. package/src/color/core/index.ts +9 -0
  245. package/src/color/core/interpolate.ts +51 -22
  246. package/src/color/core/stops.ts +98 -0
  247. package/src/color/core/tone.ts +79 -0
  248. package/src/color/core/uniform.ts +67 -0
  249. package/src/color/ui/ColorScaleEditor.tsx +115 -141
  250. package/src/color/ui/ColorScaleSelect.tsx +160 -36
  251. package/src/color/ui/ColorScaleTone.tsx +103 -0
  252. package/src/color/ui/ColorScaleTrack.tsx +239 -0
  253. package/src/download/core/copyFigure.ts +20 -0
  254. package/src/download/core/figureHtml.ts +1 -0
  255. package/src/download/core/figureHtmlMarkup.ts +14 -1
  256. package/src/download/core/index.ts +2 -0
  257. package/src/download/ui/FigureDownloadPanel.tsx +8 -11
  258. package/src/download/ui/figureDownloadHint.ts +1 -2
  259. package/src/download/ui/index.ts +0 -1
  260. package/src/download/ui/useFigureActions.ts +6 -42
  261. package/src/download/ui/useFigureTask.ts +46 -0
  262. package/src/host/core/connectHost.ts +240 -0
  263. package/src/host/core/hostStructure.ts +129 -0
  264. package/src/host/core/index.ts +46 -0
  265. package/src/host/core/kinds.ts +128 -0
  266. package/src/host/core/protocol.ts +197 -0
  267. package/src/host/testing/sciPeaks.ts +66 -0
  268. package/src/host/testing/standInHost.ts +116 -0
  269. package/src/host/ui/index.ts +2 -0
  270. package/src/host/ui/useHostConnection.ts +85 -0
  271. package/src/host-testing.ts +18 -0
  272. package/src/host.ts +11 -0
  273. package/src/locales/de.json +28 -2
  274. package/src/locales/en.json +28 -2
  275. package/src/locales/es.json +28 -2
  276. package/src/locales/fr.json +28 -2
  277. package/src/locales/it.json +28 -2
  278. package/src/molecule3d/core/index.ts +2 -0
  279. package/src/molecule3d/core/settings.ts +35 -0
  280. package/src/molecule3d/ui/Molecule3DExport.tsx +18 -17
  281. package/src/molecule3d/ui/Molecule3DOptions.tsx +51 -3
  282. package/src/molecule3d/ui/MoleculeCanvas3D.tsx +11 -3
  283. package/src/molecule3d/ui/exportMoleculeImage.ts +20 -0
  284. package/src/molecule3d/ui/moleculeViewer3DProps.ts +2 -1
  285. package/src/molecule3d/ui/useImageExport.ts +25 -3
  286. package/src/overlay/ui/FigureBar.tsx +96 -0
  287. package/src/overlay/ui/OverlayBar.tsx +15 -5
  288. package/src/overlay/ui/OverlayBarStrip.tsx +34 -22
  289. package/src/overlay/ui/OverlayIconButton.tsx +10 -2
  290. package/src/overlay/ui/index.ts +4 -0
  291. package/src/overlay/ui/overlayBarShape.ts +19 -0
  292. package/src/overlay/ui/overlayStyles.ts +61 -0
  293. package/src/overlay/ui/useFigureBarAwake.ts +56 -0
  294. package/src/periodic/core/elements.ts +8 -3
  295. package/src/periodic/core/index.ts +8 -1
  296. package/src/periodic/core/layout.ts +45 -3
  297. package/src/periodic/ui/BlockStrip.tsx +81 -0
  298. package/src/periodic/ui/ElementCell.tsx +9 -8
  299. package/src/periodic/ui/PeriodicTable.tsx +4 -1
  300. package/src/periodic/ui/PeriodicTableHelp.tsx +205 -0
  301. package/src/periodic/ui/cellStyles.ts +3 -2
  302. package/src/periodic/ui/index.ts +5 -0
  303. package/src/periodic/ui/periodicTableProps.ts +5 -4
  304. package/src/periodic/ui/tableLayout.ts +9 -3
  305. package/src/share/ui/ShareDialogContent.tsx +9 -13
  306. package/src/share/ui/ShareLinkBar.tsx +16 -4
  307. package/src/share/ui/SharePreview.tsx +6 -2
  308. package/src/structure/ui/StructurePicturePane.tsx +3 -3
  309. package/styles/chrome.css +43 -32
  310. package/lib/clipboard/core/copyPng.d.ts +0 -13
  311. package/lib/clipboard/core/copyPng.d.ts.map +0 -1
  312. package/lib/clipboard/core/copyPng.js +0 -19
  313. package/lib/clipboard/core/copyPng.js.map +0 -1
  314. package/src/clipboard/core/copyPng.ts +0 -20
@@ -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',
@@ -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';