matterviz 0.1.5 → 0.1.7

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 (98) hide show
  1. package/dist/DraggablePanel.svelte +73 -57
  2. package/dist/FilePicker.svelte +263 -0
  3. package/dist/FilePicker.svelte.d.ts +15 -0
  4. package/dist/Icon.svelte +7 -3
  5. package/dist/SettingsSection.svelte +116 -0
  6. package/dist/SettingsSection.svelte.d.ts +11 -0
  7. package/dist/api/mp.d.ts +6 -0
  8. package/dist/api/mp.js +22 -0
  9. package/dist/api/optimade.d.ts +43 -0
  10. package/dist/api/optimade.js +133 -0
  11. package/dist/app.css +15 -3
  12. package/dist/composition/BarChart.svelte +1 -1
  13. package/dist/composition/BubbleChart.svelte +1 -1
  14. package/dist/composition/Composition.svelte +1 -4
  15. package/dist/composition/PieChart.svelte +1 -1
  16. package/dist/composition/index.d.ts +2 -0
  17. package/dist/composition/parse.d.ts +9 -9
  18. package/dist/composition/parse.js +119 -214
  19. package/dist/element/ElementHeading.svelte +2 -2
  20. package/dist/element/ElementHeading.svelte.d.ts +1 -0
  21. package/dist/element/ElementPhoto.svelte +3 -3
  22. package/dist/element/ElementStats.svelte +3 -4
  23. package/dist/element/ElementStats.svelte.d.ts +1 -1
  24. package/dist/element/ElementTile.svelte +3 -3
  25. package/dist/element/ElementTile.svelte.d.ts +2 -2
  26. package/dist/element/Nucleus.svelte +0 -9
  27. package/dist/icons.d.ts +34 -8
  28. package/dist/icons.js +65 -8
  29. package/dist/index.d.ts +12 -18
  30. package/dist/index.js +3 -0
  31. package/dist/io/decompress.d.ts +6 -2
  32. package/dist/io/decompress.js +27 -27
  33. package/dist/io/export.d.ts +0 -6
  34. package/dist/io/export.js +18 -149
  35. package/dist/{mp-api.d.ts → io/fetch.d.ts} +0 -2
  36. package/dist/{mp-api.js → io/fetch.js} +2 -8
  37. package/dist/io/index.d.ts +0 -2
  38. package/dist/io/index.js +63 -63
  39. package/dist/labels.d.ts +3 -3
  40. package/dist/labels.js +1 -1
  41. package/dist/math.d.ts +3 -1
  42. package/dist/math.js +25 -21
  43. package/dist/periodic-table/PeriodicTable.svelte.d.ts +3 -3
  44. package/dist/periodic-table/TableInset.svelte +2 -2
  45. package/dist/plot/ColorBar.svelte +2 -4
  46. package/dist/plot/Histogram.svelte +212 -128
  47. package/dist/plot/Histogram.svelte.d.ts +13 -1
  48. package/dist/plot/HistogramControls.svelte +203 -93
  49. package/dist/plot/HistogramControls.svelte.d.ts +6 -1
  50. package/dist/plot/Line.svelte +1 -1
  51. package/dist/plot/Line.svelte.d.ts +1 -1
  52. package/dist/plot/PlotLegend.svelte +11 -13
  53. package/dist/plot/ScatterPlot.svelte +16 -13
  54. package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
  55. package/dist/plot/ScatterPlotControls.svelte +221 -198
  56. package/dist/plot/ScatterPlotControls.svelte.d.ts +2 -2
  57. package/dist/plot/index.js +3 -4
  58. package/dist/settings.d.ts +142 -0
  59. package/dist/settings.js +617 -0
  60. package/dist/state.svelte.d.ts +2 -2
  61. package/dist/structure/Bond.svelte +50 -9
  62. package/dist/structure/Bond.svelte.d.ts +10 -2
  63. package/dist/structure/CanvasTooltip.svelte +25 -0
  64. package/dist/structure/CanvasTooltip.svelte.d.ts +10 -0
  65. package/dist/structure/Lattice.svelte +30 -7
  66. package/dist/structure/Lattice.svelte.d.ts +3 -2
  67. package/dist/structure/Structure.svelte +176 -101
  68. package/dist/structure/Structure.svelte.d.ts +15 -3
  69. package/dist/structure/StructureControls.svelte +650 -342
  70. package/dist/structure/StructureControls.svelte.d.ts +4 -10
  71. package/dist/structure/StructureInfoPanel.svelte +7 -83
  72. package/dist/structure/StructureScene.svelte +203 -139
  73. package/dist/structure/StructureScene.svelte.d.ts +16 -6
  74. package/dist/structure/Vector.svelte +2 -2
  75. package/dist/structure/bonding.d.ts +22 -9
  76. package/dist/structure/bonding.js +224 -112
  77. package/dist/structure/export.d.ts +10 -0
  78. package/dist/structure/export.js +339 -0
  79. package/dist/structure/index.d.ts +27 -25
  80. package/dist/structure/index.js +2 -22
  81. package/dist/{io → structure}/parse.d.ts +9 -2
  82. package/dist/{io → structure}/parse.js +587 -262
  83. package/dist/structure/pbc.js +55 -36
  84. package/dist/structure/supercell.d.ts +8 -0
  85. package/dist/structure/supercell.js +159 -0
  86. package/dist/theme/ThemeControl.svelte +9 -8
  87. package/dist/theme/themes.js +2 -2
  88. package/dist/trajectory/Trajectory.svelte +363 -170
  89. package/dist/trajectory/Trajectory.svelte.d.ts +16 -13
  90. package/dist/trajectory/TrajectoryInfoPanel.svelte +150 -155
  91. package/dist/trajectory/index.d.ts +34 -6
  92. package/dist/trajectory/index.js +102 -33
  93. package/dist/trajectory/parse.d.ts +35 -6
  94. package/dist/trajectory/parse.js +776 -522
  95. package/dist/trajectory/plotting.d.ts +13 -8
  96. package/dist/trajectory/plotting.js +316 -280
  97. package/package.json +21 -19
  98. package/readme.md +30 -18
