react-cheminfo 0.73.0 → 0.75.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/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +26 -0
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/core/types.d.ts +1 -1
- package/lib/ecosystem/core/types.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +7 -0
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/formula/core/elementRanges.d.ts +25 -0
- package/lib/formula/core/elementRanges.d.ts.map +1 -0
- package/lib/formula/core/elementRanges.js +35 -0
- package/lib/formula/core/elementRanges.js.map +1 -0
- package/lib/formula/core/formulaCandidates.d.ts +59 -0
- package/lib/formula/core/formulaCandidates.d.ts.map +1 -0
- package/lib/formula/core/formulaCandidates.js +50 -0
- package/lib/formula/core/formulaCandidates.js.map +1 -0
- package/lib/formula/core/index.d.ts +4 -0
- package/lib/formula/core/index.d.ts.map +1 -1
- package/lib/formula/core/index.js +2 -0
- package/lib/formula/core/index.js.map +1 -1
- package/lib/formula/ui/ElementRangeFields.d.ts +19 -0
- package/lib/formula/ui/ElementRangeFields.d.ts.map +1 -0
- package/lib/formula/ui/ElementRangeFields.js +23 -0
- package/lib/formula/ui/ElementRangeFields.js.map +1 -0
- package/lib/formula/ui/FormulaLookup.d.ts +47 -0
- package/lib/formula/ui/FormulaLookup.d.ts.map +1 -0
- package/lib/formula/ui/FormulaLookup.js +74 -0
- package/lib/formula/ui/FormulaLookup.js.map +1 -0
- package/lib/formula/ui/FormulaLookupResults.d.ts +20 -0
- package/lib/formula/ui/FormulaLookupResults.d.ts.map +1 -0
- package/lib/formula/ui/FormulaLookupResults.js +45 -0
- package/lib/formula/ui/FormulaLookupResults.js.map +1 -0
- package/lib/formula/ui/index.d.ts +2 -0
- package/lib/formula/ui/index.d.ts.map +1 -1
- package/lib/formula/ui/index.js +1 -0
- package/lib/formula/ui/index.js.map +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/locales/de.json +38 -0
- package/lib/locales/en.json +38 -0
- package/lib/locales/es.json +38 -0
- package/lib/locales/fr.json +38 -0
- package/lib/locales/it.json +38 -0
- package/lib/structure/core/fragmentIdCode.d.ts +15 -0
- package/lib/structure/core/fragmentIdCode.d.ts.map +1 -0
- package/lib/structure/core/fragmentIdCode.js +25 -0
- package/lib/structure/core/fragmentIdCode.js.map +1 -0
- package/lib/structure/core/index.d.ts +2 -0
- package/lib/structure/core/index.d.ts.map +1 -1
- package/lib/structure/core/index.js +1 -0
- package/lib/structure/core/index.js.map +1 -1
- package/lib/structure/core/structureSearch.d.ts +44 -0
- package/lib/structure/core/structureSearch.d.ts.map +1 -0
- package/lib/structure/core/structureSearch.js +31 -0
- package/lib/structure/core/structureSearch.js.map +1 -0
- package/lib/structure/core/substructureMatch.d.ts +33 -0
- package/lib/structure/core/substructureMatch.d.ts.map +1 -0
- package/lib/structure/core/substructureMatch.js +73 -0
- package/lib/structure/core/substructureMatch.js.map +1 -0
- package/lib/structure/ui/Structure.d.ts +18 -0
- package/lib/structure/ui/Structure.d.ts.map +1 -1
- package/lib/structure/ui/Structure.js +2 -2
- package/lib/structure/ui/Structure.js.map +1 -1
- package/lib/structure/ui/StructureSearchDialog.d.ts +55 -0
- package/lib/structure/ui/StructureSearchDialog.d.ts.map +1 -0
- package/lib/structure/ui/StructureSearchDialog.js +77 -0
- package/lib/structure/ui/StructureSearchDialog.js.map +1 -0
- package/lib/structure/ui/StructureSvg.d.ts +9 -0
- package/lib/structure/ui/StructureSvg.d.ts.map +1 -1
- package/lib/structure/ui/StructureSvg.js +7 -2
- package/lib/structure/ui/StructureSvg.js.map +1 -1
- package/lib/structure/ui/index.d.ts +4 -0
- package/lib/structure/ui/index.d.ts.map +1 -1
- package/lib/structure/ui/index.js +2 -0
- package/lib/structure/ui/index.js.map +1 -1
- package/lib/structure/ui/useStructureSearchModes.d.ts +18 -0
- package/lib/structure/ui/useStructureSearchModes.d.ts.map +1 -0
- package/lib/structure/ui/useStructureSearchModes.js +36 -0
- package/lib/structure/ui/useStructureSearchModes.js.map +1 -0
- package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
- package/lib/structure/ui/useToolbarFloor.js +20 -10
- package/lib/structure/ui/useToolbarFloor.js.map +1 -1
- package/package.json +4 -3
- package/src/ecosystem/core/sites.ts +26 -0
- package/src/ecosystem/core/types.ts +3 -1
- package/src/ecosystem/ui/glyphs.tsx +46 -0
- package/src/formula/core/elementRanges.ts +44 -0
- package/src/formula/core/formulaCandidates.ts +109 -0
- package/src/formula/core/index.ts +15 -0
- package/src/formula/ui/ElementRangeFields.tsx +65 -0
- package/src/formula/ui/FormulaLookup.tsx +184 -0
- package/src/formula/ui/FormulaLookupResults.tsx +89 -0
- package/src/formula/ui/index.ts +2 -0
- package/src/locales/de.json +38 -0
- package/src/locales/en.json +38 -0
- package/src/locales/es.json +38 -0
- package/src/locales/fr.json +38 -0
- package/src/locales/it.json +38 -0
- package/src/structure/core/fragmentIdCode.ts +25 -0
- package/src/structure/core/index.ts +10 -0
- package/src/structure/core/structureSearch.ts +68 -0
- package/src/structure/core/substructureMatch.ts +85 -0
- package/src/structure/ui/Structure.tsx +24 -0
- package/src/structure/ui/StructureSearchDialog.tsx +218 -0
- package/src/structure/ui/StructureSvg.tsx +35 -0
- package/src/structure/ui/index.ts +4 -0
- package/src/structure/ui/useStructureSearchModes.ts +51 -0
- package/src/structure/ui/useToolbarFloor.ts +19 -9
- package/styles/chrome.css +164 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dialog a structure search is drawn in, shared by every site that
|
|
3
|
+
* filters a collection by structure.
|
|
4
|
+
*
|
|
5
|
+
* This file stays free of `openchemlib`: the editor loads it behind its own
|
|
6
|
+
* `React.lazy`, and an example is read only once it is clicked.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogBody,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
HTMLSelect,
|
|
15
|
+
} from '@blueprintjs/core';
|
|
16
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import { formatPercent } from '../../format/core/numbers.ts';
|
|
20
|
+
import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
|
|
21
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
22
|
+
import { isEmptyIdCode } from '../core/editorValue.ts';
|
|
23
|
+
import type {
|
|
24
|
+
StructureSearchExample,
|
|
25
|
+
StructureSearchMode,
|
|
26
|
+
StructureSearchQuery,
|
|
27
|
+
} from '../core/structureSearch.ts';
|
|
28
|
+
import {
|
|
29
|
+
STRUCTURE_SEARCH_MODES,
|
|
30
|
+
similarityThresholds,
|
|
31
|
+
} from '../core/structureSearch.ts';
|
|
32
|
+
|
|
33
|
+
import { StructureEditor } from './StructureEditor.tsx';
|
|
34
|
+
import { useStructureDraft } from './useStructureDraft.ts';
|
|
35
|
+
import { useStructureSearchModes } from './useStructureSearchModes.ts';
|
|
36
|
+
|
|
37
|
+
/** What {@link StructureSearchDialog} needs. */
|
|
38
|
+
export interface StructureSearchDialogProps {
|
|
39
|
+
/** Whether the dialog is on screen. */
|
|
40
|
+
isOpen: boolean;
|
|
41
|
+
/** The question the dialog opens on; its drawing is loaded into the canvas. */
|
|
42
|
+
value: StructureSearchQuery;
|
|
43
|
+
/**
|
|
44
|
+
* Called when the dialog closes — by Apply, by Escape or by its × alike —
|
|
45
|
+
* with what it then holds, so no drawing is lost to a stray Escape. The
|
|
46
|
+
* idCode is empty when the canvas was cleared.
|
|
47
|
+
*/
|
|
48
|
+
onClose: (query: StructureSearchQuery) => void;
|
|
49
|
+
/**
|
|
50
|
+
* The modes offered, in order.
|
|
51
|
+
* @default STRUCTURE_SEARCH_MODES
|
|
52
|
+
*/
|
|
53
|
+
modes?: readonly StructureSearchMode[];
|
|
54
|
+
/**
|
|
55
|
+
* The fragments offered under the canvas.
|
|
56
|
+
* @default benzene, pyridine, amide, sulfonamide and trifluoromethyl
|
|
57
|
+
*/
|
|
58
|
+
examples?: readonly StructureSearchExample[];
|
|
59
|
+
/**
|
|
60
|
+
* More controls beside the similarity threshold, such as the fingerprint a
|
|
61
|
+
* site compares.
|
|
62
|
+
* @default undefined
|
|
63
|
+
*/
|
|
64
|
+
similarityControls?: ReactNode;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the dialog is rendered through a portal on `document.body`.
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
usePortal?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const DEFAULT_EXAMPLES: ReadonlyArray<{ key: ChromeKey; smiles: string }> = [
|
|
73
|
+
{ key: 'structure.search.exampleBenzene', smiles: 'c1ccccc1' },
|
|
74
|
+
{ key: 'structure.search.examplePyridine', smiles: 'c1ccncc1' },
|
|
75
|
+
{ key: 'structure.search.exampleAmide', smiles: 'CC(=O)N' },
|
|
76
|
+
{ key: 'structure.search.exampleSulfonamide', smiles: 'S(=O)(=O)N' },
|
|
77
|
+
{ key: 'structure.search.exampleTrifluoromethyl', smiles: 'FC(F)F' },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Draw a structure and say how it is matched: contained, similar, or the same
|
|
82
|
+
* compound with or without its stereochemistry and tautomerism.
|
|
83
|
+
*
|
|
84
|
+
* What is drawn stays a draft until the dialog closes, and the canvas is never
|
|
85
|
+
* reloaded under the pen: only opening the dialog and clicking an example put
|
|
86
|
+
* a structure in it.
|
|
87
|
+
* @param props - See {@link StructureSearchDialogProps}.
|
|
88
|
+
* @returns The dialog.
|
|
89
|
+
*/
|
|
90
|
+
export function StructureSearchDialog(
|
|
91
|
+
props: StructureSearchDialogProps,
|
|
92
|
+
): ReactElement {
|
|
93
|
+
const {
|
|
94
|
+
isOpen,
|
|
95
|
+
value,
|
|
96
|
+
onClose,
|
|
97
|
+
modes = STRUCTURE_SEARCH_MODES,
|
|
98
|
+
examples,
|
|
99
|
+
similarityControls,
|
|
100
|
+
usePortal = true,
|
|
101
|
+
} = props;
|
|
102
|
+
const t = useChromeT();
|
|
103
|
+
const modeLabels = useStructureSearchModes(modes);
|
|
104
|
+
const draft = useStructureDraft(value.idCode);
|
|
105
|
+
const [mode, setMode] = useState(value.mode);
|
|
106
|
+
const [threshold, setThreshold] = useState(value.threshold);
|
|
107
|
+
// An example is read by a lazily loaded module: until it is in the canvas,
|
|
108
|
+
// Apply would hand back the drawing it replaces.
|
|
109
|
+
const [loading, setLoading] = useState(false);
|
|
110
|
+
const offered: readonly StructureSearchExample[] =
|
|
111
|
+
examples ??
|
|
112
|
+
DEFAULT_EXAMPLES.map((example) => ({
|
|
113
|
+
label: t(example.key),
|
|
114
|
+
smiles: example.smiles,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
function close(): void {
|
|
118
|
+
const idCode = isEmptyIdCode(draft.value) ? '' : draft.value;
|
|
119
|
+
onClose({ idCode, mode, threshold });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadExample(smiles: string): Promise<void> {
|
|
123
|
+
setLoading(true);
|
|
124
|
+
try {
|
|
125
|
+
const { fragmentIdCode } = await import('../core/fragmentIdCode.ts');
|
|
126
|
+
const idCode = fragmentIdCode(smiles);
|
|
127
|
+
if (idCode !== '') draft.load(idCode);
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Dialog
|
|
135
|
+
isOpen={isOpen}
|
|
136
|
+
onOpening={() => {
|
|
137
|
+
draft.load(value.idCode);
|
|
138
|
+
setMode(modes.includes(value.mode) ? value.mode : (modes[0] ?? mode));
|
|
139
|
+
setThreshold(value.threshold);
|
|
140
|
+
}}
|
|
141
|
+
onClose={close}
|
|
142
|
+
usePortal={usePortal}
|
|
143
|
+
title={t('structure.search.title')}
|
|
144
|
+
className="structure-search"
|
|
145
|
+
>
|
|
146
|
+
<DialogBody>
|
|
147
|
+
<div className="structure-search__row">
|
|
148
|
+
{modeLabels.map((option) => (
|
|
149
|
+
<Button
|
|
150
|
+
key={option.mode}
|
|
151
|
+
size="small"
|
|
152
|
+
active={mode === option.mode}
|
|
153
|
+
intent={mode === option.mode ? 'primary' : 'none'}
|
|
154
|
+
text={option.label}
|
|
155
|
+
title={option.help}
|
|
156
|
+
onClick={() => setMode(option.mode)}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
{mode === 'similarity' && (
|
|
161
|
+
<div className="structure-search__row">
|
|
162
|
+
<span className="structure-search__label">
|
|
163
|
+
{t('structure.search.similarityLabel')}
|
|
164
|
+
</span>
|
|
165
|
+
<HTMLSelect
|
|
166
|
+
minimal
|
|
167
|
+
value={threshold}
|
|
168
|
+
aria-label={t('structure.search.threshold')}
|
|
169
|
+
title={t('structure.search.thresholdHelp')}
|
|
170
|
+
options={similarityThresholds(threshold).map((option) => ({
|
|
171
|
+
value: option,
|
|
172
|
+
label: t('structure.search.thresholdOption', {
|
|
173
|
+
percent: formatPercent(option, 0),
|
|
174
|
+
}),
|
|
175
|
+
}))}
|
|
176
|
+
onChange={(event) =>
|
|
177
|
+
setThreshold(Number(event.currentTarget.value))
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
{similarityControls}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
<StructureEditor
|
|
184
|
+
fragment
|
|
185
|
+
value={draft.value}
|
|
186
|
+
revision={draft.revision}
|
|
187
|
+
onChange={(change) => draft.update(change.idCode)}
|
|
188
|
+
/>
|
|
189
|
+
<div className="structure-search__row structure-search__examples">
|
|
190
|
+
<span className="structure-search__label">
|
|
191
|
+
{t('structure.search.examples')}
|
|
192
|
+
</span>
|
|
193
|
+
{offered.map((example) => (
|
|
194
|
+
<Button
|
|
195
|
+
key={example.label}
|
|
196
|
+
variant="minimal"
|
|
197
|
+
size="small"
|
|
198
|
+
text={example.label}
|
|
199
|
+
title={example.help}
|
|
200
|
+
onClick={() => void loadExample(example.smiles)}
|
|
201
|
+
/>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
</DialogBody>
|
|
205
|
+
<DialogFooter
|
|
206
|
+
actions={
|
|
207
|
+
<Button
|
|
208
|
+
intent="primary"
|
|
209
|
+
icon="search"
|
|
210
|
+
text={t('structure.search.apply')}
|
|
211
|
+
loading={loading}
|
|
212
|
+
onClick={close}
|
|
213
|
+
/>
|
|
214
|
+
}
|
|
215
|
+
/>
|
|
216
|
+
</Dialog>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { AtomLabelPlacement } from '../core/atomLabels.ts';
|
|
|
17
17
|
import { applyAtomLabels } from '../core/atomLabels.ts';
|
|
18
18
|
import { sourceMolecule } from '../core/sourceMolecule.ts';
|
|
19
19
|
import type { StructureSource } from '../core/structureSource.ts';
|
|
20
|
+
import { substructureMatch } from '../core/substructureMatch.ts';
|
|
20
21
|
|
|
21
22
|
import { StructurePlaceholder } from './StructurePlaceholder.tsx';
|
|
22
23
|
|
|
@@ -46,6 +47,15 @@ interface StructureSvgProps {
|
|
|
46
47
|
bondHighlight?: number[];
|
|
47
48
|
/** The colour the highlighted bonds are painted. */
|
|
48
49
|
bondHighlightColor: string;
|
|
50
|
+
/**
|
|
51
|
+
* A query idCode whose matches are painted in place of the highlights.
|
|
52
|
+
* @default undefined
|
|
53
|
+
*/
|
|
54
|
+
substructure?: string;
|
|
55
|
+
/** The colour the substructure match is painted. */
|
|
56
|
+
substructureColor: string;
|
|
57
|
+
/** How opaque the substructure match is painted, from 0 to 1. */
|
|
58
|
+
substructureOpacity: number;
|
|
49
59
|
/** Write the index of every atom next to it. */
|
|
50
60
|
showAtomNumber: boolean;
|
|
51
61
|
/** Write the index of every bond next to it. */
|
|
@@ -94,6 +104,13 @@ export function StructureSvg(props: StructureSvgProps): ReactElement {
|
|
|
94
104
|
onAtomClick,
|
|
95
105
|
onBondClick,
|
|
96
106
|
showCIPParity,
|
|
107
|
+
atomHighlight,
|
|
108
|
+
atomHighlightColor,
|
|
109
|
+
bondHighlight,
|
|
110
|
+
bondHighlightColor,
|
|
111
|
+
substructure,
|
|
112
|
+
substructureColor,
|
|
113
|
+
substructureOpacity,
|
|
97
114
|
...rest
|
|
98
115
|
} = props;
|
|
99
116
|
const { kind, value, coordinates } = source;
|
|
@@ -106,6 +123,14 @@ export function StructureSvg(props: StructureSvgProps): ReactElement {
|
|
|
106
123
|
() => readDrawing(kind, value, coordinates, labelsKey, atomLabelPlacement),
|
|
107
124
|
[kind, value, coordinates, labelsKey, atomLabelPlacement],
|
|
108
125
|
);
|
|
126
|
+
// Matched on the very molecule drawn, so the indices are the drawn atoms.
|
|
127
|
+
const match = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
drawing === null || substructure === undefined
|
|
130
|
+
? null
|
|
131
|
+
: substructureMatch(drawing.molecule, substructure),
|
|
132
|
+
[drawing, substructure],
|
|
133
|
+
);
|
|
109
134
|
|
|
110
135
|
if (drawing === null) {
|
|
111
136
|
return (
|
|
@@ -118,6 +143,16 @@ export function StructureSvg(props: StructureSvgProps): ReactElement {
|
|
|
118
143
|
return (
|
|
119
144
|
<SvgRenderer
|
|
120
145
|
{...rest}
|
|
146
|
+
atomHighlight={match === null ? atomHighlight : match.atoms}
|
|
147
|
+
atomHighlightColor={
|
|
148
|
+
match === null ? atomHighlightColor : substructureColor
|
|
149
|
+
}
|
|
150
|
+
bondHighlight={match === null ? bondHighlight : match.bonds}
|
|
151
|
+
bondHighlightColor={
|
|
152
|
+
match === null ? bondHighlightColor : substructureColor
|
|
153
|
+
}
|
|
154
|
+
atomHighlightOpacity={match === null ? undefined : substructureOpacity}
|
|
155
|
+
bondHighlightOpacity={match === null ? undefined : substructureOpacity}
|
|
121
156
|
molecule={drawing.molecule}
|
|
122
157
|
noCarbonLabelWithCustomLabel={drawing.labelled}
|
|
123
158
|
suppressCIPParity={!showCIPParity}
|
|
@@ -25,6 +25,10 @@ export type { StructureDropProps } from './StructureDrop.tsx';
|
|
|
25
25
|
export { StructureDrop } from './StructureDrop.tsx';
|
|
26
26
|
export type { StructureEditorHelpProps } from './StructureEditorHelp.tsx';
|
|
27
27
|
export { StructureEditorHelp } from './StructureEditorHelp.tsx';
|
|
28
|
+
export type { StructureSearchDialogProps } from './StructureSearchDialog.tsx';
|
|
29
|
+
export { StructureSearchDialog } from './StructureSearchDialog.tsx';
|
|
30
|
+
export type { StructureSearchModeLabel } from './useStructureSearchModes.ts';
|
|
31
|
+
export { useStructureSearchModes } from './useStructureSearchModes.ts';
|
|
28
32
|
export type { StructureExportDialogProps } from './StructureExportDialog.tsx';
|
|
29
33
|
export { StructureExportDialog } from './StructureExportDialog.tsx';
|
|
30
34
|
export type { StructureExportButtonProps } from './StructureExportButton.tsx';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
|
|
2
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
3
|
+
import type { StructureSearchMode } from '../core/structureSearch.ts';
|
|
4
|
+
import { STRUCTURE_SEARCH_MODES } from '../core/structureSearch.ts';
|
|
5
|
+
|
|
6
|
+
/** A mode, with what a button says and what its tooltip adds. */
|
|
7
|
+
export interface StructureSearchModeLabel {
|
|
8
|
+
/** The mode. */
|
|
9
|
+
mode: StructureSearchMode;
|
|
10
|
+
/** What the button says. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** One line on what the mode finds. */
|
|
13
|
+
help: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MODE_KEYS: Record<StructureSearchMode, [ChromeKey, ChromeKey]> = {
|
|
17
|
+
substructure: [
|
|
18
|
+
'structure.search.substructure',
|
|
19
|
+
'structure.search.substructureHelp',
|
|
20
|
+
],
|
|
21
|
+
similarity: [
|
|
22
|
+
'structure.search.similarity',
|
|
23
|
+
'structure.search.similarityHelp',
|
|
24
|
+
],
|
|
25
|
+
exact: ['structure.search.exact', 'structure.search.exactHelp'],
|
|
26
|
+
exactNoStereo: [
|
|
27
|
+
'structure.search.exactNoStereo',
|
|
28
|
+
'structure.search.exactNoStereoHelp',
|
|
29
|
+
],
|
|
30
|
+
exactNoStereoTautomer: [
|
|
31
|
+
'structure.search.exactNoStereoTautomer',
|
|
32
|
+
'structure.search.exactNoStereoTautomerHelp',
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The modes in the reader's language, for a site that shows the mode in force
|
|
38
|
+
* beside the drawing.
|
|
39
|
+
* @param modes - The modes, in order.
|
|
40
|
+
* @returns Each mode with its label and its one-line help.
|
|
41
|
+
*/
|
|
42
|
+
export function useStructureSearchModes(
|
|
43
|
+
modes: readonly StructureSearchMode[] = STRUCTURE_SEARCH_MODES,
|
|
44
|
+
): StructureSearchModeLabel[] {
|
|
45
|
+
const t = useChromeT();
|
|
46
|
+
return modes.map((mode) => ({
|
|
47
|
+
mode,
|
|
48
|
+
label: t(MODE_KEYS[mode][0]),
|
|
49
|
+
help: t(MODE_KEYS[mode][1]),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
@@ -41,18 +41,28 @@ export function useToolbarFloor(
|
|
|
41
41
|
if (container === null) return;
|
|
42
42
|
|
|
43
43
|
return watchToolbar(container, (toolbar) => {
|
|
44
|
+
let frame = 0;
|
|
44
45
|
const size = new ResizeObserver(() => {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
// Written on the next frame: growing the box from inside this callback
|
|
47
|
+
// resizes the editor element above the toolbar, whose own observer the
|
|
48
|
+
// browser then drops with a "ResizeObserver loop" error.
|
|
49
|
+
cancelAnimationFrame(frame);
|
|
50
|
+
frame = requestAnimationFrame(() => {
|
|
51
|
+
// offsetHeight, because a dialog opens under a scaling transform and
|
|
52
|
+
// a measured rectangle would be that of the half drawn toolbar.
|
|
53
|
+
const height = toolbar.offsetHeight;
|
|
54
|
+
// Zero is a toolbar being torn down or not yet laid out, never a
|
|
55
|
+
// toolbar that needs no room: writing the fallback for it would undo
|
|
56
|
+
// a correct floor and leave the palette cut off.
|
|
57
|
+
if (height === 0) return;
|
|
58
|
+
container.style.minHeight = `${toolbarFloorHeight(height, minHeight)}px`;
|
|
59
|
+
});
|
|
53
60
|
});
|
|
54
61
|
size.observe(toolbar);
|
|
55
|
-
return () =>
|
|
62
|
+
return () => {
|
|
63
|
+
cancelAnimationFrame(frame);
|
|
64
|
+
size.disconnect();
|
|
65
|
+
};
|
|
56
66
|
});
|
|
57
67
|
}, [minHeight, revision]);
|
|
58
68
|
|
package/styles/chrome.css
CHANGED
|
@@ -889,6 +889,37 @@ a.source-badge:hover {
|
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
/* The structure search dialog: the modes above the canvas, the fragments to
|
|
893
|
+
start from under it. A drag on the canvas that leaves it would otherwise
|
|
894
|
+
select the labels. */
|
|
895
|
+
.structure-search {
|
|
896
|
+
width: min(56rem, calc(100vw - 2rem));
|
|
897
|
+
user-select: none;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.structure-search :is(input, textarea) {
|
|
901
|
+
user-select: text;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.structure-search__row {
|
|
905
|
+
display: flex;
|
|
906
|
+
flex-wrap: wrap;
|
|
907
|
+
align-items: center;
|
|
908
|
+
margin-bottom: 0.6rem;
|
|
909
|
+
gap: 0.35rem;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.structure-search__examples {
|
|
913
|
+
margin-top: 0.6rem;
|
|
914
|
+
margin-bottom: 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.structure-search__label {
|
|
918
|
+
margin-right: 0.25rem;
|
|
919
|
+
color: var(--text-muted);
|
|
920
|
+
font-size: 0.8125rem;
|
|
921
|
+
}
|
|
922
|
+
|
|
892
923
|
/* ── Tokens for molecules.cheminfo.org ─────────────────────────────────────── */
|
|
893
924
|
|
|
894
925
|
/* The heading, when the tokens were checked, and the button asking again, on
|
|
@@ -1729,6 +1760,139 @@ span.taxon-lineage__name {
|
|
|
1729
1760
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1730
1761
|
}
|
|
1731
1762
|
|
|
1763
|
+
/* ── A formula lookup ────────────────────────────────────────────────────── */
|
|
1764
|
+
|
|
1765
|
+
/* Only the panel's own look: where it sits — a column beside a question, a
|
|
1766
|
+
sticky rail, a shadow — is the page's, through the class it passes. */
|
|
1767
|
+
.formula-lookup {
|
|
1768
|
+
display: flex;
|
|
1769
|
+
flex-direction: column;
|
|
1770
|
+
border: 1px solid var(--border);
|
|
1771
|
+
background: var(--surface);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
.formula-lookup__header {
|
|
1775
|
+
display: flex;
|
|
1776
|
+
align-items: center;
|
|
1777
|
+
padding: 0.25rem 0.5rem 0.25rem 0.75rem;
|
|
1778
|
+
border-bottom: 1px solid var(--border);
|
|
1779
|
+
color: var(--text-muted);
|
|
1780
|
+
font-size: 0.8rem;
|
|
1781
|
+
font-weight: 700;
|
|
1782
|
+
gap: 0.25rem;
|
|
1783
|
+
letter-spacing: 0.04em;
|
|
1784
|
+
text-transform: uppercase;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
.formula-lookup__header > .bp6-button {
|
|
1788
|
+
margin-left: auto;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
.formula-lookup__body {
|
|
1792
|
+
display: flex;
|
|
1793
|
+
flex-direction: column;
|
|
1794
|
+
padding: 0.75rem;
|
|
1795
|
+
gap: 0.5rem;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
.formula-lookup__field {
|
|
1799
|
+
display: flex;
|
|
1800
|
+
flex-direction: column;
|
|
1801
|
+
color: var(--text-muted);
|
|
1802
|
+
font-size: 0.8rem;
|
|
1803
|
+
font-weight: 600;
|
|
1804
|
+
gap: 2px;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
.formula-lookup__hint {
|
|
1808
|
+
color: var(--text-muted);
|
|
1809
|
+
font-size: 0.75rem;
|
|
1810
|
+
line-height: 1.4;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/* Two elements to a line: each box holds two or three digits. */
|
|
1814
|
+
.formula-lookup__ranges {
|
|
1815
|
+
display: grid;
|
|
1816
|
+
margin-top: 0.25rem;
|
|
1817
|
+
gap: 0.25rem 0.5rem;
|
|
1818
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
.formula-lookup__range {
|
|
1822
|
+
display: flex;
|
|
1823
|
+
min-width: 0;
|
|
1824
|
+
align-items: center;
|
|
1825
|
+
gap: 3px;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.formula-lookup__range .bp6-input-group {
|
|
1829
|
+
min-width: 0;
|
|
1830
|
+
flex: 1 1 0;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
.formula-lookup__range .bp6-input {
|
|
1834
|
+
padding-right: 4px;
|
|
1835
|
+
padding-left: 4px;
|
|
1836
|
+
text-align: center;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
.formula-lookup__element {
|
|
1840
|
+
width: 1.3rem;
|
|
1841
|
+
font-variant-numeric: tabular-nums;
|
|
1842
|
+
font-weight: 700;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
.formula-lookup__dash {
|
|
1846
|
+
color: var(--text-muted);
|
|
1847
|
+
font-size: 0.75rem;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
.formula-lookup__results {
|
|
1851
|
+
display: flex;
|
|
1852
|
+
flex-direction: column;
|
|
1853
|
+
padding: 0.75rem;
|
|
1854
|
+
border-top: 1px solid var(--border);
|
|
1855
|
+
gap: 0.25rem;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/* A search still to come leaves the band empty rather than collapsing it. */
|
|
1859
|
+
.formula-lookup__results:empty {
|
|
1860
|
+
min-height: 2.5rem;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
.formula-lookup__count {
|
|
1864
|
+
color: var(--text-muted);
|
|
1865
|
+
font-size: 0.75rem;
|
|
1866
|
+
font-weight: 600;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
.formula-lookup__list {
|
|
1870
|
+
display: flex;
|
|
1871
|
+
overflow-y: auto;
|
|
1872
|
+
max-height: 40vh;
|
|
1873
|
+
flex-direction: column;
|
|
1874
|
+
padding: 0;
|
|
1875
|
+
margin: 0;
|
|
1876
|
+
list-style: none;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/* A formula is never split across two lines. */
|
|
1880
|
+
.formula-lookup__list li {
|
|
1881
|
+
display: flex;
|
|
1882
|
+
align-items: baseline;
|
|
1883
|
+
justify-content: space-between;
|
|
1884
|
+
padding: 2px 0;
|
|
1885
|
+
font-size: 0.85rem;
|
|
1886
|
+
gap: 0.5rem;
|
|
1887
|
+
white-space: nowrap;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
.formula-lookup__ppm {
|
|
1891
|
+
color: var(--text-muted);
|
|
1892
|
+
font-size: 0.72rem;
|
|
1893
|
+
font-variant-numeric: tabular-nums;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1732
1896
|
/* ── A list of exercises ─────────────────────────────────────────────────── */
|
|
1733
1897
|
|
|
1734
1898
|
.exercise-list {
|