matterviz 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/FilePicker.svelte +40 -50
  2. package/dist/api/optimade.js +10 -7
  3. package/dist/app.css +15 -0
  4. package/dist/brillouin/BrillouinZone.svelte +18 -9
  5. package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
  6. package/dist/brillouin/compute.js +10 -16
  7. package/dist/brillouin/geometry.js +2 -2
  8. package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
  9. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
  10. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
  11. package/dist/chempot-diagram/async-compute.svelte.js +12 -13
  12. package/dist/chempot-diagram/color.d.ts +21 -1
  13. package/dist/chempot-diagram/color.js +70 -1
  14. package/dist/chempot-diagram/compute.d.ts +7 -0
  15. package/dist/chempot-diagram/compute.js +85 -26
  16. package/dist/chempot-diagram/export.js +1 -6
  17. package/dist/chempot-diagram/temperature.js +9 -10
  18. package/dist/composition/BarChart.svelte +7 -9
  19. package/dist/composition/Composition.svelte +3 -8
  20. package/dist/composition/Formula.svelte +2 -2
  21. package/dist/composition/FormulaFilter.svelte +27 -93
  22. package/dist/composition/format.js +6 -8
  23. package/dist/composition/parse.js +50 -105
  24. package/dist/constants.d.ts +0 -1
  25. package/dist/constants.js +6 -17
  26. package/dist/convex-hull/ConvexHull2D.svelte +0 -2
  27. package/dist/convex-hull/ConvexHull3D.svelte +19 -47
  28. package/dist/convex-hull/ConvexHull4D.svelte +65 -95
  29. package/dist/convex-hull/StructurePopup.svelte +1 -1
  30. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  31. package/dist/convex-hull/helpers.js +1 -7
  32. package/dist/convex-hull/thermodynamics.js +19 -27
  33. package/dist/coordination/CoordinationBarPlot.svelte +12 -21
  34. package/dist/element/BohrAtom.svelte +0 -1
  35. package/dist/element/ElementTile.svelte +26 -44
  36. package/dist/element/data.d.ts +2 -0
  37. package/dist/element/data.js +5 -1
  38. package/dist/element/index.d.ts +1 -1
  39. package/dist/element/index.js +1 -1
  40. package/dist/fermi-surface/FermiSurface.svelte +22 -23
  41. package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
  42. package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
  43. package/dist/fermi-surface/compute.js +22 -55
  44. package/dist/fermi-surface/parse.js +38 -70
  45. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  46. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  47. package/dist/file-viewer/PlotPanel.svelte +350 -0
  48. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  49. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  50. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  51. package/dist/file-viewer/detect.d.ts +11 -0
  52. package/dist/file-viewer/detect.js +381 -0
  53. package/dist/file-viewer/eligibility.d.ts +4 -0
  54. package/dist/file-viewer/eligibility.js +53 -0
  55. package/dist/file-viewer/host-protocol.d.ts +50 -0
  56. package/dist/file-viewer/host-transfer.d.ts +25 -0
  57. package/dist/file-viewer/host-transfer.js +42 -0
  58. package/dist/file-viewer/index.d.ts +5 -0
  59. package/dist/file-viewer/index.js +5 -0
  60. package/dist/file-viewer/main.d.ts +44 -0
  61. package/dist/file-viewer/main.js +535 -0
  62. package/dist/file-viewer/parse.d.ts +9 -0
  63. package/dist/file-viewer/parse.js +170 -0
  64. package/dist/file-viewer/plot-utils.d.ts +24 -0
  65. package/dist/file-viewer/plot-utils.js +191 -0
  66. package/dist/file-viewer/types.d.ts +4 -0
  67. package/dist/file-viewer/types.js +5 -0
  68. package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
  69. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.js +1 -1
  72. package/dist/io/ExportPane.svelte +1 -1
  73. package/dist/io/decompress.d.ts +2 -0
  74. package/dist/io/decompress.js +3 -2
  75. package/dist/io/export.d.ts +7 -4
  76. package/dist/io/export.js +70 -18
  77. package/dist/io/file-drop.js +60 -35
  78. package/dist/io/url-drop.d.ts +2 -0
  79. package/dist/io/url-drop.js +28 -16
  80. package/dist/isosurface/Isosurface.svelte +687 -196
  81. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  82. package/dist/isosurface/IsosurfaceControls.svelte +564 -85
  83. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  84. package/dist/isosurface/VolumeSlice.svelte +224 -0
  85. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  86. package/dist/isosurface/coloring.d.ts +20 -0
  87. package/dist/isosurface/coloring.js +118 -0
  88. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  89. package/dist/isosurface/geometry.worker.d.ts +1 -0
  90. package/dist/isosurface/geometry.worker.js +59 -0
  91. package/dist/isosurface/grid.d.ts +10 -0
  92. package/dist/isosurface/grid.js +31 -0
  93. package/dist/isosurface/index.d.ts +4 -0
  94. package/dist/isosurface/index.js +4 -0
  95. package/dist/isosurface/profile.d.ts +12 -0
  96. package/dist/isosurface/profile.js +19 -0
  97. package/dist/isosurface/sampling.d.ts +34 -0
  98. package/dist/isosurface/sampling.js +408 -0
  99. package/dist/isosurface/slice-rendering.d.ts +13 -0
  100. package/dist/isosurface/slice-rendering.js +79 -0
  101. package/dist/isosurface/slice.d.ts +22 -2
  102. package/dist/isosurface/slice.js +200 -116
  103. package/dist/isosurface/types.d.ts +24 -1
  104. package/dist/isosurface/types.js +120 -8
  105. package/dist/json-path.d.ts +4 -0
  106. package/dist/json-path.js +89 -0
  107. package/dist/labels.js +1 -1
  108. package/dist/layout/InfoCard.svelte +1 -3
  109. package/dist/layout/SettingsSection.svelte +52 -61
  110. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  111. package/dist/layout/ViewerChrome.svelte +7 -1
  112. package/dist/layout/json-tree/JsonNode.svelte +14 -48
  113. package/dist/layout/json-tree/JsonTree.svelte +48 -95
  114. package/dist/layout/json-tree/JsonValue.svelte +3 -18
  115. package/dist/layout/json-tree/utils.d.ts +0 -3
  116. package/dist/layout/json-tree/utils.js +50 -179
  117. package/dist/marching-cubes.d.ts +8 -1
  118. package/dist/marching-cubes.js +115 -48
  119. package/dist/math.d.ts +3 -2
  120. package/dist/math.js +29 -38
  121. package/dist/overlays/ContextMenu.svelte +15 -32
  122. package/dist/overlays/DraggablePane.svelte +39 -58
  123. package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
  124. package/dist/overlays/GlassChip.svelte +3 -1
  125. package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
  126. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
  127. package/dist/phase-diagram/build-diagram.js +1 -6
  128. package/dist/phase-diagram/svg-to-diagram.js +9 -14
  129. package/dist/plot/bar/BarPlot.svelte +38 -42
  130. package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
  131. package/dist/plot/box/BoxPlot.svelte +27 -23
  132. package/dist/plot/core/auto-place.d.ts +1 -1
  133. package/dist/plot/core/auto-place.js +1 -1
  134. package/dist/plot/core/data-transform.d.ts +1 -2
  135. package/dist/plot/core/data-transform.js +0 -10
  136. package/dist/plot/core/layout.d.ts +12 -5
  137. package/dist/plot/core/layout.js +37 -27
  138. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  139. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  140. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  141. package/dist/plot/core/placed-tween.svelte.js +68 -21
  142. package/dist/plot/core/svg.js +0 -1
  143. package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
  144. package/dist/plot/core/utils/hierarchy-chart.js +12 -0
  145. package/dist/plot/core/utils/hierarchy-labels.js +3 -2
  146. package/dist/plot/histogram/Histogram.svelte +21 -26
  147. package/dist/plot/sankey/sankey.js +13 -9
  148. package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
  149. package/dist/plot/scatter/ScatterPlot.svelte +67 -52
  150. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  151. package/dist/plot/scatter/adaptive-density.js +36 -20
  152. package/dist/plot/sunburst/Sunburst.svelte +20 -20
  153. package/dist/plot/treemap/Treemap.svelte +175 -60
  154. package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
  155. package/dist/plot/treemap/index.d.ts +1 -0
  156. package/dist/plot/treemap/labels.d.ts +38 -0
  157. package/dist/plot/treemap/labels.js +80 -0
  158. package/dist/plot/treemap/treemap.d.ts +4 -2
  159. package/dist/plot/treemap/treemap.js +5 -2
  160. package/dist/rdf/RdfPlot.svelte +17 -35
  161. package/dist/rdf/calc-rdf.js +59 -44
  162. package/dist/rdf/index.d.ts +0 -1
  163. package/dist/sanitize.js +84 -26
  164. package/dist/scene/SceneCamera.svelte +1 -1
  165. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  166. package/dist/scene/index.d.ts +1 -2
  167. package/dist/scene/index.js +1 -1
  168. package/dist/scene/{props.js → props.svelte.js} +15 -1
  169. package/dist/settings.js +159 -482
  170. package/dist/spectral/Bands.svelte +151 -158
  171. package/dist/spectral/BrillouinBandsDos.svelte +4 -19
  172. package/dist/spectral/Dos.svelte +26 -24
  173. package/dist/spectral/helpers.js +40 -105
  174. package/dist/structure/AtomLegend.svelte +8 -13
  175. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  176. package/dist/structure/CellSelect.svelte +35 -10
  177. package/dist/structure/Structure.svelte +177 -99
  178. package/dist/structure/Structure.svelte.d.ts +1 -1
  179. package/dist/structure/StructureCarousel.svelte +10 -3
  180. package/dist/structure/StructureControls.svelte +8 -26
  181. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  182. package/dist/structure/StructureInfoPane.svelte +65 -76
  183. package/dist/structure/StructureScene.svelte +75 -109
  184. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  185. package/dist/structure/StructureViewport.svelte +120 -55
  186. package/dist/structure/StructureViewport.svelte.d.ts +3 -1
  187. package/dist/structure/atom-properties.d.ts +1 -1
  188. package/dist/structure/atom-properties.js +4 -3
  189. package/dist/structure/bond-order-perception.js +3 -8
  190. package/dist/structure/bonding.d.ts +0 -1
  191. package/dist/structure/bonding.js +16 -23
  192. package/dist/structure/export.js +50 -74
  193. package/dist/structure/format-detect.js +5 -7
  194. package/dist/structure/index.js +6 -12
  195. package/dist/structure/parse.js +38 -100
  196. package/dist/structure/partial-occupancy.js +7 -4
  197. package/dist/structure/pbc.js +8 -10
  198. package/dist/structure/polyhedra.js +5 -4
  199. package/dist/structure/prop-groups.d.ts +85 -0
  200. package/dist/structure/prop-groups.js +18 -0
  201. package/dist/symmetry/SymmetryStats.svelte +0 -4
  202. package/dist/symmetry/WyckoffTable.svelte +5 -4
  203. package/dist/symmetry/cell-transform.js +5 -14
  204. package/dist/symmetry/index.js +6 -9
  205. package/dist/table/HeatmapTable.svelte +61 -103
  206. package/dist/table/ToggleMenu.svelte +166 -44
  207. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  208. package/dist/theme/embedded.d.ts +18 -0
  209. package/dist/theme/embedded.js +206 -0
  210. package/dist/theme/index.d.ts +4 -0
  211. package/dist/theme/index.js +1 -2
  212. package/dist/theme/themes.mjs +31 -60
  213. package/dist/tooltip/TooltipContent.svelte +1 -1
  214. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  215. package/dist/tooltip/index.d.ts +8 -1
  216. package/dist/trajectory/Trajectory.svelte +140 -120
  217. package/dist/trajectory/TrajectoryError.svelte +1 -66
  218. package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
  219. package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
  220. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  221. package/dist/trajectory/extract.js +19 -29
  222. package/dist/trajectory/format-detect.d.ts +2 -0
  223. package/dist/trajectory/format-detect.js +7 -5
  224. package/dist/trajectory/frame-reader.js +3 -4
  225. package/dist/trajectory/helpers.js +16 -49
  226. package/dist/trajectory/index.js +2 -8
  227. package/dist/trajectory/parse/ase.js +1 -1
  228. package/dist/trajectory/parse/index.d.ts +4 -3
  229. package/dist/trajectory/parse/index.js +12 -17
  230. package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
  231. package/dist/trajectory/parse/vaspout-h5.js +1 -4
  232. package/dist/trajectory/parse/xyz.js +48 -2
  233. package/dist/trajectory/plotting.js +14 -37
  234. package/dist/xrd/XrdPlot.svelte +43 -68
  235. package/dist/xrd/calc-xrd.js +22 -58
  236. package/dist/xrd/parse.js +13 -22
  237. package/package.json +40 -8
  238. package/readme.md +6 -5
  239. package/dist/scene/types.d.ts +0 -26
  240. package/dist/tooltip/types.d.ts +0 -8
  241. package/dist/trajectory/constants.d.ts +0 -6
  242. package/dist/trajectory/constants.js +0 -11
  243. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  244. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -327,11 +327,10 @@
