react-cheminfo 0.74.0 → 0.76.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 (98) hide show
  1. package/lib/download/core/copySvg.d.ts +12 -0
  2. package/lib/download/core/copySvg.d.ts.map +1 -0
  3. package/lib/download/core/copySvg.js +38 -0
  4. package/lib/download/core/copySvg.js.map +1 -0
  5. package/lib/download/core/index.d.ts +1 -0
  6. package/lib/download/core/index.d.ts.map +1 -1
  7. package/lib/download/core/index.js +1 -0
  8. package/lib/download/core/index.js.map +1 -1
  9. package/lib/ecosystem/core/sites.d.ts.map +1 -1
  10. package/lib/ecosystem/core/sites.js +26 -0
  11. package/lib/ecosystem/core/sites.js.map +1 -1
  12. package/lib/ecosystem/core/types.d.ts +1 -1
  13. package/lib/ecosystem/core/types.d.ts.map +1 -1
  14. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  15. package/lib/ecosystem/ui/glyphs.js +7 -0
  16. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  17. package/lib/formula/core/elementRanges.d.ts +25 -0
  18. package/lib/formula/core/elementRanges.d.ts.map +1 -0
  19. package/lib/formula/core/elementRanges.js +35 -0
  20. package/lib/formula/core/elementRanges.js.map +1 -0
  21. package/lib/formula/core/formulaCandidates.d.ts +59 -0
  22. package/lib/formula/core/formulaCandidates.d.ts.map +1 -0
  23. package/lib/formula/core/formulaCandidates.js +50 -0
  24. package/lib/formula/core/formulaCandidates.js.map +1 -0
  25. package/lib/formula/core/index.d.ts +4 -0
  26. package/lib/formula/core/index.d.ts.map +1 -1
  27. package/lib/formula/core/index.js +2 -0
  28. package/lib/formula/core/index.js.map +1 -1
  29. package/lib/formula/ui/ElementRangeFields.d.ts +19 -0
  30. package/lib/formula/ui/ElementRangeFields.d.ts.map +1 -0
  31. package/lib/formula/ui/ElementRangeFields.js +23 -0
  32. package/lib/formula/ui/ElementRangeFields.js.map +1 -0
  33. package/lib/formula/ui/FormulaLookup.d.ts +47 -0
  34. package/lib/formula/ui/FormulaLookup.d.ts.map +1 -0
  35. package/lib/formula/ui/FormulaLookup.js +74 -0
  36. package/lib/formula/ui/FormulaLookup.js.map +1 -0
  37. package/lib/formula/ui/FormulaLookupResults.d.ts +20 -0
  38. package/lib/formula/ui/FormulaLookupResults.d.ts.map +1 -0
  39. package/lib/formula/ui/FormulaLookupResults.js +45 -0
  40. package/lib/formula/ui/FormulaLookupResults.js.map +1 -0
  41. package/lib/formula/ui/index.d.ts +2 -0
  42. package/lib/formula/ui/index.d.ts.map +1 -1
  43. package/lib/formula/ui/index.js +1 -0
  44. package/lib/formula/ui/index.js.map +1 -1
  45. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  46. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  47. package/lib/ir/core/bandAssignments.d.ts.map +1 -1
  48. package/lib/ir/core/bandAssignments.js +27 -0
  49. package/lib/ir/core/bandAssignments.js.map +1 -1
  50. package/lib/ir/ui/BandRow.d.ts +4 -4
  51. package/lib/ir/ui/BandRow.d.ts.map +1 -1
  52. package/lib/ir/ui/BandRow.js +41 -14
  53. package/lib/ir/ui/BandRow.js.map +1 -1
  54. package/lib/locales/de.json +18 -0
  55. package/lib/locales/en.json +18 -0
  56. package/lib/locales/es.json +18 -0
  57. package/lib/locales/fr.json +18 -0
  58. package/lib/locales/it.json +18 -0
  59. package/lib/structure/ui/CardStructure.d.ts +43 -0
  60. package/lib/structure/ui/CardStructure.d.ts.map +1 -0
  61. package/lib/structure/ui/CardStructure.js +28 -0
  62. package/lib/structure/ui/CardStructure.js.map +1 -0
  63. package/lib/structure/ui/StructurePicturePane.d.ts +1 -1
  64. package/lib/structure/ui/StructurePicturePane.d.ts.map +1 -1
  65. package/lib/structure/ui/StructurePicturePane.js +5 -2
  66. package/lib/structure/ui/StructurePicturePane.js.map +1 -1
  67. package/lib/structure/ui/index.d.ts +2 -0
  68. package/lib/structure/ui/index.d.ts.map +1 -1
  69. package/lib/structure/ui/index.js +1 -0
  70. package/lib/structure/ui/index.js.map +1 -1
  71. package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
  72. package/lib/structure/ui/useToolbarFloor.js +20 -10
  73. package/lib/structure/ui/useToolbarFloor.js.map +1 -1
  74. package/package.json +4 -3
  75. package/src/download/core/copySvg.ts +40 -0
  76. package/src/download/core/index.ts +1 -0
  77. package/src/ecosystem/core/sites.ts +26 -0
  78. package/src/ecosystem/core/types.ts +3 -1
  79. package/src/ecosystem/ui/glyphs.tsx +46 -0
  80. package/src/formula/core/elementRanges.ts +44 -0
  81. package/src/formula/core/formulaCandidates.ts +109 -0
  82. package/src/formula/core/index.ts +15 -0
  83. package/src/formula/ui/ElementRangeFields.tsx +65 -0
  84. package/src/formula/ui/FormulaLookup.tsx +184 -0
  85. package/src/formula/ui/FormulaLookupResults.tsx +89 -0
  86. package/src/formula/ui/index.ts +2 -0
  87. package/src/ir/core/bandAssignments.ts +27 -0
  88. package/src/ir/ui/BandRow.tsx +84 -26
  89. package/src/locales/de.json +18 -0
  90. package/src/locales/en.json +18 -0
  91. package/src/locales/es.json +18 -0
  92. package/src/locales/fr.json +18 -0
  93. package/src/locales/it.json +18 -0
  94. package/src/structure/ui/CardStructure.tsx +76 -0
  95. package/src/structure/ui/StructurePicturePane.tsx +14 -1
  96. package/src/structure/ui/index.ts +2 -0
  97. package/src/structure/ui/useToolbarFloor.ts +19 -9
  98. package/styles/chrome.css +136 -3
