react-cheminfo 0.30.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 +20 -0
- 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 +10 -2
- package/lib/locales/en.json +10 -2
- package/lib/locales/es.json +10 -2
- package/lib/locales/fr.json +10 -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/package.json +1 -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 +10 -2
- package/src/locales/en.json +10 -2
- package/src/locales/es.json +10 -2
- package/src/locales/fr.json +10 -2
- package/src/overlay/ui/OverlayValueMenu.tsx +7 -1
|
@@ -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
|
@@ -183,12 +183,20 @@
|
|
|
183
183
|
"download.format": "Format",
|
|
184
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.",
|
|
185
185
|
"download.resolution": "Auflösung",
|
|
186
|
-
"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.",
|
|
187
187
|
"download.resolutionExample": "Eine auf dem Bildschirm 640 Pixel breite Abbildung wird 1280 Pixel breit gespeichert.",
|
|
188
188
|
"download.noFigure": "Auf der Seite gibt es noch keine Abbildung zum Speichern.",
|
|
189
189
|
"download.hintRasterSvg": "Ein SVG, das ein Bild von {pixels} enthält: eine 3D-Szene hat keine Vektorform.",
|
|
190
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "Öffnet sich mit {pixels} und bleibt in jeder Größe scharf.",
|
|
191
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",
|
|
192
200
|
"pedagogy.check": "Prüfen",
|
|
193
201
|
"pedagogy.revealHint": "Hinweis zeigen",
|
|
194
202
|
"pedagogy.revealHintCount": "Hinweis zeigen ({revealed}/{total})",
|
package/src/locales/en.json
CHANGED
|
@@ -183,12 +183,20 @@
|
|
|
183
183
|
"download.format": "Format",
|
|
184
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.",
|
|
185
185
|
"download.resolution": "Resolution",
|
|
186
|
-
"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.",
|
|
187
187
|
"download.resolutionExample": "A figure 640 pixels wide on screen is saved 1280 pixels wide.",
|
|
188
188
|
"download.noFigure": "There is no figure on the page to save yet.",
|
|
189
189
|
"download.hintRasterSvg": "An SVG holding a picture of {pixels}: a 3D scene has no vector form.",
|
|
190
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "Opens at {pixels}, and stays sharp at any size.",
|
|
191
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",
|
|
192
200
|
"pedagogy.check": "Check",
|
|
193
201
|
"pedagogy.revealHint": "Reveal hint",
|
|
194
202
|
"pedagogy.revealHintCount": "Reveal hint ({revealed}/{total})",
|
package/src/locales/es.json
CHANGED
|
@@ -183,12 +183,20 @@
|
|
|
183
183
|
"download.format": "Formato",
|
|
184
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.",
|
|
185
185
|
"download.resolution": "Resolución",
|
|
186
|
-
"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.",
|
|
187
187
|
"download.resolutionExample": "Una figura de 640 píxeles de ancho en pantalla se guarda con 1280 píxeles de ancho.",
|
|
188
188
|
"download.noFigure": "Todavía no hay ninguna figura en la página que guardar.",
|
|
189
189
|
"download.hintRasterSvg": "Un SVG que contiene una imagen de {pixels}: una escena 3D no tiene forma vectorial.",
|
|
190
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "Se abre a {pixels}, y se mantiene nítido a cualquier tamaño.",
|
|
191
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",
|
|
192
200
|
"pedagogy.check": "Comprobar",
|
|
193
201
|
"pedagogy.revealHint": "Mostrar una pista",
|
|
194
202
|
"pedagogy.revealHintCount": "Mostrar una pista ({revealed}/{total})",
|
package/src/locales/fr.json
CHANGED
|
@@ -183,12 +183,20 @@
|
|
|
183
183
|
"download.format": "Format",
|
|
184
184
|
"download.formatHelp": "Un PNG est une image de la figure, ce qu'acceptent une diapositive, une fenêtre de discussion ou un ticket. Un SVG est la figure elle-même : chaque trait reste net quel que soit l'agrandissement, et il reste modifiable ensuite.",
|
|
185
185
|
"download.resolution": "Résolution",
|
|
186
|
-
"download.resolutionHelp": "Combien de
|
|
186
|
+
"download.resolutionHelp": "Combien de fois la figure à l'écran mesure le fichier. Un PNG reçoit d'autant plus de pixels : deux convient pour une diapositive, trois ou quatre pour l'impression. Un SVG reste net à toute taille, et s'ouvre d'autant plus grand.",
|
|
187
187
|
"download.resolutionExample": "Une figure large de 640 pixels à l'écran est enregistrée large de 1280 pixels.",
|
|
188
188
|
"download.noFigure": "Il n'y a encore aucune figure à enregistrer sur la page.",
|
|
189
189
|
"download.hintRasterSvg": "Un SVG contenant une image de {pixels} : une scène 3D n'a pas de forme vectorielle.",
|
|
190
|
-
"download.hintSvg": "
|
|
190
|
+
"download.hintSvg": "S'ouvre à {pixels}, et reste net à toute taille.",
|
|
191
191
|
"download.hintPng": "Enregistré {pixels}.",
|
|
192
|
+
"download.size": "Taille",
|
|
193
|
+
"download.sizeHelp": "La forme à laquelle la figure est dessinée pour le fichier. Elle est remise en page, si bien que les axes et les étiquettes s'ajustent à cette forme au lieu d'y être étirés. Une colonne de revue mesure 8,5 cm, et son texte s'imprime à la taille où il se lit à l'écran.",
|
|
194
|
+
"download.sizeScreen": "Comme affichée",
|
|
195
|
+
"download.sizeColumn": "Colonne de revue",
|
|
196
|
+
"download.sizeCustom": "Personnalisée",
|
|
197
|
+
"download.customSize": "Largeur × hauteur",
|
|
198
|
+
"download.width": "Largeur",
|
|
199
|
+
"download.height": "Hauteur",
|
|
192
200
|
"pedagogy.check": "Vérifier",
|
|
193
201
|
"pedagogy.revealHint": "Révéler un indice",
|
|
194
202
|
"pedagogy.revealHintCount": "Révéler un indice ({revealed}/{total})",
|
|
@@ -108,7 +108,13 @@ export function OverlayValueMenu<TValue extends string = string>(
|
|
|
108
108
|
text={option.label}
|
|
109
109
|
htmlTitle={option.title}
|
|
110
110
|
disabled={option.disabled}
|
|
111
|
-
|
|
111
|
+
// A menu opened from inside a panel would otherwise dismiss the
|
|
112
|
+
// panel too, taking the reader's other settings away mid-choice.
|
|
113
|
+
shouldDismissPopover={false}
|
|
114
|
+
onClick={() => {
|
|
115
|
+
onChange(option.value);
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}}
|
|
112
118
|
/>
|
|
113
119
|
))}
|
|
114
120
|
</Menu>
|