matterviz 0.1.15 → 0.2.0

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 (122) hide show
  1. package/dist/FilePicker.svelte +9 -9
  2. package/dist/brillouin/BrillouinZone.svelte +4 -4
  3. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  4. package/dist/brillouin/BrillouinZoneControls.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  6. package/dist/composition/Composition.svelte +1 -1
  7. package/dist/composition/Composition.svelte.d.ts +2 -0
  8. package/dist/composition/Formula.svelte +9 -2
  9. package/dist/composition/FormulaFilter.svelte +429 -0
  10. package/dist/composition/FormulaFilter.svelte.d.ts +15 -0
  11. package/dist/composition/index.d.ts +3 -1
  12. package/dist/composition/index.js +2 -1
  13. package/dist/constants.js +5 -1
  14. package/dist/{phase-diagram/PhaseDiagram.svelte → convex-hull/ConvexHull.svelte} +17 -16
  15. package/dist/convex-hull/ConvexHull.svelte.d.ts +10 -0
  16. package/dist/{phase-diagram/PhaseDiagram2D.svelte → convex-hull/ConvexHull2D.svelte} +124 -101
  17. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +11 -0
  18. package/dist/{phase-diagram/PhaseDiagram3D.svelte → convex-hull/ConvexHull3D.svelte} +99 -83
  19. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +8 -0
  20. package/dist/{phase-diagram/PhaseDiagram4D.svelte → convex-hull/ConvexHull4D.svelte} +128 -129
  21. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +8 -0
  22. package/dist/{phase-diagram/PhaseDiagramControls.svelte → convex-hull/ConvexHullControls.svelte} +14 -6
  23. package/dist/{phase-diagram/PhaseDiagramControls.svelte.d.ts → convex-hull/ConvexHullControls.svelte.d.ts} +7 -7
  24. package/dist/{phase-diagram/PhaseDiagramInfoPane.svelte → convex-hull/ConvexHullInfoPane.svelte} +10 -10
  25. package/dist/{phase-diagram/PhaseDiagramInfoPane.svelte.d.ts → convex-hull/ConvexHullInfoPane.svelte.d.ts} +6 -6
  26. package/dist/{phase-diagram/PhaseDiagramStats.svelte → convex-hull/ConvexHullStats.svelte} +40 -64
  27. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +10 -0
  28. package/dist/convex-hull/PhaseEntryTooltip.svelte +76 -0
  29. package/dist/{phase-diagram → convex-hull}/PhaseEntryTooltip.svelte.d.ts +2 -1
  30. package/dist/{phase-diagram → convex-hull}/StructurePopup.svelte +7 -5
  31. package/dist/{phase-diagram → convex-hull}/barycentric-coords.d.ts +3 -3
  32. package/dist/{phase-diagram → convex-hull}/barycentric-coords.js +3 -3
  33. package/dist/{phase-diagram → convex-hull}/helpers.d.ts +5 -6
  34. package/dist/{phase-diagram → convex-hull}/helpers.js +17 -27
  35. package/dist/{phase-diagram → convex-hull}/index.d.ts +20 -19
  36. package/dist/{phase-diagram → convex-hull}/index.js +11 -11
  37. package/dist/{phase-diagram → convex-hull}/thermodynamics.d.ts +4 -5
  38. package/dist/{phase-diagram → convex-hull}/thermodynamics.js +5 -5
  39. package/dist/{phase-diagram → convex-hull}/types.d.ts +5 -7
  40. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  41. package/dist/element/ElementTile.svelte.d.ts +1 -1
  42. package/dist/feedback/index.d.ts +0 -1
  43. package/dist/feedback/index.js +0 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +2 -2
  46. package/dist/layout/InfoCard.svelte +13 -3
  47. package/dist/layout/InfoTag.svelte +152 -0
  48. package/dist/layout/InfoTag.svelte.d.ts +19 -0
  49. package/dist/layout/PropertyFilter.svelte +199 -0
  50. package/dist/layout/PropertyFilter.svelte.d.ts +24 -0
  51. package/dist/layout/fullscreen.d.ts +2 -0
  52. package/dist/layout/fullscreen.js +33 -0
  53. package/dist/layout/index.d.ts +6 -0
  54. package/dist/layout/index.js +4 -0
  55. package/dist/overlays/DraggablePane.svelte +22 -6
  56. package/dist/periodic-table/PeriodicTable.svelte +12 -3
  57. package/dist/plot/BarPlot.svelte +54 -31
  58. package/dist/plot/BarPlot.svelte.d.ts +1 -6
  59. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  60. package/dist/plot/Histogram.svelte +104 -74
  61. package/dist/plot/Histogram.svelte.d.ts +1 -6
  62. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  63. package/dist/plot/ScatterPlot.svelte +75 -59
  64. package/dist/plot/ScatterPlot.svelte.d.ts +3 -13
  65. package/dist/plot/ScatterPlotControls.svelte +30 -18
  66. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -0
  67. package/dist/plot/SpacegroupBarPlot.svelte +7 -2
  68. package/dist/plot/index.d.ts +1 -0
  69. package/dist/plot/index.js +1 -0
  70. package/dist/plot/svg.d.ts +1 -0
  71. package/dist/plot/svg.js +11 -0
  72. package/dist/plot/types.d.ts +23 -10
  73. package/dist/rdf/RdfPlot.svelte +1 -1
  74. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  75. package/dist/settings.d.ts +8 -10
  76. package/dist/settings.js +55 -57
  77. package/dist/{bands → spectral}/Bands.svelte +134 -18
  78. package/dist/{bands → spectral}/Bands.svelte.d.ts +2 -1
  79. package/dist/{bands → spectral}/helpers.d.ts +22 -1
  80. package/dist/{bands → spectral}/helpers.js +169 -16
  81. package/dist/{bands → spectral}/index.js +1 -1
  82. package/dist/{bands → spectral}/types.d.ts +10 -0
  83. package/dist/structure/AtomLegend.svelte +150 -59
  84. package/dist/structure/AtomLegend.svelte.d.ts +2 -1
  85. package/dist/structure/Structure.svelte +111 -32
  86. package/dist/structure/Structure.svelte.d.ts +2 -1
  87. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  88. package/dist/structure/StructureInfoPane.svelte +3 -3
  89. package/dist/structure/StructureScene.svelte +75 -6
  90. package/dist/structure/StructureScene.svelte.d.ts +5 -3
  91. package/dist/structure/atom-properties.d.ts +1 -1
  92. package/dist/structure/atom-properties.js +1 -1
  93. package/dist/structure/bonding.d.ts +2 -1
  94. package/dist/structure/bonding.js +1 -1
  95. package/dist/structure/parse.js +2 -2
  96. package/dist/symmetry/SymmetryStats.svelte +3 -3
  97. package/dist/trajectory/Trajectory.svelte +19 -6
  98. package/dist/trajectory/Trajectory.svelte.d.ts +2 -1
  99. package/dist/trajectory/TrajectoryInfoPane.svelte +1 -1
  100. package/dist/trajectory/index.js +1 -1
  101. package/dist/trajectory/parse.d.ts +4 -1
  102. package/dist/trajectory/parse.js +161 -4
  103. package/dist/trajectory/plotting.js +2 -1
  104. package/package.json +12 -8
  105. package/dist/phase-diagram/PhaseDiagram.svelte.d.ts +0 -10
  106. package/dist/phase-diagram/PhaseDiagram2D.svelte.d.ts +0 -11
  107. package/dist/phase-diagram/PhaseDiagram3D.svelte.d.ts +0 -8
  108. package/dist/phase-diagram/PhaseDiagram4D.svelte.d.ts +0 -8
  109. package/dist/phase-diagram/PhaseDiagramStats.svelte.d.ts +0 -10
  110. package/dist/phase-diagram/PhaseEntryTooltip.svelte +0 -68
  111. /package/dist/{phase-diagram → convex-hull}/StructurePopup.svelte.d.ts +0 -0
  112. /package/dist/{phase-diagram → convex-hull}/types.js +0 -0
  113. /package/dist/{feedback → layout}/FullscreenToggle.svelte +0 -0
  114. /package/dist/{feedback → layout}/FullscreenToggle.svelte.d.ts +0 -0
  115. /package/dist/{bands → spectral}/BandsAndDos.svelte +0 -0
  116. /package/dist/{bands → spectral}/BandsAndDos.svelte.d.ts +0 -0
  117. /package/dist/{bands → spectral}/BrillouinBandsDos.svelte +0 -0
  118. /package/dist/{bands → spectral}/BrillouinBandsDos.svelte.d.ts +0 -0
  119. /package/dist/{bands → spectral}/Dos.svelte +0 -0
  120. /package/dist/{bands → spectral}/Dos.svelte.d.ts +0 -0
  121. /package/dist/{bands → spectral}/index.d.ts +0 -0
  122. /package/dist/{bands → spectral}/types.js +0 -0
