react-cheminfo 0.23.0 → 0.25.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 +39 -26
- package/lib/chrome/ui/HeaderToggle.d.ts +52 -0
- package/lib/chrome/ui/HeaderToggle.d.ts.map +1 -0
- package/lib/chrome/ui/HeaderToggle.js +31 -0
- package/lib/chrome/ui/HeaderToggle.js.map +1 -0
- package/lib/chrome/ui/index.d.ts +2 -0
- package/lib/chrome/ui/index.d.ts.map +1 -1
- package/lib/chrome/ui/index.js +1 -0
- package/lib/chrome/ui/index.js.map +1 -1
- package/lib/citation/core/gfn2Paper.d.ts +10 -0
- package/lib/citation/core/gfn2Paper.d.ts.map +1 -0
- package/lib/citation/core/gfn2Paper.js +28 -0
- package/lib/citation/core/gfn2Paper.js.map +1 -0
- package/lib/citation/core/index.d.ts +1 -0
- package/lib/citation/core/index.d.ts.map +1 -1
- package/lib/citation/core/index.js +1 -0
- package/lib/citation/core/index.js.map +1 -1
- package/lib/clipboard/ui/ClickToCopy.d.ts +8 -7
- package/lib/clipboard/ui/ClickToCopy.d.ts.map +1 -1
- package/lib/clipboard/ui/ClickToCopy.js +3 -3
- package/lib/clipboard/ui/ClickToCopy.js.map +1 -1
- package/lib/conformers.d.ts +27 -0
- package/lib/conformers.d.ts.map +1 -0
- package/lib/conformers.js +19 -0
- package/lib/conformers.js.map +1 -0
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/credits/core/credits.d.ts +12 -0
- package/lib/credits/core/credits.d.ts.map +1 -1
- package/lib/credits/core/credits.js +14 -0
- package/lib/credits/core/credits.js.map +1 -1
- package/lib/ecosystem/core/index.d.ts +1 -1
- package/lib/ecosystem/core/index.d.ts.map +1 -1
- package/lib/ecosystem/core/index.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +11 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +11 -4
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/ui/EcosystemLinks.d.ts.map +1 -1
- package/lib/ecosystem/ui/EcosystemLinks.js +3 -1
- package/lib/ecosystem/ui/EcosystemLinks.js.map +1 -1
- package/lib/ecosystem/ui/SiteTile.d.ts.map +1 -1
- package/lib/ecosystem/ui/SiteTile.js +3 -1
- package/lib/ecosystem/ui/SiteTile.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +6 -5
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/format/core/index.d.ts +1 -1
- package/lib/format/core/index.d.ts.map +1 -1
- package/lib/format/core/index.js +1 -1
- package/lib/format/core/index.js.map +1 -1
- package/lib/format/core/words.d.ts +9 -0
- package/lib/format/core/words.d.ts.map +1 -1
- package/lib/format/core/words.js +16 -0
- package/lib/format/core/words.js.map +1 -1
- package/lib/language/core/index.d.ts +3 -0
- package/lib/language/core/index.d.ts.map +1 -0
- package/lib/language/core/index.js +3 -0
- package/lib/language/core/index.js.map +1 -0
- package/lib/language/core/languageName.d.ts +8 -0
- package/lib/language/core/languageName.d.ts.map +1 -0
- package/lib/language/core/languageName.js +16 -0
- package/lib/language/core/languageName.js.map +1 -0
- package/lib/language/core/languageParam.d.ts +35 -0
- package/lib/language/core/languageParam.d.ts.map +1 -0
- package/lib/language/core/languageParam.js +67 -0
- package/lib/language/core/languageParam.js.map +1 -0
- package/lib/language/ui/SiteLanguage.d.ts +20 -0
- package/lib/language/ui/SiteLanguage.d.ts.map +1 -0
- package/lib/language/ui/SiteLanguage.js +14 -0
- package/lib/language/ui/SiteLanguage.js.map +1 -0
- package/lib/language/ui/index.d.ts +4 -0
- package/lib/language/ui/index.d.ts.map +1 -0
- package/lib/language/ui/index.js +3 -0
- package/lib/language/ui/index.js.map +1 -0
- package/lib/language/ui/siteLanguageContext.d.ts +12 -0
- package/lib/language/ui/siteLanguageContext.d.ts.map +1 -0
- package/lib/language/ui/siteLanguageContext.js +19 -0
- package/lib/language/ui/siteLanguageContext.js.map +1 -0
- package/lib/orbital/core/atomicOrbitals.d.ts +9 -1
- package/lib/orbital/core/atomicOrbitals.d.ts.map +1 -1
- package/lib/orbital/core/atomicOrbitals.js +7 -0
- package/lib/orbital/core/atomicOrbitals.js.map +1 -1
- package/lib/orbital/core/hydrogenic.d.ts +2 -1
- package/lib/orbital/core/hydrogenic.d.ts.map +1 -1
- package/lib/orbital/core/hydrogenic.js +2 -1
- package/lib/orbital/core/hydrogenic.js.map +1 -1
- package/lib/orbital/core/index.d.ts +1 -0
- package/lib/orbital/core/index.d.ts.map +1 -1
- package/lib/orbital/core/index.js +1 -0
- package/lib/orbital/core/index.js.map +1 -1
- package/lib/orbital/core/slaterEnergy.d.ts +35 -0
- package/lib/orbital/core/slaterEnergy.d.ts.map +1 -0
- package/lib/orbital/core/slaterEnergy.js +66 -0
- package/lib/orbital/core/slaterEnergy.js.map +1 -0
- package/lib/shared/ui/MenuButton.d.ts +3 -2
- package/lib/shared/ui/MenuButton.d.ts.map +1 -1
- package/lib/shared/ui/MenuButton.js +1 -1
- package/lib/shared/ui/MenuButton.js.map +1 -1
- package/lib/structure/core/conformerMinimise.d.ts +36 -0
- package/lib/structure/core/conformerMinimise.d.ts.map +1 -0
- package/lib/structure/core/conformerMinimise.js +81 -0
- package/lib/structure/core/conformerMinimise.js.map +1 -0
- package/lib/structure/core/conformerMinimum.d.ts +62 -0
- package/lib/structure/core/conformerMinimum.d.ts.map +1 -0
- package/lib/structure/core/conformerMinimum.js +90 -0
- package/lib/structure/core/conformerMinimum.js.map +1 -0
- package/lib/structure/core/conformerOptions.d.ts +62 -0
- package/lib/structure/core/conformerOptions.d.ts.map +1 -0
- package/lib/structure/core/conformerOptions.js +83 -0
- package/lib/structure/core/conformerOptions.js.map +1 -0
- package/lib/structure/core/conformerRefine.d.ts +104 -0
- package/lib/structure/core/conformerRefine.d.ts.map +1 -0
- package/lib/structure/core/conformerRefine.js +130 -0
- package/lib/structure/core/conformerRefine.js.map +1 -0
- package/lib/structure/core/conformerRefineRank.d.ts +36 -0
- package/lib/structure/core/conformerRefineRank.d.ts.map +1 -0
- package/lib/structure/core/conformerRefineRank.js +69 -0
- package/lib/structure/core/conformerRefineRank.js.map +1 -0
- package/lib/structure/core/conformerSession.d.ts +50 -0
- package/lib/structure/core/conformerSession.d.ts.map +1 -0
- package/lib/structure/core/conformerSession.js +88 -0
- package/lib/structure/core/conformerSession.js.map +1 -0
- package/lib/structure/core/conformerShape.d.ts +36 -0
- package/lib/structure/core/conformerShape.d.ts.map +1 -0
- package/lib/structure/core/conformerShape.js +150 -0
- package/lib/structure/core/conformerShape.js.map +1 -0
- package/lib/structure/core/conformers.d.ts +107 -0
- package/lib/structure/core/conformers.d.ts.map +1 -0
- package/lib/structure/core/conformers.js +149 -0
- package/lib/structure/core/conformers.js.map +1 -0
- package/lib/structure/core/geometryRelaxer.d.ts +66 -0
- package/lib/structure/core/geometryRelaxer.d.ts.map +1 -0
- package/lib/structure/core/geometryRelaxer.js +2 -0
- package/lib/structure/core/geometryRelaxer.js.map +1 -0
- package/lib/structure/core/index.d.ts +4 -0
- package/lib/structure/core/index.d.ts.map +1 -1
- package/lib/structure/core/index.js +2 -0
- package/lib/structure/core/index.js.map +1 -1
- package/lib/structure/core/moleculeCoordinates.d.ts +31 -0
- package/lib/structure/core/moleculeCoordinates.d.ts.map +1 -0
- package/lib/structure/core/moleculeCoordinates.js +80 -0
- package/lib/structure/core/moleculeCoordinates.js.map +1 -0
- package/lib/structure/core/molfileExport.d.ts +42 -0
- package/lib/structure/core/molfileExport.d.ts.map +1 -0
- package/lib/structure/core/molfileExport.js +47 -0
- package/lib/structure/core/molfileExport.js.map +1 -0
- package/lib/structure/core/oclResources.d.ts +12 -0
- package/lib/structure/core/oclResources.d.ts.map +1 -0
- package/lib/structure/core/oclResources.js +35 -0
- package/lib/structure/core/oclResources.js.map +1 -0
- package/lib/translate/core/catalog.d.ts +61 -0
- package/lib/translate/core/catalog.d.ts.map +1 -0
- package/lib/translate/core/catalog.js +49 -0
- package/lib/translate/core/catalog.js.map +1 -0
- package/lib/translate/core/checkTranslation.d.ts +32 -0
- package/lib/translate/core/checkTranslation.d.ts.map +1 -0
- package/lib/translate/core/checkTranslation.js +98 -0
- package/lib/translate/core/checkTranslation.js.map +1 -0
- package/lib/translate/core/contribution.d.ts +88 -0
- package/lib/translate/core/contribution.d.ts.map +1 -0
- package/lib/translate/core/contribution.js +14 -0
- package/lib/translate/core/contribution.js.map +1 -0
- package/lib/translate/core/index.d.ts +19 -0
- package/lib/translate/core/index.d.ts.map +1 -0
- package/lib/translate/core/index.js +15 -0
- package/lib/translate/core/index.js.map +1 -0
- package/lib/translate/core/marker.d.ts +45 -0
- package/lib/translate/core/marker.d.ts.map +1 -0
- package/lib/translate/core/marker.js +79 -0
- package/lib/translate/core/marker.js.map +1 -0
- package/lib/translate/core/mergeMessages.d.ts +35 -0
- package/lib/translate/core/mergeMessages.d.ts.map +1 -0
- package/lib/translate/core/mergeMessages.js +69 -0
- package/lib/translate/core/mergeMessages.js.map +1 -0
- package/lib/translate/core/session.d.ts +102 -0
- package/lib/translate/core/session.d.ts.map +1 -0
- package/lib/translate/core/session.js +162 -0
- package/lib/translate/core/session.js.map +1 -0
- package/lib/translate/core/start.d.ts +103 -0
- package/lib/translate/core/start.d.ts.map +1 -0
- package/lib/translate/core/start.js +92 -0
- package/lib/translate/core/start.js.map +1 -0
- package/lib/translate/core/suggestions.d.ts +77 -0
- package/lib/translate/core/suggestions.d.ts.map +1 -0
- package/lib/translate/core/suggestions.js +143 -0
- package/lib/translate/core/suggestions.js.map +1 -0
- package/lib/translate/core/tables.d.ts +100 -0
- package/lib/translate/core/tables.d.ts.map +1 -0
- package/lib/translate/core/tables.js +51 -0
- package/lib/translate/core/tables.js.map +1 -0
- package/lib/translate.d.ts +2 -0
- package/lib/translate.d.ts.map +1 -0
- package/lib/translate.js +6 -0
- package/lib/translate.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/lib/xtb/core/xtbRelaxer.d.ts +61 -0
- package/lib/xtb/core/xtbRelaxer.d.ts.map +1 -0
- package/lib/xtb/core/xtbRelaxer.js +108 -0
- package/lib/xtb/core/xtbRelaxer.js.map +1 -0
- package/lib/xtb.d.ts +11 -0
- package/lib/xtb.d.ts.map +1 -0
- package/lib/xtb.js +10 -0
- package/lib/xtb.js.map +1 -0
- package/package.json +18 -11
- package/src/chrome/ui/HeaderToggle.tsx +100 -0
- package/src/chrome/ui/index.ts +2 -0
- package/src/citation/core/gfn2Paper.ts +32 -0
- package/src/citation/core/index.ts +1 -0
- package/src/clipboard/ui/ClickToCopy.tsx +8 -7
- package/src/conformers.ts +87 -0
- package/src/core.ts +1 -0
- package/src/credits/core/credits.ts +16 -0
- package/src/ecosystem/core/index.ts +1 -0
- package/src/ecosystem/core/sites.ts +24 -4
- package/src/ecosystem/ui/EcosystemLinks.tsx +3 -1
- package/src/ecosystem/ui/SiteTile.tsx +3 -1
- package/src/ecosystem/ui/glyphs.tsx +16 -22
- package/src/format/core/index.ts +1 -1
- package/src/format/core/words.ts +19 -0
- package/src/language/core/index.ts +7 -0
- package/src/language/core/languageName.ts +16 -0
- package/src/language/core/languageParam.ts +70 -0
- package/src/language/ui/SiteLanguage.tsx +26 -0
- package/src/language/ui/index.ts +3 -0
- package/src/language/ui/siteLanguageContext.ts +20 -0
- package/src/orbital/core/atomicOrbitals.ts +17 -1
- package/src/orbital/core/hydrogenic.ts +2 -1
- package/src/orbital/core/index.ts +1 -0
- package/src/orbital/core/slaterEnergy.ts +83 -0
- package/src/shared/ui/MenuButton.tsx +4 -3
- package/src/structure/core/conformerMinimise.ts +99 -0
- package/src/structure/core/conformerMinimum.ts +125 -0
- package/src/structure/core/conformerOptions.ts +125 -0
- package/src/structure/core/conformerRefine.ts +234 -0
- package/src/structure/core/conformerRefineRank.ts +89 -0
- package/src/structure/core/conformerSession.ts +123 -0
- package/src/structure/core/conformerShape.ts +181 -0
- package/src/structure/core/conformers.ts +282 -0
- package/src/structure/core/geometryRelaxer.ts +71 -0
- package/src/structure/core/index.ts +13 -0
- package/src/structure/core/moleculeCoordinates.ts +93 -0
- package/src/structure/core/molfileExport.ts +62 -0
- package/src/structure/core/oclResources.ts +41 -0
- package/src/translate/core/catalog.ts +83 -0
- package/src/translate/core/checkTranslation.ts +128 -0
- package/src/translate/core/contribution.ts +99 -0
- package/src/translate/core/index.ts +72 -0
- package/src/translate/core/marker.ts +85 -0
- package/src/translate/core/mergeMessages.ts +74 -0
- package/src/translate/core/session.ts +250 -0
- package/src/translate/core/start.ts +183 -0
- package/src/translate/core/suggestions.ts +211 -0
- package/src/translate/core/tables.ts +120 -0
- package/src/translate.ts +5 -0
- package/src/ui.ts +1 -0
- package/src/xtb/core/xtbRelaxer.ts +160 -0
- package/src/xtb.ts +16 -0
- package/styles/chrome.css +61 -30
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a translator is offered before typing: the translations already made
|
|
3
|
+
* for the same English, for English that differs only where the text is not
|
|
4
|
+
* language, and for English that merely looks like it.
|
|
5
|
+
*
|
|
6
|
+
* A table is where this earns its place. Half of 118 element names are the
|
|
7
|
+
* same word in two languages and the other half differ by an ending; a column
|
|
8
|
+
* of quantities is a handful of phrases repeated with one word changed. A
|
|
9
|
+
* translator who is shown what was done three rows above confirms instead of
|
|
10
|
+
* typing, and the column stays consistent with itself — which is the thing a
|
|
11
|
+
* long table loses first.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here invents a translation. Every suggestion is a translation
|
|
14
|
+
* somebody already wrote, either as it stands or with the part that is not
|
|
15
|
+
* language put back.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import leven from 'leven';
|
|
19
|
+
|
|
20
|
+
/** One message already translated, which a suggestion is drawn from. */
|
|
21
|
+
export interface TranslatedPair {
|
|
22
|
+
/** The key it is kept under, so the translator can see where it came from. */
|
|
23
|
+
key: string;
|
|
24
|
+
/** The English message. */
|
|
25
|
+
source: string;
|
|
26
|
+
/** Its translation. */
|
|
27
|
+
target: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How a suggestion was arrived at.
|
|
32
|
+
*
|
|
33
|
+
* - `exact` — the same English message, translated elsewhere.
|
|
34
|
+
* - `pattern` — English that differs only in a part that is not language (a
|
|
35
|
+
* number, a symbol, a formula), with that part put back into the
|
|
36
|
+
* translation.
|
|
37
|
+
* - `similar` — English that looks like this one; the translation is offered
|
|
38
|
+
* to be adapted, not to be taken as it is.
|
|
39
|
+
*/
|
|
40
|
+
export type SuggestionKind = 'exact' | 'pattern' | 'similar';
|
|
41
|
+
|
|
42
|
+
/** One translation offered for a message. */
|
|
43
|
+
export interface MessageSuggestion {
|
|
44
|
+
/** What goes in the box if the translator takes it. */
|
|
45
|
+
message: string;
|
|
46
|
+
/** How it was arrived at. */
|
|
47
|
+
kind: SuggestionKind;
|
|
48
|
+
/** Between 0 and 1; 1 is the same English message. */
|
|
49
|
+
score: number;
|
|
50
|
+
/** The already-translated message it was drawn from. */
|
|
51
|
+
from: TranslatedPair;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How suggestions are chosen. */
|
|
55
|
+
export interface SuggestionOptions {
|
|
56
|
+
/**
|
|
57
|
+
* The most suggestions to return.
|
|
58
|
+
* @default 5
|
|
59
|
+
*/
|
|
60
|
+
limit?: number;
|
|
61
|
+
/**
|
|
62
|
+
* How alike two English messages must be before one is offered for the
|
|
63
|
+
* other, between 0 and 1.
|
|
64
|
+
* @default 0.55
|
|
65
|
+
*/
|
|
66
|
+
minimumScore?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DEFAULT_LIMIT = 5;
|
|
70
|
+
const DEFAULT_MINIMUM = 0.55;
|
|
71
|
+
const KIND_ORDER: Record<SuggestionKind, number> = {
|
|
72
|
+
exact: 0,
|
|
73
|
+
pattern: 1,
|
|
74
|
+
similar: 2,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The translations worth offering for one English message.
|
|
79
|
+
* @param source - The English message being translated.
|
|
80
|
+
* @param pairs - Every message already translated in this language.
|
|
81
|
+
* @param options - See {@link SuggestionOptions}.
|
|
82
|
+
* @returns The suggestions, best first; empty when nothing is close enough.
|
|
83
|
+
*/
|
|
84
|
+
export function messageSuggestions(
|
|
85
|
+
source: string,
|
|
86
|
+
pairs: readonly TranslatedPair[],
|
|
87
|
+
options: SuggestionOptions = {},
|
|
88
|
+
): MessageSuggestion[] {
|
|
89
|
+
const { limit = DEFAULT_LIMIT, minimumScore = DEFAULT_MINIMUM } = options;
|
|
90
|
+
if (source === '' || limit <= 0) return [];
|
|
91
|
+
|
|
92
|
+
const found: MessageSuggestion[] = [];
|
|
93
|
+
for (const pair of pairs) {
|
|
94
|
+
if (pair.target === '' || pair.source === '') continue;
|
|
95
|
+
const suggestion = suggestFrom(source, pair, minimumScore);
|
|
96
|
+
if (suggestion !== undefined) found.push(suggestion);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
found.sort(compareSuggestions);
|
|
100
|
+
const best: MessageSuggestion[] = [];
|
|
101
|
+
const taken = new Set<string>();
|
|
102
|
+
for (const suggestion of found) {
|
|
103
|
+
if (taken.has(suggestion.message)) continue;
|
|
104
|
+
taken.add(suggestion.message);
|
|
105
|
+
best.push(suggestion);
|
|
106
|
+
if (best.length === limit) break;
|
|
107
|
+
}
|
|
108
|
+
return best;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How alike two messages are, as the suggestions rank them.
|
|
113
|
+
* @param first - One message.
|
|
114
|
+
* @param second - The other.
|
|
115
|
+
* @returns 1 when they are the same, 0 when they share nothing.
|
|
116
|
+
*/
|
|
117
|
+
export function messageSimilarity(first: string, second: string): number {
|
|
118
|
+
const longest = Math.max(first.length, second.length);
|
|
119
|
+
if (longest === 0) return 1;
|
|
120
|
+
return 1 - leven(first, second) / longest;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The best offer one already-translated message can make for another.
|
|
125
|
+
* @param source - The English being translated.
|
|
126
|
+
* @param pair - The already-translated message to draw from.
|
|
127
|
+
* @param minimumScore - How alike the two must be to be worth offering.
|
|
128
|
+
* @returns The suggestion, or `undefined` when the pair is too far away.
|
|
129
|
+
*/
|
|
130
|
+
function suggestFrom(
|
|
131
|
+
source: string,
|
|
132
|
+
pair: TranslatedPair,
|
|
133
|
+
minimumScore: number,
|
|
134
|
+
): MessageSuggestion | undefined {
|
|
135
|
+
if (pair.source === source) {
|
|
136
|
+
return { message: pair.target, kind: 'exact', score: 1, from: pair };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// An edit can never be smaller than the difference in length, so a pair that
|
|
140
|
+
// cannot reach the threshold is dropped before the distance is computed.
|
|
141
|
+
const longest = Math.max(source.length, pair.source.length);
|
|
142
|
+
const shortest = Math.min(source.length, pair.source.length);
|
|
143
|
+
if (longest === 0 || shortest / longest < minimumScore) return undefined;
|
|
144
|
+
|
|
145
|
+
const pattern = patternSuggestion(source, pair);
|
|
146
|
+
if (pattern !== undefined && pattern.score >= minimumScore) return pattern;
|
|
147
|
+
|
|
148
|
+
const score = messageSimilarity(source, pair.source);
|
|
149
|
+
if (score < minimumScore) return undefined;
|
|
150
|
+
return { message: pair.target, kind: 'similar', score, from: pair };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A translation whose English differs from this one only in a run of text
|
|
155
|
+
* that survives into the translation unchanged — a number, a symbol, a
|
|
156
|
+
* formula. That run is the only thing put back, so nothing is invented.
|
|
157
|
+
* @param source - The English being translated.
|
|
158
|
+
* @param pair - The already-translated message to draw from.
|
|
159
|
+
* @returns The suggestion, or `undefined` when the run cannot be put back.
|
|
160
|
+
*/
|
|
161
|
+
function patternSuggestion(
|
|
162
|
+
source: string,
|
|
163
|
+
pair: TranslatedPair,
|
|
164
|
+
): MessageSuggestion | undefined {
|
|
165
|
+
const prefix = commonPrefix(source, pair.source);
|
|
166
|
+
const suffix = commonSuffix(source.slice(prefix), pair.source.slice(prefix));
|
|
167
|
+
const own = source.slice(prefix, source.length - suffix);
|
|
168
|
+
const theirs = pair.source.slice(prefix, pair.source.length - suffix);
|
|
169
|
+
if (theirs === '') return undefined;
|
|
170
|
+
|
|
171
|
+
// The varying run must appear once and only once, or putting it back would
|
|
172
|
+
// be a guess about which occurrence was meant.
|
|
173
|
+
const at = pair.target.indexOf(theirs);
|
|
174
|
+
if (at === -1 || pair.target.includes(theirs, at + 1)) return undefined;
|
|
175
|
+
|
|
176
|
+
const message =
|
|
177
|
+
pair.target.slice(0, at) + own + pair.target.slice(at + theirs.length);
|
|
178
|
+
const kept = prefix + suffix;
|
|
179
|
+
const score = kept / Math.max(source.length, pair.source.length);
|
|
180
|
+
return { message, kind: 'pattern', score, from: pair };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function commonPrefix(first: string, second: string): number {
|
|
184
|
+
const limit = Math.min(first.length, second.length);
|
|
185
|
+
let length = 0;
|
|
186
|
+
while (length < limit && first[length] === second[length]) length++;
|
|
187
|
+
return length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function commonSuffix(first: string, second: string): number {
|
|
191
|
+
const limit = Math.min(first.length, second.length);
|
|
192
|
+
let length = 0;
|
|
193
|
+
while (
|
|
194
|
+
length < limit &&
|
|
195
|
+
first[first.length - 1 - length] === second[second.length - 1 - length]
|
|
196
|
+
) {
|
|
197
|
+
length++;
|
|
198
|
+
}
|
|
199
|
+
return length;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function compareSuggestions(
|
|
203
|
+
first: MessageSuggestion,
|
|
204
|
+
second: MessageSuggestion,
|
|
205
|
+
): number {
|
|
206
|
+
if (first.kind !== second.kind) {
|
|
207
|
+
return KIND_ORDER[first.kind] - KIND_ORDER[second.kind];
|
|
208
|
+
}
|
|
209
|
+
if (first.score !== second.score) return second.score - first.score;
|
|
210
|
+
return first.from.key < second.from.key ? -1 : 1;
|
|
211
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tables of a site whose rows are translated: the element sheet, a list of
|
|
3
|
+
* ions, the rules of a nomenclature.
|
|
4
|
+
*
|
|
5
|
+
* A site's prose is not only its interface. Most of what a chemistry tool
|
|
6
|
+
* shows is a table, and a table mixes two kinds of column: what was measured
|
|
7
|
+
* or computed — an atomic number, a mass, a formula, an identifier — and what
|
|
8
|
+
* was written — a name, where the name comes from, a note. The first kind is
|
|
9
|
+
* the same in every language and must never reach a translator; the second is
|
|
10
|
+
* exactly what a translator is for.
|
|
11
|
+
*
|
|
12
|
+
* The line between them is drawn once, here. A site declares its tables, and
|
|
13
|
+
* only a field it declares can be edited, in a catalog key built the same way
|
|
14
|
+
* everywhere:
|
|
15
|
+
*
|
|
16
|
+
* ```text
|
|
17
|
+
* <table>.<row>.<field> element.Fe.name, element.Fe.origin
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Everything else about the row — the mass, the density, the id itself — stays
|
|
21
|
+
* in the data the site generates from its source, out of reach.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** How much room a field's text needs, which is how the editor draws it. */
|
|
25
|
+
export type TranslatableFieldSize = 'line' | 'paragraph';
|
|
26
|
+
|
|
27
|
+
/** One translated column of a table. */
|
|
28
|
+
export interface TranslatableField {
|
|
29
|
+
/** Last part of the key, e.g. `name`. */
|
|
30
|
+
id: string;
|
|
31
|
+
/** What the column is called in the editor, in English. */
|
|
32
|
+
label: string;
|
|
33
|
+
/**
|
|
34
|
+
* How much text it holds, which is whether the editor gives it one line or
|
|
35
|
+
* a box.
|
|
36
|
+
* @default 'line'
|
|
37
|
+
*/
|
|
38
|
+
size?: TranslatableFieldSize;
|
|
39
|
+
/**
|
|
40
|
+
* Whether every row is expected to carry it, so the editor can say what is
|
|
41
|
+
* still missing rather than only what is there.
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
required?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* One sentence telling the translator what the column is, shown above the
|
|
47
|
+
* column in the editor.
|
|
48
|
+
* @default undefined
|
|
49
|
+
*/
|
|
50
|
+
hint?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One row of a table. */
|
|
54
|
+
export interface TranslatableRow {
|
|
55
|
+
/** Middle part of the key, e.g. `Fe`. */
|
|
56
|
+
id: string;
|
|
57
|
+
/**
|
|
58
|
+
* What names the row in the editor — an atomic number and a symbol, a
|
|
59
|
+
* formula. It is data, never translated, and never a key.
|
|
60
|
+
*/
|
|
61
|
+
label: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A table whose rows carry translated text. */
|
|
65
|
+
export interface TranslatableTable {
|
|
66
|
+
/** First part of the key, e.g. `element`. */
|
|
67
|
+
id: string;
|
|
68
|
+
/** What the table is called in the editor, in English. */
|
|
69
|
+
label: string;
|
|
70
|
+
/** The catalog its keys belong to, as the page registered it. */
|
|
71
|
+
catalogId: string;
|
|
72
|
+
/** The columns a translator may write, in the order the editor shows them. */
|
|
73
|
+
fields: readonly TranslatableField[];
|
|
74
|
+
/** The rows, in the order the editor lists them. */
|
|
75
|
+
rows: readonly TranslatableRow[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The key one field of one row is kept under.
|
|
80
|
+
* @param table - The table the row belongs to.
|
|
81
|
+
* @param rowId - The row.
|
|
82
|
+
* @param fieldId - The column.
|
|
83
|
+
* @returns The catalog key, e.g. `element.Fe.name`.
|
|
84
|
+
*/
|
|
85
|
+
export function tableKey(
|
|
86
|
+
table: Pick<TranslatableTable, 'id'>,
|
|
87
|
+
rowId: string,
|
|
88
|
+
fieldId: string,
|
|
89
|
+
): string {
|
|
90
|
+
return `${table.id}.${rowId}.${fieldId}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Every key a table can hold, row by row and field by field.
|
|
95
|
+
*
|
|
96
|
+
* It is what the editor walks to draw the grid, and what tells it which cells
|
|
97
|
+
* are still empty: a key absent from the catalog is a translation nobody has
|
|
98
|
+
* written yet, not a field that does not exist.
|
|
99
|
+
* @param table - The table to walk.
|
|
100
|
+
* @returns One entry per cell, in reading order.
|
|
101
|
+
*/
|
|
102
|
+
export function tableCells(table: TranslatableTable): TableCell[] {
|
|
103
|
+
const cells: TableCell[] = [];
|
|
104
|
+
for (const row of table.rows) {
|
|
105
|
+
for (const field of table.fields) {
|
|
106
|
+
cells.push({ key: tableKey(table, row.id, field.id), row, field });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return cells;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One cell of a table: where it is, and what it is kept under. */
|
|
113
|
+
export interface TableCell {
|
|
114
|
+
/** The catalog key. */
|
|
115
|
+
key: string;
|
|
116
|
+
/** The row it belongs to. */
|
|
117
|
+
row: TranslatableRow;
|
|
118
|
+
/** The column it belongs to. */
|
|
119
|
+
field: TranslatableField;
|
|
120
|
+
}
|
package/src/translate.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Everything a page, an overlay or a server needs to translate a site of the
|
|
2
|
+
// family: the catalogs, the invisible marker, the checks, and the session a
|
|
3
|
+
// page in translate mode hands to the overlay. It is its own entry point
|
|
4
|
+
// because an ordinary visit loads none of it.
|
|
5
|
+
export * from './translate/core/index.ts';
|
package/src/ui.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './ecosystem/ui/index.ts';
|
|
|
14
14
|
export * from './error/ui/index.ts';
|
|
15
15
|
export * from './help/ui/index.ts';
|
|
16
16
|
export * from './hooks/ui/index.ts';
|
|
17
|
+
export * from './language/ui/index.ts';
|
|
17
18
|
export * from './overlay/ui/index.ts';
|
|
18
19
|
export * from './parallel/ui/index.ts';
|
|
19
20
|
export * from './pedagogy/ui/index.ts';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type * as XtbWasm from 'xtb-wasm';
|
|
2
|
+
|
|
3
|
+
import { errorMessage } from '../../error/core/toError.ts';
|
|
4
|
+
import type {
|
|
5
|
+
GeometryRelaxer,
|
|
6
|
+
RelaxableGeometry,
|
|
7
|
+
RelaxedGeometry,
|
|
8
|
+
RelaxerOptions,
|
|
9
|
+
} from '../../structure/core/geometryRelaxer.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* kcal/mol per hartree, CODATA 2018. Every energy a relaxer reports is in
|
|
13
|
+
* kcal/mol, so it can be read against a force-field energy without the reader
|
|
14
|
+
* converting anything.
|
|
15
|
+
*/
|
|
16
|
+
export const KCAL_PER_MOL_PER_HARTREE = 627.509_474_063_1;
|
|
17
|
+
|
|
18
|
+
/** How many occjs instances a relaxation may use. */
|
|
19
|
+
export interface XtbRelaxerOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Hard cap on the number of GFN2 instances. Each one costs about 100 MB of
|
|
22
|
+
* WebAssembly memory, so a long batch on a small machine is worth capping.
|
|
23
|
+
* @default one per structure, one below the core count
|
|
24
|
+
*/
|
|
25
|
+
maxWorkers?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Total molecular charge in units of e, applied to every structure.
|
|
28
|
+
* @default 0
|
|
29
|
+
*/
|
|
30
|
+
charge?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Unpaired electrons, applied to every structure. `1` for a doublet radical.
|
|
33
|
+
* @default 0
|
|
34
|
+
*/
|
|
35
|
+
unpairedElectrons?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Cycle budget of one optimization.
|
|
38
|
+
* @default 250
|
|
39
|
+
*/
|
|
40
|
+
maxCycles?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* GFN2-xTB as a {@link GeometryRelaxer}: the second stage that decides how good
|
|
45
|
+
* a force field's conformers really are.
|
|
46
|
+
*
|
|
47
|
+
* `xtb-wasm` is loaded on the **first call**, never on the import, so a page that
|
|
48
|
+
* offers refinement as an action downloads the 21.5 MB WebAssembly only once a
|
|
49
|
+
* visitor asks for it. The module is cached afterwards, and the worker pool it
|
|
50
|
+
* starts is kept alive between calls.
|
|
51
|
+
*
|
|
52
|
+
* Energies come back in kcal/mol, which is the unit `conformer.energy` is
|
|
53
|
+
* already in.
|
|
54
|
+
* @param options - Instance cap, charge, spin and the cycle budget.
|
|
55
|
+
* @returns A relaxer to hand to `refineConformers`, or to call directly.
|
|
56
|
+
*/
|
|
57
|
+
export function xtbRelaxer(options: XtbRelaxerOptions = {}): GeometryRelaxer {
|
|
58
|
+
return async (
|
|
59
|
+
geometries: readonly RelaxableGeometry[],
|
|
60
|
+
runOptions: RelaxerOptions = {},
|
|
61
|
+
): Promise<RelaxedGeometry[]> => {
|
|
62
|
+
const xtb = await loadXtb();
|
|
63
|
+
const settings = {
|
|
64
|
+
charge: options.charge ?? 0,
|
|
65
|
+
unpairedElectrons: options.unpairedElectrons ?? 0,
|
|
66
|
+
...(options.maxCycles === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { maxCycles: options.maxCycles }),
|
|
69
|
+
};
|
|
70
|
+
const requests = geometries.map((geometry) => ({ geometry, settings }));
|
|
71
|
+
const results = isXtbRelaxerAvailable()
|
|
72
|
+
? await xtb.relaxGeometries(requests, {
|
|
73
|
+
maxWorkers: options.maxWorkers,
|
|
74
|
+
onSettled: runOptions.onSettled,
|
|
75
|
+
signal: runOptions.signal,
|
|
76
|
+
})
|
|
77
|
+
: await relaxHere(xtb, requests, runOptions);
|
|
78
|
+
const relaxed: RelaxedGeometry[] = [];
|
|
79
|
+
for (const result of results) {
|
|
80
|
+
relaxed.push({
|
|
81
|
+
coordinates: result.coordinates,
|
|
82
|
+
energy: result.energy.total * KCAL_PER_MOL_PER_HARTREE,
|
|
83
|
+
dispersionEnergy:
|
|
84
|
+
result.energy.dispersion === null
|
|
85
|
+
? null
|
|
86
|
+
: result.energy.dispersion * KCAL_PER_MOL_PER_HARTREE,
|
|
87
|
+
cycles: result.cycles,
|
|
88
|
+
converged: result.converged,
|
|
89
|
+
warnings: result.warnings,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return relaxed;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether this environment has the worker pool the relaxer prefers.
|
|
98
|
+
*
|
|
99
|
+
* Without `Worker` — Node, a prerender pass, a test run — the relaxer still
|
|
100
|
+
* works, one structure at a time on the calling thread, so a site keeps one code
|
|
101
|
+
* path. It is the caller's business only when the difference in speed matters.
|
|
102
|
+
* @returns `true` in a browser, `false` in a plain Node process.
|
|
103
|
+
*/
|
|
104
|
+
export function isXtbRelaxerAvailable(): boolean {
|
|
105
|
+
return typeof Worker !== 'undefined';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Drop the GFN2 worker pool and the memory it holds, about 100 MB per instance.
|
|
110
|
+
* The next relaxation starts a fresh one.
|
|
111
|
+
*/
|
|
112
|
+
export async function disposeXtbRelaxer(): Promise<void> {
|
|
113
|
+
const loaded = await pending;
|
|
114
|
+
loaded?.disposeOccPool();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Relax one structure after another on this thread, for want of a pool.
|
|
119
|
+
* @param xtb - The loaded `xtb-wasm` module.
|
|
120
|
+
* @param requests - The structures to relax, with their settings.
|
|
121
|
+
* @param runOptions - Progress and cancellation.
|
|
122
|
+
* @returns One `xtb-wasm` result per request, in request order.
|
|
123
|
+
* @throws {Error} When the run is cancelled, or when occ fails.
|
|
124
|
+
*/
|
|
125
|
+
async function relaxHere(
|
|
126
|
+
xtb: XtbModule,
|
|
127
|
+
requests: ReadonlyArray<Parameters<XtbModule['relaxGeometryInProcess']>[0]>,
|
|
128
|
+
runOptions: RelaxerOptions,
|
|
129
|
+
): Promise<Array<Awaited<ReturnType<XtbModule['relaxGeometryInProcess']>>>> {
|
|
130
|
+
const results = [];
|
|
131
|
+
for (const [index, request] of requests.entries()) {
|
|
132
|
+
if (runOptions.signal?.aborted === true) {
|
|
133
|
+
throw new Error('calculation cancelled');
|
|
134
|
+
}
|
|
135
|
+
/* eslint-disable-next-line no-await-in-loop -- there is one thread: the
|
|
136
|
+
whole point of this path is that the structures run in turn. */
|
|
137
|
+
results.push(await xtb.relaxGeometryInProcess(request));
|
|
138
|
+
runOptions.onSettled?.(index + 1, requests.length);
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type XtbModule = typeof XtbWasm;
|
|
144
|
+
|
|
145
|
+
let pending: Promise<XtbModule> | null = null;
|
|
146
|
+
|
|
147
|
+
function loadXtb(): Promise<XtbModule> {
|
|
148
|
+
pending ??= import('xtb-wasm').catch((error: unknown) => {
|
|
149
|
+
// A failed chunk load must not poison every later call of the session.
|
|
150
|
+
pending = null;
|
|
151
|
+
// The reason is in the message, not only in `cause`: what a reader sees is
|
|
152
|
+
// whatever the UI renders, and "could not be loaded" alone names neither a
|
|
153
|
+
// missing install nor a chunk that failed to come down the wire.
|
|
154
|
+
throw new Error(
|
|
155
|
+
`xtb-wasm could not be loaded, so geometries cannot be refined: ${errorMessage(error)}`,
|
|
156
|
+
{ cause: error },
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
return pending;
|
|
160
|
+
}
|
package/src/xtb.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GFN2-xTB, the opt-in second stage after a force field.
|
|
3
|
+
*
|
|
4
|
+
* Its own door because it drags `xtb-wasm` and a 21.5 MB WebAssembly binary
|
|
5
|
+
* behind it, and because most pages never refine anything. The contract it
|
|
6
|
+
* implements — `GeometryRelaxer` — lives in `react-cheminfo/core`, so the code
|
|
7
|
+
* that orchestrates a refinement (`refineConformers`) loads none of this.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type { XtbRelaxerOptions } from './xtb/core/xtbRelaxer.ts';
|
|
11
|
+
export {
|
|
12
|
+
KCAL_PER_MOL_PER_HARTREE,
|
|
13
|
+
disposeXtbRelaxer,
|
|
14
|
+
isXtbRelaxerAvailable,
|
|
15
|
+
xtbRelaxer,
|
|
16
|
+
} from './xtb/core/xtbRelaxer.ts';
|
package/styles/chrome.css
CHANGED
|
@@ -170,6 +170,47 @@ button {
|
|
|
170
170
|
flex: 1;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/* A utility of the bar is its glyph: the name it carries is the tooltip and the
|
|
174
|
+
accessible name, never written beside it. About, Cite, Tools and Share read
|
|
175
|
+
as four marks the eye passes over, and the pages keep the width the labels
|
|
176
|
+
would take — which in a long language is most of the bar. An entry with no
|
|
177
|
+
glyph keeps its text, or it would be left an empty button: that is how a
|
|
178
|
+
toggle of the bar still shows the value it is set to. */
|
|
179
|
+
.app-header-actions .nav-link--icon .nav-link__label,
|
|
180
|
+
.app-header-actions
|
|
181
|
+
.bp6-button:has(> .bp6-icon:not(.bp6-icon-caret-down))
|
|
182
|
+
> .bp6-button-text,
|
|
183
|
+
.app-header-actions
|
|
184
|
+
.bp6-button:has(> .bp6-icon:not(.bp6-icon-caret-down))
|
|
185
|
+
> .bp6-icon-caret-down {
|
|
186
|
+
display: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.app-header-actions .nav-link--icon,
|
|
190
|
+
.citation-button .bp6-button,
|
|
191
|
+
.ecosystem-button .bp6-button {
|
|
192
|
+
padding: 0.3rem 0.5rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* A setting the bar shows as its own values — the language, the scale a
|
|
196
|
+
temperature is read on — the one in force pressed. The values carry no
|
|
197
|
+
glyph, which is what keeps them written where a utility is only its mark. */
|
|
198
|
+
.site-toggle .bp6-button {
|
|
199
|
+
padding: 0.25rem 0.4rem;
|
|
200
|
+
color: var(--text-muted);
|
|
201
|
+
font-size: 0.8125rem;
|
|
202
|
+
font-weight: 500;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.site-toggle .bp6-button.bp6-active {
|
|
206
|
+
color: var(--text);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Narrow: the one value in force, a click away from the next. */
|
|
210
|
+
.site-toggle--compact {
|
|
211
|
+
color: var(--text);
|
|
212
|
+
}
|
|
213
|
+
|
|
173
214
|
/* Carried by a link and by a button alike, so the whole row reads as one menu
|
|
174
215
|
rather than as a button dropped among addresses. */
|
|
175
216
|
.nav-link {
|
|
@@ -257,29 +298,11 @@ button {
|
|
|
257
298
|
flex: none;
|
|
258
299
|
}
|
|
259
300
|
|
|
260
|
-
/* The utilities give up their labels and keep their icons. An entry with no
|
|
261
|
-
icon keeps its label, or it would be left an empty button. */
|
|
262
301
|
@container app-header (max-width: 48rem) {
|
|
263
302
|
.app-header__inner {
|
|
264
303
|
padding: 0 0.75rem;
|
|
265
304
|
gap: 0.5rem;
|
|
266
305
|
}
|
|
267
|
-
|
|
268
|
-
.app-header-actions .nav-link--icon .nav-link__label,
|
|
269
|
-
.app-header-actions
|
|
270
|
-
.bp6-button:has(> .bp6-icon:not(.bp6-icon-caret-down))
|
|
271
|
-
> .bp6-button-text,
|
|
272
|
-
.app-header-actions
|
|
273
|
-
.bp6-button:has(> .bp6-icon:not(.bp6-icon-caret-down))
|
|
274
|
-
> .bp6-icon-caret-down {
|
|
275
|
-
display: none;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.app-header-actions .nav-link--icon,
|
|
279
|
-
.citation-button .bp6-button,
|
|
280
|
-
.ecosystem-button .bp6-button {
|
|
281
|
-
padding: 0.3rem 0.5rem;
|
|
282
|
-
}
|
|
283
306
|
}
|
|
284
307
|
|
|
285
308
|
/* A phone: the pages fold into one menu beside the brand. */
|
|
@@ -365,14 +388,25 @@ select,
|
|
|
365
388
|
|
|
366
389
|
/* ── Click to copy ───────────────────────────────────────────────────────── */
|
|
367
390
|
|
|
368
|
-
/* The
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
the
|
|
391
|
+
/* The cursor is the promise: a pointer carrying a clipboard, so the hover says
|
|
392
|
+
"click and this is yours" without a glyph crowding the line of text. The OS
|
|
393
|
+
`copy` cursor is not it — its badge is a plus, which reads as "add". The glyph
|
|
394
|
+
stays for what the cursor cannot say: the tick or cross after a click, and the
|
|
395
|
+
affordance for a keyboard, which has no pointer. Green and red are the
|
|
396
|
+
confirmation's own colours, the ones Blueprint gives success and danger. */
|
|
397
|
+
:root {
|
|
398
|
+
/* A cursor is a file of its own and cannot read a token, so its ink and its
|
|
399
|
+
halo are literal; the halo is what keeps it legible on a dark background. */
|
|
400
|
+
--cursor-copy:
|
|
401
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M1 1 1 17 5 13 7.5 18.5 10 17.4 7.6 12.4 12 12.4Z' fill='%231c2127' stroke='%23ffffff' stroke-width='2' stroke-linejoin='round' paint-order='stroke'/%3E%3Crect x='13' y='11.5' width='10' height='11.5' rx='2' fill='%231c2127' stroke='%23ffffff' stroke-width='2' paint-order='stroke'/%3E%3Crect x='15.5' y='9.8' width='5' height='3.6' rx='1.2' fill='%231c2127' stroke='%23ffffff' stroke-width='1.6' paint-order='stroke'/%3E%3C/svg%3E")
|
|
402
|
+
1 1,
|
|
403
|
+
copy;
|
|
404
|
+
}
|
|
405
|
+
|
|
372
406
|
.click-to-copy {
|
|
373
407
|
position: relative;
|
|
374
408
|
border-radius: 4px;
|
|
375
|
-
cursor: copy;
|
|
409
|
+
cursor: var(--cursor-copy);
|
|
376
410
|
transition: background-color 120ms ease;
|
|
377
411
|
}
|
|
378
412
|
|
|
@@ -398,8 +432,9 @@ select,
|
|
|
398
432
|
transition: opacity 120ms ease;
|
|
399
433
|
}
|
|
400
434
|
|
|
401
|
-
/*
|
|
402
|
-
|
|
435
|
+
/* The glyph is shown to a keyboard and after a copy, never on hover. In a line
|
|
436
|
+
of text it floats just past the value, so the line keeps its spacing; a block
|
|
437
|
+
keeps room for it inside its right edge. */
|
|
403
438
|
.click-to-copy--inline .click-to-copy__icon {
|
|
404
439
|
left: 100%;
|
|
405
440
|
transform: translate(3px, -50%);
|
|
@@ -433,17 +468,13 @@ select,
|
|
|
433
468
|
.click-to-copy:not([data-copy]):hover {
|
|
434
469
|
background-color: color-mix(in oklab, var(--accent) 12%, transparent);
|
|
435
470
|
}
|
|
436
|
-
|
|
437
|
-
.click-to-copy:hover .click-to-copy__icon {
|
|
438
|
-
opacity: 1;
|
|
439
|
-
}
|
|
440
471
|
}
|
|
441
472
|
|
|
442
473
|
/* Blueprint's interactive and striped tables paint and point every cell from
|
|
443
474
|
three classes deep, so a copyable cell repeats its class to stay ahead. */
|
|
444
475
|
.click-to-copy.click-to-copy.click-to-copy.click-to-copy:is(td, th) {
|
|
445
476
|
border-radius: 0;
|
|
446
|
-
cursor: copy;
|
|
477
|
+
cursor: var(--cursor-copy);
|
|
447
478
|
}
|
|
448
479
|
|
|
449
480
|
.click-to-copy.click-to-copy.click-to-copy.click-to-copy:is(
|