react-cheminfo 0.1.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/LICENSE +21 -0
- package/README.md +210 -0
- package/lib/citation/core/clipboard.d.ts +14 -0
- package/lib/citation/core/clipboard.d.ts.map +1 -0
- package/lib/citation/core/clipboard.js +25 -0
- package/lib/citation/core/clipboard.js.map +1 -0
- package/lib/citation/core/download.d.ts +11 -0
- package/lib/citation/core/download.d.ts.map +1 -0
- package/lib/citation/core/download.js +20 -0
- package/lib/citation/core/download.js.map +1 -0
- package/lib/citation/core/formats.d.ts +91 -0
- package/lib/citation/core/formats.d.ts.map +1 -0
- package/lib/citation/core/formats.js +125 -0
- package/lib/citation/core/formats.js.map +1 -0
- package/lib/citation/core/index.d.ts +10 -0
- package/lib/citation/core/index.d.ts.map +1 -0
- package/lib/citation/core/index.js +7 -0
- package/lib/citation/core/index.js.map +1 -0
- package/lib/citation/core/reference.d.ts +30 -0
- package/lib/citation/core/reference.d.ts.map +1 -0
- package/lib/citation/core/reference.js +9 -0
- package/lib/citation/core/reference.js.map +1 -0
- package/lib/citation/core/render.d.ts +21 -0
- package/lib/citation/core/render.d.ts.map +1 -0
- package/lib/citation/core/render.js +74 -0
- package/lib/citation/core/render.js.map +1 -0
- package/lib/citation/core/segments.d.ts +47 -0
- package/lib/citation/core/segments.d.ts.map +1 -0
- package/lib/citation/core/segments.js +134 -0
- package/lib/citation/core/segments.js.map +1 -0
- package/lib/citation/ui/CitationMenu.d.ts +14 -0
- package/lib/citation/ui/CitationMenu.d.ts.map +1 -0
- package/lib/citation/ui/CitationMenu.js +63 -0
- package/lib/citation/ui/CitationMenu.js.map +1 -0
- package/lib/citation/ui/CitationPreview.d.ts +18 -0
- package/lib/citation/ui/CitationPreview.d.ts.map +1 -0
- package/lib/citation/ui/CitationPreview.js +55 -0
- package/lib/citation/ui/CitationPreview.js.map +1 -0
- package/lib/citation/ui/CiteButton.d.ts +22 -0
- package/lib/citation/ui/CiteButton.d.ts.map +1 -0
- package/lib/citation/ui/CiteButton.js +15 -0
- package/lib/citation/ui/CiteButton.js.map +1 -0
- package/lib/citation/ui/entries.d.ts +45 -0
- package/lib/citation/ui/entries.d.ts.map +1 -0
- package/lib/citation/ui/entries.js +52 -0
- package/lib/citation/ui/entries.js.map +1 -0
- package/lib/citation/ui/index.d.ts +7 -0
- package/lib/citation/ui/index.d.ts.map +1 -0
- package/lib/citation/ui/index.js +4 -0
- package/lib/citation/ui/index.js.map +1 -0
- package/lib/core.d.ts +3 -0
- package/lib/core.d.ts.map +1 -0
- package/lib/core.js +3 -0
- package/lib/core.js.map +1 -0
- package/lib/ecosystem/core/index.d.ts +3 -0
- package/lib/ecosystem/core/index.d.ts.map +1 -0
- package/lib/ecosystem/core/index.js +2 -0
- package/lib/ecosystem/core/index.js.map +1 -0
- package/lib/ecosystem/core/sites.d.ts +57 -0
- package/lib/ecosystem/core/sites.d.ts.map +1 -0
- package/lib/ecosystem/core/sites.js +105 -0
- package/lib/ecosystem/core/sites.js.map +1 -0
- package/lib/ecosystem/ui/EcosystemButton.d.ts +24 -0
- package/lib/ecosystem/ui/EcosystemButton.d.ts.map +1 -0
- package/lib/ecosystem/ui/EcosystemButton.js +14 -0
- package/lib/ecosystem/ui/EcosystemButton.js.map +1 -0
- package/lib/ecosystem/ui/EcosystemMenu.d.ts +17 -0
- package/lib/ecosystem/ui/EcosystemMenu.d.ts.map +1 -0
- package/lib/ecosystem/ui/EcosystemMenu.js +112 -0
- package/lib/ecosystem/ui/EcosystemMenu.js.map +1 -0
- package/lib/ecosystem/ui/index.d.ts +7 -0
- package/lib/ecosystem/ui/index.d.ts.map +1 -0
- package/lib/ecosystem/ui/index.js +4 -0
- package/lib/ecosystem/ui/index.js.map +1 -0
- package/lib/ecosystem/ui/marks.d.ts +18 -0
- package/lib/ecosystem/ui/marks.d.ts.map +1 -0
- package/lib/ecosystem/ui/marks.js +43 -0
- package/lib/ecosystem/ui/marks.js.map +1 -0
- package/lib/shared/ui/MenuButton.d.ts +34 -0
- package/lib/shared/ui/MenuButton.d.ts.map +1 -0
- package/lib/shared/ui/MenuButton.js +18 -0
- package/lib/shared/ui/MenuButton.js.map +1 -0
- package/lib/shared/ui/index.d.ts +2 -0
- package/lib/shared/ui/index.d.ts.map +1 -0
- package/lib/shared/ui/index.js +2 -0
- package/lib/shared/ui/index.js.map +1 -0
- package/lib/ui.d.ts +4 -0
- package/lib/ui.d.ts.map +1 -0
- package/lib/ui.js +4 -0
- package/lib/ui.js.map +1 -0
- package/package.json +79 -0
- package/src/citation/core/clipboard.ts +32 -0
- package/src/citation/core/download.ts +27 -0
- package/src/citation/core/formats.ts +169 -0
- package/src/citation/core/index.ts +23 -0
- package/src/citation/core/reference.ts +33 -0
- package/src/citation/core/render.ts +78 -0
- package/src/citation/core/segments.ts +170 -0
- package/src/citation/ui/CitationMenu.tsx +132 -0
- package/src/citation/ui/CitationPreview.tsx +84 -0
- package/src/citation/ui/CiteButton.tsx +39 -0
- package/src/citation/ui/entries.tsx +144 -0
- package/src/citation/ui/index.ts +6 -0
- package/src/core.ts +2 -0
- package/src/ecosystem/core/index.ts +7 -0
- package/src/ecosystem/core/sites.ts +164 -0
- package/src/ecosystem/ui/EcosystemButton.tsx +41 -0
- package/src/ecosystem/ui/EcosystemMenu.tsx +191 -0
- package/src/ecosystem/ui/index.ts +6 -0
- package/src/ecosystem/ui/marks.tsx +207 -0
- package/src/shared/ui/MenuButton.tsx +67 -0
- package/src/shared/ui/index.ts +1 -0
- package/src/ui.ts +3 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
|
|
2
|
+
import { SvgLogoDoi } from 'cheminfo-font';
|
|
3
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { copyCitation } from '../core/clipboard.ts';
|
|
7
|
+
import type { CitationFormat } from '../core/formats.ts';
|
|
8
|
+
import { CITATION_DOWNLOADS, CITATION_FORMATS } from '../core/formats.ts';
|
|
9
|
+
import type { Reference } from '../core/reference.ts';
|
|
10
|
+
import { doiUrl } from '../core/reference.ts';
|
|
11
|
+
import type { CitationStyle } from '../core/segments.ts';
|
|
12
|
+
import { CITATION_STYLES } from '../core/segments.ts';
|
|
13
|
+
|
|
14
|
+
import type { CopyState } from './entries.tsx';
|
|
15
|
+
import { CopyEntry, DownloadEntry } from './entries.tsx';
|
|
16
|
+
|
|
17
|
+
// How long the tick replaces the clipboard glyph of an entry after a copy.
|
|
18
|
+
const FEEDBACK_MS = 1500;
|
|
19
|
+
|
|
20
|
+
const MENU_STYLE: CSSProperties = { minWidth: 240 };
|
|
21
|
+
const DOI_ICON_STYLE: CSSProperties = { width: 16, height: 16 };
|
|
22
|
+
|
|
23
|
+
export interface CitationMenuProps {
|
|
24
|
+
/** The work being cited. */
|
|
25
|
+
reference: Reference;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What the Cite button opens: the article at its DOI, the reference copied in
|
|
30
|
+
* the style a journal asks for, and the files a reference manager imports.
|
|
31
|
+
* @param props - The work being cited.
|
|
32
|
+
* @returns The citation menu.
|
|
33
|
+
*/
|
|
34
|
+
export function CitationMenu(props: CitationMenuProps): ReactElement {
|
|
35
|
+
const { reference } = props;
|
|
36
|
+
// Which entry showed feedback last, keyed `format` or `format:style`.
|
|
37
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
38
|
+
const [hasFailed, setHasFailed] = useState(false);
|
|
39
|
+
const timeout = useRef<number | null>(null);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
if (timeout.current !== null) window.clearTimeout(timeout.current);
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
function announce(key: string, failed: boolean): void {
|
|
48
|
+
setCopied(key);
|
|
49
|
+
setHasFailed(failed);
|
|
50
|
+
if (timeout.current !== null) window.clearTimeout(timeout.current);
|
|
51
|
+
timeout.current = window.setTimeout(() => {
|
|
52
|
+
setCopied(null);
|
|
53
|
+
setHasFailed(false);
|
|
54
|
+
}, FEEDBACK_MS);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function copy(format: CitationFormat, style?: CitationStyle): void {
|
|
58
|
+
const key = style === undefined ? format.id : `${format.id}:${style.id}`;
|
|
59
|
+
copyCitation(reference, format.id, style?.id).then(
|
|
60
|
+
() => {
|
|
61
|
+
announce(key, false);
|
|
62
|
+
},
|
|
63
|
+
() => {
|
|
64
|
+
// A denied clipboard permission is the usual cause, and the menu is the
|
|
65
|
+
// only place the user can be told about it.
|
|
66
|
+
announce(key, true);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stateOf(key: string): CopyState | null {
|
|
72
|
+
return copied === key ? feedback(hasFailed) : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Menu className="citation-menu" style={MENU_STYLE}>
|
|
77
|
+
<MenuItem
|
|
78
|
+
icon={<SvgLogoDoi style={DOI_ICON_STYLE} />}
|
|
79
|
+
text={`${reference.journalAbbreviation} ${reference.year}`}
|
|
80
|
+
label={reference.doi}
|
|
81
|
+
href={doiUrl(reference)}
|
|
82
|
+
target="_blank"
|
|
83
|
+
rel="noreferrer"
|
|
84
|
+
/>
|
|
85
|
+
<MenuDivider title="Copy the reference as" />
|
|
86
|
+
{CITATION_FORMATS.map((format) =>
|
|
87
|
+
format.styled ? (
|
|
88
|
+
<MenuItem
|
|
89
|
+
key={format.id}
|
|
90
|
+
icon="clipboard"
|
|
91
|
+
text={format.label}
|
|
92
|
+
label={format.hint}
|
|
93
|
+
>
|
|
94
|
+
{CITATION_STYLES.map((style) => (
|
|
95
|
+
<CopyEntry
|
|
96
|
+
key={style.id}
|
|
97
|
+
reference={reference}
|
|
98
|
+
format={format}
|
|
99
|
+
style={style}
|
|
100
|
+
state={stateOf(`${format.id}:${style.id}`)}
|
|
101
|
+
onCopy={copy}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</MenuItem>
|
|
105
|
+
) : (
|
|
106
|
+
<CopyEntry
|
|
107
|
+
key={format.id}
|
|
108
|
+
reference={reference}
|
|
109
|
+
format={format}
|
|
110
|
+
state={stateOf(format.id)}
|
|
111
|
+
onCopy={copy}
|
|
112
|
+
/>
|
|
113
|
+
),
|
|
114
|
+
)}
|
|
115
|
+
<MenuDivider title="Import into a reference manager" />
|
|
116
|
+
{CITATION_DOWNLOADS.map((download) => (
|
|
117
|
+
<DownloadEntry
|
|
118
|
+
key={download.format}
|
|
119
|
+
reference={reference}
|
|
120
|
+
download={download}
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
123
|
+
</Menu>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function feedback(failed: boolean): CopyState {
|
|
128
|
+
if (failed) {
|
|
129
|
+
return { icon: 'cross', intent: 'danger', label: 'copy failed' };
|
|
130
|
+
}
|
|
131
|
+
return { icon: 'tick', intent: 'success', label: 'copied' };
|
|
132
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { CitationFormatId } from '../core/formats.ts';
|
|
4
|
+
import { formatCitation } from '../core/formats.ts';
|
|
5
|
+
import type { Reference } from '../core/reference.ts';
|
|
6
|
+
import type { CitationStyleId } from '../core/segments.ts';
|
|
7
|
+
import { citationSegments } from '../core/segments.ts';
|
|
8
|
+
|
|
9
|
+
// A BibTeX entry and a one-line citation share this box, so the text wraps and
|
|
10
|
+
// the box never grows past what a tooltip can hold. The height is the last
|
|
11
|
+
// resort, for a window too short to show an entry whole.
|
|
12
|
+
const PREVIEW_STYLE: CSSProperties = {
|
|
13
|
+
maxWidth: 440,
|
|
14
|
+
maxHeight: '70vh',
|
|
15
|
+
overflow: 'auto',
|
|
16
|
+
margin: 0,
|
|
17
|
+
color: '#e5e8eb',
|
|
18
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
19
|
+
fontSize: 11.5,
|
|
20
|
+
lineHeight: 1.45,
|
|
21
|
+
whiteSpace: 'pre-wrap',
|
|
22
|
+
overflowWrap: 'anywhere',
|
|
23
|
+
};
|
|
24
|
+
// HTML is previewed as it reads once pasted, so it takes the reading font.
|
|
25
|
+
const RICH_STYLE: CSSProperties = {
|
|
26
|
+
maxWidth: 440,
|
|
27
|
+
color: '#e5e8eb',
|
|
28
|
+
fontSize: 12,
|
|
29
|
+
lineHeight: 1.45,
|
|
30
|
+
};
|
|
31
|
+
const LINK_STYLE: CSSProperties = { color: '#8abbff' };
|
|
32
|
+
|
|
33
|
+
export interface CitationPreviewProps {
|
|
34
|
+
reference: Reference;
|
|
35
|
+
format: CitationFormatId;
|
|
36
|
+
/** Journal style, for the formats that have one. */
|
|
37
|
+
style?: CitationStyleId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What a copy of the reference puts on the clipboard, as it will read once
|
|
42
|
+
* pasted: the emphasis of the style for HTML, the source itself otherwise.
|
|
43
|
+
* @param props - Reference, format and style to preview.
|
|
44
|
+
* @returns The body of the preview tooltip.
|
|
45
|
+
*/
|
|
46
|
+
export function CitationPreview(props: CitationPreviewProps): ReactElement {
|
|
47
|
+
const { reference, format, style } = props;
|
|
48
|
+
|
|
49
|
+
if (format === 'html' && style !== undefined) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="citation-preview" style={RICH_STYLE}>
|
|
52
|
+
{citationSegments(reference, style).map((segment, index) => (
|
|
53
|
+
// The segments of one citation are a fixed list, never reordered.
|
|
54
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
55
|
+
<Segment key={index} segment={segment} />
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<pre className="citation-preview" style={PREVIEW_STYLE}>
|
|
63
|
+
{formatCitation(reference, format, style)}
|
|
64
|
+
</pre>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Segment(props: {
|
|
69
|
+
segment: ReturnType<typeof citationSegments>[number];
|
|
70
|
+
}): ReactElement {
|
|
71
|
+
const { segment } = props;
|
|
72
|
+
switch (segment.kind) {
|
|
73
|
+
case 'italic':
|
|
74
|
+
return <em>{segment.text}</em>;
|
|
75
|
+
case 'bold':
|
|
76
|
+
return <strong>{segment.text}</strong>;
|
|
77
|
+
case 'link':
|
|
78
|
+
return <span style={LINK_STYLE}>{segment.text}</span>;
|
|
79
|
+
case 'text':
|
|
80
|
+
return <span>{segment.text}</span>;
|
|
81
|
+
default:
|
|
82
|
+
return <span />;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { HeaderButtonProps } from '../../shared/ui/MenuButton.tsx';
|
|
4
|
+
import { MenuButton } from '../../shared/ui/MenuButton.tsx';
|
|
5
|
+
import type { Reference } from '../core/reference.ts';
|
|
6
|
+
|
|
7
|
+
import { CitationMenu } from './CitationMenu.tsx';
|
|
8
|
+
|
|
9
|
+
export interface CiteButtonProps extends HeaderButtonProps {
|
|
10
|
+
/** The work the site asks to be cited. */
|
|
11
|
+
reference: Reference;
|
|
12
|
+
/**
|
|
13
|
+
* Text of the button. In a compact bar it is not written, but it stays what
|
|
14
|
+
* the pointer and a screen reader are told.
|
|
15
|
+
* @default 'Cite'
|
|
16
|
+
*/
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The Cite entry of a site header: one button opening the article at its DOI,
|
|
22
|
+
* the reference in the style a journal asks for, and the files a reference
|
|
23
|
+
* manager imports.
|
|
24
|
+
* @param props - The work being cited, and how the menu opens.
|
|
25
|
+
* @returns The button and its menu.
|
|
26
|
+
*/
|
|
27
|
+
export function CiteButton(props: CiteButtonProps): ReactElement {
|
|
28
|
+
const { reference, label = 'Cite', ...rest } = props;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<MenuButton
|
|
32
|
+
{...rest}
|
|
33
|
+
className="citation-button"
|
|
34
|
+
icon="citation"
|
|
35
|
+
label={label}
|
|
36
|
+
menu={<CitationMenu reference={reference} />}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { IconName, Intent } from '@blueprintjs/core';
|
|
2
|
+
import { MenuItem, Tooltip } from '@blueprintjs/core';
|
|
3
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
4
|
+
|
|
5
|
+
import { downloadCitation } from '../core/download.ts';
|
|
6
|
+
import type { CitationDownload, CitationFormat } from '../core/formats.ts';
|
|
7
|
+
import type { Reference } from '../core/reference.ts';
|
|
8
|
+
import type { CitationStyle } from '../core/segments.ts';
|
|
9
|
+
|
|
10
|
+
import { CitationPreview } from './CitationPreview.tsx';
|
|
11
|
+
|
|
12
|
+
// An entry carrying its preview tooltip is also a popover target, and
|
|
13
|
+
// Blueprint's `.bp6-submenu .bp6-popover-target` then turns the row into a
|
|
14
|
+
// block, stacking the icon, the name and the journals. An inline rule outranks
|
|
15
|
+
// any selector, so the row survives wherever the entry is used.
|
|
16
|
+
const ENTRY_STYLE: CSSProperties = { display: 'flex' };
|
|
17
|
+
// Journal names are set in italic, as every one of these styles asks for.
|
|
18
|
+
const JOURNALS_STYLE: CSSProperties = { fontStyle: 'italic' };
|
|
19
|
+
// Long enough that running the pointer down the menu opens no preview.
|
|
20
|
+
const PREVIEW_OPEN_DELAY = 400;
|
|
21
|
+
|
|
22
|
+
/** Feedback shown on an entry just after it was clicked. */
|
|
23
|
+
export interface CopyState {
|
|
24
|
+
icon: IconName;
|
|
25
|
+
intent: Intent;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CopyEntryProps {
|
|
30
|
+
/** The work being cited. */
|
|
31
|
+
reference: Reference;
|
|
32
|
+
/** The format the entry copies. */
|
|
33
|
+
format: CitationFormat;
|
|
34
|
+
/**
|
|
35
|
+
* Style of the entry, when it sits in the submenu of a styled format.
|
|
36
|
+
* @default undefined
|
|
37
|
+
*/
|
|
38
|
+
style?: CitationStyle;
|
|
39
|
+
/** Feedback of the last copy, while it is showing. */
|
|
40
|
+
state: CopyState | null;
|
|
41
|
+
/** Called with what the entry copies. */
|
|
42
|
+
onCopy: (format: CitationFormat, style?: CitationStyle) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One entry that copies, previewing on hover what it puts on the clipboard.
|
|
47
|
+
* @param props - The reference, the format, and the feedback showing.
|
|
48
|
+
* @returns The menu entry.
|
|
49
|
+
*/
|
|
50
|
+
export function CopyEntry(props: CopyEntryProps): ReactElement {
|
|
51
|
+
const { reference, format, style, state, onCopy } = props;
|
|
52
|
+
const entry = style ?? format;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<PreviewTooltip reference={reference} format={format.id} style={style}>
|
|
56
|
+
{(targetProps) => (
|
|
57
|
+
<MenuItem
|
|
58
|
+
{...targetProps}
|
|
59
|
+
style={ENTRY_STYLE}
|
|
60
|
+
icon={state?.icon ?? 'clipboard'}
|
|
61
|
+
intent={state?.intent}
|
|
62
|
+
text={entry.label}
|
|
63
|
+
labelElement={
|
|
64
|
+
// The hint of a style entry is a list of journals, in italic.
|
|
65
|
+
style !== undefined && state === null ? (
|
|
66
|
+
<span style={JOURNALS_STYLE}>{entry.hint}</span>
|
|
67
|
+
) : (
|
|
68
|
+
(state?.label ?? entry.hint)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
shouldDismissPopover={false}
|
|
72
|
+
onClick={() => {
|
|
73
|
+
onCopy(format, style);
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</PreviewTooltip>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DownloadEntryProps {
|
|
82
|
+
/** The work being saved. */
|
|
83
|
+
reference: Reference;
|
|
84
|
+
/** The file to write, from `CITATION_DOWNLOADS`. */
|
|
85
|
+
download: CitationDownload;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One entry saving the file a reference manager imports.
|
|
90
|
+
* @param props - The entry options.
|
|
91
|
+
* @returns The menu entry.
|
|
92
|
+
*/
|
|
93
|
+
export function DownloadEntry(props: DownloadEntryProps): ReactElement {
|
|
94
|
+
const { reference, download } = props;
|
|
95
|
+
return (
|
|
96
|
+
<PreviewTooltip reference={reference} format={download.format}>
|
|
97
|
+
{(targetProps) => (
|
|
98
|
+
<MenuItem
|
|
99
|
+
{...targetProps}
|
|
100
|
+
style={ENTRY_STYLE}
|
|
101
|
+
icon="download"
|
|
102
|
+
text={download.label}
|
|
103
|
+
labelElement={download.hint}
|
|
104
|
+
onClick={() => {
|
|
105
|
+
downloadCitation(reference, download);
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
</PreviewTooltip>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The hover preview of what an entry copies or saves.
|
|
115
|
+
* @param props - The preview options.
|
|
116
|
+
* @param props.reference - The work being written.
|
|
117
|
+
* @param props.format - The format the entry produces.
|
|
118
|
+
* @param props.style - The journal style, for a format that has one.
|
|
119
|
+
* @param props.children - The entry the preview is anchored on.
|
|
120
|
+
* @returns The entry, wrapped in its preview tooltip.
|
|
121
|
+
*/
|
|
122
|
+
function PreviewTooltip(props: {
|
|
123
|
+
reference: Reference;
|
|
124
|
+
format: CitationFormat['id'];
|
|
125
|
+
style?: CitationStyle;
|
|
126
|
+
children: (targetProps: Record<string, unknown>) => ReactElement;
|
|
127
|
+
}): ReactElement {
|
|
128
|
+
const { reference, format, style, children } = props;
|
|
129
|
+
return (
|
|
130
|
+
<Tooltip
|
|
131
|
+
placement="left"
|
|
132
|
+
popoverClassName="citation-tooltip"
|
|
133
|
+
hoverOpenDelay={PREVIEW_OPEN_DELAY}
|
|
134
|
+
content={
|
|
135
|
+
<CitationPreview
|
|
136
|
+
reference={reference}
|
|
137
|
+
format={format}
|
|
138
|
+
style={style?.id}
|
|
139
|
+
/>
|
|
140
|
+
}
|
|
141
|
+
renderTarget={({ isOpen, ...targetProps }) => children(targetProps)}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CitationMenuProps } from './CitationMenu.tsx';
|
|
2
|
+
export { CitationMenu } from './CitationMenu.tsx';
|
|
3
|
+
export type { CitationPreviewProps } from './CitationPreview.tsx';
|
|
4
|
+
export { CitationPreview } from './CitationPreview.tsx';
|
|
5
|
+
export type { CiteButtonProps } from './CiteButton.tsx';
|
|
6
|
+
export { CiteButton } from './CiteButton.tsx';
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** The sites the menu links to, in the order they are listed. */
|
|
2
|
+
export type SiteId =
|
|
3
|
+
| 'inchi'
|
|
4
|
+
| 'vcl'
|
|
5
|
+
| 'smiles'
|
|
6
|
+
| 'chemcalc'
|
|
7
|
+
| 'nmrium'
|
|
8
|
+
| 'surge'
|
|
9
|
+
| 'tex'
|
|
10
|
+
| 'lcao'
|
|
11
|
+
| 'regexp'
|
|
12
|
+
| 'pdb';
|
|
13
|
+
|
|
14
|
+
export interface SiteName {
|
|
15
|
+
/** First half of the name, set in the site's leading colour. */
|
|
16
|
+
lead: string;
|
|
17
|
+
/** Second half, set in the site's answering colour. */
|
|
18
|
+
alt: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a faint dot separates the two halves, as an address-shaped name
|
|
21
|
+
* takes and a product-shaped one does not.
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
dot?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EcosystemSite {
|
|
28
|
+
id: SiteId;
|
|
29
|
+
/** The name, split the way the site's own wordmark splits it. */
|
|
30
|
+
name: SiteName;
|
|
31
|
+
/** Where the site lives, written the way it is typed. */
|
|
32
|
+
host: string;
|
|
33
|
+
/** One line on what the site does. */
|
|
34
|
+
tagline: string;
|
|
35
|
+
/** The colour the first half of the name is set in. */
|
|
36
|
+
brand: string;
|
|
37
|
+
/**
|
|
38
|
+
* The colour the second half is set in. It is the site's answering colour
|
|
39
|
+
* darkened until it is readable on white, because several of those are
|
|
40
|
+
* yellows and ambers that sit around 2:1 there.
|
|
41
|
+
*/
|
|
42
|
+
brandAlt: string;
|
|
43
|
+
/** The plate of the site's mark, and the colour one element of it carries. */
|
|
44
|
+
mark: SiteMarkColors;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SiteMarkColors {
|
|
48
|
+
/** The rounded square the mark is drawn on. */
|
|
49
|
+
plate: string;
|
|
50
|
+
/** The colour exactly one element of the mark takes. */
|
|
51
|
+
accent: string;
|
|
52
|
+
/**
|
|
53
|
+
* The hairline a plate as light as the tile behind it needs, so it still
|
|
54
|
+
* reads as a plate rather than as a drawing floating on the row.
|
|
55
|
+
* @default undefined
|
|
56
|
+
*/
|
|
57
|
+
edge?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every site of the family, each with the two colours it owns. The marks that
|
|
62
|
+
* go with them live in `marks.tsx`.
|
|
63
|
+
*/
|
|
64
|
+
export const ECOSYSTEM_SITES: readonly EcosystemSite[] = [
|
|
65
|
+
{
|
|
66
|
+
id: 'inchi',
|
|
67
|
+
name: { lead: 'inchi', alt: 'cheminfo', dot: true },
|
|
68
|
+
host: 'inchi.cheminfo.org',
|
|
69
|
+
tagline: 'InChI and InChIKey from a structure, in the browser.',
|
|
70
|
+
brand: '#5b21b6',
|
|
71
|
+
brandAlt: '#a16207',
|
|
72
|
+
mark: { plate: '#5b21b6', accent: '#fcd34d' },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'vcl',
|
|
76
|
+
name: { lead: 'vcl', alt: 'cheminfo', dot: true },
|
|
77
|
+
host: 'vcl.cheminfo.org',
|
|
78
|
+
tagline: 'Combine a core and fragments into a screened library.',
|
|
79
|
+
brand: '#2d72d2',
|
|
80
|
+
brandAlt: '#9a3412',
|
|
81
|
+
mark: { plate: '#2d72d2', accent: '#eb6847' },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'smiles',
|
|
85
|
+
name: { lead: 'smiles', alt: 'cheminfo', dot: true },
|
|
86
|
+
host: 'smiles.cheminfo.org',
|
|
87
|
+
tagline: 'Draw a structure, read its SMILES, learn the notation.',
|
|
88
|
+
brand: '#1c6e42',
|
|
89
|
+
brandAlt: '#9a3412',
|
|
90
|
+
mark: { plate: '#1c6e42', accent: '#ea580c' },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'chemcalc',
|
|
94
|
+
name: { lead: 'Chem', alt: 'Calc' },
|
|
95
|
+
host: 'www.chemcalc.org',
|
|
96
|
+
tagline: 'Molecular formula, exact mass and isotopic distribution.',
|
|
97
|
+
brand: '#5b52e0',
|
|
98
|
+
brandAlt: '#0d9488',
|
|
99
|
+
mark: { plate: '#5b52e0', accent: '#09d3ac' },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'nmrium',
|
|
103
|
+
name: { lead: 'NMR', alt: 'ium' },
|
|
104
|
+
host: 'www.nmrium.org',
|
|
105
|
+
tagline: 'Process and assign NMR spectra in the browser.',
|
|
106
|
+
brand: '#ea580c',
|
|
107
|
+
brandAlt: '#2b143e',
|
|
108
|
+
mark: { plate: '#2b143e', accent: '#ea580c' },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'surge',
|
|
112
|
+
name: { lead: 'surge', alt: 'cheminfo', dot: true },
|
|
113
|
+
host: 'surge.cheminfo.org',
|
|
114
|
+
tagline: 'Every constitutional isomer of a molecular formula.',
|
|
115
|
+
brand: '#4338ca',
|
|
116
|
+
brandAlt: '#be123c',
|
|
117
|
+
mark: { plate: '#4338ca', accent: '#e11d48' },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'tex',
|
|
121
|
+
name: { lead: 'tex', alt: 'cheminfo', dot: true },
|
|
122
|
+
host: 'tex.cheminfo.org',
|
|
123
|
+
tagline: 'LaTeX formulas rendered to SVG or PNG from a URL.',
|
|
124
|
+
brand: '#a21caf',
|
|
125
|
+
brandAlt: '#a16207',
|
|
126
|
+
mark: { plate: '#a21caf', accent: '#facc15' },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'lcao',
|
|
130
|
+
name: { lead: 'lcao', alt: 'cheminfo', dot: true },
|
|
131
|
+
host: 'lcao.cheminfo.org',
|
|
132
|
+
tagline: 'Combine atomic orbitals into molecular ones.',
|
|
133
|
+
brand: '#1565c0',
|
|
134
|
+
brandAlt: '#c62828',
|
|
135
|
+
mark: { plate: '#ffffff', accent: '#c62828', edge: '#dfe3e8' },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'regexp',
|
|
139
|
+
name: { lead: 'regexp', alt: 'cheminfo', dot: true },
|
|
140
|
+
host: 'regexp.cheminfo.org',
|
|
141
|
+
tagline: 'Learn regular expressions in a live playground.',
|
|
142
|
+
brand: '#1e3a8a',
|
|
143
|
+
brandAlt: '#a16207',
|
|
144
|
+
mark: { plate: '#1e3a8a', accent: '#fde047' },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'pdb',
|
|
148
|
+
name: { lead: 'pdb', alt: 'cheminfo', dot: true },
|
|
149
|
+
host: 'pdb.cheminfo.org',
|
|
150
|
+
tagline: 'A fast look at any Protein Data Bank entry.',
|
|
151
|
+
brand: '#2563eb',
|
|
152
|
+
brandAlt: '#b45309',
|
|
153
|
+
mark: { plate: '#2563eb', accent: '#fbbf24' },
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The address of a site, which is its host over https.
|
|
159
|
+
* @param site - The site being linked to.
|
|
160
|
+
* @returns The URL to open.
|
|
161
|
+
*/
|
|
162
|
+
export function siteUrl(site: EcosystemSite): string {
|
|
163
|
+
return `https://${site.host}/`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { HeaderButtonProps } from '../../shared/ui/MenuButton.tsx';
|
|
4
|
+
import { MenuButton } from '../../shared/ui/MenuButton.tsx';
|
|
5
|
+
import type { SiteId } from '../core/sites.ts';
|
|
6
|
+
|
|
7
|
+
import { EcosystemMenu } from './EcosystemMenu.tsx';
|
|
8
|
+
|
|
9
|
+
export interface EcosystemButtonProps extends HeaderButtonProps {
|
|
10
|
+
/**
|
|
11
|
+
* The site this button sits on, which is shown but never linked.
|
|
12
|
+
* @default undefined
|
|
13
|
+
*/
|
|
14
|
+
currentSiteId?: SiteId;
|
|
15
|
+
/**
|
|
16
|
+
* Text of the button. In a compact bar it is not written, but it stays what
|
|
17
|
+
* the pointer and a screen reader are told.
|
|
18
|
+
* @default 'Tools'
|
|
19
|
+
*/
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Tools entry of a site header: one button opening every other site of the
|
|
25
|
+
* family, each behind its own little logo.
|
|
26
|
+
* @param props - The site it sits on, and how the menu opens.
|
|
27
|
+
* @returns The button and its menu.
|
|
28
|
+
*/
|
|
29
|
+
export function EcosystemButton(props: EcosystemButtonProps): ReactElement {
|
|
30
|
+
const { currentSiteId, label = 'Tools', ...rest } = props;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<MenuButton
|
|
34
|
+
{...rest}
|
|
35
|
+
className="ecosystem-button"
|
|
36
|
+
icon="grid-view"
|
|
37
|
+
label={label}
|
|
38
|
+
menu={<EcosystemMenu currentSiteId={currentSiteId} />}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|