327
327
 
328
328
  // Normalize export_data config
329
329
  type ExportFormat = `csv` | `json`
330
- const default_formats: ExportFormat[] = [`csv`, `json`]
331
330
  let export_config = $derived(
332
331
  export_data
333
332
  ? {
334
- formats: default_formats,
333
+ formats: [`csv`, `json`] as ExportFormat[],
335
334
  filename: `table-export`,
336
335
  ...(typeof export_data === `object` ? export_data : {}),
337
336
  }
@@ -381,9 +380,7 @@
381
380
  ] as const
382
381
 
383
382
  // Columns that have a color gradient
384
- let colored_columns = $derived(
385
- columns.filter((col) => col.color_scale !== null && col.color_scale !== undefined),
386
- )
383
+ let colored_columns = $derived(columns.filter((col) => col.color_scale != null))
387
384
 
388
385
  // Column resize state
389
386
  let resize_col_id = $state<string | null>(null)
@@ -748,22 +745,19 @@
748
745
  // Skip client-side sorting when using async onsort callback or sort_data is false
749
746
  if (onsort || !sort_data) return filtered_data
750
747
 
751
- if (!sort_state.column && multi_sort.length === 0) return filtered_data
752
-
753
- // Build sort criteria: multi_sort takes precedence, fallback to single sort
748
+ // Build sort criteria: multi_sort (Shift+click) takes precedence over single sort
754
749
  const sort_criteria =