@@ -78,14 +78,32 @@ function handle_click_outside(event) {
78
78
  const is_inside_panel = panel_div &&
79
79
  (target === panel_div || panel_div.contains(target));
80
80
  if (!is_toggle_button && !is_inside_panel && !has_been_dragged &&
81
- !currently_dragging) {
81
+ !currently_dragging)
82
82
  close_panel();
83
- }
84
83
  }
85
84
  // Button click handler
86
- function handle_button_click(event, action) {
85
+ const handle_button_click = (action) => (event) => {
87
86
  event.stopPropagation();
88
87
  action();
88
+ };
89
+ // Debounced resize handler for better performance
90
+ let resize_timeout = $state(undefined);
91
+ function handle_resize() {
92
+ if (!show || has_been_dragged || currently_dragging)
93
+ return;
94
+ if (resize_timeout)
95
+ clearTimeout(resize_timeout);
96
+ const current_timeout = setTimeout(() => {
97
+ if (resize_timeout !== current_timeout)
98
+ return;
99
+ if (show && toggle_panel_btn && !has_been_dragged && panel_div) {
100
+ const pos = calculate_position();
101
+ initial_position = pos;
102
+ panel_div.style.left = pos.left;
103
+ panel_div.style.top = pos.top;
104
+ }
105
+ }, 50); // Debounce resize events
106
+ resize_timeout = current_timeout;
89
107
  }
90
108
  // Position panel when shown
91
109
  $effect(() => {
@@ -97,20 +115,19 @@ $effect(() => {
97
115
  left: pos.left,
98
116
  top: pos.top,
99
117
  right: `auto`,
100
- bottom: `auto`,
101
118
  });
102
119
  }
103
120
  }
104
121
  });
105
122
  </script>
106
123
 
107
- <svelte:window onkeydown={on_keydown} />
124
+ <svelte:window onkeydown={on_keydown} onresize={handle_resize} />
108
125
  <svelte:document onclick={handle_click_outside} />
109
126
 
