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
@@ -0,0 +1,79 @@
1
+ import { clamp } from '../../format/core/clamp.ts';
2
+
3
+ import { parseHexColor, toHexColor } from './hex.ts';
4
+ import type { HsvColor } from './hsv.ts';
5
+ import { hsvToRgb, rgbToHsv } from './hsv.ts';
6
+ import type { ColorStop } from './interpolate.ts';
7
+
8
+ /** How strong and how bright a scale's colours are, taken over all its anchors. */
9
+ export interface ScaleTone {
10
+ /** How far the anchors are from grey, from 0 to 1. */
11
+ saturation: number;
12
+ /** How bright they are, from 0 for black to 1. */
13
+ value: number;
14
+ }
15
+
16
+ /**
17
+ * The saturation and the brightness of a scale's anchors, averaged: what a
18
+ * control setting both for the whole scale opens on.
19
+ * @param stops - The anchors.
20
+ * @returns Their mean saturation and mean value; both 0 when there is no anchor.
21
+ */
22
+ export function scaleTone(stops: readonly ColorStop[]): ScaleTone {
23
+ const colors = toHsv(stops);
24
+ if (colors.length === 0) return { saturation: 0, value: 0 };
25
+ let saturation = 0;
26
+ let value = 0;
27
+ for (const color of colors) {
28
+ saturation += color.saturation;
29
+ value += color.value;
30
+ }
31
+ return {
32
+ saturation: saturation / colors.length,
33
+ value: value / colors.length,
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Give every anchor the same saturation, the same brightness, or both, keeping
39
+ * each one's hue — so the scale then only turns round the wheel. A grey anchor
40
+ * has no hue of its own and takes the hue of the nearest one that has.
41
+ * @param stops - The anchors.
42
+ * @param tone - What to set; a field left out keeps each anchor's own.
43
+ * @returns The recoloured anchors, in the same order.
44
+ */
45
+ export function setScaleTone(
46
+ stops: readonly ColorStop[],
47
+ tone: Partial<ScaleTone>,
48
+ ): ColorStop[] {
49
+ const colors = toHsv(stops);
50
+ const result: ColorStop[] = [];
51
+ for (let index = 0; index < stops.length; index++) {
52
+ const stop = stops[index];
53
+ const color = colors[index];
54
+ if (stop === undefined || color === undefined) continue;
55
+ const toned = hsvToRgb({
56
+ hue: hueNear(colors, index),
57
+ saturation: clamp(tone.saturation ?? color.saturation, 0, 1),
58
+ value: clamp(tone.value ?? color.value, 0, 1),
59
+ });
60
+ result.push({ ...stop, color: toHexColor(toned) });
61
+ }
62
+ return result;
63
+ }
64
+
65
+ function toHsv(stops: readonly ColorStop[]): HsvColor[] {
66
+ const colors: HsvColor[] = [];
67
+ for (const stop of stops) colors.push(rgbToHsv(parseHexColor(stop.color)));
68
+ return colors;
69
+ }
70
+
71
+ function hueNear(colors: readonly HsvColor[], index: number): number {
72
+ for (let distance = 0; distance < colors.length; distance++) {
73
+ const before = colors[index - distance];
74
+ if (before !== undefined && before.saturation > 0) return before.hue;
75
+ const after = colors[index + distance];
76
+ if (after !== undefined && after.saturation > 0) return after.hue;
77
+ }
78
+ return 0;
79
+ }
@@ -0,0 +1,67 @@
1
+ import { TOKEN } from '../../tokens/core/familyTokens.ts';
2
+
3
+ import { readableInk } from './contrast.ts';
4
+ import { normalizeHexColor } from './hex.ts';
5
+ import type { Swatch } from './interpolate.ts';
6
+
7
+ /**
8
+ * What a colour-scale picker offering one colour writes for it, and a link
9
+ * carries: every value the same colour, so the figure needs no key. Followed by
10
+ * `-` and a hex colour, e.g. `uniform-1c6e42`, it names that colour.
11
+ */
12
+ export const UNIFORM_COLOR_SCALE_ID = 'uniform';
13
+
14
+ /**
15
+ * The one colour of a uniform scale that names none: the family's border grey
16
+ * rather than white, so a cell still stands out from a white page and from the
17
+ * white ground of a saved figure.
18
+ */
19
+ export const UNIFORM_SWATCH: Swatch = {
20
+ background: TOKEN.border,
21
+ foreground: TOKEN.text,
22
+ };
23
+
24
+ const PREFIX = `${UNIFORM_COLOR_SCALE_ID}-`;
25
+
26
+ /**
27
+ * Whether a scale text asks for one colour.
28
+ * @param text - A scale id or a custom scale, as a link carries it.
29
+ * @returns `true` for {@link UNIFORM_COLOR_SCALE_ID}, with or without a colour.
30
+ */
31
+ export function isUniformColorScale(text: string | undefined): boolean {
32
+ const wanted = (text ?? '').trim().toLowerCase();
33
+ return wanted === UNIFORM_COLOR_SCALE_ID || wanted.startsWith(PREFIX);
34
+ }
35
+
36
+ /**
37
+ * The colour a uniform scale text names, and the ink to write on it.
38
+ *
39
+ * A colour with a typo in it falls back to the grey rather than failing, so a
40
+ * link written by hand still opens the figure.
41
+ * @param text - A scale text, e.g. `uniform` or `uniform-1c6e42`.
42
+ * @returns The swatch it names, or {@link UNIFORM_SWATCH}.
43
+ */
44
+ export function uniformSwatch(text: string | undefined): Swatch {
45
+ const color = uniformColor(text);
46
+ if (color === null) return UNIFORM_SWATCH;
47
+ return { background: color, foreground: readableInk(color) };
48
+ }
49
+
50
+ /**
51
+ * Write one colour as the scale text a link carries; the `#` is dropped, since
52
+ * a query string is cut in two at the first one.
53
+ * @param color - The colour, as `#rgb` or `#rrggbb`.
54
+ * @returns E.g. `uniform-1c6e42`, or {@link UNIFORM_COLOR_SCALE_ID} when the colour is not a hex colour.
55
+ */
56
+ export function formatUniformColorScale(color: string): string {
57
+ const normalized = normalizeHexColor(color);
58
+ return normalized === null
59
+ ? UNIFORM_COLOR_SCALE_ID
60
+ : `${PREFIX}${normalized.slice(1)}`;
61
+ }
62
+
63
+ function uniformColor(text: string | undefined): string | null {
64
+ const wanted = (text ?? '').trim().toLowerCase();
65
+ if (!wanted.startsWith(PREFIX)) return null;
66
+ return normalizeHexColor(`#${wanted.slice(PREFIX.length)}`);
67
+ }
@@ -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;