755
750
  multi_sort.length > 0 ? multi_sort : sort_state.column ? [sort_state] : []
756
-
757
751
  if (sort_criteria.length === 0) return filtered_data
758
752
 
753
+ const valid_column_ids = new Set(ordered_columns.map(get_col_id))
759
754
  return [...filtered_data].sort((row1, row2) => {
760
755
  for (const { column, ascending } of sort_criteria) {
761
- const matched_col = ordered_columns.find((col) => get_col_id(col) === column)
762
- if (!matched_col) continue
756
+ // criteria hold column IDs; skip stale entries referencing removed columns
757
+ if (!valid_column_ids.has(column)) continue
763
758
 
764
- const col_id = get_col_id(matched_col)
765
- const val1 = row1[col_id]
766
- const val2 = row2[col_id]
759
+ const val1 = row1[column]
760
+ const val2 = row2[column]
767
761
 
768
762
  if (val1 === val2) continue
769
763
 
@@ -934,13 +928,7 @@
934
928
  }
935
929
  } else {
936
930
  // Add to multi-sort
937
- multi_sort = [
938
- ...multi_sort,
939
- {
940
- column: col_id,
941
- ascending: col.better === `lower`,
942
- },
943
- ]
931
+ multi_sort = [...multi_sort, { column: col_id, ascending: col.better === `lower` }]
944
932
  }
945
933
  // Clear single sort when using multi-sort
946
934
  sort = { column: ``, dir: `asc` }
@@ -1047,25 +1035,23 @@
1047
1035
  visible_columns.length + (show_row_select ? 1 : 0) + (show_row_numbers ? 1 : 0),
1048
1036
  )
