react-cheminfo 0.73.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  2. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  3. package/lib/locales/de.json +22 -0
  4. package/lib/locales/en.json +22 -0
  5. package/lib/locales/es.json +22 -0
  6. package/lib/locales/fr.json +22 -0
  7. package/lib/locales/it.json +22 -0
  8. package/lib/structure/core/fragmentIdCode.d.ts +15 -0
  9. package/lib/structure/core/fragmentIdCode.d.ts.map +1 -0
  10. package/lib/structure/core/fragmentIdCode.js +25 -0
  11. package/lib/structure/core/fragmentIdCode.js.map +1 -0
  12. package/lib/structure/core/index.d.ts +2 -0
  13. package/lib/structure/core/index.d.ts.map +1 -1
  14. package/lib/structure/core/index.js +1 -0
  15. package/lib/structure/core/index.js.map +1 -1
  16. package/lib/structure/core/structureSearch.d.ts +44 -0
  17. package/lib/structure/core/structureSearch.d.ts.map +1 -0
  18. package/lib/structure/core/structureSearch.js +31 -0
  19. package/lib/structure/core/structureSearch.js.map +1 -0
  20. package/lib/structure/core/substructureMatch.d.ts +33 -0
  21. package/lib/structure/core/substructureMatch.d.ts.map +1 -0
  22. package/lib/structure/core/substructureMatch.js +73 -0
  23. package/lib/structure/core/substructureMatch.js.map +1 -0
  24. package/lib/structure/ui/Structure.d.ts +18 -0
  25. package/lib/structure/ui/Structure.d.ts.map +1 -1
  26. package/lib/structure/ui/Structure.js +2 -2
  27. package/lib/structure/ui/Structure.js.map +1 -1
  28. package/lib/structure/ui/StructureSearchDialog.d.ts +55 -0
  29. package/lib/structure/ui/StructureSearchDialog.d.ts.map +1 -0
  30. package/lib/structure/ui/StructureSearchDialog.js +77 -0
  31. package/lib/structure/ui/StructureSearchDialog.js.map +1 -0
  32. package/lib/structure/ui/StructureSvg.d.ts +9 -0
  33. package/lib/structure/ui/StructureSvg.d.ts.map +1 -1
  34. package/lib/structure/ui/StructureSvg.js +7 -2
  35. package/lib/structure/ui/StructureSvg.js.map +1 -1
  36. package/lib/structure/ui/index.d.ts +4 -0
  37. package/lib/structure/ui/index.d.ts.map +1 -1
  38. package/lib/structure/ui/index.js +2 -0
  39. package/lib/structure/ui/index.js.map +1 -1
  40. package/lib/structure/ui/useStructureSearchModes.d.ts +18 -0
  41. package/lib/structure/ui/useStructureSearchModes.d.ts.map +1 -0
  42. package/lib/structure/ui/useStructureSearchModes.js +36 -0
  43. package/lib/structure/ui/useStructureSearchModes.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/locales/de.json +22 -0
  46. package/src/locales/en.json +22 -0
  47. package/src/locales/es.json +22 -0
  48. package/src/locales/fr.json +22 -0
  49. package/src/locales/it.json +22 -0
  50. package/src/structure/core/fragmentIdCode.ts +25 -0
  51. package/src/structure/core/index.ts +10 -0
  52. package/src/structure/core/structureSearch.ts +68 -0
  53. package/src/structure/core/substructureMatch.ts +85 -0
  54. package/src/structure/ui/Structure.tsx +24 -0
  55. package/src/structure/ui/StructureSearchDialog.tsx +218 -0
  56. package/src/structure/ui/StructureSvg.tsx +35 -0
  57. package/src/structure/ui/index.ts +4 -0
  58. package/src/structure/ui/useStructureSearchModes.ts +51 -0
  59. package/styles/chrome.css +31 -0
