matterviz 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/MillerIndexInput.svelte +60 -0
  4. package/dist/MillerIndexInput.svelte.d.ts +7 -0
  5. package/dist/app.css +38 -2
  6. package/dist/brillouin/BrillouinZone.svelte +20 -62
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  9. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  10. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  14. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  16. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  17. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  18. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  19. package/dist/chempot-diagram/color.d.ts +10 -0
  20. package/dist/chempot-diagram/color.js +33 -0
  21. package/dist/chempot-diagram/compute.d.ts +38 -0
  22. package/dist/chempot-diagram/compute.js +650 -0
  23. package/dist/chempot-diagram/index.d.ts +5 -0
  24. package/dist/chempot-diagram/index.js +5 -0
  25. package/dist/chempot-diagram/pointer.d.ts +16 -0
  26. package/dist/chempot-diagram/pointer.js +40 -0
  27. package/dist/chempot-diagram/temperature.d.ts +15 -0
  28. package/dist/chempot-diagram/temperature.js +37 -0
  29. package/dist/chempot-diagram/types.d.ts +83 -0
  30. package/dist/chempot-diagram/types.js +27 -0
  31. package/dist/colors/index.d.ts +3 -1
  32. package/dist/colors/index.js +4 -0
  33. package/dist/composition/BarChart.svelte +13 -22
  34. package/dist/composition/BubbleChart.svelte +5 -3
  35. package/dist/composition/FormulaFilter.svelte +770 -90
  36. package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
  37. package/dist/composition/PieChart.svelte +43 -18
  38. package/dist/composition/PieChart.svelte.d.ts +1 -1
  39. package/dist/constants.d.ts +1 -0
  40. package/dist/constants.js +2 -0
  41. package/dist/convex-hull/ConvexHull.svelte +14 -1
  42. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull2D.svelte +14 -45
  44. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHull3D.svelte +396 -134
  46. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  47. package/dist/convex-hull/ConvexHull4D.svelte +93 -42
  48. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHullControls.svelte +94 -31
  50. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
  51. package/dist/convex-hull/ConvexHullStats.svelte +697 -128
  52. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  53. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  54. package/dist/convex-hull/GasPressureControls.svelte +72 -38
  55. package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
  56. package/dist/convex-hull/TemperatureSlider.svelte +46 -19
  57. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
  58. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  59. package/dist/convex-hull/demo-temperature.js +36 -0
  60. package/dist/convex-hull/gas-thermodynamics.js +16 -5
  61. package/dist/convex-hull/helpers.d.ts +7 -1
  62. package/dist/convex-hull/helpers.js +45 -15
  63. package/dist/convex-hull/index.d.ts +15 -1
  64. package/dist/convex-hull/index.js +1 -0
  65. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  66. package/dist/convex-hull/thermodynamics.js +106 -17
  67. package/dist/convex-hull/types.d.ts +7 -0
  68. package/dist/convex-hull/types.js +11 -0
  69. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  70. package/dist/element/BohrAtom.svelte +1 -1
  71. package/dist/element/data.js +2 -14
  72. package/dist/element/data.json.gz +0 -0
  73. package/dist/element/index.d.ts +1 -1
  74. package/dist/element/index.js +1 -0
  75. package/dist/element/types.d.ts +1 -0
  76. package/dist/fermi-surface/FermiSurface.svelte +21 -65
  77. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  78. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  79. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  80. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  81. package/dist/fermi-surface/compute.js +1 -21
  82. package/dist/fermi-surface/marching-cubes.d.ts +2 -13
  83. package/dist/fermi-surface/marching-cubes.js +2 -519
  84. package/dist/fermi-surface/parse.js +17 -23
  85. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  86. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  87. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  88. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  89. package/dist/heatmap-matrix/index.d.ts +53 -0
  90. package/dist/heatmap-matrix/index.js +100 -0
  91. package/dist/heatmap-matrix/shared.d.ts +2 -0
  92. package/dist/heatmap-matrix/shared.js +4 -0
  93. package/dist/icons.d.ts +119 -0
  94. package/dist/icons.js +119 -0
  95. package/dist/index.d.ts +6 -1
  96. package/dist/index.js +6 -1
  97. package/dist/io/export.js +15 -3
  98. package/dist/io/file-drop.d.ts +7 -0
  99. package/dist/io/file-drop.js +43 -0
  100. package/dist/io/index.d.ts +2 -2
  101. package/dist/io/index.js +2 -112
  102. package/dist/io/types.d.ts +1 -0
  103. package/dist/io/url-drop.d.ts +2 -0
  104. package/dist/io/url-drop.js +118 -0
  105. package/dist/isosurface/Isosurface.svelte +231 -0
  106. package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
  107. package/dist/isosurface/IsosurfaceControls.svelte +273 -0
  108. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
  109. package/dist/isosurface/index.d.ts +5 -0
  110. package/dist/isosurface/index.js +6 -0
  111. package/dist/isosurface/parse.d.ts +6 -0
  112. package/dist/isosurface/parse.js +548 -0
  113. package/dist/isosurface/slice.d.ts +11 -0
  114. package/dist/isosurface/slice.js +145 -0
  115. package/dist/isosurface/types.d.ts +55 -0
  116. package/dist/isosurface/types.js +178 -0
  117. package/dist/labels.d.ts +2 -1
  118. package/dist/labels.js +1 -0
  119. package/dist/layout/InfoTag.svelte +62 -62
  120. package/dist/layout/SubpageGrid.svelte +74 -0
  121. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  122. package/dist/layout/index.d.ts +1 -0
  123. package/dist/layout/index.js +1 -0
  124. package/dist/layout/json-tree/JsonNode.svelte +226 -53
  125. package/dist/layout/json-tree/JsonTree.svelte +425 -51
  126. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  127. package/dist/layout/json-tree/JsonValue.svelte +218 -97
  128. package/dist/layout/json-tree/types.d.ts +27 -2
  129. package/dist/layout/json-tree/utils.d.ts +14 -1
  130. package/dist/layout/json-tree/utils.js +254 -0
  131. package/dist/marching-cubes.d.ts +14 -0
  132. package/dist/marching-cubes.js +519 -0
  133. package/dist/math.d.ts +8 -0
  134. package/dist/math.js +374 -7
  135. package/dist/overlays/ContextMenu.svelte +3 -2
  136. package/dist/overlays/DraggablePane.svelte +163 -58
  137. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  138. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  139. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  140. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  141. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  142. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  143. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  144. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  145. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  146. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  147. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  148. package/dist/phase-diagram/index.d.ts +2 -0
  149. package/dist/phase-diagram/index.js +2 -0
  150. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  151. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  152. package/dist/phase-diagram/types.d.ts +10 -0
  153. package/dist/phase-diagram/utils.d.ts +7 -4
  154. package/dist/phase-diagram/utils.js +149 -59
  155. package/dist/plot/AxisLabel.svelte +26 -0
  156. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  157. package/dist/plot/BarPlot.svelte +473 -228
  158. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  159. package/dist/plot/BarPlotControls.svelte +3 -2
  160. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  161. package/dist/plot/ColorBar.svelte +54 -54
  162. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  163. package/dist/plot/ElementScatter.svelte +4 -3
  164. package/dist/plot/FillArea.svelte +4 -1
  165. package/dist/plot/Histogram.svelte +320 -230
  166. package/dist/plot/Histogram.svelte.d.ts +2 -2
  167. package/dist/plot/HistogramControls.svelte +29 -10
  168. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  169. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  170. package/dist/plot/PlotControls.svelte +109 -27
  171. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  172. package/dist/plot/PlotLegend.svelte +1 -1
  173. package/dist/plot/PortalSelect.svelte +2 -1
  174. package/dist/plot/ReferenceLine.svelte +2 -1
  175. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  176. package/dist/plot/ReferencePlane.svelte +1 -3
  177. package/dist/plot/ScatterPlot.svelte +343 -209
  178. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  179. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  180. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  181. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  182. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  183. package/dist/plot/ScatterPlotControls.svelte +95 -55
  184. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  185. package/dist/plot/ZeroLines.svelte +44 -0
  186. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  187. package/dist/plot/ZoomRect.svelte +21 -0
  188. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  189. package/dist/plot/axis-utils.d.ts +1 -1
  190. package/dist/plot/data-cleaning.js +1 -5
  191. package/dist/plot/index.d.ts +6 -2
  192. package/dist/plot/index.js +6 -2
  193. package/dist/plot/interactions.d.ts +8 -10
  194. package/dist/plot/interactions.js +10 -19
  195. package/dist/plot/layout.d.ts +7 -1
  196. package/dist/plot/layout.js +12 -4
  197. package/dist/plot/reference-line.d.ts +4 -21
  198. package/dist/plot/reference-line.js +7 -81
  199. package/dist/plot/types.d.ts +42 -17
  200. package/dist/plot/types.js +10 -0
  201. package/dist/plot/utils/label-placement.js +14 -11
  202. package/dist/plot/utils.d.ts +1 -0
  203. package/dist/plot/utils.js +14 -0
  204. package/dist/rdf/RdfPlot.svelte +55 -66
  205. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  206. package/dist/rdf/index.d.ts +1 -1
  207. package/dist/rdf/index.js +1 -1
  208. package/dist/settings.d.ts +5 -0
  209. package/dist/settings.js +37 -3
  210. package/dist/spectral/Bands.svelte +515 -143
  211. package/dist/spectral/Bands.svelte.d.ts +22 -2
  212. package/dist/spectral/helpers.d.ts +23 -1
  213. package/dist/spectral/helpers.js +65 -9
  214. package/dist/spectral/types.d.ts +2 -0
  215. package/dist/structure/AtomLegend.svelte +31 -10
  216. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  217. package/dist/structure/CellSelect.svelte +92 -22
  218. package/dist/structure/Lattice.svelte +2 -0
  219. package/dist/structure/Structure.svelte +716 -173
  220. package/dist/structure/Structure.svelte.d.ts +7 -2
  221. package/dist/structure/StructureControls.svelte +26 -14
  222. package/dist/structure/StructureControls.svelte.d.ts +5 -1
  223. package/dist/structure/StructureInfoPane.svelte +7 -1
  224. package/dist/structure/StructureScene.svelte +386 -95
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -4
  226. package/dist/structure/atom-properties.d.ts +6 -2
  227. package/dist/structure/atom-properties.js +38 -25
  228. package/dist/structure/export.js +10 -7
  229. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  230. package/dist/structure/ferrox-wasm-types.js +0 -3
  231. package/dist/structure/ferrox-wasm.d.ts +3 -2
  232. package/dist/structure/ferrox-wasm.js +1 -2
  233. package/dist/structure/index.d.ts +7 -0
  234. package/dist/structure/index.js +22 -0
  235. package/dist/structure/parse.js +19 -16
  236. package/dist/structure/partial-occupancy.d.ts +25 -0
  237. package/dist/structure/partial-occupancy.js +102 -0
  238. package/dist/structure/validation.js +6 -3
  239. package/dist/symmetry/SymmetryStats.svelte +18 -4
  240. package/dist/symmetry/WyckoffTable.svelte +18 -10
  241. package/dist/symmetry/index.d.ts +7 -4
  242. package/dist/symmetry/index.js +83 -18
  243. package/dist/table/HeatmapTable.svelte +468 -69
  244. package/dist/table/HeatmapTable.svelte.d.ts +13 -1
  245. package/dist/table/ToggleMenu.svelte +291 -44
  246. package/dist/table/ToggleMenu.svelte.d.ts +4 -1
  247. package/dist/table/index.d.ts +3 -0
  248. package/dist/tooltip/index.d.ts +1 -1
  249. package/dist/tooltip/index.js +1 -0
  250. package/dist/trajectory/Trajectory.svelte +147 -145
  251. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  252. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  253. package/dist/trajectory/constants.d.ts +6 -0
  254. package/dist/trajectory/constants.js +7 -0
  255. package/dist/trajectory/extract.js +3 -5
  256. package/dist/trajectory/format-detect.d.ts +9 -0
  257. package/dist/trajectory/format-detect.js +76 -0
  258. package/dist/trajectory/frame-reader.d.ts +17 -0
  259. package/dist/trajectory/frame-reader.js +339 -0
  260. package/dist/trajectory/helpers.d.ts +15 -0
  261. package/dist/trajectory/helpers.js +187 -0
  262. package/dist/trajectory/index.d.ts +1 -0
  263. package/dist/trajectory/index.js +11 -4
  264. package/dist/trajectory/parse/ase.d.ts +2 -0
  265. package/dist/trajectory/parse/ase.js +76 -0
  266. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  267. package/dist/trajectory/parse/hdf5.js +121 -0
  268. package/dist/trajectory/parse/index.d.ts +12 -0
  269. package/dist/trajectory/parse/index.js +304 -0
  270. package/dist/trajectory/parse/lammps.d.ts +5 -0
  271. package/dist/trajectory/parse/lammps.js +169 -0
  272. package/dist/trajectory/parse/vasp.d.ts +2 -0
  273. package/dist/trajectory/parse/vasp.js +65 -0
  274. package/dist/trajectory/parse/xyz.d.ts +2 -0
  275. package/dist/trajectory/parse/xyz.js +109 -0
  276. package/dist/trajectory/types.d.ts +11 -0
  277. package/dist/trajectory/types.js +1 -0
  278. package/dist/utils.d.ts +2 -0
  279. package/dist/utils.js +4 -0
  280. package/dist/xrd/XrdPlot.svelte +6 -4
  281. package/dist/xrd/calc-xrd.js +0 -1
  282. package/package.json +33 -23
  283. package/readme.md +4 -4
  284. package/dist/trajectory/parse.d.ts +0 -42
  285. package/dist/trajectory/parse.js +0 -1267
  286. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -1,11 +1,31 @@
