matterviz 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/app.css +29 -0
  4. package/dist/brillouin/BrillouinZone.svelte +19 -61
  5. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  6. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  7. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  9. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  10. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  11. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  12. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  13. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  16. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  17. package/dist/chempot-diagram/color.d.ts +10 -0
  18. package/dist/chempot-diagram/color.js +33 -0
  19. package/dist/chempot-diagram/compute.d.ts +38 -0
  20. package/dist/chempot-diagram/compute.js +650 -0
  21. package/dist/chempot-diagram/index.d.ts +5 -0
  22. package/dist/chempot-diagram/index.js +5 -0
  23. package/dist/chempot-diagram/pointer.d.ts +16 -0
  24. package/dist/chempot-diagram/pointer.js +40 -0
  25. package/dist/chempot-diagram/temperature.d.ts +15 -0
  26. package/dist/chempot-diagram/temperature.js +37 -0
  27. package/dist/chempot-diagram/types.d.ts +83 -0
  28. package/dist/chempot-diagram/types.js +27 -0
  29. package/dist/colors/index.d.ts +3 -1
  30. package/dist/colors/index.js +4 -0
  31. package/dist/composition/BarChart.svelte +13 -22
  32. package/dist/composition/BubbleChart.svelte +5 -3
  33. package/dist/composition/FormulaFilter.svelte +586 -94
  34. package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
  35. package/dist/composition/PieChart.svelte +43 -18
  36. package/dist/composition/PieChart.svelte.d.ts +1 -1
  37. package/dist/convex-hull/ConvexHull.svelte +4 -2
  38. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  39. package/dist/convex-hull/ConvexHull2D.svelte +13 -44
  40. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  41. package/dist/convex-hull/ConvexHull3D.svelte +16 -7
  42. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull4D.svelte +17 -7
  44. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHullStats.svelte +701 -226
  47. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  48. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  49. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  50. package/dist/convex-hull/demo-temperature.js +36 -0
  51. package/dist/convex-hull/helpers.d.ts +1 -1
  52. package/dist/convex-hull/helpers.js +2 -4
  53. package/dist/convex-hull/index.d.ts +1 -0
  54. package/dist/convex-hull/index.js +1 -0
  55. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  56. package/dist/convex-hull/thermodynamics.js +106 -17
  57. package/dist/convex-hull/types.d.ts +5 -0
  58. package/dist/convex-hull/types.js +5 -0
  59. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  60. package/dist/element/BohrAtom.svelte +1 -1
  61. package/dist/element/data.js +2 -14
  62. package/dist/element/data.json.gz +0 -0
  63. package/dist/element/types.d.ts +1 -0
  64. package/dist/fermi-surface/FermiSurface.svelte +20 -64
  65. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  66. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  67. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  68. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  69. package/dist/fermi-surface/parse.js +16 -22
  70. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  71. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  72. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  73. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  74. package/dist/heatmap-matrix/index.d.ts +53 -0
  75. package/dist/heatmap-matrix/index.js +100 -0
  76. package/dist/heatmap-matrix/shared.d.ts +2 -0
  77. package/dist/heatmap-matrix/shared.js +4 -0
  78. package/dist/icons.d.ts +111 -0
  79. package/dist/icons.js +111 -0
  80. package/dist/index.d.ts +3 -1
  81. package/dist/index.js +3 -1
  82. package/dist/io/export.js +15 -3
  83. package/dist/io/file-drop.d.ts +7 -0
  84. package/dist/io/file-drop.js +43 -0
  85. package/dist/io/index.d.ts +2 -2
  86. package/dist/io/index.js +2 -112
  87. package/dist/io/types.d.ts +1 -0
  88. package/dist/io/url-drop.d.ts +2 -0
  89. package/dist/io/url-drop.js +118 -0
  90. package/dist/isosurface/Isosurface.svelte +101 -45
  91. package/dist/isosurface/IsosurfaceControls.svelte +19 -0
  92. package/dist/isosurface/parse.js +73 -30
  93. package/dist/isosurface/slice.d.ts +2 -1
  94. package/dist/isosurface/slice.js +3 -3
  95. package/dist/isosurface/types.d.ts +13 -1
  96. package/dist/isosurface/types.js +98 -0
  97. package/dist/labels.d.ts +2 -1
  98. package/dist/labels.js +1 -0
  99. package/dist/layout/InfoTag.svelte +62 -62
  100. package/dist/layout/SubpageGrid.svelte +74 -0
  101. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  102. package/dist/layout/index.d.ts +1 -0
  103. package/dist/layout/index.js +1 -0
  104. package/dist/layout/json-tree/JsonNode.svelte +83 -85
  105. package/dist/layout/json-tree/JsonTree.svelte +20 -19
  106. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  107. package/dist/layout/json-tree/JsonValue.svelte +196 -116
  108. package/dist/layout/json-tree/types.d.ts +10 -2
  109. package/dist/layout/json-tree/utils.d.ts +2 -0
  110. package/dist/layout/json-tree/utils.js +33 -0
  111. package/dist/math.d.ts +7 -0
  112. package/dist/math.js +358 -7
  113. package/dist/overlays/ContextMenu.svelte +3 -2
  114. package/dist/overlays/DraggablePane.svelte +163 -58
  115. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  116. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  117. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  118. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  119. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  120. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  121. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  122. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  123. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  124. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  125. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  126. package/dist/phase-diagram/index.d.ts +2 -0
  127. package/dist/phase-diagram/index.js +2 -0
  128. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  129. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  130. package/dist/phase-diagram/types.d.ts +10 -0
  131. package/dist/phase-diagram/utils.d.ts +7 -4
  132. package/dist/phase-diagram/utils.js +149 -59
  133. package/dist/plot/AxisLabel.svelte +26 -0
  134. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  135. package/dist/plot/BarPlot.svelte +473 -228
  136. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  137. package/dist/plot/BarPlotControls.svelte +3 -2
  138. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  139. package/dist/plot/ColorBar.svelte +54 -54
  140. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  141. package/dist/plot/ColorScaleSelect.svelte +1 -1
  142. package/dist/plot/ElementScatter.svelte +3 -2
  143. package/dist/plot/FillArea.svelte +4 -1
  144. package/dist/plot/Histogram.svelte +320 -230
  145. package/dist/plot/Histogram.svelte.d.ts +2 -2
  146. package/dist/plot/HistogramControls.svelte +29 -10
  147. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  148. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  149. package/dist/plot/PlotControls.svelte +109 -27
  150. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  151. package/dist/plot/PlotLegend.svelte +1 -1
  152. package/dist/plot/PortalSelect.svelte +2 -1
  153. package/dist/plot/ReferenceLine.svelte +2 -1
  154. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  155. package/dist/plot/ReferencePlane.svelte +1 -3
  156. package/dist/plot/ScatterPlot.svelte +343 -209
  157. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  158. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  159. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  160. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  161. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  162. package/dist/plot/ScatterPlotControls.svelte +95 -55
  163. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  164. package/dist/plot/ZeroLines.svelte +44 -0
  165. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  166. package/dist/plot/ZoomRect.svelte +21 -0
  167. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  168. package/dist/plot/axis-utils.d.ts +1 -1
  169. package/dist/plot/index.d.ts +6 -2
  170. package/dist/plot/index.js +6 -2
  171. package/dist/plot/interactions.d.ts +8 -10
  172. package/dist/plot/interactions.js +2 -3
  173. package/dist/plot/layout.d.ts +7 -1
  174. package/dist/plot/layout.js +12 -4
  175. package/dist/plot/reference-line.d.ts +4 -21
  176. package/dist/plot/reference-line.js +7 -81
  177. package/dist/plot/types.d.ts +42 -17
  178. package/dist/plot/types.js +10 -0
  179. package/dist/plot/utils/label-placement.js +13 -10
  180. package/dist/plot/utils.d.ts +1 -0
  181. package/dist/plot/utils.js +14 -0
  182. package/dist/rdf/RdfPlot.svelte +55 -66
  183. package/dist/settings.d.ts +3 -0
  184. package/dist/settings.js +17 -3
  185. package/dist/spectral/Bands.svelte +515 -143
  186. package/dist/spectral/Bands.svelte.d.ts +22 -2
  187. package/dist/spectral/helpers.d.ts +23 -1
  188. package/dist/spectral/helpers.js +65 -9
  189. package/dist/spectral/types.d.ts +2 -0
  190. package/dist/structure/AtomLegend.svelte +29 -8
  191. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  192. package/dist/structure/CellSelect.svelte +92 -22
  193. package/dist/structure/Structure.svelte +108 -118
  194. package/dist/structure/Structure.svelte.d.ts +1 -1
  195. package/dist/structure/StructureControls.svelte +25 -22
  196. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  197. package/dist/structure/StructureInfoPane.svelte +7 -1
  198. package/dist/structure/StructureScene.svelte +104 -66
  199. package/dist/structure/StructureScene.svelte.d.ts +2 -1
  200. package/dist/structure/atom-properties.d.ts +6 -2
  201. package/dist/structure/atom-properties.js +38 -25
  202. package/dist/structure/export.js +10 -7
  203. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  204. package/dist/structure/ferrox-wasm-types.js +0 -3
  205. package/dist/structure/ferrox-wasm.d.ts +3 -2
  206. package/dist/structure/ferrox-wasm.js +1 -2
  207. package/dist/structure/index.d.ts +6 -0
  208. package/dist/structure/index.js +22 -0
  209. package/dist/structure/parse.js +19 -16
  210. package/dist/structure/partial-occupancy.d.ts +25 -0
  211. package/dist/structure/partial-occupancy.js +102 -0
  212. package/dist/structure/validation.js +6 -3
  213. package/dist/symmetry/SymmetryStats.svelte +18 -4
  214. package/dist/symmetry/WyckoffTable.svelte +18 -10
  215. package/dist/symmetry/index.d.ts +7 -4
  216. package/dist/symmetry/index.js +83 -18
  217. package/dist/table/HeatmapTable.svelte +425 -65
  218. package/dist/table/HeatmapTable.svelte.d.ts +12 -1
  219. package/dist/table/ToggleMenu.svelte +2 -0
  220. package/dist/table/index.d.ts +2 -0
  221. package/dist/trajectory/Trajectory.svelte +147 -145
  222. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  223. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  224. package/dist/trajectory/constants.d.ts +6 -0
  225. package/dist/trajectory/constants.js +7 -0
  226. package/dist/trajectory/extract.js +3 -5
  227. package/dist/trajectory/format-detect.d.ts +9 -0
  228. package/dist/trajectory/format-detect.js +76 -0
  229. package/dist/trajectory/frame-reader.d.ts +17 -0
  230. package/dist/trajectory/frame-reader.js +339 -0
  231. package/dist/trajectory/helpers.d.ts +15 -0
  232. package/dist/trajectory/helpers.js +187 -0
  233. package/dist/trajectory/index.d.ts +1 -0
  234. package/dist/trajectory/index.js +11 -4
  235. package/dist/trajectory/parse/ase.d.ts +2 -0
  236. package/dist/trajectory/parse/ase.js +76 -0
  237. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  238. package/dist/trajectory/parse/hdf5.js +121 -0
  239. package/dist/trajectory/parse/index.d.ts +12 -0
  240. package/dist/trajectory/parse/index.js +304 -0
  241. package/dist/trajectory/parse/lammps.d.ts +5 -0
  242. package/dist/trajectory/parse/lammps.js +169 -0
  243. package/dist/trajectory/parse/vasp.d.ts +2 -0
  244. package/dist/trajectory/parse/vasp.js +65 -0
  245. package/dist/trajectory/parse/xyz.d.ts +2 -0
  246. package/dist/trajectory/parse/xyz.js +109 -0
  247. package/dist/trajectory/types.d.ts +11 -0
  248. package/dist/trajectory/types.js +1 -0
  249. package/dist/utils.d.ts +2 -0
  250. package/dist/utils.js +4 -0
  251. package/dist/xrd/XrdPlot.svelte +6 -4
  252. package/dist/xrd/calc-xrd.js +0 -1
  253. package/package.json +30 -24
  254. package/readme.md +4 -4
  255. package/dist/trajectory/parse.d.ts +0 -42
  256. package/dist/trajectory/parse.js +0 -1267
  257. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">import { luminance, watch_dark_mode } from '../colors';
