matterviz 0.3.4 → 0.3.5
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/dist/FilePicker.svelte +1 -1
- package/dist/app.css +7 -0
- package/dist/brillouin/BrillouinZone.svelte +5 -2
- package/dist/brillouin/compute.js +8 -4
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +6 -6
- package/dist/chempot-diagram/async-compute.svelte.js +5 -4
- package/dist/chempot-diagram/chempot-worker.js +2 -2
- package/dist/chempot-diagram/compute.js +16 -16
- package/dist/composition/FormulaFilter.svelte +3 -3
- package/dist/constants.js +2 -8
- package/dist/convex-hull/ConvexHull.svelte +2 -2
- package/dist/convex-hull/ConvexHull2D.svelte +11 -10
- package/dist/convex-hull/ConvexHull3D.svelte +16 -14
- package/dist/convex-hull/ConvexHull4D.svelte +26 -14
- package/dist/convex-hull/ConvexHullControls.svelte +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +68 -61
- package/dist/convex-hull/ConvexHullStats.svelte +23 -6
- package/dist/convex-hull/GasPressureControls.svelte +3 -3
- package/dist/convex-hull/TemperatureSlider.svelte +1 -1
- package/dist/convex-hull/barycentric-coords.js +2 -2
- package/dist/convex-hull/helpers.js +45 -27
- package/dist/convex-hull/thermodynamics.js +2 -2
- package/dist/element/BohrAtom.svelte +25 -27
- package/dist/element/BohrAtom.svelte.d.ts +2 -2
- package/dist/element/data.d.ts +2 -3
- package/dist/fermi-surface/FermiSurface.svelte +5 -2
- package/dist/fermi-surface/compute.js +3 -3
- package/dist/fermi-surface/parse.js +2 -2
- package/dist/fermi-surface/symmetry.js +1 -1
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +8 -8
- package/dist/icons.d.ts +6 -6
- package/dist/icons.js +6 -6
- package/dist/io/decompress.js +12 -7
- package/dist/io/export.js +20 -16
- package/dist/io/is-binary.js +19 -4
- package/dist/isosurface/parse.js +8 -8
- package/dist/isosurface/types.js +9 -9
- package/dist/layout/InfoTag.svelte +1 -1
- package/dist/layout/json-tree/JsonNode.svelte +1 -0
- package/dist/layout/json-tree/utils.js +2 -1
- package/dist/marching-cubes.js +1 -1
- package/dist/math.js +1 -1
- package/dist/overlays/CopyButton.svelte +45 -0
- package/dist/overlays/CopyButton.svelte.d.ts +8 -0
- package/dist/overlays/InfoPaneCards.svelte +149 -0
- package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +33 -35
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +2 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +27 -29
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +2 -2
- package/dist/phase-diagram/parse.js +3 -3
- package/dist/phase-diagram/svg-to-diagram.js +10 -12
- package/dist/plot/BarPlot.svelte +24 -15
- package/dist/plot/BarPlot.svelte.d.ts +3 -2
- package/dist/plot/FillArea.svelte +2 -3
- package/dist/plot/FillArea.svelte.d.ts +3 -2
- package/dist/plot/Histogram.svelte +37 -19
- package/dist/plot/Line.svelte +2 -3
- package/dist/plot/Line.svelte.d.ts +2 -2
- package/dist/plot/PlotLegend.svelte +79 -8
- package/dist/plot/PlotLegend.svelte.d.ts +4 -0
- package/dist/plot/PortalSelect.svelte +5 -5
- package/dist/plot/ScatterPlot.svelte +47 -33
- package/dist/plot/ScatterPlot.svelte.d.ts +5 -4
- package/dist/plot/ScatterPlot3D.svelte +6 -3
- package/dist/plot/ScatterPoint.svelte +10 -4
- package/dist/plot/ScatterPoint.svelte.d.ts +4 -2
- package/dist/plot/SpacegroupBarPlot.svelte +5 -4
- package/dist/plot/data-cleaning.js +9 -9
- package/dist/plot/index.d.ts +0 -6
- package/dist/plot/scales.d.ts +3 -3
- package/dist/plot/scales.js +29 -29
- package/dist/plot/types.d.ts +5 -9
- package/dist/rdf/calc-rdf.js +1 -1
- package/dist/sanitize.js +22 -15
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +12 -3
- package/dist/spectral/Bands.svelte +6 -6
- package/dist/spectral/BandsAndDos.svelte +4 -4
- package/dist/spectral/BrillouinBandsDos.svelte +3 -3
- package/dist/spectral/Dos.svelte +2 -2
- package/dist/spectral/helpers.js +1 -1
- package/dist/structure/AtomLegend.svelte +4 -4
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Cylinder.svelte +7 -7
- package/dist/structure/Structure.svelte +169 -27
- package/dist/structure/Structure.svelte.d.ts +6 -2
- package/dist/structure/StructureControls.svelte +130 -16
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +519 -218
- package/dist/structure/StructureInfoPane.svelte.d.ts +2 -1
- package/dist/structure/StructureScene.svelte +399 -68
- package/dist/structure/StructureScene.svelte.d.ts +8 -4
- package/dist/structure/atom-properties.js +3 -1
- package/dist/structure/bond-order-perception.d.ts +13 -0
- package/dist/structure/bond-order-perception.js +367 -0
- package/dist/structure/bonding.d.ts +10 -1
- package/dist/structure/bonding.js +232 -11
- package/dist/structure/export.js +6 -4
- package/dist/structure/index.d.ts +19 -4
- package/dist/structure/index.js +3 -0
- package/dist/structure/label-placement.d.ts +14 -0
- package/dist/structure/label-placement.js +72 -0
- package/dist/structure/parse.d.ts +2 -1
- package/dist/structure/parse.js +25 -36
- package/dist/structure/supercell.js +35 -2
- package/dist/symmetry/SymmetryStats.svelte +1 -1
- package/dist/symmetry/cell-transform.js +15 -1
- package/dist/symmetry/index.js +3 -3
- package/dist/table/HeatmapTable.svelte +3 -3
- package/dist/table/ToggleMenu.svelte +1 -1
- package/dist/trajectory/Trajectory.svelte +2 -2
- package/dist/trajectory/TrajectoryInfoPane.svelte +14 -88
- package/dist/trajectory/extract.js +4 -4
- package/dist/trajectory/frame-reader.js +2 -2
- package/dist/trajectory/parse/ase.js +2 -6
- package/dist/trajectory/parse/hdf5.js +1 -3
- package/dist/trajectory/plotting.js +1 -1
- package/dist/utils.js +1 -1
- package/dist/xrd/calc-xrd.js +1 -1
- package/package.json +22 -37
- package/dist/structure/ferrox-wasm-types.d.ts +0 -46
- package/dist/structure/ferrox-wasm-types.js +0 -18
- package/dist/structure/ferrox-wasm.d.ts +0 -94
- package/dist/structure/ferrox-wasm.js +0 -249
|
@@ -1,19 +1,66 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { get_electro_neg_formula } from '../composition'
|
|
3
|
-
import { element_data } from '../element'
|
|
3
|
+
import { element_data, type ElementSymbol } from '../element'
|
|
4
4
|
import Icon from '../Icon.svelte'
|
|
5
5
|
import { format_num } from '../labels'
|
|
6
6
|
import type { InfoItem } from '../layout'
|
|
7
|
+
import CopyButton from '../overlays/CopyButton.svelte'
|
|
7
8
|
import DraggablePane from '../overlays/DraggablePane.svelte'
|
|
9
|
+
import { sanitize_html } from '../sanitize'
|
|
10
|
+
import { colors } from '../state.svelte'
|
|
8
11
|
import type { AnyStructure, Site } from './'
|
|
9
12
|
import { get_density } from './'
|
|
10
13
|
import { wyckoff_positions_from_moyo, WyckoffTable } from '../symmetry'
|
|
11
14
|
import type { MoyoDataset } from '@spglib/moyo-wasm'
|
|
12
15
|
import type { ComponentProps } from 'svelte'
|
|
13
16
|
import type { HTMLAttributes } from 'svelte/elements'
|
|
14
|
-
import { sanitize_html } from '../sanitize'
|
|
15
17
|
import { SvelteSet } from 'svelte/reactivity'
|
|
16
18
|
|
|
19
|
+
type SiteDetail = {
|
|
20
|
+
label: string
|
|
21
|
+
value: string
|
|
22
|
+
key: string
|
|
23
|
+
tooltip?: string
|
|
24
|
+
}
|
|
25
|
+
type SiteCard = {
|
|
26
|
+
idx: number
|
|
27
|
+
element: string
|
|
28
|
+
element_name: string
|
|
29
|
+
title: string
|
|
30
|
+
details: SiteDetail[]
|
|
31
|
+
search_text: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SITE_WINDOW_SIZE = 100
|
|
35
|
+
const USAGE_TIP_ITEMS: InfoItem[] = [
|
|
36
|
+
{
|
|
37
|
+
label: `File Drop`,
|
|
38
|
+
value: `Drop POSCAR, XYZ, CIF or JSON files to load structures`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: `Atom Selection`,
|
|
42
|
+
value:
|
|
43
|
+
`Click atoms to select them, then pick distance or angle mode to measure all pairwise distances/angles`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: `Navigation`,
|
|
47
|
+
value: `Hold Shift/Cmd/Ctrl + drag to pan the scene`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: `Camera Reset`,
|
|
51
|
+
value: `Double-click anywhere to reset camera to default view`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: `Colors`,
|
|
55
|
+
value:
|
|
56
|
+
`Click legend labels to change colors, double-click to reset, right-click to remap elements`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: `Keyboard`,
|
|
60
|
+
value: `Press 'f' for fullscreen, 'i' to toggle this pane`,
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
|
|
17
64
|
let {
|
|
18
65
|
structure,
|
|
19
66
|
pane_open = $bindable(false),
|
|
@@ -21,6 +68,7 @@
|
|
|
21
68
|
toggle_props = {},
|
|
22
69
|
pane_props = {},
|
|
23
70
|
highlighted_sites = $bindable([]),
|
|
71
|
+
hovered_site_idx = $bindable(null),
|
|
24
72
|
selected_sites = $bindable([]),
|
|
25
73
|
sym_data = null,
|
|
26
74
|
...rest
|
|
@@ -31,12 +79,16 @@
|
|
|
31
79
|
toggle_props?: ComponentProps<typeof DraggablePane>[`toggle_props`]
|
|
32
80
|
pane_props?: ComponentProps<typeof DraggablePane>[`pane_props`]
|
|
33
81
|
highlighted_sites?: number[] // Sites highlighted from Wyckoff table hover
|
|
82
|
+
hovered_site_idx?: number | null // Site hovered in this pane or in the 3D scene
|
|
34
83
|
selected_sites?: number[] // Sites selected from Wyckoff table click
|
|
35
84
|
sym_data?: MoyoDataset | null // Symmetry analysis data (bindable for external access)
|
|
36
85
|
} = $props()
|
|
37
86
|
|
|
38
87
|
let copied_items = new SvelteSet<string>()
|
|
39
88
|
let sites_expanded = $state(false)
|
|
89
|
+
let site_filter = $state(``)
|
|
90
|
+
let site_window_start = $state(0)
|
|
91
|
+
let site_cards_el = $state<HTMLDivElement>()
|
|
40
92
|
|
|
41
93
|
async function copy_to_clipboard(label: string, value: string, key: string) {
|
|
42
94
|
try {
|
|
@@ -48,24 +100,122 @@
|
|
|
48
100
|
}
|
|
49
101
|
}
|
|
50
102
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
103
|
+
function copy_event(
|
|
104
|
+
event: MouseEvent,
|
|
105
|
+
label: string,
|
|
106
|
+
value: string,
|
|
107
|
+
key: string,
|
|
108
|
+
) {
|
|
109
|
+
event.stopPropagation()
|
|
110
|
+
copy_to_clipboard(label, value, key)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function copy_info_item(item: InfoItem) {
|
|
114
|
+
copy_to_clipboard(item.label, String(item.value), item.key ?? item.label)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function set_site_hover(site_idx: number | null) {
|
|
118
|
+
highlighted_sites = site_idx === null ? [] : [site_idx]
|
|
119
|
+
hovered_site_idx = site_idx
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function select_site(site_idx: number, event?: MouseEvent | KeyboardEvent) {
|
|
123
|
+
if (event?.shiftKey) {
|
|
124
|
+
selected_sites = selected_sites.includes(site_idx)
|
|
125
|
+
? selected_sites.filter((idx) => idx !== site_idx)
|
|
126
|
+
: [...selected_sites, site_idx]
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
selected_sites = selected_sites.length === 1 && selected_sites[0] === site_idx
|
|
130
|
+
? []
|
|
131
|
+
: [site_idx]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function update_site_filter(event: Event): void {
|
|
135
|
+
if (!(event.currentTarget instanceof HTMLInputElement)) return
|
|
136
|
+
site_filter = event.currentTarget.value
|
|
137
|
+
site_window_start = 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handle_site_keydown(event: KeyboardEvent, card: SiteCard) {
|
|
141
|
+
const plain_key = !event.altKey && !event.ctrlKey && !event.metaKey
|
|
142
|
+
if ([`Enter`, ` `].includes(event.key)) {
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
select_site(card.idx, event)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (event.key === `c` && plain_key) {
|
|
148
|
+
event.preventDefault()
|
|
149
|
+
copy_to_clipboard(card.title, site_summary(card), `site-${card.idx}-summary`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
if (![`ArrowDown`, `ArrowUp`].includes(event.key)) return
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
const current_card = event.currentTarget as HTMLDivElement | null
|
|
155
|
+
const sibling_cards = Array.from(
|
|
156
|
+
current_card?.parentElement?.querySelectorAll<HTMLDivElement>(`.site-card`) ?? [],
|
|
157
|
+
)
|
|
158
|
+
const current_idx = sibling_cards.indexOf(current_card as HTMLDivElement)
|
|
159
|
+
const next_idx = event.key === `ArrowDown`
|
|
160
|
+
? Math.min(current_idx + 1, sibling_cards.length - 1)
|
|
161
|
+
: Math.max(current_idx - 1, 0)
|
|
162
|
+
sibling_cards[next_idx]?.focus()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function get_element_name(element: string): string {
|
|
166
|
+
return element_data?.find((el) => el.symbol === element)?.name || element
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function site_summary(card: SiteCard): string {
|
|
170
|
+
return [
|
|
171
|
+
card.element_name,
|
|
172
|
+
...card.details.map(({ label, value }) => `${label}: ${value}`),
|
|
173
|
+
].join(`; `)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function format_site_property(prop_key: string, prop_value: unknown): SiteDetail | null {
|
|
177
|
+
if (prop_value == null) return null
|
|
178
|
+
const format_numeric_value = (value: unknown, format = `.3~f`): string | null => {
|
|
179
|
+
const numeric_value = Number(value)
|
|
180
|
+
return Number.isNaN(numeric_value) ? null : format_num(numeric_value, format)
|
|
181
|
+
}
|
|
182
|
+
const format_value_list = (values: unknown[]): string =>
|
|
183
|
+
`(${values.map((value) => format_numeric_value(value) ?? String(value)).join(`, `)})`
|
|
184
|
+
let tooltip: string | undefined
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
prop_key === `force` && Array.isArray(prop_value) &&
|
|
188
|
+
prop_value.length === 3 && prop_value.every((value) => typeof value === `number`)
|
|
189
|
+
) {
|
|
190
|
+
const force_values = prop_value as [number, number, number]
|
|
191
|
+
const value = `${format_num(Math.hypot(...force_values), `.3~f`)} eV/Å`
|
|
192
|
+
tooltip = `Force vector: ${
|
|
193
|
+
force_values.map((force) => format_num(force, `.3~f`)).join(`, `)
|
|
194
|
+
} eV/Å`
|
|
195
|
+
return { label: prop_key, value, key: prop_key, tooltip }
|
|
196
|
+
}
|
|
197
|
+
if (prop_key === `magmom` || prop_key.includes(`magnet`)) {
|
|
198
|
+
const formatted_value = format_numeric_value(prop_value)
|
|
199
|
+
if (!formatted_value) return null
|
|
200
|
+
tooltip = `Magnetic moment in Bohr magnetons`
|
|
201
|
+
return { label: prop_key, value: `${formatted_value} μB`, key: prop_key, tooltip }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const value = Array.isArray(prop_value)
|
|
205
|
+
? format_value_list(prop_value)
|
|
206
|
+
: format_numeric_value(prop_value) ?? String(prop_value)
|
|
207
|
+
return { label: prop_key, value, key: prop_key }
|
|
55
208
|
}
|
|
56
209
|
|
|
57
210
|
let pane_data = $derived.by(() => {
|
|
58
211
|
if (!structure) return []
|
|
59
212
|
const sections: { title: string; items: InfoItem[] }[] = []
|
|
60
|
-
const [min_threshold, max_threshold] = atom_count_thresholds
|
|
61
213
|
|
|
62
214
|
// Structure Info
|
|
63
215
|
const structure_items: InfoItem[] = [
|
|
64
216
|
{
|
|
65
217
|
label: `Formula`,
|
|
66
|
-
value: `${
|
|
67
|
-
get_electro_neg_formula(structure)
|
|
68
|
-
} (${structure.sites.length} sites)`,
|
|
218
|
+
value: `${get_electro_neg_formula(structure)} (${structure.sites.length} sites)`,
|
|
69
219
|
key: `structure-formula`,
|
|
70
220
|
},
|
|
71
221
|
{
|
|
@@ -80,10 +230,7 @@
|
|
|
80
230
|
// Only display scalar values (skip arrays and objects)
|
|
81
231
|
if (value == null || typeof value === `object`) continue
|
|
82
232
|
structure_items.push({
|
|
83
|
-
label: key.replace(/_/g, ` `).replace(
|
|
84
|
-
/\b\w/g,
|
|
85
|
-
(char) => char.toUpperCase(),
|
|
86
|
-
),
|
|
233
|
+
label: key.replace(/_/g, ` `).replace(/\b\w/g, (char) => char.toUpperCase()),
|
|
87
234
|
value: String(value),
|
|
88
235
|
key: `structure-prop-${key}`,
|
|
89
236
|
})
|
|
@@ -99,23 +246,17 @@
|
|
|
99
246
|
items: [
|
|
100
247
|
{
|
|
101
248
|
label: `Volume, Density`,
|
|
102
|
-
value: `${format_num(volume, `.3~s`)} ų, ${
|
|
103
|
-
format_num(get_density(structure), `.3~f`)
|
|
104
|
-
} g/cm³`,
|
|
249
|
+
value: `${format_num(volume, `.3~s`)} ų, ${format_num(get_density(structure), `.3~f`)} g/cm³`,
|
|
105
250
|
key: `cell-volume-density`,
|
|
106
251
|
},
|
|
107
252
|
{
|
|
108
253
|
label: `a, b, c`,
|
|
109
|
-
value: `${format_num(a, `.4~f`)}, ${format_num(b, `.4~f`)}, ${
|
|
110
|
-
format_num(c, `.4~f`)
|
|
111
|
-
} Å`,
|
|
254
|
+
value: `${format_num(a, `.4~f`)}, ${format_num(b, `.4~f`)}, ${format_num(c, `.4~f`)} Å`,
|
|
112
255
|
key: `cell-abc`,
|
|
113
256
|
},
|
|
114
257
|
{
|
|
115
258
|
label: `α, β, γ`,
|
|
116
|
-
value: `${format_num(alpha, `.2~f`)}°, ${format_num(beta, `.2~f`)}°, ${
|
|
117
|
-
format_num(gamma, `.2~f`)
|
|
118
|
-
}°`,
|
|
259
|
+
value: `${format_num(alpha, `.2~f`)}°, ${format_num(beta, `.2~f`)}°, ${format_num(gamma, `.2~f`)}°`,
|
|
119
260
|
key: `cell-angles`,
|
|
120
261
|
},
|
|
121
262
|
],
|
|
@@ -149,169 +290,104 @@
|
|
|
149
290
|
sections.push({
|
|
150
291
|
title: `Symmetry`,
|
|
151
292
|
items: [
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
key: `symmetry-space-group`,
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
label: `Hall Number`,
|
|
159
|
-
value: String(sym_data.hall_number),
|
|
160
|
-
key: `symmetry-hall-number`,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
label: `Pearson Symbol`,
|
|
164
|
-
value: sym_data.pearson_symbol,
|
|
165
|
-
key: `symmetry-pearson-symbol`,
|
|
166
|
-
},
|
|
293
|
+
{ label: `Space Group`, value: space_group_value, key: `symmetry-space-group` },
|
|
294
|
+
{ label: `Hall Number`, value: String(sym_data.hall_number), key: `symmetry-hall-number` },
|
|
295
|
+
{ label: `Pearson Symbol`, value: sym_data.pearson_symbol, key: `symmetry-pearson-symbol` },
|
|
167
296
|
{
|
|
168
297
|
label: `Symmetry Ops`,
|
|
169
|
-
value:
|
|
170
|
-
`${operations.length} (${translations} trans, ${rotations} rot, ${roto_translations} roto-trans)`,
|
|
298
|
+
value: `${operations.length} (${translations} trans, ${rotations} rot, ${roto_translations} roto-trans)`,
|
|
171
299
|
key: `symmetry-operations-total`,
|
|
172
300
|
},
|
|
173
301
|
],
|
|
174
302
|
})
|
|
175
303
|
}
|
|
176
304
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (atom_count <= max_threshold) {
|
|
180
|
-
const site_items: InfoItem[] = []
|
|
181
|
-
|
|
182
|
-
// Merged toggle button with Sites title
|
|
183
|
-
if (atom_count >= min_threshold) {
|
|
184
|
-
const toggle_label = sites_expanded
|
|
185
|
-
? `Hide Sites`
|
|
186
|
-
: `Show ${atom_count} sites`
|
|
187
|
-
site_items.push({
|
|
188
|
-
label: toggle_label,
|
|
189
|
-
value: sites_expanded ? `▲` : `▼`,
|
|
190
|
-
key: `sites-toggle`,
|
|
191
|
-
tooltip: `Click to ${
|
|
192
|
-
sites_expanded ? `hide` : `show`
|
|
193
|
-
} all site information`,
|
|
194
|
-
})
|
|
195
|
-
}
|
|
305
|
+
return sections
|
|
306
|
+
})
|
|
196
307
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
label: `${element}${idx + 1}`,
|
|
206
|
-
value: element_name,
|
|
207
|
-
key: `site-${idx}-header`,
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
if (site.abc) {
|
|
211
|
-
site_items.push({
|
|
212
|
-
label: ` Fractional`,
|
|
213
|
-
value: `(${site.abc.map((x) => format_num(x, `.4~f`)).join(`, `)})`,
|
|
214
|
-
key: `site-${idx}-fractional`,
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
if (site.xyz) {
|
|
218
|
-
site_items.push({
|
|
219
|
-
label: ` Cartesian`,
|
|
220
|
-
value: `(${site.xyz.map((x) => format_num(x, `.4~f`)).join(`, `)}) Å`,
|
|
221
|
-
key: `site-${idx}-cartesian`,
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (site.properties) {
|
|
226
|
-
for (const [prop_key, prop_value] of Object.entries(site.properties)) {
|
|
227
|
-
if (prop_value != null && prop_value !== undefined) {
|
|
228
|
-
let formatted_value: string
|
|
229
|
-
let tooltip: string | undefined
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
prop_key === `force` && Array.isArray(prop_value) &&
|
|
233
|
-
prop_value.length === 3 && prop_value.every((v) =>
|
|
234
|
-
typeof v === `number`
|
|
235
|
-
)
|
|
236
|
-
) {
|
|
237
|
-
const force_magnitude = Math.hypot(...prop_value)
|
|
238
|
-
formatted_value = `${format_num(force_magnitude, `.3~f`)} eV/Å`
|
|
239
|
-
tooltip = `Force vector: (${
|
|
240
|
-
prop_value.map((force) => format_num(force, `.3~f`)).join(`, `)
|
|
241
|
-
}) eV/Å`
|
|
242
|
-
} else if (prop_key === `magmom` || prop_key.includes(`magnet`)) {
|
|
243
|
-
const num_val = Number(prop_value)
|
|
244
|
-
if (isNaN(num_val)) continue
|
|
245
|
-
formatted_value = `${format_num(num_val, `.3~f`)} μB`
|
|
246
|
-
tooltip = `Magnetic moment in Bohr magnetons`
|
|
247
|
-
} else if (Array.isArray(prop_value)) {
|
|
248
|
-
formatted_value = `(${
|
|
249
|
-
prop_value.map((v) => {
|
|
250
|
-
const num_val = Number(v)
|
|
251
|
-
return isNaN(num_val) ? String(v) : format_num(num_val, `.3~f`)
|
|
252
|
-
}).join(`, `)
|
|
253
|
-
})`
|
|
254
|
-
} else {
|
|
255
|
-
const num_val = Number(prop_value)
|
|
256
|
-
formatted_value = isNaN(num_val)
|
|
257
|
-
? String(prop_value)
|
|
258
|
-
: format_num(num_val, `.3~f`)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
site_items.push({
|
|
262
|
-
label: ` ${prop_key}`,
|
|
263
|
-
value: formatted_value,
|
|
264
|
-
key: `site-${idx}-${prop_key}`,
|
|
265
|
-
tooltip,
|
|
266
|
-
})
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
}
|
|
308
|
+
let atom_count = $derived(structure?.sites.length ?? 0)
|
|
309
|
+
let sites_allowed_by_threshold = $derived(atom_count <= atom_count_thresholds[1])
|
|
310
|
+
let sites_need_toggle = $derived(
|
|
311
|
+
sites_allowed_by_threshold && atom_count >= atom_count_thresholds[0],
|
|
312
|
+
)
|
|
313
|
+
let site_cards_visible = $derived(
|
|
314
|
+
sites_allowed_by_threshold && (!sites_need_toggle || sites_expanded),
|
|
315
|
+
)
|
|
272
316
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
317
|
+
let site_cards = $derived.by((): SiteCard[] => {
|
|
318
|
+
if (!structure || !site_cards_visible) return []
|
|
319
|
+
return structure.sites.map((site: Site, idx: number) => {
|
|
320
|
+
const element = site.species?.[0]?.element || `Unknown`
|
|
321
|
+
const element_name = get_element_name(element)
|
|
322
|
+
const details: SiteDetail[] = []
|
|
323
|
+
for (const [label, key, coords, unit] of [
|
|
324
|
+
[`Fractional`, `fractional`, site.abc, ``],
|
|
325
|
+
[`Cartesian`, `cartesian`, site.xyz, ` Å`],
|
|
326
|
+
] as const) {
|
|
327
|
+
if (!coords) continue
|
|
328
|
+
details.push({
|
|
329
|
+
label,
|
|
330
|
+
key,
|
|
331
|
+
value: `(${coords.map((coord) => format_num(coord, `.4~f`)).join(`, `)})${unit}`,
|
|
277
332
|
})
|
|
278
333
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
value: `Hold Shift/Cmd/Ctrl + drag to pan the scene`,
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
label: `Camera Reset`,
|
|
300
|
-
value: `Double-click anywhere to reset camera to default view`,
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
label: `Colors`,
|
|
304
|
-
value:
|
|
305
|
-
`Click legend labels to change colors, double-click to reset, right-click to remap elements`,
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
label: `Keyboard`,
|
|
309
|
-
value: `Press 'f' for fullscreen, 'i' to toggle this pane`,
|
|
310
|
-
},
|
|
311
|
-
],
|
|
334
|
+
if (site.properties) {
|
|
335
|
+
for (const [prop_key, prop_value] of Object.entries(site.properties)) {
|
|
336
|
+
const detail = format_site_property(prop_key, prop_value)
|
|
337
|
+
if (detail) details.push(detail)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const title = `${element}${idx + 1}`
|
|
341
|
+
return {
|
|
342
|
+
idx,
|
|
343
|
+
element,
|
|
344
|
+
element_name,
|
|
345
|
+
title,
|
|
346
|
+
details,
|
|
347
|
+
search_text: `${title} ${element} ${element_name} ${
|
|
348
|
+
details.map(({ label, value }) => `${label} ${value}`).join(` `)
|
|
349
|
+
}`.toLowerCase(),
|
|
350
|
+
}
|
|
312
351
|
})
|
|
352
|
+
})
|
|
313
353
|
|
|
314
|
-
|
|
354
|
+
let visible_site_cards = $derived.by(() => {
|
|
355
|
+
const filter = site_filter.trim().toLowerCase()
|
|
356
|
+
if (!filter) return site_cards
|
|
357
|
+
return site_cards.filter(({ search_text }) => search_text.includes(filter))
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
let rendered_site_cards = $derived(
|
|
361
|
+
visible_site_cards.slice(site_window_start, site_window_start + SITE_WINDOW_SIZE),
|
|
362
|
+
)
|
|
363
|
+
let site_window_end = $derived(
|
|
364
|
+
Math.min(site_window_start + SITE_WINDOW_SIZE, visible_site_cards.length),
|
|
365
|
+
)
|
|
366
|
+
let sites_hidden_by_threshold = $derived(sites_need_toggle && !sites_expanded)
|
|
367
|
+
let show_sites_section = $derived(
|
|
368
|
+
site_cards.length > 0 || sites_hidden_by_threshold || sites_need_toggle,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
$effect(() => {
|
|
372
|
+
if (site_window_start >= visible_site_cards.length) {
|
|
373
|
+
site_window_start = Math.max(0, visible_site_cards.length - SITE_WINDOW_SIZE)
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
$effect(() => {
|
|
378
|
+
const selected_site_idx = selected_sites[0]
|
|
379
|
+
if (!pane_open || selected_site_idx === undefined) return
|
|
380
|
+
const visible_idx = visible_site_cards.findIndex(({ idx }) => idx === selected_site_idx)
|
|
381
|
+
if (visible_idx < 0) return
|
|
382
|
+
const selected_window_start = Math.floor(visible_idx / SITE_WINDOW_SIZE) *
|
|
383
|
+
SITE_WINDOW_SIZE
|
|
384
|
+
if (selected_window_start !== site_window_start) {
|
|
385
|
+
site_window_start = selected_window_start
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
site_cards_el
|
|
389
|
+
?.querySelector(`[data-site-idx="${selected_site_idx}"]`)
|
|
390
|
+
?.scrollIntoView({ block: `nearest` })
|
|
315
391
|
})
|
|
316
392
|
|
|
317
393
|
// Compute Wyckoff positions from symmetry data
|
|
@@ -340,38 +416,29 @@
|
|
|
340
416
|
{/if}
|
|
341
417
|
{#each section.items as item (item.key ?? item.label)}
|
|
342
418
|
{@const { key, label, value, tooltip } = item}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
{#if key !== `sites-toggle` && key && copied_items.has(key)}
|
|
367
|
-
<Icon
|
|
368
|
-
icon="Check"
|
|
369
|
-
style="color: var(--success-color, #10b981); width: 12px; height: 12px"
|
|
370
|
-
class="copy-checkmark"
|
|
371
|
-
/>
|
|
372
|
-
{/if}
|
|
373
|
-
</div>
|
|
374
|
-
{/if}
|
|
419
|
+
<div
|
|
420
|
+
class="info-row clickable"
|
|
421
|
+
title={`Click to copy: ${label}: ${value}`}
|
|
422
|
+
onclick={() => copy_info_item(item)}
|
|
423
|
+
role="button"
|
|
424
|
+
tabindex="0"
|
|
425
|
+
onkeydown={(event) => {
|
|
426
|
+
if ([`Enter`, ` `].includes(event.key)) {
|
|
427
|
+
event.preventDefault()
|
|
428
|
+
copy_info_item(item)
|
|
429
|
+
}
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
<span>{@html sanitize_html(label)}</span>
|
|
433
|
+
<span title={tooltip}>{@html sanitize_html(value)}</span>
|
|
434
|
+
{#if key && copied_items.has(key)}
|
|
435
|
+
<Icon
|
|
436
|
+
icon="Check"
|
|
437
|
+
style="color: var(--success-color, #10b981); width: 12px; height: 12px"
|
|
438
|
+
class="copy-checkmark"
|
|
439
|
+
/>
|
|
440
|
+
{/if}
|
|
441
|
+
</div>
|
|
375
442
|
{/each}
|
|
376
443
|
|
|
377
444
|
{#if section.title === `Symmetry` && wyckoff_positions.length > 0}
|
|
@@ -384,22 +451,261 @@
|
|
|
384
451
|
{/if}
|
|
385
452
|
</section>
|
|
386
453
|
{/each}
|
|
454
|
+
|
|
455
|
+
{#if show_sites_section}
|
|
456
|
+
<hr />
|
|
457
|
+
<section class="sites-section">
|
|
458
|
+
<div class="sites-header">
|
|
459
|
+
<h4 id="sites">Sites</h4>
|
|
460
|
+
{#if sites_need_toggle}
|
|
461
|
+
<button
|
|
462
|
+
type="button"
|
|
463
|
+
class="sites-toggle"
|
|
464
|
+
onclick={() => (sites_expanded = !sites_expanded)}
|
|
465
|
+
title="{sites_expanded ? `Hide` : `Show`} all site information"
|
|
466
|
+
>
|
|
467
|
+
{sites_expanded ? `Hide` : `Show ${structure.sites.length} sites`}
|
|
468
|
+
</button>
|
|
469
|
+
{/if}
|
|
470
|
+
</div>
|
|
471
|
+
{#if sites_hidden_by_threshold}
|
|
472
|
+
<p class="sites-note">Site list hidden for this {structure.sites.length}-site structure.</p>
|
|
473
|
+
{:else if site_cards.length > 0}
|
|
474
|
+
<input
|
|
475
|
+
class="site-filter"
|
|
476
|
+
type="search"
|
|
477
|
+
value={site_filter}
|
|
478
|
+
oninput={update_site_filter}
|
|
479
|
+
placeholder="Filter sites by element, index, coordinate, or property"
|
|
480
|
+
aria-label="Filter sites"
|
|
481
|
+
/>
|
|
482
|
+
{#if visible_site_cards.length === 0}
|
|
483
|
+
<p class="sites-note">No sites match "{site_filter}".</p>
|
|
484
|
+
{:else}
|
|
485
|
+
{#if visible_site_cards.length > SITE_WINDOW_SIZE}
|
|
486
|
+
<div class="site-window-controls">
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
disabled={site_window_start === 0}
|
|
490
|
+
onclick={() =>
|
|
491
|
+
site_window_start = Math.max(0, site_window_start - SITE_WINDOW_SIZE)}
|
|
492
|
+
>
|
|
493
|
+
Previous
|
|
494
|
+
</button>
|
|
495
|
+
<span>{site_window_start + 1}-{site_window_end} of {visible_site_cards.length}</span>
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
498
|
+
disabled={site_window_end >= visible_site_cards.length}
|
|
499
|
+
onclick={() =>
|
|
500
|
+
site_window_start = Math.min(
|
|
501
|
+
Math.max(0, visible_site_cards.length - SITE_WINDOW_SIZE),
|
|
502
|
+
site_window_start + SITE_WINDOW_SIZE,
|
|
503
|
+
)}
|
|
504
|
+
>
|
|
505
|
+
Next
|
|
506
|
+
</button>
|
|
507
|
+
</div>
|
|
508
|
+
{/if}
|
|
509
|
+
<div class="site-cards" bind:this={site_cards_el}>
|
|
510
|
+
{#each rendered_site_cards as card (card.idx)}
|
|
511
|
+
{@const is_highlighted = highlighted_sites.includes(card.idx) ||
|
|
512
|
+
hovered_site_idx === card.idx}
|
|
513
|
+
{@const is_selected = selected_sites.includes(card.idx)}
|
|
514
|
+
<div
|
|
515
|
+
class="site-card"
|
|
516
|
+
class:highlighted={is_highlighted}
|
|
517
|
+
class:selected={is_selected}
|
|
518
|
+
data-site-idx={card.idx}
|
|
519
|
+
style:--site-color={colors.element?.[card.element as ElementSymbol] ?? `#888`}
|
|
520
|
+
title="Click to select {card.title}. Press c to copy."
|
|
521
|
+
role="button"
|
|
522
|
+
tabindex="0"
|
|
523
|
+
onmouseenter={() => set_site_hover(card.idx)}
|
|
524
|
+
onmouseleave={() => set_site_hover(null)}
|
|
525
|
+
onfocus={() => set_site_hover(card.idx)}
|
|
526
|
+
onblur={() => set_site_hover(null)}
|
|
527
|
+
onclick={(event) => select_site(card.idx, event)}
|
|
528
|
+
onkeydown={(event) => handle_site_keydown(event, card)}
|
|
529
|
+
>
|
|
530
|
+
<div class="site-card-header">
|
|
531
|
+
<span class="site-title">
|
|
532
|
+
<span class="site-color" aria-hidden="true"></span>
|
|
533
|
+
<strong>{card.title}</strong>
|
|
534
|
+
<span>{card.element_name}</span>
|
|
535
|
+
</span>
|
|
536
|
+
<CopyButton
|
|
537
|
+
label="Copy {card.title}"
|
|
538
|
+
title="Copy {card.title}"
|
|
539
|
+
copied={copied_items.has(`site-${card.idx}-summary`)}
|
|
540
|
+
onclick={(event) =>
|
|
541
|
+
copy_event(event, card.title, site_summary(card), `site-${card.idx}-summary`)}
|
|
542
|
+
/>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="site-card-details">
|
|
545
|
+
{#each card.details as detail (`site-${card.idx}-${detail.key}`)}
|
|
546
|
+
<div class="site-detail">
|
|
547
|
+
<span>{@html sanitize_html(detail.label)}</span>
|
|
548
|
+
<span title={detail.tooltip}>{@html sanitize_html(detail.value)}</span>
|
|
549
|
+
<CopyButton
|
|
550
|
+
label="Copy {card.title} {detail.label}"
|
|
551
|
+
title="Copy {detail.label}"
|
|
552
|
+
copied={copied_items.has(`site-${card.idx}-${detail.key}`)}
|
|
553
|
+
onclick={(event) =>
|
|
554
|
+
copy_event(
|
|
555
|
+
event,
|
|
556
|
+
`${card.title} ${detail.label}`,
|
|
557
|
+
detail.value,
|
|
558
|
+
`site-${card.idx}-${detail.key}`,
|
|
559
|
+
)}
|
|
560
|
+
/>
|
|
561
|
+
</div>
|
|
562
|
+
{/each}
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
{/each}
|
|
566
|
+
</div>
|
|
567
|
+
{/if}
|
|
568
|
+
{/if}
|
|
569
|
+
</section>
|
|
570
|
+
{/if}
|
|
571
|
+
|
|
572
|
+
<hr />
|
|
573
|
+
<section>
|
|
574
|
+
<h4 id="usage-tips">Usage Tips</h4>
|
|
575
|
+
{#each USAGE_TIP_ITEMS as { label, value } (label)}
|
|
576
|
+
<div class="tips-item">
|
|
577
|
+
<span>{@html sanitize_html(label)}</span>
|
|
578
|
+
<span>{@html sanitize_html(value)}</span>
|
|
579
|
+
</div>
|
|
580
|
+
{/each}
|
|
581
|
+
</section>
|
|
387
582
|
</DraggablePane>
|
|
388
583
|
|
|
389
584
|
<style>
|
|
390
|
-
|
|
585
|
+
.info-row,
|
|
586
|
+
.tips-item {
|
|
391
587
|
display: flex;
|
|
392
588
|
justify-content: space-between;
|
|
393
589
|
gap: 6pt;
|
|
394
590
|
padding: 1pt;
|
|
395
591
|
line-height: 1.5;
|
|
396
592
|
}
|
|
397
|
-
|
|
593
|
+
.info-row.clickable {
|
|
594
|
+
cursor: pointer;
|
|
595
|
+
position: relative;
|
|
596
|
+
&:hover {
|
|
597
|
+
background: var(--pane-btn-bg-hover, rgba(255, 255, 255, 0.03));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
.sites-header {
|
|
601
|
+
display: flex;
|
|
602
|
+
align-items: center;
|
|
603
|
+
justify-content: space-between;
|
|
604
|
+
gap: 6pt;
|
|
605
|
+
h4 {
|
|
606
|
+
margin: 0.5em 0;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
.sites-toggle,
|
|
610
|
+
.site-window-controls button {
|
|
611
|
+
border: 0;
|
|
612
|
+
border-radius: var(--border-radius, 3pt);
|
|
613
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
614
|
+
color: inherit;
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
}
|
|
617
|
+
.sites-toggle {
|
|
618
|
+
padding: 2pt 5pt;
|
|
619
|
+
font-size: 0.8em;
|
|
620
|
+
}
|
|
621
|
+
.site-filter {
|
|
622
|
+
box-sizing: border-box;
|
|
623
|
+
width: 100%;
|
|
624
|
+
margin-bottom: 5pt;
|
|
625
|
+
padding: 4pt 6pt;
|
|
626
|
+
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
627
|
+
border-radius: var(--border-radius, 3pt);
|
|
628
|
+
background: color-mix(in srgb, var(--pane-bg, Canvas) 88%, currentColor);
|
|
629
|
+
color: inherit;
|
|
630
|
+
}
|
|
631
|
+
.sites-note {
|
|
632
|
+
margin: 0.25em 0 0.5em;
|
|
633
|
+
opacity: 0.75;
|
|
634
|
+
font-size: 0.85em;
|
|
635
|
+
}
|
|
636
|
+
.site-window-controls {
|
|
637
|
+
display: flex;
|
|
638
|
+
align-items: center;
|
|
639
|
+
justify-content: space-between;
|
|
640
|
+
gap: 5pt;
|
|
641
|
+
margin-bottom: 5pt;
|
|
642
|
+
font-size: 0.8em;
|
|
643
|
+
button {
|
|
644
|
+
padding: 2pt 5pt;
|
|
645
|
+
&:disabled {
|
|
646
|
+
cursor: not-allowed;
|
|
647
|
+
opacity: 0.45;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
.site-cards {
|
|
652
|
+
display: grid;
|
|
653
|
+
gap: 5pt;
|
|
654
|
+
}
|
|
655
|
+
.site-card {
|
|
656
|
+
border-left: 3px solid var(--site-color, #888);
|
|
657
|
+
border-radius: var(--border-radius, 3pt);
|
|
658
|
+
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
659
|
+
padding: 5pt;
|
|
398
660
|
cursor: pointer;
|
|
399
|
-
|
|
661
|
+
outline: none;
|
|
662
|
+
&:is(:hover, :focus-visible, .highlighted) {
|
|
663
|
+
background: color-mix(in srgb, var(--site-color, currentColor) 18%, transparent);
|
|
664
|
+
}
|
|
665
|
+
&.selected {
|
|
666
|
+
box-shadow: inset 0 0 0 1px var(--site-color, currentColor);
|
|
667
|
+
background: color-mix(in srgb, var(--site-color, currentColor) 25%, transparent);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
.site-card-header,
|
|
671
|
+
.site-title,
|
|
672
|
+
.site-detail {
|
|
673
|
+
display: flex;
|
|
674
|
+
align-items: center;
|
|
675
|
+
gap: 5pt;
|
|
400
676
|
}
|
|
401
|
-
|
|
402
|
-
|
|
677
|
+
.site-card-header {
|
|
678
|
+
justify-content: space-between;
|
|
679
|
+
}
|
|
680
|
+
.site-title {
|
|
681
|
+
min-width: 0;
|
|
682
|
+
span:last-child {
|
|
683
|
+
opacity: 0.75;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
.site-color {
|
|
687
|
+
width: 0.75em;
|
|
688
|
+
height: 0.75em;
|
|
689
|
+
flex: 0 0 auto;
|
|
690
|
+
border-radius: 50%;
|
|
691
|
+
background: var(--site-color, #888);
|
|
692
|
+
}
|
|
693
|
+
.site-card-details {
|
|
694
|
+
display: grid;
|
|
695
|
+
gap: 2pt;
|
|
696
|
+
margin-top: 3pt;
|
|
697
|
+
font-size: 0.86em;
|
|
698
|
+
}
|
|
699
|
+
.site-detail {
|
|
700
|
+
justify-content: space-between;
|
|
701
|
+
span:first-child {
|
|
702
|
+
opacity: 0.75;
|
|
703
|
+
}
|
|
704
|
+
span:nth-child(2) {
|
|
705
|
+
overflow: hidden;
|
|
706
|
+
text-overflow: ellipsis;
|
|
707
|
+
white-space: nowrap;
|
|
708
|
+
}
|
|
403
709
|
}
|
|
404
710
|
section :global(.copy-checkmark) {
|
|
405
711
|
position: absolute;
|
|
@@ -419,16 +725,11 @@
|
|
|
419
725
|
opacity: 0;
|
|
420
726
|
}
|
|
421
727
|
}
|
|
422
|
-
|
|
423
|
-
border-left: 2px solid #3b82f6;
|
|
424
|
-
margin-left: 10pt;
|
|
425
|
-
padding-left: 6pt;
|
|
426
|
-
}
|
|
427
|
-
section div.tips-item {
|
|
728
|
+
.tips-item {
|
|
428
729
|
flex-direction: column;
|
|
429
730
|
gap: 2pt;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
731
|
+
span:last-child {
|
|
732
|
+
opacity: 0.8;
|
|
733
|
+
}
|
|
433
734
|
}
|
|
434
735
|
</style>
|