matterviz 0.3.0 → 0.3.2
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 +37 -20
- package/dist/Icon.svelte +2 -2
- package/dist/MillerIndexInput.svelte +60 -0
- package/dist/MillerIndexInput.svelte.d.ts +7 -0
- package/dist/app.css +38 -2
- package/dist/brillouin/BrillouinZone.svelte +20 -62
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
- package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- package/dist/chempot-diagram/color.d.ts +10 -0
- package/dist/chempot-diagram/color.js +33 -0
- package/dist/chempot-diagram/compute.d.ts +38 -0
- package/dist/chempot-diagram/compute.js +650 -0
- package/dist/chempot-diagram/index.d.ts +5 -0
- package/dist/chempot-diagram/index.js +5 -0
- package/dist/chempot-diagram/pointer.d.ts +16 -0
- package/dist/chempot-diagram/pointer.js +40 -0
- package/dist/chempot-diagram/temperature.d.ts +15 -0
- package/dist/chempot-diagram/temperature.js +37 -0
- package/dist/chempot-diagram/types.d.ts +83 -0
- package/dist/chempot-diagram/types.js +27 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +4 -0
- package/dist/composition/BarChart.svelte +13 -22
- package/dist/composition/BubbleChart.svelte +5 -3
- package/dist/composition/FormulaFilter.svelte +770 -90
- package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
- package/dist/composition/PieChart.svelte +43 -18
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/convex-hull/ConvexHull.svelte +14 -1
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +14 -45
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +396 -134
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +93 -42
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +94 -31
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
- package/dist/convex-hull/ConvexHullStats.svelte +697 -128
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
- package/dist/convex-hull/GasPressureControls.svelte +72 -38
- package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
- package/dist/convex-hull/TemperatureSlider.svelte +46 -19
- package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
- package/dist/convex-hull/demo-temperature.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +36 -0
- package/dist/convex-hull/gas-thermodynamics.js +16 -5
- package/dist/convex-hull/helpers.d.ts +7 -1
- package/dist/convex-hull/helpers.js +45 -15
- package/dist/convex-hull/index.d.ts +15 -1
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +8 -21
- package/dist/convex-hull/thermodynamics.js +106 -17
- package/dist/convex-hull/types.d.ts +7 -0
- package/dist/convex-hull/types.js +11 -0
- package/dist/coordination/CoordinationBarPlot.svelte +29 -46
- package/dist/element/BohrAtom.svelte +1 -1
- package/dist/element/data.js +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -0
- package/dist/element/types.d.ts +1 -0
- package/dist/fermi-surface/FermiSurface.svelte +21 -65
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/compute.js +1 -21
- package/dist/fermi-surface/marching-cubes.d.ts +2 -13
- package/dist/fermi-surface/marching-cubes.js +2 -519
- package/dist/fermi-surface/parse.js +17 -23
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
- package/dist/heatmap-matrix/index.d.ts +53 -0
- package/dist/heatmap-matrix/index.js +100 -0
- package/dist/heatmap-matrix/shared.d.ts +2 -0
- package/dist/heatmap-matrix/shared.js +4 -0
- package/dist/icons.d.ts +119 -0
- package/dist/icons.js +119 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/io/export.js +15 -3
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +43 -0
- package/dist/io/index.d.ts +2 -2
- package/dist/io/index.js +2 -112
- package/dist/io/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +118 -0
- package/dist/isosurface/Isosurface.svelte +231 -0
- package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
- package/dist/isosurface/IsosurfaceControls.svelte +273 -0
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
- package/dist/isosurface/index.d.ts +5 -0
- package/dist/isosurface/index.js +6 -0
- package/dist/isosurface/parse.d.ts +6 -0
- package/dist/isosurface/parse.js +548 -0
- package/dist/isosurface/slice.d.ts +11 -0
- package/dist/isosurface/slice.js +145 -0
- package/dist/isosurface/types.d.ts +55 -0
- package/dist/isosurface/types.js +178 -0
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +1 -0
- package/dist/layout/InfoTag.svelte +62 -62
- package/dist/layout/SubpageGrid.svelte +74 -0
- package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +226 -53
- package/dist/layout/json-tree/JsonTree.svelte +425 -51
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +218 -97
- package/dist/layout/json-tree/types.d.ts +27 -2
- package/dist/layout/json-tree/utils.d.ts +14 -1
- package/dist/layout/json-tree/utils.js +254 -0
- package/dist/marching-cubes.d.ts +14 -0
- package/dist/marching-cubes.js +519 -0
- package/dist/math.d.ts +8 -0
- package/dist/math.js +374 -7
- package/dist/overlays/ContextMenu.svelte +3 -2
- package/dist/overlays/DraggablePane.svelte +163 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- package/dist/phase-diagram/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +865 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +7 -4
- package/dist/phase-diagram/utils.js +149 -59
- package/dist/plot/AxisLabel.svelte +26 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +473 -228
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +3 -2
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +54 -54
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ElementScatter.svelte +4 -3
- package/dist/plot/FillArea.svelte +4 -1
- package/dist/plot/Histogram.svelte +320 -230
- package/dist/plot/Histogram.svelte.d.ts +2 -2
- package/dist/plot/HistogramControls.svelte +29 -10
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
- package/dist/plot/PlotControls.svelte +109 -27
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +1 -1
- package/dist/plot/PortalSelect.svelte +2 -1
- package/dist/plot/ReferenceLine.svelte +2 -1
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferencePlane.svelte +1 -3
- package/dist/plot/ScatterPlot.svelte +343 -209
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +203 -250
- package/dist/plot/ScatterPlot3DScene.svelte +4 -7
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +95 -55
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ZeroLines.svelte +44 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +21 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- package/dist/plot/data-cleaning.js +1 -5
- package/dist/plot/index.d.ts +6 -2
- package/dist/plot/index.js +6 -2
- package/dist/plot/interactions.d.ts +8 -10
- package/dist/plot/interactions.js +10 -19
- package/dist/plot/layout.d.ts +7 -1
- package/dist/plot/layout.js +12 -4
- package/dist/plot/reference-line.d.ts +4 -21
- package/dist/plot/reference-line.js +7 -81
- package/dist/plot/types.d.ts +42 -17
- package/dist/plot/types.js +10 -0
- package/dist/plot/utils/label-placement.js +14 -11
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +55 -66
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/index.d.ts +1 -1
- package/dist/rdf/index.js +1 -1
- package/dist/settings.d.ts +5 -0
- package/dist/settings.js +37 -3
- package/dist/spectral/Bands.svelte +515 -143
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- package/dist/spectral/helpers.d.ts +23 -1
- package/dist/spectral/helpers.js +65 -9
- package/dist/spectral/types.d.ts +2 -0
- package/dist/structure/AtomLegend.svelte +31 -10
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CellSelect.svelte +92 -22
- package/dist/structure/Lattice.svelte +2 -0
- package/dist/structure/Structure.svelte +716 -173
- package/dist/structure/Structure.svelte.d.ts +7 -2
- package/dist/structure/StructureControls.svelte +26 -14
- package/dist/structure/StructureControls.svelte.d.ts +5 -1
- package/dist/structure/StructureInfoPane.svelte +7 -1
- package/dist/structure/StructureScene.svelte +386 -95
- package/dist/structure/StructureScene.svelte.d.ts +15 -4
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +38 -25
- package/dist/structure/export.js +10 -7
- package/dist/structure/ferrox-wasm-types.d.ts +3 -2
- package/dist/structure/ferrox-wasm-types.js +0 -3
- package/dist/structure/ferrox-wasm.d.ts +3 -2
- package/dist/structure/ferrox-wasm.js +1 -2
- package/dist/structure/index.d.ts +7 -0
- package/dist/structure/index.js +22 -0
- package/dist/structure/parse.js +19 -16
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +102 -0
- package/dist/structure/validation.js +6 -3
- package/dist/symmetry/SymmetryStats.svelte +18 -4
- package/dist/symmetry/WyckoffTable.svelte +18 -10
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +83 -18
- package/dist/table/HeatmapTable.svelte +468 -69
- package/dist/table/HeatmapTable.svelte.d.ts +13 -1
- package/dist/table/ToggleMenu.svelte +291 -44
- package/dist/table/ToggleMenu.svelte.d.ts +4 -1
- package/dist/table/index.d.ts +3 -0
- package/dist/tooltip/index.d.ts +1 -1
- package/dist/tooltip/index.js +1 -0
- package/dist/trajectory/Trajectory.svelte +147 -145
- package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +3 -5
- package/dist/trajectory/format-detect.d.ts +9 -0
- package/dist/trajectory/format-detect.js +76 -0
- package/dist/trajectory/frame-reader.d.ts +17 -0
- package/dist/trajectory/frame-reader.js +339 -0
- package/dist/trajectory/helpers.d.ts +15 -0
- package/dist/trajectory/helpers.js +187 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +11 -4
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +76 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +121 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +304 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +169 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +65 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +109 -0
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/dist/xrd/XrdPlot.svelte +6 -4
- package/dist/xrd/calc-xrd.js +0 -1
- package/package.json +33 -23
- package/readme.md +4 -4
- package/dist/trajectory/parse.d.ts +0 -42
- package/dist/trajectory/parse.js +0 -1267
- /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
|
@@ -1,40 +1,195 @@
|
|
|
1
1
|
<script lang="ts">import Icon from '../Icon.svelte';
|
|
2
|
+
import { get_alphabetical_formula } from './format';
|
|
3
|
+
import { ELEM_SYMBOLS } from '../labels';
|
|
2
4
|
import { tooltip } from 'svelte-multiselect';
|
|
3
|
-
import { extract_formula_elements, has_wildcards, normalize_element_symbols, parse_formula_with_wildcards, } from './parse';
|
|
4
|
-
const
|
|
5
|
+
import { extract_formula_elements, has_wildcards, normalize_element_symbols, parse_formula, parse_formula_with_wildcards, } from './parse';
|
|
6
|
+
const DEFAULT_SEARCH_EXAMPLES = [
|
|
5
7
|
{
|
|
6
|
-
label: `
|
|
7
|
-
description: `Materials containing
|
|
8
|
-
examples: [`Li,Fe`,
|
|
8
|
+
label: `Has elements`,
|
|
9
|
+
description: `Materials containing these elements. Operators/ranges: +Li,-O,Fe:1-2. Use * for any element.`,
|
|
10
|
+
examples: [`Li,Fe`, `+Li,-O`, `Li,*,*`],
|
|
9
11
|
},
|
|
10
12
|
{
|
|
11
13
|
label: `Chemical system`,
|
|
12
|
-
description: `Materials with only these elements (no others).
|
|
14
|
+
description: `Materials with only these elements (no others). Wildcards/ranges supported.`,
|
|
13
15
|
examples: [`Li-Fe-O`, `Li-Fe-*-*`, `*-*-O`],
|
|
14
16
|
},
|
|
15
17
|
{
|
|
16
18
|
label: `Exact formula`,
|
|
17
|
-
description: `Materials with this exact stoichiometry.
|
|
19
|
+
description: `Materials with this exact stoichiometry. Unicode paste, wildcards, and canonicalization supported.`,
|
|
18
20
|
examples: [`LiFePO4`, `LiFe*2*`, `*2O3`],
|
|
19
21
|
},
|
|
20
22
|
];
|
|
21
|
-
|
|
23
|
+
const SUBSCRIPT_TO_ASCII = {
|
|
24
|
+
[`\u2080`]: `0`,
|
|
25
|
+
[`\u2081`]: `1`,
|
|
26
|
+
[`\u2082`]: `2`,
|
|
27
|
+
[`\u2083`]: `3`,
|
|
28
|
+
[`\u2084`]: `4`,
|
|
29
|
+
[`\u2085`]: `5`,
|
|
30
|
+
[`\u2086`]: `6`,
|
|
31
|
+
[`\u2087`]: `7`,
|
|
32
|
+
[`\u2088`]: `8`,
|
|
33
|
+
[`\u2089`]: `9`,
|
|
34
|
+
};
|
|
35
|
+
const SUPERSCRIPT_TO_ASCII = {
|
|
36
|
+
[`\u2070`]: `0`,
|
|
37
|
+
[`\u00B9`]: `1`,
|
|
38
|
+
[`\u00B2`]: `2`,
|
|
39
|
+
[`\u00B3`]: `3`,
|
|
40
|
+
[`\u2074`]: `4`,
|
|
41
|
+
[`\u2075`]: `5`,
|
|
42
|
+
[`\u2076`]: `6`,
|
|
43
|
+
[`\u2077`]: `7`,
|
|
44
|
+
[`\u2078`]: `8`,
|
|
45
|
+
[`\u2079`]: `9`,
|
|
46
|
+
[`\u207A`]: `+`,
|
|
47
|
+
[`\u207B`]: `-`,
|
|
48
|
+
};
|
|
49
|
+
let { value = $bindable(``), search_mode = $bindable(`elements`), input_element = $bindable(null), show_clear_button = true, show_examples = true, show_mode_lock = true, show_chip_editor = true, normalize_exact = true, examples = DEFAULT_SEARCH_EXAMPLES, disabled = false, mode_locked = $bindable(false), max_history = 5, // Max recent inputs to remember; 0 disables history dropdown
|
|
50
|
+
history_key = `formula-filter-history`, // localStorage key for persisting history
|
|
51
|
+
validate, onparse, on_validation, onchange, onclear, ...rest } = $props();
|
|
22
52
|
let input_value = $state(value);
|
|
23
53
|
let examples_open = $state(false);
|
|
54
|
+
let history_open = $state(false);
|
|
24
55
|
let wrapper = $state(null);
|
|
25
56
|
let examples_wrapper = $state(null);
|
|
26
57
|
let focused_item_idx = $state(-1);
|
|
58
|
+
let focused_history_idx = $state(-1);
|
|
27
59
|
let anchor_left = $state(false);
|
|
60
|
+
let history_query = $state(``);
|
|
61
|
+
let validation = $state({ state: `valid`, message: null });
|
|
28
62
|
// Flatten examples for keyboard navigation
|
|
29
|
-
|
|
63
|
+
let all_examples = $derived(examples.flatMap((cat) => cat.examples));
|
|
64
|
+
// === History Management ===
|
|
65
|
+
const has_storage = typeof localStorage !== `undefined`;
|
|
66
|
+
const history_pins_key = $derived(`${history_key}-pins`);
|
|
67
|
+
function load_history() {
|
|
68
|
+
if (max_history <= 0 || !has_storage)
|
|
69
|
+
return [];
|
|
70
|
+
try {
|
|
71
|
+
const raw = localStorage.getItem(history_key);
|
|
72
|
+
if (!raw)
|
|
73
|
+
return [];
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (!Array.isArray(parsed))
|
|
76
|
+
return [];
|
|
77
|
+
return parsed.filter((item) => typeof item === `string`).slice(0, max_history);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function save_history(entries) {
|
|
84
|
+
if (max_history <= 0 || !has_storage)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
localStorage.setItem(history_key, JSON.stringify(entries.slice(0, max_history)));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// localStorage may be unavailable (e.g. private browsing)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function load_pinned() {
|
|
94
|
+
if (max_history <= 0 || !has_storage)
|
|
95
|
+
return [];
|
|
96
|
+
try {
|
|
97
|
+
const raw = localStorage.getItem(history_pins_key);
|
|
98
|
+
if (!raw)
|
|
99
|
+
return [];
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!Array.isArray(parsed))
|
|
102
|
+
return [];
|
|
103
|
+
return parsed.filter((item) => typeof item === `string`);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function save_pinned(entries) {
|
|
110
|
+
if (max_history <= 0 || !has_storage)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
localStorage.setItem(history_pins_key, JSON.stringify(entries));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// localStorage may be unavailable
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let history = $state(load_history());
|
|
120
|
+
let pinned_history = $state(load_pinned());
|
|
121
|
+
function add_to_history(entry) {
|
|
122
|
+
if (max_history <= 0 || !entry.trim())
|
|
123
|
+
return;
|
|
124
|
+
// Remove duplicate if present, then prepend
|
|
125
|
+
const filtered = history.filter((item) => item !== entry);
|
|
126
|
+
history = [entry, ...filtered].slice(0, max_history);
|
|
127
|
+
// Keep pin state for retained entries only
|
|
128
|
+
pinned_history = pinned_history.filter((item) => history.includes(item));
|
|
129
|
+
save_history(history);
|
|
130
|
+
save_pinned(pinned_history);
|
|
131
|
+
}
|
|
132
|
+
function remove_from_history(entry) {
|
|
133
|
+
history = history.filter((item) => item !== entry);
|
|
134
|
+
pinned_history = pinned_history.filter((item) => item !== entry);
|
|
135
|
+
save_history(history);
|
|
136
|
+
save_pinned(pinned_history);
|
|
137
|
+
// Clamp focused index to prevent out-of-bounds access on Enter
|
|
138
|
+
if (history.length === 0)
|
|
139
|
+
history_open = false;
|
|
140
|
+
else if (focused_history_idx >= visible_history.length) {
|
|
141
|
+
focused_history_idx = visible_history.length - 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function toggle_pin_history(entry) {
|
|
145
|
+
pinned_history = pinned_history.includes(entry)
|
|
146
|
+
? pinned_history.filter((item) => item !== entry)
|
|
147
|
+
: [entry, ...pinned_history.filter((item) => item !== entry)];
|
|
148
|
+
save_pinned(pinned_history);
|
|
149
|
+
}
|
|
150
|
+
function clear_history() {
|
|
151
|
+
history = [];
|
|
152
|
+
pinned_history = [];
|
|
153
|
+
save_history(history);
|
|
154
|
+
save_pinned(pinned_history);
|
|
155
|
+
close_history();
|
|
156
|
+
}
|
|
157
|
+
function is_pinned(entry) {
|
|
158
|
+
return pinned_history.includes(entry);
|
|
159
|
+
}
|
|
160
|
+
// Filtered history: exclude current value to avoid redundant suggestion
|
|
161
|
+
let visible_history = $derived.by(() => {
|
|
162
|
+
const filtered = history
|
|
163
|
+
.filter((item) => item !== value)
|
|
164
|
+
.filter((item) => item.toLowerCase().includes(history_query.toLowerCase().trim()));
|
|
165
|
+
const pinned = filtered.filter((item) => pinned_history.includes(item));
|
|
166
|
+
const unpinned = filtered.filter((item) => !pinned_history.includes(item));
|
|
167
|
+
return [...pinned, ...unpinned];
|
|
168
|
+
});
|
|
169
|
+
function close_history() {
|
|
170
|
+
history_open = false;
|
|
171
|
+
history_query = ``;
|
|
172
|
+
focused_history_idx = -1;
|
|
173
|
+
}
|
|
174
|
+
function open_history() {
|
|
175
|
+
if (max_history <= 0 || visible_history.length === 0 || examples_open)
|
|
176
|
+
return;
|
|
177
|
+
history_open = true;
|
|
178
|
+
history_query = ``;
|
|
179
|
+
focused_history_idx = -1;
|
|
180
|
+
}
|
|
30
181
|
function handle_document_click(event) {
|
|
31
|
-
if (!wrapper || !examples_open)
|
|
182
|
+
if (!wrapper || (!examples_open && !history_open))
|
|
32
183
|
return;
|
|
33
184
|
const target = event.target;
|
|
34
185
|
if (!(target instanceof Node))
|
|
35
186
|
return;
|
|
36
|
-
if (!wrapper.contains(target))
|
|
37
|
-
|
|
187
|
+
if (!wrapper.contains(target)) {
|
|
188
|
+
if (examples_open)
|
|
189
|
+
close_examples();
|
|
190
|
+
if (history_open)
|
|
191
|
+
close_history();
|
|
192
|
+
}
|
|
38
193
|
}
|
|
39
194
|
function close_examples(restore_focus = true) {
|
|
40
195
|
examples_open = false;
|
|
@@ -46,14 +201,15 @@ function close_examples(restore_focus = true) {
|
|
|
46
201
|
// and re-infer mode accordingly. Without this, mode would only be set on first render.
|
|
47
202
|
let last_synced = $state(null);
|
|
48
203
|
$effect(() => {
|
|
49
|
-
input_value = value;
|
|
50
204
|
if (value !== last_synced) {
|
|
51
205
|
last_synced = value;
|
|
52
|
-
|
|
206
|
+
input_value = value;
|
|
207
|
+
if (value && !mode_locked) {
|
|
53
208
|
const inferred = infer_mode(value);
|
|
54
209
|
if (inferred !== search_mode)
|
|
55
210
|
search_mode = inferred;
|
|
56
211
|
}
|
|
212
|
+
run_validation(value, search_mode);
|
|
57
213
|
}
|
|
58
214
|
});
|
|
59
215
|
// Detect if dropdown would exit viewport on the right and adjust anchor
|
|
@@ -74,14 +230,210 @@ function infer_mode(input) {
|
|
|
74
230
|
const trimmed = input.trim();
|
|
75
231
|
if (!trimmed)
|
|
76
232
|
return `elements`;
|
|
233
|
+
if (/^[+\-!]\s*\w/.test(trimmed))
|
|
234
|
+
return `elements`;
|
|
235
|
+
if (trimmed.includes(`+`) || trimmed.includes(`!`))
|
|
236
|
+
return `elements`;
|
|
237
|
+
if (trimmed.includes(`:`))
|
|
238
|
+
return trimmed.includes(`-`) ? `chemsys` : `elements`;
|
|
77
239
|
if (trimmed.includes(`,`))
|
|
78
|
-
return `elements`; // Li,Fe,O →
|
|
240
|
+
return `elements`; // Li,Fe,O → has elements
|
|
79
241
|
if (trimmed.includes(`-`))
|
|
80
242
|
return `chemsys`; // Li-Fe-O → chemical system
|
|
81
243
|
return `exact`; // LiFePO4 → exact formula
|
|
82
244
|
}
|
|
83
245
|
// Cycle through modes: elements → chemsys → exact → elements
|
|
84
246
|
const MODE_CYCLE = [`elements`, `chemsys`, `exact`];
|
|
247
|
+
function normalize_unicode_formula(input) {
|
|
248
|
+
let normalized = input;
|
|
249
|
+
for (const [subscript, ascii] of Object.entries(SUBSCRIPT_TO_ASCII)) {
|
|
250
|
+
normalized = normalized.replaceAll(subscript, ascii);
|
|
251
|
+
}
|
|
252
|
+
for (const [superscript, ascii] of Object.entries(SUPERSCRIPT_TO_ASCII)) {
|
|
253
|
+
normalized = normalized.replaceAll(superscript, ascii);
|
|
254
|
+
}
|
|
255
|
+
return normalized
|
|
256
|
+
.replaceAll(`·`, ``)
|
|
257
|
+
.replaceAll(`⋅`, ``)
|
|
258
|
+
.replaceAll(`−`, `-`)
|
|
259
|
+
.replace(/\s+/g, ``);
|
|
260
|
+
}
|
|
261
|
+
function normalize_exact_formula(input) {
|
|
262
|
+
const sanitized_input = normalize_unicode_formula(input.trim());
|
|
263
|
+
if (!sanitize_exact_formula(sanitized_input).is_valid)
|
|
264
|
+
return sanitized_input;
|
|
265
|
+
if (!has_wildcards(sanitized_input)) {
|
|
266
|
+
const canonical = get_alphabetical_formula(sanitized_input, true, ``);
|
|
267
|
+
return canonical || sanitized_input;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const tokens = parse_formula_with_wildcards(sanitized_input);
|
|
271
|
+
const explicit = tokens
|
|
272
|
+
.filter((token) => token.element !== null)
|
|
273
|
+
.map((token) => ({ element: token.element, count: token.count }));
|
|
274
|
+
const wildcard_tokens = tokens.filter((token) => token.element === null);
|
|
275
|
+
// Merge explicit element counts before sorting.
|
|
276
|
+
const merged_explicit = [];
|
|
277
|
+
for (const token of explicit) {
|
|
278
|
+
const existing = merged_explicit.find((item) => item.element === token.element);
|
|
279
|
+
if (existing)
|
|
280
|
+
existing.count += token.count;
|
|
281
|
+
else
|
|
282
|
+
merged_explicit.push(token);
|
|
283
|
+
}
|
|
284
|
+
const sorted_explicit = merged_explicit.sort((elem_a, elem_b) => elem_a.element.localeCompare(elem_b.element));
|
|
285
|
+
const wildcard_str = wildcard_tokens.map((token) => token.count > 1 ? `*${token.count}` : `*`).join(``);
|
|
286
|
+
const explicit_str = sorted_explicit.map((token) => token.count > 1 ? `${token.element}${token.count}` : token.element).join(``);
|
|
287
|
+
return `${explicit_str}${wildcard_str}`;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return sanitized_input;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function is_valid_constraint(constraint) {
|
|
294
|
+
if (!constraint)
|
|
295
|
+
return true;
|
|
296
|
+
return /^\d+$/.test(constraint) || /^\d+-\d+$/.test(constraint) ||
|
|
297
|
+
/^(>=|<=|>|<)\d+$/.test(constraint);
|
|
298
|
+
}
|
|
299
|
+
function strip_operator_prefix(token) {
|
|
300
|
+
const operator = token.startsWith(`-`) || token.startsWith(`!`)
|
|
301
|
+
? `exclude`
|
|
302
|
+
: `include`;
|
|
303
|
+
const value = token.startsWith(`+`) || token.startsWith(`-`) || token.startsWith(`!`)
|
|
304
|
+
? token.slice(1)
|
|
305
|
+
: token;
|
|
306
|
+
return { operator, value };
|
|
307
|
+
}
|
|
308
|
+
function serialize_token(token) {
|
|
309
|
+
const prefix = token.operator === `exclude` ? `-` : ``;
|
|
310
|
+
const suffix = token.constraint ? `:${token.constraint}` : ``;
|
|
311
|
+
return `${prefix}${token.element}${suffix}`;
|
|
312
|
+
}
|
|
313
|
+
function token_chip_label(token) {
|
|
314
|
+
const prefix = token.operator === `exclude` ? `-` : `+`;
|
|
315
|
+
const suffix = token.constraint ? `:${token.constraint}` : ``;
|
|
316
|
+
return `${prefix}${token.element}${suffix}`;
|
|
317
|
+
}
|
|
318
|
+
function parse_token(raw_token) {
|
|
319
|
+
const token = raw_token.trim();
|
|
320
|
+
const { operator, value: without_operator } = strip_operator_prefix(token);
|
|
321
|
+
const [element_part, constraint] = without_operator.split(`:`);
|
|
322
|
+
const element = element_part.trim();
|
|
323
|
+
const is_wildcard = element === `*`;
|
|
324
|
+
const is_valid_element = is_wildcard ||
|
|
325
|
+
ELEM_SYMBOLS.includes(element);
|
|
326
|
+
const normalized_constraint = constraint?.trim() || null;
|
|
327
|
+
const is_valid = is_valid_element && (normalized_constraint === null ||
|
|
328
|
+
is_valid_constraint(normalized_constraint));
|
|
329
|
+
return {
|
|
330
|
+
raw: raw_token,
|
|
331
|
+
element,
|
|
332
|
+
operator,
|
|
333
|
+
constraint: normalized_constraint,
|
|
334
|
+
is_wildcard,
|
|
335
|
+
is_valid,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function tokenize_query(input, mode) {
|
|
339
|
+
const trimmed = input.trim();
|
|
340
|
+
if (!trimmed)
|
|
341
|
+
return [];
|
|
342
|
+
if (mode === `exact`) {
|
|
343
|
+
return [{
|
|
344
|
+
raw: trimmed,
|
|
345
|
+
element: trimmed,
|
|
346
|
+
operator: `include`,
|
|
347
|
+
constraint: null,
|
|
348
|
+
is_wildcard: has_wildcards(trimmed),
|
|
349
|
+
is_valid: sanitize_exact_formula(trimmed).is_valid,
|
|
350
|
+
}];
|
|
351
|
+
}
|
|
352
|
+
const normalized = mode === `chemsys` ? trimmed.replaceAll(`,`, `-`) : trimmed;
|
|
353
|
+
const tokens = mode === `chemsys`
|
|
354
|
+
// Keep range constraints like Fe:1-2 intact while splitting token separators.
|
|
355
|
+
? normalized.split(/-(?!\d)/)
|
|
356
|
+
: normalized.split(`,`);
|
|
357
|
+
return tokens
|
|
358
|
+
.map((token) => token.trim())
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.map(parse_token);
|
|
361
|
+
}
|
|
362
|
+
function sanitize_exact_formula(input) {
|
|
363
|
+
const trimmed = input.trim();
|
|
364
|
+
if (!trimmed)
|
|
365
|
+
return { is_valid: true, error_message: null };
|
|
366
|
+
try {
|
|
367
|
+
if (has_wildcards(trimmed)) {
|
|
368
|
+
parse_formula_with_wildcards(trimmed);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
parse_formula(trimmed);
|
|
372
|
+
}
|
|
373
|
+
return { is_valid: true, error_message: null };
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const message = error instanceof Error ? error.message : `Invalid exact formula`;
|
|
377
|
+
return { is_valid: false, error_message: message };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function normalize_tokenized_input(input, mode) {
|
|
381
|
+
const separator = mode === `chemsys` ? `-` : `,`;
|
|
382
|
+
const parsed_tokens = tokenize_query(input, mode);
|
|
383
|
+
if (parsed_tokens.length === 0)
|
|
384
|
+
return ``;
|
|
385
|
+
const normalized_tokens = parsed_tokens
|
|
386
|
+
.filter((token) => token.is_valid)
|
|
387
|
+
.map((token) => ({
|
|
388
|
+
...token,
|
|
389
|
+
element: token.is_wildcard
|
|
390
|
+
? `*`
|
|
391
|
+
: normalize_element_symbols(token.element).at(0) || token.element,
|
|
392
|
+
}))
|
|
393
|
+
.sort((token_a, token_b) => {
|
|
394
|
+
if (token_a.operator !== token_b.operator) {
|
|
395
|
+
return token_a.operator === `include` ? -1 : 1;
|
|
396
|
+
}
|
|
397
|
+
if (token_a.is_wildcard !== token_b.is_wildcard) {
|
|
398
|
+
return token_a.is_wildcard ? 1 : -1;
|
|
399
|
+
}
|
|
400
|
+
return token_a.element.localeCompare(token_b.element);
|
|
401
|
+
});
|
|
402
|
+
return normalized_tokens
|
|
403
|
+
.map(serialize_token)
|
|
404
|
+
.join(separator);
|
|
405
|
+
}
|
|
406
|
+
function parse_query(normalized_value, mode) {
|
|
407
|
+
const tokens = tokenize_query(normalized_value, mode);
|
|
408
|
+
const first_invalid_token = tokens.find((token) => !token.is_valid);
|
|
409
|
+
const exact_validation = mode === `exact`
|
|
410
|
+
? sanitize_exact_formula(normalized_value)
|
|
411
|
+
: {
|
|
412
|
+
is_valid: !first_invalid_token,
|
|
413
|
+
error_message: first_invalid_token
|
|
414
|
+
? `Invalid token: ${first_invalid_token.raw}`
|
|
415
|
+
: null,
|
|
416
|
+
};
|
|
417
|
+
return {
|
|
418
|
+
value: normalized_value,
|
|
419
|
+
normalized_value,
|
|
420
|
+
search_mode: mode,
|
|
421
|
+
tokens,
|
|
422
|
+
has_wildcards: tokens.some((token) => token.is_wildcard),
|
|
423
|
+
is_valid: exact_validation.is_valid,
|
|
424
|
+
error_message: exact_validation.error_message,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function run_validation(next_value, next_mode) {
|
|
428
|
+
const parsed = parse_query(next_value, next_mode);
|
|
429
|
+
onparse?.(parsed);
|
|
430
|
+
const default_validation = parsed.is_valid
|
|
431
|
+
? { state: `valid`, message: null }
|
|
432
|
+
: { state: `invalid`, message: parsed.error_message ?? `Invalid filter query` };
|
|
433
|
+
const custom_validation = validate?.(next_value, next_mode, parsed);
|
|
434
|
+
validation = custom_validation ?? default_validation;
|
|
435
|
+
on_validation?.(validation);
|
|
436
|
+
}
|
|
85
437
|
// Extract elements from any input format (formula, comma-separated, dash-separated)
|
|
86
438
|
// Always returns elements in alphabetical order for consistency, preserving wildcards (*)
|
|
87
439
|
function extract_elements(input) {
|
|
@@ -102,9 +454,13 @@ function extract_elements(input) {
|
|
|
102
454
|
// For formulas with wildcards, we can't parse them normally
|
|
103
455
|
if (has_wildcards(trimmed)) { // Use shared utility and extract unique elements
|
|
104
456
|
const tokens = parse_formula_with_wildcards(trimmed);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
457
|
+
const unique_elements = [];
|
|
458
|
+
for (const token of tokens) {
|
|
459
|
+
if (token.element !== null && !unique_elements.includes(token.element)) {
|
|
460
|
+
unique_elements.push(token.element);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const elements = unique_elements.sort();
|
|
108
464
|
const wildcards = tokens.filter((token) => token.element === null).map(() => `*`);
|
|
109
465
|
return [...elements, ...wildcards];
|
|
110
466
|
}
|
|
@@ -127,6 +483,8 @@ function format_for_mode(elements, mode) {
|
|
|
127
483
|
return elements.join(``);
|
|
128
484
|
}
|
|
129
485
|
function cycle_mode() {
|
|
486
|
+
if (mode_locked)
|
|
487
|
+
return;
|
|
130
488
|
const current_idx = MODE_CYCLE.indexOf(search_mode);
|
|
131
489
|
const next_idx = (current_idx + 1) % MODE_CYCLE.length;
|
|
132
490
|
const next_mode = MODE_CYCLE[next_idx];
|
|
@@ -135,57 +493,89 @@ function cycle_mode() {
|
|
|
135
493
|
const reformatted = format_for_mode(elements, next_mode);
|
|
136
494
|
search_mode = next_mode;
|
|
137
495
|
last_synced = value = input_value = reformatted; // update last_synced to prevent effect re-inference
|
|
496
|
+
run_validation(reformatted, next_mode);
|
|
138
497
|
onchange?.(reformatted, next_mode);
|
|
139
498
|
}
|
|
140
|
-
function set_value(new_value) {
|
|
141
|
-
const mode = infer_mode(new_value);
|
|
499
|
+
function set_value(new_value, forced_mode) {
|
|
500
|
+
const mode = forced_mode ?? (mode_locked ? search_mode : infer_mode(new_value));
|
|
142
501
|
last_synced = value = input_value = new_value; // update last_synced to prevent effect re-inference
|
|
143
502
|
search_mode = mode;
|
|
503
|
+
if (new_value.trim())
|
|
504
|
+
add_to_history(new_value);
|
|
505
|
+
close_history();
|
|
506
|
+
run_validation(value, mode);
|
|
144
507
|
onchange?.(value, mode);
|
|
145
508
|
}
|
|
146
509
|
function sync_value() {
|
|
147
|
-
const trimmed = input_value.trim();
|
|
510
|
+
const trimmed = normalize_unicode_formula(input_value).trim();
|
|
148
511
|
if (!trimmed)
|
|
149
512
|
return set_value(``);
|
|
150
|
-
const mode = infer_mode(trimmed);
|
|
151
|
-
if (mode === `exact`)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
];
|
|
165
|
-
set_value(normalized.join(separator));
|
|
513
|
+
const mode = mode_locked ? search_mode : infer_mode(trimmed);
|
|
514
|
+
if (mode === `exact`) {
|
|
515
|
+
const exact_value = normalize_exact ? normalize_exact_formula(trimmed) : trimmed;
|
|
516
|
+
return set_value(exact_value, mode);
|
|
517
|
+
}
|
|
518
|
+
const parsed = parse_query(trimmed, mode);
|
|
519
|
+
if (!parsed.is_valid) {
|
|
520
|
+
// Preserve user input on invalid tokens instead of silently dropping them.
|
|
521
|
+
input_value = trimmed;
|
|
522
|
+
run_validation(trimmed, mode);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const normalized = normalize_tokenized_input(trimmed, mode);
|
|
526
|
+
set_value(normalized, mode);
|
|
166
527
|
}
|
|
167
528
|
function onkeydown(event) {
|
|
168
529
|
if (event.key === `Enter`) {
|
|
169
530
|
event.preventDefault();
|
|
170
|
-
|
|
531
|
+
if (history_open && focused_history_idx >= 0) {
|
|
532
|
+
set_value(visible_history[focused_history_idx]);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
sync_value();
|
|
536
|
+
}
|
|
171
537
|
}
|
|
172
538
|
else if (event.key === `Escape`) {
|
|
173
|
-
if (
|
|
539
|
+
if (history_open)
|
|
540
|
+
close_history();
|
|
541
|
+
else if (examples_open)
|
|
174
542
|
examples_open = false;
|
|
175
543
|
else if (input_value)
|
|
176
544
|
clear_filter();
|
|
177
545
|
}
|
|
546
|
+
else if (history_open && visible_history.length > 0) {
|
|
547
|
+
const len = visible_history.length;
|
|
548
|
+
if (event.key === `ArrowDown`) {
|
|
549
|
+
event.preventDefault();
|
|
550
|
+
focused_history_idx = (focused_history_idx + 1) % len;
|
|
551
|
+
}
|
|
552
|
+
else if (event.key === `ArrowUp`) {
|
|
553
|
+
event.preventDefault();
|
|
554
|
+
focused_history_idx = focused_history_idx <= 0
|
|
555
|
+
? len - 1
|
|
556
|
+
: focused_history_idx - 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function oninput() {
|
|
561
|
+
if (history_open) {
|
|
562
|
+
history_query = input_value;
|
|
563
|
+
focused_history_idx = visible_history.length > 0 ? 0 : -1;
|
|
564
|
+
}
|
|
565
|
+
const mode = mode_locked ? search_mode : infer_mode(input_value);
|
|
566
|
+
run_validation(input_value, mode);
|
|
178
567
|
}
|
|
179
568
|
function clear_filter() {
|
|
180
569
|
onclear?.();
|
|
181
570
|
set_value(``);
|
|
182
571
|
}
|
|
183
572
|
function apply_example(example) {
|
|
184
|
-
set_value(example);
|
|
573
|
+
set_value(example, mode_locked ? search_mode : infer_mode(example));
|
|
185
574
|
close_examples();
|
|
186
575
|
}
|
|
187
576
|
function toggle_examples(event) {
|
|
188
577
|
event.stopPropagation();
|
|
578
|
+
close_history();
|
|
189
579
|
examples_open = !examples_open;
|
|
190
580
|
focused_item_idx = examples_open ? 0 : -1;
|
|
191
581
|
if (examples_open)
|
|
@@ -211,6 +601,19 @@ function handle_menu_keydown(event) {
|
|
|
211
601
|
key_actions[event.key]();
|
|
212
602
|
}
|
|
213
603
|
}
|
|
604
|
+
function toggle_mode_lock() {
|
|
605
|
+
mode_locked = !mode_locked;
|
|
606
|
+
}
|
|
607
|
+
function remove_token(token_idx) {
|
|
608
|
+
if (search_mode === `exact`)
|
|
609
|
+
return;
|
|
610
|
+
const separator = search_mode === `chemsys` ? `-` : `,`;
|
|
611
|
+
const tokens = tokenize_query(input_value, search_mode)
|
|
612
|
+
.filter((_, idx) => idx !== token_idx);
|
|
613
|
+
const next_value = tokens.map(serialize_token).join(separator);
|
|
614
|
+
input_value = next_value;
|
|
615
|
+
set_value(next_value, search_mode);
|
|
616
|
+
}
|
|
214
617
|
// Focus the active menu item when index changes
|
|
215
618
|
$effect(() => {
|
|
216
619
|
if (!examples_open || focused_item_idx < 0)
|
|
@@ -224,11 +627,13 @@ let placeholder = $derived(search_mode === `chemsys`
|
|
|
224
627
|
? `LiFePO4 or LiFe*2*`
|
|
225
628
|
: `Li,Fe,O or Li,*,*`);
|
|
226
629
|
const MODE_LABELS = {
|
|
227
|
-
elements: `
|
|
630
|
+
elements: `has elements`,
|
|
228
631
|
chemsys: `chemical system`,
|
|
229
632
|
exact: `exact formula`,
|
|
230
633
|
};
|
|
231
634
|
let mode_hint = $derived(MODE_LABELS[search_mode]);
|
|
635
|
+
let parsed_tokens = $derived(tokenize_query(input_value, search_mode));
|
|
636
|
+
let show_chip_row = $derived(show_chip_editor && search_mode !== `exact` && parsed_tokens.length > 0);
|
|
232
637
|
// Preview of next mode cycle step for tooltip
|
|
233
638
|
let next_mode = $derived.by(() => {
|
|
234
639
|
const next = MODE_CYCLE[(MODE_CYCLE.indexOf(search_mode) + 1) % MODE_CYCLE.length];
|
|
@@ -240,34 +645,133 @@ let next_mode = $derived.by(() => {
|
|
|
240
645
|
|
|
241
646
|
<svelte:document onclick={handle_document_click} />
|
|
242
647
|
|
|
243
|
-
<div
|
|
648
|
+
<div
|
|
649
|
+
class="formula-filter"
|
|
650
|
+
bind:this={wrapper}
|
|
651
|
+
class:disabled
|
|
652
|
+
class:invalid={validation.state === `invalid`}
|
|
653
|
+
class:warning={validation.state === `warning`}
|
|
654
|
+
{...rest}
|
|
655
|
+
>
|
|
244
656
|
<input
|
|
245
657
|
bind:this={input_element}
|
|
246
658
|
bind:value={input_value}
|
|
247
|
-
onblur={
|
|
659
|
+
onblur={() => {
|
|
660
|
+
// mousedown preventDefault on history items prevents blur, so this only
|
|
661
|
+
// fires when focus genuinely leaves (tab out, click outside, etc.)
|
|
662
|
+
// sync_value → set_value → close_history, so no separate close needed
|
|
663
|
+
sync_value()
|
|
664
|
+
}}
|
|
665
|
+
onfocus={open_history}
|
|
666
|
+
{oninput}
|
|
667
|
+
onpaste={() => {
|
|
668
|
+
requestAnimationFrame(() => {
|
|
669
|
+
input_value = normalize_unicode_formula(input_value)
|
|
670
|
+
oninput()
|
|
671
|
+
})
|
|
672
|
+
}}
|
|
248
673
|
{onkeydown}
|
|
249
674
|
{placeholder}
|
|
250
675
|
{disabled}
|
|
251
676
|
aria-label="Formula filter"
|
|
252
677
|
/>
|
|
678
|
+
{#if history_open && visible_history.length > 0}
|
|
679
|
+
<div class="history-dropdown" role="listbox" aria-label="Recent searches">
|
|
680
|
+
<div class="history-header-row">
|
|
681
|
+
<span class="history-header">Recent</span>
|
|
682
|
+
<button
|
|
683
|
+
type="button"
|
|
684
|
+
class="history-clear-all"
|
|
685
|
+
title="Clear history"
|
|
686
|
+
aria-label="Clear all history"
|
|
687
|
+
onmousedown={(event) => {
|
|
688
|
+
event.preventDefault()
|
|
689
|
+
clear_history()
|
|
690
|
+
}}
|
|
691
|
+
>
|
|
692
|
+
Clear
|
|
693
|
+
</button>
|
|
694
|
+
</div>
|
|
695
|
+
{#each visible_history as entry, idx (entry)}
|
|
696
|
+
<div class="history-item" class:focused={idx === focused_history_idx}>
|
|
697
|
+
<button
|
|
698
|
+
type="button"
|
|
699
|
+
class="history-value"
|
|
700
|
+
role="option"
|
|
701
|
+
aria-selected={idx === focused_history_idx}
|
|
702
|
+
onmousedown={(event) => {
|
|
703
|
+
event.preventDefault()
|
|
704
|
+
set_value(entry)
|
|
705
|
+
}}
|
|
706
|
+
>
|
|
707
|
+
{entry}
|
|
708
|
+
</button>
|
|
709
|
+
<button
|
|
710
|
+
type="button"
|
|
711
|
+
class="history-pin"
|
|
712
|
+
title={is_pinned(entry) ? `Unpin entry` : `Pin entry`}
|
|
713
|
+
aria-label={is_pinned(entry) ? `Unpin ${entry}` : `Pin ${entry}`}
|
|
714
|
+
onmousedown={(event) => {
|
|
715
|
+
event.preventDefault()
|
|
716
|
+
toggle_pin_history(entry)
|
|
717
|
+
}}
|
|
718
|
+
>
|
|
719
|
+
<Icon
|
|
720
|
+
icon={is_pinned(entry) ? `Star` : `Circle`}
|
|
721
|
+
style="width: 0.8em; height: 0.8em"
|
|
722
|
+
/>
|
|
723
|
+
</button>
|
|
724
|
+
<button
|
|
725
|
+
type="button"
|
|
726
|
+
class="history-remove"
|
|
727
|
+
title="Remove from history"
|
|
728
|
+
aria-label="Remove {entry} from history"
|
|
729
|
+
onmousedown={(event) => {
|
|
730
|
+
event.preventDefault()
|
|
731
|
+
remove_from_history(entry)
|
|
732
|
+
}}
|
|
733
|
+
>
|
|
734
|
+
<Icon icon="Close" style="width: 0.7em; height: 0.7em" />
|
|
735
|
+
</button>
|
|
736
|
+
</div>
|
|
737
|
+
{/each}
|
|
738
|
+
</div>
|
|
739
|
+
{/if}
|
|
253
740
|
{#if input_value}
|
|
254
741
|
<button
|
|
255
742
|
type="button"
|
|
256
743
|
class="mode-hint clickable"
|
|
744
|
+
class:locked={mode_locked}
|
|
257
745
|
onclick={cycle_mode}
|
|
258
|
-
title=
|
|
259
|
-
|
|
746
|
+
title={mode_locked
|
|
747
|
+
? `Mode is locked`
|
|
748
|
+
: `Click to switch to '${next_mode.mode}' → ${next_mode.value}`}
|
|
749
|
+
{@attach tooltip()}
|
|
260
750
|
aria-label="Change search mode"
|
|
261
751
|
>
|
|
262
752
|
{mode_hint}
|
|
263
753
|
</button>
|
|
264
754
|
{/if}
|
|
755
|
+
{#if show_mode_lock && !disabled}
|
|
756
|
+
<button
|
|
757
|
+
type="button"
|
|
758
|
+
class="icon-btn lock-btn"
|
|
759
|
+
class:active={mode_locked}
|
|
760
|
+
onclick={toggle_mode_lock}
|
|
761
|
+
title={mode_locked ? `Unlock mode inference` : `Lock current mode`}
|
|
762
|
+
{@attach tooltip()}
|
|
763
|
+
aria-label={mode_locked ? `Unlock mode` : `Lock mode`}
|
|
764
|
+
>
|
|
765
|
+
<Icon icon={mode_locked ? `Lock` : `Unlock`} style="width: 1em; height: 1em" />
|
|
766
|
+
</button>
|
|
767
|
+
{/if}
|
|
265
768
|
{#if show_clear_button && value && !disabled}
|
|
266
769
|
<button
|
|
267
770
|
type="button"
|
|
268
771
|
class="icon-btn clear-btn"
|
|
269
772
|
onclick={clear_filter}
|
|
270
773
|
title="Clear (Escape)"
|
|
774
|
+
{@attach tooltip()}
|
|
271
775
|
aria-label="Clear filter"
|
|
272
776
|
>
|
|
273
777
|
<Icon icon="Close" style="width: 1em; height: 1em" />
|
|
@@ -295,7 +799,7 @@ let next_mode = $derived.by(() => {
|
|
|
295
799
|
tabindex="-1"
|
|
296
800
|
onkeydown={handle_menu_keydown}
|
|
297
801
|
>
|
|
298
|
-
{#each
|
|
802
|
+
{#each examples as category (category.label)}
|
|
299
803
|
<div class="example-category">
|
|
300
804
|
<div class="category-label">{category.label}:</div>
|
|
301
805
|
<div class="example-tags">
|
|
@@ -320,24 +824,58 @@ let next_mode = $derived.by(() => {
|
|
|
320
824
|
</div>
|
|
321
825
|
{/if}
|
|
322
826
|
</div>
|
|
827
|
+
{#if show_chip_row}
|
|
828
|
+
<div class="token-chip-row">
|
|
829
|
+
{#each parsed_tokens as
|
|
830
|
+
token,
|
|
831
|
+
idx
|
|
832
|
+
(`${token.operator}:${token.element}:${token.constraint ?? ``}:${idx}`)
|
|
833
|
+
}
|
|
834
|
+
<button
|
|
835
|
+
type="button"
|
|
836
|
+
class="token-chip"
|
|
837
|
+
class:exclude={token.operator === `exclude`}
|
|
838
|
+
class:invalid={!token.is_valid}
|
|
839
|
+
onclick={() => remove_token(idx)}
|
|
840
|
+
title="Click to remove token"
|
|
841
|
+
aria-label="Remove token {token.raw}"
|
|
842
|
+
>
|
|
843
|
+
{token_chip_label(token)}
|
|
844
|
+
</button>
|
|
845
|
+
{/each}
|
|
846
|
+
</div>
|
|
847
|
+
{/if}
|
|
848
|
+
{#if validation.message}
|
|
849
|
+
<div class="validation-message" class:invalid={validation.state === `invalid`}>
|
|
850
|
+
{validation.message}
|
|
851
|
+
</div>
|
|
852
|
+
{/if}
|
|
323
853
|
|
|
324
854
|
<style>
|
|
325
855
|
.formula-filter {
|
|
326
856
|
position: relative;
|
|
327
857
|
display: flex;
|
|
328
858
|
align-items: center;
|
|
329
|
-
gap:
|
|
330
|
-
padding: 4pt 8pt;
|
|
331
|
-
border-radius:
|
|
332
|
-
background: var(--filter-bg, rgba(128, 128, 128, 0.05));
|
|
859
|
+
gap: var(--formula-filter-gap, 1pt);
|
|
860
|
+
padding: var(--formula-filter-padding, 4pt 8pt);
|
|
861
|
+
border-radius: var(--formula-filter-border-radius, var(--border-radius, 3pt));
|
|
862
|
+
background: var(--formula-filter-bg, rgba(128, 128, 128, 0.05));
|
|
333
863
|
transition: background 0.15s;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
864
|
+
&.invalid {
|
|
865
|
+
outline: 1px solid rgba(239, 68, 68, 0.65);
|
|
866
|
+
background: rgba(239, 68, 68, 0.08);
|
|
867
|
+
}
|
|
868
|
+
&.warning {
|
|
869
|
+
outline: 1px solid rgba(245, 158, 11, 0.6);
|
|
870
|
+
background: rgba(245, 158, 11, 0.08);
|
|
871
|
+
}
|
|
872
|
+
&:focus-within {
|
|
873
|
+
background: rgba(77, 182, 255, 0.08);
|
|
874
|
+
}
|
|
875
|
+
&.disabled {
|
|
876
|
+
opacity: 0.5;
|
|
877
|
+
pointer-events: none;
|
|
878
|
+
}
|
|
341
879
|
}
|
|
342
880
|
input {
|
|
343
881
|
flex: 1;
|
|
@@ -348,33 +886,117 @@ let next_mode = $derived.by(() => {
|
|
|
348
886
|
padding: 2pt 0;
|
|
349
887
|
outline: none;
|
|
350
888
|
font-family: var(--mono-font, monospace);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
889
|
+
&::placeholder {
|
|
890
|
+
opacity: 0.4;
|
|
891
|
+
}
|
|
354
892
|
}
|
|
355
893
|
.mode-hint {
|
|
356
894
|
opacity: 0.5;
|
|
357
895
|
white-space: nowrap;
|
|
896
|
+
&.clickable {
|
|
897
|
+
display: inline-flex;
|
|
898
|
+
align-items: center;
|
|
899
|
+
gap: 2pt;
|
|
900
|
+
background: rgba(77, 182, 255, 0.1);
|
|
901
|
+
border: 1px solid rgba(77, 182, 255, 0.25);
|
|
902
|
+
border-radius: 4px;
|
|
903
|
+
padding: 1pt 5pt;
|
|
904
|
+
cursor: pointer;
|
|
905
|
+
color: var(--highlight, #4db6ff);
|
|
906
|
+
opacity: 0.8;
|
|
907
|
+
transition: opacity 0.15s, background 0.15s;
|
|
908
|
+
&:hover {
|
|
909
|
+
opacity: 1;
|
|
910
|
+
background: rgba(77, 182, 255, 0.2);
|
|
911
|
+
border-color: rgba(77, 182, 255, 0.4);
|
|
912
|
+
}
|
|
913
|
+
&.locked {
|
|
914
|
+
cursor: not-allowed;
|
|
915
|
+
opacity: 0.5;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
358
918
|
}
|
|
359
|
-
.
|
|
360
|
-
display:
|
|
919
|
+
.icon-btn {
|
|
920
|
+
display: flex;
|
|
361
921
|
align-items: center;
|
|
362
|
-
|
|
363
|
-
background:
|
|
364
|
-
border:
|
|
365
|
-
border-radius: 4px;
|
|
366
|
-
padding: 1pt 5pt;
|
|
922
|
+
justify-content: center;
|
|
923
|
+
background: none;
|
|
924
|
+
border: none;
|
|
367
925
|
cursor: pointer;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
926
|
+
padding: 3pt;
|
|
927
|
+
border-radius: 50%;
|
|
928
|
+
color: inherit;
|
|
929
|
+
opacity: 0.4;
|
|
930
|
+
&:hover {
|
|
931
|
+
opacity: 1;
|
|
932
|
+
background: rgba(128, 128, 128, 0.15);
|
|
933
|
+
}
|
|
934
|
+
&.active {
|
|
935
|
+
opacity: 1;
|
|
936
|
+
color: var(--highlight, #4db6ff);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
.history-dropdown {
|
|
940
|
+
position: absolute;
|
|
941
|
+
top: calc(100% + 2pt);
|
|
942
|
+
left: 0;
|
|
943
|
+
right: 0;
|
|
944
|
+
z-index: 101;
|
|
945
|
+
background: var(--dropdown-bg, var(--surface-bg, #fff));
|
|
946
|
+
border: 1px solid var(--dropdown-border, rgba(128, 128, 128, 0.2));
|
|
947
|
+
border-radius: 8px;
|
|
948
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
949
|
+
padding: 4pt 0;
|
|
950
|
+
display: flex;
|
|
951
|
+
flex-direction: column;
|
|
371
952
|
}
|
|
372
|
-
.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
953
|
+
.history-header {
|
|
954
|
+
font-size: 0.7em;
|
|
955
|
+
font-weight: 600;
|
|
956
|
+
opacity: 0.45;
|
|
957
|
+
padding: 2pt 10pt 4pt;
|
|
958
|
+
text-transform: uppercase;
|
|
959
|
+
letter-spacing: 0.5px;
|
|
376
960
|
}
|
|
377
|
-
.
|
|
961
|
+
.history-header-row {
|
|
962
|
+
display: flex;
|
|
963
|
+
align-items: center;
|
|
964
|
+
justify-content: space-between;
|
|
965
|
+
gap: 6pt;
|
|
966
|
+
padding-right: 6pt;
|
|
967
|
+
}
|
|
968
|
+
.history-clear-all {
|
|
969
|
+
border: none;
|
|
970
|
+
background: transparent;
|
|
971
|
+
cursor: pointer;
|
|
972
|
+
font-size: 0.75em;
|
|
973
|
+
opacity: 0.6;
|
|
974
|
+
&:hover {
|
|
975
|
+
opacity: 1;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
.history-item {
|
|
979
|
+
display: flex;
|
|
980
|
+
align-items: center;
|
|
981
|
+
padding: 0 4pt 0 0;
|
|
982
|
+
&:is(.focused, :hover) {
|
|
983
|
+
background: rgba(77, 182, 255, 0.08);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
.history-value {
|
|
987
|
+
flex: 1;
|
|
988
|
+
text-align: left;
|
|
989
|
+
background: none;
|
|
990
|
+
border: none;
|
|
991
|
+
cursor: pointer;
|
|
992
|
+
padding: 4pt 10pt;
|
|
993
|
+
font-family: var(--mono-font, monospace);
|
|
994
|
+
font-size: 0.88em;
|
|
995
|
+
color: inherit;
|
|
996
|
+
}
|
|
997
|
+
.history-remove {
|
|
998
|
+
min-width: 24px;
|
|
999
|
+
min-height: 24px;
|
|
378
1000
|
display: flex;
|
|
379
1001
|
align-items: center;
|
|
380
1002
|
justify-content: center;
|
|
@@ -383,16 +1005,28 @@ let next_mode = $derived.by(() => {
|
|
|
383
1005
|
cursor: pointer;
|
|
384
1006
|
padding: 3pt;
|
|
385
1007
|
border-radius: 50%;
|
|
1008
|
+
opacity: 0.3;
|
|
386
1009
|
color: inherit;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
background: rgba(128, 128, 128, 0.15);
|
|
1010
|
+
&:hover {
|
|
1011
|
+
opacity: 0.8;
|
|
1012
|
+
background: rgba(128, 128, 128, 0.15);
|
|
1013
|
+
}
|
|
392
1014
|
}
|
|
393
|
-
.
|
|
394
|
-
|
|
395
|
-
|
|
1015
|
+
.history-pin {
|
|
1016
|
+
display: flex;
|
|
1017
|
+
align-items: center;
|
|
1018
|
+
justify-content: center;
|
|
1019
|
+
background: none;
|
|
1020
|
+
border: none;
|
|
1021
|
+
cursor: pointer;
|
|
1022
|
+
padding: 3pt;
|
|
1023
|
+
border-radius: 50%;
|
|
1024
|
+
opacity: 0.3;
|
|
1025
|
+
color: inherit;
|
|
1026
|
+
&:hover {
|
|
1027
|
+
opacity: 0.8;
|
|
1028
|
+
background: rgba(128, 128, 128, 0.15);
|
|
1029
|
+
}
|
|
396
1030
|
}
|
|
397
1031
|
.examples-wrapper {
|
|
398
1032
|
position: relative;
|
|
@@ -412,10 +1046,10 @@ let next_mode = $derived.by(() => {
|
|
|
412
1046
|
display: flex;
|
|
413
1047
|
flex-direction: column;
|
|
414
1048
|
gap: 6pt;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1049
|
+
&.anchor-left {
|
|
1050
|
+
right: auto;
|
|
1051
|
+
left: 0;
|
|
1052
|
+
}
|
|
419
1053
|
}
|
|
420
1054
|
.example-category {
|
|
421
1055
|
display: flex;
|
|
@@ -443,9 +1077,55 @@ let next_mode = $derived.by(() => {
|
|
|
443
1077
|
font-family: var(--mono-font, monospace);
|
|
444
1078
|
color: var(--highlight, #4db6ff);
|
|
445
1079
|
cursor: pointer;
|
|
1080
|
+
&:hover {
|
|
1081
|
+
background: rgba(77, 182, 255, 0.2);
|
|
1082
|
+
border-color: rgba(77, 182, 255, 0.5);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
.token-chip-row {
|
|
1086
|
+
margin-top: 4pt;
|
|
1087
|
+
display: flex;
|
|
1088
|
+
flex-wrap: wrap;
|
|
1089
|
+
gap: 4pt;
|
|
446
1090
|
}
|
|
447
|
-
.
|
|
448
|
-
|
|
449
|
-
|
|
1091
|
+
.token-chip {
|
|
1092
|
+
border: 1px solid rgba(77, 182, 255, 0.35);
|
|
1093
|
+
background: rgba(77, 182, 255, 0.12);
|
|
1094
|
+
border-radius: 4px;
|
|
1095
|
+
font-family: var(--mono-font, monospace);
|
|
1096
|
+
font-size: 0.78em;
|
|
1097
|
+
padding: 2pt 6pt;
|
|
1098
|
+
cursor: pointer;
|
|
1099
|
+
color: inherit;
|
|
1100
|
+
&.exclude {
|
|
1101
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
1102
|
+
background: rgba(239, 68, 68, 0.12);
|
|
1103
|
+
}
|
|
1104
|
+
&.invalid {
|
|
1105
|
+
border-color: rgba(239, 68, 68, 0.65);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
.validation-message {
|
|
1109
|
+
margin-top: 4pt;
|
|
1110
|
+
font-size: 0.74em;
|
|
1111
|
+
opacity: 0.75;
|
|
1112
|
+
&.invalid {
|
|
1113
|
+
color: rgb(239, 68, 68);
|
|
1114
|
+
opacity: 0.95;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
@media (max-width: 700px) {
|
|
1118
|
+
.icon-btn {
|
|
1119
|
+
min-width: 28px;
|
|
1120
|
+
min-height: 28px;
|
|
1121
|
+
padding: 5pt;
|
|
1122
|
+
}
|
|
1123
|
+
:is(.history-remove, .history-pin) {
|
|
1124
|
+
min-width: 28px;
|
|
1125
|
+
min-height: 28px;
|
|
1126
|
+
}
|
|
1127
|
+
.history-value {
|
|
1128
|
+
padding: 6pt 10pt;
|
|
1129
|
+
}
|
|
450
1130
|
}
|
|
451
1131
|
</style>
|