matterviz 0.3.1 → 0.3.2

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