1
- <script lang="ts">import Icon from '../Icon.svelte';
1
+ <script lang="ts">import { luminance, watch_dark_mode } from '../colors';
2
+ import Icon from '../Icon.svelte';
2
3
  import { format_num } from '../labels';
4
+ import { SettingsSection } from '../layout';
5
+ import ContextMenu from '../overlays/ContextMenu.svelte';
6
+ import DraggablePane from '../overlays/DraggablePane.svelte';
3
7
  import { calc_cell_color, strip_html } from './';
8
+ import { normalize_unicode_minus } from '../utils';
4
9
  import { tooltip } from 'svelte-multiselect/attachments';
5
10
  import { flip } from 'svelte/animate';
6
11
  import { SvelteMap } from 'svelte/reactivity';
7
12
  let { data = $bindable([]), columns = [], sort_hint = undefined, cell, special_cells, controls, initial_sort = undefined, sort = $bindable({ column: ``, dir: `asc` }), // allows external control/sync of sorting
8
- fixed_header = false, default_num_format = `.3`, show_heatmap = $bindable(true), heatmap_class = `heatmap`, onrowdblclick, column_order = $bindable([]), export_data = false, show_column_toggle = false, search = false, show_row_select = false, pagination = false, selected_rows = $bindable([]), hidden_columns = $bindable([]), scroll_style, onsort = undefined, onsorterror = undefined, loading = $bindable(false), sort_data = true, ...rest } = $props();
13
+ fixed_header = false, default_num_format = `.3`, show_heatmap = $bindable(true), heatmap_class = `heatmap`, onrowclick, onrowdblclick, column_order = $bindable([]), export_data = false, show_column_toggle = false, search = false, show_row_select = false, pagination = false, selected_rows = $bindable([]), hidden_columns = $bindable([]), scroll_style, root_style, onsort = undefined, onsorterror = undefined, loading = $bindable(false), sort_data = true, heatmap_opacity = $bindable(1), empty_message = `No data`, show_row_numbers = false, allow_better_toggle = false, show_controls = $bindable(false), controls_open = $bindable(false), header_cell, footer, ...rest } = $props();
14
+ let container_el = $state();
15
+ // Read --page-bg from computed style for text contrast calculation.
16
+ // Recalculates on mount and when the theme changes (dark/light mode toggle).
17
+ let page_bg_lum = $state(luminance(`white`));
18
+ $effect(() => {
19
+ if (!container_el)
20
+ return;
21
+ const read_page_bg = () => {
22
+ const page_bg = getComputedStyle(container_el).getPropertyValue(`--page-bg`)
23
+ .trim();
24
+ page_bg_lum = luminance(page_bg || `white`);
25
+ };
26
+ read_page_bg();
27
+ return watch_dark_mode(read_page_bg);
28
+ });
9
29
  // Detect HTML to prevent setting raw HTML as data-sort-value. Simple string matching
