react-cheminfo 0.6.0 → 0.8.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 +16 -0
- package/lib/citation/core/index.d.ts +2 -0
- package/lib/citation/core/index.d.ts.map +1 -1
- package/lib/citation/core/index.js +2 -0
- package/lib/citation/core/index.js.map +1 -1
- package/lib/citation/core/platformPaper.d.ts +14 -0
- package/lib/citation/core/platformPaper.d.ts.map +1 -0
- package/lib/citation/core/platformPaper.js +28 -0
- package/lib/citation/core/platformPaper.js.map +1 -0
- package/lib/citation/core/teachingPaper.d.ts +15 -0
- package/lib/citation/core/teachingPaper.d.ts.map +1 -0
- package/lib/citation/core/teachingPaper.js +32 -0
- package/lib/citation/core/teachingPaper.js.map +1 -0
- package/lib/color/core/index.d.ts +1 -1
- package/lib/color/core/index.d.ts.map +1 -1
- package/lib/color/core/index.js +1 -1
- package/lib/color/core/index.js.map +1 -1
- 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/ecosystem/core/sites.js +5 -5
- package/lib/orbital/core/index.d.ts +1 -1
- package/lib/orbital/core/index.d.ts.map +1 -1
- package/lib/orbital/core/index.js +1 -1
- package/lib/orbital/core/index.js.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts +15 -2
- package/lib/pedagogy/ui/TutorialStepStrip.d.ts.map +1 -1
- package/lib/pedagogy/ui/TutorialStepStrip.js +15 -4
- package/lib/pedagogy/ui/TutorialStepStrip.js.map +1 -1
- package/lib/pedagogy/ui/index.d.ts +1 -1
- package/lib/pedagogy/ui/index.d.ts.map +1 -1
- package/lib/pedagogy/ui/index.js.map +1 -1
- package/lib/periodic/core/categories.d.ts +25 -0
- package/lib/periodic/core/categories.d.ts.map +1 -0
- package/lib/periodic/core/categories.js +70 -0
- package/lib/periodic/core/categories.js.map +1 -0
- package/lib/periodic/core/elements.d.ts +49 -0
- package/lib/periodic/core/elements.d.ts.map +1 -0
- package/lib/periodic/core/elements.js +175 -0
- package/lib/periodic/core/elements.js.map +1 -0
- package/lib/periodic/core/index.d.ts +6 -0
- package/lib/periodic/core/index.d.ts.map +1 -0
- package/lib/periodic/core/index.js +4 -0
- package/lib/periodic/core/index.js.map +1 -0
- package/lib/periodic/core/layout.d.ts +73 -0
- package/lib/periodic/core/layout.d.ts.map +1 -0
- package/lib/periodic/core/layout.js +132 -0
- package/lib/periodic/core/layout.js.map +1 -0
- package/lib/periodic/ui/CategoryLegend.d.ts +26 -0
- package/lib/periodic/ui/CategoryLegend.d.ts.map +1 -0
- package/lib/periodic/ui/CategoryLegend.js +53 -0
- package/lib/periodic/ui/CategoryLegend.js.map +1 -0
- package/lib/periodic/ui/ElementCell.d.ts +53 -0
- package/lib/periodic/ui/ElementCell.d.ts.map +1 -0
- package/lib/periodic/ui/ElementCell.js +56 -0
- package/lib/periodic/ui/ElementCell.js.map +1 -0
- package/lib/periodic/ui/PeriodicTable.d.ts +90 -0
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -0
- package/lib/periodic/ui/PeriodicTable.js +77 -0
- package/lib/periodic/ui/PeriodicTable.js.map +1 -0
- package/lib/periodic/ui/PeriodicTableChrome.d.ts +36 -0
- package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -0
- package/lib/periodic/ui/PeriodicTableChrome.js +72 -0
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -0
- package/lib/periodic/ui/index.d.ts +7 -0
- package/lib/periodic/ui/index.d.ts.map +1 -0
- package/lib/periodic/ui/index.js +4 -0
- package/lib/periodic/ui/index.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/package.json +2 -1
- package/src/citation/core/index.ts +2 -0
- package/src/citation/core/platformPaper.ts +32 -0
- package/src/citation/core/teachingPaper.ts +36 -0
- package/src/color/core/index.ts +6 -1
- package/src/core.ts +1 -0
- package/src/ecosystem/core/sites.ts +5 -5
- package/src/orbital/core/index.ts +1 -1
- package/src/pedagogy/ui/TutorialStepStrip.tsx +35 -9
- package/src/pedagogy/ui/index.ts +4 -1
- package/src/periodic/core/categories.ts +83 -0
- package/src/periodic/core/elements.ts +217 -0
- package/src/periodic/core/index.ts +27 -0
- package/src/periodic/core/layout.ts +183 -0
- package/src/periodic/ui/CategoryLegend.tsx +105 -0
- package/src/periodic/ui/ElementCell.tsx +137 -0
- package/src/periodic/ui/PeriodicTable.tsx +226 -0
- package/src/periodic/ui/PeriodicTableChrome.tsx +170 -0
- package/src/periodic/ui/index.ts +6 -0
- package/src/ui.ts +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One cell of the periodic table: the atomic number, the symbol, and whatever
|
|
3
|
+
* the tool asked to be written under it.
|
|
4
|
+
*
|
|
5
|
+
* A cell is a real `<button>`, so Tab reaches it, Enter and Space activate it,
|
|
6
|
+
* and a screen reader reads the element's name rather than its symbol.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
10
|
+
|
|
11
|
+
import type { Swatch } from '../../color/core/scale.ts';
|
|
12
|
+
|
|
13
|
+
/** What {@link ElementCell} needs to draw one element. */
|
|
14
|
+
export interface ElementCellProps {
|
|
15
|
+
/** Atomic number, written small in the corner. */
|
|
16
|
+
atomicNumber: number;
|
|
17
|
+
/** Chemical symbol, the largest thing in the cell. */
|
|
18
|
+
symbol: string;
|
|
19
|
+
/** Full name, which is what the cell is announced as. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Background, and the ink that stays readable on it. */
|
|
22
|
+
swatch: Swatch;
|
|
23
|
+
/** Column of the grid, one-based. */
|
|
24
|
+
column: number;
|
|
25
|
+
/** Row of the grid, one-based. */
|
|
26
|
+
row: number;
|
|
27
|
+
onSelect: (symbol: string) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Third line, under the symbol: the value the tool is showing.
|
|
30
|
+
* @default '' — nothing is written
|
|
31
|
+
*/
|
|
32
|
+
detail?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the cell is the one the tools are pointed at.
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
isSelected?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the cell is inside the set the tool is showing. Everything outside
|
|
40
|
+
* it is dimmed rather than removed, so the table keeps its shape.
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
isIncluded?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Called on pointer enter with the symbol, and on leave with `null`.
|
|
46
|
+
* @default undefined
|
|
47
|
+
*/
|
|
48
|
+
onHover?: (symbol: string | null) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A single element of the table.
|
|
53
|
+
* @param props - See {@link ElementCellProps}.
|
|
54
|
+
* @returns The cell.
|
|
55
|
+
*/
|
|
56
|
+
export function ElementCell(props: ElementCellProps): ReactElement {
|
|
57
|
+
const {
|
|
58
|
+
atomicNumber,
|
|
59
|
+
symbol,
|
|
60
|
+
name,
|
|
61
|
+
swatch,
|
|
62
|
+
column,
|
|
63
|
+
row,
|
|
64
|
+
onSelect,
|
|
65
|
+
detail = '',
|
|
66
|
+
isSelected = false,
|
|
67
|
+
isIncluded = true,
|
|
68
|
+
onHover,
|
|
69
|
+
} = props;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
data-testid={`element-${symbol}`}
|
|
75
|
+
data-symbol={symbol}
|
|
76
|
+
aria-label={`${name} (${symbol}, Z = ${String(atomicNumber)})`}
|
|
77
|
+
aria-pressed={isSelected}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
onSelect(symbol);
|
|
80
|
+
}}
|
|
81
|
+
onPointerEnter={() => onHover?.(symbol)}
|
|
82
|
+
onPointerLeave={() => onHover?.(null)}
|
|
83
|
+
style={{
|
|
84
|
+
...cellStyle,
|
|
85
|
+
gridColumn: column,
|
|
86
|
+
gridRow: row,
|
|
87
|
+
background: swatch.background,
|
|
88
|
+
color: swatch.foreground,
|
|
89
|
+
opacity: isIncluded ? 1 : 0.28,
|
|
90
|
+
// An outline rather than a fill: a table coloured by a property must
|
|
91
|
+
// keep saying what the value is while a cell is selected.
|
|
92
|
+
outline: isSelected ? `2px solid ${swatch.foreground}` : 'none',
|
|
93
|
+
outlineOffset: -3,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<span style={numberStyle}>{atomicNumber}</span>
|
|
97
|
+
<span style={symbolStyle}>{symbol}</span>
|
|
98
|
+
{detail === '' ? null : <span style={detailStyle}>{detail}</span>}
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cellStyle = {
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
border: '1px solid rgb(255 255 255 / 0.55)',
|
|
106
|
+
borderRadius: 3,
|
|
107
|
+
cursor: 'pointer',
|
|
108
|
+
display: 'flex',
|
|
109
|
+
flexDirection: 'column',
|
|
110
|
+
font: 'inherit',
|
|
111
|
+
justifyContent: 'center',
|
|
112
|
+
lineHeight: 1.05,
|
|
113
|
+
minWidth: 0,
|
|
114
|
+
overflow: 'hidden',
|
|
115
|
+
padding: 1,
|
|
116
|
+
transition: 'opacity 120ms ease',
|
|
117
|
+
} as const satisfies CSSProperties;
|
|
118
|
+
|
|
119
|
+
const numberStyle = {
|
|
120
|
+
alignSelf: 'flex-start',
|
|
121
|
+
fontSize: 'clamp(0.34rem, 1.6cqw, 0.5rem)',
|
|
122
|
+
opacity: 0.8,
|
|
123
|
+
paddingLeft: 2,
|
|
124
|
+
} as const satisfies CSSProperties;
|
|
125
|
+
|
|
126
|
+
const symbolStyle = {
|
|
127
|
+
fontSize: 'clamp(0.55rem, 3.3cqw, 1.05rem)',
|
|
128
|
+
fontWeight: 700,
|
|
129
|
+
} as const satisfies CSSProperties;
|
|
130
|
+
|
|
131
|
+
const detailStyle = {
|
|
132
|
+
fontSize: 'clamp(0.34rem, 1.6cqw, 0.5rem)',
|
|
133
|
+
maxWidth: '100%',
|
|
134
|
+
overflow: 'hidden',
|
|
135
|
+
textOverflow: 'ellipsis',
|
|
136
|
+
whiteSpace: 'nowrap',
|
|
137
|
+
} as const satisfies CSSProperties;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The periodic table.
|
|
3
|
+
*
|
|
4
|
+
* It knows nothing about what it is showing: the caller says what colour each
|
|
5
|
+
* cell takes and what is written in it, and gets back which element was
|
|
6
|
+
* clicked. That is what lets the same grid be an element picker, a property
|
|
7
|
+
* map, and a chart's selection control.
|
|
8
|
+
*
|
|
9
|
+
* Everything site-specific arrives through a callback keyed on the element, so
|
|
10
|
+
* a site's own richer element record never has to cross into this component.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CSSProperties, KeyboardEvent, ReactElement } from 'react';
|
|
14
|
+
import { useEffect, useRef } from 'react';
|
|
15
|
+
|
|
16
|
+
import type { Swatch } from '../../color/core/scale.ts';
|
|
17
|
+
import { categorySwatch } from '../core/categories.ts';
|
|
18
|
+
import type { PeriodicElement } from '../core/elements.ts';
|
|
19
|
+
import type { ElementRange } from '../core/layout.ts';
|
|
20
|
+
import {
|
|
21
|
+
COLUMN_COUNT,
|
|
22
|
+
elementByArrowKey,
|
|
23
|
+
placedElements,
|
|
24
|
+
} from '../core/layout.ts';
|
|
25
|
+
|
|
26
|
+
import { CategoryLegend } from './CategoryLegend.tsx';
|
|
27
|
+
import { ElementCell } from './ElementCell.tsx';
|
|
28
|
+
import {
|
|
29
|
+
HeaderStrips,
|
|
30
|
+
InnerTransitionMarkers,
|
|
31
|
+
} from './PeriodicTableChrome.tsx';
|
|
32
|
+
|
|
33
|
+
/** What {@link PeriodicTable} needs. */
|
|
34
|
+
export interface PeriodicTableProps {
|
|
35
|
+
/**
|
|
36
|
+
* Symbol of the element the tools are pointed at.
|
|
37
|
+
* @default undefined — none is
|
|
38
|
+
*/
|
|
39
|
+
selected?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Called with the symbol of the element that was clicked, and by the arrow
|
|
42
|
+
* keys.
|
|
43
|
+
* @default undefined — the table is a figure rather than a control
|
|
44
|
+
*/
|
|
45
|
+
onSelect?: (symbol: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* The colour each cell takes.
|
|
48
|
+
* @default the family colour
|
|
49
|
+
*/
|
|
50
|
+
swatchOf?: (element: PeriodicElement) => Swatch;
|
|
51
|
+
/**
|
|
52
|
+
* What is written under the symbol; an empty string writes nothing.
|
|
53
|
+
* @default nothing is written
|
|
54
|
+
*/
|
|
55
|
+
detailOf?: (element: PeriodicElement) => string;
|
|
56
|
+
/**
|
|
57
|
+
* How an element is named, for the label a screen reader reads. The hook a
|
|
58
|
+
* site translating the table writes its own names through.
|
|
59
|
+
* @default the English name
|
|
60
|
+
*/
|
|
61
|
+
nameOf?: (element: PeriodicElement) => string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether an element is inside the set the tool is showing. Everything
|
|
64
|
+
* outside it is dimmed rather than removed, so the table keeps its shape.
|
|
65
|
+
* @default every element is
|
|
66
|
+
*/
|
|
67
|
+
isIncluded?: (element: PeriodicElement) => boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Whether to draw the group and period strips.
|
|
70
|
+
* @default false
|
|
71
|
+
*/
|
|
72
|
+
headers?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Called when a whole group or period header is clicked. Without it the
|
|
75
|
+
* strips are labels rather than buttons.
|
|
76
|
+
* @default undefined
|
|
77
|
+
*/
|
|
78
|
+
onSelectRange?: (range: ElementRange) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Whether to draw the family legend under the grid.
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
legend?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the dashed markers stand where the two inner-transition series were
|
|
86
|
+
* lifted out of the main block.
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
markers?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Whether the arrow keys walk the table by atomic number.
|
|
92
|
+
* @default true
|
|
93
|
+
*/
|
|
94
|
+
keyboard?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Called on pointer enter with the symbol, and on leave with `null`.
|
|
97
|
+
* @default undefined
|
|
98
|
+
*/
|
|
99
|
+
onHover?: (symbol: string | null) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The 118 elements, laid out as the table.
|
|
104
|
+
* @param props - See {@link PeriodicTableProps}.
|
|
105
|
+
* @returns The grid, its optional chrome, and its optional legend.
|
|
106
|
+
*/
|
|
107
|
+
export function PeriodicTable(props: PeriodicTableProps): ReactElement {
|
|
108
|
+
const {
|
|
109
|
+
selected,
|
|
110
|
+
onSelect,
|
|
111
|
+
swatchOf = defaultSwatchOf,
|
|
112
|
+
detailOf,
|
|
113
|
+
nameOf = defaultNameOf,
|
|
114
|
+
isIncluded,
|
|
115
|
+
headers = false,
|
|
116
|
+
onSelectRange,
|
|
117
|
+
legend = false,
|
|
118
|
+
markers = true,
|
|
119
|
+
keyboard = true,
|
|
120
|
+
onHover,
|
|
121
|
+
} = props;
|
|
122
|
+
|
|
123
|
+
const gridRef = useRef<HTMLDivElement>(null);
|
|
124
|
+
const cameFromKeyRef = useRef(false);
|
|
125
|
+
const offset = headers ? 1 : 0;
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!cameFromKeyRef.current) return;
|
|
129
|
+
cameFromKeyRef.current = false;
|
|
130
|
+
const cell = gridRef.current?.querySelector<HTMLButtonElement>(
|
|
131
|
+
`[data-symbol="${CSS.escape(selected ?? '')}"]`,
|
|
132
|
+
);
|
|
133
|
+
cell?.focus();
|
|
134
|
+
cell?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
135
|
+
}, [selected]);
|
|
136
|
+
|
|
137
|
+
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>): void {
|
|
138
|
+
if (!keyboard || onSelect === undefined) return;
|
|
139
|
+
const next = elementByArrowKey(event.key, selected);
|
|
140
|
+
if (next === null) return;
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
cameFromKeyRef.current = true;
|
|
143
|
+
onSelect(next.symbol);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div style={rootStyle}>
|
|
148
|
+
<div
|
|
149
|
+
ref={gridRef}
|
|
150
|
+
role="grid"
|
|
151
|
+
aria-label="Periodic table"
|
|
152
|
+
data-testid="periodic-table"
|
|
153
|
+
style={headers ? gridWithHeadersStyle : gridStyle}
|
|
154
|
+
onKeyDown={handleKeyDown}
|
|
155
|
+
>
|
|
156
|
+
{headers ? <HeaderStrips onSelectRange={onSelectRange} /> : null}
|
|
157
|
+
{markers ? <InnerTransitionMarkers offset={offset} /> : null}
|
|
158
|
+
{placedElements().map(({ element, cell }) => (
|
|
159
|
+
<ElementCell
|
|
160
|
+
key={element.symbol}
|
|
161
|
+
atomicNumber={element.atomicNumber}
|
|
162
|
+
symbol={element.symbol}
|
|
163
|
+
name={nameOf(element)}
|
|
164
|
+
detail={detailOf?.(element)}
|
|
165
|
+
swatch={swatchOf(element)}
|
|
166
|
+
isSelected={element.symbol === selected}
|
|
167
|
+
isIncluded={isIncluded?.(element)}
|
|
168
|
+
column={cell.column + offset}
|
|
169
|
+
row={cell.row + offset}
|
|
170
|
+
onSelect={onSelect ?? noop}
|
|
171
|
+
onHover={onHover}
|
|
172
|
+
/>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
{legend ? <CategoryLegend /> : null}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function defaultSwatchOf(element: PeriodicElement): Swatch {
|
|
181
|
+
return categorySwatch(element.category);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function defaultNameOf(element: PeriodicElement): string {
|
|
185
|
+
return element.name;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function noop(): void {
|
|
189
|
+
// A table with no `onSelect` is a figure; its cells stay buttons so the
|
|
190
|
+
// keyboard and a screen reader still reach every element.
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rootStyle = {
|
|
194
|
+
display: 'flex',
|
|
195
|
+
flexDirection: 'column',
|
|
196
|
+
gap: 8,
|
|
197
|
+
} as const satisfies CSSProperties;
|
|
198
|
+
|
|
199
|
+
const baseGridStyle = {
|
|
200
|
+
display: 'grid',
|
|
201
|
+
gap: 2,
|
|
202
|
+
width: '100%',
|
|
203
|
+
// The cells size their type against this box rather than against the page,
|
|
204
|
+
// so the same table reads at 320px beside a chart and at 900px on its own.
|
|
205
|
+
containerType: 'inline-size',
|
|
206
|
+
} as const satisfies CSSProperties;
|
|
207
|
+
|
|
208
|
+
const gridStyle = {
|
|
209
|
+
...baseGridStyle,
|
|
210
|
+
// The eighth row is the gap the inner-transition series are lifted out into.
|
|
211
|
+
gridTemplateColumns: `repeat(${String(COLUMN_COUNT)}, minmax(0, 1fr))`,
|
|
212
|
+
gridTemplateRows:
|
|
213
|
+
'repeat(7, minmax(0, 1fr)) 0.5rem repeat(2, minmax(0, 1fr))',
|
|
214
|
+
aspectRatio: `${String(COLUMN_COUNT)} / 9.7`,
|
|
215
|
+
minWidth: 280,
|
|
216
|
+
} as const satisfies CSSProperties;
|
|
217
|
+
|
|
218
|
+
const gridWithHeadersStyle = {
|
|
219
|
+
...baseGridStyle,
|
|
220
|
+
// A leading column and a leading row hold the period and group numbers.
|
|
221
|
+
gridTemplateColumns: `1.4rem repeat(${String(COLUMN_COUNT)}, minmax(0, 1fr))`,
|
|
222
|
+
gridTemplateRows:
|
|
223
|
+
'1rem repeat(7, minmax(0, 1fr)) 0.5rem repeat(2, minmax(0, 1fr))',
|
|
224
|
+
aspectRatio: `${String(COLUMN_COUNT + 1.2)} / 10.6`,
|
|
225
|
+
minWidth: 280,
|
|
226
|
+
} as const satisfies CSSProperties;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The furniture around the cells: the group and period strips, and the two
|
|
3
|
+
* markers the inner-transition series were lifted out of.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
7
|
+
|
|
8
|
+
import type { ElementRange } from '../core/layout.ts';
|
|
9
|
+
import {
|
|
10
|
+
COLUMN_COUNT,
|
|
11
|
+
INNER_TRANSITION_MARKERS,
|
|
12
|
+
INNER_TRANSITION_ROWS,
|
|
13
|
+
} from '../core/layout.ts';
|
|
14
|
+
|
|
15
|
+
/** What {@link HeaderStrips} needs. */
|
|
16
|
+
export interface HeaderStripsProps {
|
|
17
|
+
/**
|
|
18
|
+
* Called with the run whose header was clicked. Without it the strips are
|
|
19
|
+
* labels rather than buttons.
|
|
20
|
+
* @default undefined
|
|
21
|
+
*/
|
|
22
|
+
onSelectRange?: (range: ElementRange) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The 1–18 strip along the top and the 1–7 strip down the left.
|
|
27
|
+
*
|
|
28
|
+
* They are buttons when the table takes a range: "plot period 3" is one click
|
|
29
|
+
* there and eight on the cells.
|
|
30
|
+
* @param props - See {@link HeaderStripsProps}.
|
|
31
|
+
* @returns The two strips, placed on the grid.
|
|
32
|
+
*/
|
|
33
|
+
export function HeaderStrips(props: HeaderStripsProps): ReactElement {
|
|
34
|
+
const { onSelectRange } = props;
|
|
35
|
+
const groups: ReactElement[] = [];
|
|
36
|
+
for (let group = 1; group <= COLUMN_COUNT; group++) {
|
|
37
|
+
groups.push(
|
|
38
|
+
<HeaderCell
|
|
39
|
+
key={`group-${String(group)}`}
|
|
40
|
+
label={String(group)}
|
|
41
|
+
title={`Group ${String(group)}`}
|
|
42
|
+
column={group + 1}
|
|
43
|
+
row={1}
|
|
44
|
+
onClick={
|
|
45
|
+
onSelectRange &&
|
|
46
|
+
(() => {
|
|
47
|
+
onSelectRange({ kind: 'group', value: group });
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const periods: ReactElement[] = [];
|
|
54
|
+
for (let period = 1; period <= 7; period++) {
|
|
55
|
+
periods.push(
|
|
56
|
+
<HeaderCell
|
|
57
|
+
key={`period-${String(period)}`}
|
|
58
|
+
label={String(period)}
|
|
59
|
+
title={`Period ${String(period)}`}
|
|
60
|
+
column={1}
|
|
61
|
+
row={period + 1}
|
|
62
|
+
onClick={
|
|
63
|
+
onSelectRange &&
|
|
64
|
+
(() => {
|
|
65
|
+
onSelectRange({ kind: 'period', value: period });
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
{groups}
|
|
75
|
+
{periods}
|
|
76
|
+
{INNER_TRANSITION_ROWS.map(({ row, period }) => (
|
|
77
|
+
<div
|
|
78
|
+
key={`inner-${String(row)}`}
|
|
79
|
+
style={{ ...headerStyle, gridColumn: 1, gridRow: row + 1 }}
|
|
80
|
+
>
|
|
81
|
+
{period}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What {@link InnerTransitionMarkers} needs. */
|
|
89
|
+
export interface InnerTransitionMarkersProps {
|
|
90
|
+
/** 1 when the table draws its header strips, 0 otherwise. */
|
|
91
|
+
offset: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The two cells the lanthanoids and the actinoids were lifted out of.
|
|
96
|
+
* @param props - See {@link InnerTransitionMarkersProps}.
|
|
97
|
+
* @returns The markers, placed on the grid.
|
|
98
|
+
*/
|
|
99
|
+
export function InnerTransitionMarkers(
|
|
100
|
+
props: InnerTransitionMarkersProps,
|
|
101
|
+
): ReactElement {
|
|
102
|
+
const { offset } = props;
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{INNER_TRANSITION_MARKERS.map(({ cell, label, category }) => (
|
|
106
|
+
<div
|
|
107
|
+
key={category}
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
style={{
|
|
110
|
+
...markerStyle,
|
|
111
|
+
gridColumn: cell.column + offset,
|
|
112
|
+
gridRow: cell.row + offset,
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{label}
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface HeaderCellProps {
|
|
123
|
+
label: string;
|
|
124
|
+
title: string;
|
|
125
|
+
column: number;
|
|
126
|
+
row: number;
|
|
127
|
+
onClick?: (() => void) | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function HeaderCell(props: HeaderCellProps): ReactElement {
|
|
131
|
+
const { label, title, column, row, onClick } = props;
|
|
132
|
+
const style = { ...headerStyle, gridColumn: column, gridRow: row };
|
|
133
|
+
if (onClick === undefined) return <div style={style}>{label}</div>;
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
aria-label={title}
|
|
138
|
+
onClick={onClick}
|
|
139
|
+
style={{ ...style, ...headerButtonStyle }}
|
|
140
|
+
>
|
|
141
|
+
{label}
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const headerStyle = {
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
color: 'rgb(95 107 124)',
|
|
149
|
+
display: 'flex',
|
|
150
|
+
fontSize: 'clamp(0.4rem, 1.9cqw, 0.62rem)',
|
|
151
|
+
justifyContent: 'center',
|
|
152
|
+
padding: 0,
|
|
153
|
+
} as const satisfies CSSProperties;
|
|
154
|
+
|
|
155
|
+
const headerButtonStyle = {
|
|
156
|
+
background: 'none',
|
|
157
|
+
border: 'none',
|
|
158
|
+
cursor: 'pointer',
|
|
159
|
+
font: 'inherit',
|
|
160
|
+
} as const satisfies CSSProperties;
|
|
161
|
+
|
|
162
|
+
const markerStyle = {
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
border: '1px dashed rgb(182 191 204)',
|
|
165
|
+
borderRadius: 3,
|
|
166
|
+
color: 'rgb(95 107 124)',
|
|
167
|
+
display: 'flex',
|
|
168
|
+
fontSize: 'clamp(0.36rem, 1.7cqw, 0.55rem)',
|
|
169
|
+
justifyContent: 'center',
|
|
170
|
+
} as const satisfies CSSProperties;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CategoryLegendProps } from './CategoryLegend.tsx';
|
|
2
|
+
export { CategoryLegend } from './CategoryLegend.tsx';
|
|
3
|
+
export type { ElementCellProps } from './ElementCell.tsx';
|
|
4
|
+
export { ElementCell } from './ElementCell.tsx';
|
|
5
|
+
export type { PeriodicTableProps } from './PeriodicTable.tsx';
|
|
6
|
+
export { PeriodicTable } from './PeriodicTable.tsx';
|
package/src/ui.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './error/ui/index.ts';
|
|
|
11
11
|
export * from './help/ui/index.ts';
|
|
12
12
|
export * from './hooks/ui/index.ts';
|
|
13
13
|
export * from './pedagogy/ui/index.ts';
|
|
14
|
+
export * from './periodic/ui/index.ts';
|
|
14
15
|
export * from './share/ui/index.ts';
|
|
15
16
|
export * from './shared/ui/index.ts';
|
|
16
17
|
export * from './state/ui/index.ts';
|