@@ -54,6 +54,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
54
54
  from: 500,
55
55
  to: 600,
56
56
  strength: 'S',
57
+ note: 'Only with bromine in the formula; the mass spectrum shows its 1:1 M+2',
57
58
  },
58
59
  {
59
60
  id: 'c-cl-stretch',
@@ -62,6 +63,16 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
62
63
  from: 600,
63
64
  to: 800,
64
65
  strength: 'S',
66
+ note: 'Only with chlorine in the formula; the mass spectrum shows its 3:1 M+2',
67
+ },
68
+ {
69
+ id: 'alkyne-ch-bend',
70
+ group: 'alkyne',
71
+ vibration: '≡C–H bend',
72
+ from: 610,
73
+ to: 700,
74
+ strength: 'S',
75
+ note: 'Broad, and only beside the sharp ≡C–H stretch near 3300',
65
76
  },
66
77
  {
67
78
  id: 'aromatic-ch-oop',
@@ -97,6 +108,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
97
108
  from: 1000,
98
109
  to: 1400,
99
110
  strength: 'S',
111
+ note: 'Only with fluorine in the formula; a CF₃ gives several strong bands',
100
112
  },
101
113
  {
102
114
  id: 'c-n-stretch',
@@ -105,6 +117,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
105
117
  from: 1080,
106
118
  to: 1360,
107
119
  strength: 'm',
120
+ note: 'Only with nitrogen in the formula, and rarely decisive on its own',
108
121
  },
109
122
  {
110
123
  id: 'nitro-symmetric',
@@ -122,6 +135,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
122
135
  from: 1330,
123
136
  to: 1430,
124
137
  strength: 'm',
138
+ note: 'Only beside the broad O–H stretch above 3200',
125
139
  },
126
140
  {
127
141
  id: 'alkane-ch-bend',
@@ -148,6 +162,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
148
162
  from: 1500,
149
163
  to: 1560,
150
164
  strength: 'S',
165
+ note: 'Never alone: the symmetric partner sits near 1300–1390',
151
166
  },
152
167
  {
153
168
  id: 'nh-bend',
@@ -156,6 +171,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
156
171
  from: 1550,
157
172
  to: 1640,
158
173
  strength: 'm',
174
+ note: 'Only beside an N–H stretch between 3300 and 3500',
159
175
  },
160
176
  {
161
177
  id: 'alkene-cc-stretch',
@@ -218,6 +234,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
218
234
  from: 1770,
219
235
  to: 1815,
220
236
  strength: 'S',
237
+ note: 'The highest single carbonyl, and only with chlorine in the formula',
221
238
  },
222
239
  {
223
240
  id: 'anhydride-co-stretch',
@@ -228,6 +245,15 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
228
245
  strength: 'S',
229
246
  note: 'Two bands, about 60 apart, which is what names it',
230
247
  },