110
127
  {#if show_panel}
111
128
  <button
112
129
  bind:this={toggle_panel_btn}
113
- onclick={(event) => handle_button_click(event, custom_toggle || toggle_panel)}
130
+ onclick={handle_button_click(custom_toggle || toggle_panel)}
114
131
  aria-expanded={show}
115
132
  aria-controls="draggable-panel"
116
133
  {...toggle_props}
@@ -137,32 +154,30 @@ $effect(() => {
137
154
  {...panel_props}
138
155
  class="draggable-panel {show ? `panel-open` : ``} {panel_props.class ?? ``}"
139
156
  >
140
- <div class="panel-header">
141
- <div class="control-buttons">
142
- {#if show_control_buttons}
143
- <button
144
- class="reset-button"
145
- onclick={(event) => handle_button_click(event, reset_position)}
146
- title="Reset panel position"
147
- aria-label="Reset panel position"
148
- >
149
- <Icon icon="Reset" style="width: 1.25em; height: 1.25em" />
150
- </button>
151
- <button
152
- class="close-button"
153
- onclick={(event) => handle_button_click(event, close_panel)}
154
- title="Close panel"
155
- aria-label="Close panel"
156
- >
157
- <Icon icon="Cross" style="width: 1.25em; height: 1.25em" />
158
- </button>
159
- {/if}
160
- <Icon
161
- icon="DragIndicator"
162
- class="drag-handle"
163
- style="width: 1.25em; height: 1.25em"
164
- />
165
- </div>
157
+ <div class="control-buttons">
158
+ {#if show_control_buttons}
159
+ <button
160
+ class="reset-button"
161
+ onclick={handle_button_click(reset_position)}
162
+ title="Reset panel position"
163
+ aria-label="Reset panel position"
164
+ >
165
+ <Icon icon="Reset" style="width: 1.25em; height: 1.25em" />
166
+ </button>
167
+ <button
168
+ class="close-button"
169
+ onclick={handle_button_click(close_panel)}
170
+ title="Close panel"
171
+ aria-label="Close panel"
172
+ >
173
+ <Icon icon="Cross" style="width: 1.25em; height: 1.25em" />
174
+ </button>
175
+ {/if}
176
+ <Icon
177
+ icon="DragIndicator"
178
+ class="drag-handle"
179
+ style="width: 1.25em; height: 1.25em"
180
+ />
166
181
  </div>
167
182
 
168
183
  {@render children()}
@@ -178,7 +193,7 @@ $effect(() => {
178
193
  border-radius: var(--panel-toggle-border-radius, 3pt);
179
194
  background-color: transparent;
180
195
  transition: background-color 0.2s;
181
- font-size: clamp(1em, 2cqw, 1.6em);
196
+ font-size: var(--panel-toggle-font-size, clamp(1.1em, calc(1cqw + 1cqh), 1.4em));
182
197
  }
183
198
  button.panel-toggle:hover {
184
199
  background-color: color-mix(in srgb, currentColor 8%, transparent);
@@ -190,7 +205,7 @@ $effect(() => {
190
205
  border-radius: 6px;
191
206
  padding: var(--panel-padding, 1ex);
192
207
  box-sizing: border-box;
193
- z-index: 10;
208
+ z-index: var(--panel-z-index, 10);
194
209
  display: grid;
195
210
  gap: 4pt;
196
211
  text-align: left;
@@ -198,7 +213,8 @@ $effect(() => {
198
213
  transition: opacity 0.3s, background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
199
214
  width: 28em;
200
215
  max-width: 90cqw;
201
- overflow: auto;
216
+ overflow-x: hidden;
217
+ overflow-y: auto;
202
218
  max-height: calc(100vh - 3em);
203
219
  pointer-events: auto;
204
220
  }
@@ -210,14 +226,14 @@ $effect(() => {
210
226
  }
211
227
  /* Panel content styling */
212
228
  .draggable-panel :global(h4) {
213
- margin: 8pt 0 2pt;
214
- font-size: 0.9em;
229
+ margin: 2pt 0;
230
+ font-size: 0.95em;
215
231
  }
216
232
  .draggable-panel :global(hr) {
217
233
  border: none;
218
234
  background: var(--panel-hr-bg, rgba(255, 255, 255, 0.1));
219
- margin: 0;
220
- height: 0.5px;
235
+ margin: 4pt 0;
236
+ height: 1px;
221
237
  }
222
238
  .draggable-panel :global(label) {
223
239
  display: flex;
@@ -227,6 +243,7 @@ $effect(() => {
227
243
  .draggable-panel :global(input[type='text']) {
228
244
  flex: 1;
229
245
  padding: 4px 6px;
246
+ margin: var(--panel-input-margin, 0 0 0 5pt);
230
247
  }
231
248
  .draggable-panel :global(input[type='text'].invalid) {
232
249
  border-color: var(--error-color, #ff6b6b);
@@ -237,11 +254,9 @@ $effect(() => {
237
254
  box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
238
255
  }
239
256
  .draggable-panel :global(input[type='range']) {
240
- margin-left: auto;
257
+ margin-left: 4pt;
241
258
  width: 100px;
242
259
  flex-shrink: 0;
243
- }
244
- .draggable-panel :global(input[type='range']) {
245
260
  flex: 1;
246
261
  min-width: 60px;
247
262
  }
@@ -282,27 +297,28 @@ $effect(() => {
282
297
  gap: 8pt;
283
298
  align-items: center;
284
299
  }
285
- .draggable-panel :global(.panel-row label.slider-control) {
300
+ .draggable-panel :global(.panel-grid) {
301
+ display: grid;
302
+ gap: 8pt;
303
+ align-items: center;
304
+ }
305
+ .draggable-panel :global(label:has(input[type='range'])) {
286
306
  flex: 1;
287
307
  }
288
- /* Panel header styling */
289
- .draggable-panel .panel-header {
308
+ .draggable-panel .control-buttons {
290
309
  display: flex;
291
310
  justify-content: flex-end;
292
311
  align-items: center;
293
- position: absolute;
294
- top: 5px;
295
- left: 5px;
296
- right: 5px;
297
- height: 1.3em;
298
- z-index: 10;
299
- pointer-events: none; /* Allow events to pass through to children */
300
- }
301
- .draggable-panel .control-buttons {
302
- display: flex;
312
+ position: sticky;
313
+ top: 0;
314
+ right: 0;
315
+ height: 0;
316
+ /* Cancel the 12 pt top/bottom padding without relying on width-based percentages */
303
317
  gap: 5px;
304
- align-items: center;
305
- pointer-events: auto; /* Re-enable pointer events for buttons */
318
+ padding: 12pt 3pt;
319
+ margin-bottom: calc(-2 * 12pt);
320
+ box-sizing: border-box;
321
+ justify-self: end;
306
322
  }
307
323
  .draggable-panel :global(.drag-handle) {
308
324
  width: 1.3em;
@@ -0,0 +1,263 @@
1
+ <script lang="ts">let { files, active_files = [], show_category_filters = false, category_labels = {}, on_drag_start, on_drag_end, type_mapper, file_type_colors = {
2
+ cif: `rgba(100, 149, 237, 0.8)`,
3
+ xyz: `rgba(50, 205, 50, 0.8)`,
4
+ extxyz: `rgba(50, 205, 50, 0.8)`,
5
+ poscar: `rgba(255, 140, 0, 0.8)`,
6
+ json: `rgba(138, 43, 226, 0.8)`,
7
+ traj: `rgba(255, 192, 203, 0.8)`,
8
+ hdf5: `rgba(255, 69, 0, 0.8)`,
9
+ gz: `rgba(169, 169, 169, 0.8)`,
10
+ md: `rgba(255, 215, 0, 0.8)`,
11
+ yaml: `rgba(255, 0, 255, 0.8)`,
12
+ xdatcar: `rgba(255, 215, 0, 0.8)`,
13
+ }, ...rest } = $props();
14
+ let active_category_filter = $state(null);
15
+ let active_type_filter = $state(null);
16
+ // Helper function to get the base file type (removing .gz extension)
17
+ const get_base_file_type = (filename) => {
18
+ // Use custom type mapper if provided
19
+ if (type_mapper)
20
+ return type_mapper(filename);
21
+ let base_name = filename.toLowerCase();
22
+ // Remove .gz extension if present
23
+ if (base_name.endsWith(`.gz`))
24
+ base_name = base_name.slice(0, -3);
25
+ return base_name.split(`.`).pop() || `file`;
26
+ };
27
+ // Filter files based on active filters
28
+ let filtered_files = $derived(files.filter((file) => {
29
+ if (active_category_filter && file.category) {
30
+ return file.category === active_category_filter;
31
+ }
32
+ if (active_type_filter) {
33
+ const normalized_type = get_base_file_type(file.name);
34
+ return normalized_type === active_type_filter;
35
+ }
36
+ return true;
37
+ }));
38
+ const toggle_filter = (kind, filter) => {
39
+ if (kind === `category`) {
40
+ active_category_filter = active_category_filter === filter ? null : filter;
41
+ active_type_filter = null;
42
+ }
43
+ else {
44
+ active_type_filter = active_type_filter === filter ? null : filter;
45
+ active_category_filter = null;
46
+ }
47
+ };
48
+ const handle_drag_start = (file) => (event) => {
49
+ const file_url = file.url || file.name; // Get the URL to drag (falling back to name)
50
+ const payload = JSON.stringify({
51
+ name: file.name,
52
+ url: file_url,
53
+ type: file.type || get_base_file_type(file.name),
54
+ category: file.category,
55
+ });
56
+ // Set file data as JSON for applications that can handle it
57
+ event.dataTransfer?.setData(`application/json`, payload);
58
+ // Also set plain text as fallback for external applications
59
+ event.dataTransfer?.setData(`text/plain`, file_url);
60
+ on_drag_start?.(file, event);
61
+ };
62
+ // Get unique file types for format filters
63
+ let uniq_formats = $derived([...new Set(files.map((file) => get_base_file_type(file.name)))].sort());
64
+ // Get unique category types for category filters
65
+ let uniq_categories = $derived(show_category_filters
66
+ ? [...new Set(files.map((file) => file.category))].sort().filter(Boolean)
67
+ : []);
68
+ export {};
69
+ </script>
70
+
71
+ <div class="file-picker" {...rest}>
72
+ <div class="legend">
73
+ {#if show_category_filters}
74
+ {#each uniq_categories as category (category)}
75
+ {@const is_active = active_category_filter === category}
76
+ <span
77
+ class="legend-item"
78
+ class:active={is_active}
79
+ onclick={() => category && toggle_filter(`category`, category)}
80
+ onkeydown={(evt) =>
81
+ (evt.key === `Enter` || evt.key === ` `) &&
82
+ category &&
83
+ toggle_filter(`category`, category)}
84
+ role="button"
85
+ tabindex="0"
86
+ title="Filter to show only {category}"
87
+ >
88
+ {(category && category_labels[category]) || category}
89
+ </span>
90
+ {/each}
91
+ {#if uniq_categories.length > 0 && uniq_formats.length > 0}&emsp;{/if}
92
+ {/if}
93
+
94
+ {#each uniq_formats as format (format)}
95
+ {@const is_active = active_type_filter === format}
96
+ <span
97
+ class="legend-item format-item"
98
+ class:active={is_active}
99
+ onclick={() => toggle_filter(`type`, format)}
100
+ onkeydown={(evt) =>
101
+ (evt.key === `Enter` || evt.key === ` `) && toggle_filter(`type`, format)}
102
+ role="button"
103
+ tabindex="0"
104
+ title="Filter to show only {format.toUpperCase()} files"
105
+ >
106
+ <span
107
+ class="format-circle"
108
+ style:background-color={file_type_colors[format]}
109
+ ></span> {format.toUpperCase()}
110
+ </span>
111
+ {/each}
112
+
113
+ {#if active_category_filter || active_type_filter}
114
+ <button
115
+ title="Clear all filters"
116
+ class="clear-filter"
117
+ onclick={() => {
118
+ active_category_filter = null
119
+ active_type_filter = null
120
+ }}
121
+ >
122
+
123
+ </button>
124
+ {/if}
125
+ </div>
126
+
127
+ {#each filtered_files as file (file.name)}
128
+ {@const base_type = get_base_file_type(file.name)}
129
+ {@const is_compressed = file.name.toLowerCase().endsWith(`.gz`)}
130
+ <div
131
+ class="file-item"
132
+ class:active={active_files.includes(file.name)}
133
+ class:compressed={is_compressed}
134
+ style:background-color={file_type_colors[base_type]?.replace(`0.8`, `0.08`)}
135
+ draggable="true"
136
+ ondragstart={handle_drag_start(file)}
137
+ ondragend={() => on_drag_end?.()}
138
+ role="button"
139
+ tabindex="0"
140
+ title="Drag this {base_type.toUpperCase()} file"
141
+ >
142
+ <div class="drag-handle">
143
+ <div class="drag-bar"></div>
144
+ <div class="drag-bar"></div>
145
+ <div class="drag-bar"></div>
146
+ </div>
147
+ <div class="file-name">
148
+ {file.name}{file.category ? `\u00A0${file.category}` : ``}
149
+ {#if is_compressed}<span class="compression-indicator">📦</span>{/if}
150
+ </div>
151
+ </div>
152
+ {/each}
153
+ </div>
154
+
155
+ <style>
156
+ .file-picker {
157
+ display: flex;
158
+ flex-wrap: wrap;
159
+ gap: 0.5em;
160
+ flex: 1;
161
+ align-content: start;
162
+ }
163
+ .legend {
164
+ width: 100%;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.8em;
168
+ font-size: 0.6em;
169
+ opacity: 0.8;
170
+ margin: 0 0 0.5em;
171
+ }
172
+ .legend-item {
173
+ cursor: pointer;
174
+ padding: 0.2em 0.4em;
175
+ border-radius: 3px;
176
+ transition: all 0.2s ease;
177
+ border: 1px solid transparent;
178
+ }
179
+ .legend-item:hover {
180
+ opacity: 1;
181
+ background: rgba(255, 255, 255, 0.1);
182
+ border-color: rgba(255, 255, 255, 0.3);
183
+ }
184
+ .legend-item.active {
185
+ opacity: 1;
186
+ background: rgba(255, 255, 255, 0.2);
187
+ border-color: rgba(255, 255, 255, 0.5);
188
+ font-weight: bold;
189
+ }
190
+ .clear-filter {
191
+ background-color: var(--btn-bg);
192
+ border-radius: 50%;
193
+ display: flex;
194
+ place-content: center;
195
+ }
196
+ .clear-filter:hover {
197
+ background-color: var(--btn-hover-bg);
198
+ }
199
+ .format-item {
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 0.3em;
203
+ }
204
+ .format-circle {
205
+ width: 8px;
206
+ height: 8px;
207
+ border-radius: 50%;
208
+ display: inline-block;
209
+ }
210
+ .file-item {
211
+ display: flex;
212
+ align-items: center;
213
+ padding: 4pt 8pt;
214
+ border: 1px solid rgba(255, 255, 255, 0.2);
215
+ border-radius: 20px;
216
+ cursor: grab;
217
+ background: rgba(255, 255, 255, 0.1);
218
+ transition: all 0.2s ease;
219
+ gap: 0.5em;
220
+ }
221
+ .file-item.active {
222
+ border-color: var(--success-color, #00ff00);
223
+ background: rgba(0, 255, 0, 0.15);
224
+ box-shadow: 0 0 8px rgba(0, 255, 0, 0.3);
225
+ }
226
+ .file-item:active {
227
+ cursor: grabbing;
228
+ }
229
+ .file-item:hover {
230
+ border-color: var(--accent-color, #007acc);
231
+ background: rgba(0, 122, 204, 0.2);
232
+ filter: brightness(1.1);
233
+ }
234
+ .drag-handle {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 2px;
238
+ opacity: 0.6;
239
+ }
240
+ .drag-bar {
241
+ width: 12px;
242
+ height: 2px;
243
+ background: currentColor;
244
+ border-radius: 1px;
245
+ }
246
+ .file-name {
247
+ font-size: 0.7em;
248
+ line-height: 1.1;
249
+ white-space: pre-line;
250
+ }
251
+ .compression-indicator {
252
+ opacity: 0.7;
253
+ font-size: 0.8em;
254
+ margin-left: 0.2em;
255
+ }
256
+ .file-item.compressed {
257
+ border-style: dashed;
258
+ opacity: 0.9;
259
+ }
260
+ .file-item.compressed:hover {
261
+ opacity: 1;
262
+ }
263
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { FileInfo } from './';
2
+ interface Props {
3
+ files: FileInfo[];
4
+ active_files?: string[];
5
+ show_category_filters?: boolean;
6
+ category_labels?: Record<string, string>;
7
+ on_drag_start?: (file: FileInfo, event: DragEvent) => void;
8
+ on_drag_end?: () => void;
9
+ type_mapper?: (filename: string) => string;
10
+ file_type_colors?: Record<string, string>;
11
+ [key: string]: unknown;
12
+ }
13
+ declare const FilePicker: import("svelte").Component<Props, {}, "">;
14
+ type FilePicker = ReturnType<typeof FilePicker>;
15
+ export default FilePicker;
package/dist/Icon.svelte CHANGED
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">import { icon_data } from './icons';
2
2
  let { icon, ...rest } = $props();
3
- const data = $derived.by(() => {
3
+ const { path, ...svg_props } = $derived.by(() => {
4
4
  if (!(icon in icon_data)) {
5
5
  console.error(`Icon '${icon}' not found`);
6
6
  return icon_data.Alert; // fallback
@@ -9,8 +9,12 @@ const data = $derived.by(() => {
9
9
  });
10
10
  </script>
11
11
 
12
- <svg viewBox={data.viewBox} fill="currentColor" {...rest}>
13
- <path d={data.path} />
12
+ <svg fill="currentColor" {...svg_props} {...rest}>
13
+ {#if path.trim().startsWith(`<`)}
14
+ {@html path}
15
+ {:else}
16
+ <path d={path} />
17
+ {/if}
14
18
  </svg>
15
19
 
16
20
  <style>
@@ -0,0 +1,116 @@
1
+ <script lang="ts">import { Icon } from './';
2
+ let { title, current_values, children, on_reset = () => { }, ...rest } = $props();
3
+ // Create a deep copy of current_values on mount to use as reference values
4
+ function deep_copy(obj) {
5
+ if (obj === null || typeof obj !== `object`)
6
+ return obj;
7
+ if (obj instanceof Date)
8
+ return new Date(obj.getTime());
9
+ if (obj instanceof RegExp)
10
+ return new RegExp(obj);
11
+ if (Array.isArray(obj)) {
12
+ return obj.map((item) => typeof item === `object` && item !== null ? deep_copy(item) : item);
13
+ }
14
+ const copy = {};
15
+ for (const [key, value] of Object.entries(obj)) {
16
+ copy[key] = typeof value === `object` && value !== null
17
+ ? deep_copy(value)
18
+ : value;
19
+ }
20
+ return copy;
21
+ }
22
+ const reference_values = deep_copy(current_values);
23
+ // Check if any values have changed from reference values
24
+ let has_changes = $derived.by(() => {
25
+ for (const [key, reference_value] of Object.entries(reference_values)) {
26
+ const current_value = current_values[key];
27
+ // Deep comparison for arrays
28
+ if (Array.isArray(reference_value) && Array.isArray(current_value)) {
29
+ if (reference_value.length !== current_value.length)
30
+ return true;
31
+ if (reference_value.some((val, idx) => {
32
+ const curr_val = current_value[idx];
33
+ // Handle nested objects/arrays in arrays
34
+ if (typeof val === `object` && val !== null &&
35
+ typeof curr_val === `object` && curr_val !== null)
36
+ return JSON.stringify(val) !== JSON.stringify(curr_val); // Quick deep comparison fallback
37
+ return val !== curr_val;
38
+ })) {
39
+ return true;
40
+ }
41
+ continue;
42
+ }
43
+ // Handle undefined/null comparisons properly
44
+ if (reference_value === undefined && current_value === undefined)
45
+ continue;
46
+ if (reference_value === null && current_value === null)
47
+ continue;
48
+ // Basic comparison for primitives
49
+ if (current_value !== reference_value) {
50
+ return true;
51
+ }
52
+ }
53
+ return false;
54
+ });
55
+ function handle_reset(event) {
56
+ event.stopPropagation();
57
+ event.preventDefault();
58
+ on_reset();
59
+ }
60
+ </script>
61
+
62
+ <h4>
63
+ {title}
64
+
65
+ {#if has_changes}
66
+ <button
67
+ class="reset-button"
68
+ onclick={handle_reset}
69
+ title="Reset {title.toLowerCase()} to defaults"
70
+ aria-label="Reset {title.toLowerCase()} to defaults"
71
+ >
72
+ <Icon icon="Reset" style="width: 0.9em; height: 0.9em" />
73
+ Reset
74
+ </button>
75
+ {/if}
76
+ </h4>
77
+ <section {...rest}>
78
+ {@render children()}
79
+ </section>
80
+
81
+ <style>
82
+ h4 {
83
+ margin: 0;
84
+ position: relative;
85
+ }
86
+ .reset-button {
87
+ position: absolute;
88
+ top: 0;
89
+ right: 0;
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 2pt;
93
+ padding: var(--reset-btn-padding, 1pt 4pt);
94
+ font-size: 0.65em;
95
+ border-radius: var(--reset-btn-border-radius, 2pt);
96
+ background: var(--btn-bg, rgba(0, 0, 0, 0.1));
97
+ color: var(--text-color-muted, #6b7280);
98
+ border: 1px solid var(--border-color, #d1d5db);
99
+ cursor: pointer;
100
+ z-index: 5;
101
+ transition: all 0.15s ease;
102
+ box-shadow: none;
103
+ opacity: 0.7;
104
+ }
105
+ .reset-button:hover {
106
+ background: var(--btn-hover-bg, rgba(0, 0, 0, 0.2));
107
+ color: var(--text-color, #374151);
108
+ opacity: 1;
109
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
110
+ }
111
+ .reset-button.standalone {
112
+ position: absolute;
113
+ top: -8pt;
114
+ right: -8pt;
115
+ }
116
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ title: string;
4
+ current_values: Record<string, unknown>;
5
+ children: Snippet<[]>;
6
+ on_reset?: () => void;
7
+ [key: string]: unknown;
8
+ }
9
+ declare const SettingsSection: import("svelte").Component<Props, {}, "">;
10
+ type SettingsSection = ReturnType<typeof SettingsSection>;
11
+ export default SettingsSection;
@@ -0,0 +1,6 @@
1
+ export declare const mp_bucket = "https://materialsproject-build.s3.amazonaws.com/collections/2022-10-28";
2
+ export declare function fetch_material_data<T extends Record<string, unknown>>(material_id: string, bucket?: string): Promise<{
3
+ summary: T | null;
4
+ similarity: T | null;
5
+ robocrys: T | null;
6
+ }>;
package/dist/api/mp.js ADDED
@@ -0,0 +1,22 @@
1
+ import { fetch_zipped } from '../io/fetch';
2
+ // TODO update to get MP details pages working again
3
+ export const mp_bucket = `https://materialsproject-build.s3.amazonaws.com/collections/2022-10-28`;
4
+ // Fetch all material data in parallel
5
+ export async function fetch_material_data(material_id, bucket = mp_bucket) {
6
+ try {
7
+ const results = await Promise.allSettled([
8
+ fetch_zipped(`${bucket}/summary/${material_id}.json.gz`),
9
+ fetch_zipped(`${bucket}/similarity/${material_id}.json.gz`),
10
+ fetch_zipped(`${bucket}/robocrys/${material_id}.json.gz`),
11
+ ]);
12
+ return {
13
+ summary: results[0].status === `fulfilled` ? results[0].value : null,
14
+ similarity: results[1].status === `fulfilled` ? results[1].value : null,
15
+ robocrys: results[2].status === `fulfilled` ? results[2].value : null,
16
+ };
17
+ }
18
+ catch (err) {
19
+ console.error(`Failed to fetch material data:`, err);
20
+ return { summary: null, similarity: null, robocrys: null };
21
+ }
22
+ }