10
30
  // suffices since false positives just skip setting the attr (sorting still works by inner data-sort-value).
11
31
  function is_html_str(val) {
@@ -27,6 +47,8 @@ let initial_sort_config = $derived(initial_sort
27
47
  let pagination_config = $derived(pagination
28
48
  ? { page_size: 25, ...(typeof pagination === `object` ? pagination : {}) }
29
49
  : null);
50
+ // Mutable page size — writable $derived allows user to change via dropdown
51
+ let effective_page_size = $derived(pagination_config?.page_size ?? 25);
30
52
  // Normalize search config
31
53
  let search_config = $derived(search
32
54
  ? {
@@ -61,11 +83,41 @@ let current_page = $state(1);
61
83
  // Dropdown states
62
84
  let show_column_dropdown = $state(false);
63
85
  let show_export_dropdown = $state(false);
86
+ // Per-column gradient direction overrides (user-toggled via header)
87
+ let better_overrides = new SvelteMap();
88
+ // Per-column color scale overrides
89
+ let color_scale_overrides = new SvelteMap();
90
+ const color_scale_options = [
91
+ `interpolateViridis`,
92
+ `interpolatePlasma`,
93
+ `interpolateInferno`,
94
+ `interpolateCividis`,
95
+ `interpolateTurbo`,
96
+ `interpolateBlues`,
97
+ `interpolateGreens`,
98
+ `interpolateReds`,
99
+ `interpolateYlOrRd`,
100
+ ];
101
+ // Columns that have a color gradient
102
+ let colored_columns = $derived(columns.filter((col) => col.color_scale !== null && col.color_scale !== undefined));
64
103
  // Column resize state
65
104
  let resize_col_id = $state(null);
66
105
  let resize_start_x = $state(0);
67
106
  let resize_start_width = $state(0);
68
107
  let column_widths = $state({});
108
+ // Auto-discover columns from data keys when none are provided
109
+ $effect.pre(() => {
110
+ if (columns.length > 0 || data.length === 0)
111
+ return;
112
+ const seen = {};
113
+ for (const row of data.slice(0, 50)) {
114
+ for (const key of Object.keys(row)) {
115
+ if (key !== `style` && key !== `class`)
116
+ seen[key] = true;
117
+ }
118
+ }
119
+ columns = Object.keys(seen).map((key) => ({ label: key }));
120
+ });
69
121
  // Helper to make column IDs (needed since column labels in different groups can be repeated)
70
122
  const get_col_id = (col) => col.group ? `${col.key ?? col.label} (${col.group})` : (col.key ?? col.label);
71
123
  // Sync column_order with columns: initialize if empty, remove stale IDs, append new IDs
@@ -97,13 +149,19 @@ let ordered_columns = $derived.by(() => {
97
149
  // Add columns in specified order, then any remaining columns that weren't in the order list
98
150
  const ordered = column_order
99
151
  .map((id) => col_map.get(id))
100
- .filter(Boolean);
152
+ .filter((col) => col != null);
101
153
  const ordered_ids = new Set(ordered.map(get_col_id));
102
154
  const remaining = columns.filter((col) => !ordered_ids.has(get_col_id(col)));
103
155
  return [...ordered, ...remaining];
104
156
  });
105
157
  let drag_col_id = $state(null);
106
158
  let drag_over_col_id = $state(null);
159
+ // Merge root_style with rest.style for root div; omit style from rest to avoid duplicate
160
+ let rest_props = $derived.by(() => {
161
+ const { style: rest_style, ...other_props } = rest;
162
+ const merged = [rest_style, root_style].filter(Boolean).join(`; `);
163
+ return { ...other_props, ...(merged ? { style: merged } : {}) };
164
+ });
107
165
  // WeakMap to assign stable unique IDs to row objects for efficient comparison and keying
108
166
  // This avoids O(n) JSON.stringify calls and prevents unnecessary re-renders
109
167
  const row_id_map = new WeakMap();
@@ -272,10 +330,10 @@ let sorted_data = $derived.by(() => {
272
330
  let paginated_data = $derived.by(() => {
273
331
  if (!pagination_config)
274
332
  return sorted_data;
275
- const start = (current_page - 1) * pagination_config.page_size;
276
- return sorted_data.slice(start, start + pagination_config.page_size);
333
+ const start = (current_page - 1) * effective_page_size;
334
+ return sorted_data.slice(start, start + effective_page_size);
277
335
  });
278
- let total_pages = $derived(Math.ceil(sorted_data.length / (pagination_config?.page_size ?? 25)));
336
+ let total_pages = $derived(Math.ceil(sorted_data.length / effective_page_size));
279
337
  // Track previous values to detect actual changes
280
338
  let prev_search_query = $state(``);
281
339
  let prev_data_length = $state(0);
@@ -290,6 +348,10 @@ $effect(() => {
290
348
  prev_search_query = search_query;
291
349
  prev_data_length = sorted_data.length;
292
350
  }
351
+ else if (total_pages > 0 && current_page > total_pages) {
352
+ // Clamp when total pages decreases (e.g., page size increase)
353
+ current_page = total_pages;
354
+ }
293
355
  });
294
356
  async function sort_rows(column, group, event) {
295
357
  // Find the column using both label and group if provided
@@ -375,14 +437,14 @@ function parse_numeric_val(val) {
375
437
  const error_match = val.match(/^([-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/);
376
438
  if (error_match) {
377
439
  // Normalize unicode minus (U+2212) to ASCII hyphen for Number()
378
- const normalized = error_match[1].replace(/−/g, `-`);
440
+ const normalized = normalize_unicode_minus(error_match[1]);
379
441
  const num = Number(normalized);
380
442
  if (!isNaN(num))
381
443
  return num;
382
444
  }
383
445
  // Try parsing as a plain number (handles "1.23" strings)
384
446
  // Also normalize unicode minus for plain numbers
385
- const normalized_val = val.replace(/−/g, `-`);
447
+ const normalized_val = normalize_unicode_minus(val);
386
448
  const plain_num = Number(normalized_val);
387
449
  if (!isNaN(plain_num) && val.trim() !== ``)
388
450
  return plain_num;
@@ -410,11 +472,25 @@ function calc_color(val, col) {
410
472
  const col_id = get_col_id(col);
411
473
  // Use memoized parsed values for the column
412
474
  const numeric_vals = parsed_column_values.get(col_id) ?? [];
413
- // calc_cell_color handles null/NaN filtering internally
414
- return calc_cell_color(numeric_val, numeric_vals, col.better, col.color_scale || `interpolateViridis`, col.scale_type || `linear`);
475
+ const better = better_overrides.get(col_id) ?? col.better;
476
+ const scale = (color_scale_overrides.get(col_id) ?? col.color_scale ??
477
+ `interpolateViridis`);
478
+ const color = calc_cell_color(numeric_val, numeric_vals, better, scale, col.scale_type || `linear`);
479
+ // Recompute text contrast against effective bg (cell bg blended with page bg by opacity).
480
+ // Approximation: blend luminances directly; accurate enough for black/white text choice.
481
+ if (color.bg && heatmap_opacity < 1) {
482
+ const blended_lum = luminance(color.bg) * heatmap_opacity +
483
+ page_bg_lum * (1 - heatmap_opacity);
484
+ color.text = blended_lum > 0.7 ? `black` : `white`;
485
+ }
486
+ return color;
415
487
  }
416
488
  let visible_columns = $derived(ordered_columns.filter((col) => col.visible !== false && !hidden_columns.includes(get_col_id(col))));
417
489
  const sort_indicator = (col, sort_state) => {
490
+ const hide_sort_indicator = col.show_sort_indicator === false ||
491
+ col.style?.includes(`--hide-sort-indicator`);
492
+ if (hide_sort_indicator)
493
+ return ``;
418
494
  const col_id = get_col_id(col);
419
495
  // Check multi-sort first
420
496
  const multi_idx = multi_sort.findIndex((s) => s.column === col_id);
@@ -424,13 +500,24 @@ const sort_indicator = (col, sort_state) => {
424
500
  return `<span style="font-size: 0.8em;">${arrow}${badge}</span>`;
425
501
  }
426
502
  const is_sorted = sort_state.column === col_id;
427
- // Show ↓ for ascending/↑ for descending when sorted
428
- // Show ↑ for higher-is-better/↓ for lower-is-better when not sorted
429
- const arrow = is_sorted
430
- ? (sort_state.ascending ? `↓` : `↑`)
431
- : (col.better === `higher` ? `↑` : col.better === `lower` ? `↓` : ``);
503
+ if (!is_sorted)
504
+ return ``;
505
+ // Show indicator only for actively sorted columns.
506
+ const arrow = sort_state.ascending ? `↓` : `↑`;
432
507
  return arrow ? `<span style="font-size: 0.8em;">${arrow}</span>` : ``;
433
508
  };
509
+ // Context menu state for column header right-click
510
+ let context_menu_col = $state(null);
511
+ let context_menu_pos = $state({ x: 0, y: 0 });
512
+ const better_sections = [
513
+ {
514
+ title: `Gradient direction`,
515
+ options: [
516
+ { value: `higher`, label: `▲ Higher is better` },
517
+ { value: `lower`, label: `▼ Lower is better` },
518
+ ],
519
+ },
520
+ ];
434
521
  // Row selection using WeakMap-based ID lookup instead of O(n) JSON.stringify comparison
435
522
  function toggle_row_select(row) {
436
523
  const row_id = get_row_id(row);
@@ -446,35 +533,57 @@ function is_row_selected(row) {
446
533
  const row_id = get_row_id(row);
447
534
  return selected_rows.some((r) => get_row_id(r) === row_id);
448
535
  }
449
- // Export functions
450
- function export_csv(filename = `table-export`) {
451
- const headers = visible_columns.map((col) => col.label);
452
- const rows = sorted_data.map((row) => visible_columns.map((col) => {
536
+ // Select-all: checks if every row on the current page is selected
537
+ let all_page_selected = $derived(paginated_data.length > 0 && paginated_data.every((row) => is_row_selected(row)));
538
+ function toggle_select_all() {
539
+ if (all_page_selected) {
540
+ const page_ids = new Set(paginated_data.map(get_row_id));
541
+ selected_rows = selected_rows.filter((row) => !page_ids.has(get_row_id(row)));
542
+ }
543
+ else {
544
+ const already = new Set(selected_rows.map(get_row_id));
545
+ const new_rows = paginated_data.filter((row) => !already.has(get_row_id(row)));
546
+ selected_rows = [...selected_rows, ...new_rows];
547
+ }
548
+ }
549
+ // Data source for exports: selected rows when any are selected, otherwise all sorted data
550
+ let export_rows = $derived(show_row_select && selected_rows.length > 0 ? selected_rows : sorted_data);
551
+ // Serialize table as delimited text (shared by CSV export and clipboard copy)
552
+ // Per RFC 4180, fields containing commas, double quotes, or newlines must be quoted
553
+ function serialize_table(delimiter, csv_quote = false) {
554
+ const quote = (str) => {
555
+ if (!csv_quote)
556
+ return str;
557
+ if (str.includes(`,`) || str.includes(`"`) || str.includes(`\n`)) {
558
+ return `"${str.replace(/"/g, `""`)}"`;
559
+ }
560
+ return str;
561
+ };
562
+ const headers = visible_columns.map((col) => quote(strip_html(col.label)));
563
+ const rows = export_rows.map((row) => visible_columns.map((col) => {
453
564
  const val = row[get_col_id(col)];
454
565
  if (val == null)
455
566
  return ``;
456
- const str_val = strip_html(String(val));
457
- // Escape quotes and wrap in quotes if contains comma
458
- if (str_val.includes(`,`) || str_val.includes(`"`)) {
459
- return `"${str_val.replace(/"/g, `""`)}"`;
460
- }
461
- return str_val;
567
+ return quote(strip_html(String(val)));
462
568
  }));
463
- const csv_content = [headers.join(`,`), ...rows.map((r) => r.join(`,`))].join(`\n`);
464
- download_file(csv_content, `${filename}.csv`, `text/csv`);
569
+ return [headers.join(delimiter), ...rows.map((r) => r.join(delimiter))].join(`\n`);
570
+ }
571
+ function export_csv(filename = `table-export`) {
572
+ download_file(serialize_table(`,`, true), `${filename}.csv`, `text/csv`);
465
573
  }
466
574
  function export_json(filename = `table-export`) {
467
- const rows = sorted_data.map((row) => {
575
+ const rows = export_rows.map((row) => {
468
576
  const clean_row = {};
469
577
  for (const col of visible_columns) {
470
578
  const col_id = get_col_id(col);
471
579
  const val = row[col_id];
472
- clean_row[col.label] = typeof val === `string` ? strip_html(val) : val;
580
+ clean_row[strip_html(col.label)] = typeof val === `string`
581
+ ? strip_html(val)
582
+ : val;
473
583
  }
474
584
  return clean_row;
475
585
  });
476
- const json_content = JSON.stringify(rows, null, 2);
477
- download_file(json_content, `${filename}.json`, `application/json`);
586
+ download_file(JSON.stringify(rows, null, 2), `${filename}.json`, `application/json`);
478
587
  }
479
588
  function download_file(content, filename, mime_type) {
480
589
  const blob = new Blob([content], { type: mime_type });
@@ -487,6 +596,9 @@ function download_file(content, filename, mime_type) {
487
596
  document.body.removeChild(link);
488
597
  URL.revokeObjectURL(url);
489
598
  }
599
+ function copy_to_clipboard() {
600
+ navigator.clipboard.writeText(serialize_table(`\t`));
601
+ }
490
602
  // Column visibility toggle
491
603
  function toggle_column(col_id) {
492
604
  if (hidden_columns.includes(col_id)) {
@@ -502,7 +614,7 @@ function start_resize(event, col) {
502
614
  event.stopPropagation();
503
615
  resize_col_id = get_col_id(col);
504
616
  resize_start_x = event.clientX;
505
- const th = event.target.parentElement;
617
+ const th = event.target instanceof Element ? event.target.parentElement : null;
506
618
  resize_start_width = th?.offsetWidth ?? 100;
507
619
  document.addEventListener(`mousemove`, handle_resize);
508
620
  document.addEventListener(`mouseup`, stop_resize);
@@ -543,9 +655,15 @@ let hint_config = $derived(sort_hint
543
655
 
544
656
  <div
545
657
  {@attach tooltip()}
546
- {...rest}
547
- class="table-container {rest.class ?? ``}"
548
- onmouseleave={() => [show_column_dropdown, show_export_dropdown] = [false, false]}
658
+ {...rest_props}
659
+ bind:this={container_el}
660
+ class="table-container {rest_props.class ?? ``}"
661
+ style:--heatmap-opacity="{heatmap_opacity * 100}%"
662
+ onmouseleave={() => {
663
+ show_column_dropdown = false
664
+ show_export_dropdown = false
665
+ context_menu_col = null
666
+ }}
549
667
  >
550
668
  <!-- Floating control buttons -->
551
669
  <section class="control-buttons">
@@ -566,12 +684,16 @@ let hint_config = $derived(sort_hint
566
684
  search_query = ``
567
685
  search_expanded = false
568
686
  }}
569
- title="Clear"
687
+ {@attach tooltip({ content: `Clear`, placement: `top` })}
570
688
  >
571
689
  <Icon icon="Cross" style="width: 10px" />
572
690
  </button>
573
691
  {:else}
574
- <button class="icon-btn" onclick={() => search_expanded = true} title="Search">
692
+ <button
693
+ class="icon-btn"
694
+ onclick={() => search_expanded = true}
695
+ {@attach tooltip({ content: `Search`, placement: `top` })}
696
+ >
575
697
  <Icon icon="Search" style="width: 14px" />
576
698
  </button>
577
699
  {/if}
@@ -583,7 +705,7 @@ let hint_config = $derived(sort_hint
583
705
  class="icon-btn"
584
706
  class:active={show_column_dropdown}
585
707
  onclick={() => show_column_dropdown = !show_column_dropdown}
586
- title="Columns"
708
+ {@attach tooltip({ content: `Columns`, placement: `top` })}
587
709
  >
588
710
  <Icon icon="Columns" style="width: 14px" />
589
711
  </button>
@@ -611,7 +733,7 @@ let hint_config = $derived(sort_hint
611
733
  class="icon-btn"
612
734
  class:active={show_export_dropdown}
613
735
  onclick={() => show_export_dropdown = !show_export_dropdown}
614
- title="Export"
736
+ {@attach tooltip({ content: `Export`, placement: `top` })}
615
737
  >
616
738
  <Icon icon="Export" style="width: 14px" />
617
739
  </button>
@@ -639,6 +761,15 @@ let hint_config = $derived(sort_hint
639
761
  <Icon icon="Download" style="width: 12px" /> JSON
640
762
  </button>
641
763
  {/if}
764
+ <button
765
+ class="dropdown-option"
766
+ onclick={() => {
767
+ copy_to_clipboard()
768
+ show_export_dropdown = false
769
+ }}
770
+ >
771
+ <Icon icon="Copy" style="width: 12px" /> Copy
772
+ </button>
642
773
  </div>
643
774
  {/if}
644
775
  </div>
@@ -660,6 +791,106 @@ let hint_config = $derived(sort_hint
660
791
  {/if}
661
792
  </section>
662
793
 
794
+ {#if show_controls}
795
+ <DraggablePane
796
+ bind:show={controls_open}
797
+ closed_icon="Settings"
798
+ open_icon="Cross"
799
+ toggle_props={{
800
+ title: `${controls_open ? `Close` : `Open`} table controls`,
801
+ style: `position: absolute; top: 5pt; right: 1ex; z-index: 10`,
802
+ }}
803
+ pane_props={{ style: `max-height: 60vh; overflow-y: auto; font-size: 0.85em` }}
804
+ >
805
+ <SettingsSection
806
+ title="Heatmap"
807
+ current_values={{ show_heatmap, heatmap_opacity }}
808
+ on_reset={() => {
809
+ show_heatmap = true
810
+ heatmap_opacity = 1
811
+ }}
812
+ >
813
+ <label><input type="checkbox" bind:checked={show_heatmap} /> Show heatmap</label>
814
+ {#if show_heatmap}
815
+ <label>
816
+ Opacity
817
+ <input
818
+ type="range"
819
+ min="0"
820
+ max="1"
821
+ step="0.05"
822
+ bind:value={heatmap_opacity}
823
+ />
824
+ <input
825
+ type="number"
826
+ min="0"
827
+ max="1"
828
+ step="0.05"
829
+ bind:value={heatmap_opacity}
830
+ style="width: 3.5em"
831
+ />
832
+ </label>
833
+ {/if}
834
+ </SettingsSection>
835
+
836
+ <SettingsSection
837
+ title="Display"
838
+ current_values={{ show_row_numbers }}
839
+ on_reset={() => {
840
+ show_row_numbers = false
841
+ }}
842
+ >
843
+ <label><input type="checkbox" bind:checked={show_row_numbers} /> Row
844
+ numbers</label>
845
+ </SettingsSection>
846
+
847
+ {#if colored_columns.length > 0}
848
+ <SettingsSection
849
+ title="Column Colors"
850
+ current_values={Object.fromEntries([...better_overrides, ...color_scale_overrides])}
851
+ on_reset={() => {
852
+ better_overrides.clear()
853
+ color_scale_overrides.clear()
854
+ }}
855
+ >
856
+ {#each colored_columns as col (get_col_id(col))}
857
+ {@const col_id = get_col_id(col)}
858
+ <div class="col-color-row">
859
+ <span class="col-color-label">{@html col.label}</span>
860
+ <select
861
+ value={color_scale_overrides.get(col_id) ?? col.color_scale ??
862
+ `interpolateViridis`}
863
+ onchange={(event) => {
864
+ const val = event.currentTarget.value
865
+ if (
866
+ val === (col.color_scale ?? `interpolateViridis`)
867
+ ) color_scale_overrides.delete(col_id)
868
+ else color_scale_overrides.set(col_id, val)
869
+ }}
870
+ >
871
+ {#each color_scale_options as scale (scale)}
872
+ <option value={scale}>{scale.replace(`interpolate`, ``)}</option>
873
+ {/each}
874
+ </select>
875
+ <select
876
+ value={better_overrides.get(col_id) ?? col.better ?? ``}
877
+ onchange={(event) => {
878
+ const val = event.currentTarget.value
879
+ if (!val) better_overrides.delete(col_id)
880
+ else better_overrides.set(col_id, val as `higher` | `lower`)
881
+ }}
882
+ >
883
+ <option value="">Default</option>
884
+ <option value="higher">▲ High</option>
885
+ <option value="lower">▼ Low</option>
886
+ </select>
887
+ </div>
888
+ {/each}
889
+ </SettingsSection>
890
+ {/if}
891
+ </DraggablePane>
892
+ {/if}
893
+
663
894
  {@render sort_hint_element(`top`)}
664
895
 
665
896
  <div
@@ -681,20 +912,24 @@ let hint_config = $derived(sort_hint
681
912
  {#if show_row_select}
682
913
  <th class="select-col"></th>
683
914
  {/if}
684
- {#each visible_columns as
685
- { label, group, description, sticky }
686
- (label + group)
687
- }
688
- {#if !group}
689
- <th class:sticky-col={sticky}></th>
915
+ {#if show_row_numbers}
916
+ <th class="row-num-col"></th>
917
+ {/if}
918
+ {#each visible_columns as col (get_col_id(col))}
919
+ {#if !col.group}
920
+ <th class:sticky-col={col.sticky}></th>
690
921
  {:else}
691
- {@const group_cols = visible_columns.filter((c) => c.group === group)}
922
+ {@const group_cols = visible_columns.filter((c) =>
923
+ c.group === col.group
924
+ )}
692
925
  <!-- Only render the group header once for each group by checking if this is the first column of this group -->
693
- {#if visible_columns.findIndex((c) => c.group === group) ===
926
+ {#if visible_columns.findIndex((c) => c.group === col.group) ===
694
927
  visible_columns.findIndex((c) =>
695
- c.group === group && c.label === label
928
+ c.group === col.group && c.label === col.label
696
929
  )}
697
- <th title={description} colspan={group_cols.length}>{@html group}</th>
930
+ <th title={col.description} colspan={group_cols.length}>
931
+ {@html col.group}
932
+ </th>
698
933
  {/if}
699
934
  {/if}
700
935
  {/each}
@@ -703,11 +938,21 @@ let hint_config = $derived(sort_hint
703
938
  <!-- Second level headers -->
704
939
  <tr>
705
940
  {#if show_row_select}
706
- <th class="select-col" title="Select rows">
707
- <Icon icon="Checkbox" style="width: 14px; opacity: 0.7" />
941
+ <th
942
+ class="select-col"
943
+ title={all_page_selected ? `Deselect all` : `Select all on this page`}
944
+ >
945
+ <input
946
+ type="checkbox"
947
+ checked={all_page_selected}
948
+ onchange={toggle_select_all}
949
+ />
708
950
  </th>
709
951
  {/if}
710
- {#each visible_columns as col (col.label + col.group)}
952
+ {#if show_row_numbers}
953
+ <th class="row-num-col">#</th>
954
+ {/if}
955
+ {#each visible_columns as col (get_col_id(col))}
711
956
  {@const col_id = get_col_id(col)}
712
957
  {@const drag_side = drag_over_col_id === col_id
713
958
  ? get_drag_side(col_id)
@@ -717,6 +962,20 @@ let hint_config = $derived(sort_hint
717
962
  title={col.description}
718
963
  tabindex={col.sortable === false ? undefined : 0}
719
964
  role={col.sortable === false ? undefined : `button`}
965
+ oncontextmenu={(event) => {
966
+ if (
967
+ !allow_better_toggle || col.color_scale === null ||
968
+ col.color_scale === undefined
969
+ ) return
970
+ event.preventDefault()
971
+ event.stopPropagation()
972
+ context_menu_col = col_id
973
+ const rect = container_el?.getBoundingClientRect()
974
+ context_menu_pos = {
975
+ x: event.clientX - (rect?.left ?? 0),
976
+ y: event.clientY - (rect?.top ?? 0),
977
+ }
978
+ }}
720
979
  onclick={(event) => {
721
980
  if (!drag_col_id && !resize_col_id) {
722
981
  sort_rows(
@@ -762,7 +1021,11 @@ let hint_config = $derived(sort_hint
762
1021
  event.currentTarget.removeAttribute(`aria-grabbed`)
763
1022
  }}
764
1023
  >
765
- {@html col.label}
1024
+ {#if header_cell}
1025
+ {@render header_cell({ col })}
1026
+ {:else}
1027
+ {@html col.label}
1028
+ {/if}
766
1029
  {@html sort_indicator(col, sort_state)}
767
1030
  <!-- Column resize handle -->
768
1031
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
@@ -780,14 +1043,32 @@ let hint_config = $derived(sort_hint
780
1043
  </tr>
781
1044
  </thead>
782
1045
  <tbody>
783
- {#each paginated_data as row (get_row_id(row))}
1046
+ {#each paginated_data as row, row_idx (get_row_id(row))}
784
1047
  {@const row_selected = show_row_select && is_row_selected(row)}
785
1048
  <tr
786
1049
  animate:flip={{ duration: 500 }}
787
1050
  style={row.style}
788
1051
  class={row.class ?? ``}
789
1052
  class:selected={row_selected}
1053
+ tabindex={onrowclick ? 0 : undefined}
1054
+ onclick={onrowclick ? (event) => onrowclick(event, row) : undefined}
790
1055
  ondblclick={onrowdblclick ? (event) => onrowdblclick(event, row) : undefined}
1056
+ onkeydown={onrowclick
1057
+ ? (event) => {
1058
+ if (event.key === `Enter` || event.key === ` `) {
1059
+ event.preventDefault()
1060
+ onrowclick(event, row)
1061
+ } else if (event.key === `ArrowDown`) {
1062
+ event.preventDefault()
1063
+ const next = event.currentTarget.nextElementSibling
1064
+ if (next instanceof HTMLElement) next.focus()
1065
+ } else if (event.key === `ArrowUp`) {
1066
+ event.preventDefault()
1067
+ const prev = event.currentTarget.previousElementSibling
1068
+ if (prev instanceof HTMLElement) prev.focus()
1069
+ }
1070
+ }
1071
+ : undefined}
791
1072
  >
792
1073
  {#if show_row_select}
793
1074
  <td class="select-col">
@@ -798,7 +1079,12 @@ let hint_config = $derived(sort_hint
798
1079
  />
799
1080
  </td>
800
1081
  {/if}
801
- {#each visible_columns as col (col.label + col.group)}
1082
+ {#if show_row_numbers}
1083
+ <td class="row-num-col">
1084
+ {(current_page - 1) * effective_page_size + row_idx + 1}
1085
+ </td>
1086
+ {/if}
1087
+ {#each visible_columns as col (get_col_id(col))}
802
1088
  {@const val = row[get_col_id(col)]}
803
1089
  {@const color = calc_color(val, col)}
804
1090
  {@const col_width = column_widths[get_col_id(col)]}
@@ -806,7 +1092,7 @@ let hint_config = $derived(sort_hint
806
1092
  data-col={col.label}
807
1093
  data-sort-value={is_html_str(val) ? null : val}
808
1094
  class:sticky-col={col.sticky}
809
- style:background-color={color.bg}
1095
+ style:--cell-bg={color.bg}
810
1096
  style:color={color.text}
811
1097
  style={`${col.cell_style ?? col.style ?? ``}${
812
1098
  col_width
@@ -830,8 +1116,24 @@ let hint_config = $derived(sort_hint
830
1116
  </td>
831
1117
  {/each}
832
1118
  </tr>
1119
+ {:else}
1120
+ {#if empty_message}
1121
+ <tr class="empty-row">
1122
+ <td
1123
+ colspan={visible_columns.length + (show_row_select ? 1 : 0) +
1124
+ (show_row_numbers ? 1 : 0)}
1125
+ >
1126
+ {empty_message}
1127
+ </td>
1128
+ </tr>
1129
+ {/if}
833
1130
  {/each}
834
1131
  </tbody>
1132
+ {#if footer}
1133
+ <tfoot>
1134
+ {@render footer()}
1135
+ </tfoot>
1136
+ {/if}
835
1137
  </table>
836
1138
  </div>
837
1139
 
@@ -888,8 +1190,47 @@ let hint_config = $derived(sort_hint
888
1190
  >
889
1191
  »
890
1192
  </button>
1193
+ {#if pagination_config.page_sizes}
1194
+ <select
1195
+ class="page-size-select"
1196
+ onchange={(event) => {
1197
+ effective_page_size = parseInt(event.currentTarget.value, 10)
1198
+ current_page = 1
1199
+ }}
1200
+ >
1201
+ {#each pagination_config.page_sizes as size (size)}
1202
+ <option value={size} selected={size === effective_page_size}>
1203
+ {size} / page
1204
+ </option>
1205
+ {/each}
1206
+ </select>
1207
+ {/if}
891
1208
  </div>
892
1209
  {/if}
1210
+
1211
+ <ContextMenu
1212
+ sections={better_sections}
1213
+ selected_values={{ 'Gradient direction': better_overrides.get(context_menu_col ?? ``) ?? `` }}
1214
+ position={context_menu_pos}
1215
+ visible={context_menu_col !== null}
1216
+ on_close={() => context_menu_col = null}
1217
+ style={[
1218
+ `--surface-bg: light-dark(#fff, #1e1e1e)`,
1219
+ `--border-color: light-dark(rgba(0,0,0,0.15), rgba(255,255,255,0.15))`,
1220
+ `--text-color: light-dark(#333, #eee)`,
1221
+ `--text-color-muted: light-dark(#888, #999)`,
1222
+ `--surface-bg-hover: light-dark(rgba(0,0,0,0.06), rgba(255,255,255,0.1))`,
1223
+ `--accent-color: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.15))`,
1224
+ `z-index: 200`,
1225
+ ].join(`; `)}
1226
+ on_select={(_, option) => {
1227
+ if (!context_menu_col) return
1228
+ const current = better_overrides.get(context_menu_col)
1229
+ if (current === option.value) better_overrides.delete(context_menu_col)
1230
+ else better_overrides.set(context_menu_col, option.value as `higher` | `lower`)
1231
+ context_menu_col = null
1232
+ }}
1233
+ />
893
1234
  </div>
894
1235
 
895
1236
  <style>
@@ -897,15 +1238,21 @@ let hint_config = $derived(sort_hint
897
1238
  font-size: var(--heatmap-font-size, 0.9em);
898
1239
  width: fit-content;
899
1240
  max-width: 100%;
1241
+ max-height: inherit;
900
1242
  margin: 0 auto;
901
1243
  position: relative;
1244
+ display: flex;
1245
+ flex-direction: column;
902
1246
  }
903
1247
  .table-scroll {
904
1248
  position: relative;
1249
+ overflow: auto;
905
1250
  }
906
1251
  .table-scroll.has-scroll {
907
- overflow: auto;
908
- border: 1px solid var(--border, #333);
1252
+ border: 1px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.12));
1253
+ border-radius: var(--border-radius, 3pt);
1254
+ overflow-x: hidden;
1255
+ overflow-y: auto;
909
1256
  }
910
1257
  table {
911
1258
  border-collapse: separate;
@@ -919,6 +1266,13 @@ let hint_config = $derived(sort_hint
919
1266
  white-space: nowrap;
920
1267
  overflow: hidden;
921
1268
  text-overflow: ellipsis;
1269
+ /* --cell-bg is set inline per-cell by calc_color(); --heatmap-opacity is set
1270
+ on the container from the heatmap_opacity prop to fade cell backgrounds */
1271
+ background-color: color-mix(
1272
+ in srgb,
1273
+ var(--cell-bg, transparent) var(--heatmap-opacity, 100%),
1274
+ transparent
1275
+ );
922
1276
  }
923
1277
  th {
924
1278
  background: var(--heatmap-header-bg, var(--page-bg, Canvas));
@@ -962,6 +1316,13 @@ let hint_config = $derived(sort_hint
962
1316
  tbody tr:hover {
963
1317
  filter: var(--heatmap-row-hover-filter, brightness(1.1));
964
1318
  }
1319
+ tbody tr[tabindex] {
1320
+ cursor: pointer;
1321
+ }
1322
+ tbody tr:focus-visible {
1323
+ outline: 2px solid var(--highlight, #4a9eff);
1324
+ outline-offset: -2px;
1325
+ }
965
1326
  td[data-sort-value] {
966
1327
  cursor: default;
967
1328
  }
@@ -979,7 +1340,7 @@ let hint_config = $derived(sort_hint
979
1340
  justify-content: flex-end;
980
1341
  align-items: center;
981
1342
  gap: 2px;
982
- margin-bottom: 4px;
1343
+ margin-bottom: 1px;
983
1344
  opacity: 0;
984
1345
  pointer-events: none;
985
1346
  transition: opacity 0.15s;
@@ -990,21 +1351,21 @@ let hint_config = $derived(sort_hint
990
1351
  pointer-events: auto;
991
1352
  }
992
1353
  .icon-btn {
993
- padding: 5px 8px;
1354
+ padding: 2px 4px;
994
1355
  border: none;
995
- border-radius: 4px;
1356
+ border-radius: 3px;
996
1357
  background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.1));
997
1358
  color: light-dark(#333, #ddd);
998
1359
  cursor: pointer;
999
1360
  display: flex;
1000
1361
  align-items: center;
1001
1362
  justify-content: center;
1002
- gap: 4px;
1003
- font-size: 0.95em;
1363
+ gap: 2px;
1364
+ font-size: 0.8em;
1004
1365
  }
1005
1366
  .icon-btn :global(svg) {
1006
- width: 16px;
1007
- height: 16px;
1367
+ width: 12px;
1368
+ height: 12px;
1008
1369
  }
1009
1370
  .icon-btn:hover {
1010
1371
  background: light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.2));
@@ -1066,13 +1427,13 @@ let hint_config = $derived(sort_hint
1066
1427
  gap: 6px;
1067
1428
  }
1068
1429
  .search-input {
1069
- padding: 5px 8px;
1430
+ padding: 2px 4px;
1070
1431
  border: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.2));
1071
- border-radius: 4px;
1432
+ border-radius: 3px;
1072
1433
  background: light-dark(rgba(255, 255, 255, 0.9), rgba(0, 0, 0, 0.3));
1073
1434
  color: light-dark(#333, #eee);
1074
- font-size: 0.95em;
1075
- width: 120px;
1435
+ font-size: 0.8em;
1436
+ width: 110px;
1076
1437
  box-sizing: border-box;
1077
1438
  }
1078
1439
  .search-input:focus {
@@ -1179,6 +1540,23 @@ let hint_config = $derived(sort_hint
1179
1540
  font-size: 0.85em;
1180
1541
  }
1181
1542
 
1543
+ .col-color-row {
1544
+ display: flex;
1545
+ align-items: center;
1546
+ gap: 4px;
1547
+ padding: 2px 0;
1548
+ select {
1549
+ font-size: 0.85em;
1550
+ padding: 1px 2px;
1551
+ }
1552
+ }
1553
+ .col-color-label {
1554
+ flex: 1;
1555
+ overflow: hidden;
1556
+ text-overflow: ellipsis;
1557
+ white-space: nowrap;
1558
+ min-width: 0;
1559
+ }
1182
1560
  /* Column resize */
1183
1561
  .resize-handle {
1184
1562
  position: absolute;
@@ -1216,4 +1594,25 @@ let hint_config = $derived(sort_hint
1216
1594
  transform: rotate(360deg);
1217
1595
  }
1218
1596
  }
1597
+ .empty-row td {
1598
+ text-align: center;
1599
+ padding: 2em !important;
1600
+ color: var(--text-muted, #888);
1601
+ font-style: italic;
1602
+ }
1603
+ .row-num-col {
1604
+ text-align: right;
1605
+ color: var(--text-muted, #888);
1606
+ font-size: 0.85em;
1607
+ width: 2em;
1608
+ padding-right: 8px !important;
1609
+ }
1610
+ .page-size-select {
1611
+ padding: 2px 4px;
1612
+ border: 1px solid light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2));
1613
+ border-radius: 3px;
1614
+ background: light-dark(#fff, #333);
1615
+ color: inherit;
1616
+ font-size: 0.9em;
1617
+ }
1219
1618
  </style>