2
2
  import Icon from '../Icon.svelte';
3
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';
4
7
  import { calc_cell_color, strip_html } from './';
8
+ import { normalize_unicode_minus } from '../utils';
5
9
  import { tooltip } from 'svelte-multiselect/attachments';
6
10
  import { flip } from 'svelte/animate';
7
11
  import { SvelteMap } from 'svelte/reactivity';
8
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
9
- 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, heatmap_opacity = $bindable(1), ...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();
10
14
  let container_el = $state();
11
15
  // Read --page-bg from computed style for text contrast calculation.
12
16
  // Recalculates on mount and when the theme changes (dark/light mode toggle).
@@ -43,6 +47,8 @@ let initial_sort_config = $derived(initial_sort
43
47
  let pagination_config = $derived(pagination
44
48
  ? { page_size: 25, ...(typeof pagination === `object` ? pagination : {}) }
45
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);
46
52
  // Normalize search config
47
53
  let search_config = $derived(search
48
54
  ? {
@@ -77,11 +83,41 @@ let current_page = $state(1);
77
83
  // Dropdown states
78
84
  let show_column_dropdown = $state(false);
79
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));
80
103
  // Column resize state
81
104
  let resize_col_id = $state(null);
82
105
  let resize_start_x = $state(0);
83
106
  let resize_start_width = $state(0);
84
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
+ });
85
121
  // Helper to make column IDs (needed since column labels in different groups can be repeated)
