matterviz 0.3.2 → 0.3.4
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/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/element/data.js +1 -1
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,26 +1,70 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Icon from '../Icon.svelte'
|
|
3
|
+
import { get_alphabetical_formula } from './format'
|
|
4
|
+
import { ELEM_SYMBOLS } from '../labels'
|
|
5
|
+
import { tooltip } from 'svelte-multiselect'
|
|
6
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
7
|
+
import type { FormulaSearchMode } from './index'
|
|
8
|
+
import {
|
|
9
|
+
extract_formula_elements,
|
|
10
|
+
has_wildcards,
|
|
11
|
+
normalize_element_symbols,
|
|
12
|
+
parse_formula,
|
|
13
|
+
parse_formula_with_wildcards,
|
|
14
|
+
} from './parse'
|
|
15
|
+
|
|
16
|
+
type SearchExampleCategory = {
|
|
17
|
+
label: string
|
|
18
|
+
description: string
|
|
19
|
+
examples: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type FormulaFilterToken = {
|
|
23
|
+
raw: string
|
|
24
|
+
element: string
|
|
25
|
+
operator: `include` | `exclude`
|
|
26
|
+
constraint: string | null
|
|
27
|
+
is_wildcard: boolean
|
|
28
|
+
is_valid: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type FormulaFilterParseResult = {
|
|
32
|
+
value: string
|
|
33
|
+
normalized_value: string
|
|
34
|
+
search_mode: FormulaSearchMode
|
|
35
|
+
tokens: FormulaFilterToken[]
|
|
36
|
+
has_wildcards: boolean
|
|
37
|
+
is_valid: boolean
|
|
38
|
+
error_message: string | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type FormulaFilterValidation = {
|
|
42
|
+
state: `valid` | `warning` | `invalid`
|
|
43
|
+
message: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_SEARCH_EXAMPLES: SearchExampleCategory[] = [
|
|
7
47
|
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
48
|
+
label: `Has elements`,
|
|
49
|
+
description:
|
|
50
|
+
`Materials containing these elements. Operators/ranges: +Li,-O,Fe:1-2. Use * for any element.`,
|
|
51
|
+
examples: [`Li,Fe`, `+Li,-O`, `Li,*,*`],
|
|
11
52
|
},
|
|
12
53
|
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
54
|
+
label: `Chemical system`,
|
|
55
|
+
description:
|
|
56
|
+
`Materials with only these elements (no others). Wildcards/ranges supported.`,
|
|
57
|
+
examples: [`Li-Fe-O`, `Li-Fe-*-*`, `*-*-O`],
|
|
16
58
|
},
|
|
17
59
|
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
60
|
+
label: `Exact formula`,
|
|
61
|
+
description:
|
|
62
|
+
`Materials with this exact stoichiometry. Unicode paste, wildcards, and canonicalization supported.`,
|
|
63
|
+
examples: [`LiFePO4`, `LiFe*2*`, `*2O3`],
|
|
21
64
|
},
|
|
22
|
-
]
|
|
23
|
-
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const SUBSCRIPT_TO_ASCII: Record<string, string> = {
|
|
24
68
|
[`\u2080`]: `0`,
|
|
25
69
|
[`\u2081`]: `1`,
|
|
26
70
|
[`\u2082`]: `2`,
|
|
@@ -31,8 +75,9 @@ const SUBSCRIPT_TO_ASCII = {
|
|
|
31
75
|
[`\u2087`]: `7`,
|
|
32
76
|
[`\u2088`]: `8`,
|
|
33
77
|
[`\u2089`]: `9`,
|
|
34
|
-
}
|
|
35
|
-
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SUPERSCRIPT_TO_ASCII: Record<string, string> = {
|
|
36
81
|
[`\u2070`]: `0`,
|
|
37
82
|
[`\u00B9`]: `1`,
|
|
38
83
|
[`\u00B2`]: `2`,
|
|
@@ -45,602 +90,685 @@ const SUPERSCRIPT_TO_ASCII = {
|
|
|
45
90
|
[`\u2079`]: `9`,
|
|
46
91
|
[`\u207A`]: `+`,
|
|
47
92
|
[`\u207B`]: `-`,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let {
|
|
96
|
+
value = $bindable(``),
|
|
97
|
+
search_mode = $bindable(`elements`),
|
|
98
|
+
input_element = $bindable(null),
|
|
99
|
+
show_clear_button = true,
|
|
100
|
+
show_examples = true,
|
|
101
|
+
show_mode_lock = true,
|
|
102
|
+
show_chip_editor = true,
|
|
103
|
+
normalize_exact = true,
|
|
104
|
+
examples = DEFAULT_SEARCH_EXAMPLES,
|
|
105
|
+
disabled = false,
|
|
106
|
+
mode_locked = $bindable(false),
|
|
107
|
+
max_history = 5, // Max recent inputs to remember; 0 disables history dropdown
|
|
108
|
+
history_key = `formula-filter-history`, // localStorage key for persisting history
|
|
109
|
+
validate,
|
|
110
|
+
onparse,
|
|
111
|
+
on_validation,
|
|
112
|
+
onchange,
|
|
113
|
+
onclear,
|
|
114
|
+
...rest
|
|
115
|
+
}: {
|
|
116
|
+
value: string // Current filter value (normalized on blur/enter)
|
|
117
|
+
search_mode?: FormulaSearchMode // Inferred search mode based on input format
|
|
118
|
+
input_element?: HTMLInputElement | null // Reference to the input element for programmatic focus
|
|
119
|
+
show_clear_button?: boolean // Show clear button when value is non-empty
|
|
120
|
+
show_examples?: boolean // Show the help button and examples dropdown
|
|
121
|
+
show_mode_lock?: boolean // Show mode lock toggle button
|
|
122
|
+
show_chip_editor?: boolean // Show token chip editor for tokenized modes
|
|
123
|
+
normalize_exact?: boolean // Canonicalize exact formulas on submit
|
|
124
|
+
examples?: SearchExampleCategory[] // Override built-in search example categories
|
|
125
|
+
disabled?: boolean // Disable all inputs
|
|
126
|
+
mode_locked?: boolean // Prevent auto mode inference and mode cycling
|
|
127
|
+
max_history?: number // Max recent inputs to remember; 0 disables history dropdown
|
|
128
|
+
history_key?: string // localStorage key for persisting history
|
|
129
|
+
validate?: (
|
|
130
|
+
value: string,
|
|
131
|
+
search_mode: FormulaSearchMode,
|
|
132
|
+
parsed: FormulaFilterParseResult,
|
|
133
|
+
) => FormulaFilterValidation | null
|
|
134
|
+
onparse?: (parsed: FormulaFilterParseResult) => void
|
|
135
|
+
on_validation?: (validation: FormulaFilterValidation) => void
|
|
136
|
+
onchange?: (value: string, search_mode: FormulaSearchMode) => void // Callback when value changes
|
|
137
|
+
onclear?: () => void // Callback when clear button is clicked
|
|
138
|
+
} & HTMLAttributes<HTMLDivElement> = $props()
|
|
139
|
+
|
|
140
|
+
let input_value = $state(value)
|
|
141
|
+
let examples_open = $state(false)
|
|
142
|
+
let history_open = $state(false)
|
|
143
|
+
let wrapper: HTMLDivElement | null = $state(null)
|
|
144
|
+
let examples_wrapper: HTMLDivElement | null = $state(null)
|
|
145
|
+
let focused_item_idx = $state(-1)
|
|
146
|
+
let focused_history_idx = $state(-1)
|
|
147
|
+
let anchor_left = $state(false)
|
|
148
|
+
let history_query = $state(``)
|
|
149
|
+
let validation = $state<FormulaFilterValidation>({ state: `valid`, message: null })
|
|
150
|
+
|
|
151
|
+
// Flatten examples for keyboard navigation
|
|
152
|
+
let all_examples = $derived(examples.flatMap((cat) => cat.examples))
|
|
153
|
+
|
|
154
|
+
// === History Management ===
|
|
155
|
+
const has_storage = typeof localStorage !== `undefined`
|
|
156
|
+
const history_pins_key = $derived(`${history_key}-pins`)
|
|
157
|
+
|
|
158
|
+
function load_history(): string[] {
|
|
159
|
+
if (max_history <= 0 || !has_storage) return []
|
|
70
160
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
catch {
|
|
80
|
-
|
|
161
|
+
const raw = localStorage.getItem(history_key)
|
|
162
|
+
if (!raw) return []
|
|
163
|
+
const parsed: unknown = JSON.parse(raw)
|
|
164
|
+
if (!Array.isArray(parsed)) return []
|
|
165
|
+
return parsed.filter((item): item is string => typeof item === `string`).slice(
|
|
166
|
+
0,
|
|
167
|
+
max_history,
|
|
168
|
+
)
|
|
169
|
+
} catch {
|
|
170
|
+
return []
|
|
81
171
|
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function save_history(entries: string[]): void {
|
|
175
|
+
if (max_history <= 0 || !has_storage) return
|
|
86
176
|
try {
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// localStorage may be unavailable (e.g. private browsing)
|
|
177
|
+
localStorage.setItem(history_key, JSON.stringify(entries.slice(0, max_history)))
|
|
178
|
+
} catch {
|
|
179
|
+
// localStorage may be unavailable (e.g. private browsing)
|
|
91
180
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function load_pinned(): string[] {
|
|
184
|
+
if (max_history <= 0 || !has_storage) return []
|
|
96
185
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
186
|
+
const raw = localStorage.getItem(history_pins_key)
|
|
187
|
+
if (!raw) return []
|
|
188
|
+
const parsed: unknown = JSON.parse(raw)
|
|
189
|
+
if (!Array.isArray(parsed)) return []
|
|
190
|
+
return parsed.filter((item): item is string => typeof item === `string`)
|
|
191
|
+
} catch {
|
|
192
|
+
return []
|
|
104
193
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
function save_pinned(entries) {
|
|
110
|
-
if (max_history <= 0 || !has_storage)
|
|
111
|
-
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function save_pinned(entries: string[]): void {
|
|
197
|
+
if (max_history <= 0 || !has_storage) return
|
|
112
198
|
try {
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// localStorage may be unavailable
|
|
199
|
+
localStorage.setItem(history_pins_key, JSON.stringify(entries))
|
|
200
|
+
} catch {
|
|
201
|
+
// localStorage may be unavailable
|
|
117
202
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let history = $state<string[]>(load_history())
|
|
206
|
+
let pinned_history = $state<string[]>(load_pinned())
|
|
207
|
+
|
|
208
|
+
function add_to_history(entry: string): void {
|
|
209
|
+
if (max_history <= 0 || !entry.trim()) return
|
|
124
210
|
// Remove duplicate if present, then prepend
|
|
125
|
-
const filtered = history.filter((item) => item !== entry)
|
|
126
|
-
history = [entry, ...filtered].slice(0, max_history)
|
|
211
|
+
const filtered = history.filter((item) => item !== entry)
|
|
212
|
+
history = [entry, ...filtered].slice(0, max_history)
|
|
127
213
|
// 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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
214
|
+
pinned_history = pinned_history.filter((item) => history.includes(item))
|
|
215
|
+
save_history(history)
|
|
216
|
+
save_pinned(pinned_history)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function remove_from_history(entry: string): void {
|
|
220
|
+
history = history.filter((item) => item !== entry)
|
|
221
|
+
pinned_history = pinned_history.filter((item) => item !== entry)
|
|
222
|
+
save_history(history)
|
|
223
|
+
save_pinned(pinned_history)
|
|
137
224
|
// Clamp focused index to prevent out-of-bounds access on Enter
|
|
138
|
-
if (history.length === 0)
|
|
139
|
-
history_open = false;
|
|
225
|
+
if (history.length === 0) history_open = false
|
|
140
226
|
else if (focused_history_idx >= visible_history.length) {
|
|
141
|
-
|
|
227
|
+
focused_history_idx = visible_history.length - 1
|
|
142
228
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toggle_pin_history(entry: string): void {
|
|
145
232
|
pinned_history = pinned_history.includes(entry)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
save_pinned(pinned_history)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
233
|
+
? pinned_history.filter((item) => item !== entry)
|
|
234
|
+
: [entry, ...pinned_history.filter((item) => item !== entry)]
|
|
235
|
+
save_pinned(pinned_history)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function clear_history(): void {
|
|
239
|
+
history = []
|
|
240
|
+
pinned_history = []
|
|
241
|
+
save_history(history)
|
|
242
|
+
save_pinned(pinned_history)
|
|
243
|
+
close_history()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function is_pinned(entry: string): boolean {
|
|
247
|
+
return pinned_history.includes(entry)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Filtered history: exclude current value to avoid redundant suggestion
|
|
251
|
+
let visible_history = $derived.by(() => {
|
|
162
252
|
const filtered = history
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
253
|
+
.filter((item) => item !== value)
|
|
254
|
+
.filter((item) =>
|
|
255
|
+
item.toLowerCase().includes(history_query.toLowerCase().trim())
|
|
256
|
+
)
|
|
257
|
+
const pinned = filtered.filter((item) => pinned_history.includes(item))
|
|
258
|
+
const unpinned = filtered.filter((item) => !pinned_history.includes(item))
|
|
259
|
+
return [...pinned, ...unpinned]
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
function close_history(): void {
|
|
263
|
+
history_open = false
|
|
264
|
+
history_query = ``
|
|
265
|
+
focused_history_idx = -1
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function open_history(): void {
|
|
269
|
+
if (max_history <= 0 || visible_history.length === 0 || examples_open) return
|
|
270
|
+
history_open = true
|
|
271
|
+
history_query = ``
|
|
272
|
+
focused_history_idx = -1
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function handle_document_click(event: MouseEvent): void {
|
|
276
|
+
if (!wrapper || (!examples_open && !history_open)) return
|
|
277
|
+
const target = event.target
|
|
278
|
+
if (!(target instanceof Node)) return
|
|
187
279
|
if (!wrapper.contains(target)) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (history_open)
|
|
191
|
-
close_history();
|
|
280
|
+
if (examples_open) close_examples()
|
|
281
|
+
if (history_open) close_history()
|
|
192
282
|
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
$
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function close_examples(restore_focus = true): void {
|
|
286
|
+
examples_open = false
|
|
287
|
+
focused_item_idx = -1
|
|
288
|
+
if (restore_focus) input_element?.focus({ preventScroll: true })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Track last synced value to detect external changes (e.g. from URL params)
|
|
292
|
+
// and re-infer mode accordingly. Without this, mode would only be set on first render.
|
|
293
|
+
let last_synced = $state<string | null>(null)
|
|
294
|
+
$effect(() => {
|
|
204
295
|
if (value !== last_synced) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
run_validation(value, search_mode);
|
|
296
|
+
last_synced = value
|
|
297
|
+
input_value = value
|
|
298
|
+
if (value && !mode_locked) {
|
|
299
|
+
const inferred = infer_mode(value)
|
|
300
|
+
if (inferred !== search_mode) search_mode = inferred
|
|
301
|
+
}
|
|
302
|
+
run_validation(value, search_mode)
|
|
213
303
|
}
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Detect if dropdown would exit viewport on the right and adjust anchor
|
|
307
|
+
$effect(() => {
|
|
308
|
+
if (!examples_open || !examples_wrapper) return
|
|
219
309
|
requestAnimationFrame(() => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
})
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (/^[+\-!]\s*\w/.test(trimmed))
|
|
234
|
-
|
|
235
|
-
if (trimmed.includes(
|
|
236
|
-
|
|
237
|
-
if (trimmed.includes(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const MODE_CYCLE = [`elements`, `chemsys`, `exact`];
|
|
247
|
-
function normalize_unicode_formula(input) {
|
|
248
|
-
let normalized = input;
|
|
310
|
+
const dropdown = examples_wrapper?.querySelector(`.examples-dropdown`) as
|
|
311
|
+
| HTMLElement
|
|
312
|
+
| null
|
|
313
|
+
if (!dropdown) return
|
|
314
|
+
const rect = dropdown.getBoundingClientRect()
|
|
315
|
+
if (rect.right > window.innerWidth && !anchor_left) anchor_left = true
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// Infer search mode from input format
|
|
320
|
+
function infer_mode(input: string): FormulaSearchMode {
|
|
321
|
+
const trimmed = input.trim()
|
|
322
|
+
if (!trimmed) return `elements`
|
|
323
|
+
if (/^[+\-!]\s*\w/.test(trimmed)) return `elements`
|
|
324
|
+
if (trimmed.includes(`+`) || trimmed.includes(`!`)) return `elements`
|
|
325
|
+
if (trimmed.includes(`:`)) return trimmed.includes(`-`) ? `chemsys` : `elements`
|
|
326
|
+
if (trimmed.includes(`,`)) return `elements` // Li,Fe,O → has elements
|
|
327
|
+
if (trimmed.includes(`-`)) return `chemsys` // Li-Fe-O → chemical system
|
|
328
|
+
return `exact` // LiFePO4 → exact formula
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Cycle through modes: elements → chemsys → exact → elements
|
|
332
|
+
const MODE_CYCLE: FormulaSearchMode[] = [`elements`, `chemsys`, `exact`]
|
|
333
|
+
|
|
334
|
+
function normalize_unicode_formula(input: string): string {
|
|
335
|
+
let normalized = input
|
|
249
336
|
for (const [subscript, ascii] of Object.entries(SUBSCRIPT_TO_ASCII)) {
|
|
250
|
-
|
|
337
|
+
normalized = normalized.replaceAll(subscript, ascii)
|
|
251
338
|
}
|
|
252
339
|
for (const [superscript, ascii] of Object.entries(SUPERSCRIPT_TO_ASCII)) {
|
|
253
|
-
|
|
340
|
+
normalized = normalized.replaceAll(superscript, ascii)
|
|
254
341
|
}
|
|
255
342
|
return normalized
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
343
|
+
.replaceAll(`·`, ``)
|
|
344
|
+
.replaceAll(`⋅`, ``)
|
|
345
|
+
.replaceAll(`−`, `-`)
|
|
346
|
+
.replace(/\s+/g, ``)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalize_exact_formula(input: string): string {
|
|
350
|
+
const sanitized_input = normalize_unicode_formula(input.trim())
|
|
351
|
+
if (!sanitize_exact_formula(sanitized_input).is_valid) return sanitized_input
|
|
352
|
+
|
|
265
353
|
if (!has_wildcards(sanitized_input)) {
|
|
266
|
-
|
|
267
|
-
|
|
354
|
+
const canonical = get_alphabetical_formula(sanitized_input, true, ``)
|
|
355
|
+
return canonical || sanitized_input
|
|
268
356
|
}
|
|
357
|
+
|
|
269
358
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
359
|
+
const tokens = parse_formula_with_wildcards(sanitized_input)
|
|
360
|
+
const explicit = tokens
|
|
361
|
+
.filter((token) => token.element !== null)
|
|
362
|
+
.map((token) => ({ element: token.element as string, count: token.count }))
|
|
363
|
+
const wildcard_tokens = tokens.filter((token) => token.element === null)
|
|
364
|
+
|
|
365
|
+
// Merge explicit element counts before sorting.
|
|
366
|
+
const merged_explicit: Array<{ element: string; count: number }> = []
|
|
367
|
+
for (const token of explicit) {
|
|
368
|
+
const existing = merged_explicit.find((item) =>
|
|
369
|
+
item.element === token.element
|
|
370
|
+
)
|
|
371
|
+
if (existing) existing.count += token.count
|
|
372
|
+
else merged_explicit.push(token)
|
|
373
|
+
}
|
|
374
|
+
const sorted_explicit = merged_explicit.sort((elem_a, elem_b) =>
|
|
375
|
+
elem_a.element.localeCompare(elem_b.element)
|
|
376
|
+
)
|
|
377
|
+
const wildcard_str = wildcard_tokens.map((token) =>
|
|
378
|
+
token.count > 1 ? `*${token.count}` : `*`
|
|
379
|
+
).join(``)
|
|
380
|
+
const explicit_str = sorted_explicit.map((token) =>
|
|
381
|
+
token.count > 1 ? `${token.element}${token.count}` : token.element
|
|
382
|
+
).join(``)
|
|
383
|
+
return `${explicit_str}${wildcard_str}`
|
|
384
|
+
} catch {
|
|
385
|
+
return sanitized_input
|
|
291
386
|
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function is_valid_constraint(constraint: string): boolean {
|
|
390
|
+
if (!constraint) return true
|
|
296
391
|
return /^\d+$/.test(constraint) || /^\d+-\d+$/.test(constraint) ||
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
392
|
+
/^(>=|<=|>|<)\d+$/.test(constraint)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function strip_operator_prefix(
|
|
396
|
+
token: string,
|
|
397
|
+
): { operator: FormulaFilterToken[`operator`]; value: string } {
|
|
300
398
|
const operator = token.startsWith(`-`) || token.startsWith(`!`)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const value =
|
|
399
|
+
? `exclude`
|
|
400
|
+
: `include`
|
|
401
|
+
const value =
|
|
402
|
+
token.startsWith(`+`) || token.startsWith(`-`) || token.startsWith(`!`)
|
|
304
403
|
? token.slice(1)
|
|
305
|
-
: token
|
|
306
|
-
return { operator, value }
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
404
|
+
: token
|
|
405
|
+
return { operator, value }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function serialize_token(
|
|
409
|
+
token: Pick<FormulaFilterToken, `operator` | `element` | `constraint`>,
|
|
410
|
+
): string {
|
|
411
|
+
const prefix = token.operator === `exclude` ? `-` : ``
|
|
412
|
+
const suffix = token.constraint ? `:${token.constraint}` : ``
|
|
413
|
+
return `${prefix}${token.element}${suffix}`
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function token_chip_label(
|
|
417
|
+
token: Pick<FormulaFilterToken, `operator` | `element` | `constraint`>,
|
|
418
|
+
): string {
|
|
419
|
+
const prefix = token.operator === `exclude` ? `-` : `+`
|
|
420
|
+
const suffix = token.constraint ? `:${token.constraint}` : ``
|
|
421
|
+
return `${prefix}${token.element}${suffix}`
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function parse_token(raw_token: string): FormulaFilterToken {
|
|
425
|
+
const token = raw_token.trim()
|
|
426
|
+
const { operator, value: without_operator } = strip_operator_prefix(token)
|
|
427
|
+
const [element_part, constraint] = without_operator.split(`:`)
|
|
428
|
+
const element = element_part.trim()
|
|
429
|
+
const is_wildcard = element === `*`
|
|
324
430
|
const is_valid_element = is_wildcard ||
|
|
325
|
-
|
|
326
|
-
const normalized_constraint = constraint?.trim() || null
|
|
431
|
+
ELEM_SYMBOLS.includes(element as (typeof ELEM_SYMBOLS)[number])
|
|
432
|
+
const normalized_constraint = constraint?.trim() || null
|
|
327
433
|
const is_valid = is_valid_element && (normalized_constraint === null ||
|
|
328
|
-
|
|
434
|
+
is_valid_constraint(normalized_constraint))
|
|
435
|
+
|
|
329
436
|
return {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
437
|
+
raw: raw_token,
|
|
438
|
+
element,
|
|
439
|
+
operator,
|
|
440
|
+
constraint: normalized_constraint,
|
|
441
|
+
is_wildcard,
|
|
442
|
+
is_valid,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function tokenize_query(
|
|
447
|
+
input: string,
|
|
448
|
+
mode: FormulaSearchMode,
|
|
449
|
+
): FormulaFilterToken[] {
|
|
450
|
+
const trimmed = input.trim()
|
|
451
|
+
if (!trimmed) return []
|
|
342
452
|
if (mode === `exact`) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
453
|
+
return [{
|
|
454
|
+
raw: trimmed,
|
|
455
|
+
element: trimmed,
|
|
456
|
+
operator: `include`,
|
|
457
|
+
constraint: null,
|
|
458
|
+
is_wildcard: has_wildcards(trimmed),
|
|
459
|
+
is_valid: sanitize_exact_formula(trimmed).is_valid,
|
|
460
|
+
}]
|
|
351
461
|
}
|
|
352
|
-
const normalized = mode === `chemsys` ? trimmed.replaceAll(`,`, `-`) : trimmed
|
|
462
|
+
const normalized = mode === `chemsys` ? trimmed.replaceAll(`,`, `-`) : trimmed
|
|
353
463
|
const tokens = mode === `chemsys`
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
464
|
+
// Keep range constraints like Fe:1-2 intact while splitting token separators.
|
|
465
|
+
? normalized.split(/-(?!\d)/)
|
|
466
|
+
: normalized.split(`,`)
|
|
357
467
|
return tokens
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
468
|
+
.map((token) => token.trim())
|
|
469
|
+
.filter(Boolean)
|
|
470
|
+
.map(parse_token)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function sanitize_exact_formula(
|
|
474
|
+
input: string,
|
|
475
|
+
): { is_valid: boolean; error_message: string | null } {
|
|
476
|
+
const trimmed = input.trim()
|
|
477
|
+
if (!trimmed) return { is_valid: true, error_message: null }
|
|
366
478
|
try {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const message = error instanceof Error ? error.message : `Invalid exact formula`;
|
|
377
|
-
return { is_valid: false, error_message: message };
|
|
479
|
+
if (has_wildcards(trimmed)) {
|
|
480
|
+
parse_formula_with_wildcards(trimmed)
|
|
481
|
+
} else {
|
|
482
|
+
parse_formula(trimmed)
|
|
483
|
+
}
|
|
484
|
+
return { is_valid: true, error_message: null }
|
|
485
|
+
} catch (error) {
|
|
486
|
+
const message = error instanceof Error ? error.message : `Invalid exact formula`
|
|
487
|
+
return { is_valid: false, error_message: message }
|
|
378
488
|
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function normalize_tokenized_input(input: string, mode: FormulaSearchMode): string {
|
|
492
|
+
const separator = mode === `chemsys` ? `-` : `,`
|
|
493
|
+
const parsed_tokens = tokenize_query(input, mode)
|
|
494
|
+
if (parsed_tokens.length === 0) return ``
|
|
495
|
+
|
|
385
496
|
const normalized_tokens = parsed_tokens
|
|
386
|
-
|
|
387
|
-
|
|
497
|
+
.filter((token) => token.is_valid)
|
|
498
|
+
.map((token) => ({
|
|
388
499
|
...token,
|
|
389
500
|
element: token.is_wildcard
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
501
|
+
? `*`
|
|
502
|
+
: normalize_element_symbols(token.element).at(0) || token.element,
|
|
503
|
+
}))
|
|
504
|
+
.sort((token_a, token_b) => {
|
|
394
505
|
if (token_a.operator !== token_b.operator) {
|
|
395
|
-
|
|
506
|
+
return token_a.operator === `include` ? -1 : 1
|
|
396
507
|
}
|
|
397
508
|
if (token_a.is_wildcard !== token_b.is_wildcard) {
|
|
398
|
-
|
|
509
|
+
return token_a.is_wildcard ? 1 : -1
|
|
399
510
|
}
|
|
400
|
-
return token_a.element.localeCompare(token_b.element)
|
|
401
|
-
|
|
511
|
+
return token_a.element.localeCompare(token_b.element)
|
|
512
|
+
})
|
|
513
|
+
|
|
402
514
|
return normalized_tokens
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
515
|
+
.map(serialize_token)
|
|
516
|
+
.join(separator)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function parse_query(
|
|
520
|
+
normalized_value: string,
|
|
521
|
+
mode: FormulaSearchMode,
|
|
522
|
+
): FormulaFilterParseResult {
|
|
523
|
+
const tokens = tokenize_query(normalized_value, mode)
|
|
524
|
+
const first_invalid_token = tokens.find((token) => !token.is_valid)
|
|
409
525
|
const exact_validation = mode === `exact`
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
526
|
+
? sanitize_exact_formula(normalized_value)
|
|
527
|
+
: {
|
|
528
|
+
is_valid: !first_invalid_token,
|
|
529
|
+
error_message: first_invalid_token
|
|
530
|
+
? `Invalid token: ${first_invalid_token.raw}`
|
|
531
|
+
: null,
|
|
532
|
+
}
|
|
417
533
|
return {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
534
|
+
value: normalized_value,
|
|
535
|
+
normalized_value,
|
|
536
|
+
search_mode: mode,
|
|
537
|
+
tokens,
|
|
538
|
+
has_wildcards: tokens.some((token) => token.is_wildcard),
|
|
539
|
+
is_valid: exact_validation.is_valid,
|
|
540
|
+
error_message: exact_validation.error_message,
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function run_validation(next_value: string, next_mode: FormulaSearchMode): void {
|
|
545
|
+
const parsed = parse_query(next_value, next_mode)
|
|
546
|
+
onparse?.(parsed)
|
|
547
|
+
|
|
548
|
+
const default_validation: FormulaFilterValidation = parsed.is_valid
|
|
549
|
+
? { state: `valid`, message: null }
|
|
550
|
+
: { state: `invalid`, message: parsed.error_message ?? `Invalid filter query` }
|
|
551
|
+
const custom_validation = validate?.(next_value, next_mode, parsed)
|
|
552
|
+
validation = custom_validation ?? default_validation
|
|
553
|
+
on_validation?.(validation)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Extract elements from any input format (formula, comma-separated, dash-separated)
|
|
557
|
+
// Always returns elements in alphabetical order for consistency, preserving wildcards (*)
|
|
558
|
+
function extract_elements(input: string): string[] {
|
|
559
|
+
const trimmed = input.trim()
|
|
560
|
+
if (!trimmed) return []
|
|
443
561
|
// If contains commas or dashes, split by those and sort alphabetically
|
|
444
562
|
if (trimmed.includes(`,`) || trimmed.includes(`-`)) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
563
|
+
const parts = trimmed.split(/[-,]/).map((str) => str.trim()).filter(Boolean)
|
|
564
|
+
// Separate wildcards from regular elements
|
|
565
|
+
const wildcards = parts.filter((part) => part === `*`)
|
|
566
|
+
const regular_parts = parts.filter((part) => part !== `*`)
|
|
567
|
+
// Filter valid elements and sort alphabetically, then append wildcards
|
|
568
|
+
const valid_elements = normalize_element_symbols(regular_parts.join(`,`)).sort()
|
|
569
|
+
return [...valid_elements, ...wildcards]
|
|
452
570
|
}
|
|
453
571
|
// Otherwise parse as formula (already returns sorted by default)
|
|
454
572
|
// For formulas with wildcards, we can't parse them normally
|
|
455
573
|
if (has_wildcards(trimmed)) { // Use shared utility and extract unique elements
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
574
|
+
const tokens = parse_formula_with_wildcards(trimmed)
|
|
575
|
+
const unique_elements: string[] = []
|
|
576
|
+
for (const token of tokens) {
|
|
577
|
+
if (token.element !== null && !unique_elements.includes(token.element)) {
|
|
578
|
+
unique_elements.push(token.element)
|
|
462
579
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
580
|
+
}
|
|
581
|
+
const elements = unique_elements.sort()
|
|
582
|
+
const wildcards = tokens.filter((token) => token.element === null).map(() =>
|
|
583
|
+
`*`
|
|
584
|
+
)
|
|
585
|
+
return [...elements, ...wildcards]
|
|
466
586
|
}
|
|
467
587
|
try {
|
|
468
|
-
|
|
588
|
+
return extract_formula_elements(trimmed, { sorted: true })
|
|
589
|
+
} catch {
|
|
590
|
+
return []
|
|
469
591
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
return ``;
|
|
478
|
-
if (mode === `elements`)
|
|
479
|
-
return elements.join(`,`);
|
|
480
|
-
if (mode === `chemsys`)
|
|
481
|
-
return elements.join(`-`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Format elements for the given mode
|
|
595
|
+
function format_for_mode(elements: string[], mode: FormulaSearchMode): string {
|
|
596
|
+
if (elements.length === 0) return ``
|
|
597
|
+
if (mode === `elements`) return elements.join(`,`)
|
|
598
|
+
if (mode === `chemsys`) return elements.join(`-`)
|
|
482
599
|
// For exact mode, just join without separator (user will need to add counts)
|
|
483
|
-
return elements.join(``)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const current_idx = MODE_CYCLE.indexOf(search_mode)
|
|
489
|
-
const next_idx = (current_idx + 1) % MODE_CYCLE.length
|
|
490
|
-
const next_mode = MODE_CYCLE[next_idx]
|
|
600
|
+
return elements.join(``)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function cycle_mode(): void {
|
|
604
|
+
if (mode_locked) return
|
|
605
|
+
const current_idx = MODE_CYCLE.indexOf(search_mode)
|
|
606
|
+
const next_idx = (current_idx + 1) % MODE_CYCLE.length
|
|
607
|
+
const next_mode = MODE_CYCLE[next_idx]
|
|
608
|
+
|
|
491
609
|
// Extract elements from current value and reformat for new mode
|
|
492
|
-
const elements = extract_elements(value)
|
|
493
|
-
const reformatted = format_for_mode(elements, next_mode)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
610
|
+
const elements = extract_elements(value)
|
|
611
|
+
const reformatted = format_for_mode(elements, next_mode)
|
|
612
|
+
|
|
613
|
+
search_mode = next_mode
|
|
614
|
+
last_synced = value = input_value = reformatted // update last_synced to prevent effect re-inference
|
|
615
|
+
run_validation(reformatted, next_mode)
|
|
616
|
+
onchange?.(reformatted, next_mode)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function set_value(new_value: string, forced_mode?: FormulaSearchMode): void {
|
|
620
|
+
const mode = forced_mode ?? (mode_locked ? search_mode : infer_mode(new_value))
|
|
621
|
+
last_synced = value = input_value = new_value // update last_synced to prevent effect re-inference
|
|
622
|
+
search_mode = mode
|
|
623
|
+
if (new_value.trim()) add_to_history(new_value)
|
|
624
|
+
close_history()
|
|
625
|
+
run_validation(value, mode)
|
|
626
|
+
onchange?.(value, mode)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function sync_value(): void {
|
|
630
|
+
const trimmed = normalize_unicode_formula(input_value).trim()
|
|
631
|
+
if (!trimmed) return set_value(``)
|
|
632
|
+
|
|
633
|
+
const mode = mode_locked ? search_mode : infer_mode(trimmed)
|
|
514
634
|
if (mode === `exact`) {
|
|
515
|
-
|
|
516
|
-
|
|
635
|
+
const exact_value = normalize_exact ? normalize_exact_formula(trimmed) : trimmed
|
|
636
|
+
return set_value(exact_value, mode)
|
|
517
637
|
}
|
|
518
|
-
|
|
638
|
+
|
|
639
|
+
const parsed = parse_query(trimmed, mode)
|
|
519
640
|
if (!parsed.is_valid) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
641
|
+
// Preserve user input on invalid tokens instead of silently dropping them.
|
|
642
|
+
input_value = trimmed
|
|
643
|
+
run_validation(trimmed, mode)
|
|
644
|
+
return
|
|
524
645
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
646
|
+
|
|
647
|
+
const normalized = normalize_tokenized_input(trimmed, mode)
|
|
648
|
+
set_value(normalized, mode)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function onkeydown(event: KeyboardEvent): void {
|
|
529
652
|
if (event.key === `Enter`) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
}
|
|
653
|
+
event.preventDefault()
|
|
654
|
+
if (history_open && focused_history_idx >= 0) {
|
|
655
|
+
set_value(visible_history[focused_history_idx])
|
|
656
|
+
} else {
|
|
657
|
+
sync_value()
|
|
658
|
+
}
|
|
659
|
+
} else if (event.key === `Escape`) {
|
|
660
|
+
if (history_open) close_history()
|
|
661
|
+
else if (examples_open) examples_open = false
|
|
662
|
+
else if (input_value) clear_filter()
|
|
663
|
+
} else if (history_open && visible_history.length > 0) {
|
|
664
|
+
const len = visible_history.length
|
|
665
|
+
if (event.key === `ArrowDown`) {
|
|
666
|
+
event.preventDefault()
|
|
667
|
+
focused_history_idx = (focused_history_idx + 1) % len
|
|
668
|
+
} else if (event.key === `ArrowUp`) {
|
|
669
|
+
event.preventDefault()
|
|
670
|
+
focused_history_idx = focused_history_idx <= 0
|
|
671
|
+
? len - 1
|
|
672
|
+
: focused_history_idx - 1
|
|
673
|
+
}
|
|
558
674
|
}
|
|
559
|
-
}
|
|
560
|
-
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function oninput(): void {
|
|
561
678
|
if (history_open) {
|
|
562
|
-
|
|
563
|
-
|
|
679
|
+
history_query = input_value
|
|
680
|
+
focused_history_idx = visible_history.length > 0 ? 0 : -1
|
|
564
681
|
}
|
|
565
|
-
const mode = mode_locked ? search_mode : infer_mode(input_value)
|
|
566
|
-
run_validation(input_value, mode)
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
682
|
+
const mode = mode_locked ? search_mode : infer_mode(input_value)
|
|
683
|
+
run_validation(input_value, mode)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function clear_filter(): void {
|
|
687
|
+
onclear?.()
|
|
688
|
+
set_value(``)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function apply_example(example: string): void {
|
|
692
|
+
set_value(example, mode_locked ? search_mode : infer_mode(example))
|
|
693
|
+
close_examples()
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function toggle_examples(event: MouseEvent): void {
|
|
697
|
+
event.stopPropagation()
|
|
698
|
+
close_history()
|
|
699
|
+
examples_open = !examples_open
|
|
700
|
+
focused_item_idx = examples_open ? 0 : -1
|
|
701
|
+
if (examples_open) anchor_left = false
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function handle_menu_keydown(event: KeyboardEvent): void {
|
|
705
|
+
const len = all_examples.length
|
|
706
|
+
if (!len) return
|
|
588
707
|
const is_button_activation = (event.key === `Enter` || event.key === ` `) &&
|
|
589
|
-
|
|
590
|
-
if (is_button_activation)
|
|
591
|
-
|
|
592
|
-
const key_actions = {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
708
|
+
event.target instanceof HTMLButtonElement
|
|
709
|
+
if (is_button_activation) return
|
|
710
|
+
|
|
711
|
+
const key_actions: Record<string, () => void> = {
|
|
712
|
+
ArrowDown: () => (focused_item_idx = (focused_item_idx + 1) % len),
|
|
713
|
+
ArrowUp: () => (focused_item_idx = (focused_item_idx - 1 + len) % len),
|
|
714
|
+
Home: () => (focused_item_idx = 0),
|
|
715
|
+
End: () => (focused_item_idx = len - 1),
|
|
716
|
+
Escape: close_examples,
|
|
717
|
+
}
|
|
718
|
+
|
|
599
719
|
if (event.key in key_actions) {
|
|
600
|
-
|
|
601
|
-
|
|
720
|
+
event.preventDefault()
|
|
721
|
+
key_actions[event.key]()
|
|
602
722
|
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function toggle_mode_lock(): void {
|
|
726
|
+
mode_locked = !mode_locked
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function remove_token(token_idx: number): void {
|
|
730
|
+
if (search_mode === `exact`) return
|
|
731
|
+
const separator = search_mode === `chemsys` ? `-` : `,`
|
|
611
732
|
const tokens = tokenize_query(input_value, search_mode)
|
|
612
|
-
|
|
613
|
-
const next_value = tokens.map(serialize_token).join(separator)
|
|
614
|
-
input_value = next_value
|
|
615
|
-
set_value(next_value, search_mode)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const items = wrapper?.querySelectorAll(`[data-example-item]`)
|
|
622
|
-
items?.[focused_item_idx]?.focus({ preventScroll: true })
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
733
|
+
.filter((_, idx) => idx !== token_idx)
|
|
734
|
+
const next_value = tokens.map(serialize_token).join(separator)
|
|
735
|
+
input_value = next_value
|
|
736
|
+
set_value(next_value, search_mode)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Focus the active menu item when index changes
|
|
740
|
+
$effect(() => {
|
|
741
|
+
if (!examples_open || focused_item_idx < 0) return
|
|
742
|
+
const items = wrapper?.querySelectorAll<HTMLButtonElement>(`[data-example-item]`)
|
|
743
|
+
items?.[focused_item_idx]?.focus({ preventScroll: true })
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
let placeholder = $derived(
|
|
747
|
+
search_mode === `chemsys`
|
|
748
|
+
? `Li-Fe-O or Li-*-*`
|
|
749
|
+
: search_mode === `exact`
|
|
750
|
+
? `LiFePO4 or LiFe*2*`
|
|
751
|
+
: `Li,Fe,O or Li,*,*`,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
const MODE_LABELS: Record<FormulaSearchMode, string> = {
|
|
630
755
|
elements: `has elements`,
|
|
631
756
|
chemsys: `chemical system`,
|
|
632
757
|
exact: `exact formula`,
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
let
|
|
636
|
-
let
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let mode_hint = $derived(MODE_LABELS[search_mode])
|
|
761
|
+
let parsed_tokens = $derived(tokenize_query(input_value, search_mode))
|
|
762
|
+
let show_chip_row = $derived(
|
|
763
|
+
show_chip_editor && search_mode !== `exact` && parsed_tokens.length > 0,
|
|
764
|
+
)
|
|
765
|
+
// Preview of next mode cycle step for tooltip
|
|
766
|
+
let next_mode = $derived.by(() => {
|
|
767
|
+
const next = MODE_CYCLE[(MODE_CYCLE.indexOf(search_mode) + 1) % MODE_CYCLE.length]
|
|
768
|
+
const mode = MODE_LABELS[next]
|
|
769
|
+
const next_value = format_for_mode(extract_elements(value), next)
|
|
770
|
+
return { mode, value: next_value }
|
|
771
|
+
})
|
|
644
772
|
</script>
|
|
645
773
|
|
|
646
774
|
<svelte:document onclick={handle_document_click} />
|