@@ -0,0 +1,68 @@
1
+ /** How a drawn structure is matched against a collection. */
2
+ export type StructureSearchMode =
3
+ | 'substructure'
4
+ | 'similarity'
5
+ | 'exact'
6
+ | 'exactNoStereo'
7
+ | 'exactNoStereoTautomer';
8
+
9
+ /**
10
+ * Every mode, in the order a dialog offers them: the two broad scans, then
11
+ * exact and its two relaxations.
12
+ */
13
+ export const STRUCTURE_SEARCH_MODES: readonly StructureSearchMode[] = [
14
+ 'substructure',
15
+ 'similarity',
16
+ 'exact',
17
+ 'exactNoStereo',
18
+ 'exactNoStereoTautomer',
19
+ ];
20
+
21
+ /** A structure question: what is drawn, and how it is matched. */
22
+ export interface StructureSearchQuery {
23
+ /**
24
+ * The drawing as an idCode, with the coordinates the editor laid out written
25
+ * after a space; empty when nothing is drawn.
26
+ */
27
+ idCode: string;
28
+ /** How the drawing is matched. */
29
+ mode: StructureSearchMode;
30
+ /** The least Tanimoto similarity a hit reaches, from 0 to 1. */
31
+ threshold: number;
32
+ }
33
+
34
+ /** A fragment a drawing can start from. */
35
+ export interface StructureSearchExample {
36
+ /** What the button says. */
37
+ label: string;
38
+ /** The fragment, as a SMILES. */
39
+ smiles: string;
40
+ /**
41
+ * One line on what the fragment finds.
42
+ * @default undefined
43
+ */
44
+ help?: string;
45
+ }
46
+
47
+ /**
48
+ * The least similarities offered, as Tanimoto coefficients, in steps of 5 %.
49
+ */
50
+ export const SIMILARITY_THRESHOLDS: readonly number[] = [
51
+ 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95,
52
+ ];
53
+
54
+ /**
55
+ * The thresholds to offer when one is already chosen: the usual steps, plus the
56
+ * chosen one when it is none of them, so a link asking for 0.42 still finds it
57
+ * in the list.
58
+ * @param current - The threshold in force.
59
+ * @returns The thresholds, ascending.
60
+ */
61
+ export function similarityThresholds(current: number): number[] {
62
+ if (SIMILARITY_THRESHOLDS.includes(current) || !Number.isFinite(current)) {
63
+ return [...SIMILARITY_THRESHOLDS];
64
+ }
65
+ return [...SIMILARITY_THRESHOLDS, current].toSorted(
66
+ (left, right) => left - right,
67
+ );
68
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Where a query fragment sits in a molecule, as the atoms and bonds to paint.
3
+ *
4
+ * **Not exported from `core/index.ts`**: it value-imports `openchemlib`, so a
5
+ * barrel carrying it would pull the whole library into every site that imports
6
+ * `react-cheminfo/core`. Only the lazily loaded depiction calls it.
7
+ */
8
+
9
+ import { Molecule, SSSearcher } from 'openchemlib';
10
+
11
+ import { isEmptyIdCode, splitIdCode } from './editorValue.ts';
12
+
13
+ /** The atoms and bonds of a molecule that a query covers. */
14
+ export interface SubstructureMatch {
15
+ /** Atom indices, ascending, as openchemlib counts atoms from 0. */
16
+ atoms: number[];
17
+ /** Bond indices, ascending, as openchemlib counts bonds from 0. */
18
+ bonds: number[];
19
+ }
20
+
21
+ /**
22
+ * Find every match of a query in a molecule, merged into one set of atoms and
23
+ * bonds.
24
+ *
25
+ * Every match is painted, not only the first: a query for a phenyl ring that a
26
+ * molecule carries twice is shown twice. A bond is painted only when it is a
27
+ * bond of the query, so a ring closing between two matched atoms that the
28
+ * query leaves open stays unpainted. The query is read as a fragment whether
29
+ * or not it was drawn as one — a molecule not flagged as a fragment only ever
30
+ * matches itself, which is never what a substructure search asked.
31
+ * @param molecule - The molecule the depiction draws; its atom order is the
32
+ * one the indices refer to.
33
+ * @param query - The query as an openchemlib idCode, coordinates included or
34
+ * not.
35
+ * @returns The covered atoms and bonds, both empty when nothing matches.
36
+ */
37
+ export function substructureMatch(
38
+ molecule: Molecule,
39
+ query: string,
40
+ ): SubstructureMatch {
41
+ if (isEmptyIdCode(query)) return { atoms: [], bonds: [] };
42
+ const fragment = Molecule.fromIDCode(splitIdCode(query).idCode);
43
+ fragment.setFragment(true);
44
+
45
+ const searcher = new SSSearcher();
46
+ searcher.setMol(fragment, molecule);
47
+ if (searcher.findFragmentInMolecule({ countMode: 'overlapping' }) === 0) {
48
+ return { atoms: [], bonds: [] };
49
+ }
50
+
51
+ const atomCovered = new Uint8Array(molecule.getAllAtoms());
52
+ const bondCovered = new Uint8Array(molecule.getAllBonds());
53
+ const fragmentBonds = fragment.getAllBonds();
54
+ for (const match of searcher.getMatchList()) {
55
+ for (const target of match) {
56
+ // An exclude group of the query maps onto nothing.
57
+ if (target >= 0) atomCovered[target] = 1;
58
+ }
59
+ for (let bond = 0; bond < fragmentBonds; bond++) {
60
+ const first = match[fragment.getBondAtom(0, bond)] ?? -1;
61
+ const second = match[fragment.getBondAtom(1, bond)] ?? -1;
62
+ if (first < 0 || second < 0) continue;
63
+ const target = molecule.getBond(first, second);
64
+ if (target >= 0) bondCovered[target] = 1;
65
+ }
66
+ }
67
+
68
+ return {
69
+ atoms: coveredIndices(atomCovered),
70
+ bonds: coveredIndices(bondCovered),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * The positions of a flag array that are set, in ascending order.
76
+ * @param flags - One flag per atom or bond.
77
+ * @returns The indices whose flag is set.
78
+ */
79
+ function coveredIndices(flags: Uint8Array): number[] {
80
+ const indices: number[] = [];
81
+ for (let index = 0; index < flags.length; index++) {
82
+ if (flags[index] === 1) indices.push(index);
83
+ }
84
+ return indices;
85
+ }
@@ -133,6 +133,24 @@ export interface StructureProps {
133
133
  * @default '#ffd8a8'
134
134
  */
135
135
  bondHighlightColor?: string;
136
+ /**
137
+ * A query fragment, as an openchemlib idCode, whose every match is painted —
138
+ * atoms and bonds alike, so the hit reads as one shape. When set, it takes
139
+ * the place of `atomHighlight` and `bondHighlight`.
140
+ * @default undefined
141
+ */
142
+ substructure?: string;
143
+ /**
144
+ * The colour the `substructure` match is painted.
145
+ * @default '#ffd500'
146
+ */
147
+ substructureColor?: string;
148
+ /**
149
+ * How opaque the `substructure` match is painted, from 0 to 1. Light, so the
150
+ * bonds and the atom labels under it stay legible.
151
+ * @default 0.15
152
+ */
153
+ substructureOpacity?: number;
136
154
  /**
137
155
  * Called with the index of the atom that was clicked, counted from 0 as
138
156
  * openchemlib counts atoms.
@@ -181,6 +199,9 @@ export function Structure(props: StructureProps): ReactElement {
181
199
  atomHighlightColor = '#a5d8ff',
182
200
  bondHighlight,
183
201
  bondHighlightColor = '#ffd8a8',
202
+ substructure,
203
+ substructureColor = '#ffd500',
204
+ substructureOpacity = 0.15,
184
205
  onAtomClick,
185
206
  onBondClick,
186
207
  fallback = '—',
@@ -208,6 +229,9 @@ export function Structure(props: StructureProps): ReactElement {
208
229
  atomHighlightColor={atomHighlightColor}
209
230
  bondHighlight={bondHighlight}
210
231
  bondHighlightColor={bondHighlightColor}
232
+ substructure={substructure}
233
+ substructureColor={substructureColor}
234
+ substructureOpacity={substructureOpacity}
211
235
  showAtomNumber={labels.atoms ?? false}
212
236
  showBondNumber={labels.bonds ?? false}
213
237
  showMapping={labels.mapping ?? false}
@@ -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
+ }
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