86
122
  const get_col_id = (col) => col.group ? `${col.key ?? col.label} (${col.group})` : (col.key ?? col.label);
87
123
  // Sync column_order with columns: initialize if empty, remove stale IDs, append new IDs
@@ -113,13 +149,19 @@ let ordered_columns = $derived.by(() => {
113
149
  // Add columns in specified order, then any remaining columns that weren't in the order list
114
150
  const ordered = column_order
115
151
  .map((id) => col_map.get(id))
116
- .filter(Boolean);
152
+ .filter((col) => col != null);
117
153
  const ordered_ids = new Set(ordered.map(get_col_id));
118
154
  const remaining = columns.filter((col) => !ordered_ids.has(get_col_id(col)));
119
155
  return [...ordered, ...remaining];
120
156
  });
121
157
  let drag_col_id = $state(null);
122
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
+ });
123
165
  // WeakMap to assign stable unique IDs to row objects for efficient comparison and keying
124
166
  // This avoids O(n) JSON.stringify calls and prevents unnecessary re-renders
125
167
  const row_id_map = new WeakMap();
@@ -288,10 +330,10 @@ let sorted_data = $derived.by(() => {
288
330
  let paginated_data = $derived.by(() => {
289
331
  if (!pagination_config)
290
332
  return sorted_data;
291
- const start = (current_page - 1) * pagination_config.page_size;
292
- 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);
293
335
  });
294
- 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));
295
337
  // Track previous values to detect actual changes
