matterviz 0.3.1 → 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/app.css +29 -0
- package/dist/brillouin/BrillouinZone.svelte +19 -61
- 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 +586 -94
- package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
- package/dist/composition/PieChart.svelte +43 -18
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull.svelte +4 -2
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +13 -44
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +16 -7
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +17 -7
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullStats.svelte +701 -226
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
- package/dist/convex-hull/demo-temperature.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +36 -0
- package/dist/convex-hull/helpers.d.ts +1 -1
- package/dist/convex-hull/helpers.js +2 -4
- package/dist/convex-hull/index.d.ts +1 -0
- 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 +5 -0
- package/dist/convex-hull/types.js +5 -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/types.d.ts +1 -0
- package/dist/fermi-surface/FermiSurface.svelte +20 -64
- 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/parse.js +16 -22
- 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 +111 -0
- package/dist/icons.js +111 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -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 +101 -45
- package/dist/isosurface/IsosurfaceControls.svelte +19 -0
- package/dist/isosurface/parse.js +73 -30
- package/dist/isosurface/slice.d.ts +2 -1
- package/dist/isosurface/slice.js +3 -3
- package/dist/isosurface/types.d.ts +13 -1
- package/dist/isosurface/types.js +98 -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 +83 -85
- package/dist/layout/json-tree/JsonTree.svelte +20 -19
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +196 -116
- package/dist/layout/json-tree/types.d.ts +10 -2
- package/dist/layout/json-tree/utils.d.ts +2 -0
- package/dist/layout/json-tree/utils.js +33 -0
- package/dist/math.d.ts +7 -0
- package/dist/math.js +358 -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/ColorScaleSelect.svelte +1 -1
- package/dist/plot/ElementScatter.svelte +3 -2
- 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/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 +2 -3
- 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 +13 -10
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +55 -66
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +17 -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 +29 -8
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CellSelect.svelte +92 -22
- package/dist/structure/Structure.svelte +108 -118
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +25 -22
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +7 -1
- package/dist/structure/StructureScene.svelte +104 -66
- package/dist/structure/StructureScene.svelte.d.ts +2 -1
- 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 +6 -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 +425 -65
- package/dist/table/HeatmapTable.svelte.d.ts +12 -1
- package/dist/table/ToggleMenu.svelte +2 -0
- package/dist/table/index.d.ts +2 -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 +30 -24
- 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,26 +1,54 @@
|
|
|
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
8
|
label: `Has elements`,
|
|
7
|
-
description: `Materials containing
|
|
8
|
-
examples: [`Li,Fe`,
|
|
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
|
|
22
50
|
history_key = `formula-filter-history`, // localStorage key for persisting history
|
|
23
|
-
onchange, onclear, ...rest } = $props();
|
|
51
|
+
validate, onparse, on_validation, onchange, onclear, ...rest } = $props();
|
|
24
52
|
let input_value = $state(value);
|
|
25
53
|
let examples_open = $state(false);
|
|
26
54
|
let history_open = $state(false);
|
|
@@ -29,10 +57,13 @@ let examples_wrapper = $state(null);
|
|
|
29
57
|
let focused_item_idx = $state(-1);
|
|
30
58
|
let focused_history_idx = $state(-1);
|
|
31
59
|
let anchor_left = $state(false);
|
|
60
|
+
let history_query = $state(``);
|
|
61
|
+
let validation = $state({ state: `valid`, message: null });
|
|
32
62
|
// Flatten examples for keyboard navigation
|
|
33
|
-
|
|
63
|
+
let all_examples = $derived(examples.flatMap((cat) => cat.examples));
|
|
34
64
|
// === History Management ===
|
|
35
65
|
const has_storage = typeof localStorage !== `undefined`;
|
|
66
|
+
const history_pins_key = $derived(`${history_key}-pins`);
|
|
36
67
|
function load_history() {
|
|
37
68
|
if (max_history <= 0 || !has_storage)
|
|
38
69
|
return [];
|
|
@@ -59,18 +90,50 @@ function save_history(entries) {
|
|
|
59
90
|
// localStorage may be unavailable (e.g. private browsing)
|
|
60
91
|
}
|
|
61
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
|
+
}
|
|
62
119
|
let history = $state(load_history());
|
|
120
|
+
let pinned_history = $state(load_pinned());
|
|
63
121
|
function add_to_history(entry) {
|
|
64
122
|
if (max_history <= 0 || !entry.trim())
|
|
65
123
|
return;
|
|
66
124
|
// Remove duplicate if present, then prepend
|
|
67
125
|
const filtered = history.filter((item) => item !== entry);
|
|
68
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));
|
|
69
129
|
save_history(history);
|
|
130
|
+
save_pinned(pinned_history);
|
|
70
131
|
}
|
|
71
132
|
function remove_from_history(entry) {
|
|
72
133
|
history = history.filter((item) => item !== entry);
|
|
134
|
+
pinned_history = pinned_history.filter((item) => item !== entry);
|
|
73
135
|
save_history(history);
|
|
136
|
+
save_pinned(pinned_history);
|
|
74
137
|
// Clamp focused index to prevent out-of-bounds access on Enter
|
|
75
138
|
if (history.length === 0)
|
|
76
139
|
history_open = false;
|
|
@@ -78,16 +141,41 @@ function remove_from_history(entry) {
|
|
|
78
141
|
focused_history_idx = visible_history.length - 1;
|
|
79
142
|
}
|
|
80
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
|
+
}
|
|
81
160
|
// Filtered history: exclude current value to avoid redundant suggestion
|
|
82
|
-
let visible_history = $derived
|
|
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
|
+
});
|
|
83
169
|
function close_history() {
|
|
84
170
|
history_open = false;
|
|
171
|
+
history_query = ``;
|
|
85
172
|
focused_history_idx = -1;
|
|
86
173
|
}
|
|
87
174
|
function open_history() {
|
|
88
175
|
if (max_history <= 0 || visible_history.length === 0 || examples_open)
|
|
89
176
|
return;
|
|
90
177
|
history_open = true;
|
|
178
|
+
history_query = ``;
|
|
91
179
|
focused_history_idx = -1;
|
|
92
180
|
}
|
|
93
181
|
function handle_document_click(event) {
|
|
@@ -113,14 +201,15 @@ function close_examples(restore_focus = true) {
|
|
|
113
201
|
// and re-infer mode accordingly. Without this, mode would only be set on first render.
|
|
114
202
|
let last_synced = $state(null);
|
|
115
203
|
$effect(() => {
|
|
116
|
-
input_value = value;
|
|
117
204
|
if (value !== last_synced) {
|
|
118
205
|
last_synced = value;
|
|
119
|
-
|
|
206
|
+
input_value = value;
|
|
207
|
+
if (value && !mode_locked) {
|
|
120
208
|
const inferred = infer_mode(value);
|
|
121
209
|
if (inferred !== search_mode)
|
|
122
210
|
search_mode = inferred;
|
|
123
211
|
}
|
|
212
|
+
run_validation(value, search_mode);
|
|
124
213
|
}
|
|
125
214
|
});
|
|
126
215
|
// Detect if dropdown would exit viewport on the right and adjust anchor
|
|
@@ -141,6 +230,12 @@ function infer_mode(input) {
|
|
|
141
230
|
const trimmed = input.trim();
|
|
142
231
|
if (!trimmed)
|
|
143
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`;
|
|
144
239
|
if (trimmed.includes(`,`))
|
|
145
240
|
return `elements`; // Li,Fe,O → has elements
|
|
146
241
|
if (trimmed.includes(`-`))
|
|
@@ -149,6 +244,196 @@ function infer_mode(input) {
|
|
|
149
244
|
}
|
|
150
245
|
// Cycle through modes: elements → chemsys → exact → elements
|
|
151
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
|
+
}
|
|
152
437
|
// Extract elements from any input format (formula, comma-separated, dash-separated)
|
|
153
438
|
// Always returns elements in alphabetical order for consistency, preserving wildcards (*)
|
|
154
439
|
function extract_elements(input) {
|
|
@@ -169,9 +454,13 @@ function extract_elements(input) {
|
|
|
169
454
|
// For formulas with wildcards, we can't parse them normally
|
|
170
455
|
if (has_wildcards(trimmed)) { // Use shared utility and extract unique elements
|
|
171
456
|
const tokens = parse_formula_with_wildcards(trimmed);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
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();
|
|
175
464
|
const wildcards = tokens.filter((token) => token.element === null).map(() => `*`);
|
|
176
465
|
return [...elements, ...wildcards];
|
|
177
466
|
}
|
|
@@ -194,6 +483,8 @@ function format_for_mode(elements, mode) {
|
|
|
194
483
|
return elements.join(``);
|
|
195
484
|
}
|
|
196
485
|
function cycle_mode() {
|
|
486
|
+
if (mode_locked)
|
|
487
|
+
return;
|
|
197
488
|
const current_idx = MODE_CYCLE.indexOf(search_mode);
|
|
198
489
|
const next_idx = (current_idx + 1) % MODE_CYCLE.length;
|
|
199
490
|
const next_mode = MODE_CYCLE[next_idx];
|
|
@@ -202,37 +493,37 @@ function cycle_mode() {
|
|
|
202
493
|
const reformatted = format_for_mode(elements, next_mode);
|
|
203
494
|
search_mode = next_mode;
|
|
204
495
|
last_synced = value = input_value = reformatted; // update last_synced to prevent effect re-inference
|
|
496
|
+
run_validation(reformatted, next_mode);
|
|
205
497
|
onchange?.(reformatted, next_mode);
|
|
206
498
|
}
|
|
207
|
-
function set_value(new_value) {
|
|
208
|
-
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));
|
|
209
501
|
last_synced = value = input_value = new_value; // update last_synced to prevent effect re-inference
|
|
210
502
|
search_mode = mode;
|
|
211
503
|
if (new_value.trim())
|
|
212
504
|
add_to_history(new_value);
|
|
213
505
|
close_history();
|
|
506
|
+
run_validation(value, mode);
|
|
214
507
|
onchange?.(value, mode);
|
|
215
508
|
}
|
|
216
509
|
function sync_value() {
|
|
217
|
-
const trimmed = input_value.trim();
|
|
510
|
+
const trimmed = normalize_unicode_formula(input_value).trim();
|
|
218
511
|
if (!trimmed)
|
|
219
512
|
return set_value(``);
|
|
220
|
-
const mode = infer_mode(trimmed);
|
|
221
|
-
if (mode === `exact`)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
];
|
|
235
|
-
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);
|
|
236
527
|
}
|
|
237
528
|
function onkeydown(event) {
|
|
238
529
|
if (event.key === `Enter`) {
|
|
@@ -266,12 +557,20 @@ function onkeydown(event) {
|
|
|
266
557
|
}
|
|
267
558
|
}
|
|
268
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);
|
|
567
|
+
}
|
|
269
568
|
function clear_filter() {
|
|
270
569
|
onclear?.();
|
|
271
570
|
set_value(``);
|
|
272
571
|
}
|
|
273
572
|
function apply_example(example) {
|
|
274
|
-
set_value(example);
|
|
573
|
+
set_value(example, mode_locked ? search_mode : infer_mode(example));
|
|
275
574
|
close_examples();
|
|
276
575
|
}
|
|
277
576
|
function toggle_examples(event) {
|
|
@@ -302,6 +601,19 @@ function handle_menu_keydown(event) {
|
|
|
302
601
|
key_actions[event.key]();
|
|
303
602
|
}
|
|
304
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
|
+
}
|
|
305
617
|
// Focus the active menu item when index changes
|
|
306
618
|
$effect(() => {
|
|
307
619
|
if (!examples_open || focused_item_idx < 0)
|
|
@@ -320,6 +632,8 @@ const MODE_LABELS = {
|
|
|
320
632
|
exact: `exact formula`,
|
|
321
633
|
};
|
|
322
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);
|
|
323
637
|
// Preview of next mode cycle step for tooltip
|
|
324
638
|
let next_mode = $derived.by(() => {
|
|
325
639
|
const next = MODE_CYCLE[(MODE_CYCLE.indexOf(search_mode) + 1) % MODE_CYCLE.length];
|
|
@@ -331,7 +645,14 @@ let next_mode = $derived.by(() => {
|
|
|
331
645
|
|
|
332
646
|
<svelte:document onclick={handle_document_click} />
|
|
333
647
|
|
|
334
|
-
<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
|
+
>
|
|
335
656
|
<input
|
|
336
657
|
bind:this={input_element}
|
|
337
658
|
bind:value={input_value}
|
|
@@ -342,6 +663,13 @@ let next_mode = $derived.by(() => {
|
|
|
342
663
|
sync_value()
|
|
343
664
|
}}
|
|
344
665
|
onfocus={open_history}
|
|
666
|
+
{oninput}
|
|
667
|
+
onpaste={() => {
|
|
668
|
+
requestAnimationFrame(() => {
|
|
669
|
+
input_value = normalize_unicode_formula(input_value)
|
|
670
|
+
oninput()
|
|
671
|
+
})
|
|
672
|
+
}}
|
|
345
673
|
{onkeydown}
|
|
346
674
|
{placeholder}
|
|
347
675
|
{disabled}
|
|
@@ -349,7 +677,21 @@ let next_mode = $derived.by(() => {
|
|
|
349
677
|
/>
|
|
350
678
|
{#if history_open && visible_history.length > 0}
|
|
351
679
|
<div class="history-dropdown" role="listbox" aria-label="Recent searches">
|
|
352
|
-
<
|
|
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>
|
|
353
695
|
{#each visible_history as entry, idx (entry)}
|
|
354
696
|
<div class="history-item" class:focused={idx === focused_history_idx}>
|
|
355
697
|
<button
|
|
@@ -364,6 +706,21 @@ let next_mode = $derived.by(() => {
|
|
|
364
706
|
>
|
|
365
707
|
{entry}
|
|
366
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>
|
|
367
724
|
<button
|
|
368
725
|
type="button"
|
|
369
726
|
class="history-remove"
|
|
@@ -384,20 +741,37 @@ let next_mode = $derived.by(() => {
|
|
|
384
741
|
<button
|
|
385
742
|
type="button"
|
|
386
743
|
class="mode-hint clickable"
|
|
744
|
+
class:locked={mode_locked}
|
|
387
745
|
onclick={cycle_mode}
|
|
388
|
-
title=
|
|
389
|
-
|
|
746
|
+
title={mode_locked
|
|
747
|
+
? `Mode is locked`
|
|
748
|
+
: `Click to switch to '${next_mode.mode}' → ${next_mode.value}`}
|
|
749
|
+
{@attach tooltip()}
|
|
390
750
|
aria-label="Change search mode"
|
|
391
751
|
>
|
|
392
752
|
{mode_hint}
|
|
393
753
|
</button>
|
|
394
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}
|
|
395
768
|
{#if show_clear_button && value && !disabled}
|
|
396
769
|
<button
|
|
397
770
|
type="button"
|
|
398
771
|
class="icon-btn clear-btn"
|
|
399
772
|
onclick={clear_filter}
|
|
400
773
|
title="Clear (Escape)"
|
|
774
|
+
{@attach tooltip()}
|
|
401
775
|
aria-label="Clear filter"
|
|
402
776
|
>
|
|
403
777
|
<Icon icon="Close" style="width: 1em; height: 1em" />
|
|
@@ -425,7 +799,7 @@ let next_mode = $derived.by(() => {
|
|
|
425
799
|
tabindex="-1"
|
|
426
800
|
onkeydown={handle_menu_keydown}
|
|
427
801
|
>
|
|
428
|
-
{#each
|
|
802
|
+
{#each examples as category (category.label)}
|
|
429
803
|
<div class="example-category">
|
|
430
804
|
<div class="category-label">{category.label}:</div>
|
|
431
805
|
<div class="example-tags">
|
|
@@ -450,24 +824,58 @@ let next_mode = $derived.by(() => {
|
|
|
450
824
|
</div>
|
|
451
825
|
{/if}
|
|
452
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}
|
|
453
853
|
|
|
454
854
|
<style>
|
|
455
855
|
.formula-filter {
|
|
456
856
|
position: relative;
|
|
457
857
|
display: flex;
|
|
458
858
|
align-items: center;
|
|
459
|
-
gap:
|
|
460
|
-
padding: 4pt 8pt;
|
|
461
|
-
border-radius:
|
|
462
|
-
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));
|
|
463
863
|
transition: background 0.15s;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
879
|
}
|
|
472
880
|
input {
|
|
473
881
|
flex: 1;
|
|
@@ -478,31 +886,35 @@ let next_mode = $derived.by(() => {
|
|
|
478
886
|
padding: 2pt 0;
|
|
479
887
|
outline: none;
|
|
480
888
|
font-family: var(--mono-font, monospace);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
889
|
+
&::placeholder {
|
|
890
|
+
opacity: 0.4;
|
|
891
|
+
}
|
|
484
892
|
}
|
|
485
893
|
.mode-hint {
|
|
486
894
|
opacity: 0.5;
|
|
487
895
|
white-space: nowrap;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
+
}
|
|
506
918
|
}
|
|
507
919
|
.icon-btn {
|
|
508
920
|
display: flex;
|
|
@@ -515,14 +927,14 @@ let next_mode = $derived.by(() => {
|
|
|
515
927
|
border-radius: 50%;
|
|
516
928
|
color: inherit;
|
|
517
929
|
opacity: 0.4;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
+
}
|
|
526
938
|
}
|
|
527
939
|
.history-dropdown {
|
|
528
940
|
position: absolute;
|
|
@@ -546,14 +958,30 @@ let next_mode = $derived.by(() => {
|
|
|
546
958
|
text-transform: uppercase;
|
|
547
959
|
letter-spacing: 0.5px;
|
|
548
960
|
}
|
|
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
|
+
}
|
|
549
978
|
.history-item {
|
|
550
979
|
display: flex;
|
|
551
980
|
align-items: center;
|
|
552
981
|
padding: 0 4pt 0 0;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
background: rgba(77, 182, 255, 0.08);
|
|
982
|
+
&:is(.focused, :hover) {
|
|
983
|
+
background: rgba(77, 182, 255, 0.08);
|
|
984
|
+
}
|
|
557
985
|
}
|
|
558
986
|
.history-value {
|
|
559
987
|
flex: 1;
|
|
@@ -567,6 +995,8 @@ let next_mode = $derived.by(() => {
|
|
|
567
995
|
color: inherit;
|
|
568
996
|
}
|
|
569
997
|
.history-remove {
|
|
998
|
+
min-width: 24px;
|
|
999
|
+
min-height: 24px;
|
|
570
1000
|
display: flex;
|
|
571
1001
|
align-items: center;
|
|
572
1002
|
justify-content: center;
|
|
@@ -577,10 +1007,26 @@ let next_mode = $derived.by(() => {
|
|
|
577
1007
|
border-radius: 50%;
|
|
578
1008
|
opacity: 0.3;
|
|
579
1009
|
color: inherit;
|
|
1010
|
+
&:hover {
|
|
1011
|
+
opacity: 0.8;
|
|
1012
|
+
background: rgba(128, 128, 128, 0.15);
|
|
1013
|
+
}
|
|
580
1014
|
}
|
|
581
|
-
.history-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}
|
|
584
1030
|
}
|
|
585
1031
|
.examples-wrapper {
|
|
586
1032
|
position: relative;
|
|
@@ -600,10 +1046,10 @@ let next_mode = $derived.by(() => {
|
|
|
600
1046
|
display: flex;
|
|
601
1047
|
flex-direction: column;
|
|
602
1048
|
gap: 6pt;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1049
|
+
&.anchor-left {
|
|
1050
|
+
right: auto;
|
|
1051
|
+
left: 0;
|
|
1052
|
+
}
|
|
607
1053
|
}
|
|
608
1054
|
.example-category {
|
|
609
1055
|
display: flex;
|
|
@@ -631,9 +1077,55 @@ let next_mode = $derived.by(() => {
|
|
|
631
1077
|
font-family: var(--mono-font, monospace);
|
|
632
1078
|
color: var(--highlight, #4db6ff);
|
|
633
1079
|
cursor: pointer;
|
|
1080
|
+
&:hover {
|
|
1081
|
+
background: rgba(77, 182, 255, 0.2);
|
|
1082
|
+
border-color: rgba(77, 182, 255, 0.5);
|
|
1083
|
+
}
|
|
634
1084
|
}
|
|
635
|
-
.
|
|
636
|
-
|
|
637
|
-
|
|
1085
|
+
.token-chip-row {
|
|
1086
|
+
margin-top: 4pt;
|
|
1087
|
+
display: flex;
|
|
1088
|
+
flex-wrap: wrap;
|
|
1089
|
+
gap: 4pt;
|
|
1090
|
+
}
|
|
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
|
+
}
|
|
638
1130
|
}
|
|
639
1131
|
</style>
|