matterviz 0.3.1 → 0.3.3
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 +154 -96
- package/dist/Icon.svelte +20 -14
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -178
- package/dist/brillouin/BrillouinZone.svelte +299 -198
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
- 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 +327 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- 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.d.ts +10 -0
- package/dist/chempot-diagram/color.js +32 -0
- package/dist/chempot-diagram/compute.d.ts +48 -0
- package/dist/chempot-diagram/compute.js +812 -0
- package/dist/chempot-diagram/index.d.ts +6 -0
- package/dist/chempot-diagram/index.js +6 -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 +36 -0
- package/dist/chempot-diagram/types.d.ts +86 -0
- package/dist/chempot-diagram/types.js +28 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +9 -3
- package/dist/composition/BarChart.svelte +141 -77
- package/dist/composition/BubbleChart.svelte +107 -52
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +973 -353
- package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
- package/dist/composition/PieChart.svelte +199 -99
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- 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 -38
- package/dist/convex-hull/ConvexHull2D.svelte +551 -393
- package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
- package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +821 -249
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +41 -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.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +40 -0
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +10 -1
- package/dist/convex-hull/helpers.js +79 -38
- 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 +163 -69
- package/dist/convex-hull/types.d.ts +12 -12
- package/dist/convex-hull/types.js +0 -12
- package/dist/coordination/CoordinationBarPlot.svelte +232 -176
- package/dist/element/BohrAtom.svelte +56 -13
- 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 +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/types.d.ts +1 -0
- 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 +336 -239
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
- 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 +37 -33
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -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 +158 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +138 -140
- 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/is-binary.js +2 -3
- package/dist/io/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +117 -0
- package/dist/isosurface/Isosurface.svelte +220 -110
- package/dist/isosurface/IsosurfaceControls.svelte +65 -28
- package/dist/isosurface/parse.js +104 -56
- package/dist/isosurface/slice.d.ts +2 -1
- package/dist/isosurface/slice.js +8 -13
- package/dist/isosurface/types.d.ts +14 -1
- package/dist/isosurface/types.js +152 -5
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +12 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +125 -94
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +82 -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 +266 -223
- package/dist/layout/json-tree/JsonTree.svelte +516 -429
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +281 -173
- 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 +37 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +20 -17
- package/dist/math.js +474 -57
- package/dist/overlays/ContextMenu.svelte +66 -40
- package/dist/overlays/DraggablePane.svelte +331 -154
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- 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 +559 -267
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- 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/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +869 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +8 -4
- package/dist/phase-diagram/utils.js +219 -74
- package/dist/plot/AxisLabel.svelte +51 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +1461 -768
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +33 -6
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +533 -383
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ColorScaleSelect.svelte +28 -7
- package/dist/plot/ElementScatter.svelte +38 -16
- package/dist/plot/FillArea.svelte +152 -92
- package/dist/plot/Histogram.svelte +1162 -709
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +81 -18
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- 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 +221 -96
- 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 -146
- package/dist/plot/ReferenceLine.svelte +77 -22
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -123
- package/dist/plot/ScatterPlot.svelte +1880 -1156
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +300 -297
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -406
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +150 -70
- 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 +96 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +23 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- 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/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 +11 -2
- package/dist/plot/layout.js +44 -17
- package/dist/plot/reference-line.d.ts +5 -22
- package/dist/plot/reference-line.js +12 -84
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +53 -40
- package/dist/plot/types.js +12 -7
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -63
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +173 -132
- 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 +21 -6
- package/dist/settings.js +63 -19
- package/dist/spectral/Bands.svelte +963 -412
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- 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.d.ts +23 -1
- package/dist/spectral/helpers.js +119 -51
- package/dist/spectral/types.d.ts +2 -0
- 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 +231 -129
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +148 -51
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1077 -821
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +373 -139
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -231
- package/dist/structure/StructureScene.svelte +919 -445
- package/dist/structure/StructureScene.svelte.d.ts +16 -7
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +42 -29
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +22 -34
- 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 +2 -3
- package/dist/structure/index.d.ts +16 -0
- package/dist/structure/index.js +88 -6
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +130 -155
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +99 -0
- 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 +5 -3
- package/dist/symmetry/SymmetryStats.svelte +94 -37
- package/dist/symmetry/WyckoffTable.svelte +42 -14
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +87 -21
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +1112 -516
- package/dist/table/HeatmapTable.svelte.d.ts +12 -1
- package/dist/table/ToggleMenu.svelte +125 -90
- package/dist/table/index.d.ts +2 -0
- 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 +889 -687
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +13 -31
- 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 +332 -0
- package/dist/trajectory/helpers.d.ts +14 -0
- package/dist/trajectory/helpers.js +172 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +23 -14
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +77 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +129 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +299 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +179 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +68 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +110 -0
- package/dist/trajectory/plotting.js +13 -8
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +17 -0
- package/dist/xrd/XrdPlot.svelte +337 -245
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -19
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +103 -101
- 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
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,337 +1,786 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
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[] = [
|
|
5
47
|
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,*,*`],
|
|
9
52
|
},
|
|
10
53
|
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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`],
|
|
14
58
|
},
|
|
15
59
|
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
60
|
+
label: `Exact formula`,
|
|
61
|
+
description:
|
|
62
|
+
`Materials with this exact stoichiometry. Unicode paste, wildcards, and canonicalization supported.`,
|
|
63
|
+
examples: [`LiFePO4`, `LiFe*2*`, `*2O3`],
|
|
19
64
|
},
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const SUBSCRIPT_TO_ASCII: Record<string, string> = {
|
|
68
|
+
[`\u2080`]: `0`,
|
|
69
|
+
[`\u2081`]: `1`,
|
|
70
|
+
[`\u2082`]: `2`,
|
|
71
|
+
[`\u2083`]: `3`,
|
|
72
|
+
[`\u2084`]: `4`,
|
|
73
|
+
[`\u2085`]: `5`,
|
|
74
|
+
[`\u2086`]: `6`,
|
|
75
|
+
[`\u2087`]: `7`,
|
|
76
|
+
[`\u2088`]: `8`,
|
|
77
|
+
[`\u2089`]: `9`,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SUPERSCRIPT_TO_ASCII: Record<string, string> = {
|
|
81
|
+
[`\u2070`]: `0`,
|
|
82
|
+
[`\u00B9`]: `1`,
|
|
83
|
+
[`\u00B2`]: `2`,
|
|
84
|
+
[`\u00B3`]: `3`,
|
|
85
|
+
[`\u2074`]: `4`,
|
|
86
|
+
[`\u2075`]: `5`,
|
|
87
|
+
[`\u2076`]: `6`,
|
|
88
|
+
[`\u2077`]: `7`,
|
|
89
|
+
[`\u2078`]: `8`,
|
|
90
|
+
[`\u2079`]: `9`,
|
|
91
|
+
[`\u207A`]: `+`,
|
|
92
|
+
[`\u207B`]: `-`,
|
|
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 []
|
|
39
160
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
catch {
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 []
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function save_history(entries: string[]): void {
|
|
175
|
+
if (max_history <= 0 || !has_storage) return
|
|
55
176
|
try {
|
|
56
|
-
|
|
177
|
+
localStorage.setItem(history_key, JSON.stringify(entries.slice(0, max_history)))
|
|
178
|
+
} catch {
|
|
179
|
+
// localStorage may be unavailable (e.g. private browsing)
|
|
57
180
|
}
|
|
58
|
-
|
|
59
|
-
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function load_pinned(): string[] {
|
|
184
|
+
if (max_history <= 0 || !has_storage) return []
|
|
185
|
+
try {
|
|
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 []
|
|
60
193
|
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function
|
|
64
|
-
if (max_history <= 0 || !
|
|
65
|
-
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function save_pinned(entries: string[]): void {
|
|
197
|
+
if (max_history <= 0 || !has_storage) return
|
|
198
|
+
try {
|
|
199
|
+
localStorage.setItem(history_pins_key, JSON.stringify(entries))
|
|
200
|
+
} catch {
|
|
201
|
+
// localStorage may be unavailable
|
|
202
|
+
}
|
|
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
|
|
66
210
|
// Remove duplicate if present, then prepend
|
|
67
|
-
const filtered = history.filter((item) => item !== entry)
|
|
68
|
-
history = [entry, ...filtered].slice(0, max_history)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
211
|
+
const filtered = history.filter((item) => item !== entry)
|
|
212
|
+
history = [entry, ...filtered].slice(0, max_history)
|
|
213
|
+
// Keep pin state for retained entries only
|
|
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)
|
|
74
224
|
// Clamp focused index to prevent out-of-bounds access on Enter
|
|
75
|
-
if (history.length === 0)
|
|
76
|
-
history_open = false;
|
|
225
|
+
if (history.length === 0) history_open = false
|
|
77
226
|
else if (focused_history_idx >= visible_history.length) {
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
227
|
+
focused_history_idx = visible_history.length - 1
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toggle_pin_history(entry: string): void {
|
|
232
|
+
pinned_history = pinned_history.includes(entry)
|
|
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(() => {
|
|
252
|
+
const filtered = history
|
|
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
|
|
99
279
|
if (!wrapper.contains(target)) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
$effect(() => {
|
|
116
|
-
input_value = value;
|
|
280
|
+
if (examples_open) close_examples()
|
|
281
|
+
if (history_open) close_history()
|
|
282
|
+
}
|
|
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(() => {
|
|
117
295
|
if (value !== last_synced) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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)
|
|
124
303
|
}
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Detect if dropdown would exit viewport on the right and adjust anchor
|
|
307
|
+
$effect(() => {
|
|
308
|
+
if (!examples_open || !examples_wrapper) return
|
|
130
309
|
requestAnimationFrame(() => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (trimmed
|
|
145
|
-
|
|
146
|
-
if (trimmed.includes(`-`)
|
|
147
|
-
|
|
148
|
-
return `
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
336
|
+
for (const [subscript, ascii] of Object.entries(SUBSCRIPT_TO_ASCII)) {
|
|
337
|
+
normalized = normalized.replaceAll(subscript, ascii)
|
|
338
|
+
}
|
|
339
|
+
for (const [superscript, ascii] of Object.entries(SUPERSCRIPT_TO_ASCII)) {
|
|
340
|
+
normalized = normalized.replaceAll(superscript, ascii)
|
|
341
|
+
}
|
|
342
|
+
return normalized
|
|
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
|
+
|
|
353
|
+
if (!has_wildcards(sanitized_input)) {
|
|
354
|
+
const canonical = get_alphabetical_formula(sanitized_input, true, ``)
|
|
355
|
+
return canonical || sanitized_input
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
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
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function is_valid_constraint(constraint: string): boolean {
|
|
390
|
+
if (!constraint) return true
|
|
391
|
+
return /^\d+$/.test(constraint) || /^\d+-\d+$/.test(constraint) ||
|
|
392
|
+
/^(>=|<=|>|<)\d+$/.test(constraint)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function strip_operator_prefix(
|
|
396
|
+
token: string,
|
|
397
|
+
): { operator: FormulaFilterToken[`operator`]; value: string } {
|
|
398
|
+
const operator = token.startsWith(`-`) || token.startsWith(`!`)
|
|
399
|
+
? `exclude`
|
|
400
|
+
: `include`
|
|
401
|
+
const value =
|
|
402
|
+
token.startsWith(`+`) || token.startsWith(`-`) || token.startsWith(`!`)
|
|
403
|
+
? token.slice(1)
|
|
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 === `*`
|
|
430
|
+
const is_valid_element = is_wildcard ||
|
|
431
|
+
ELEM_SYMBOLS.includes(element as (typeof ELEM_SYMBOLS)[number])
|
|
432
|
+
const normalized_constraint = constraint?.trim() || null
|
|
433
|
+
const is_valid = is_valid_element && (normalized_constraint === null ||
|
|
434
|
+
is_valid_constraint(normalized_constraint))
|
|
435
|
+
|
|
436
|
+
return {
|
|
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 []
|
|
452
|
+
if (mode === `exact`) {
|
|
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
|
+
}]
|
|
461
|
+
}
|
|
462
|
+
const normalized = mode === `chemsys` ? trimmed.replaceAll(`,`, `-`) : trimmed
|
|
463
|
+
const tokens = mode === `chemsys`
|
|
464
|
+
// Keep range constraints like Fe:1-2 intact while splitting token separators.
|
|
465
|
+
? normalized.split(/-(?!\d)/)
|
|
466
|
+
: normalized.split(`,`)
|
|
467
|
+
return tokens
|
|
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 }
|
|
478
|
+
try {
|
|
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 }
|
|
488
|
+
}
|
|
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
|
+
|
|
496
|
+
const normalized_tokens = parsed_tokens
|
|
497
|
+
.filter((token) => token.is_valid)
|
|
498
|
+
.map((token) => ({
|
|
499
|
+
...token,
|
|
500
|
+
element: token.is_wildcard
|
|
501
|
+
? `*`
|
|
502
|
+
: normalize_element_symbols(token.element).at(0) || token.element,
|
|
503
|
+
}))
|
|
504
|
+
.sort((token_a, token_b) => {
|
|
505
|
+
if (token_a.operator !== token_b.operator) {
|
|
506
|
+
return token_a.operator === `include` ? -1 : 1
|
|
507
|
+
}
|
|
508
|
+
if (token_a.is_wildcard !== token_b.is_wildcard) {
|
|
509
|
+
return token_a.is_wildcard ? 1 : -1
|
|
510
|
+
}
|
|
511
|
+
return token_a.element.localeCompare(token_b.element)
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
return normalized_tokens
|
|
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)
|
|
525
|
+
const exact_validation = mode === `exact`
|
|
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
|
+
}
|
|
533
|
+
return {
|
|
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 []
|
|
158
561
|
// If contains commas or dashes, split by those and sort alphabetically
|
|
159
562
|
if (trimmed.includes(`,`) || trimmed.includes(`-`)) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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]
|
|
167
570
|
}
|
|
168
571
|
// Otherwise parse as formula (already returns sorted by default)
|
|
169
572
|
// For formulas with wildcards, we can't parse them normally
|
|
170
573
|
if (has_wildcards(trimmed)) { // Use shared utility and extract unique elements
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const elements = unique_elements.sort()
|
|
582
|
+
const wildcards = tokens.filter((token) => token.element === null).map(() =>
|
|
583
|
+
`*`
|
|
584
|
+
)
|
|
585
|
+
return [...elements, ...wildcards]
|
|
177
586
|
}
|
|
178
587
|
try {
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Format elements for the given mode
|
|
186
|
-
function format_for_mode(elements, mode) {
|
|
187
|
-
if (elements.length === 0)
|
|
188
|
-
|
|
189
|
-
if (mode === `
|
|
190
|
-
return elements.join(`,`);
|
|
191
|
-
if (mode === `chemsys`)
|
|
192
|
-
return elements.join(`-`);
|
|
588
|
+
return extract_formula_elements(trimmed, { sorted: true })
|
|
589
|
+
} catch {
|
|
590
|
+
return []
|
|
591
|
+
}
|
|
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(`-`)
|
|
193
599
|
// For exact mode, just join without separator (user will need to add counts)
|
|
194
|
-
return elements.join(``)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
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
|
+
|
|
200
609
|
// Extract elements from current value and reformat for new mode
|
|
201
|
-
const elements = extract_elements(value)
|
|
202
|
-
const reformatted = format_for_mode(elements, next_mode)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
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)
|
|
634
|
+
if (mode === `exact`) {
|
|
635
|
+
const exact_value = normalize_exact ? normalize_exact_formula(trimmed) : trimmed
|
|
636
|
+
return set_value(exact_value, mode)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const parsed = parse_query(trimmed, mode)
|
|
640
|
+
if (!parsed.is_valid) {
|
|
641
|
+
// Preserve user input on invalid tokens instead of silently dropping them.
|
|
642
|
+
input_value = trimmed
|
|
643
|
+
run_validation(trimmed, mode)
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const normalized = normalize_tokenized_input(trimmed, mode)
|
|
648
|
+
set_value(normalized, mode)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function onkeydown(event: KeyboardEvent): void {
|
|
238
652
|
if (event.key === `Enter`) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
674
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
clear_filter();
|
|
254
|
-
}
|
|
255
|
-
else if (history_open && visible_history.length > 0) {
|
|
256
|
-
const len = visible_history.length;
|
|
257
|
-
if (event.key === `ArrowDown`) {
|
|
258
|
-
event.preventDefault();
|
|
259
|
-
focused_history_idx = (focused_history_idx + 1) % len;
|
|
260
|
-
}
|
|
261
|
-
else if (event.key === `ArrowUp`) {
|
|
262
|
-
event.preventDefault();
|
|
263
|
-
focused_history_idx = focused_history_idx <= 0
|
|
264
|
-
? len - 1
|
|
265
|
-
: focused_history_idx - 1;
|
|
266
|
-
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function oninput(): void {
|
|
678
|
+
if (history_open) {
|
|
679
|
+
history_query = input_value
|
|
680
|
+
focused_history_idx = visible_history.length > 0 ? 0 : -1
|
|
267
681
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
set_value(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
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
|
|
289
707
|
const is_button_activation = (event.key === `Enter` || event.key === ` `) &&
|
|
290
|
-
|
|
291
|
-
if (is_button_activation)
|
|
292
|
-
|
|
293
|
-
const key_actions = {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
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
|
+
|
|
300
719
|
if (event.key in key_actions) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
720
|
+
event.preventDefault()
|
|
721
|
+
key_actions[event.key]()
|
|
722
|
+
}
|
|
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` ? `-` : `,`
|
|
732
|
+
const tokens = tokenize_query(input_value, search_mode)
|
|
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> = {
|
|
318
755
|
elements: `has elements`,
|
|
319
756
|
chemsys: `chemical system`,
|
|
320
757
|
exact: `exact formula`,
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
let
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
})
|
|
330
772
|
</script>
|
|
331
773
|
|
|
332
774
|
<svelte:document onclick={handle_document_click} />
|
|
333
775
|
|
|
334
|
-
<div
|
|
776
|
+
<div
|
|
777
|
+
class="formula-filter"
|
|
778
|
+
bind:this={wrapper}
|
|
779
|
+
class:disabled
|
|
780
|
+
class:invalid={validation.state === `invalid`}
|
|
781
|
+
class:warning={validation.state === `warning`}
|
|
782
|
+
{...rest}
|
|
783
|
+
>
|
|
335
784
|
<input
|
|
336
785
|
bind:this={input_element}
|
|
337
786
|
bind:value={input_value}
|
|
@@ -342,6 +791,13 @@ let next_mode = $derived.by(() => {
|
|
|
342
791
|
sync_value()
|
|
343
792
|
}}
|
|
344
793
|
onfocus={open_history}
|
|
794
|
+
{oninput}
|
|
795
|
+
onpaste={() => {
|
|
796
|
+
requestAnimationFrame(() => {
|
|
797
|
+
input_value = normalize_unicode_formula(input_value)
|
|
798
|
+
oninput()
|
|
799
|
+
})
|
|
800
|
+
}}
|
|
345
801
|
{onkeydown}
|
|
346
802
|
{placeholder}
|
|
347
803
|
{disabled}
|
|
@@ -349,7 +805,21 @@ let next_mode = $derived.by(() => {
|
|
|
349
805
|
/>
|
|
350
806
|
{#if history_open && visible_history.length > 0}
|
|
351
807
|
<div class="history-dropdown" role="listbox" aria-label="Recent searches">
|
|
352
|
-
<
|
|
808
|
+
<div class="history-header-row">
|
|
809
|
+
<span class="history-header">Recent</span>
|
|
810
|
+
<button
|
|
811
|
+
type="button"
|
|
812
|
+
class="history-clear-all"
|
|
813
|
+
title="Clear history"
|
|
814
|
+
aria-label="Clear all history"
|
|
815
|
+
onmousedown={(event) => {
|
|
816
|
+
event.preventDefault()
|
|
817
|
+
clear_history()
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
Clear
|
|
821
|
+
</button>
|
|
822
|
+
</div>
|
|
353
823
|
{#each visible_history as entry, idx (entry)}
|
|
354
824
|
<div class="history-item" class:focused={idx === focused_history_idx}>
|
|
355
825
|
<button
|
|
@@ -364,6 +834,21 @@ let next_mode = $derived.by(() => {
|
|
|
364
834
|
>
|
|
365
835
|
{entry}
|
|
366
836
|
</button>
|
|
837
|
+
<button
|
|
838
|
+
type="button"
|
|
839
|
+
class="history-pin"
|
|
840
|
+
title={is_pinned(entry) ? `Unpin entry` : `Pin entry`}
|
|
841
|
+
aria-label={is_pinned(entry) ? `Unpin ${entry}` : `Pin ${entry}`}
|
|
842
|
+
onmousedown={(event) => {
|
|
843
|
+
event.preventDefault()
|
|
844
|
+
toggle_pin_history(entry)
|
|
845
|
+
}}
|
|
846
|
+
>
|
|
847
|
+
<Icon
|
|
848
|
+
icon={is_pinned(entry) ? `Star` : `Circle`}
|
|
849
|
+
style="width: 0.8em; height: 0.8em"
|
|
850
|
+
/>
|
|
851
|
+
</button>
|
|
367
852
|
<button
|
|
368
853
|
type="button"
|
|
369
854
|
class="history-remove"
|
|
@@ -384,20 +869,37 @@ let next_mode = $derived.by(() => {
|
|
|
384
869
|
<button
|
|
385
870
|
type="button"
|
|
386
871
|
class="mode-hint clickable"
|
|
872
|
+
class:locked={mode_locked}
|
|
387
873
|
onclick={cycle_mode}
|
|
388
|
-
title=
|
|
389
|
-
|
|
874
|
+
title={mode_locked
|
|
875
|
+
? `Mode is locked`
|
|
876
|
+
: `Click to switch to '${next_mode.mode}' → ${next_mode.value}`}
|
|
877
|
+
{@attach tooltip()}
|
|
390
878
|
aria-label="Change search mode"
|
|
391
879
|
>
|
|
392
880
|
{mode_hint}
|
|
393
881
|
</button>
|
|
394
882
|
{/if}
|
|
883
|
+
{#if show_mode_lock && !disabled}
|
|
884
|
+
<button
|
|
885
|
+
type="button"
|
|
886
|
+
class="icon-btn lock-btn"
|
|
887
|
+
class:active={mode_locked}
|
|
888
|
+
onclick={toggle_mode_lock}
|
|
889
|
+
title={mode_locked ? `Unlock mode inference` : `Lock current mode`}
|
|
890
|
+
{@attach tooltip()}
|
|
891
|
+
aria-label={mode_locked ? `Unlock mode` : `Lock mode`}
|
|
892
|
+
>
|
|
893
|
+
<Icon icon={mode_locked ? `Lock` : `Unlock`} style="width: 1em; height: 1em" />
|
|
894
|
+
</button>
|
|
895
|
+
{/if}
|
|
395
896
|
{#if show_clear_button && value && !disabled}
|
|
396
897
|
<button
|
|
397
898
|
type="button"
|
|
398
899
|
class="icon-btn clear-btn"
|
|
399
900
|
onclick={clear_filter}
|
|
400
901
|
title="Clear (Escape)"
|
|
902
|
+
{@attach tooltip()}
|
|
401
903
|
aria-label="Clear filter"
|
|
402
904
|
>
|
|
403
905
|
<Icon icon="Close" style="width: 1em; height: 1em" />
|
|
@@ -425,7 +927,7 @@ let next_mode = $derived.by(() => {
|
|
|
425
927
|
tabindex="-1"
|
|
426
928
|
onkeydown={handle_menu_keydown}
|
|
427
929
|
>
|
|
428
|
-
{#each
|
|
930
|
+
{#each examples as category (category.label)}
|
|
429
931
|
<div class="example-category">
|
|
430
932
|
<div class="category-label">{category.label}:</div>
|
|
431
933
|
<div class="example-tags">
|
|
@@ -450,24 +952,58 @@ let next_mode = $derived.by(() => {
|
|
|
450
952
|
</div>
|
|
451
953
|
{/if}
|
|
452
954
|
</div>
|
|
955
|
+
{#if show_chip_row}
|
|
956
|
+
<div class="token-chip-row">
|
|
957
|
+
{#each parsed_tokens as
|
|
958
|
+
token,
|
|
959
|
+
idx
|
|
960
|
+
(`${token.operator}:${token.element}:${token.constraint ?? ``}:${idx}`)
|
|
961
|
+
}
|
|
962
|
+
<button
|
|
963
|
+
type="button"
|
|
964
|
+
class="token-chip"
|
|
965
|
+
class:exclude={token.operator === `exclude`}
|
|
966
|
+
class:invalid={!token.is_valid}
|
|
967
|
+
onclick={() => remove_token(idx)}
|
|
968
|
+
title="Click to remove token"
|
|
969
|
+
aria-label="Remove token {token.raw}"
|
|
970
|
+
>
|
|
971
|
+
{token_chip_label(token)}
|
|
972
|
+
</button>
|
|
973
|
+
{/each}
|
|
974
|
+
</div>
|
|
975
|
+
{/if}
|
|
976
|
+
{#if validation.message}
|
|
977
|
+
<div class="validation-message" class:invalid={validation.state === `invalid`}>
|
|
978
|
+
{validation.message}
|
|
979
|
+
</div>
|
|
980
|
+
{/if}
|
|
453
981
|
|
|
454
982
|
<style>
|
|
455
983
|
.formula-filter {
|
|
456
984
|
position: relative;
|
|
457
985
|
display: flex;
|
|
458
986
|
align-items: center;
|
|
459
|
-
gap:
|
|
460
|
-
padding: 4pt 8pt;
|
|
461
|
-
border-radius:
|
|
462
|
-
background: var(--filter-bg, rgba(128, 128, 128, 0.05));
|
|
987
|
+
gap: var(--formula-filter-gap, 1pt);
|
|
988
|
+
padding: var(--formula-filter-padding, 4pt 8pt);
|
|
989
|
+
border-radius: var(--formula-filter-border-radius, var(--border-radius, 3pt));
|
|
990
|
+
background: var(--formula-filter-bg, rgba(128, 128, 128, 0.05));
|
|
463
991
|
transition: background 0.15s;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
992
|
+
&.invalid {
|
|
993
|
+
outline: 1px solid rgba(239, 68, 68, 0.65);
|
|
994
|
+
background: rgba(239, 68, 68, 0.08);
|
|
995
|
+
}
|
|
996
|
+
&.warning {
|
|
997
|
+
outline: 1px solid rgba(245, 158, 11, 0.6);
|
|
998
|
+
background: rgba(245, 158, 11, 0.08);
|
|
999
|
+
}
|
|
1000
|
+
&:focus-within {
|
|
1001
|
+
background: rgba(77, 182, 255, 0.08);
|
|
1002
|
+
}
|
|
1003
|
+
&.disabled {
|
|
1004
|
+
opacity: 0.5;
|
|
1005
|
+
pointer-events: none;
|
|
1006
|
+
}
|
|
471
1007
|
}
|
|
472
1008
|
input {
|
|
473
1009
|
flex: 1;
|
|
@@ -478,31 +1014,35 @@ let next_mode = $derived.by(() => {
|
|
|
478
1014
|
padding: 2pt 0;
|
|
479
1015
|
outline: none;
|
|
480
1016
|
font-family: var(--mono-font, monospace);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1017
|
+
&::placeholder {
|
|
1018
|
+
opacity: 0.4;
|
|
1019
|
+
}
|
|
484
1020
|
}
|
|
485
1021
|
.mode-hint {
|
|
486
1022
|
opacity: 0.5;
|
|
487
1023
|
white-space: nowrap;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
1024
|
+
&.clickable {
|
|
1025
|
+
display: inline-flex;
|
|
1026
|
+
align-items: center;
|
|
1027
|
+
gap: 2pt;
|
|
1028
|
+
background: rgba(77, 182, 255, 0.1);
|
|
1029
|
+
border: 1px solid rgba(77, 182, 255, 0.25);
|
|
1030
|
+
border-radius: 4px;
|
|
1031
|
+
padding: 1pt 5pt;
|
|
1032
|
+
cursor: pointer;
|
|
1033
|
+
color: var(--highlight, #4db6ff);
|
|
1034
|
+
opacity: 0.8;
|
|
1035
|
+
transition: opacity 0.15s, background 0.15s;
|
|
1036
|
+
&:hover {
|
|
1037
|
+
opacity: 1;
|
|
1038
|
+
background: rgba(77, 182, 255, 0.2);
|
|
1039
|
+
border-color: rgba(77, 182, 255, 0.4);
|
|
1040
|
+
}
|
|
1041
|
+
&.locked {
|
|
1042
|
+
cursor: not-allowed;
|
|
1043
|
+
opacity: 0.5;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
506
1046
|
}
|
|
507
1047
|
.icon-btn {
|
|
508
1048
|
display: flex;
|
|
@@ -515,14 +1055,14 @@ let next_mode = $derived.by(() => {
|
|
|
515
1055
|
border-radius: 50%;
|
|
516
1056
|
color: inherit;
|
|
517
1057
|
opacity: 0.4;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1058
|
+
&:hover {
|
|
1059
|
+
opacity: 1;
|
|
1060
|
+
background: rgba(128, 128, 128, 0.15);
|
|
1061
|
+
}
|
|
1062
|
+
&.active {
|
|
1063
|
+
opacity: 1;
|
|
1064
|
+
color: var(--highlight, #4db6ff);
|
|
1065
|
+
}
|
|
526
1066
|
}
|
|
527
1067
|
.history-dropdown {
|
|
528
1068
|
position: absolute;
|
|
@@ -546,14 +1086,30 @@ let next_mode = $derived.by(() => {
|
|
|
546
1086
|
text-transform: uppercase;
|
|
547
1087
|
letter-spacing: 0.5px;
|
|
548
1088
|
}
|
|
1089
|
+
.history-header-row {
|
|
1090
|
+
display: flex;
|
|
1091
|
+
align-items: center;
|
|
1092
|
+
justify-content: space-between;
|
|
1093
|
+
gap: 6pt;
|
|
1094
|
+
padding-right: 6pt;
|
|
1095
|
+
}
|
|
1096
|
+
.history-clear-all {
|
|
1097
|
+
border: none;
|
|
1098
|
+
background: transparent;
|
|
1099
|
+
cursor: pointer;
|
|
1100
|
+
font-size: 0.75em;
|
|
1101
|
+
opacity: 0.6;
|
|
1102
|
+
&:hover {
|
|
1103
|
+
opacity: 1;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
549
1106
|
.history-item {
|
|
550
1107
|
display: flex;
|
|
551
1108
|
align-items: center;
|
|
552
1109
|
padding: 0 4pt 0 0;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
background: rgba(77, 182, 255, 0.08);
|
|
1110
|
+
&:is(.focused, :hover) {
|
|
1111
|
+
background: rgba(77, 182, 255, 0.08);
|
|
1112
|
+
}
|
|
557
1113
|
}
|
|
558
1114
|
.history-value {
|
|
559
1115
|
flex: 1;
|
|
@@ -567,6 +1123,8 @@ let next_mode = $derived.by(() => {
|
|
|
567
1123
|
color: inherit;
|
|
568
1124
|
}
|
|
569
1125
|
.history-remove {
|
|
1126
|
+
min-width: 24px;
|
|
1127
|
+
min-height: 24px;
|
|
570
1128
|
display: flex;
|
|
571
1129
|
align-items: center;
|
|
572
1130
|
justify-content: center;
|
|
@@ -577,10 +1135,26 @@ let next_mode = $derived.by(() => {
|
|
|
577
1135
|
border-radius: 50%;
|
|
578
1136
|
opacity: 0.3;
|
|
579
1137
|
color: inherit;
|
|
1138
|
+
&:hover {
|
|
1139
|
+
opacity: 0.8;
|
|
1140
|
+
background: rgba(128, 128, 128, 0.15);
|
|
1141
|
+
}
|
|
580
1142
|
}
|
|
581
|
-
.history-
|
|
582
|
-
|
|
583
|
-
|
|
1143
|
+
.history-pin {
|
|
1144
|
+
display: flex;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
justify-content: center;
|
|
1147
|
+
background: none;
|
|
1148
|
+
border: none;
|
|
1149
|
+
cursor: pointer;
|
|
1150
|
+
padding: 3pt;
|
|
1151
|
+
border-radius: 50%;
|
|
1152
|
+
opacity: 0.3;
|
|
1153
|
+
color: inherit;
|
|
1154
|
+
&:hover {
|
|
1155
|
+
opacity: 0.8;
|
|
1156
|
+
background: rgba(128, 128, 128, 0.15);
|
|
1157
|
+
}
|
|
584
1158
|
}
|
|
585
1159
|
.examples-wrapper {
|
|
586
1160
|
position: relative;
|
|
@@ -600,10 +1174,10 @@ let next_mode = $derived.by(() => {
|
|
|
600
1174
|
display: flex;
|
|
601
1175
|
flex-direction: column;
|
|
602
1176
|
gap: 6pt;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1177
|
+
&.anchor-left {
|
|
1178
|
+
right: auto;
|
|
1179
|
+
left: 0;
|
|
1180
|
+
}
|
|
607
1181
|
}
|
|
608
1182
|
.example-category {
|
|
609
1183
|
display: flex;
|
|
@@ -631,9 +1205,55 @@ let next_mode = $derived.by(() => {
|
|
|
631
1205
|
font-family: var(--mono-font, monospace);
|
|
632
1206
|
color: var(--highlight, #4db6ff);
|
|
633
1207
|
cursor: pointer;
|
|
1208
|
+
&:hover {
|
|
1209
|
+
background: rgba(77, 182, 255, 0.2);
|
|
1210
|
+
border-color: rgba(77, 182, 255, 0.5);
|
|
1211
|
+
}
|
|
634
1212
|
}
|
|
635
|
-
.
|
|
636
|
-
|
|
637
|
-
|
|
1213
|
+
.token-chip-row {
|
|
1214
|
+
margin-top: 4pt;
|
|
1215
|
+
display: flex;
|
|
1216
|
+
flex-wrap: wrap;
|
|
1217
|
+
gap: 4pt;
|
|
1218
|
+
}
|
|
1219
|
+
.token-chip {
|
|
1220
|
+
border: 1px solid rgba(77, 182, 255, 0.35);
|
|
1221
|
+
background: rgba(77, 182, 255, 0.12);
|
|
1222
|
+
border-radius: 4px;
|
|
1223
|
+
font-family: var(--mono-font, monospace);
|
|
1224
|
+
font-size: 0.78em;
|
|
1225
|
+
padding: 2pt 6pt;
|
|
1226
|
+
cursor: pointer;
|
|
1227
|
+
color: inherit;
|
|
1228
|
+
&.exclude {
|
|
1229
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
1230
|
+
background: rgba(239, 68, 68, 0.12);
|
|
1231
|
+
}
|
|
1232
|
+
&.invalid {
|
|
1233
|
+
border-color: rgba(239, 68, 68, 0.65);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
.validation-message {
|
|
1237
|
+
margin-top: 4pt;
|
|
1238
|
+
font-size: 0.74em;
|
|
1239
|
+
opacity: 0.75;
|
|
1240
|
+
&.invalid {
|
|
1241
|
+
color: rgb(239, 68, 68);
|
|
1242
|
+
opacity: 0.95;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
@media (max-width: 700px) {
|
|
1246
|
+
.icon-btn {
|
|
1247
|
+
min-width: 28px;
|
|
1248
|
+
min-height: 28px;
|
|
1249
|
+
padding: 5pt;
|
|
1250
|
+
}
|
|
1251
|
+
:is(.history-remove, .history-pin) {
|
|
1252
|
+
min-width: 28px;
|
|
1253
|
+
min-height: 28px;
|
|
1254
|
+
}
|
|
1255
|
+
.history-value {
|
|
1256
|
+
padding: 6pt 10pt;
|
|
1257
|
+
}
|
|
638
1258
|
}
|
|
639
1259
|
</style>
|