1049
1037
 
1038
+ // Arrow (plus a numbered badge under multi-sort) for actively sorted columns
1050
1039
  const sort_indicator = (col: Label, current_sort_state: SortState) => {
1051
- const hide_sort_indicator =
1052
- col.show_sort_indicator === false || col.style?.includes(`--hide-sort-indicator`)
1053
- if (hide_sort_indicator) return ``
1054
-
1040
+ if (col.show_sort_indicator === false || col.style?.includes(`--hide-sort-indicator`)) {
1041
+ return ``
1042
+ }
1055
1043
  const col_id = get_col_id(col)
1056
-
1057
- // Check multi-sort first
1058
1044
  const multi_idx = multi_sort.findIndex((sort_entry) => sort_entry.column === col_id)
1059
- if (multi_idx !== -1) {
1060
- const arrow = multi_sort[multi_idx].ascending ? `↓` : `↑`
1061
- const badge = multi_sort.length > 1 ? `<sup>${multi_idx + 1}</sup>` : ``
1062
- return `<span style="font-size: 0.8em;">${arrow}${badge}</span>`
1063
- }
1064
-
1065
- // Show indicator only for actively sorted columns.
1066
- if (current_sort_state.column !== col_id) return ``
1067
- const arrow = current_sort_state.ascending ? `↓` : `↑`
1068
- return `<span style="font-size: 0.8em;">${arrow}</span>`
1045
+ const active =
1046
+ multi_idx !== -1
1047
+ ? multi_sort[multi_idx]
1048
+ : current_sort_state.column === col_id
1049
+ ? current_sort_state
1050
+ : null
1051
+ if (!active) return ``
1052
+ const badge =
1053
+ multi_idx !== -1 && multi_sort.length > 1 ? `<sup>${multi_idx + 1}</sup>` : ``
1054
+ return `<span style="font-size: 0.8em;">${active.ascending ? `↓` : `↑`}${badge}</span>`
1069
1055
  }
1070
1056
 
1071
1057
  // Context menu state for column right-click (headers and body cells)
@@ -1200,7 +1186,7 @@
1200
1186
  // Raw cell value as clipboard text (numbers keep full precision, dates go
1201
1187
  // ISO, HTML cells lose their markup)
1202
1188
  const cell_copy_text = (val: CellVal): string => {
1203
- if (val == null || (typeof val === `number` && Number.isNaN(val))) return ``
1189
+ if (is_invalid(val)) return ``
1204
1190
  if (val instanceof Date) return val.toISOString()
1205
1191
  if (typeof val === `object`) return JSON.stringify(val)
1206
1192
  return strip_html(String(val)).trim()
@@ -1367,6 +1353,11 @@
1367
1353
  download(JSON.stringify(rows, null, 2), `${filename}.json`, `application/json`)
1368
1354
  }
1369
1355
 
1356
+ const export_actions = [
1357
+ [`csv`, export_csv],
1358
+ [`json`, export_json],
1359
+ ] as const
1360
+
1370
1361
  function copy_to_clipboard() {
1371
1362
  navigator.clipboard.writeText(serialize_table(`\t`))
1372
1363
  }
@@ -1532,28 +1523,20 @@
1532
1523
  </button>