@@ -172,13 +172,13 @@ let uniq_categories = $derived([...new Set(files.map(get_category_id))].filter(B
172
172
  }
173
173
  .legend-item:hover {
174
174
  opacity: 1;
175
- background: rgba(255, 255, 255, 0.1);
176
- border-color: rgba(255, 255, 255, 0.3);
175
+ background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.1));
176
+ border-color: light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.3));
177
177
  }
178
178
  .legend-item.active {
179
179
  opacity: 1;
180
- background: rgba(255, 255, 255, 0.2);
181
- border-color: rgba(255, 255, 255, 0.5);
180
+ background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.2));
181
+ border-color: light-dark(rgba(0, 0, 0, 0.25), rgba(255, 255, 255, 0.5));
182
182
  font-weight: bold;
183
183
  }
184
184
  .clear-filter {
@@ -205,24 +205,24 @@ let uniq_categories = $derived([...new Set(files.map(get_category_id))].filter(B
205
205
  display: flex;
206
206
  align-items: center;
207
207
  padding: 4pt 8pt;
208
- border: 1px solid rgba(255, 255, 255, 0.2);
208
+ border: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.2));
209
209
  border-radius: 20px;
210
210
  cursor: grab;
211
- background: rgba(255, 255, 255, 0.1);
211
+ background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.1));
212
212
  transition: all 0.2s ease;