248
+ {
249
+ id: 'allene-stretch',
250
+ group: 'allene',
251
+ vibration: 'C=C=C asymmetric stretch',
252
+ from: 1900,
253
+ to: 2000,
254
+ strength: 'm',
255
+ note: 'Near 1950, where only a cumulated double bond absorbs',
256
+ },
231
257
  {
232
258
  id: 'nitrile-stretch',
233
259
  group: 'nitrile',
@@ -289,6 +315,7 @@ export const BAND_ASSIGNMENTS: readonly BandAssignment[] = [
289
315
  from: 3000,
290
316
  to: 3100,
291
317
  strength: 'm',
318
+ note: 'Above 3000, with the ring pair near 1500 and 1600',
292
319
  },
293
320
  {
294
321
  id: 'alkyne-ch-stretch',
@@ -1,4 +1,5 @@
1
1
  import { Button, Icon } from '@blueprintjs/core';
2
+ import { useState } from 'react';
2
3
 
3
4
  import {
4
5
  panelCellStyle,
@@ -34,10 +35,10 @@ export interface BandRowProps {
34
35
  /**
35
36
  * One band: where it is, how strong, and every vibration it could be.
36
37
  *
37
- * The candidates are written as one line rather than a list, so a strong band
38
- * with four of them stays one row of the table and the wavenumbers keep reading
39
- * down the column. The `title` carries them again with each one's note, which is
40
- * where the reason to prefer one lives.
38
+ * The candidates start as one line rather than a list, so a strong band with
39
+ * four of them stays one row of the table and the wavenumbers keep reading down
40
+ * the column. Its "more" opens the row onto every candidate with its note, which
41
+ * is where the reason to prefer one lives.
41
42
  * @param props - Component props.
42
43
  * @returns The row.
43
44
  */
@@ -47,7 +48,7 @@ export function BandRow(props: BandRowProps) {
47
48
 
48
49
  return (
49
50
  <tr
50
- style={lit ? litRowStyle : undefined}
51
+ style={lit ? litRowStyle : rowStyle}
51
52
  onPointerEnter={() => onHighlight?.(band)}
52
53
  onPointerLeave={() => onHighlight?.(null)}
53
54
  >
@@ -80,35 +81,63 @@ interface CandidatesCellProps {
80
81
  }
81
82
 
82
83
  /**
83
- * What a band could be: the narrowest candidate, and how many more there are.
84
+ * What a band could be: the narrowest candidate and how many more there are,
85
+ * opened on a click into every candidate with the note that tells it apart.
84
86
  * @param props - Component props.
85
87
  * @returns The cell.
86
88
  */
87
89
  function CandidatesCell(props: CandidatesCellProps) {
88
90
  const { candidates } = props.entry;
89
- const detail = candidates
90
- .map((candidate) =>
91
- candidate.note === undefined
92
- ? `${candidate.group} ${candidate.vibration}`
93
- : `${candidate.group} ${candidate.vibration} — ${candidate.note}`,
94
- )
95
- .join('\n');
91
+ const [open, setOpen] = useState(false);
96
92
 
97
- return (
98
- <td style={panelCellStyle} title={detail}>
99
- {candidates.length === 0 ? (
93
+ if (candidates.length === 0) {
94
+ return (
95
+ <td style={panelCellStyle}>
100
96
  <span style={{ opacity: 0.5 }}>—</span>
97
+ </td>
98
+ );
99
+ }
100
+
101
+ const [first] = candidates;
102
+ const others = candidates.length - 1;
103
+
104
+ return (
105
+ <td style={candidatesCellStyle}>
106
+ {open ? (
107
+ <ul style={candidateListStyle}>
108
+ {candidates.map((candidate) => (
109
+ <li key={candidate.id}>
110
+ {candidate.group} {candidate.vibration}
111
+ {candidate.note === undefined ? null : (
112
+ <div style={candidateNoteStyle}>{candidate.note}</div>
113
+ )}
114
+ </li>
115
+ ))}
116
+ </ul>
101
117
  ) : (
102
- <span style={{ whiteSpace: 'normal' }}>
103
- {candidates[0]?.group} {candidates[0]?.vibration}
104
- {candidates.length > 1 ? (
105
- <span style={{ opacity: 0.6 }}>
106
- {' '}
107
- <Icon icon="more" size={10} /> {candidates.length - 1} more
108
- </span>
109
- ) : null}
110
- </span>
118
+ <>
119
+ {first?.group} {first?.vibration}
120
+ </>
111
121
  )}
122
+ {others > 0 ? (
123
+ <>
124
+ {open ? null : ' '}
125
+ <button
126
+ type="button"
127
+ style={moreButtonStyle}
128
+ aria-expanded={open}
129
+ onClick={() => setOpen(!open)}
130
+ >
131
+ {open ? (
132
+ 'less'
133
+ ) : (
134
+ <>
135
+ <Icon icon="more" size={10} /> {others} more
136
+ </>
137
+ )}
138
+ </button>
139
+ </>
140
+ ) : null}
112
141
  </td>
113
142
  );
114
143
  }
@@ -120,6 +149,35 @@ const STRENGTH_TITLES = {
120
149
  S: 'strong, relative to this spectrum',
121
150
  } as const;
122
151
 
123
- const litRowStyle = { background: '#fff3e0' } as const;
152
+ // Top, so an opened row keeps its wavenumber on the line of its first candidate.
153
+ const rowStyle = { verticalAlign: 'top' } as const;
154
+
155
+ const litRowStyle = { ...rowStyle, background: '#fff3e0' } as const;
124
156
 
125
157
  const removeCellStyle = { ...panelCellStyle, padding: 0, width: 1 } as const;
158
+
159
+ const candidatesCellStyle = {
160
+ ...panelCellStyle,
161
+ whiteSpace: 'normal',
162
+ } as const;
163
+
164
+ const candidateListStyle = {
165
+ display: 'flex',
166
+ flexDirection: 'column',
167
+ gap: 4,
168
+ margin: 0,
169
+ padding: 0,
170
+ listStyle: 'none',
171
+ } as const;
172
+
173
+ const candidateNoteStyle = { fontSize: '0.9em', opacity: 0.6 } as const;
174
+
175
+ const moreButtonStyle = {
176
+ padding: 0,
177
+ border: 0,
178
+ background: 'none',
179
+ color: 'inherit',
180
+ font: 'inherit',
181
+ opacity: 0.6,
182
+ cursor: 'pointer',
183
+ } as const;
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Reguläre Ausdrücke in einer interaktiven Spielwiese lernen.",
104
104
  "site.pdb.tagline": "Ein schneller Blick auf jeden Eintrag der Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Eine Struktur aus einem 1H-NMR-Spektrum und einer Formel.",
106
+ "site.spectroscopy.tagline": "Eine Struktur aus ihren IR-, Massen- und NMR-Spektren bestimmen.",
106
107
  "site.equilibrium.tagline": "Chemische Gleichgewichte: pH, Speziation und Titrationskurven.",
107
108
  "site.polycarp.tagline": "Die Mikrostruktur einer radikalischen Copolymerisation vorhersagen.",
108
109
  "site.3d.tagline": "Konformere in 3D aus einer Struktur, die Sie zeichnen.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "„{formula}“ gibt einem Atom die Anzahl {count}; Anzahlen sind ganze Zahlen.",
346
347
  "formula.unknownNotation": "„{formula}“ verwendet eine Schreibweise, die dieses Werkzeug nicht zählt.",
347
348
  "formula.unreadable": "„{formula}“ ist keine Formel, die dieses Werkzeug lesen kann ({reason}).",
349
+ "formula.lookup.title": "Formelsuche",
350
+ "formula.lookup.reset": "Masse und Elementbereiche zurücksetzen",
351
+ "formula.lookup.mass": "Monoisotopische Masse",
352
+ "formula.lookup.massPlaceholder": "z. B. 128.0626",
353
+ "formula.lookup.neutral": "Die des neutralen Moleküls: Ziehen Sie die Protonen ab, die das Spektrum hinzugefügt hat.",
354
+ "formula.lookup.fewest": "Mindestzahl der {element}-Atome",
355
+ "formula.lookup.most": "Höchstzahl der {element}-Atome",
356
+ "formula.lookup.typeMass": "Geben Sie eine Masse ein, um die Formeln aufzulisten, die diese Bereiche zulassen.",
357
+ "formula.lookup.closest": "Die {count} nächsten von {total} Formeln",
358
+ "formula.lookup.countOne": "{count} Formel",
359
+ "formula.lookup.countMany": "{count} Formeln",
360
+ "formula.lookup.nothingFits": "Keine Formel passt. Erweitern Sie einen Bereich oder prüfen Sie die Masse.",
361
+ "formula.lookup.tooWide": "Diese Bereiche lassen zu viele Formeln für die Suche zu. Schränken Sie einen ein.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, Unsättigungsgrad {unsaturation}",
348
364
  "slides.exit": "Beenden",
349
365
  "slides.previousSlide": "Vorige Folie",
350
366
  "slides.nextSlide": "Nächste Folie",
@@ -571,6 +587,8 @@
571
587
  "structure.export.saveSvg": "Als SVG speichern",
572
588
  "structure.export.savePng": "Als PNG speichern",
573
589
  "structure.export.copyPng": "PNG kopieren",
590
+ "structure.export.copySvg": "SVG kopieren",
591
+ "structure.export.svgCopied": "In die Zwischenablage kopiert: SVG.",
574
592
  "structure.export.smiles": "SMILES",
575
593
  "structure.export.smilesHelp": "Die Struktur in einer Zeile, Stereochemie eingeschlossen. Jedes Programm liest sie.",
576
594
  "structure.export.molfileV3000": "Molfile V3000",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Learn regular expressions in a live playground.",
104
104
  "site.pdb.tagline": "A fast look at any Protein Data Bank entry.",
105
105
  "site.elucidation.tagline": "A structure from a 1H NMR spectrum and a formula.",
106
+ "site.spectroscopy.tagline": "Find a structure from its IR, mass and NMR spectra.",
106
107
  "site.equilibrium.tagline": "Chemical equilibria: pH, speciation and titration curves.",
107
108
  "site.polycarp.tagline": "Predict the microstructure of a radical copolymerisation.",
108
109
  "site.3d.tagline": "Conformers in 3D from a structure you draw.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "“{formula}” gives an atom a count of {count}; counts are whole numbers.",
346
347
  "formula.unknownNotation": "“{formula}” uses a notation this tool does not count.",
347
348
  "formula.unreadable": "“{formula}” is not a formula this tool can read ({reason}).",
349
+ "formula.lookup.title": "Formula lookup",
350
+ "formula.lookup.reset": "Reset the mass and the element ranges",
351
+ "formula.lookup.mass": "Monoisotopic mass",
352
+ "formula.lookup.massPlaceholder": "e.g. 128.0626",
353
+ "formula.lookup.neutral": "Of the neutral molecule: take the protons the spectrum added back off.",
354
+ "formula.lookup.fewest": "Fewest {element} atoms",
355
+ "formula.lookup.most": "Most {element} atoms",
356
+ "formula.lookup.typeMass": "Type a mass to list the formulas these ranges allow.",
357
+ "formula.lookup.closest": "The {count} closest of {total} formulas",
358
+ "formula.lookup.countOne": "{count} formula",
359
+ "formula.lookup.countMany": "{count} formulas",
360
+ "formula.lookup.nothingFits": "Nothing fits. Widen a range, or check the mass.",
361
+ "formula.lookup.tooWide": "These ranges allow too many formulas to search. Narrow one.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, unsaturation {unsaturation}",
348
364
  "slides.exit": "Exit",
349
365
  "slides.previousSlide": "Previous slide",
350
366
  "slides.nextSlide": "Next slide",
@@ -515,6 +531,8 @@
515
531
  "structure.export.saveSvg": "Save SVG",
516
532
  "structure.export.savePng": "Save PNG",
517
533
  "structure.export.copyPng": "Copy PNG",
534
+ "structure.export.copySvg": "Copy SVG",
535
+ "structure.export.svgCopied": "Copied the SVG to the clipboard.",
518
536
  "structure.export.smiles": "SMILES",
519
537
  "structure.export.smilesHelp": "The structure on one line, stereochemistry included. Every tool reads it.",
520
538
  "structure.export.molfileV3000": "Molfile V3000",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Aprender expresiones regulares en un entorno interactivo.",
104
104
  "site.pdb.tagline": "Una mirada rápida a cualquier entrada del Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Una estructura a partir de un espectro de RMN de 1H y una fórmula.",
106
+ "site.spectroscopy.tagline": "Encontrar una estructura a partir de sus espectros IR, de masas y de RMN.",
106
107
  "site.equilibrium.tagline": "Equilibrios químicos: pH, especiación y curvas de valoración.",
107
108
  "site.polycarp.tagline": "Predecir la microestructura de una copolimerización radicalaria.",
108
109
  "site.3d.tagline": "Confórmeros en 3D a partir de una estructura que usted dibuja.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "«{formula}» da a un átomo la cantidad {count}; las cantidades son números enteros.",
346
347
  "formula.unknownNotation": "«{formula}» usa una notación que esta herramienta no cuenta.",
347
348
  "formula.unreadable": "«{formula}» no es una fórmula que esta herramienta sepa leer ({reason}).",
349
+ "formula.lookup.title": "Búsqueda de fórmulas",
350
+ "formula.lookup.reset": "Restablecer la masa y los intervalos de elementos",
351
+ "formula.lookup.mass": "Masa monoisotópica",
352
+ "formula.lookup.massPlaceholder": "p. ej. 128.0626",
353
+ "formula.lookup.neutral": "La de la molécula neutra: reste los protones que el espectro añadió.",
354
+ "formula.lookup.fewest": "Mínimo de átomos de {element}",
355
+ "formula.lookup.most": "Máximo de átomos de {element}",
356
+ "formula.lookup.typeMass": "Escriba una masa para listar las fórmulas que permiten estos intervalos.",
357
+ "formula.lookup.closest": "Las {count} más cercanas de {total} fórmulas",
358
+ "formula.lookup.countOne": "{count} fórmula",
359
+ "formula.lookup.countMany": "{count} fórmulas",
360
+ "formula.lookup.nothingFits": "Ninguna fórmula encaja. Amplíe un intervalo o compruebe la masa.",
361
+ "formula.lookup.tooWide": "Estos intervalos permiten demasiadas fórmulas para buscarlas. Reduzca uno.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, insaturación {unsaturation}",
348
364
  "slides.exit": "Salir",
349
365
  "slides.previousSlide": "Diapositiva anterior",
350
366
  "slides.nextSlide": "Diapositiva siguiente",
@@ -571,6 +587,8 @@
571
587
  "structure.export.saveSvg": "Guardar en SVG",
572
588
  "structure.export.savePng": "Guardar en PNG",
573
589
  "structure.export.copyPng": "Copiar el PNG",
590
+ "structure.export.copySvg": "Copiar el SVG",
591
+ "structure.export.svgCopied": "Copiado al portapapeles: SVG.",
574
592
  "structure.export.smiles": "SMILES",
575
593
  "structure.export.smilesHelp": "La estructura en una línea, con su estereoquímica. Todas las herramientas la leen.",
576
594
  "structure.export.molfileV3000": "Molfile V3000",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Apprendre les expressions régulières dans un bac à sable interactif.",
104
104
  "site.pdb.tagline": "Un coup d'œil rapide sur n'importe quelle entrée de la Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Une structure à partir d'un spectre RMN 1H et d'une formule.",
106
+ "site.spectroscopy.tagline": "Trouver une structure à partir de ses spectres IR, de masse et RMN.",
106
107
  "site.equilibrium.tagline": "Équilibres chimiques : pH, spéciation et courbes de titrage.",
107
108
  "site.polycarp.tagline": "Prédire la microstructure d'une copolymérisation radicalaire.",
108
109
  "site.3d.tagline": "Les conformères en 3D d'une structure que vous dessinez.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "« {formula} » compte {count} pour un atome ; un nombre d’atomes est entier.",
346
347
  "formula.unknownNotation": "« {formula} » utilise une notation que cet outil ne compte pas.",
347
348
  "formula.unreadable": "« {formula} » n’est pas une formule que cet outil sait lire ({reason}).",
349
+ "formula.lookup.title": "Recherche de formule",
350
+ "formula.lookup.reset": "Réinitialiser la masse et les intervalles d’éléments",
351
+ "formula.lookup.mass": "Masse monoisotopique",
352
+ "formula.lookup.massPlaceholder": "p. ex. 128.0626",
353
+ "formula.lookup.neutral": "Celle de la molécule neutre : retirez les protons que le spectre a ajoutés.",
354
+ "formula.lookup.fewest": "{element} : nombre minimal d’atomes",
355
+ "formula.lookup.most": "{element} : nombre maximal d’atomes",
356
+ "formula.lookup.typeMass": "Tapez une masse pour lister les formules que ces intervalles permettent.",
357
+ "formula.lookup.closest": "Les {count} plus proches sur {total} formules",
358
+ "formula.lookup.countOne": "{count} formule",
359
+ "formula.lookup.countMany": "{count} formules",
360
+ "formula.lookup.nothingFits": "Aucune formule ne convient. Élargissez un intervalle, ou vérifiez la masse.",
361
+ "formula.lookup.tooWide": "Ces intervalles permettent trop de formules pour les parcourir. Resserrez-en un.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, insaturation {unsaturation}",
348
364
  "slides.exit": "Quitter",
349
365
  "slides.previousSlide": "Diapositive précédente",
350
366
  "slides.nextSlide": "Diapositive suivante",
@@ -571,6 +587,8 @@
571
587
  "structure.export.saveSvg": "Enregistrer en SVG",
572
588
  "structure.export.savePng": "Enregistrer en PNG",
573
589
  "structure.export.copyPng": "Copier le PNG",
590
+ "structure.export.copySvg": "Copier le SVG",
591
+ "structure.export.svgCopied": "Copié dans le presse-papiers : SVG.",
574
592
  "structure.export.smiles": "SMILES",
575
593
  "structure.export.smilesHelp": "La structure sur une ligne, stéréochimie comprise. Tous les logiciels le lisent.",
576
594
  "structure.export.molfileV3000": "Molfile V3000",
@@ -103,6 +103,7 @@
103
103
  "site.regexp.tagline": "Imparare le espressioni regolari in un ambiente interattivo.",
104
104
  "site.pdb.tagline": "Uno sguardo rapido a qualsiasi voce della Protein Data Bank.",
105
105
  "site.elucidation.tagline": "Una struttura da uno spettro NMR 1H e da una formula.",
106
+ "site.spectroscopy.tagline": "Trovare una struttura dai suoi spettri IR, di massa e NMR.",
106
107
  "site.equilibrium.tagline": "Equilibri chimici: pH, speciazione e curve di titolazione.",
107
108
  "site.polycarp.tagline": "Prevedere la microstruttura di una copolimerizzazione radicalica.",
108
109
  "site.3d.tagline": "Conformeri in 3D a partire da una struttura che si disegna.",
@@ -345,6 +346,21 @@
345
346
  "formula.wholeCounts": "«{formula}» dà a un atomo il numero {count}; i numeri di atomi sono interi.",
346
347
  "formula.unknownNotation": "«{formula}» usa una notazione che questo strumento non conta.",
347
348
  "formula.unreadable": "«{formula}» non è una formula che questo strumento sa leggere ({reason}).",
349
+ "formula.lookup.title": "Ricerca di formule",
350
+ "formula.lookup.reset": "Reimposta la massa e gli intervalli degli elementi",
351
+ "formula.lookup.mass": "Massa monoisotopica",
352
+ "formula.lookup.massPlaceholder": "es. 128.0626",
353
+ "formula.lookup.neutral": "Quella della molecola neutra: togli i protoni che lo spettro ha aggiunto.",
354
+ "formula.lookup.fewest": "Numero minimo di atomi di {element}",
355
+ "formula.lookup.most": "Numero massimo di atomi di {element}",
356
+ "formula.lookup.typeMass": "Scrivi una massa per elencare le formule che questi intervalli consentono.",
357
+ "formula.lookup.closest": "Le {count} più vicine su {total} formule",
358
+ "formula.lookup.countOne": "{count} formula",
359
+ "formula.lookup.countMany": "{count} formule",
360
+ "formula.lookup.nothingFits": "Nessuna formula corrisponde. Allarga un intervallo o controlla la massa.",
361
+ "formula.lookup.tooWide": "Questi intervalli consentono troppe formule per la ricerca. Restringine uno.",
362
+ "formula.lookup.ppm": "{value} ppm",
363
+ "formula.lookup.detail": "{mass} Da, insaturazione {unsaturation}",
348
364
  "slides.exit": "Esci",
349
365
  "slides.previousSlide": "Diapositiva precedente",
350
366
  "slides.nextSlide": "Diapositiva successiva",
@@ -515,6 +531,8 @@
515
531
  "structure.export.saveSvg": "Salva in SVG",
516
532
  "structure.export.savePng": "Salva in PNG",
517
533
  "structure.export.copyPng": "Copia il PNG",
534
+ "structure.export.copySvg": "Copia l’SVG",
535
+ "structure.export.svgCopied": "Copiato negli appunti: SVG.",
518
536
  "structure.export.smiles": "SMILES",
519
537
  "structure.export.smilesHelp": "La struttura su una riga, stereochimica compresa. Ogni strumento la legge.",
520
538
  "structure.export.molfileV3000": "Molfile V3000",
@@ -0,0 +1,76 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+
3
+ import { OverlayLayer } from '../../overlay/ui/OverlayLayer.tsx';
4
+ import { structureSource } from '../core/structureSource.ts';
5
+
6
+ import type { StructureProps } from './Structure.tsx';
7
+ import { Structure } from './Structure.tsx';
8
+ import { StructureExportButton } from './StructureExportButton.tsx';
9
+
10
+ /** What {@link CardStructure} draws and hands over. */
11
+ export interface CardStructureProps extends StructureProps {
12
+ /**
13
+ * Base name of every file saved from the export, extension excluded.
14
+ * @default 'structure'
15
+ */
16
+ name?: string;
17
+ /**
18
+ * Whether the structure is a query fragment, such as a toxicophore, whose
19
+ * idCode is not a molecule's.
20
+ * @default false
21
+ */
22
+ fragment?: boolean;
23
+ /**
24
+ * What the glyph is called, for the pointer and for a screen reader.
25
+ * @default the chrome's own line, `Copy or save the structure: SMILES,
26
+ * molfile`
27
+ */
28
+ label?: string;
29
+ /**
30
+ * The drawing, when the site draws the structure itself — its own
31
+ * highlights, its own numbering. The export still reads the notations.
32
+ * @default a `Structure` drawn from the same notations, at the same size
33
+ */
34
+ children?: ReactNode;
35
+ }
36
+
37
+ /**
38
+ * A structure of a card, with the glyph that hands it over: the export
39
+ * dialog, which copies it as an SVG or a PNG, a SMILES or a molfile, V2000 or
40
+ * V3000. Every structure an answer draws carries it, alone or in a grid, so
41
+ * none has to be redrawn to be reused.
42
+ *
43
+ * The glyph stands beside the top right corner, faded until the structure is
44
+ * pointed at, as the glyphs of `CardFigure` (`react-cheminfo/card`) do;
45
+ * `styles/card.css` draws both. A structure with nothing to read draws its
46
+ * placeholder alone.
47
+ * @param props - The structure, its size, and the name of its files.
48
+ * @returns The structure and its glyph.
49
+ */
50
+ export function CardStructure(props: CardStructureProps): ReactElement {
51
+ const { name, fragment, label, children, ...structure } = props;
52
+ const { idCode, coordinates, molfile, smiles } = structure;
53
+ const drawing = children ?? <Structure {...structure} />;
54
+ const source = structureSource({ idCode, coordinates, molfile, smiles });
55
+ if (source.kind === 'empty') return <>{drawing}</>;
56
+
57
+ return (
58
+ <div className="card-figure card-structure">
59
+ <div className="card-figure__drawing">{drawing}</div>
60
+ {/* Compact: the glyphs of a figure shown in somebody else's page. */}
61
+ <OverlayLayer density="compact" awake>
62
+ <div className="card-figure__tools" data-testid="figure-tools">
63
+ <StructureExportButton
64
+ idCode={idCode}
65
+ coordinates={coordinates}
66
+ molfile={molfile}
67
+ smiles={smiles}
68
+ name={name}
69
+ fragment={fragment}
70
+ label={label}
71
+ />
72
+ </div>
73
+ </OverlayLayer>
74
+ </div>
75
+ );
76
+ }
@@ -4,6 +4,7 @@ import type { CSSProperties, ReactElement } from 'react';
4
4
  import { useMemo, useState } from 'react';
5
5
 
6
6
  import { copyFigure } from '../../download/core/copyFigure.ts';
7
+ import { copySvg } from '../../download/core/copySvg.ts';
7
8
  import { downloadBlob } from '../../download/core/downloadBlob.ts';
8
9
  import { downloadText } from '../../download/core/downloadText.ts';
9
10
  import { figurePng } from '../../download/core/figurePng.ts';
@@ -37,7 +38,7 @@ const DEFAULT_SCALE = 2;
37
38
 
38
39
  /**
39
40
  * The structure as a picture: what it looks like, how big it leaves, and the
40
- * three ways it leaves.
41
+ * four ways it leaves.
41
42
  *
42
43
  * What is shown is the file: the preview is the very SVG the save button
43
44
  * writes, laid out smaller, so nobody saves a picture they have not seen. The
@@ -136,6 +137,18 @@ export function StructurePicturePane(
136
137
  })
137
138
  }
138
139
  />
140
+ <Button
141
+ icon="clipboard"
142
+ size="small"
143
+ text={t('structure.export.copySvg')}
144
+ onClick={() =>
145
+ void run(async () =>
146
+ (await copySvg(picture.markup))
147
+ ? t('structure.export.svgCopied')
148
+ : t('clipboard.failed'),
149
+ )
150
+ }
151
+ />
139
152
  <Button
140
153
  icon="clipboard"
141
154
  size="small"
@@ -33,6 +33,8 @@ export type { StructureExportDialogProps } from './StructureExportDialog.tsx';
33
33
  export { StructureExportDialog } from './StructureExportDialog.tsx';
34
34
  export type { StructureExportButtonProps } from './StructureExportButton.tsx';
35
35
  export { StructureExportButton } from './StructureExportButton.tsx';
36
+ export type { CardStructureProps } from './CardStructure.tsx';
37
+ export { CardStructure } from './CardStructure.tsx';
36
38
  export type { StructureExportPanelProps } from './StructureExportPanel.tsx';
37
39
  export type { ToolbarFloorOptions } from './useToolbarFloor.ts';
38
40
  export { useToolbarFloor } from './useToolbarFloor.ts';
@@ -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
- // offsetHeight, because a dialog opens under a scaling transform and a
46
- // measured rectangle would be that of the half drawn toolbar.
47
- const height = toolbar.offsetHeight;
48
- // Zero is a toolbar being torn down or not yet laid out, never a
49
- // toolbar that needs no room: writing the fallback for it would undo a
50
- // correct floor and leave the palette cut off.
51
- if (height === 0) return;
52
- container.style.minHeight = `${toolbarFloorHeight(height, minHeight)}px`;
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 () => size.disconnect();
62
+ return () => {
63
+ cancelAnimationFrame(frame);
64
+ size.disconnect();
65
+ };
56
66
  });
57
67
  }, [minHeight, revision]);
58
68