react-cheminfo 0.29.0 → 0.31.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 +62 -2
- package/lib/chrome/ui/AccountButton.d.ts +76 -0
- package/lib/chrome/ui/AccountButton.d.ts.map +1 -0
- package/lib/chrome/ui/AccountButton.js +66 -0
- package/lib/chrome/ui/AccountButton.js.map +1 -0
- package/lib/chrome/ui/index.d.ts +2 -0
- package/lib/chrome/ui/index.d.ts.map +1 -1
- package/lib/chrome/ui/index.js +1 -0
- package/lib/chrome/ui/index.js.map +1 -1
- package/lib/clipboard/ui/ClickToCopy.d.ts +6 -0
- package/lib/clipboard/ui/ClickToCopy.d.ts.map +1 -1
- package/lib/clipboard/ui/ClickToCopy.js +3 -3
- package/lib/clipboard/ui/ClickToCopy.js.map +1 -1
- package/lib/download/core/downloadFigure.d.ts +7 -5
- package/lib/download/core/downloadFigure.d.ts.map +1 -1
- package/lib/download/core/downloadFigure.js +7 -6
- package/lib/download/core/downloadFigure.js.map +1 -1
- package/lib/download/core/figureLayout.d.ts +55 -0
- package/lib/download/core/figureLayout.d.ts.map +1 -0
- package/lib/download/core/figureLayout.js +89 -0
- package/lib/download/core/figureLayout.js.map +1 -0
- package/lib/download/core/figureScale.d.ts +9 -0
- package/lib/download/core/figureScale.d.ts.map +1 -1
- package/lib/download/core/figureScale.js +10 -0
- package/lib/download/core/figureScale.js.map +1 -1
- package/lib/download/core/figureSettle.d.ts +51 -0
- package/lib/download/core/figureSettle.d.ts.map +1 -0
- package/lib/download/core/figureSettle.js +81 -0
- package/lib/download/core/figureSettle.js.map +1 -0
- package/lib/download/core/figureSvg.d.ts +7 -1
- package/lib/download/core/figureSvg.d.ts.map +1 -1
- package/lib/download/core/figureSvg.js +1 -0
- package/lib/download/core/figureSvg.js.map +1 -1
- package/lib/download/core/figureSvgDocument.d.ts +7 -0
- package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
- package/lib/download/core/figureSvgDocument.js +11 -2
- package/lib/download/core/figureSvgDocument.js.map +1 -1
- package/lib/download/core/index.d.ts +5 -1
- package/lib/download/core/index.d.ts.map +1 -1
- package/lib/download/core/index.js +3 -1
- package/lib/download/core/index.js.map +1 -1
- package/lib/download/ui/FigureDownload.d.ts +18 -4
- package/lib/download/ui/FigureDownload.d.ts.map +1 -1
- package/lib/download/ui/FigureDownload.js +35 -9
- package/lib/download/ui/FigureDownload.js.map +1 -1
- package/lib/download/ui/FigureDownloadPanel.d.ts +17 -7
- package/lib/download/ui/FigureDownloadPanel.d.ts.map +1 -1
- package/lib/download/ui/FigureDownloadPanel.js +30 -22
- package/lib/download/ui/FigureDownloadPanel.js.map +1 -1
- package/lib/download/ui/FigureSizeField.d.ts +27 -0
- package/lib/download/ui/FigureSizeField.d.ts.map +1 -0
- package/lib/download/ui/FigureSizeField.js +87 -0
- package/lib/download/ui/FigureSizeField.js.map +1 -0
- package/lib/download/ui/FigureSizeRows.d.ts +34 -0
- package/lib/download/ui/FigureSizeRows.d.ts.map +1 -0
- package/lib/download/ui/FigureSizeRows.js +54 -0
- package/lib/download/ui/FigureSizeRows.js.map +1 -0
- package/lib/download/ui/index.d.ts +2 -0
- package/lib/download/ui/index.d.ts.map +1 -1
- package/lib/download/ui/index.js.map +1 -1
- package/lib/download/ui/useFigureRedraw.d.ts +27 -0
- package/lib/download/ui/useFigureRedraw.d.ts.map +1 -0
- package/lib/download/ui/useFigureRedraw.js +78 -0
- package/lib/download/ui/useFigureRedraw.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 +13 -2
- package/lib/locales/en.json +13 -2
- package/lib/locales/es.json +13 -2
- package/lib/locales/fr.json +13 -2
- package/lib/overlay/ui/OverlayValueMenu.d.ts.map +1 -1
- package/lib/overlay/ui/OverlayValueMenu.js +7 -1
- package/lib/overlay/ui/OverlayValueMenu.js.map +1 -1
- package/lib/structure/core/conformerBoltzmann.d.ts +56 -0
- package/lib/structure/core/conformerBoltzmann.d.ts.map +1 -0
- package/lib/structure/core/conformerBoltzmann.js +96 -0
- package/lib/structure/core/conformerBoltzmann.js.map +1 -0
- package/lib/structure/core/conformerEnergy.d.ts +69 -0
- package/lib/structure/core/conformerEnergy.d.ts.map +1 -0
- package/lib/structure/core/conformerEnergy.js +47 -0
- package/lib/structure/core/conformerEnergy.js.map +1 -0
- package/lib/structure/core/index.d.ts +3 -0
- package/lib/structure/core/index.d.ts.map +1 -1
- package/lib/structure/core/index.js +2 -0
- package/lib/structure/core/index.js.map +1 -1
- package/lib/structure/ui/ConformerTable.d.ts +73 -0
- package/lib/structure/ui/ConformerTable.d.ts.map +1 -0
- package/lib/structure/ui/ConformerTable.js +259 -0
- package/lib/structure/ui/ConformerTable.js.map +1 -0
- package/lib/structure/ui/conformerColumns.d.ts +30 -0
- package/lib/structure/ui/conformerColumns.d.ts.map +1 -0
- package/lib/structure/ui/conformerColumns.js +37 -0
- package/lib/structure/ui/conformerColumns.js.map +1 -0
- package/lib/ui.d.ts +4 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +2 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/chrome/ui/AccountButton.tsx +185 -0
- package/src/chrome/ui/index.ts +2 -0
- package/src/clipboard/ui/ClickToCopy.tsx +9 -1
- package/src/download/core/downloadFigure.ts +12 -9
- package/src/download/core/figureLayout.ts +114 -0
- package/src/download/core/figureScale.ts +13 -0
- package/src/download/core/figureSettle.ts +112 -0
- package/src/download/core/figureSvg.ts +8 -1
- package/src/download/core/figureSvgDocument.ts +19 -2
- package/src/download/core/index.ts +13 -0
- package/src/download/ui/FigureDownload.tsx +88 -35
- package/src/download/ui/FigureDownloadPanel.tsx +45 -22
- package/src/download/ui/FigureSizeField.tsx +153 -0
- package/src/download/ui/FigureSizeRows.tsx +108 -0
- package/src/download/ui/index.ts +2 -0
- package/src/download/ui/useFigureRedraw.tsx +115 -0
- package/src/locales/de.json +13 -2
- package/src/locales/en.json +13 -2
- package/src/locales/es.json +13 -2
- package/src/locales/fr.json +13 -2
- package/src/overlay/ui/OverlayValueMenu.tsx +7 -1
- package/src/structure/core/conformerBoltzmann.ts +102 -0
- package/src/structure/core/conformerEnergy.ts +97 -0
- package/src/structure/core/index.ts +15 -0
- package/src/structure/ui/ConformerTable.tsx +429 -0
- package/src/structure/ui/conformerColumns.ts +55 -0
- package/src/ui.ts +12 -0
- package/styles/chrome.css +55 -0
|
@@ -8,6 +8,7 @@ import { OverlayPanel } from '../../overlay/ui/OverlayPanel.tsx';
|
|
|
8
8
|
import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
|
|
9
9
|
import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
|
|
10
10
|
import type { FigureFormat } from '../core/downloadFigure.ts';
|
|
11
|
+
import { figureLayoutSize } from '../core/figureLayout.ts';
|
|
11
12
|
import type { FigurePixels } from '../core/figureScale.ts';
|
|
12
13
|
import {
|
|
13
14
|
figurePixels,
|
|
@@ -16,6 +17,9 @@ import {
|
|
|
16
17
|
formatFigurePixels,
|
|
17
18
|
} from '../core/figureScale.ts';
|
|
18
19
|
|
|
20
|
+
import type { FigureSizing } from './FigureSizeRows.tsx';
|
|
21
|
+
import { FigureSizeRows } from './FigureSizeRows.tsx';
|
|
22
|
+
|
|
19
23
|
/** What {@link FigureDownloadPanel} is drawn from. */
|
|
20
24
|
export interface FigureDownloadPanelProps {
|
|
21
25
|
/** What the panel is called. */
|
|
@@ -40,21 +44,30 @@ export interface FigureDownloadPanelProps {
|
|
|
40
44
|
onSave: () => void;
|
|
41
45
|
/**
|
|
42
46
|
* Whether the SVG holds a rendered picture rather than vector drawings, as
|
|
43
|
-
* for a WebGL scene: its resolution then
|
|
47
|
+
* for a WebGL scene: its resolution is then the pixels of that picture, it
|
|
48
|
+
* meets the same ceiling as a PNG, and the hint says so.
|
|
44
49
|
* @default false
|
|
45
50
|
*/
|
|
46
51
|
rasterSvg?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The shape the figure is drawn at, for a figure that can be drawn again at
|
|
54
|
+
* another size. Without it the figure is saved at the size it has on screen,
|
|
55
|
+
* and no size is offered.
|
|
56
|
+
* @default undefined
|
|
57
|
+
*/
|
|
58
|
+
sizing?: FigureSizing;
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/**
|
|
50
|
-
* The
|
|
62
|
+
* The choices behind the save glyph, and the button that writes the file.
|
|
51
63
|
*
|
|
52
|
-
* The resolution
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
64
|
+
* The resolution applies to both formats, and means what the reader needs it
|
|
65
|
+
* to: a PNG is painted with that many more pixels, and an SVG, drawn the same,
|
|
66
|
+
* opens that many times larger in the slide or the document it is dropped
|
|
67
|
+
* into. Only a PNG meets the ceiling of what a browser can paint, so only its
|
|
68
|
+
* choices past that ceiling are greyed.
|
|
56
69
|
*
|
|
57
|
-
*
|
|
70
|
+
* The sentence at the foot is the panel's own hint line rather than a row of its own: it
|
|
58
71
|
* is not a setting, it is what the two settings above it currently amount to,
|
|
59
72
|
* and it is the only thing here that answers the question a reader actually
|
|
60
73
|
* has, which is how big the file is going to be. Saving itself is a command
|
|
@@ -68,14 +81,21 @@ export function FigureDownloadPanel(
|
|
|
68
81
|
): ReactElement {
|
|
69
82
|
const { title, format, scale, scales, size, failure, saving } = props;
|
|
70
83
|
const { onFormatChange, onScaleChange, onSave, rasterSvg = false } = props;
|
|
84
|
+
const { sizing } = props;
|
|
71
85
|
const t = useChromeT();
|
|
72
86
|
|
|
73
|
-
|
|
87
|
+
// What the file is drawn at: the figure on screen, or the shape picked.
|
|
88
|
+
const drawn =
|
|
89
|
+
size === null || sizing === undefined
|
|
90
|
+
? size
|
|
91
|
+
: figureLayoutSize(sizing.layout, size, sizing.custom ?? size);
|
|
92
|
+
|
|
93
|
+
const painted = format === 'png' || rasterSvg;
|
|
74
94
|
|
|
75
95
|
return (
|
|
76
96
|
<OverlayPanel
|
|
77
97
|
title={title}
|
|
78
|
-
hint={hintOf(format,
|
|
98
|
+
hint={hintOf(format, drawn, scale, failure, rasterSvg, t)}
|
|
79
99
|
actions={
|
|
80
100
|
<OverlayAction
|
|
81
101
|
text={t('download.save')}
|
|
@@ -96,6 +116,9 @@ export function FigureDownloadPanel(
|
|
|
96
116
|
options={FORMAT_CHOICES}
|
|
97
117
|
onChange={onFormatChange}
|
|
98
118
|
/>
|
|
119
|
+
{sizing === undefined ? null : (
|
|
120
|
+
<FigureSizeRows sizing={sizing} size={size} />
|
|
121
|
+
)}
|
|
99
122
|
<OverlaySegmented
|
|
100
123
|
label={t('download.resolution')}
|
|
101
124
|
help={{
|
|
@@ -107,8 +130,7 @@ export function FigureDownloadPanel(
|
|
|
107
130
|
},
|
|
108
131
|
}}
|
|
109
132
|
value={String(scale)}
|
|
110
|
-
options={scaleChoices(scales,
|
|
111
|
-
disabled={vector}
|
|
133
|
+
options={scaleChoices(scales, drawn, painted)}
|
|
112
134
|
onChange={(picked) => onScaleChange(Number(picked))}
|
|
113
135
|
/>
|
|
114
136
|
</OverlayPanel>
|
|
@@ -125,14 +147,17 @@ const FORMAT_CHOICES: ReadonlyArray<OverlayOption<FigureFormat>> = [
|
|
|
125
147
|
* The multiples offered, each saying what it would actually produce.
|
|
126
148
|
*
|
|
127
149
|
* One too large for the browser to paint is greyed rather than dropped, so the
|
|
128
|
-
* reader learns the ceiling exists before they meet it as a blank file.
|
|
150
|
+
* reader learns the ceiling exists before they meet it as a blank file. An SVG
|
|
151
|
+
* is not painted, so it has no such ceiling.
|
|
129
152
|
* @param scales - The multiples the caller offers.
|
|
130
|
-
* @param size - How big the figure is
|
|
153
|
+
* @param size - How big the figure is drawn for the file.
|
|
154
|
+
* @param painted - Whether the file is painted into pixels.
|
|
131
155
|
* @returns The choices, in the order offered.
|
|
132
156
|
*/
|
|
133
157
|
function scaleChoices(
|
|
134
158
|
scales: readonly number[],
|
|
135
159
|
size: FigurePixels | null,
|
|
160
|
+
painted: boolean,
|
|
136
161
|
): readonly OverlayOption[] {
|
|
137
162
|
const choices: OverlayOption[] = [];
|
|
138
163
|
for (const scale of scales) {
|
|
@@ -141,7 +166,7 @@ function scaleChoices(
|
|
|
141
166
|
value: String(scale),
|
|
142
167
|
label: figureScaleLabel(scale),
|
|
143
168
|
title: pixels === null ? undefined : formatFigurePixels(pixels),
|
|
144
|
-
disabled: size !== null && !figureScaleFits(size, scale),
|
|
169
|
+
disabled: painted && size !== null && !figureScaleFits(size, scale),
|
|
145
170
|
});
|
|
146
171
|
}
|
|
147
172
|
return choices;
|
|
@@ -150,8 +175,8 @@ function scaleChoices(
|
|
|
150
175
|
/**
|
|
151
176
|
* The line at the foot of the panel: what pressing save is about to do.
|
|
152
177
|
* @param format - Which file the reader is about to write.
|
|
153
|
-
* @param size - How big the figure is
|
|
154
|
-
* @param scale - The multiple it is
|
|
178
|
+
* @param size - How big the figure is drawn for the file.
|
|
179
|
+
* @param scale - The multiple it is saved at.
|
|
155
180
|
* @param failure - What went wrong last time, if anything.
|
|
156
181
|
* @param rasterSvg - Whether the SVG embeds a rendered picture.
|
|
157
182
|
* @param t - The chrome's formatter, so the sentence is in the language of
|
|
@@ -173,10 +198,8 @@ function hintOf(
|
|
|
173
198
|
pixels: formatFigurePixels(figurePixels(size, scale)),
|
|
174
199
|
});
|
|
175
200
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
pixels: formatFigurePixels(figurePixels(size, scale)),
|
|
181
|
-
});
|
|
201
|
+
const pixels = formatFigurePixels(figurePixels(size, scale));
|
|
202
|
+
return format === 'svg'
|
|
203
|
+
? t('download.hintSvg', { pixels })
|
|
204
|
+
: t('download.hintPng', { pixels });
|
|
182
205
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { CSSProperties, KeyboardEvent, ReactElement } from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { OverlayMetrics } from '../../overlay/core/overlayMetrics.ts';
|
|
5
|
+
import { OverlayRow } from '../../overlay/ui/OverlayRow.tsx';
|
|
6
|
+
import { useOverlaySurface } from '../../overlay/ui/overlaySurface.ts';
|
|
7
|
+
import { overlayFocusRing } from '../../overlay/ui/overlayValueStyles.ts';
|
|
8
|
+
import { figureLayoutSide } from '../core/figureLayout.ts';
|
|
9
|
+
import type { FigurePixels } from '../core/figureScale.ts';
|
|
10
|
+
|
|
11
|
+
/** What {@link FigureSizeField} needs. */
|
|
12
|
+
export interface FigureSizeFieldProps {
|
|
13
|
+
/** What the row is called. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** What the first box is called, for a screen reader. */
|
|
16
|
+
widthLabel: string;
|
|
17
|
+
/** What the second box is called. */
|
|
18
|
+
heightLabel: string;
|
|
19
|
+
/** The size typed so far. */
|
|
20
|
+
value: FigurePixels;
|
|
21
|
+
/** Called with the size, each side already held to the range. */
|
|
22
|
+
onChange: (size: FigurePixels) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The two sides of a figure, typed in pixels.
|
|
27
|
+
*
|
|
28
|
+
* These are typed rather than stepped: a reader asking for 1920 × 1080 knows
|
|
29
|
+
* the numbers, and a stepper would take a hundred presses to reach them. A side
|
|
30
|
+
* is taken when the reader leaves its box or presses Enter, never on each key,
|
|
31
|
+
* since the `1` of a `1200` on its way in is not a width anybody asked for.
|
|
32
|
+
* @param props - See {@link FigureSizeFieldProps}.
|
|
33
|
+
* @returns The row.
|
|
34
|
+
*/
|
|
35
|
+
export function FigureSizeField(props: FigureSizeFieldProps): ReactElement {
|
|
36
|
+
const { label, widthLabel, heightLabel, value, onChange } = props;
|
|
37
|
+
const { metrics } = useOverlaySurface();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<OverlayRow label={label}>
|
|
41
|
+
<span style={{ ...FIELD_STYLE, fontSize: metrics.fontSize }}>
|
|
42
|
+
<SideInput
|
|
43
|
+
label={widthLabel}
|
|
44
|
+
value={value.width}
|
|
45
|
+
metrics={metrics}
|
|
46
|
+
onCommit={(width) => onChange({ ...value, width })}
|
|
47
|
+
/>
|
|
48
|
+
<span aria-hidden="true">×</span>
|
|
49
|
+
<SideInput
|
|
50
|
+
label={heightLabel}
|
|
51
|
+
value={value.height}
|
|
52
|
+
metrics={metrics}
|
|
53
|
+
onCommit={(height) => onChange({ ...value, height })}
|
|
54
|
+
/>
|
|
55
|
+
<span style={UNIT_STYLE}>px</span>
|
|
56
|
+
</span>
|
|
57
|
+
</OverlayRow>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface SideInputProps {
|
|
62
|
+
/** What the box is called. */
|
|
63
|
+
label: string;
|
|
64
|
+
/** The side as last taken. */
|
|
65
|
+
value: number;
|
|
66
|
+
/** The measurements the panel is drawn from. */
|
|
67
|
+
metrics: OverlayMetrics;
|
|
68
|
+
/** Called with the side once it is taken. */
|
|
69
|
+
onCommit: (side: number) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One side, typed.
|
|
74
|
+
* @param props - See {@link SideInputProps}.
|
|
75
|
+
* @returns The box.
|
|
76
|
+
*/
|
|
77
|
+
function SideInput(props: SideInputProps): ReactElement {
|
|
78
|
+
const { label, value, metrics, onCommit } = props;
|
|
79
|
+
const [draft, setDraft] = useState(String(value));
|
|
80
|
+
const [taken, setTaken] = useState(value);
|
|
81
|
+
const [focused, setFocused] = useState(false);
|
|
82
|
+
|
|
83
|
+
// A side changed from outside — another shape picked, a clamp — replaces
|
|
84
|
+
// whatever was being typed.
|
|
85
|
+
if (taken !== value) {
|
|
86
|
+
setTaken(value);
|
|
87
|
+
setDraft(String(value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function commit(): void {
|
|
91
|
+
const typed = Number.parseFloat(draft);
|
|
92
|
+
const side = Number.isFinite(typed) ? figureLayoutSide(typed) : value;
|
|
93
|
+
setDraft(String(side));
|
|
94
|
+
if (side !== value) onCommit(side);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onKeyDown(event: KeyboardEvent<HTMLInputElement>): void {
|
|
98
|
+
if (event.key !== 'Enter') return;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
commit();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
inputMode="numeric"
|
|
107
|
+
aria-label={label}
|
|
108
|
+
value={draft}
|
|
109
|
+
style={{ ...sideStyle(metrics), ...overlayFocusRing(focused) }}
|
|
110
|
+
onChange={(event) => setDraft(event.currentTarget.value)}
|
|
111
|
+
onFocus={() => setFocused(true)}
|
|
112
|
+
onBlur={() => {
|
|
113
|
+
setFocused(false);
|
|
114
|
+
commit();
|
|
115
|
+
}}
|
|
116
|
+
onKeyDown={onKeyDown}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A box as tall as every other control of the panel, and wide enough for four
|
|
123
|
+
* digits.
|
|
124
|
+
* @param metrics - The measurements the panel is drawn from.
|
|
125
|
+
* @returns The box's rules.
|
|
126
|
+
*/
|
|
127
|
+
function sideStyle(metrics: OverlayMetrics): CSSProperties {
|
|
128
|
+
return {
|
|
129
|
+
boxSizing: 'border-box',
|
|
130
|
+
width: '5em',
|
|
131
|
+
height: metrics.controlHeight,
|
|
132
|
+
padding: '0 6px',
|
|
133
|
+
border: '1px solid var(--border)',
|
|
134
|
+
borderRadius: metrics.controlRadius,
|
|
135
|
+
background: 'var(--surface)',
|
|
136
|
+
color: 'var(--text)',
|
|
137
|
+
font: 'inherit',
|
|
138
|
+
fontSize: metrics.fontSize,
|
|
139
|
+
fontVariantNumeric: 'tabular-nums',
|
|
140
|
+
textAlign: 'right',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const FIELD_STYLE = {
|
|
145
|
+
display: 'inline-flex',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
gap: 6,
|
|
148
|
+
color: 'var(--text-muted)',
|
|
149
|
+
} as const satisfies CSSProperties;
|
|
150
|
+
|
|
151
|
+
const UNIT_STYLE = {
|
|
152
|
+
color: 'var(--text-faint)',
|
|
153
|
+
} as const satisfies CSSProperties;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
|
|
4
|
+
import type { Translate } from '../../i18n/ui/useT.ts';
|
|
5
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
6
|
+
import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
|
|
7
|
+
import { OverlaySelect } from '../../overlay/ui/OverlaySelect.tsx';
|
|
8
|
+
import type { FigureLayout } from '../core/figureLayout.ts';
|
|
9
|
+
import { FIGURE_LAYOUTS, figureLayoutSize } from '../core/figureLayout.ts';
|
|
10
|
+
import type { FigurePixels } from '../core/figureScale.ts';
|
|
11
|
+
import { formatFigurePixels } from '../core/figureScale.ts';
|
|
12
|
+
|
|
13
|
+
import { FigureSizeField } from './FigureSizeField.tsx';
|
|
14
|
+
|
|
15
|
+
/** The shape a figure is saved at, and what changes it. */
|
|
16
|
+
export interface FigureSizing {
|
|
17
|
+
/** The shape picked. */
|
|
18
|
+
layout: FigureLayout;
|
|
19
|
+
/**
|
|
20
|
+
* The size typed for `custom`, or `null` before anything was typed, when it
|
|
21
|
+
* starts from the figure on screen.
|
|
22
|
+
*/
|
|
23
|
+
custom: FigurePixels | null;
|
|
24
|
+
/** Called with the shape picked. */
|
|
25
|
+
onLayoutChange: (layout: FigureLayout) => void;
|
|
26
|
+
/** Called with the size typed. */
|
|
27
|
+
onCustomChange: (size: FigurePixels) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What {@link FigureSizeRows} needs. */
|
|
31
|
+
export interface FigureSizeRowsProps {
|
|
32
|
+
/** The shape and what changes it. */
|
|
33
|
+
sizing: FigureSizing;
|
|
34
|
+
/** How big the figure is on the page, or `null` where there is none. */
|
|
35
|
+
size: FigurePixels | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The shape the figure is drawn at, and the two sides of a custom one.
|
|
40
|
+
*
|
|
41
|
+
* Each shape says the pixels it would produce, so choosing `16:9` for a figure
|
|
42
|
+
* already close to it is visibly a choice that changes little.
|
|
43
|
+
* @param props - See {@link FigureSizeRowsProps}.
|
|
44
|
+
* @returns The rows.
|
|
45
|
+
*/
|
|
46
|
+
export function FigureSizeRows(props: FigureSizeRowsProps): ReactElement {
|
|
47
|
+
const { sizing, size } = props;
|
|
48
|
+
const { layout, custom, onLayoutChange, onCustomChange } = sizing;
|
|
49
|
+
const t = useChromeT();
|
|
50
|
+
|
|
51
|
+
const options: Array<OverlayOption<FigureLayout>> = [];
|
|
52
|
+
for (const shape of FIGURE_LAYOUTS) {
|
|
53
|
+
options.push({
|
|
54
|
+
value: shape,
|
|
55
|
+
label: layoutLabel(shape, t),
|
|
56
|
+
title:
|
|
57
|
+
size === null
|
|
58
|
+
? undefined
|
|
59
|
+
: formatFigurePixels(figureLayoutSize(shape, size, custom ?? size)),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<OverlaySelect<FigureLayout>
|
|
66
|
+
label={t('download.size')}
|
|
67
|
+
help={{ title: t('download.size'), body: t('download.sizeHelp') }}
|
|
68
|
+
value={layout}
|
|
69
|
+
options={options}
|
|
70
|
+
disabled={size === null}
|
|
71
|
+
onChange={onLayoutChange}
|
|
72
|
+
/>
|
|
73
|
+
{layout === 'custom' && size !== null ? (
|
|
74
|
+
<FigureSizeField
|
|
75
|
+
label={t('download.customSize')}
|
|
76
|
+
widthLabel={t('download.width')}
|
|
77
|
+
heightLabel={t('download.height')}
|
|
78
|
+
value={figureLayoutSize('custom', size, custom ?? size)}
|
|
79
|
+
onChange={onCustomChange}
|
|
80
|
+
/>
|
|
81
|
+
) : null}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* What a shape reads in the picker. A ratio is written as a ratio in every
|
|
88
|
+
* language.
|
|
89
|
+
* @param layout - The shape.
|
|
90
|
+
* @param t - The chrome's formatter.
|
|
91
|
+
* @returns Its label.
|
|
92
|
+
*/
|
|
93
|
+
function layoutLabel(layout: FigureLayout, t: Translate<ChromeKey>): string {
|
|
94
|
+
switch (layout) {
|
|
95
|
+
case 'standard':
|
|
96
|
+
return '4:3';
|
|
97
|
+
case 'wide':
|
|
98
|
+
return '16:9';
|
|
99
|
+
case 'column':
|
|
100
|
+
return t('download.sizeColumn');
|
|
101
|
+
case 'custom':
|
|
102
|
+
return t('download.sizeCustom');
|
|
103
|
+
case 'screen':
|
|
104
|
+
return t('download.sizeScreen');
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`unknown figure layout: ${String(layout)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/download/ui/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type { FigureDownloadProps } from './FigureDownload.tsx';
|
|
|
11
11
|
export { FigureDownload } from './FigureDownload.tsx';
|
|
12
12
|
export type { FigureDownloadPanelProps } from './FigureDownloadPanel.tsx';
|
|
13
13
|
export { FigureDownloadPanel } from './FigureDownloadPanel.tsx';
|
|
14
|
+
export type { FigureSizing } from './FigureSizeRows.tsx';
|
|
15
|
+
export type { FigureRenderer } from './useFigureRedraw.tsx';
|
|
14
16
|
|
|
15
17
|
export type { ExportDialogFrameProps } from './ExportDialogFrame.tsx';
|
|
16
18
|
export { ExportDialogFrame } from './ExportDialogFrame.tsx';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
|
|
5
|
+
import type { FigurePixels } from '../core/figureScale.ts';
|
|
6
|
+
import { whenFigureSettles } from '../core/figureSettle.ts';
|
|
7
|
+
import { figureElement } from '../core/figureTarget.ts';
|
|
8
|
+
|
|
9
|
+
/** Draws a figure at the size asked for, for a file. */
|
|
10
|
+
export type FigureRenderer = (size: FigurePixels) => ReactNode;
|
|
11
|
+
|
|
12
|
+
/** What {@link useFigureRedraw} hands back. */
|
|
13
|
+
export interface FigureRedraw {
|
|
14
|
+
/**
|
|
15
|
+
* Draw the figure off the page at a size, wait for it to settle, and hand
|
|
16
|
+
* the box it was drawn in to `save`.
|
|
17
|
+
*/
|
|
18
|
+
redraw: (
|
|
19
|
+
targetId: string,
|
|
20
|
+
size: FigurePixels,
|
|
21
|
+
save: (box: Element) => Promise<void>,
|
|
22
|
+
) => Promise<void>;
|
|
23
|
+
/** The copy being drawn, to be rendered anywhere in the caller's tree. */
|
|
24
|
+
portal: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RedrawJob {
|
|
28
|
+
/** The box off the page the copy is drawn in. */
|
|
29
|
+
box: HTMLElement;
|
|
30
|
+
/** The size it is drawn at. */
|
|
31
|
+
size: FigurePixels;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A second copy of a figure, drawn at another size for as long as it takes to
|
|
36
|
+
* save it.
|
|
37
|
+
*
|
|
38
|
+
* The copy is rendered through a portal, so it sees every provider the figure
|
|
39
|
+
* on the page sees, and the portal's box is placed next to the figure's own,
|
|
40
|
+
* so it inherits the same tokens and the same type. It sits far to the left of
|
|
41
|
+
* the page, out of sight, and is taken away once the file is written.
|
|
42
|
+
* @param render - Draws the figure at a size.
|
|
43
|
+
* @returns See {@link FigureRedraw}.
|
|
44
|
+
*/
|
|
45
|
+
export function useFigureRedraw(render?: FigureRenderer): FigureRedraw {
|
|
46
|
+
const [job, setJob] = useState<RedrawJob | null>(null);
|
|
47
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => () => controllerRef.current?.abort(), []);
|
|
50
|
+
|
|
51
|
+
async function redraw(
|
|
52
|
+
targetId: string,
|
|
53
|
+
size: FigurePixels,
|
|
54
|
+
save: (box: Element) => Promise<void>,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const box = mountRedrawBox(figureElement(targetId), size);
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
controllerRef.current = controller;
|
|
59
|
+
setJob({ box, size });
|
|
60
|
+
try {
|
|
61
|
+
await whenFigureSettles(box, { signal: controller.signal });
|
|
62
|
+
await save(box);
|
|
63
|
+
} finally {
|
|
64
|
+
controllerRef.current = null;
|
|
65
|
+
setJob(null);
|
|
66
|
+
box.remove();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const portal =
|
|
71
|
+
job === null || render === undefined
|
|
72
|
+
? null
|
|
73
|
+
: createPortal(render(job.size), job.box);
|
|
74
|
+
|
|
75
|
+
return { redraw, portal };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The box the copy is drawn in, beside the figure it copies.
|
|
80
|
+
*
|
|
81
|
+
* Its type is copied from the figure's box, since a chart inherits its labels'
|
|
82
|
+
* size from wherever it is mounted and the copy is mounted one level up.
|
|
83
|
+
* @param target - The box the figure on the page is mounted in.
|
|
84
|
+
* @param size - The size the copy is drawn at.
|
|
85
|
+
* @returns The box, already in the document.
|
|
86
|
+
*/
|
|
87
|
+
function mountRedrawBox(target: Element, size: FigurePixels): HTMLElement {
|
|
88
|
+
const box = document.createElement('div');
|
|
89
|
+
box.setAttribute('aria-hidden', 'true');
|
|
90
|
+
box.inert = true;
|
|
91
|
+
const type = window.getComputedStyle(target);
|
|
92
|
+
Object.assign(box.style, {
|
|
93
|
+
position: 'absolute',
|
|
94
|
+
top: '0',
|
|
95
|
+
left: '-100000px',
|
|
96
|
+
width: `${size.width}px`,
|
|
97
|
+
height: `${size.height}px`,
|
|
98
|
+
pointerEvents: 'none',
|
|
99
|
+
fontFamily: type.fontFamily,
|
|
100
|
+
fontSize: type.fontSize,
|
|
101
|
+
lineHeight: type.lineHeight,
|
|
102
|
+
color: type.color,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (target instanceof HTMLElement) {
|
|
106
|
+
target.after(box);
|
|
107
|
+
return box;
|
|
108
|
+
}
|
|
109
|
+
// A figure named by its `<svg>` gets the box beside the nearest element
|
|
110
|
+
// that may hold a `<div>`.
|
|
111
|
+
let host: Element | null = target;
|
|
112
|
+
while (host instanceof SVGElement) host = host.parentElement;
|
|
113
|
+
(host ?? document.body).append(box);
|
|
114
|
+
return box;
|
|
115
|
+
}
|
package/src/locales/de.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"language.label": "Sprache",
|
|
3
3
|
"chrome.pages": "Seiten",
|
|
4
|
+
"account.signIn": "Anmelden",
|
|
5
|
+
"account.signOut": "Abmelden",
|
|
6
|
+
"account.signedInAs": "Angemeldet als {name}",
|
|
4
7
|
"ecosystem.tools": "Werkzeuge",
|
|
5
8
|
"ecosystem.menuHeading": "Unsere Werkzeuge, alle im Browser",
|
|
6
9
|
"ecosystem.otherTools": "Unsere weiteren Werkzeuge",
|
|
@@ -180,12 +183,20 @@
|
|
|
180
183
|
"download.format": "Format",
|
|
181
184
|
"download.formatHelp": "Ein PNG ist ein Bild der Abbildung, und das ist es, was eine Folie, ein Chatfenster oder ein Ticket annimmt. Ein SVG ist die Abbildung selbst: jede Linie bleibt scharf, wie weit man sie auch vergrößert, und sie lässt sich nachträglich bearbeiten.",
|
|
182
185
|
"download.resolution": "Auflösung",
|
|
183
|
-
"download.resolutionHelp": "Wie viele
|
|
186
|
+
"download.resolutionHelp": "Wie viele Male größer als die Abbildung auf dem Bildschirm die Datei ist. Ein PNG bekommt entsprechend mehr Pixel: zwei passt für eine Folie, drei oder vier für den Druck. Ein SVG bleibt in jeder Größe scharf und öffnet sich entsprechend größer.",
|
|
184
187
|
"download.resolutionExample": "Eine auf dem Bildschirm 640 Pixel breite Abbildung wird 1280 Pixel breit gespeichert.",
|
|
185
188
|
"download.noFigure": "Auf der Seite gibt es noch keine Abbildung zum Speichern.",
|
|
186
189
|
"download.hintRasterSvg": "Ein SVG, das ein Bild von {pixels} enthält: eine 3D-Szene hat keine Vektorform.",
|
|
187
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "Öffnet sich mit {pixels} und bleibt in jeder Größe scharf.",
|
|
188
191
|
"download.hintPng": "Gespeichert {pixels}.",
|
|
192
|
+
"download.size": "Größe",
|
|
193
|
+
"download.sizeHelp": "Die Form, in der die Abbildung für die Datei gezeichnet wird. Sie wird neu gesetzt, sodass Achsen und Beschriftungen in diese Form passen, statt hineingedehnt zu werden. Eine Zeitschriftenspalte ist 8,5 cm breit, und ihr Text wird in der Größe gedruckt, in der er auf dem Bildschirm steht.",
|
|
194
|
+
"download.sizeScreen": "Wie angezeigt",
|
|
195
|
+
"download.sizeColumn": "Zeitschriftenspalte",
|
|
196
|
+
"download.sizeCustom": "Eigene",
|
|
197
|
+
"download.customSize": "Breite × Höhe",
|
|
198
|
+
"download.width": "Breite",
|
|
199
|
+
"download.height": "Höhe",
|
|
189
200
|
"pedagogy.check": "Prüfen",
|
|
190
201
|
"pedagogy.revealHint": "Hinweis zeigen",
|
|
191
202
|
"pedagogy.revealHintCount": "Hinweis zeigen ({revealed}/{total})",
|
package/src/locales/en.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"language.label": "Language",
|
|
3
3
|
"chrome.pages": "Pages",
|
|
4
|
+
"account.signIn": "Sign in",
|
|
5
|
+
"account.signOut": "Sign out",
|
|
6
|
+
"account.signedInAs": "Signed in as {name}",
|
|
4
7
|
"ecosystem.tools": "Tools",
|
|
5
8
|
"ecosystem.menuHeading": "Our tools, all in the browser",
|
|
6
9
|
"ecosystem.otherTools": "Our other tools",
|
|
@@ -180,12 +183,20 @@
|
|
|
180
183
|
"download.format": "Format",
|
|
181
184
|
"download.formatHelp": "A PNG is a picture of the figure, which is what a slide, a chat window or an issue accepts. An SVG is the figure itself: every line stays sharp however far it is enlarged, and it can still be edited afterwards.",
|
|
182
185
|
"download.resolution": "Resolution",
|
|
183
|
-
"download.resolutionHelp": "How many
|
|
186
|
+
"download.resolutionHelp": "How many times the figure on screen the file is. A PNG gets that many more pixels: two is right for a slide, three or four for print. An SVG stays sharp at any size, and opens that many times larger.",
|
|
184
187
|
"download.resolutionExample": "A figure 640 pixels wide on screen is saved 1280 pixels wide.",
|
|
185
188
|
"download.noFigure": "There is no figure on the page to save yet.",
|
|
186
189
|
"download.hintRasterSvg": "An SVG holding a picture of {pixels}: a 3D scene has no vector form.",
|
|
187
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "Opens at {pixels}, and stays sharp at any size.",
|
|
188
191
|
"download.hintPng": "Saved {pixels}.",
|
|
192
|
+
"download.size": "Size",
|
|
193
|
+
"download.sizeHelp": "The shape the figure is drawn at for the file. It is laid out again, so the axes and labels fit that shape instead of being stretched into it. A journal column is 8.5 cm wide, and its text prints at the size it reads on screen.",
|
|
194
|
+
"download.sizeScreen": "As shown",
|
|
195
|
+
"download.sizeColumn": "Journal column",
|
|
196
|
+
"download.sizeCustom": "Custom",
|
|
197
|
+
"download.customSize": "Width × height",
|
|
198
|
+
"download.width": "Width",
|
|
199
|
+
"download.height": "Height",
|
|
189
200
|
"pedagogy.check": "Check",
|
|
190
201
|
"pedagogy.revealHint": "Reveal hint",
|
|
191
202
|
"pedagogy.revealHintCount": "Reveal hint ({revealed}/{total})",
|
package/src/locales/es.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"language.label": "Idioma",
|
|
3
3
|
"chrome.pages": "Páginas",
|
|
4
|
+
"account.signIn": "Iniciar sesión",
|
|
5
|
+
"account.signOut": "Cerrar sesión",
|
|
6
|
+
"account.signedInAs": "Sesión iniciada como {name}",
|
|
4
7
|
"ecosystem.tools": "Herramientas",
|
|
5
8
|
"ecosystem.menuHeading": "Nuestras herramientas, todas en el navegador",
|
|
6
9
|
"ecosystem.otherTools": "Nuestras otras herramientas",
|
|
@@ -180,12 +183,20 @@
|
|
|
180
183
|
"download.format": "Formato",
|
|
181
184
|
"download.formatHelp": "Un PNG es una imagen de la figura, que es lo que aceptan una diapositiva, una ventana de chat o una incidencia. Un SVG es la figura misma: cada línea sigue nítida por mucho que se amplíe, y todavía puede editarse después.",
|
|
182
185
|
"download.resolution": "Resolución",
|
|
183
|
-
"download.resolutionHelp": "
|
|
186
|
+
"download.resolutionHelp": "Cuántas veces la figura en pantalla mide el archivo. Un PNG recibe tantos píxeles más: dos sirve para una diapositiva, tres o cuatro para imprimir. Un SVG se mantiene nítido a cualquier tamaño, y se abre tantas veces más grande.",
|
|
184
187
|
"download.resolutionExample": "Una figura de 640 píxeles de ancho en pantalla se guarda con 1280 píxeles de ancho.",
|
|
185
188
|
"download.noFigure": "Todavía no hay ninguna figura en la página que guardar.",
|
|
186
189
|
"download.hintRasterSvg": "Un SVG que contiene una imagen de {pixels}: una escena 3D no tiene forma vectorial.",
|
|
187
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "Se abre a {pixels}, y se mantiene nítido a cualquier tamaño.",
|
|
188
191
|
"download.hintPng": "Guardado {pixels}.",
|
|
192
|
+
"download.size": "Tamaño",
|
|
193
|
+
"download.sizeHelp": "La forma con la que se dibuja la figura para el archivo. Se vuelve a maquetar, de modo que los ejes y las etiquetas se ajustan a esa forma en lugar de estirarse. Una columna de revista mide 8,5 cm, y su texto se imprime al tamaño con que se lee en pantalla.",
|
|
194
|
+
"download.sizeScreen": "Como se muestra",
|
|
195
|
+
"download.sizeColumn": "Columna de revista",
|
|
196
|
+
"download.sizeCustom": "Personalizado",
|
|
197
|
+
"download.customSize": "Ancho × alto",
|
|
198
|
+
"download.width": "Ancho",
|
|
199
|
+
"download.height": "Alto",
|
|
189
200
|
"pedagogy.check": "Comprobar",
|
|
190
201
|
"pedagogy.revealHint": "Mostrar una pista",
|
|
191
202
|
"pedagogy.revealHintCount": "Mostrar una pista ({revealed}/{total})",
|