213
213
  gap: 0.5em;
214
214
  }
215
215
  .file-item.active {
216
216
  border-color: var(--success-color, #00ff00);
217
- background: rgba(0, 255, 0, 0.15);
218
- box-shadow: 0 0 8px rgba(0, 255, 0, 0.3);
217
+ background: light-dark(rgba(0, 255, 0, 0.12), rgba(0, 255, 0, 0.2));
218
+ box-shadow: 0 0 8px light-dark(rgba(0, 255, 0, 0.25), rgba(0, 255, 0, 0.35));
219
219
  }
220
220
  .file-item:active {
221
221
  cursor: grabbing;
222
222
  }
223
223
  .file-item:hover {
224
224
  border-color: var(--accent-color, #007acc);
225
- background: rgba(0, 122, 204, 0.2);
225
+ background: light-dark(rgba(0, 122, 204, 0.15), rgba(0, 122, 204, 0.25));
226
226
  filter: brightness(1.1);
227
227
  }
228
228
  .file-name {
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">import { Icon, Spinner, toggle_fullscreen } from '..';
2
2
  import { decompress_file, handle_url_drop, load_from_url } from '../io';
3
- import { set_fullscreen_bg } from '../phase-diagram/helpers';
3
+ import { set_fullscreen_bg } from '../layout';
4
4
  import { DEFAULTS } from '../settings';
5
5
  import { parse_any_structure } from '../structure/parse';
6
6
  import { Canvas } from '@threlte/core';
@@ -197,7 +197,6 @@ $effect(() => {
197
197
  title="{fullscreen ? `Exit` : `Enter`} fullscreen"
198
198
  aria-pressed={fullscreen}
199
199
  class="fullscreen-toggle"
200
- style="padding: 0"
201
200
  {@attach tooltip()}
202
201
  >
203
202
  {#if typeof fullscreen_toggle === `function`}
@@ -316,11 +315,12 @@ $effect(() => {
316
315
  section.control-buttons > :global(button) {
317
316
  background-color: transparent;
318
317
  display: flex;
319
- padding: 0;
318
+ padding: 4px;
319
+ border-radius: var(--border-radius, 3pt);
320
320
  font-size: clamp(0.85em, 2cqmin, 2.5em);
321
321
  }
322
322
  section.control-buttons :global(button:hover) {
323
- background-color: var(--pane-btn-bg-hover);
323
+ background-color: color-mix(in srgb, currentColor 8%, transparent);
324
324
  }
325
325
  .filename {
326
326
  font-family: monospace;
@@ -62,6 +62,6 @@ type $$ComponentProps = {
62
62
  on_error?: (data: BZHandlerData) => void;
63
63
  on_fullscreen_change?: (data: BZHandlerData) => void;
64
64
  } & HTMLAttributes<HTMLDivElement>;
65
- declare const BrillouinZone: import("svelte").Component<$$ComponentProps, {}, "height" | "width" | "loading" | "dragover" | "structure" | "fullscreen" | "controls_open" | "camera_projection" | "info_pane_open" | "hovered" | "wrapper" | "png_dpi" | "error_msg" | "bz_order" | "surface_color" | "surface_opacity" | "edge_color" | "edge_width" | "show_vectors" | "bz_data" | "vector_scale">;
65
+ declare const BrillouinZone: import("svelte").Component<$$ComponentProps, {}, "dragover" | "height" | "width" | "fullscreen" | "structure" | "camera_projection" | "info_pane_open" | "controls_open" | "wrapper" | "hovered" | "png_dpi" | "loading" | "error_msg" | "bz_order" | "surface_color" | "surface_opacity" | "edge_color" | "edge_width" | "show_vectors" | "bz_data" | "vector_scale">;
66
66
  type BrillouinZone = ReturnType<typeof BrillouinZone>;
67
67
  export default BrillouinZone;
@@ -9,6 +9,6 @@ type $$ComponentProps = {
9
9
  show_vectors?: boolean;
10
10
  camera_projection?: CameraProjection;
11
11
  };
12
- declare const BrillouinZoneControls: import("svelte").Component<$$ComponentProps, {}, "controls_open" | "camera_projection" | "bz_order" | "surface_color" | "surface_opacity" | "edge_color" | "edge_width" | "show_vectors">;
12
+ declare const BrillouinZoneControls: import("svelte").Component<$$ComponentProps, {}, "camera_projection" | "controls_open" | "bz_order" | "surface_color" | "surface_opacity" | "edge_color" | "edge_width" | "show_vectors">;
13
13
  type BrillouinZoneControls = ReturnType<typeof BrillouinZoneControls>;
14
14
  export default BrillouinZoneControls;
@@ -38,6 +38,6 @@ type $$ComponentProps = {
38
38
  hovered_k_point?: Vec3 | null;
39
39
  hovered_qpoint_index?: number | null;
40
40
  };
41
- declare const BrillouinZoneScene: import("svelte").Component<$$ComponentProps, {}, "camera" | "camera_position" | "camera_projection" | "scene" | "camera_is_moving" | "surface_color" | "surface_opacity" | "edge_color" | "edge_width" | "show_vectors" | "bz_data" | "vector_scale">;
41
+ declare const BrillouinZoneScene: import("svelte").Component<$$ComponentProps, {}, "camera_position" | "camera_projection" | "camera_is_moving" | "scene" | "camera" | "surface_color" | "surface_opacity" | "edge_color" | "edge_width" | "show_vectors" | "bz_data" | "vector_scale">;
42
42
  type BrillouinZoneScene = ReturnType<typeof BrillouinZoneScene>;
43
43
  export default BrillouinZoneScene;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">import { ContextMenu } from '..';
2
2
  import { export_svg_as_png, export_svg_as_svg } from '../io/export';
3
- import { BarChart, BubbleChart, PieChart } from './index';
4
3
  import { get_electro_neg_formula } from './format';
4
+ import { BarChart, BubbleChart, PieChart } from './index';
5
5
  import { parse_composition } from './parse';
6
6
  let { composition, mode = `pie`, on_composition_change, color_scheme = `Vesta`, ...rest } = $props();
7
7
  // Make these reactive so context menu changes propagate
@@ -6,6 +6,8 @@ type $$ComponentProps = SVGAttributes<SVGSVGElement> & {
6
6
  mode?: CompositionChartMode;
7
7
  on_composition_change?: (composition: CompositionType) => void;
8
8
  color_scheme?: ColorSchemeName;
9
+ size?: number;
10
+ interactive?: boolean;
9
11
  };
10
12
  declare const Composition: import("svelte").Component<$$ComponentProps, {}, "">;
11
13
  type Composition = ReturnType<typeof Composition>;
@@ -145,9 +145,16 @@ function show_tooltip(element, event) {
145
145
  align-items: center;
146
146
  gap: var(--formula-tooltip-gap, 5pt);
147
147
  padding: var(--formula-tooltip-padding, 3pt 4pt);
148
- background: var(--formula-tooltip-bg, rgba(0, 0, 0, 0.9));
148
+ background: var(
149
+ --formula-tooltip-bg,
150
+ light-dark(rgba(255, 255, 255, 0.95), rgba(30, 30, 30, 0.95))
151
+ );
152
+ color: var(--formula-tooltip-color, light-dark(#222, #eee));
149
153
  border-radius: var(--formula-tooltip-border-radius, var(--border-radius, 3pt));
150
- box-shadow: var(--formula-tooltip-box-shadow, 0 4px 12px rgba(0, 0, 0, 0.3));
154
+ box-shadow: var(
155
+ --formula-tooltip-box-shadow,
156
+ 0 4px 12px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.4))
157
+ );
151
158
  z-index: var(--tooltip-z-index, 2);
152
159
  }
153
160
  .script-wrapper {
@@ -0,0 +1,429 @@
1
+ <script lang="ts">import Icon from '../Icon.svelte';
2
+ import { tooltip } from 'svelte-multiselect';
3
+ import { extract_formula_elements, normalize_element_symbols } from './parse';
4
+ const SEARCH_EXAMPLES = [
5
+ {
6
+ label: `Contains elements`,
7
+ description: `Materials containing at least these elements (may have others)`,
8
+ examples: [`Li,Fe`, `Si,O`, `Mn,Co,Ni`],
9
+ },
10
+ {
11
+ label: `Chemical system`,
12
+ description: `Materials with only these elements (no others)`,
13
+ examples: [`Li-Fe-O`, `Si-O`, `Na-Cl`],
14
+ },
15
+ {
16
+ label: `Exact formula`,
17
+ description: `Materials with this exact stoichiometry`,
18
+ examples: [`LiFePO4`, `SiO2`, `NaCl`],
19
+ },
20
+ ];
21
+ let { value = $bindable(``), search_mode = $bindable(`elements`), input_element = $bindable(null), show_clear_button = true, show_examples = true, disabled = false, onchange, onclear, ...rest } = $props();
22
+ let input_value = $state(value);
23
+ let examples_open = $state(false);
24
+ let wrapper = $state(null);
25
+ let examples_wrapper = $state(null);
26
+ let focused_item_idx = $state(-1);
27
+ let anchor_left = $state(false);
28
+ // Flatten examples for keyboard navigation
29
+ const all_examples = SEARCH_EXAMPLES.flatMap((cat) => cat.examples);
30
+ function handle_document_click(event) {
31
+ if (!wrapper || !examples_open)
32
+ return;
33
+ const target = event.target;
34
+ if (!(target instanceof Node))
35
+ return;
36
+ if (!wrapper.contains(target))
37
+ close_examples();
38
+ }
39
+ function close_examples(restore_focus = true) {
40
+ examples_open = false;
41
+ focused_item_idx = -1;
42
+ if (restore_focus)
43
+ input_element?.focus({ preventScroll: true });
44
+ }
45
+ // Track last synced value to detect external changes (e.g. from URL params)
46
+ // and re-infer mode accordingly. Without this, mode would only be set on first render.
47
+ let last_synced = $state(null);
48
+ $effect(() => {
49
+ input_value = value;
50
+ if (value !== last_synced) {
51
+ last_synced = value;
52
+ if (value) {
53
+ const inferred = infer_mode(value);
54
+ if (inferred !== search_mode)
55
+ search_mode = inferred;
56
+ }
57
+ }
58
+ });
59
+ // Detect if dropdown would exit viewport on the right and adjust anchor
60
+ $effect(() => {
61
+ if (!examples_open || !examples_wrapper)
62
+ return;
63
+ requestAnimationFrame(() => {
64
+ const dropdown = examples_wrapper?.querySelector(`.examples-dropdown`);
65
+ if (!dropdown)
66
+ return;
67
+ const rect = dropdown.getBoundingClientRect();
68
+ if (rect.right > window.innerWidth && !anchor_left)
69
+ anchor_left = true;
70
+ });
71
+ });
72
+ // Infer search mode from input format
73
+ function infer_mode(input) {
74
+ const trimmed = input.trim();
75
+ if (!trimmed)
76
+ return `elements`;
77
+ if (trimmed.includes(`,`))
78
+ return `elements`; // Li,Fe,O → contains elements
79
+ if (trimmed.includes(`-`))
80
+ return `chemsys`; // Li-Fe-O → chemical system
81
+ return `exact`; // LiFePO4 → exact formula
82
+ }
83
+ // Cycle through modes: elements → chemsys → exact → elements
84
+ const MODE_CYCLE = [`elements`, `chemsys`, `exact`];
85
+ // Extract elements from any input format (formula, comma-separated, dash-separated)
86
+ // Always returns elements in alphabetical order for consistency
87
+ function extract_elements(input) {
88
+ const trimmed = input.trim();
89
+ if (!trimmed)
90
+ return [];
91
+ // If contains commas or dashes, split by those and sort alphabetically
92
+ if (trimmed.includes(`,`) || trimmed.includes(`-`)) {
93
+ const parts = trimmed.split(/[-,]/).map((str) => str.trim()).filter(Boolean);
94
+ // Filter valid elements and sort alphabetically
95
+ return normalize_element_symbols(parts.join(`,`)).sort();
96
+ }
97
+ // Otherwise parse as formula (already returns sorted by default)
98
+ try {
99
+ return extract_formula_elements(trimmed, { sorted: true });
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ // Format elements for the given mode
106
+ function format_for_mode(elements, mode) {
107
+ if (elements.length === 0)
108
+ return ``;
109
+ if (mode === `elements`)
110
+ return elements.join(`,`);
111
+ if (mode === `chemsys`)
112
+ return elements.join(`-`);
113
+ // For exact mode, just join without separator (user will need to add counts)
114
+ return elements.join(``);
115
+ }
116
+ function cycle_mode() {
117
+ const current_idx = MODE_CYCLE.indexOf(search_mode);
118
+ const next_idx = (current_idx + 1) % MODE_CYCLE.length;
119
+ const next_mode = MODE_CYCLE[next_idx];
120
+ // Extract elements from current value and reformat for new mode
121
+ const elements = extract_elements(value);
122
+ const reformatted = format_for_mode(elements, next_mode);
123
+ search_mode = next_mode;
124
+ last_synced = value = input_value = reformatted; // update last_synced to prevent effect re-inference
125
+ onchange?.(reformatted, next_mode);
126
+ }
127
+ function set_value(new_value) {
128
+ const mode = infer_mode(new_value);
129
+ last_synced = value = input_value = new_value; // update last_synced to prevent effect re-inference
130
+ search_mode = mode;
131
+ onchange?.(value, mode);
132
+ }
133
+ function sync_value() {
134
+ const trimmed = input_value.trim();
135
+ if (!trimmed)
136
+ return set_value(``);
137
+ const mode = infer_mode(trimmed);
138
+ if (mode === `exact`)
139
+ return set_value(trimmed);
140
+ // Normalize element symbols for elements/chemsys modes
141
+ const separator = mode === `chemsys` ? `-` : `,`;
142
+ const normalized = normalize_element_symbols(trimmed.replace(/[-,]/g, `,`));
143
+ set_value(normalized.join(separator));
144
+ }
145
+ function onkeydown(event) {
146
+ if (event.key === `Enter`) {
147
+ event.preventDefault();
148
+ sync_value();
149
+ }
150
+ else if (event.key === `Escape`) {
151
+ if (examples_open)
152
+ examples_open = false;
153
+ else if (input_value)
154
+ clear_filter();
155
+ }
156
+ }
157
+ function clear_filter() {
158
+ onclear?.();
159
+ set_value(``);
160
+ }
161
+ function apply_example(example) {
162
+ set_value(example);
163
+ close_examples();
164
+ }
165
+ function toggle_examples(event) {
166
+ event.stopPropagation();
167
+ examples_open = !examples_open;
168
+ focused_item_idx = examples_open ? 0 : -1;
169
+ if (examples_open)
170
+ anchor_left = false;
171
+ }
172
+ function handle_menu_keydown(event) {
173
+ const len = all_examples.length;
174
+ if (!len)
175
+ return;
176
+ const is_button_activation = (event.key === `Enter` || event.key === ` `) &&
177
+ event.target instanceof HTMLButtonElement;
178
+ if (is_button_activation)
179
+ return;
180
+ const key_actions = {
181
+ ArrowDown: () => (focused_item_idx = (focused_item_idx + 1) % len),
182
+ ArrowUp: () => (focused_item_idx = (focused_item_idx - 1 + len) % len),
183
+ Home: () => (focused_item_idx = 0),
184
+ End: () => (focused_item_idx = len - 1),
185
+ Escape: close_examples,
186
+ };
187
+ if (event.key in key_actions) {
188
+ event.preventDefault();
189
+ key_actions[event.key]();
190
+ }
191
+ }
192
+ // Focus the active menu item when index changes
193
+ $effect(() => {
194
+ if (!examples_open || focused_item_idx < 0)
195
+ return;
196
+ const items = wrapper?.querySelectorAll(`[data-example-item]`);
197
+ items?.[focused_item_idx]?.focus({ preventScroll: true });
198
+ });
199
+ let placeholder = $derived(search_mode === `chemsys`
200
+ ? `Li-Fe-O`
201
+ : search_mode === `exact`
202
+ ? `LiFePO4`
203
+ : `Li,Fe,O`);
204
+ const MODE_LABELS = {
205
+ elements: `contains elements`,
206
+ chemsys: `chemical system`,
207
+ exact: `exact formula`,
208
+ };
209
+ let mode_hint = $derived(MODE_LABELS[search_mode]);
210
+ // Preview of next mode cycle step for tooltip
211
+ let next_mode = $derived.by(() => {
212
+ const next = MODE_CYCLE[(MODE_CYCLE.indexOf(search_mode) + 1) % MODE_CYCLE.length];
213
+ const mode = MODE_LABELS[next];
214
+ const next_value = format_for_mode(extract_elements(value), next);
215
+ return { mode, value: next_value };
216
+ });
217
+ </script>
218
+
219
+ <svelte:document onclick={handle_document_click} />
220
+
221
+ <div class="formula-filter" bind:this={wrapper} class:disabled {...rest}>
222
+ <input
223
+ bind:this={input_element}
224
+ bind:value={input_value}
225
+ onblur={sync_value}
226
+ {onkeydown}
227
+ {placeholder}
228
+ {disabled}
229
+ aria-label="Formula filter"
230
+ />
231
+ {#if input_value}
232
+ <button
233
+ type="button"
234
+ class="mode-hint clickable"
235
+ onclick={cycle_mode}
236
+ title="Click to switch to '{next_mode.mode}' → {next_mode.value}"
237
+ {@attach tooltip({ style: `font-size: 0.6em; padding: 1pt 5pt;` })}
238
+ aria-label="Change search mode"
239
+ >
240
+ {mode_hint}
241
+ </button>
242
+ {/if}
243
+ {#if show_clear_button && value && !disabled}
244
+ <button
245
+ type="button"
246
+ class="icon-btn clear-btn"
247
+ onclick={clear_filter}
248
+ title="Clear (Escape)"
249
+ aria-label="Clear filter"
250
+ >
251
+ <Icon icon="Close" style="width: 1em; height: 1em" />
252
+ </button>
253
+ {/if}
254
+ {#if show_examples && !disabled}
255
+ <div class="examples-wrapper" bind:this={examples_wrapper}>
256
+ <button
257
+ type="button"
258
+ class="icon-btn help-btn"
259
+ class:active={examples_open}
260
+ onclick={toggle_examples}
261
+ title="Show search examples"
262
+ aria-label="Show search examples"
263
+ aria-expanded={examples_open}
264
+ aria-haspopup="menu"
265
+ >
266
+ <Icon icon="Info" style="width: 1.1em; height: 1.1em" />
267
+ </button>
268
+ {#if examples_open}
269
+ <div
270
+ class="examples-dropdown"
271
+ class:anchor-left={anchor_left}
272
+ role="menu"
273
+ tabindex="-1"
274
+ onkeydown={handle_menu_keydown}
275
+ >
276
+ {#each SEARCH_EXAMPLES as category (category.label)}
277
+ <div class="example-category">
278
+ <div class="category-label">{category.label}:</div>
279
+ <div class="example-tags">
280
+ {#each category.examples as example (example)}
281
+ <button
282
+ type="button"
283
+ class="example-tag"
284
+ data-example-item
285
+ onclick={() => apply_example(example)}
286
+ title={category.description}
287
+ role="menuitem"
288
+ tabindex="-1"
289
+ >
290
+ {example}
291
+ </button>
292
+ {/each}
293
+ </div>
294
+ </div>
295
+ {/each}
296
+ </div>
297
+ {/if}
298
+ </div>
299
+ {/if}
300
+ </div>
301
+
302
+ <style>
303
+ .formula-filter {
304
+ position: relative;
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 6pt;
308
+ padding: 4pt 8pt;
309
+ border-radius: 6px;
310
+ background: var(--filter-bg, rgba(128, 128, 128, 0.05));
311
+ transition: background 0.15s;
312
+ }
313
+ .formula-filter:focus-within {
314
+ background: rgba(77, 182, 255, 0.08);
315
+ }
316
+ .formula-filter.disabled {
317
+ opacity: 0.5;
318
+ pointer-events: none;
319
+ }
320
+ input {
321
+ flex: 1;
322
+ min-width: 0;
323
+ border: none;
324
+ background: transparent;
325
+ color: inherit;
326
+ padding: 2pt 0;
327
+ outline: none;
328
+ font-family: var(--mono-font, monospace);
329
+ }
330
+ input::placeholder {
331
+ opacity: 0.4;
332
+ }
333
+ .mode-hint {
334
+ opacity: 0.5;
335
+ white-space: nowrap;
336
+ }
337
+ .mode-hint.clickable {
338
+ display: inline-flex;
339
+ align-items: center;
340
+ gap: 2pt;
341
+ background: rgba(77, 182, 255, 0.1);
342
+ border: 1px solid rgba(77, 182, 255, 0.25);
343
+ border-radius: 4px;
344
+ padding: 1pt 5pt;
345
+ cursor: pointer;
346
+ color: var(--highlight, #4db6ff);
347
+ opacity: 0.8;
348
+ transition: opacity 0.15s, background 0.15s;
349
+ }
350
+ .mode-hint.clickable:hover {
351
+ opacity: 1;
352
+ background: rgba(77, 182, 255, 0.2);
353
+ border-color: rgba(77, 182, 255, 0.4);
354
+ }
355
+ .icon-btn {
356
+ display: flex;
357
+ align-items: center;
358
+ justify-content: center;
359
+ background: none;
360
+ border: none;
361
+ cursor: pointer;
362
+ padding: 3pt;
363
+ border-radius: 50%;
364
+ color: inherit;
365
+ opacity: 0.4;
366
+ }
367
+ .icon-btn:hover {
368
+ opacity: 1;
369
+ background: rgba(128, 128, 128, 0.15);
370
+ }
371
+ .icon-btn.active {
372
+ opacity: 1;
373
+ color: var(--highlight, #4db6ff);
374
+ }
375
+ .examples-wrapper {
376
+ position: relative;
377
+ }
378
+ .examples-dropdown {
379
+ position: absolute;
380
+ top: calc(100% + 4pt);
381
+ right: 0;
382
+ z-index: 100;
383
+ width: max-content;
384
+ background: var(--dropdown-bg, var(--surface-bg, #fff));
385
+ border: 1px solid var(--dropdown-border, rgba(128, 128, 128, 0.2));
386
+ border-radius: 8px;
387
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
388
+ padding: 8pt;
389
+ box-sizing: border-box;
390
+ display: flex;
391
+ flex-direction: column;
392
+ gap: 6pt;
393
+ }
394
+ .examples-dropdown.anchor-left {
395
+ right: auto;
396
+ left: 0;
397
+ }
398
+ .example-category {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 6pt;
402
+ flex-wrap: wrap;
403
+ }
404
+ .category-label {
405
+ font-size: 0.75em;
406
+ font-weight: 600;
407
+ opacity: 0.6;
408
+ min-width: 115px;
409
+ }
410
+ .example-tags {
411
+ display: flex;
412
+ gap: 4pt;
413
+ flex-wrap: wrap;
414
+ }
415
+ .example-tag {
416
+ background: rgba(77, 182, 255, 0.1);
417
+ border: 1px solid rgba(77, 182, 255, 0.3);
418
+ border-radius: 4px;
419
+ padding: 3pt 7pt;
420
+ font-size: 0.82em;
421
+ font-family: var(--mono-font, monospace);
422
+ color: var(--highlight, #4db6ff);
423
+ cursor: pointer;
424
+ }
425
+ .example-tag:hover {
426
+ background: rgba(77, 182, 255, 0.2);
427
+ border-color: rgba(77, 182, 255, 0.5);
428
+ }
429
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { FormulaSearchMode } from './index';
3
+ type $$ComponentProps = {
4
+ value: string;
5
+ search_mode?: FormulaSearchMode;
6
+ input_element?: HTMLInputElement | null;
7
+ show_clear_button?: boolean;
8
+ show_examples?: boolean;
9
+ disabled?: boolean;
10
+ onchange?: (value: string, search_mode: FormulaSearchMode) => void;
11
+ onclear?: () => void;
12
+ } & HTMLAttributes<HTMLDivElement>;
13
+ declare const FormulaFilter: import("svelte").Component<$$ComponentProps, {}, "value" | "search_mode" | "input_element">;
14
+ type FormulaFilter = ReturnType<typeof FormulaFilter>;
15
+ export default FormulaFilter;
@@ -2,11 +2,13 @@ import type { ElementSymbol } from '..';
2
2
  export { default as BarChart } from './BarChart.svelte';
3
3
  export { default as BubbleChart } from './BubbleChart.svelte';
4
4
  export { default as Composition } from './Composition.svelte';
5
- export { default as Formula } from './Formula.svelte';
6
5
  export * from './format';
6
+ export { default as Formula } from './Formula.svelte';
7
+ export { default as FormulaFilter } from './FormulaFilter.svelte';
7
8
  export * from './parse';
8
9
  export { default as PieChart } from './PieChart.svelte';
9
10
  export type CompositionType = Partial<Record<ElementSymbol, number>>;
11
+ export type FormulaSearchMode = `elements` | `chemsys` | `exact`;
10
12
  export type ChartSegmentData = {
11
13
  element: ElementSymbol;
12
14
  amount: number;
@@ -1,8 +1,9 @@
1
1
  export { default as BarChart } from './BarChart.svelte';
2
2
  export { default as BubbleChart } from './BubbleChart.svelte';
3
3
  export { default as Composition } from './Composition.svelte';
4
- export { default as Formula } from './Formula.svelte';
5
4
  export * from './format';
5
+ export { default as Formula } from './Formula.svelte';
6
+ export { default as FormulaFilter } from './FormulaFilter.svelte';
6
7
  export * from './parse';
7
8
  export { default as PieChart } from './PieChart.svelte';
8
9
  export function get_chart_font_scale(base_scale, label_text, available_space, min_scale_factor = 0.7, base_font_size = 16) {
package/dist/constants.js CHANGED
@@ -64,7 +64,11 @@ export const STRUCT_KEYWORDS_REGEX = new RegExp(`(${STRUCT_KEYWORDS.join(`|`)})`
64
64
  export const STRUCT_KEYWORDS_STRICT_REGEX = new RegExp(`(${STRUCT_KEYWORDS_STRICT.join(`|`)})`, `i`);
65
65
  export const TRAJ_KEYWORDS_SIMPLE_REGEX = new RegExp(`(${TRAJ_KEYWORDS.join(`|`)})`, `i`);
66
66
  // File extensions for different file types
67
- export const TRAJ_EXTENSIONS = Object.freeze([`.traj`, `.xtc`]);
67
+ export const TRAJ_EXTENSIONS = Object.freeze([
68
+ `.traj`,
69
+ `.xtc`,
70
+ `.lammpstrj`,
71
+ ]);
68
72
  export const TRAJ_EXTENSIONS_REGEX = new RegExp(`\\.(${TRAJ_EXTENSIONS.map((ext) => ext.slice(1)).join(`|`)})$`, `i`);
69
73
  export const STRUCTURE_EXTENSIONS = Object.freeze([
70
74
  `.cif`,