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