296
338
  let prev_search_query = $state(``);
297
339
  let prev_data_length = $state(0);
@@ -306,6 +348,10 @@ $effect(() => {
306
348
  prev_search_query = search_query;
307
349
  prev_data_length = sorted_data.length;
308
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
+ }
309
355
  });
310
356
  async function sort_rows(column, group, event) {
311
357
  // Find the column using both label and group if provided
@@ -391,14 +437,14 @@ function parse_numeric_val(val) {
391
437
  const error_match = val.match(/^([-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/);
392
438
  if (error_match) {
393
439
  // Normalize unicode minus (U+2212) to ASCII hyphen for Number()
394
- const normalized = error_match[1].replace(/−/g, `-`);
440
+ const normalized = normalize_unicode_minus(error_match[1]);
395
441
  const num = Number(normalized);
396
442
  if (!isNaN(num))
397
443
  return num;
398
444
  }
399
445
  // Try parsing as a plain number (handles "1.23" strings)
400
446
  // Also normalize unicode minus for plain numbers
401
- const normalized_val = val.replace(/−/g, `-`);
447
+ const normalized_val = normalize_unicode_minus(val);
402
448
  const plain_num = Number(normalized_val);
403
449
  if (!isNaN(plain_num) && val.trim() !== ``)
404
450
  return plain_num;
@@ -426,8 +472,10 @@ function calc_color(val, col) {
426
472
  const col_id = get_col_id(col);
427
473
  // Use memoized parsed values for the column
428
474
  const numeric_vals = parsed_column_values.get(col_id) ?? [];
429
- // calc_cell_color handles null/NaN filtering internally
430
- const color = 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`);
431
479
  // Recompute text contrast against effective bg (cell bg blended with page bg by opacity).
432
480
  // Approximation: blend luminances directly; accurate enough for black/white text choice.
433
481
  if (color.bg && heatmap_opacity < 1) {
@@ -439,6 +487,10 @@ function calc_color(val, col) {
439
487
  }
440
488
  let visible_columns = $derived(ordered_columns.filter((col) => col.visible !== false && !hidden_columns.includes(get_col_id(col))));
441
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 ``;
442
494
  const col_id = get_col_id(col);
443
495
  // Check multi-sort first
444
496
  const multi_idx = multi_sort.findIndex((s) => s.column === col_id);
@@ -448,13 +500,24 @@ const sort_indicator = (col, sort_state) => {
448
500
  return `<span style="font-size: 0.8em;">${arrow}${badge}</span>`;
449
501
  }
450
502
  const is_sorted = sort_state.column === col_id;
451
- // Show ↓ for ascending/↑ for descending when sorted
452
- // Show ↑ for higher-is-better/↓ for lower-is-better when not sorted
453
- const arrow = is_sorted
454
- ? (sort_state.ascending ? `↓` : `↑`)
455
- : (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 ? `↓` : `↑`;
456
507
  return arrow ? `<span style="font-size: 0.8em;">${arrow}</span>` : ``;
457
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
+ ];
458
521
  // Row selection using WeakMap-based ID lookup instead of O(n) JSON.stringify comparison
459
522
  function toggle_row_select(row) {
460
523
  const row_id = get_row_id(row);
@@ -470,35 +533,57 @@ function is_row_selected(row) {
470
533
  const row_id = get_row_id(row);
471
534
  return selected_rows.some((r) => get_row_id(r) === row_id);
472
535
  }
473
- // Export functions
474
- function export_csv(filename = `table-export`) {
475
- const headers = visible_columns.map((col) => col.label);
476
- 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) => {
477
564
  const val = row[get_col_id(col)];
478
565
  if (val == null)
479
566
  return ``;
480
- const str_val = strip_html(String(val));
481
- // Escape quotes and wrap in quotes if contains comma
482
- if (str_val.includes(`,`) || str_val.includes(`"`)) {
483
- return `"${str_val.replace(/"/g, `""`)}"`;
484
- }
485
- return str_val;
567
+ return quote(strip_html(String(val)));
486
568
  }));
487
- const csv_content = [headers.join(`,`), ...rows.map((r) => r.join(`,`))].join(`\n`);
488
- 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`);
489
573
  }
490
574
  function export_json(filename = `table-export`) {
491
- const rows = sorted_data.map((row) => {
575
+ const rows = export_rows.map((row) => {
492
576
  const clean_row = {};
493
577
  for (const col of visible_columns) {
494
578
  const col_id = get_col_id(col);
495
579
  const val = row[col_id];
496
- 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;
497
583
  }
498
584
  return clean_row;
499
585
  });
500
- const json_content = JSON.stringify(rows, null, 2);
501
- download_file(json_content, `${filename}.json`, `application/json`);
586
+ download_file(JSON.stringify(rows, null, 2), `${filename}.json`, `application/json`);
502
587
  }
503
588
  function download_file(content, filename, mime_type) {
504
589
  const blob = new Blob([content], { type: mime_type });
@@ -511,6 +596,9 @@ function download_file(content, filename, mime_type) {
511
596
  document.body.removeChild(link);
512
597
  URL.revokeObjectURL(url);
513
598
  }
599
+ function copy_to_clipboard() {
600
+ navigator.clipboard.writeText(serialize_table(`\t`));
601
+ }
514
602
  // Column visibility toggle
515
603
  function toggle_column(col_id) {
516
604
  if (hidden_columns.includes(col_id)) {
@@ -526,7 +614,7 @@ function start_resize(event, col) {
526
614
  event.stopPropagation();
527
615
  resize_col_id = get_col_id(col);
528
616
  resize_start_x = event.clientX;
529
- const th = event.target.parentElement;
617
+ const th = event.target instanceof Element ? event.target.parentElement : null;
530
618
  resize_start_width = th?.offsetWidth ?? 100;
531
619
  document.addEventListener(`mousemove`, handle_resize);
532
620
  document.addEventListener(`mouseup`, stop_resize);
@@ -567,11 +655,15 @@ let hint_config = $derived(sort_hint
567
655
 
568
656
  <div
569
657
  {@attach tooltip()}
570
- {...rest}
658
+ {...rest_props}
571
659
  bind:this={container_el}
572
- class="table-container {rest.class ?? ``}"
660
+ class="table-container {rest_props.class ?? ``}"
573
661
  style:--heatmap-opacity="{heatmap_opacity * 100}%"
574
- onmouseleave={() => [show_column_dropdown, show_export_dropdown] = [false, false]}
662
+ onmouseleave={() => {
663
+ show_column_dropdown = false
664
+ show_export_dropdown = false
665
+ context_menu_col = null
666
+ }}
575
667
  >
576
668
  <!-- Floating control buttons -->
577
669
  <section class="control-buttons">
@@ -592,12 +684,16 @@ let hint_config = $derived(sort_hint
592
684
  search_query = ``
593
685
  search_expanded = false
594
686
  }}
595
- title="Clear"
687
+ {@attach tooltip({ content: `Clear`, placement: `top` })}
596
688
  >
597
689
  <Icon icon="Cross" style="width: 10px" />
598
690
  </button>
599
691
  {:else}
600
- <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
+ >
601
697
  <Icon icon="Search" style="width: 14px" />
602
698
  </button>
603
699
  {/if}
@@ -609,7 +705,7 @@ let hint_config = $derived(sort_hint
609
705
  class="icon-btn"
610
706
  class:active={show_column_dropdown}
611
707
  onclick={() => show_column_dropdown = !show_column_dropdown}
612
- title="Columns"
708
+ {@attach tooltip({ content: `Columns`, placement: `top` })}
613
709
  >
614
710
  <Icon icon="Columns" style="width: 14px" />
615
711
  </button>
@@ -637,7 +733,7 @@ let hint_config = $derived(sort_hint
637
733
  class="icon-btn"
638
734
  class:active={show_export_dropdown}
639
735
  onclick={() => show_export_dropdown = !show_export_dropdown}
640
- title="Export"
736
+ {@attach tooltip({ content: `Export`, placement: `top` })}
641
737
  >
642
738
  <Icon icon="Export" style="width: 14px" />
643
739
  </button>
@@ -665,6 +761,15 @@ let hint_config = $derived(sort_hint
665
761
  <Icon icon="Download" style="width: 12px" /> JSON
666
762
  </button>
667
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>
668
773
  </div>
669
774
  {/if}
670
775
  </div>
@@ -686,6 +791,106 @@ let hint_config = $derived(sort_hint
686
791
  {/if}
687
792
  </section>
688
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
+
689
894
  {@render sort_hint_element(`top`)}
690
895
 
691
896
  <div
@@ -707,20 +912,24 @@ let hint_config = $derived(sort_hint
707
912
  {#if show_row_select}
708
913
  <th class="select-col"></th>
709
914
  {/if}
710
- {#each visible_columns as
711
- { label, group, description, sticky }
712
- (label + group)
713
- }
714
- {#if !group}
715
- <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>
716
921
  {:else}
717
- {@const group_cols = visible_columns.filter((c) => c.group === group)}
922
+ {@const group_cols = visible_columns.filter((c) =>
923
+ c.group === col.group
924
+ )}
718
925
  <!-- Only render the group header once for each group by checking if this is the first column of this group -->
719
- {#if visible_columns.findIndex((c) => c.group === group) ===
926
+ {#if visible_columns.findIndex((c) => c.group === col.group) ===
720
927
  visible_columns.findIndex((c) =>
721
- c.group === group && c.label === label
928
+ c.group === col.group && c.label === col.label
722
929
  )}
723
- <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>
724
933
  {/if}
725
934
  {/if}
726
935
  {/each}
@@ -729,11 +938,21 @@ let hint_config = $derived(sort_hint
729
938
  <!-- Second level headers -->
730
939
  <tr>
731
940
  {#if show_row_select}
732
- <th class="select-col" title="Select rows">
733
- <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
+ />
734
950
  </th>
735
951
  {/if}
736
- {#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))}
737
956
  {@const col_id = get_col_id(col)}
738
957
  {@const drag_side = drag_over_col_id === col_id
739
958
  ? get_drag_side(col_id)
@@ -743,6 +962,20 @@ let hint_config = $derived(sort_hint
743
962
  title={col.description}
744
963
  tabindex={col.sortable === false ? undefined : 0}
745
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
+ }}
746
979
  onclick={(event) => {
747
980
  if (!drag_col_id && !resize_col_id) {
748
981
  sort_rows(
@@ -788,7 +1021,11 @@ let hint_config = $derived(sort_hint
788
1021
  event.currentTarget.removeAttribute(`aria-grabbed`)
789
1022
  }}
790
1023
  >
791
- {@html col.label}
1024
+ {#if header_cell}
1025
+ {@render header_cell({ col })}
1026
+ {:else}
1027
+ {@html col.label}
1028
+ {/if}
792
1029
  {@html sort_indicator(col, sort_state)}
793
1030
  <!-- Column resize handle -->
794
1031
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
@@ -806,14 +1043,32 @@ let hint_config = $derived(sort_hint
806
1043
  </tr>
807
1044
  </thead>
808
1045
  <tbody>
809
- {#each paginated_data as row (get_row_id(row))}
1046
+ {#each paginated_data as row, row_idx (get_row_id(row))}
810
1047
  {@const row_selected = show_row_select && is_row_selected(row)}
811
1048
  <tr
812
1049
  animate:flip={{ duration: 500 }}
813
1050
  style={row.style}
814
1051
  class={row.class ?? ``}
815
1052
  class:selected={row_selected}
1053
+ tabindex={onrowclick ? 0 : undefined}
1054
+ onclick={onrowclick ? (event) => onrowclick(event, row) : undefined}
816
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}
817
1072
  >
818
1073
  {#if show_row_select}
819
1074
  <td class="select-col">
@@ -824,7 +1079,12 @@ let hint_config = $derived(sort_hint
824
1079
  />
825
1080
  </td>
826
1081
  {/if}
827
- {#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))}
828
1088
  {@const val = row[get_col_id(col)]}
829
1089
  {@const color = calc_color(val, col)}
830
1090
  {@const col_width = column_widths[get_col_id(col)]}
@@ -856,8 +1116,24 @@ let hint_config = $derived(sort_hint
856
1116
  </td>
857
1117
  {/each}
858
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}
859
1130
  {/each}
860
1131
  </tbody>
1132
+ {#if footer}
1133
+ <tfoot>
1134
+ {@render footer()}
1135
+ </tfoot>
1136
+ {/if}
861
1137
  </table>
862
1138
  </div>
863
1139
 
@@ -914,8 +1190,47 @@ let hint_config = $derived(sort_hint
914
1190
  >
915
1191
  »
916
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}
917
1208
  </div>
918
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
+ />
919
1234
  </div>
920
1235
 
921
1236
  <style>
@@ -1001,6 +1316,13 @@ let hint_config = $derived(sort_hint
1001
1316
  tbody tr:hover {
1002
1317
  filter: var(--heatmap-row-hover-filter, brightness(1.1));
1003
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
+ }
1004
1326
  td[data-sort-value] {
1005
1327
  cursor: default;
1006
1328
  }
@@ -1018,7 +1340,7 @@ let hint_config = $derived(sort_hint
1018
1340
  justify-content: flex-end;
1019
1341
  align-items: center;
1020
1342
  gap: 2px;
1021
- margin-bottom: 4px;
1343
+ margin-bottom: 1px;
1022
1344
  opacity: 0;
1023
1345
  pointer-events: none;
1024
1346
  transition: opacity 0.15s;
@@ -1029,21 +1351,21 @@ let hint_config = $derived(sort_hint
1029
1351
  pointer-events: auto;
1030
1352
  }
1031
1353
  .icon-btn {
1032
- padding: 5px 8px;
1354
+ padding: 2px 4px;
1033
1355
  border: none;
1034
- border-radius: 4px;
1356
+ border-radius: 3px;
1035
1357
  background: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.1));
1036
1358
  color: light-dark(#333, #ddd);
1037
1359
  cursor: pointer;
1038
1360
  display: flex;
1039
1361
  align-items: center;
1040
1362
  justify-content: center;
1041
- gap: 4px;
1042
- font-size: 0.95em;
1363
+ gap: 2px;
1364
+ font-size: 0.8em;
1043
1365
  }
1044
1366
  .icon-btn :global(svg) {
1045
- width: 16px;
1046
- height: 16px;
1367
+ width: 12px;
1368
+ height: 12px;
1047
1369
  }
1048
1370
  .icon-btn:hover {
1049
1371
  background: light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.2));
@@ -1105,13 +1427,13 @@ let hint_config = $derived(sort_hint
1105
1427
  gap: 6px;
1106
1428
  }
1107
1429
  .search-input {
1108
- padding: 5px 8px;
1430
+ padding: 2px 4px;
1109
1431
  border: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.2));
1110
- border-radius: 4px;
1432
+ border-radius: 3px;
1111
1433
  background: light-dark(rgba(255, 255, 255, 0.9), rgba(0, 0, 0, 0.3));
1112
1434
  color: light-dark(#333, #eee);
1113
- font-size: 0.95em;
1114
- width: 120px;
1435
+ font-size: 0.8em;
1436
+ width: 110px;
1115
1437
  box-sizing: border-box;
1116
1438
  }
1117
1439
  .search-input:focus {
@@ -1218,6 +1540,23 @@ let hint_config = $derived(sort_hint
1218
1540
  font-size: 0.85em;
1219
1541
  }
1220
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
+ }
1221
1560
  /* Column resize */
1222
1561
  .resize-handle {
1223
1562
  position: absolute;
@@ -1255,4 +1594,25 @@ let hint_config = $derived(sort_hint
1255
1594
  transform: rotate(360deg);
1256
1595
  }
1257
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
+ }
1258
1618
  </style>