1533
1524
  {#if show_export_dropdown}
1534
1525
  <div class="dropdown-pane">
1535
- {#if export_config.formats.includes(`csv`)}
1536
- <button
1537
- class="dropdown-option"
1538
- onclick={() => {
1539
- export_csv(export_config.filename)
1540
- show_export_dropdown = false
1541
- }}
1542
- >
1543
- <Icon icon="Download" style="width: 12px" /> CSV
1544
- </button>
1545
- {/if}
1546
- {#if export_config.formats.includes(`json`)}
1547
- <button
1548
- class="dropdown-option"
1549
- onclick={() => {
1550
- export_json(export_config.filename)
1551
- show_export_dropdown = false
1552
- }}
1553
- >
1554
- <Icon icon="Download" style="width: 12px" /> JSON
1555
- </button>
1556
- {/if}
1526
+ {#each export_actions as [format, export_fn] (format)}
1527
+ {#if export_config.formats.includes(format)}
1528
+ <button
1529
+ class="dropdown-option"
1530
+ onclick={() => {
1531
+ export_fn(export_config.filename)
1532
+ show_export_dropdown = false
1533
+ }}
1534
+ >
1535
+ <Icon icon="Download" style="width: 12px" />
1536
+ {format.toUpperCase()}
1537
+ </button>
1538
+ {/if}
1539
+ {/each}
1557
1540
  <button
1558
1541
  class="dropdown-option"
1559
1542
  onclick={() => {
@@ -1710,12 +1693,13 @@
1710
1693
  {#if !col.group}
1711
1694
  <th class:sticky-col={col.sticky}></th>
1712
1695
  {:else}
1713
- {@const group_cols = visible_columns.filter(
1714
- (column) => column.group === col.group,
1715
- )}
1716
- <!-- Only render the group header once for each group by checking if this is the first column of this group -->
1717
- {#if visible_columns.findIndex((column) => column.group === col.group) === visible_columns.findIndex((column) => column.group === col.group && column.label === col.label)}
1718
- <th title={col.description} colspan={group_cols.length}>
1696
+ <!-- Only render the group header once per group (on its first column) -->
1697
+ {#if visible_columns.find((column) => column.group === col.group) === col}
1698
+ <th
1699
+ title={col.description}
1700
+ colspan={visible_columns.filter((column) => column.group === col.group)
1701
+ .length}
1702
+ >
1719
1703
  {@html sanitize_html(col.group)}
1720
1704
  </th>
1721
1705
  {/if}
@@ -1967,7 +1951,7 @@
1967
1951
  {date_val}
1968
1952
  {:else if typeof val === `number` && !Number.isNaN(val)}
1969
1953
  {format_num(val, col.format ?? default_num_format)}
1970
- {:else if val === undefined || val === null || Number.isNaN(val)}
1954
+ {:else if is_invalid(val)}
1971
1955
  <span {@attach tooltip({ content: `Not available` })}> n/a </span>
1972
1956
  {:else}
1973
1957
  {@html sanitize_html(val)}
@@ -2000,24 +1984,16 @@
2000
1984
  </div>
2001
1985
  {/if}
2002
1986
 
1987
+ {#snippet page_btn(label: string, title: string, target_page: number, disabled: boolean)}
1988
+ <button class="page-btn" {disabled} onclick={() => (current_page = target_page)} {title}>
1989
+ {label}
1990
+ </button>
1991
+ {/snippet}
1992
+
2003
1993
  {#if pagination_config && total_pages > 1}
2004
1994
  <div class="pagination">
2005
- <button
2006
- class="page-btn"
2007
- disabled={current_page === 1}
2008
- onclick={() => (current_page = 1)}
2009
- title="First page"
2010
- >
2011
- «
2012
- </button>
2013
- <button
2014
- class="page-btn"
2015
- disabled={current_page === 1}
2016
- onclick={() => current_page--}
2017
- title="Previous page"
2018
- >
2019
-
2020
- </button>
1995
+ {@render page_btn(`«`, `First page`, 1, current_page === 1)}
1996
+ {@render page_btn(`‹`, `Previous page`, current_page - 1, current_page === 1)}
2021
1997
  <span class="page-info">
2022
1998
  Page
2023
1999
  <input
@@ -2035,22 +2011,8 @@
2035
2011
  of {total_pages}
2036
2012
  <span class="row-count">({sorted_data.length} rows)</span>
2037
2013
  </span>
2038
- <button
2039
- class="page-btn"
2040
- disabled={current_page === total_pages}
2041
- onclick={() => current_page++}
2042
- title="Next page"
2043
- >
2044
-
2045
- </button>
2046
- <button
2047
- class="page-btn"
2048
- disabled={current_page === total_pages}
2049
- onclick={() => (current_page = total_pages)}
2050
- title="Last page"
2051
- >
2052
- »
2053
- </button>
2014
+ {@render page_btn(`›`, `Next page`, current_page + 1, current_page === total_pages)}
2015
+ {@render page_btn(`»`, `Last page`, total_pages, current_page === total_pages)}
2054
2016
  {#if pagination_config.page_sizes}
2055
2017
  <select
2056
2018
  class="page-size-select"
@@ -2450,10 +2412,6 @@
2450
2412
  vertical-align: middle;
2451
2413
  padding: 2px !important;
2452
2414
  }
2453
- .select-col :global(svg) {
2454
- display: block;
2455
- margin: auto;
2456
- }
2457
2415
  tr.selected {
2458
2416
  background: var(--highlight-bg, rgba(74, 158, 255, 0.15)) !important;
2459
2417
  }
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import Icon from '../Icon.svelte'
3
+ import { portal } from '../overlays/portal'
3
4
  import { sanitize_html } from '../sanitize'
4
- import type { Label } from './'
5
+ import { type Label, strip_html } from './'
5
6
  import { click_outside, tooltip } from 'svelte-multiselect/attachments'
6
7
  import { slide } from 'svelte/transition'
7
8
 
@@ -14,7 +15,7 @@
14
15
  }: {
15
16
  columns: Label[]
16
17
  column_panel_open?: boolean
17
- // Number of grid columns for toggle layout
18
+ // Maximum number of grid columns for toggle layout
18
19
  n_columns?: number
19
20
  collapsed_sections?: string[]
20
21
  // Called after reset with the section name (or undefined for global reset)
@@ -22,6 +23,24 @@
22
23
  } = $props()
23
24
 
24
25
  const col_id = (col: Label) => col.key ?? col.label
26
+ const toggle_menu_id = $props.id()
27
+ const dropdown_selector = `[data-toggle-menu-id="${toggle_menu_id}"]`
28
+ const COLUMN_FILTER_THRESHOLD = 20
29
+ const DEFAULT_MAX_MENU_COLUMNS = 3
30
+ const PREFERRED_MAX_MENU_ROWS = 10
31
+ let column_filter = $state(``)
32
+ let show_column_filter = $derived(columns.length > COLUMN_FILTER_THRESHOLD)
33
+ let normalized_column_filter = $derived(
34
+ show_column_filter ? column_filter.trim().toLowerCase() : ``,
35
+ )
36
+ const column_matches_filter = (col: Label): boolean =>
37
+ !normalized_column_filter ||
38
+ [col.key, strip_html(col.label), col.description, col.group]
39
+ .filter(Boolean)
40
+ .join(` `)
41
+ .toLowerCase()
42
+ .includes(normalized_column_filter)
43
+ let filtered_columns = $derived(columns.filter(column_matches_filter))
25
44
 
26
45
  // Snapshot default visibility when column set changes (new dataset).
27
46
  // Compare by keys and visibility defaults. Internal updates opt out below.
@@ -96,6 +115,14 @@
96
115
  if (ungrouped.length > 0) result.push({ name: ``, items: ungrouped })
97
116
  return result
98
117
  })
118
+ let filtered_sections = $derived(
119
+ sections
120
+ .map((section) => ({
121
+ ...section,
122
+ items: section.items.filter(column_matches_filter),
123
+ }))
124
+ .filter((section) => section.items.length > 0),
125
+ )
99
126
 
100
127
  // Check if any column defines a group (to decide whether to show sections)
101
128
  let has_sections = $derived(columns.some((col) => col.group))
@@ -113,31 +140,56 @@
113
140
  columns = [...columns] // trigger reactivity on parent binding
114
141
  }
115
142
 
116
- // Grid template for section items
117
- let grid_template = $derived(
118
- n_columns ? `repeat(${n_columns}, max-content)` : `repeat(auto-fill, minmax(135px, 1fr))`,
119
- )
143
+ // Prefer two tall columns, adding more only when the item count would make them unwieldy.
144
+ // n_columns caps large menus rather than forcing sparse menus to fill every column.
145
+ const grid_column_count = (item_count: number): number => {
146
+ const preferred_column_count =
147
+ item_count <= 1 ? 1 : Math.max(2, Math.ceil(item_count / PREFERRED_MAX_MENU_ROWS))
148
+ return Math.min(Math.max(1, n_columns ?? DEFAULT_MAX_MENU_COLUMNS), preferred_column_count)
149
+ }
150
+ const grid_template = (item_count: number): string =>
151
+ `repeat(${grid_column_count(item_count)}, max-content)`
120
152
 
121
- // Reposition dropdown: left-aligned by default, switch to right if it overflows viewport
122
- let details_el: HTMLElement | undefined = undefined
153
+ // Portal the dropdown to <body> so ancestor overflow/stacking contexts cannot clip it.
154
+ const dropdown_target = typeof document === `undefined` ? undefined : document.body
155
+ let details_el = $state<HTMLElement>()
156
+ let dropdown_el = $state<HTMLElement>()
157
+ const position_dropdown = (): void => {
158
+ const trigger = details_el?.querySelector(`summary`)
159
+ if (!column_panel_open || !trigger || !dropdown_el) return
160
+ const trigger_rect = trigger.getBoundingClientRect()
161
+ const dropdown_rect = dropdown_el.getBoundingClientRect()
162
+ const viewport_padding = 8
163
+ const gap = 4
164
+ const max_left = Math.max(
165
+ viewport_padding,
166
+ globalThis.innerWidth - dropdown_rect.width - viewport_padding,
167
+ )
168
+ const left = Math.min(
169
+ Math.max(viewport_padding, trigger_rect.right - dropdown_rect.width),
170
+ max_left,
171
+ )
172
+ const below = trigger_rect.bottom + gap
173
+ const top =
174
+ below + dropdown_rect.height <= globalThis.innerHeight - viewport_padding
175
+ ? below
176
+ : Math.max(viewport_padding, trigger_rect.top - dropdown_rect.height - gap)
177
+ Object.assign(dropdown_el.style, {
178
+ left: `${left}px`,
179
+ right: `auto`,
180
+ top: `${top}px`,
181
+ visibility: `visible`,
182
+ })
183
+ }
123
184
  $effect(() => {
124
- if (!column_panel_open || !details_el) return
185
+ if (!column_panel_open || !details_el || !dropdown_el) return
125
186
  // Re-run when section state changes while open
126
187
  void n_columns
127
188
  void collapsed_sections
128
- void sections
129
- const dropdown = details_el.querySelector<HTMLElement>(`.column-menu, .sections-container`)
130
- if (!dropdown) return
131
- // Reset to left-aligned
132
- dropdown.style.left = `0`
133
- dropdown.style.right = `auto`
134
- requestAnimationFrame(() => {
135
- const rect = dropdown.getBoundingClientRect()
136
- if (rect.right > window.innerWidth) {
137
- dropdown.style.left = `auto`
138
- dropdown.style.right = `0`
139
- }
140
- })
189
+ void filtered_columns
190
+ void filtered_sections
191
+ const frame = requestAnimationFrame(position_dropdown)
192
+ return () => cancelAnimationFrame(frame)
141
193
  })
142
194
  </script>
143
195
 
@@ -169,10 +221,19 @@
169
221
  <details
170
222
  class="column-toggles"
171
223
  bind:this={details_el}
172
- bind:open={column_panel_open}
173
- {@attach click_outside({ callback: () => (column_panel_open = false) })}
224
+ open={column_panel_open}
225
+ {@attach click_outside({
226
+ callback: () => (column_panel_open = false),
227
+ exclude: [dropdown_selector],
228
+ })}
174
229
  >
175
- <summary aria-expanded={column_panel_open}>
230
+ <summary
231
+ aria-expanded={column_panel_open}
232
+ onclick={(event) => {
233
+ event.preventDefault()
234
+ column_panel_open = !column_panel_open
235
+ }}
236
+ >
176
237
  Columns <Icon icon="Columns" />
177
238
  {#if has_any_changes}
178
239
  <button
@@ -191,11 +252,32 @@
191
252
  {/if}
192
253
  </summary>
193
254
 
194
- {#if has_sections}
195
- <div class="sections-container" role="group">
196
- {#each sections as section (section.name)}
255
+ <div
256
+ bind:this={dropdown_el}
257
+ class={has_sections ? `sections-container` : `column-menu`}
258
+ data-toggle-menu-id={toggle_menu_id}
259
+ hidden={!column_panel_open}
260
+ role="group"
261
+ style:grid-template-columns={has_sections
262
+ ? undefined
263
+ : grid_template(filtered_columns.length)}
264
+ {@attach portal(dropdown_target)}
265
+ >
266
+ {#if show_column_filter}
267
+ <input
268
+ aria-label="Filter columns"
269
+ bind:value={column_filter}
270
+ class="column-filter"
271
+ placeholder="Filter columns…"
272
+ type="search"
273
+ />
274
+ {/if}
275
+ {#if has_sections}
276
+ {#each filtered_sections as section (section.name)}
197
277
  {@const is_collapsed =
198
- section.name !== `` && collapsed_sections.includes(section.name)}
278
+ !normalized_column_filter &&
279
+ section.name !== `` &&
280
+ collapsed_sections.includes(section.name)}
199
281
  <div class="section">
200
282
  {#if section.name}
201
283
  <div class="section-header-row">
@@ -224,7 +306,7 @@
224
306
  {#if !is_collapsed}
225
307
  <div
226
308
  class="section-items"
227
- style:grid-template-columns={grid_template}
309
+ style:grid-template-columns={grid_template(section.items.length)}
228
310
  transition:slide={{ duration: 200 }}
229
311
  >
230
312
  {#each section.items as col, idx (col.key ?? col.label ?? idx)}
@@ -234,14 +316,15 @@
234
316
  {/if}
235
317
  </div>
236
318
  {/each}
237
- </div>
238
- {:else}
239
- <div class="column-menu" role="group" style:grid-template-columns={grid_template}>
240
- {#each columns as col, idx (col.key ?? col.label ?? idx)}
319
+ {:else}
320
+ {#each filtered_columns as col, idx (col.key ?? col.label ?? idx)}
241
321
  {@render toggle_item(col)}
242
322
  {/each}
243
- </div>
244
- {/if}
323
+ {/if}
324
+ {#if filtered_columns.length === 0}
325
+ <span class="no-matching-columns">No matching columns</span>
326
+ {/if}
327
+ </div>
245
328
  </details>
246
329
 
247
330
  <style>
@@ -268,9 +351,12 @@
268
351
  .column-menu,
269
352
  .sections-container {
270
353
  font-size: var(--tgl-font-size, 1.1em);
271
- position: absolute;
354
+ position: fixed;
272
355
  left: 0;
273
- top: calc(100% + 1pt);
356
+ top: 0;
357
+ visibility: hidden;
358
+ box-sizing: border-box;
359
+ width: max-content;
274
360
  background: var(--tgl-dropdown-bg, var(--page-bg));
275
361
  border: 1px solid
276
362
  var(--tgl-dropdown-border, color-mix(in srgb, currentColor 10%, transparent));
@@ -280,13 +366,18 @@
280
366
  0 4px 12px color-mix(in srgb, currentColor 8%, transparent)
281
367
  );
282
368
  min-width: 150px;
369
+ max-width: calc(100vw - 16px);
283
370
  max-height: var(--tgl-dropdown-max-height, min(70vh, 600px));
284
- overflow-y: auto;
285
- z-index: 1;
371
+ overflow: auto;
372
+ z-index: var(--tgl-dropdown-z-index, 10000);
373
+ }
374
+ .column-menu,
375
+ .section-items {
376
+ display: grid;
377
+ column-gap: var(--tgl-column-gap, 8px);
286
378
  }
287
379
  .column-menu {
288
380
  padding: 3pt 5pt;
289
- display: grid;
290
381
  }
291
382
  .sections-container {
292
383
  padding: 6pt 8pt;
@@ -294,6 +385,40 @@
294
385
  flex-direction: column;
295
386
  gap: 8px;
296
387
  }
388
+ .column-menu[hidden],
389
+ .sections-container[hidden] {
390
+ display: none;
391
+ }
392
+ .column-filter {
393
+ position: sticky;
394
+ z-index: 2;
395
+ top: 0;
396
+ grid-column: 1 / -1;
397
+ box-sizing: border-box;
398
+ width: 100%;
399
+ height: 1.35rem;
400
+ min-height: 0;
401
+ margin: 0 0 4px;
402
+ padding: 0 0.35rem;
403
+ border: 1px solid
404
+ var(--tgl-dropdown-border, color-mix(in srgb, currentColor 18%, transparent));
405
+ border-radius: var(--tgl-border-radius, 4pt);
406
+ outline: none;
407
+ background: var(--tgl-dropdown-bg, var(--page-bg));
408
+ color: inherit;
409
+ font: inherit;
410
+ font-size: 0.72rem;
411
+ line-height: 1.2;
412
+ &:focus {
413
+ border-color: var(--active-color, #6ea8ff);
414
+ }
415
+ }
416
+ .no-matching-columns {
417
+ grid-column: 1 / -1;
418
+ padding: 4px;
419
+ color: color-mix(in srgb, currentColor 65%, transparent);
420
+ font-size: 0.9em;
421
+ }
297
422
  .reset-btn {
298
423
  display: flex;
299
424
  align-items: center;
@@ -348,9 +473,6 @@
348
473
  width: 1em;
349
474
  flex-shrink: 0;
350
475
  }
351
- .section-items {
352
- display: grid;
353
- }
354
476
  .toggle-label {
355
477
  display: inline-block;
356
478
  margin: 1px 2px;
@@ -1,4 +1,4 @@
1
- import type { Label } from './';
1
+ import { type Label } from './';
2
2
  type $$ComponentProps = {
3
3
  columns: Label[];
4
4
  column_panel_open?: boolean;
@@ -0,0 +1,18 @@
1
+ import { type ThemeType } from './';
2
+ import './themes.mjs';
3
+ declare global {
4
+ var jupyterlab: {
5
+ application?: {
6
+ shell?: {
7
+ dataset?: {
8
+ theme?: string;
9
+ };
10
+ };
11
+ };
12
+ } | undefined;
13
+ }
14
+ type ThemeCallback = (theme_type: ThemeType) => void;
15
+ export declare function detect_parent_theme(target_element?: HTMLElement): ThemeType;
16
+ export declare function watch_theme(target_element: HTMLElement, callback: ThemeCallback): () => void;
17
+ export declare function get_theme_css(theme_type: ThemeType, is_shadow_dom?: boolean): string;
18
+ export {};