matterviz 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmptyState.svelte +10 -4
- package/dist/FilePicker.svelte +48 -60
- package/dist/Icon.svelte +7 -4
- package/dist/MillerIndexInput.svelte +2 -2
- package/dist/api/optimade.js +10 -7
- package/dist/app.css +68 -0
- package/dist/brillouin/BrillouinZone.svelte +90 -91
- package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
- package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
- package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
- package/dist/brillouin/BrillouinZoneScene.svelte +46 -68
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/compute.js +10 -16
- package/dist/brillouin/geometry.js +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte +48 -50
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +61 -169
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +559 -755
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
- package/dist/chempot-diagram/async-compute.svelte.js +13 -14
- package/dist/chempot-diagram/color.d.ts +21 -1
- package/dist/chempot-diagram/color.js +70 -1
- package/dist/chempot-diagram/compute.d.ts +7 -0
- package/dist/chempot-diagram/compute.js +85 -26
- package/dist/chempot-diagram/controls-state.svelte.js +1 -1
- package/dist/chempot-diagram/export.js +1 -6
- package/dist/chempot-diagram/temperature.js +9 -10
- package/dist/colors/index.d.ts +0 -4
- package/dist/colors/index.js +19 -20
- package/dist/composition/BarChart.svelte +30 -26
- package/dist/composition/BubbleChart.svelte +17 -17
- package/dist/composition/Composition.svelte +10 -19
- package/dist/composition/Formula.svelte +20 -27
- package/dist/composition/FormulaFilter.svelte +115 -184
- package/dist/composition/PieChart.svelte +55 -41
- package/dist/composition/format.js +7 -9
- package/dist/composition/index.d.ts +2 -0
- package/dist/composition/index.js +14 -0
- package/dist/composition/parse.d.ts +2 -2
- package/dist/composition/parse.js +61 -115
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +17 -26
- package/dist/convex-hull/ConvexHull.svelte +12 -15
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +93 -94
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +127 -186
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +131 -180
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
- package/dist/convex-hull/ConvexHullControls.svelte +46 -48
- package/dist/convex-hull/ConvexHullStats.svelte +193 -195
- package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
- package/dist/convex-hull/GasPressureControls.svelte +5 -12
- package/dist/convex-hull/StructurePopup.svelte +18 -22
- package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
- package/dist/convex-hull/TemperatureSlider.svelte +4 -10
- package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
- package/dist/convex-hull/gas-thermodynamics.js +21 -53
- package/dist/convex-hull/helpers.d.ts +8 -10
- package/dist/convex-hull/helpers.js +37 -43
- package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
- package/dist/convex-hull/thermodynamics.js +37 -44
- package/dist/coordination/CoordinationBarPlot.svelte +49 -53
- package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
- package/dist/coordination/calc-coordination.d.ts +1 -1
- package/dist/coordination/calc-coordination.js +28 -25
- package/dist/element/BohrAtom.svelte +5 -9
- package/dist/element/ElementHeading.svelte +4 -1
- package/dist/element/ElementPhoto.svelte +11 -9
- package/dist/element/ElementStats.svelte +12 -10
- package/dist/element/ElementTile.svelte +44 -67
- package/dist/element/Nucleus.svelte +4 -2
- package/dist/element/data.d.ts +3 -2
- package/dist/element/data.js +5 -1
- package/dist/element/helpers.d.ts +4 -0
- package/dist/element/helpers.js +18 -0
- package/dist/element/index.d.ts +2 -5
- package/dist/element/index.js +4 -19
- package/dist/feedback/DragOverlay.svelte +5 -5
- package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
- package/dist/feedback/StatusMessage.svelte +7 -4
- package/dist/fermi-surface/FermiSlice.svelte +21 -18
- package/dist/fermi-surface/FermiSurface.svelte +35 -44
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +7 -13
- package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +64 -136
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
- package/dist/fermi-surface/compute.js +25 -58
- package/dist/fermi-surface/parse.js +54 -88
- package/dist/file-viewer/JsonBrowser.svelte +1043 -0
- package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
- package/dist/file-viewer/PlotPanel.svelte +350 -0
- package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
- package/dist/file-viewer/detect.d.ts +11 -0
- package/dist/file-viewer/detect.js +381 -0
- package/dist/file-viewer/eligibility.d.ts +4 -0
- package/dist/file-viewer/eligibility.js +53 -0
- package/dist/file-viewer/host-protocol.d.ts +50 -0
- package/dist/file-viewer/host-transfer.d.ts +25 -0
- package/dist/file-viewer/host-transfer.js +42 -0
- package/dist/file-viewer/index.d.ts +5 -0
- package/dist/file-viewer/index.js +5 -0
- package/dist/file-viewer/main.d.ts +44 -0
- package/dist/file-viewer/main.js +535 -0
- package/dist/file-viewer/parse.d.ts +9 -0
- package/dist/file-viewer/parse.js +170 -0
- package/dist/file-viewer/plot-utils.d.ts +24 -0
- package/dist/file-viewer/plot-utils.js +191 -0
- package/dist/file-viewer/types.d.ts +4 -0
- package/dist/file-viewer/types.js +5 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +153 -219
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/index.d.ts +5 -0
- package/dist/icons.d.ts +4 -0
- package/dist/icons.js +4 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/io/ExportPane.svelte +11 -7
- package/dist/io/decompress.d.ts +4 -1
- package/dist/io/decompress.js +24 -36
- package/dist/io/export.d.ts +11 -4
- package/dist/io/export.js +75 -19
- package/dist/io/fetch.d.ts +0 -3
- package/dist/io/fetch.js +0 -11
- package/dist/io/file-drop.js +60 -35
- package/dist/io/index.d.ts +0 -1
- package/dist/io/index.js +2 -9
- package/dist/io/is-binary.d.ts +9 -0
- package/dist/io/is-binary.js +45 -1
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +40 -40
- package/dist/isosurface/Isosurface.svelte +700 -215
- package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
- package/dist/isosurface/IsosurfaceControls.svelte +590 -116
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
- package/dist/isosurface/VolumeSlice.svelte +224 -0
- package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
- package/dist/isosurface/coloring.d.ts +20 -0
- package/dist/isosurface/coloring.js +118 -0
- package/dist/isosurface/geometry-worker-types.d.ts +38 -0
- package/dist/isosurface/geometry.worker.d.ts +1 -0
- package/dist/isosurface/geometry.worker.js +59 -0
- package/dist/isosurface/grid.d.ts +10 -0
- package/dist/isosurface/grid.js +31 -0
- package/dist/isosurface/index.d.ts +4 -0
- package/dist/isosurface/index.js +4 -0
- package/dist/isosurface/parse-vaspwave.d.ts +3 -0
- package/dist/isosurface/parse-vaspwave.js +138 -0
- package/dist/isosurface/parse.js +6 -6
- package/dist/isosurface/profile.d.ts +12 -0
- package/dist/isosurface/profile.js +19 -0
- package/dist/isosurface/sampling.d.ts +34 -0
- package/dist/isosurface/sampling.js +408 -0
- package/dist/isosurface/slice-rendering.d.ts +13 -0
- package/dist/isosurface/slice-rendering.js +79 -0
- package/dist/isosurface/slice.d.ts +22 -2
- package/dist/isosurface/slice.js +200 -116
- package/dist/isosurface/types.d.ts +24 -1
- package/dist/isosurface/types.js +120 -8
- package/dist/json-path.d.ts +4 -0
- package/dist/json-path.js +89 -0
- package/dist/labels.d.ts +6 -2
- package/dist/labels.js +44 -14
- package/dist/layout/FullscreenButton.svelte +2 -1
- package/dist/layout/FullscreenToggle.svelte +10 -4
- package/dist/layout/InfoCard.svelte +8 -21
- package/dist/layout/InfoTag.svelte +1 -3
- package/dist/layout/NumberRangeInput.svelte +46 -0
- package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
- package/dist/layout/PropertyFilter.svelte +2 -6
- package/dist/layout/SettingsSection.svelte +57 -62
- package/dist/layout/SettingsSection.svelte.d.ts +6 -7
- package/dist/layout/SubpageGrid.svelte +3 -1
- package/dist/layout/ViewerChrome.svelte +16 -11
- package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
- package/dist/layout/fullscreen.js +3 -5
- package/dist/layout/fullscreen.svelte.js +1 -2
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +22 -59
- package/dist/layout/json-tree/JsonTree.svelte +101 -172
- package/dist/layout/json-tree/JsonValue.svelte +9 -32
- package/dist/layout/json-tree/utils.d.ts +0 -3
- package/dist/layout/json-tree/utils.js +50 -188
- package/dist/marching-cubes.d.ts +8 -1
- package/dist/marching-cubes.js +115 -48
- package/dist/math.d.ts +5 -2
- package/dist/math.js +48 -38
- package/dist/overlays/ContextMenu.svelte +19 -34
- package/dist/overlays/CopyButton.svelte +11 -5
- package/dist/overlays/DragControlTab.svelte +6 -1
- package/dist/overlays/DraggablePane.svelte +110 -74
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -1
- package/dist/overlays/GlassChip.svelte +32 -0
- package/dist/overlays/GlassChip.svelte.d.ts +8 -0
- package/dist/overlays/InfoPaneCards.svelte +11 -23
- package/dist/overlays/index.d.ts +7 -0
- package/dist/overlays/index.js +26 -0
- package/dist/overlays/portal.d.ts +2 -0
- package/dist/overlays/portal.js +14 -0
- package/dist/periodic-table/PeriodicTable.svelte +113 -145
- package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
- package/dist/periodic-table/PeriodicTableControls.svelte +37 -115
- package/dist/periodic-table/TableInset.svelte +9 -4
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +68 -105
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
- package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
- package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
- package/dist/phase-diagram/build-diagram.js +4 -9
- package/dist/phase-diagram/diagram-input.d.ts +1 -2
- package/dist/phase-diagram/parse.js +15 -15
- package/dist/phase-diagram/svg-to-diagram.js +28 -27
- package/dist/phase-diagram/utils.js +2 -3
- package/dist/plot/bar/BarPlot.svelte +461 -426
- package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
- package/dist/plot/bar/BarPlotControls.svelte +1 -3
- package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
- package/dist/plot/bar/SpacegroupBarPlot.svelte +46 -77
- package/dist/plot/bar/data.d.ts +5 -4
- package/dist/plot/bar/data.js +2 -1
- package/dist/plot/box/BoxPlot.svelte +322 -226
- package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
- package/dist/plot/box/Violin.svelte.d.ts +1 -0
- package/dist/plot/box/box-plot.js +1 -1
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +2 -2
- package/dist/plot/core/axis-utils.d.ts +12 -7
- package/dist/plot/core/axis-utils.js +48 -48
- package/dist/plot/core/components/AxisLabel.svelte +82 -20
- package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
- package/dist/plot/core/components/ColorBar.svelte +101 -106
- package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
- package/dist/plot/core/components/FillArea.svelte +10 -17
- package/dist/plot/core/components/HierarchyControls.svelte +249 -0
- package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
- package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
- package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/core/components/Line.svelte +18 -9
- package/dist/plot/core/components/Line.svelte.d.ts +2 -0
- package/dist/plot/core/components/PlotAxis.svelte +40 -17
- package/dist/plot/core/components/PlotControls.svelte +65 -41
- package/dist/plot/core/components/PlotLegend.svelte +31 -35
- package/dist/plot/core/components/PlotMarginals.svelte +616 -0
- package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
- package/dist/plot/core/components/PlotTooltip.svelte +1 -1
- package/dist/plot/core/components/PortalSelect.svelte +4 -8
- package/dist/plot/core/components/ReferenceLine.svelte +27 -24
- package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
- package/dist/plot/core/components/ReferencePlane.svelte +7 -8
- package/dist/plot/core/components/ZeroLines.svelte +4 -8
- package/dist/plot/core/components/ZoomRect.svelte +1 -2
- package/dist/plot/core/components/index.d.ts +1 -0
- package/dist/plot/core/components/index.js +1 -0
- package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
- package/dist/plot/core/data-cleaning-signal.js +467 -0
- package/dist/plot/core/data-cleaning.d.ts +2 -42
- package/dist/plot/core/data-cleaning.js +27 -474
- package/dist/plot/core/data-transform.d.ts +1 -2
- package/dist/plot/core/data-transform.js +9 -15
- package/dist/plot/core/fill-utils.d.ts +3 -1
- package/dist/plot/core/fill-utils.js +28 -6
- package/dist/plot/core/index.d.ts +1 -0
- package/dist/plot/core/index.js +1 -0
- package/dist/plot/core/interactions.js +0 -2
- package/dist/plot/core/layout.d.ts +15 -6
- package/dist/plot/core/layout.js +57 -38
- package/dist/plot/core/marginals.d.ts +170 -0
- package/dist/plot/core/marginals.js +436 -0
- package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
- package/dist/plot/core/pan-zoom.svelte.js +34 -17
- package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -21
- package/dist/plot/core/reference-line.js +1 -1
- package/dist/plot/core/scales.d.ts +23 -0
- package/dist/plot/core/scales.js +39 -13
- package/dist/plot/core/svg.js +0 -1
- package/dist/plot/core/types/fills.d.ts +101 -0
- package/dist/plot/core/types/fills.js +15 -0
- package/dist/plot/core/types/plot-3d.d.ts +87 -0
- package/dist/plot/core/types/plot-3d.js +4 -0
- package/dist/plot/core/types/reference-lines.d.ts +140 -0
- package/dist/plot/core/types/reference-lines.js +9 -0
- package/dist/plot/core/types.d.ts +29 -324
- package/dist/plot/core/types.js +23 -20
- package/dist/plot/core/utils/hierarchy-chart.d.ts +52 -0
- package/dist/plot/core/utils/hierarchy-chart.js +191 -0
- package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
- package/dist/plot/core/utils/hierarchy-labels.js +100 -0
- package/dist/plot/core/utils/label-placement.js +2 -2
- package/dist/plot/histogram/Histogram.svelte +314 -340
- package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
- package/dist/plot/histogram/HistogramControls.svelte +30 -28
- package/dist/plot/histogram/histogram.d.ts +31 -0
- package/dist/plot/histogram/histogram.js +70 -0
- package/dist/plot/histogram/index.d.ts +1 -0
- package/dist/plot/histogram/index.js +1 -0
- package/dist/plot/index.d.ts +1 -0
- package/dist/plot/index.js +1 -0
- package/dist/plot/sankey/Sankey.svelte +78 -69
- package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
- package/dist/plot/sankey/SankeyControls.svelte +22 -16
- package/dist/plot/sankey/sankey.js +13 -9
- package/dist/plot/scatter/BinnedScatterPlot.svelte +311 -141
- package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
- package/dist/plot/scatter/ElementScatter.svelte +4 -1
- package/dist/plot/scatter/ScatterPlot.svelte +517 -410
- package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
- package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
- package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/scatter/ScatterPoint.svelte +16 -9
- package/dist/plot/scatter/adaptive-density.d.ts +1 -1
- package/dist/plot/scatter/adaptive-density.js +36 -20
- package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
- package/dist/plot/scatter/binned-scatter-types.js +6 -1
- package/dist/plot/scatter/scatter-data.d.ts +1 -1
- package/dist/plot/scatter/scatter-data.js +8 -7
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
- package/dist/plot/sunburst/Sunburst.svelte +269 -347
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
- package/dist/plot/sunburst/index.d.ts +1 -1
- package/dist/plot/sunburst/index.js +2 -1
- package/dist/plot/sunburst/render.d.ts +2 -2
- package/dist/plot/sunburst/render.js +47 -22
- package/dist/plot/sunburst/sunburst.d.ts +5 -1
- package/dist/plot/sunburst/sunburst.js +10 -3
- package/dist/plot/treemap/Treemap.svelte +1022 -0
- package/dist/plot/treemap/Treemap.svelte.d.ts +90 -0
- package/dist/plot/treemap/index.d.ts +4 -0
- package/dist/plot/treemap/index.js +4 -0
- package/dist/plot/treemap/labels.d.ts +38 -0
- package/dist/plot/treemap/labels.js +80 -0
- package/dist/plot/treemap/treemap.d.ts +28 -0
- package/dist/plot/treemap/treemap.js +102 -0
- package/dist/rdf/RdfPlot.svelte +37 -59
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/calc-rdf.js +63 -52
- package/dist/rdf/index.d.ts +0 -1
- package/dist/sanitize.js +85 -27
- package/dist/scene/SceneCamera.svelte +1 -1
- package/dist/scene/SceneCamera.svelte.d.ts +1 -1
- package/dist/scene/bind-renderer.svelte.js +14 -6
- package/dist/scene/index.d.ts +1 -2
- package/dist/scene/index.js +1 -1
- package/dist/scene/{props.js → props.svelte.js} +15 -1
- package/dist/settings.d.ts +19 -9
- package/dist/settings.js +229 -525
- package/dist/spectral/Bands.svelte +207 -221
- package/dist/spectral/BandsAndDos.svelte +11 -9
- package/dist/spectral/BrillouinBandsDos.svelte +19 -42
- package/dist/spectral/Dos.svelte +92 -93
- package/dist/spectral/helpers.d.ts +3 -3
- package/dist/spectral/helpers.js +65 -140
- package/dist/state.svelte.js +5 -18
- package/dist/structure/Arrow.svelte +5 -18
- package/dist/structure/ArrowInstances.svelte +160 -0
- package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
- package/dist/structure/AtomLegend.svelte +120 -85
- package/dist/structure/AtomLegend.svelte.d.ts +2 -3
- package/dist/structure/Bond.svelte +31 -23
- package/dist/structure/CanvasTooltip.svelte +15 -3
- package/dist/structure/CellSelect.svelte +45 -35
- package/dist/structure/Cylinder.svelte +24 -40
- package/dist/structure/InstancedAtoms.svelte +114 -0
- package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
- package/dist/structure/Lattice.svelte +85 -149
- package/dist/structure/SiteLabels.svelte +122 -0
- package/dist/structure/SiteLabels.svelte.d.ts +18 -0
- package/dist/structure/Structure.svelte +636 -504
- package/dist/structure/Structure.svelte.d.ts +4 -2
- package/dist/structure/StructureCarousel.svelte +637 -0
- package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
- package/dist/structure/StructureControls.svelte +165 -425
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureExportPane.svelte +17 -21
- package/dist/structure/StructureInfoPane.svelte +128 -133
- package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
- package/dist/structure/StructureScene.svelte +535 -578
- package/dist/structure/StructureScene.svelte.d.ts +6 -2
- package/dist/structure/StructureViewport.svelte +451 -0
- package/dist/structure/StructureViewport.svelte.d.ts +65 -0
- package/dist/structure/atom-label-placement.d.ts +16 -12
- package/dist/structure/atom-label-placement.js +70 -37
- package/dist/structure/atom-properties.d.ts +2 -0
- package/dist/structure/atom-properties.js +105 -38
- package/dist/structure/bond-order-perception.js +3 -8
- package/dist/structure/bonding.d.ts +9 -3
- package/dist/structure/bonding.js +95 -46
- package/dist/structure/edit-history.d.ts +6 -0
- package/dist/structure/edit-history.js +27 -0
- package/dist/structure/export.d.ts +1 -0
- package/dist/structure/export.js +89 -105
- package/dist/structure/format-detect.d.ts +1 -0
- package/dist/structure/format-detect.js +20 -0
- package/dist/structure/geometry.d.ts +9 -0
- package/dist/structure/geometry.js +27 -0
- package/dist/structure/index.d.ts +16 -1
- package/dist/structure/index.js +15 -10
- package/dist/structure/measure.d.ts +1 -1
- package/dist/structure/measure.js +6 -12
- package/dist/structure/parse.d.ts +2 -1
- package/dist/structure/parse.js +272 -313
- package/dist/structure/partial-occupancy.js +10 -5
- package/dist/structure/pbc.js +85 -107
- package/dist/structure/polyhedra.d.ts +0 -1
- package/dist/structure/polyhedra.js +7 -21
- package/dist/structure/prop-groups.d.ts +85 -0
- package/dist/structure/prop-groups.js +18 -0
- package/dist/structure/supercell.d.ts +1 -2
- package/dist/structure/supercell.js +7 -13
- package/dist/symmetry/SymmetryElements.svelte +11 -15
- package/dist/symmetry/SymmetryStats.svelte +55 -71
- package/dist/symmetry/WyckoffTable.svelte +10 -28
- package/dist/symmetry/cell-transform.js +6 -14
- package/dist/symmetry/index.d.ts +1 -0
- package/dist/symmetry/index.js +13 -10
- package/dist/symmetry/spacegroups.js +3 -2
- package/dist/table/HeatmapTable.svelte +1142 -447
- package/dist/table/HeatmapTable.svelte.d.ts +12 -2
- package/dist/table/ToggleMenu.svelte +168 -53
- package/dist/table/ToggleMenu.svelte.d.ts +1 -1
- package/dist/table/index.d.ts +21 -6
- package/dist/table/index.js +34 -21
- package/dist/theme/ThemeControl.svelte +16 -8
- package/dist/theme/embedded.d.ts +18 -0
- package/dist/theme/embedded.js +206 -0
- package/dist/theme/index.d.ts +5 -5
- package/dist/theme/index.js +5 -12
- package/dist/theme/themes.mjs +44 -66
- package/dist/tooltip/KCoords.svelte +4 -1
- package/dist/tooltip/TooltipContent.svelte +3 -6
- package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
- package/dist/tooltip/index.d.ts +8 -1
- package/dist/trajectory/Trajectory.svelte +613 -386
- package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
- package/dist/trajectory/TrajectoryError.svelte +12 -74
- package/dist/trajectory/TrajectoryExportPane.svelte +14 -32
- package/dist/trajectory/TrajectoryInfoPane.svelte +127 -164
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
- package/dist/trajectory/extract.js +61 -49
- package/dist/trajectory/format-detect.d.ts +2 -0
- package/dist/trajectory/format-detect.js +17 -13
- package/dist/trajectory/frame-reader.d.ts +2 -0
- package/dist/trajectory/frame-reader.js +35 -32
- package/dist/trajectory/helpers.d.ts +2 -0
- package/dist/trajectory/helpers.js +34 -56
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +5 -8
- package/dist/trajectory/parse/ase.js +1 -1
- package/dist/trajectory/parse/h5-utils.d.ts +13 -0
- package/dist/trajectory/parse/h5-utils.js +109 -0
- package/dist/trajectory/parse/hdf5.d.ts +1 -1
- package/dist/trajectory/parse/hdf5.js +116 -106
- package/dist/trajectory/parse/index.d.ts +6 -6
- package/dist/trajectory/parse/index.js +25 -33
- package/dist/trajectory/parse/lammps.js +8 -12
- package/dist/trajectory/parse/vasp.js +10 -7
- package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
- package/dist/trajectory/parse/vaspout-electronic.js +207 -0
- package/dist/trajectory/parse/vaspout-h5.d.ts +4 -0
- package/dist/trajectory/parse/vaspout-h5.js +281 -0
- package/dist/trajectory/parse/xyz.js +59 -9
- package/dist/trajectory/plotting.d.ts +8 -9
- package/dist/trajectory/plotting.js +113 -64
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +7 -0
- package/dist/xrd/XrdPlot.svelte +79 -118
- package/dist/xrd/broadening.js +2 -1
- package/dist/xrd/calc-xrd.js +24 -62
- package/dist/xrd/parse.d.ts +0 -1
- package/dist/xrd/parse.js +69 -95
- package/package.json +94 -34
- package/readme.md +6 -5
- package/dist/api/mp.d.ts +0 -6
- package/dist/api/mp.js +0 -22
- package/dist/plot/sunburst/SunburstControls.svelte +0 -200
- package/dist/scene/types.d.ts +0 -26
- package/dist/tooltip/types.d.ts +0 -8
- package/dist/trajectory/constants.d.ts +0 -6
- package/dist/trajectory/constants.js +0 -11
- /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
- /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
|
@@ -6,21 +6,25 @@
|
|
|
6
6
|
import { SettingsSection } from '../layout'
|
|
7
7
|
import ContextMenu from '../overlays/ContextMenu.svelte'
|
|
8
8
|
import DraggablePane from '../overlays/DraggablePane.svelte'
|
|
9
|
+
import { portal } from '../overlays/portal'
|
|
9
10
|
import type {
|
|
11
|
+
CellColor,
|
|
10
12
|
CellSnippet,
|
|
11
13
|
CellVal,
|
|
14
|
+
DateTimeFormatMode,
|
|
12
15
|
ExportData,
|
|
13
16
|
InitialSort,
|
|
14
17
|
Label,
|
|
15
18
|
MultiSortState,
|
|
16
19
|
Pagination,
|
|
20
|
+
VirtualScroll,
|
|
17
21
|
RowData,
|
|
18
22
|
Search,
|
|
19
23
|
SortHint,
|
|
20
24
|
SortState,
|
|
21
25
|
SpecialCells,
|
|
22
26
|
} from './'
|
|
23
|
-
import {
|
|
27
|
+
import { make_cell_color_scale, strip_html } from './'
|
|
24
28
|
import { sanitize_html } from '../sanitize'
|
|
25
29
|
import { normalize_unicode_minus } from '../utils'
|
|
26
30
|
import type { Snippet } from 'svelte'
|
|
@@ -84,8 +88,32 @@
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
const close_datetime_select_on_outside_pointerdown = (event: PointerEvent) => {
|
|
92
|
+
if (datetime_select_open_col_id === null) return
|
|
93
|
+
if (event.target instanceof Element && event.target.closest(`.datetime-format-control`))
|
|
94
|
+
return
|
|
95
|
+
datetime_select_open_col_id = null
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
const NUMERIC_WITH_ERROR_RE =
|
|
88
|
-
/^([-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/
|
|
99
|
+
/^(?<numeric>[-+−]?(?:\d+\.?\d*|\d*\.\d+)(?:[eE][-+−]?\d+)?)\s*(?:±|\+[-−]|\()/
|
|
100
|
+
const DATA_SORT_VALUE_RE = /data-sort-value="(?<value>[^"]*)"/
|
|
101
|
+
const DATE_ONLY_RE = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/
|
|
102
|
+
const DATE_TIME_RE =
|
|
103
|
+
/^\d{4}-\d{2}-\d{2}(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/
|
|
104
|
+
type DateTimeColumnKind = `date` | `time` | `datetime`
|
|
105
|
+
const datetime_format_modes_by_kind: Record<DateTimeColumnKind, DateTimeFormatMode[]> = {
|
|
106
|
+
date: [`date`, `relative`],
|
|
107
|
+
time: [`time`],
|
|
108
|
+
datetime: [`date`, `time`, `datetime`, `iso`, `relative`],
|
|
109
|
+
}
|
|
110
|
+
const datetime_format_labels: Record<DateTimeFormatMode, string> = {
|
|
111
|
+
date: `Date`,
|
|
112
|
+
time: `Time`,
|
|
113
|
+
datetime: `Date + time`,
|
|
114
|
+
iso: `ISO`,
|
|
115
|
+
relative: `Since now`,
|
|
116
|
+
}
|
|
89
117
|
|
|
90
118
|
const parse_numeric_string = (val: string): number | null => {
|
|
91
119
|
const numeric_str = val.match(NUMERIC_WITH_ERROR_RE)?.[1] ?? val
|
|
@@ -94,14 +122,18 @@
|
|
|
94
122
|
return isNaN(num) ? null : num
|
|
95
123
|
}
|
|
96
124
|
|
|
125
|
+
const get_data_sort_value = (val: string): string | null =>
|
|
126
|
+
val.match(DATA_SORT_VALUE_RE)?.groups?.value ?? null
|
|
127
|
+
|
|
97
128
|
// Get sort value from a cell (handles HTML data-sort-value and numbers with errors)
|
|
98
129
|
const get_sort_val = (val: CellVal): string | number => {
|
|
130
|
+
if (val instanceof Date) return val.getTime()
|
|
99
131
|
if (typeof val === `string`) {
|
|
100
132
|
// Check for HTML data-sort-value attribute first
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
const num = Number(
|
|
104
|
-
return isNaN(num) ?
|
|
133
|
+
const sort_attr = get_data_sort_value(val)
|
|
134
|
+
if (sort_attr != null) {
|
|
135
|
+
const num = Number(sort_attr)
|
|
136
|
+
return isNaN(num) ? sort_attr : num
|
|
105
137
|
}
|
|
106
138
|
const num = parse_numeric_string(val)
|
|
107
139
|
if (num !== null) return num
|
|
@@ -109,6 +141,9 @@
|
|
|
109
141
|
return val as string | number
|
|
110
142
|
}
|
|
111
143
|
|
|
144
|
+
const get_cell_sort_attr = (val: CellVal): CellVal | number | null =>
|
|
145
|
+
is_html_str(val) ? null : val instanceof Date ? val.getTime() : val
|
|
146
|
+
|
|
112
147
|
let {
|
|
113
148
|
data = $bindable([]),
|
|
114
149
|
columns = [],
|
|
@@ -122,14 +157,20 @@
|
|
|
122
157
|
default_num_format = `.3`,
|
|
123
158
|
show_heatmap = $bindable(true),
|
|
124
159
|
heatmap_class = `heatmap`,
|
|
160
|
+
onrowpointerdown,
|
|
125
161
|
onrowclick,
|
|
126
162
|
onrowdblclick,
|
|
163
|
+
row_title,
|
|
127
164
|
column_order = $bindable([]),
|
|
128
165
|
export_data = false,
|
|
129
166
|
show_column_toggle = false,
|
|
130
167
|
search = false,
|
|
168
|
+
search_query = $bindable(``),
|
|
131
169
|
show_row_select = false,
|
|
132
170
|
pagination = false,
|
|
171
|
+
virtual = false,
|
|
172
|
+
on_visible_range,
|
|
173
|
+
controls_target = undefined,
|
|
133
174
|
selected_rows = $bindable([]),
|
|
134
175
|
hidden_columns = $bindable([]),
|
|
135
176
|
scroll_style,
|
|
@@ -160,8 +201,12 @@
|
|
|
160
201
|
default_num_format?: string
|
|
161
202
|
show_heatmap?: boolean
|
|
162
203
|
heatmap_class?: string
|
|
204
|
+
onrowpointerdown?: (event: PointerEvent, row: RowData) => void
|
|
163
205
|
onrowclick?: (event: MouseEvent | KeyboardEvent, row: RowData) => void
|
|
164
206
|
onrowdblclick?: (event: MouseEvent, row: RowData) => void
|
|
207
|
+
// Per-row hover tooltip content (rendered via the table tooltip
|
|
208
|
+
// attachment; `\r` breaks lines, HTML must be pre-escaped by the caller)
|
|
209
|
+
row_title?: (row: RowData) => string | null | undefined
|
|
165
210
|
// Array of column IDs to control display order. IDs are derived as:
|
|
166
211
|
// - Ungrouped columns: col.key ?? col.label
|
|
167
212
|
// - Grouped columns: `${col.key ?? col.label} (${col.group})`
|
|
@@ -170,8 +215,22 @@
|
|
|
170
215
|
export_data?: ExportData
|
|
171
216
|
show_column_toggle?: boolean
|
|
172
217
|
search?: Search
|
|
218
|
+
// Current search/filter query. Bindable so parents can control or persist it.
|
|
219
|
+
search_query?: string
|
|
173
220
|
show_row_select?: boolean
|
|
174
221
|
pagination?: Pagination
|
|
222
|
+
// Opt-in infinite-scroll row virtualization. Renders only the rows near the
|
|
223
|
+
// viewport plus spacer rows, so DOM size stays bounded for any data length.
|
|
224
|
+
// Inactive when pagination is enabled. Off by default (every row renders);
|
|
225
|
+
// pass true (or a config object) to enable.
|
|
226
|
+
virtual?: VirtualScroll
|
|
227
|
+
// Notifies the parent which slice of the sorted+filtered rows is rendered
|
|
228
|
+
// (e.g. to progressively fetch more data as the user scrolls near the end).
|
|
229
|
+
on_visible_range?: (range: { start: number; end: number; total: number }) => void
|
|
230
|
+
// Host element to render the search/export/settings buttons into (e.g. an
|
|
231
|
+
// embedding panel's own header) instead of a row above the table. When set,
|
|
232
|
+
// the buttons are always visible; when unset they render inline as usual.
|
|
233
|
+
controls_target?: HTMLElement | null
|
|
175
234
|
selected_rows?: RowData[]
|
|
176
235
|
hidden_columns?: string[]
|
|
177
236
|
scroll_style?: string
|
|
@@ -215,8 +274,7 @@
|
|
|
215
274
|
if (!container_el) return
|
|
216
275
|
const read_page_bg = () => {
|
|
217
276
|
if (!container_el) return
|
|
218
|
-
const page_bg = getComputedStyle(container_el).getPropertyValue(`--page-bg`)
|
|
219
|
-
.trim()
|
|
277
|
+
const page_bg = getComputedStyle(container_el).getPropertyValue(`--page-bg`).trim()
|
|
220
278
|
page_bg_lum = luminance(page_bg || `white`)
|
|
221
279
|
}
|
|
222
280
|
read_page_bg()
|
|
@@ -251,30 +309,31 @@
|
|
|
251
309
|
: null,
|
|
252
310
|
)
|
|
253
311
|
|
|
254
|
-
// Mutable page size
|
|
312
|
+
// Mutable page size: user can change it, but parent pagination.page_size changes still resync.
|
|
255
313
|
let effective_page_size = $derived(pagination_config?.page_size ?? 25)
|
|
256
314
|
|
|
257
315
|
// Normalize search config
|
|
258
316
|
let search_config = $derived(
|
|
259
317
|
search
|
|
260
318
|
? {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
319
|
+
placeholder: `Filter...`,
|
|
320
|
+
expanded: false,
|
|
321
|
+
keys: undefined as string[] | undefined,
|
|
322
|
+
fuzzy: false,
|
|
323
|
+
...(typeof search === `object` ? search : {}),
|
|
324
|
+
}
|
|
265
325
|
: null,
|
|
266
326
|
)
|
|
267
327
|
|
|
268
328
|
// Normalize export_data config
|
|
269
329
|
type ExportFormat = `csv` | `json`
|
|
270
|
-
const default_formats: ExportFormat[] = [`csv`, `json`]
|
|
271
330
|
let export_config = $derived(
|
|
272
331
|
export_data
|
|
273
332
|
? {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
333
|
+
formats: [`csv`, `json`] as ExportFormat[],
|
|
334
|
+
filename: `table-export`,
|
|
335
|
+
...(typeof export_data === `object` ? export_data : {}),
|
|
336
|
+
}
|
|
278
337
|
: null,
|
|
279
338
|
)
|
|
280
339
|
|
|
@@ -282,16 +341,13 @@
|
|
|
282
341
|
// This ensures immediate sorting on first render without waiting for effects
|
|
283
342
|
let sort_state = $derived<SortState>({
|
|
284
343
|
column: sort.column || initial_sort_config?.column || ``,
|
|
285
|
-
ascending: sort.column
|
|
286
|
-
? sort.dir !== `desc`
|
|
287
|
-
: initial_sort_config?.direction !== `desc`,
|
|
344
|
+
ascending: sort.column ? sort.dir !== `desc` : initial_sort_config?.direction !== `desc`,
|
|
288
345
|
})
|
|
289
346
|
|
|
290
347
|
// Multi-column sort state (for Shift+click)
|
|
291
348
|
let multi_sort = $state<MultiSortState>([])
|
|
292
349
|
|
|
293
|
-
// Search/filter state
|
|
294
|
-
let search_query = $state(``)
|
|
350
|
+
// Search/filter state (query itself is the bindable search_query prop)
|
|
295
351
|
let search_expanded = $derived(search_config?.expanded ?? false)
|
|
296
352
|
|
|
297
353
|
// Pagination state
|
|
@@ -307,6 +363,10 @@
|
|
|
307
363
|
// Per-column color scale overrides
|
|
308
364
|
let color_scale_overrides = new SvelteMap<string, string>()
|
|
309
365
|
|
|
366
|
+
// Per-column date/time display overrides (user-toggled via header)
|
|
367
|
+
let datetime_format_overrides = new SvelteMap<string, DateTimeFormatMode>()
|
|
368
|
+
let datetime_select_open_col_id = $state<string | null>(null)
|
|
369
|
+
|
|
310
370
|
const color_scale_options = [
|
|
311
371
|
`interpolateViridis`,
|
|
312
372
|
`interpolatePlasma`,
|
|
@@ -320,11 +380,7 @@
|
|
|
320
380
|
] as const
|
|
321
381
|
|
|
322
382
|
// Columns that have a color gradient
|
|
323
|
-
let colored_columns = $derived(
|
|
324
|
-
columns.filter((col) =>
|
|
325
|
-
col.color_scale !== null && col.color_scale !== undefined
|
|
326
|
-
),
|
|
327
|
-
)
|
|
383
|
+
let colored_columns = $derived(columns.filter((col) => col.color_scale != null))
|
|
328
384
|
|
|
329
385
|
// Column resize state
|
|
330
386
|
let resize_col_id = $state<string | null>(null)
|
|
@@ -347,6 +403,169 @@
|
|
|
347
403
|
// Helper to make column IDs (needed since column labels in different groups can be repeated)
|
|
348
404
|
const get_col_id = (col: Label) =>
|
|
349
405
|
col.group ? `${col.key ?? col.label} (${col.group})` : (col.key ?? col.label)
|
|
406
|
+
const get_datetime_label_id = (col_id: string) =>
|
|
407
|
+
`datetime-format-label-${encodeURIComponent(col_id)}`
|
|
408
|
+
|
|
409
|
+
const has_explicit_datetime_format = (col: Label): boolean =>
|
|
410
|
+
col.format_type === `datetime` || Boolean(col.datetime_format)
|
|
411
|
+
|
|
412
|
+
const normalize_timestamp = (val: number): number | null => {
|
|
413
|
+
if (!Number.isFinite(val)) return null
|
|
414
|
+
const abs = Math.abs(val)
|
|
415
|
+
if (abs >= 1_000_000_000_000 && abs < 100_000_000_000_000) return val
|
|
416
|
+
if (abs >= 1_000_000_000 && abs < 1_000_000_000_000) return val * 1000
|
|
417
|
+
return null
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const parse_datetime_string = (val: string): number | null => {
|
|
421
|
+
const clean = strip_html(val).trim()
|
|
422
|
+
const date_only = clean.match(DATE_ONLY_RE)
|
|
423
|
+
if (date_only?.groups) {
|
|
424
|
+
const year = Number(date_only.groups.year)
|
|
425
|
+
const month = Number(date_only.groups.month)
|
|
426
|
+
const day = Number(date_only.groups.day)
|
|
427
|
+
return new Date(year, month - 1, day).getTime()
|
|
428
|
+
}
|
|
429
|
+
if (!DATE_TIME_RE.test(clean)) return null
|
|
430
|
+
const parsed = Date.parse(
|
|
431
|
+
clean.replace(` `, `T`).replace(/\.(?<millis>\d{3})\d+/, `.$<millis>`),
|
|
432
|
+
)
|
|
433
|
+
return Number.isNaN(parsed) ? null : parsed
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const value_datetime_kind = (val: CellVal, col: Label): DateTimeColumnKind | null => {
|
|
437
|
+
if (typeof val === `string` && DATE_ONLY_RE.test(strip_html(val).trim())) {
|
|
438
|
+
return `date`
|
|
439
|
+
}
|
|
440
|
+
return parse_datetime_val(val, col) === null ? null : `datetime`
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const parse_datetime_val = (val: CellVal, col: Label): number | null => {
|
|
444
|
+
if (val instanceof Date) return Number.isNaN(val.getTime()) ? null : val.getTime()
|
|
445
|
+
if (typeof val === `number`) {
|
|
446
|
+
return has_explicit_datetime_format(col) ? normalize_timestamp(val) : null
|
|
447
|
+
}
|
|
448
|
+
if (typeof val !== `string`) return null
|
|
449
|
+
|
|
450
|
+
const parsed_text = parse_datetime_string(val)
|
|
451
|
+
if (parsed_text !== null) return parsed_text
|
|
452
|
+
if (!has_explicit_datetime_format(col)) return null
|
|
453
|
+
|
|
454
|
+
const sort_attr = get_data_sort_value(val)
|
|
455
|
+
return normalize_timestamp(Number(sort_attr ?? strip_html(val).trim()))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const infer_datetime_column_kind = (col: Label): DateTimeColumnKind | null => {
|
|
459
|
+
if (col.datetime_format === `date`) return `date`
|
|
460
|
+
if (col.datetime_format === `time`) return `time`
|
|
461
|
+
if (col.datetime_format || col.format_type === `datetime`) return `datetime`
|
|
462
|
+
|
|
463
|
+
const col_id = get_col_id(col)
|
|
464
|
+
let has_date_value = false
|
|
465
|
+
for (const row of data.slice(0, 25)) {
|
|
466
|
+
const kind = value_datetime_kind(row[col_id], col)
|
|
467
|
+
if (kind === `datetime`) return `datetime`
|
|
468
|
+
if (kind === `date`) has_date_value = true
|
|
469
|
+
}
|
|
470
|
+
if (has_date_value) return `date`
|
|
471
|
+
return null
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let datetime_column_kinds = $derived.by(() => {
|
|
475
|
+
const kinds = new SvelteMap<string, DateTimeColumnKind>()
|
|
476
|
+
for (const col of columns) {
|
|
477
|
+
const kind = infer_datetime_column_kind(col)
|
|
478
|
+
if (kind) kinds.set(get_col_id(col), kind)
|
|
479
|
+
}
|
|
480
|
+
return kinds
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
const is_datetime_column = (col: Label): boolean =>
|
|
484
|
+
datetime_column_kinds.has(get_col_id(col))
|
|
485
|
+
|
|
486
|
+
const datetime_column_kind = (col: Label): DateTimeColumnKind =>
|
|
487
|
+
datetime_column_kinds.get(get_col_id(col)) ?? `datetime`
|
|
488
|
+
|
|
489
|
+
const datetime_format_options = (col: Label): DateTimeFormatMode[] =>
|
|
490
|
+
datetime_format_modes_by_kind[datetime_column_kind(col)]
|
|
491
|
+
|
|
492
|
+
const datetime_mode = (col: Label): DateTimeFormatMode => {
|
|
493
|
+
const options = datetime_format_options(col)
|
|
494
|
+
const selected =
|
|
495
|
+
datetime_format_overrides.get(get_col_id(col)) ??
|
|
496
|
+
col.datetime_format ??
|
|
497
|
+
datetime_column_kind(col)
|
|
498
|
+
return options.includes(selected) ? selected : options[0]
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function set_datetime_format(col: Label, mode: DateTimeFormatMode) {
|
|
502
|
+
if (datetime_format_options(col).includes(mode)) {
|
|
503
|
+
datetime_format_overrides.set(get_col_id(col), mode)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const pad2 = (val: number): string => String(val).padStart(2, `0`)
|
|
508
|
+
|
|
509
|
+
function format_date(date: Date): string {
|
|
510
|
+
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function format_time(date: Date): string {
|
|
514
|
+
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function format_datetime(date: Date): string {
|
|
518
|
+
return `${format_date(date)} ${format_time(date)}`
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Ticks once a minute while any column shows relative times, so "Xm ago"
|
|
522
|
+
// cells don't go stale (format granularity is minutes).
|
|
523
|
+
let relative_now_ms = $state(Date.now())
|
|
524
|
+
$effect(() => {
|
|
525
|
+
const shows_relative = columns.some(
|
|
526
|
+
(col) => is_datetime_column(col) && datetime_mode(col) === `relative`,
|
|
527
|
+
)
|
|
528
|
+
if (!shows_relative) return
|
|
529
|
+
relative_now_ms = Date.now() // refresh immediately when relative mode turns on
|
|
530
|
+
const interval = setInterval(() => (relative_now_ms = Date.now()), 60_000)
|
|
531
|
+
return () => clearInterval(interval)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
function format_since_now(timestamp: number): string {
|
|
535
|
+
const diff = relative_now_ms - timestamp
|
|
536
|
+
let remaining_minutes = Math.max(0, Math.floor(Math.abs(diff) / 60_000))
|
|
537
|
+
const parts: string[] = []
|
|
538
|
+
const units = [
|
|
539
|
+
[`y`, 365 * 24 * 60],
|
|
540
|
+
[`mo`, 30 * 24 * 60],
|
|
541
|
+
[`w`, 7 * 24 * 60],
|
|
542
|
+
[`d`, 24 * 60],
|
|
543
|
+
[`h`, 60],
|
|
544
|
+
[`m`, 1],
|
|
545
|
+
] as const
|
|
546
|
+
|
|
547
|
+
for (const [suffix, minutes_per_unit] of units) {
|
|
548
|
+
const value = Math.floor(remaining_minutes / minutes_per_unit)
|
|
549
|
+
if (value === 0 && parts.length === 0 && suffix !== `m`) continue
|
|
550
|
+
if (value > 0 || suffix === `m`) parts.push(`${value}${suffix}`)
|
|
551
|
+
remaining_minutes -= value * minutes_per_unit
|
|
552
|
+
if (parts.length >= 3) break
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return `${parts.join(` `)} ${diff >= 0 ? `ago` : `from now`}`
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function format_datetime_cell(val: CellVal, col: Label): string | null {
|
|
559
|
+
const timestamp = parse_datetime_val(val, col)
|
|
560
|
+
if (timestamp === null) return null
|
|
561
|
+
const date = new Date(timestamp)
|
|
562
|
+
const mode = datetime_mode(col)
|
|
563
|
+
if (mode === `date`) return format_date(date)
|
|
564
|
+
if (mode === `time`) return format_time(date)
|
|
565
|
+
if (mode === `datetime`) return format_datetime(date)
|
|
566
|
+
if (mode === `iso`) return date.toISOString()
|
|
567
|
+
return format_since_now(timestamp)
|
|
568
|
+
}
|
|
350
569
|
|
|
351
570
|
// Sync column_order with columns: initialize if empty, remove stale IDs, append new IDs
|
|
352
571
|
$effect(() => {
|
|
@@ -369,8 +588,11 @@
|
|
|
369
588
|
// After drag reorder, column_order differs from col_ids (default order) but the
|
|
370
589
|
// computed new_order equals the current column_order — assigning a new array
|
|
371
590
|
// reference would re-trigger this effect endlessly.
|
|
372
|
-
if (
|
|
373
|
-
new_order.
|
|
591
|
+
if (
|
|
592
|
+
new_order.length === column_order.length &&
|
|
593
|
+
new_order.every((id, idx) => id === column_order[idx])
|
|
594
|
+
)
|
|
595
|
+
return
|
|
374
596
|
|
|
375
597
|
column_order = new_order
|
|
376
598
|
})
|
|
@@ -458,23 +680,19 @@
|
|
|
458
680
|
function handle_drop(event: DragEvent, target_col: Label) {
|
|
459
681
|
event.preventDefault()
|
|
460
682
|
|
|
461
|
-
// Block cross-group (or group→ungroup) reorders to preserve group contiguity
|
|
462
|
-
if (!drag_col_id || drag_col_id === get_col_id(target_col)) {
|
|
463
|
-
reset_drag_state()
|
|
464
|
-
return
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Block cross-group reorders to preserve group contiguity
|
|
468
|
-
if (get_drag_col_group() !== target_col.group) {
|
|
469
|
-
reset_drag_state()
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
|
|
473
683
|
const target_col_id = get_col_id(target_col)
|
|
474
|
-
const drag_idx = column_order.indexOf(drag_col_id)
|
|
684
|
+
const drag_idx = drag_col_id ? column_order.indexOf(drag_col_id) : -1
|
|
475
685
|
const target_idx = column_order.indexOf(target_col_id)
|
|
476
686
|
|
|
477
|
-
|
|
687
|
+
// Block no-op drops and cross-group (or group→ungroup) reorders to
|
|
688
|
+
// preserve group contiguity
|
|
689
|
+
if (
|
|
690
|
+
!drag_col_id ||
|
|
691
|
+
drag_col_id === target_col_id ||
|
|
692
|
+
get_drag_col_group() !== target_col.group ||
|
|
693
|
+
drag_idx === -1 ||
|
|
694
|
+
target_idx === -1
|
|
695
|
+
) {
|
|
478
696
|
reset_drag_state()
|
|
479
697
|
return
|
|
480
698
|
}
|
|
@@ -490,47 +708,56 @@
|
|
|
490
708
|
reset_drag_state()
|
|
491
709
|
}
|
|
492
710
|
|
|
711
|
+
// True when every char of query appears in text in order (subsequence match),
|
|
712
|
+
// e.g. "mdla" matches "Model A". Cheap fuzzy matching for short filter queries.
|
|
713
|
+
const fuzzy_match = (text: string, query: string): boolean => {
|
|
714
|
+
let query_idx = 0
|
|
715
|
+
for (const char of text) {
|
|
716
|
+
if (char === query[query_idx] && ++query_idx === query.length) return true
|
|
717
|
+
}
|
|
718
|
+
return query.length === 0
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const row_matches_query = (row: RowData, query: string): boolean => {
|
|
722
|
+
const values = search_config?.keys
|
|
723
|
+
? search_config.keys.map((key) => row[key])
|
|
724
|
+
: Object.values(row)
|
|
725
|
+
return values.some((val) => {
|
|
726
|
+
if (val == null) return false
|
|
727
|
+
const clean_val = strip_html(String(val)).toLowerCase()
|
|
728
|
+
if (clean_val.includes(query)) return true
|
|
729
|
+
return (search_config?.fuzzy ?? false) && fuzzy_match(clean_val, query)
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
|
|
493
733
|
// Filter data based on search query
|
|
494
734
|
let filtered_data = $derived.by(() => {
|
|
495
|
-
const base_data =
|
|
496
|
-
Object.values(row).some((val) => val !== undefined)
|
|
497
|
-
) ?? []
|
|
498
|
-
|
|
499
|
-
if (!search_query.trim()) return base_data
|
|
735
|
+
const base_data =
|
|
736
|
+
data?.filter?.((row) => Object.values(row).some((val) => val !== undefined)) ?? []
|
|
500
737
|
|
|
501
738
|
const query = search_query.toLowerCase().trim()
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const clean_val = strip_html(String(val)).toLowerCase()
|
|
506
|
-
return clean_val.includes(query)
|
|
507
|
-
})
|
|
508
|
-
)
|
|
739
|
+
if (!query) return base_data
|
|
740
|
+
|
|
741
|
+
return base_data.filter((row) => row_matches_query(row, query))
|
|
509
742
|
})
|
|
510
743
|
|
|
511
744
|
let sorted_data = $derived.by(() => {
|
|
512
745
|
// Skip client-side sorting when using async onsort callback or sort_data is false
|
|
513
746
|
if (onsort || !sort_data) return filtered_data
|
|
514
747
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const sort_criteria = multi_sort.length > 0
|
|
519
|
-
? multi_sort
|
|
520
|
-
: sort_state.column
|
|
521
|
-
? [sort_state]
|
|
522
|
-
: []
|
|
523
|
-
|
|
748
|
+
// Build sort criteria: multi_sort (Shift+click) takes precedence over single sort
|
|
749
|
+
const sort_criteria =
|
|
750
|
+
multi_sort.length > 0 ? multi_sort : sort_state.column ? [sort_state] : []
|
|
524
751
|
if (sort_criteria.length === 0) return filtered_data
|
|
525
752
|
|
|
753
|
+
const valid_column_ids = new Set(ordered_columns.map(get_col_id))
|
|
526
754
|
return [...filtered_data].sort((row1, row2) => {
|
|
527
755
|
for (const { column, ascending } of sort_criteria) {
|
|
528
|
-
|
|
529
|
-
if (!
|
|
756
|
+
// criteria hold column IDs; skip stale entries referencing removed columns
|
|
757
|
+
if (!valid_column_ids.has(column)) continue
|
|
530
758
|
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
const val2 = row2[col_id]
|
|
759
|
+
const val1 = row1[column]
|
|
760
|
+
const val2 = row2[column]
|
|
534
761
|
|
|
535
762
|
if (val1 === val2) continue
|
|
536
763
|
|
|
@@ -560,17 +787,91 @@
|
|
|
560
787
|
})
|
|
561
788
|
})
|
|
562
789
|
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
790
|
+
// --- Infinite scroll (virtualized rows). Opt-in via the `virtual` prop (and
|
|
791
|
+
// inactive under pagination): only rows near the viewport render; spacer rows
|
|
792
|
+
// preserve scroll geometry so the DOM stays bounded for any row count.
|
|
793
|
+
let scroll_el = $state<HTMLDivElement>()
|
|
794
|
+
let scroll_top = $state(0)
|
|
795
|
+
let viewport_height = $state(0)
|
|
796
|
+
let avg_row_height = $state(33) // refined from rendered rows after mount
|
|
797
|
+
|
|
798
|
+
let virtual_config = $derived(
|
|
799
|
+
pagination_config || !virtual
|
|
800
|
+
? null
|
|
801
|
+
: { overscan: 10, min_window: 60, ...(typeof virtual === `object` ? virtual : {}) },
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
let virtual_range = $derived.by(() => {
|
|
805
|
+
const total = sorted_data.length
|
|
806
|
+
if (!virtual_config) return { start: 0, end: total }
|
|
807
|
+
const { overscan, min_window } = virtual_config
|
|
808
|
+
// Shrinking data can leave scroll_top past the new content height (the
|
|
809
|
+
// browser only clamps the real scrollTop after a re-render); clamp here so
|
|
810
|
+
// the window and spacers never index past the data.
|
|
811
|
+
const max_scroll = Math.max(0, total * avg_row_height - viewport_height)
|
|
812
|
+
const first_visible = Math.floor(Math.min(scroll_top, max_scroll) / avg_row_height)
|
|
813
|
+
const visible_count = Math.ceil(viewport_height / avg_row_height)
|
|
814
|
+
const start = Math.max(0, first_visible - overscan)
|
|
815
|
+
const end = Math.min(
|
|
816
|
+
total,
|
|
817
|
+
Math.max(first_visible + visible_count + overscan, start + min_window),
|
|
818
|
+
)
|
|
819
|
+
return { start, end }
|
|
568
820
|
})
|
|
569
821
|
|
|
570
|
-
|
|
571
|
-
|
|
822
|
+
const sync_viewport = () => {
|
|
823
|
+
if (!scroll_el) return
|
|
824
|
+
scroll_top = scroll_el.scrollTop
|
|
825
|
+
viewport_height = scroll_el.clientHeight
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Refine the row-height estimate from actually rendered rows (needed for
|
|
829
|
+
// accurate spacer heights).
|
|
830
|
+
$effect(() => {
|
|
831
|
+
if (!virtual_config || !scroll_el) return
|
|
832
|
+
void virtual_range // re-measure whenever the rendered window changes
|
|
833
|
+
const rows = scroll_el.querySelectorAll<HTMLTableRowElement>(
|
|
834
|
+
`tbody tr:not(.virtual-spacer):not(.empty-row)`,
|
|
835
|
+
)
|
|
836
|
+
let height_sum = 0
|
|
837
|
+
for (const row of rows) height_sum += row.offsetHeight
|
|
838
|
+
const measured = rows.length ? height_sum / rows.length : 0
|
|
839
|
+
// threshold stops measure→window→measure feedback loops from tiny jitters
|
|
840
|
+
if (measured > 0 && Math.abs(measured - avg_row_height) > 0.5) {
|
|
841
|
+
avg_row_height = Math.min(400, Math.max(8, measured))
|
|
842
|
+
}
|
|
843
|
+
sync_viewport()
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
// Track scroll-container resizes (e.g. dashboard card resizing)
|
|
847
|
+
$effect(() => {
|
|
848
|
+
if (!virtual_config || !scroll_el || typeof ResizeObserver === `undefined`) return
|
|
849
|
+
const observer = new ResizeObserver(sync_viewport)
|
|
850
|
+
observer.observe(scroll_el)
|
|
851
|
+
return () => observer.disconnect()
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
// Window of sorted_data rendered in the DOM (one page, or the virtual window).
|
|
855
|
+
// start doubles as the absolute index of the first rendered row (for row
|
|
856
|
+
// numbering and stable cell-selection coordinates).
|
|
857
|
+
let display_range = $derived.by(() => {
|
|
858
|
+
if (!pagination_config) return virtual_range
|
|
859
|
+
const start = (current_page - 1) * effective_page_size
|
|
860
|
+
return { start, end: Math.min(sorted_data.length, start + effective_page_size) }
|
|
861
|
+
})
|
|
862
|
+
let display_rows = $derived(sorted_data.slice(display_range.start, display_range.end))
|
|
863
|
+
let spacer_top = $derived(virtual_config ? virtual_range.start * avg_row_height : 0)
|
|
864
|
+
let spacer_bottom = $derived(
|
|
865
|
+
virtual_config ? (sorted_data.length - virtual_range.end) * avg_row_height : 0,
|
|
572
866
|
)
|
|
573
867
|
|
|
868
|
+
// Report the rendered slice so hosts can progressively fetch rows on demand
|
|
869
|
+
$effect(() => {
|
|
870
|
+
on_visible_range?.({ ...display_range, total: sorted_data.length })
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
let total_pages = $derived(Math.ceil(sorted_data.length / effective_page_size))
|
|
874
|
+
|
|
574
875
|
// Track previous values to detect actual changes
|
|
575
876
|
let prev_search_query = $state(``)
|
|
576
877
|
let prev_data_length = $state(0)
|
|
@@ -620,15 +921,14 @@
|
|
|
620
921
|
} else {
|
|
621
922
|
// Toggle direction
|
|
622
923
|
multi_sort = multi_sort.map((sort_entry, idx) =>
|
|
623
|
-
idx === existing_idx
|
|
924
|
+
idx === existing_idx
|
|
925
|
+
? { ...sort_entry, ascending: !sort_entry.ascending }
|
|
926
|
+
: sort_entry,
|
|
624
927
|
)
|
|
625
928
|
}
|
|
626
929
|
} else {
|
|
627
930
|
// Add to multi-sort
|
|
628
|
-
multi_sort = [...multi_sort, {
|
|
629
|
-
column: col_id,
|
|
630
|
-
ascending: col.better === `lower`,
|
|
631
|
-
}]
|
|
931
|
+
multi_sort = [...multi_sort, { column: col_id, ascending: col.better === `lower` }]
|
|
632
932
|
}
|
|
633
933
|
// Clear single sort when using multi-sort
|
|
634
934
|
sort = { column: ``, dir: `asc` }
|
|
@@ -636,9 +936,14 @@
|
|
|
636
936
|
// Regular click - single column sort
|
|
637
937
|
multi_sort = [] // Clear multi-sort
|
|
638
938
|
// Use sort_state.column for comparison since it includes initial_sort fallback
|
|
639
|
-
const new_dir =
|
|
640
|
-
|
|
641
|
-
|
|
939
|
+
const new_dir =
|
|
940
|
+
sort_state.column !== col_id
|
|
941
|
+
? col.better === `lower`
|
|
942
|
+
? `asc`
|
|
943
|
+
: `desc`
|
|
944
|
+
: sort_state.ascending
|
|
945
|
+
? `desc`
|
|
946
|
+
: `asc`
|
|
642
947
|
|
|
643
948
|
// Save previous sort state in case we need to revert on error
|
|
644
949
|
const prev_sort = { ...sort }
|
|
@@ -677,122 +982,330 @@
|
|
|
677
982
|
return typeof val === `string` ? parse_numeric_string(val) : null
|
|
678
983
|
}
|
|
679
984
|
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
985
|
+
// Memoized per-column color scales: the O(rows) numeric scan + min/max +
|
|
986
|
+
// d3 scale construction run once per column when data/filter/overrides
|
|
987
|
+
// change, instead of once per CELL per render (which made a 50-row page over
|
|
988
|
+
// a 2,000-row snapshot rescan the full column 500+ times). Built from
|
|
989
|
+
// filtered_data (not sorted_data) since min/max don't depend on row order,
|
|
990
|
+
// so re-sorting doesn't rebuild the scales. Only visible columns get scales
|
|
991
|
+
// since calc_color is only ever invoked for visible cells.
|
|
992
|
+
let column_color_scales = $derived.by(() => {
|
|
993
|
+
const scales = new SvelteMap<string, (val: number | null | undefined) => CellColor>()
|
|
994
|
+
if (!show_heatmap) return scales
|
|
995
|
+
for (const col of visible_columns) {
|
|
684
996
|
if (col.color_scale === null) continue
|
|
685
997
|
const col_id = get_col_id(col)
|
|
686
|
-
|
|
998
|
+
const parsed_vals = filtered_data.map((row) => parse_numeric_val(row[col_id]))
|
|
999
|
+
const better = better_overrides.get(col_id) ?? col.better
|
|
1000
|
+
const scale = (color_scale_overrides.get(col_id) ??
|
|
1001
|
+
col.color_scale ??
|
|
1002
|
+
`interpolateViridis`) as Parameters<typeof make_cell_color_scale>[2]
|
|
1003
|
+
scales.set(
|
|
1004
|
+
col_id,
|
|
1005
|
+
make_cell_color_scale(parsed_vals, better, scale, col.scale_type || `linear`),
|
|
1006
|
+
)
|
|
687
1007
|
}
|
|
688
|
-
return
|
|
1008
|
+
return scales
|
|
689
1009
|
})
|
|
690
1010
|
|
|
691
|
-
function calc_color(val: CellVal, col: Label) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
1011
|
+
function calc_color(val: CellVal, col: Label): CellColor {
|
|
1012
|
+
const color_fn = column_color_scales.get(get_col_id(col))
|
|
1013
|
+
if (!color_fn) return { bg: null, text: null }
|
|
695
1014
|
|
|
696
1015
|
// Parse numeric value from strings with uncertainty notation
|
|
697
|
-
const
|
|
698
|
-
if (numeric_val === null) return { bg: null, text: null }
|
|
699
|
-
|
|
700
|
-
const col_id = get_col_id(col)
|
|
701
|
-
// Use memoized parsed values for the column
|
|
702
|
-
const numeric_vals = parsed_column_values.get(col_id) ?? []
|
|
703
|
-
|
|
704
|
-
const better = better_overrides.get(col_id) ?? col.better
|
|
705
|
-
const scale = (color_scale_overrides.get(col_id) ?? col.color_scale ??
|
|
706
|
-
`interpolateViridis`) as Parameters<typeof calc_cell_color>[3]
|
|
707
|
-
const color = calc_cell_color(
|
|
708
|
-
numeric_val,
|
|
709
|
-
numeric_vals,
|
|
710
|
-
better,
|
|
711
|
-
scale,
|
|
712
|
-
col.scale_type || `linear`,
|
|
713
|
-
)
|
|
1016
|
+
const color = color_fn(parse_numeric_val(val))
|
|
714
1017
|
|
|
715
1018
|
// Recompute text contrast against effective bg (cell bg blended with page bg by opacity).
|
|
716
1019
|
// Approximation: blend luminances directly; accurate enough for black/white text choice.
|
|
717
1020
|
if (color.bg && heatmap_opacity < 1) {
|
|
718
|
-
const blended_lum =
|
|
719
|
-
page_bg_lum * (1 - heatmap_opacity)
|
|
720
|
-
color.text
|
|
1021
|
+
const blended_lum =
|
|
1022
|
+
luminance(color.bg) * heatmap_opacity + page_bg_lum * (1 - heatmap_opacity)
|
|
1023
|
+
return { bg: color.bg, text: blended_lum > 0.7 ? `black` : `white` }
|
|
721
1024
|
}
|
|
722
1025
|
return color
|
|
723
1026
|
}
|
|
724
1027
|
|
|
725
1028
|
let visible_columns = $derived(
|
|
726
|
-
ordered_columns.filter(
|
|
727
|
-
col.visible !== false && !hidden_columns.includes(get_col_id(col))
|
|
1029
|
+
ordered_columns.filter(
|
|
1030
|
+
(col) => col.visible !== false && !hidden_columns.includes(get_col_id(col)),
|
|
728
1031
|
),
|
|
729
1032
|
)
|
|
1033
|
+
// total cell count per body row (for spacer + empty-message colspans)
|
|
1034
|
+
let body_colspan = $derived(
|
|
1035
|
+
visible_columns.length + (show_row_select ? 1 : 0) + (show_row_numbers ? 1 : 0),
|
|
1036
|
+
)
|
|
730
1037
|
|
|
1038
|
+
// Arrow (plus a numbered badge under multi-sort) for actively sorted columns
|
|
731
1039
|
const sort_indicator = (col: Label, current_sort_state: SortState) => {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1040
|
+
if (col.show_sort_indicator === false || col.style?.includes(`--hide-sort-indicator`)) {
|
|
1041
|
+
return ``
|
|
1042
|
+
}
|
|
736
1043
|
const col_id = get_col_id(col)
|
|
737
|
-
|
|
738
|
-
// Check multi-sort first
|
|
739
1044
|
const multi_idx = multi_sort.findIndex((sort_entry) => sort_entry.column === col_id)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1045
|
+
const active =
|
|
1046
|
+
multi_idx !== -1
|
|
1047
|
+
? multi_sort[multi_idx]
|
|
1048
|
+
: current_sort_state.column === col_id
|
|
1049
|
+
? current_sort_state
|
|
1050
|
+
: null
|
|
1051
|
+
if (!active) return ``
|
|
1052
|
+
const badge =
|
|
1053
|
+
multi_idx !== -1 && multi_sort.length > 1 ? `<sup>${multi_idx + 1}</sup>` : ``
|
|
1054
|
+
return `<span style="font-size: 0.8em;">${active.ascending ? `↓` : `↑`}${badge}</span>`
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Context menu state for column right-click (headers and body cells)
|
|
1058
|
+
let context_menu_col = $state<string | null>(null)
|
|
1059
|
+
let context_menu_pos = $state({ x: 0, y: 0 })
|
|
1060
|
+
|
|
1061
|
+
const better_section = {
|
|
1062
|
+
title: `Gradient direction`,
|
|
1063
|
+
options: [
|
|
1064
|
+
{ value: `higher`, label: `▲ Higher is better` },
|
|
1065
|
+
{ value: `lower`, label: `▼ Lower is better` },
|
|
1066
|
+
],
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function open_column_context_menu(event: MouseEvent, col_id: string) {
|
|
1070
|
+
event.preventDefault()
|
|
1071
|
+
event.stopPropagation()
|
|
1072
|
+
context_menu_col = col_id
|
|
1073
|
+
const rect = container_el?.getBoundingClientRect()
|
|
1074
|
+
context_menu_pos = {
|
|
1075
|
+
x: event.clientX - (rect?.left ?? 0),
|
|
1076
|
+
y: event.clientY - (rect?.top ?? 0),
|
|
744
1077
|
}
|
|
1078
|
+
}
|
|
745
1079
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1080
|
+
// ---- Cell range selection: drag selects a rectangle of cells, Shift/Cmd+
|
|
1081
|
+
// drag adds disjoint rectangles, Cmd/Ctrl+C copies as TSV (blocks separated
|
|
1082
|
+
// by newlines), Escape or clicking outside clears. Selection coordinates
|
|
1083
|
+
// are absolute sorted_data row indices plus visible-column indices, cleared
|
|
1084
|
+
// whenever the rendered data changes (sort, page, filter, refresh).
|
|
1085
|
+
interface CellRect {
|
|
1086
|
+
start_row: number
|
|
1087
|
+
start_col: number
|
|
1088
|
+
end_row: number
|
|
1089
|
+
end_col: number
|
|
1090
|
+
}
|
|
1091
|
+
let selected_cell_rects = $state<CellRect[]>([])
|
|
1092
|
+
let cell_drag_active = $state(false)
|
|
1093
|
+
let cell_drag_moved = false
|
|
1094
|
+
let suppress_row_click = false
|
|
1095
|
+
|
|
1096
|
+
const rect_bounds = (rect: CellRect) => ({
|
|
1097
|
+
row_lo: Math.min(rect.start_row, rect.end_row),
|
|
1098
|
+
row_hi: Math.max(rect.start_row, rect.end_row),
|
|
1099
|
+
col_lo: Math.min(rect.start_col, rect.end_col),
|
|
1100
|
+
col_hi: Math.max(rect.start_col, rect.end_col),
|
|
1101
|
+
})
|
|
1102
|
+
|
|
1103
|
+
let selected_cell_keys = $derived.by(() => {
|
|
1104
|
+
const keys = new Set<string>()
|
|
1105
|
+
for (const rect of selected_cell_rects) {
|
|
1106
|
+
const { row_lo, row_hi, col_lo, col_hi } = rect_bounds(rect)
|
|
1107
|
+
for (let row_idx = row_lo; row_idx <= row_hi; row_idx++) {
|
|
1108
|
+
for (let col_idx = col_lo; col_idx <= col_hi; col_idx++) {
|
|
1109
|
+
keys.add(`${row_idx}:${col_idx}`)
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return keys
|
|
1114
|
+
})
|
|
750
1115
|
|
|
751
|
-
|
|
1116
|
+
// Stale (row, col) coordinates must not survive sort/page/filter/refresh,
|
|
1117
|
+
// nor column reorder/hide (col indices point into visible_columns). Depends
|
|
1118
|
+
// on sorted_data + current_page + visible_columns (not the virtual window)
|
|
1119
|
+
// so plain scrolling in infinite mode doesn't wipe an active selection.
|
|
1120
|
+
$effect(() => {
|
|
1121
|
+
void sorted_data
|
|
1122
|
+
void current_page
|
|
1123
|
+
void visible_columns
|
|
1124
|
+
selected_cell_rects = []
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
const is_interactive_cell_target = (target: EventTarget | null): boolean =>
|
|
1128
|
+
target instanceof Element && Boolean(target.closest(`button, a, input, select, textarea`))
|
|
1129
|
+
|
|
1130
|
+
function start_cell_drag(event: PointerEvent, row_idx: number, col_idx: number) {
|
|
1131
|
+
if (event.button !== 0 || is_interactive_cell_target(event.target)) return
|
|
1132
|
+
const additive = event.shiftKey || event.metaKey || event.ctrlKey
|
|
1133
|
+
const rect = { start_row: row_idx, start_col: col_idx, end_row: row_idx, end_col: col_idx }
|
|
1134
|
+
selected_cell_rects = additive ? [...selected_cell_rects, rect] : [rect]
|
|
1135
|
+
cell_drag_active = true
|
|
1136
|
+
cell_drag_moved = false
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function extend_cell_drag(event: PointerEvent) {
|
|
1140
|
+
if (!cell_drag_active) return
|
|
1141
|
+
const target_cell =
|
|
1142
|
+
event.target instanceof Element
|
|
1143
|
+
? event.target.closest<HTMLElement>(`td[data-row-idx]`)
|
|
1144
|
+
: null
|
|
1145
|
+
const active_rect = selected_cell_rects.at(-1)
|
|
1146
|
+
if (!target_cell || !active_rect) return
|
|
1147
|
+
const row_idx = Number(target_cell.dataset.rowIdx)
|
|
1148
|
+
const col_idx = Number(target_cell.dataset.colIdx)
|
|
1149
|
+
if (row_idx === active_rect.end_row && col_idx === active_rect.end_col) return
|
|
1150
|
+
if (!cell_drag_moved) {
|
|
1151
|
+
cell_drag_moved = true
|
|
1152
|
+
// A native text selection may have started before user-select: none
|
|
1153
|
+
// kicked in; drop it so the cell selection is the only visible one.
|
|
1154
|
+
globalThis.getSelection()?.removeAllRanges()
|
|
1155
|
+
}
|
|
1156
|
+
selected_cell_rects = [
|
|
1157
|
+
...selected_cell_rects.slice(0, -1),
|
|
1158
|
+
{ ...active_rect, end_row: row_idx, end_col: col_idx },
|
|
1159
|
+
]
|
|
752
1160
|
}
|
|
753
1161
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1162
|
+
function end_cell_drag() {
|
|
1163
|
+
if (!cell_drag_active) return
|
|
1164
|
+
cell_drag_active = false
|
|
1165
|
+
// A drag that crossed cells must not fire the row click on release
|
|
1166
|
+
if (cell_drag_moved) suppress_row_click = true
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function suppress_click_after_cell_drag(event: MouseEvent) {
|
|
1170
|
+
if (!suppress_row_click) return
|
|
1171
|
+
suppress_row_click = false
|
|
1172
|
+
event.stopPropagation()
|
|
1173
|
+
event.preventDefault()
|
|
1174
|
+
}
|
|
757
1175
|
|
|
758
|
-
|
|
1176
|
+
function clear_cell_selection_on_outside_pointerdown(event: PointerEvent) {
|
|
1177
|
+
// A drag's suppress flag is consumed by the click right after pointerup;
|
|
1178
|
+
// if that click never fired (released outside the table), any NEW
|
|
1179
|
+
// interaction must not inherit it.
|
|
1180
|
+
suppress_row_click = false
|
|
1181
|
+
if (selected_cell_rects.length === 0) return
|
|
1182
|
+
if (event.target instanceof Node && container_el?.contains(event.target)) return
|
|
1183
|
+
selected_cell_rects = []
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Raw cell value as clipboard text (numbers keep full precision, dates go
|
|
1187
|
+
// ISO, HTML cells lose their markup)
|
|
1188
|
+
const cell_copy_text = (val: CellVal): string => {
|
|
1189
|
+
if (is_invalid(val)) return ``
|
|
1190
|
+
if (val instanceof Date) return val.toISOString()
|
|
1191
|
+
if (typeof val === `object`) return JSON.stringify(val)
|
|
1192
|
+
return strip_html(String(val)).trim()
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function copy_selected_cells() {
|
|
1196
|
+
const blocks = selected_cell_rects.map((rect) => {
|
|
1197
|
+
const bounds = rect_bounds(rect)
|
|
1198
|
+
const { row_lo, col_lo } = bounds
|
|
1199
|
+
// rects hold absolute indices into the sorted+filtered rows
|
|
1200
|
+
const row_hi = Math.min(bounds.row_hi, sorted_data.length - 1)
|
|
1201
|
+
const col_hi = Math.min(bounds.col_hi, visible_columns.length - 1)
|
|
1202
|
+
const lines: string[] = []
|
|
1203
|
+
for (let row_idx = row_lo; row_idx <= row_hi; row_idx++) {
|
|
1204
|
+
const cells: string[] = []
|
|
1205
|
+
for (let col_idx = col_lo; col_idx <= col_hi; col_idx++) {
|
|
1206
|
+
cells.push(
|
|
1207
|
+
cell_copy_text(sorted_data[row_idx][get_col_id(visible_columns[col_idx])]),
|
|
1208
|
+
)
|
|
1209
|
+
}
|
|
1210
|
+
lines.push(cells.join(`\t`))
|
|
1211
|
+
}
|
|
1212
|
+
return lines.join(`\n`)
|
|
1213
|
+
})
|
|
1214
|
+
void navigator.clipboard?.writeText(blocks.join(`\n`))
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Every sorted+filtered value of one column (all pages), one per line
|
|
1218
|
+
function copy_column_values(col_id: string) {
|
|
1219
|
+
void navigator.clipboard?.writeText(
|
|
1220
|
+
sorted_data.map((row) => cell_copy_text(row[col_id])).join(`\n`),
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function handle_cell_selection_keydown(event: KeyboardEvent) {
|
|
1225
|
+
if (selected_cell_rects.length === 0) return
|
|
1226
|
+
if (event.key === `Escape`) {
|
|
1227
|
+
selected_cell_rects = []
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1230
|
+
if (event.key !== `c` || !(event.metaKey || event.ctrlKey)) return
|
|
1231
|
+
// Native text selections and focused form fields keep native copy
|
|
1232
|
+
if (is_interactive_cell_target(event.target)) return
|
|
1233
|
+
if (globalThis.getSelection()?.toString()) return
|
|
1234
|
+
event.preventDefault()
|
|
1235
|
+
copy_selected_cells()
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
let context_menu_column = $derived(
|
|
1239
|
+
visible_columns.find((col) => get_col_id(col) === context_menu_col),
|
|
1240
|
+
)
|
|
1241
|
+
let context_menu_sections = $derived([
|
|
759
1242
|
{
|
|
760
|
-
title: `
|
|
1243
|
+
title: `Copy`,
|
|
761
1244
|
options: [
|
|
762
|
-
{ value: `
|
|
763
|
-
|
|
1245
|
+
{ value: `copy_column`, label: `Copy column (${sorted_data.length} values)` },
|
|
1246
|
+
...(selected_cell_keys.size > 0
|
|
1247
|
+
? [
|
|
1248
|
+
{
|
|
1249
|
+
value: `copy_selection`,
|
|
1250
|
+
label: `Copy selection (${selected_cell_keys.size} cells)`,
|
|
1251
|
+
},
|
|
1252
|
+
]
|
|
1253
|
+
: []),
|
|
764
1254
|
],
|
|
765
1255
|
},
|
|
766
|
-
|
|
1256
|
+
// Gradient direction only applies to heatmap-colored columns
|
|
1257
|
+
...(allow_better_toggle && context_menu_column?.color_scale != null
|
|
1258
|
+
? [better_section]
|
|
1259
|
+
: []),
|
|
1260
|
+
])
|
|
1261
|
+
|
|
1262
|
+
function handle_context_menu_select(section_title: string, option: { value: string }) {
|
|
1263
|
+
if (section_title === `Copy`) {
|
|
1264
|
+
if (option.value === `copy_column` && context_menu_col) {
|
|
1265
|
+
copy_column_values(context_menu_col)
|
|
1266
|
+
} else if (option.value === `copy_selection`) {
|
|
1267
|
+
copy_selected_cells()
|
|
1268
|
+
}
|
|
1269
|
+
} else if (context_menu_col) {
|
|
1270
|
+
const current = better_overrides.get(context_menu_col)
|
|
1271
|
+
if (current === option.value) better_overrides.delete(context_menu_col)
|
|
1272
|
+
else {
|
|
1273
|
+
better_overrides.set(context_menu_col, option.value as `higher` | `lower`)
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
context_menu_col = null
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Row selection via an ID-indexed Set so per-row checks are O(1) instead of
|
|
1280
|
+
// linear scans over selected_rows (matters for large virtualized datasets)
|
|
1281
|
+
let selected_id_set = $derived(new Set(selected_rows.map((row) => get_row_id(row))))
|
|
767
1282
|
|
|
768
|
-
// Row selection using WeakMap-based ID lookup instead of O(n) JSON.stringify comparison
|
|
769
1283
|
function toggle_row_select(row: RowData) {
|
|
770
1284
|
const row_id = get_row_id(row)
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
} else {
|
|
775
|
-
selected_rows = [...selected_rows, row]
|
|
776
|
-
}
|
|
1285
|
+
selected_rows = selected_id_set.has(row_id)
|
|
1286
|
+
? selected_rows.filter((selected_row) => get_row_id(selected_row) !== row_id)
|
|
1287
|
+
: [...selected_rows, row]
|
|
777
1288
|
}
|
|
778
1289
|
|
|
779
1290
|
function is_row_selected(row: RowData): boolean {
|
|
780
|
-
|
|
781
|
-
return selected_rows.some((selected_row) => get_row_id(selected_row) === row_id)
|
|
1291
|
+
return selected_id_set.has(get_row_id(row))
|
|
782
1292
|
}
|
|
783
1293
|
|
|
784
|
-
// Select-all:
|
|
1294
|
+
// Select-all scope: the current page under pagination, every sorted+filtered
|
|
1295
|
+
// row in infinite-scroll mode (the virtual window is a rendering detail)
|
|
1296
|
+
let select_all_rows = $derived(pagination_config ? display_rows : sorted_data)
|
|
785
1297
|
let all_page_selected = $derived(
|
|
786
|
-
|
|
1298
|
+
select_all_rows.length > 0 &&
|
|
1299
|
+
select_all_rows.every((row) => selected_id_set.has(get_row_id(row))),
|
|
787
1300
|
)
|
|
788
1301
|
|
|
789
1302
|
function toggle_select_all() {
|
|
790
1303
|
if (all_page_selected) {
|
|
791
|
-
const
|
|
792
|
-
selected_rows = selected_rows.filter((row) => !
|
|
1304
|
+
const scope_ids = new Set(select_all_rows.map(get_row_id))
|
|
1305
|
+
selected_rows = selected_rows.filter((row) => !scope_ids.has(get_row_id(row)))
|
|
793
1306
|
} else {
|
|
794
1307
|
const already = new Set(selected_rows.map(get_row_id))
|
|
795
|
-
const new_rows =
|
|
1308
|
+
const new_rows = select_all_rows.filter((row) => !already.has(get_row_id(row)))
|
|
796
1309
|
selected_rows = [...selected_rows, ...new_rows]
|
|
797
1310
|
}
|
|
798
1311
|
}
|
|
@@ -818,45 +1331,42 @@
|
|
|
818
1331
|
const val = row[get_col_id(col)]
|
|
819
1332
|
if (val == null) return ``
|
|
820
1333
|
return quote(strip_html(String(val)))
|
|
821
|
-
})
|
|
1334
|
+
}),
|
|
822
1335
|
)
|
|
823
1336
|
return [headers.join(delimiter), ...rows.map((row) => row.join(delimiter))].join(`\n`)
|
|
824
1337
|
}
|
|
825
1338
|
|
|
826
|
-
function export_csv(filename
|
|
1339
|
+
function export_csv(filename: string) {
|
|
827
1340
|
download(serialize_table(`,`, true), `${filename}.csv`, `text/csv`)
|
|
828
1341
|
}
|
|
829
1342
|
|
|
830
|
-
function export_json(filename
|
|
1343
|
+
function export_json(filename: string) {
|
|
831
1344
|
const rows = export_rows.map((row) => {
|
|
832
1345
|
const clean_row: Record<string, unknown> = {}
|
|
833
1346
|
for (const col of visible_columns) {
|
|
834
1347
|
const col_id = get_col_id(col)
|
|
835
1348
|
const val = row[col_id]
|
|
836
|
-
clean_row[strip_html(col.label)] = typeof val === `string`
|
|
837
|
-
? strip_html(val)
|
|
838
|
-
: val
|
|
1349
|
+
clean_row[strip_html(col.label)] = typeof val === `string` ? strip_html(val) : val
|
|
839
1350
|
}
|
|
840
1351
|
return clean_row
|
|
841
1352
|
})
|
|
842
|
-
download(
|
|
843
|
-
JSON.stringify(rows, null, 2),
|
|
844
|
-
`${filename}.json`,
|
|
845
|
-
`application/json`,
|
|
846
|
-
)
|
|
1353
|
+
download(JSON.stringify(rows, null, 2), `${filename}.json`, `application/json`)
|
|
847
1354
|
}
|
|
848
1355
|
|
|
1356
|
+
const export_actions = [
|
|
1357
|
+
[`csv`, export_csv],
|
|
1358
|
+
[`json`, export_json],
|
|
1359
|
+
] as const
|
|
1360
|
+
|
|
849
1361
|
function copy_to_clipboard() {
|
|
850
1362
|
navigator.clipboard.writeText(serialize_table(`\t`))
|
|
851
1363
|
}
|
|
852
1364
|
|
|
853
1365
|
// Column visibility toggle
|
|
854
1366
|
function toggle_column(col_id: string) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
hidden_columns = [...hidden_columns, col_id]
|
|
859
|
-
}
|
|
1367
|
+
hidden_columns = hidden_columns.includes(col_id)
|
|
1368
|
+
? hidden_columns.filter((id) => id !== col_id)
|
|
1369
|
+
: [...hidden_columns, col_id]
|
|
860
1370
|
}
|
|
861
1371
|
|
|
862
1372
|
// Column resize handlers
|
|
@@ -889,18 +1399,27 @@
|
|
|
889
1399
|
let hint_config = $derived(
|
|
890
1400
|
sort_hint
|
|
891
1401
|
? {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1402
|
+
position: `bottom` as const,
|
|
1403
|
+
permanent: false,
|
|
1404
|
+
...(typeof sort_hint === `string` ? { text: sort_hint } : sort_hint),
|
|
1405
|
+
}
|
|
896
1406
|
: null,
|
|
897
1407
|
)
|
|
898
1408
|
</script>
|
|
899
1409
|
|
|
1410
|
+
<svelte:window
|
|
1411
|
+
onpointerdown={(event) => {
|
|
1412
|
+
close_datetime_select_on_outside_pointerdown(event)
|
|
1413
|
+
clear_cell_selection_on_outside_pointerdown(event)
|
|
1414
|
+
}}
|
|
1415
|
+
onpointerup={end_cell_drag}
|
|
1416
|
+
onkeydown={handle_cell_selection_keydown}
|
|
1417
|
+
/>
|
|
1418
|
+
|
|
900
1419
|
{#snippet sort_hint_element(pos: `top` | `bottom`)}
|
|
901
1420
|
{#if hint_config?.position === pos}
|
|
902
1421
|
<div
|
|
903
|
-
class=
|
|
1422
|
+
class={[`sort-hint`, hint_config.class]}
|
|
904
1423
|
class:permanent={hint_config.permanent}
|
|
905
1424
|
style={hint_config.style}
|
|
906
1425
|
>
|
|
@@ -909,20 +1428,29 @@
|
|
|
909
1428
|
{/if}
|
|
910
1429
|
{/snippet}
|
|
911
1430
|
|
|
1431
|
+
<!-- svelte-ignore a11y_no_static_element_interactions (capture-phase guard swallowing the click that follows a cell-range drag) -->
|
|
912
1432
|
<div
|
|
913
1433
|
{@attach table_tooltips}
|
|
914
1434
|
{...rest_props}
|
|
915
1435
|
bind:this={container_el}
|
|
916
|
-
class=
|
|
1436
|
+
class={[`table-container`, rest_props.class]}
|
|
1437
|
+
class:cell-dragging={cell_drag_active}
|
|
917
1438
|
style:--heatmap-opacity="{heatmap_opacity * 100}%"
|
|
1439
|
+
onclickcapture={suppress_click_after_cell_drag}
|
|
918
1440
|
onmouseleave={() => {
|
|
919
1441
|
show_column_dropdown = false
|
|
920
1442
|
show_export_dropdown = false
|
|
921
1443
|
context_menu_col = null
|
|
922
1444
|
}}
|
|
923
1445
|
>
|
|
924
|
-
<!--
|
|
925
|
-
|
|
1446
|
+
<!-- Control buttons: render inline above the table, or teleport into a host
|
|
1447
|
+
toolbar (controls_target) so embedding panels reuse their own header row -->
|
|
1448
|
+
<section
|
|
1449
|
+
class="control-buttons"
|
|
1450
|
+
class:portaled={Boolean(controls_target)}
|
|
1451
|
+
class:force-visible={controls_open || show_column_dropdown || show_export_dropdown}
|
|
1452
|
+
{@attach portal(controls_target)}
|
|
1453
|
+
>
|
|
926
1454
|
{#if search_config}
|
|
927
1455
|
{#if search_expanded || search_query}
|
|
928
1456
|
<input
|
|
@@ -942,15 +1470,15 @@
|
|
|
942
1470
|
}}
|
|
943
1471
|
{@attach tooltip({ content: `Clear`, placement: `top` })}
|
|
944
1472
|
>
|
|
945
|
-
<Icon icon="Cross"
|
|
1473
|
+
<Icon icon="Cross" />
|
|
946
1474
|
</button>
|
|
947
1475
|
{:else}
|
|
948
1476
|
<button
|
|
949
1477
|
class="icon-btn"
|
|
950
|
-
onclick={() => search_expanded = true}
|
|
1478
|
+
onclick={() => (search_expanded = true)}
|
|
951
1479
|
{@attach tooltip({ content: `Search`, placement: `top` })}
|
|
952
1480
|
>
|
|
953
|
-
<Icon icon="Search"
|
|
1481
|
+
<Icon icon="Search" />
|
|
954
1482
|
</button>
|
|
955
1483
|
{/if}
|
|
956
1484
|
{/if}
|
|
@@ -960,10 +1488,10 @@
|
|
|
960
1488
|
<button
|
|
961
1489
|
class="icon-btn"
|
|
962
1490
|
class:active={show_column_dropdown}
|
|
963
|
-
onclick={() => show_column_dropdown = !show_column_dropdown}
|
|
1491
|
+
onclick={() => (show_column_dropdown = !show_column_dropdown)}
|
|
964
1492
|
{@attach tooltip({ content: `Columns`, placement: `top` })}
|
|
965
1493
|
>
|
|
966
|
-
<Icon icon="Columns"
|
|
1494
|
+
<Icon icon="Columns" />
|
|
967
1495
|
</button>
|
|
968
1496
|
{#if show_column_dropdown}
|
|
969
1497
|
<div class="dropdown-pane">
|
|
@@ -988,35 +1516,27 @@
|
|
|
988
1516
|
<button
|
|
989
1517
|
class="icon-btn"
|
|
990
1518
|
class:active={show_export_dropdown}
|
|
991
|
-
onclick={() => show_export_dropdown = !show_export_dropdown}
|
|
1519
|
+
onclick={() => (show_export_dropdown = !show_export_dropdown)}
|
|
992
1520
|
{@attach tooltip({ content: `Export`, placement: `top` })}
|
|
993
1521
|
>
|
|
994
|
-
<Icon icon="Export"
|
|
1522
|
+
<Icon icon="Export" />
|
|
995
1523
|
</button>
|
|
996
1524
|
{#if show_export_dropdown}
|
|
997
1525
|
<div class="dropdown-pane">
|
|
998
|
-
{#
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
onclick={() => {
|
|
1013
|
-
export_json(export_config.filename)
|
|
1014
|
-
show_export_dropdown = false
|
|
1015
|
-
}}
|
|
1016
|
-
>
|
|
1017
|
-
<Icon icon="Download" style="width: 12px" /> JSON
|
|
1018
|
-
</button>
|
|
1019
|
-
{/if}
|
|
1526
|
+
{#each export_actions as [format, export_fn] (format)}
|
|
1527
|
+
{#if export_config.formats.includes(format)}
|
|
1528
|
+
<button
|
|
1529
|
+
class="dropdown-option"
|
|
1530
|
+
onclick={() => {
|
|
1531
|
+
export_fn(export_config.filename)
|
|
1532
|
+
show_export_dropdown = false
|
|
1533
|
+
}}
|
|
1534
|
+
>
|
|
1535
|
+
<Icon icon="Download" style="width: 12px" />
|
|
1536
|
+
{format.toUpperCase()}
|
|
1537
|
+
</button>
|
|
1538
|
+
{/if}
|
|
1539
|
+
{/each}
|
|
1020
1540
|
<button
|
|
1021
1541
|
class="dropdown-option"
|
|
1022
1542
|
onclick={() => {
|
|
@@ -1034,118 +1554,114 @@
|
|
|
1034
1554
|
{#if show_row_select && selected_rows.length > 0}
|
|
1035
1555
|
<button
|
|
1036
1556
|
class="icon-btn selection-badge"
|
|
1037
|
-
onclick={() => selected_rows = []}
|
|
1557
|
+
onclick={() => (selected_rows = [])}
|
|
1038
1558
|
title="Clear {selected_rows.length} selected rows"
|
|
1039
1559
|
>
|
|
1040
1560
|
<span class="badge">{selected_rows.length}</span>
|
|
1041
|
-
<Icon icon="Cross"
|
|
1561
|
+
<Icon icon="Cross" />
|
|
1042
1562
|
</button>
|
|
1043
1563
|
{/if}
|
|
1044
1564
|
|
|
1045
|
-
{#if
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
open_icon="Cross"
|
|
1055
|
-
toggle_props={{
|
|
1056
|
-
title: `${controls_open ? `Close` : `Open`} table controls`,
|
|
1057
|
-
style: `position: absolute; top: 5pt; right: 1ex; z-index: 10`,
|
|
1058
|
-
}}
|
|
1059
|
-
pane_props={{ style: `max-height: 60vh; overflow-y: auto; font-size: 0.85em` }}
|
|
1060
|
-
>
|
|
1061
|
-
<SettingsSection
|
|
1062
|
-
title="Heatmap"
|
|
1063
|
-
current_values={{ show_heatmap, heatmap_opacity }}
|
|
1064
|
-
on_reset={() => {
|
|
1065
|
-
show_heatmap = true
|
|
1066
|
-
heatmap_opacity = 1
|
|
1067
|
-
}}
|
|
1068
|
-
>
|
|
1069
|
-
<label><input type="checkbox" bind:checked={show_heatmap} /> Show heatmap</label>
|
|
1070
|
-
{#if show_heatmap}
|
|
1071
|
-
<label>
|
|
1072
|
-
Opacity
|
|
1073
|
-
<input
|
|
1074
|
-
type="range"
|
|
1075
|
-
min="0"
|
|
1076
|
-
max="1"
|
|
1077
|
-
step="0.05"
|
|
1078
|
-
bind:value={heatmap_opacity}
|
|
1079
|
-
/>
|
|
1080
|
-
<input
|
|
1081
|
-
type="number"
|
|
1082
|
-
min="0"
|
|
1083
|
-
max="1"
|
|
1084
|
-
step="0.05"
|
|
1085
|
-
bind:value={heatmap_opacity}
|
|
1086
|
-
style="width: 3.5em"
|
|
1087
|
-
/>
|
|
1088
|
-
</label>
|
|
1089
|
-
{/if}
|
|
1090
|
-
</SettingsSection>
|
|
1091
|
-
|
|
1092
|
-
<SettingsSection
|
|
1093
|
-
title="Display"
|
|
1094
|
-
current_values={{ show_row_numbers }}
|
|
1095
|
-
on_reset={() => {
|
|
1096
|
-
show_row_numbers = false
|
|
1565
|
+
{#if show_controls}
|
|
1566
|
+
<DraggablePane
|
|
1567
|
+
bind:show={controls_open}
|
|
1568
|
+
closed_icon="Settings"
|
|
1569
|
+
open_icon="Cross"
|
|
1570
|
+
toggle_props={{ title: `${controls_open ? `Close` : `Open`} table controls` }}
|
|
1571
|
+
position="fixed"
|
|
1572
|
+
pane_props={{
|
|
1573
|
+
style: `--pane-max-height: 60vh; overflow-y: auto; font-size: 0.85em`,
|
|
1097
1574
|
}}
|
|
1098
1575
|
>
|
|
1099
|
-
<
|
|
1100
|
-
|
|
1101
|
-
|
|
1576
|
+
<SettingsSection
|
|
1577
|
+
title="Heatmap"
|
|
1578
|
+
current_values={{ show_heatmap, heatmap_opacity }}
|
|
1579
|
+
on_reset={() => {
|
|
1580
|
+
show_heatmap = true
|
|
1581
|
+
heatmap_opacity = 1
|
|
1582
|
+
}}
|
|
1583
|
+
>
|
|
1584
|
+
<label><input type="checkbox" bind:checked={show_heatmap} /> Show heatmap</label>
|
|
1585
|
+
{#if show_heatmap}
|
|
1586
|
+
<label>
|
|
1587
|
+
Opacity
|
|
1588
|
+
<input type="range" min="0" max="1" step="0.05" bind:value={heatmap_opacity} />
|
|
1589
|
+
<input
|
|
1590
|
+
type="number"
|
|
1591
|
+
min="0"
|
|
1592
|
+
max="1"
|
|
1593
|
+
step="0.05"
|
|
1594
|
+
bind:value={heatmap_opacity}
|
|
1595
|
+
style="width: 3.5em"
|
|
1596
|
+
/>
|
|
1597
|
+
</label>
|
|
1598
|
+
{/if}
|
|
1599
|
+
</SettingsSection>
|
|
1102
1600
|
|
|
1103
|
-
{#if colored_columns.length > 0}
|
|
1104
1601
|
<SettingsSection
|
|
1105
|
-
title="
|
|
1106
|
-
current_values={
|
|
1602
|
+
title="Display"
|
|
1603
|
+
current_values={{ show_row_numbers }}
|
|
1107
1604
|
on_reset={() => {
|
|
1108
|
-
|
|
1109
|
-
color_scale_overrides.clear()
|
|
1605
|
+
show_row_numbers = false
|
|
1110
1606
|
}}
|
|
1111
1607
|
>
|
|
1112
|
-
{
|
|
1113
|
-
{@const col_id = get_col_id(col)}
|
|
1114
|
-
<div class="col-color-row">
|
|
1115
|
-
<span class="col-color-label">{@html sanitize_html(col.label)}</span>
|
|
1116
|
-
<select
|
|
1117
|
-
value={color_scale_overrides.get(col_id) ?? col.color_scale ??
|
|
1118
|
-
`interpolateViridis`}
|
|
1119
|
-
onchange={(event) => {
|
|
1120
|
-
const val = event.currentTarget.value
|
|
1121
|
-
if (
|
|
1122
|
-
val === (col.color_scale ?? `interpolateViridis`)
|
|
1123
|
-
) color_scale_overrides.delete(col_id)
|
|
1124
|
-
else color_scale_overrides.set(col_id, val)
|
|
1125
|
-
}}
|
|
1126
|
-
>
|
|
1127
|
-
{#each color_scale_options as scale (scale)}
|
|
1128
|
-
<option value={scale}>{scale.replace(`interpolate`, ``)}</option>
|
|
1129
|
-
{/each}
|
|
1130
|
-
</select>
|
|
1131
|
-
<select
|
|
1132
|
-
value={better_overrides.get(col_id) ?? col.better ?? ``}
|
|
1133
|
-
onchange={(event) => {
|
|
1134
|
-
const val = event.currentTarget.value
|
|
1135
|
-
if (!val) better_overrides.delete(col_id)
|
|
1136
|
-
else better_overrides.set(col_id, val as `higher` | `lower`)
|
|
1137
|
-
}}
|
|
1138
|
-
>
|
|
1139
|
-
<option value="">Default</option>
|
|
1140
|
-
<option value="higher">▲ High</option>
|
|
1141
|
-
<option value="lower">▼ Low</option>
|
|
1142
|
-
</select>
|
|
1143
|
-
</div>
|
|
1144
|
-
{/each}
|
|
1608
|
+
<label><input type="checkbox" bind:checked={show_row_numbers} /> Row numbers</label>
|
|
1145
1609
|
</SettingsSection>
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1610
|
+
|
|
1611
|
+
{#if colored_columns.length > 0}
|
|
1612
|
+
<SettingsSection
|
|
1613
|
+
title="Column Colors"
|
|
1614
|
+
current_values={Object.fromEntries([
|
|
1615
|
+
...better_overrides,
|
|
1616
|
+
...color_scale_overrides,
|
|
1617
|
+
])}
|
|
1618
|
+
on_reset={() => {
|
|
1619
|
+
better_overrides.clear()
|
|
1620
|
+
color_scale_overrides.clear()
|
|
1621
|
+
}}
|
|
1622
|
+
>
|
|
1623
|
+
{#each colored_columns as col (get_col_id(col))}
|
|
1624
|
+
{@const col_id = get_col_id(col)}
|
|
1625
|
+
<div class="col-color-row">
|
|
1626
|
+
<span class="col-color-label">{@html sanitize_html(col.label)}</span>
|
|
1627
|
+
<select
|
|
1628
|
+
value={color_scale_overrides.get(col_id) ??
|
|
1629
|
+
col.color_scale ??
|
|
1630
|
+
`interpolateViridis`}
|
|
1631
|
+
onchange={(event) => {
|
|
1632
|
+
const val = event.currentTarget.value
|
|
1633
|
+
if (val === (col.color_scale ?? `interpolateViridis`))
|
|
1634
|
+
color_scale_overrides.delete(col_id)
|
|
1635
|
+
else color_scale_overrides.set(col_id, val)
|
|
1636
|
+
}}
|
|
1637
|
+
>
|
|
1638
|
+
{#each color_scale_options as scale (scale)}
|
|
1639
|
+
<option value={scale}>{scale.replace(`interpolate`, ``)}</option>
|
|
1640
|
+
{/each}
|
|
1641
|
+
</select>
|
|
1642
|
+
<select
|
|
1643
|
+
value={better_overrides.get(col_id) ?? col.better ?? ``}
|
|
1644
|
+
onchange={(event) => {
|
|
1645
|
+
const val = event.currentTarget.value
|
|
1646
|
+
if (!val) better_overrides.delete(col_id)
|
|
1647
|
+
else better_overrides.set(col_id, val as `higher` | `lower`)
|
|
1648
|
+
}}
|
|
1649
|
+
>
|
|
1650
|
+
<option value="">Default</option>
|
|
1651
|
+
<option value="higher">▲ High</option>
|
|
1652
|
+
<option value="lower">▼ Low</option>
|
|
1653
|
+
</select>
|
|
1654
|
+
</div>
|
|
1655
|
+
{/each}
|
|
1656
|
+
</SettingsSection>
|
|
1657
|
+
{/if}
|
|
1658
|
+
</DraggablePane>
|
|
1659
|
+
{/if}
|
|
1660
|
+
|
|
1661
|
+
{#if controls}
|
|
1662
|
+
{@render controls()}
|
|
1663
|
+
{/if}
|
|
1664
|
+
</section>
|
|
1149
1665
|
|
|
1150
1666
|
{@render sort_hint_element(`top`)}
|
|
1151
1667
|
|
|
@@ -1153,6 +1669,8 @@
|
|
|
1153
1669
|
class="table-scroll"
|
|
1154
1670
|
style={scroll_style}
|
|
1155
1671
|
class:has-scroll={scroll_style}
|
|
1672
|
+
bind:this={scroll_el}
|
|
1673
|
+
onscroll={virtual_config ? sync_viewport : undefined}
|
|
1156
1674
|
>
|
|
1157
1675
|
{#if loading}
|
|
1158
1676
|
<div class="loading-overlay">
|
|
@@ -1175,15 +1693,13 @@
|
|
|
1175
1693
|
{#if !col.group}
|
|
1176
1694
|
<th class:sticky-col={col.sticky}></th>
|
|
1177
1695
|
{:else}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
)}
|
|
1186
|
-
<th title={col.description} colspan={group_cols.length}>
|
|
1696
|
+
<!-- Only render the group header once per group (on its first column) -->
|
|
1697
|
+
{#if visible_columns.find((column) => column.group === col.group) === col}
|
|
1698
|
+
<th
|
|
1699
|
+
title={col.description}
|
|
1700
|
+
colspan={visible_columns.filter((column) => column.group === col.group)
|
|
1701
|
+
.length}
|
|
1702
|
+
>
|
|
1187
1703
|
{@html sanitize_html(col.group)}
|
|
1188
1704
|
</th>
|
|
1189
1705
|
{/if}
|
|
@@ -1210,61 +1726,47 @@
|
|
|
1210
1726
|
{/if}
|
|
1211
1727
|
{#each visible_columns as col (get_col_id(col))}
|
|
1212
1728
|
{@const col_id = get_col_id(col)}
|
|
1213
|
-
{@const
|
|
1214
|
-
|
|
1215
|
-
|
|
1729
|
+
{@const is_datetime = is_datetime_column(col)}
|
|
1730
|
+
{@const dt_mode = datetime_mode(col)}
|
|
1731
|
+
{@const datetime_label_id = get_datetime_label_id(col_id)}
|
|
1732
|
+
{@const drag_side = drag_over_col_id === col_id ? get_drag_side(col_id) : null}
|
|
1216
1733
|
{@const col_width = column_widths[col_id]}
|
|
1217
1734
|
<th
|
|
1218
1735
|
title={col.description}
|
|
1219
1736
|
tabindex={col.sortable === false ? undefined : 0}
|
|
1220
1737
|
role={col.sortable === false ? undefined : `button`}
|
|
1221
|
-
oncontextmenu={(event) =>
|
|
1222
|
-
if (
|
|
1223
|
-
!allow_better_toggle || col.color_scale === null ||
|
|
1224
|
-
col.color_scale === undefined
|
|
1225
|
-
) return
|
|
1226
|
-
event.preventDefault()
|
|
1227
|
-
event.stopPropagation()
|
|
1228
|
-
context_menu_col = col_id
|
|
1229
|
-
const rect = container_el?.getBoundingClientRect()
|
|
1230
|
-
context_menu_pos = {
|
|
1231
|
-
x: event.clientX - (rect?.left ?? 0),
|
|
1232
|
-
y: event.clientY - (rect?.top ?? 0),
|
|
1233
|
-
}
|
|
1234
|
-
}}
|
|
1738
|
+
oncontextmenu={(event) => open_column_context_menu(event, col_id)}
|
|
1235
1739
|
onclick={(event) => {
|
|
1236
1740
|
if (!drag_col_id && !resize_col_id) {
|
|
1237
|
-
sort_rows(
|
|
1238
|
-
col.label,
|
|
1239
|
-
col.group,
|
|
1240
|
-
event,
|
|
1241
|
-
)
|
|
1741
|
+
sort_rows(col.label, col.group, event)
|
|
1242
1742
|
}
|
|
1243
1743
|
}}
|
|
1244
1744
|
onkeydown={(event) => {
|
|
1245
1745
|
if (
|
|
1246
1746
|
(event.key === `Enter` || event.key === ` `) &&
|
|
1247
|
-
!drag_col_id &&
|
|
1747
|
+
!drag_col_id &&
|
|
1748
|
+
!resize_col_id
|
|
1248
1749
|
) {
|
|
1249
1750
|
event.preventDefault()
|
|
1250
1751
|
sort_rows(col.label, col.group, event)
|
|
1251
1752
|
}
|
|
1252
1753
|
}}
|
|
1253
1754
|
style={`${col.style ?? ``}${
|
|
1254
|
-
col_width
|
|
1255
|
-
? `; width: ${col_width}px; min-width: ${col_width}px`
|
|
1256
|
-
: ``
|
|
1755
|
+
col_width ? `; width: ${col_width}px; min-width: ${col_width}px` : ``
|
|
1257
1756
|
}`}
|
|
1258
1757
|
class:sticky-col={col.sticky}
|
|
1259
1758
|
class:not-sortable={col.sortable === false}
|
|
1260
1759
|
class:dragging={drag_col_id === col_id}
|
|
1261
1760
|
class:resizing={resize_col_id === col_id}
|
|
1761
|
+
class:datetime-select-open={datetime_select_open_col_id === col_id}
|
|
1262
1762
|
data-drag-side={drag_side}
|
|
1263
1763
|
draggable="true"
|
|
1264
1764
|
aria-dropeffect="move"
|
|
1265
1765
|
aria-sort={sort_state.column === col_id
|
|
1266
|
-
|
|
1267
|
-
|
|
1766
|
+
? sort_state.ascending
|
|
1767
|
+
? `ascending`
|
|
1768
|
+
: `descending`
|
|
1769
|
+
: `none`}
|
|
1268
1770
|
ondragstart={(event: DragEvent & { currentTarget: HTMLElement }) => {
|
|
1269
1771
|
handle_drag_start(event, col)
|
|
1270
1772
|
event.currentTarget.setAttribute(`aria-grabbed`, `true`)
|
|
@@ -1283,6 +1785,67 @@
|
|
|
1283
1785
|
{@html sanitize_html(col.label)}
|
|
1284
1786
|
{/if}
|
|
1285
1787
|
{@html sanitize_html(sort_indicator(col, sort_state))}
|
|
1788
|
+
{#if is_datetime}
|
|
1789
|
+
<span class="datetime-format-control">
|
|
1790
|
+
<button
|
|
1791
|
+
type="button"
|
|
1792
|
+
class="datetime-format-trigger"
|
|
1793
|
+
aria-labelledby={datetime_label_id}
|
|
1794
|
+
aria-haspopup="listbox"
|
|
1795
|
+
aria-expanded={datetime_select_open_col_id === col_id}
|
|
1796
|
+
data-mode={dt_mode}
|
|
1797
|
+
onkeydown={(event) => event.stopPropagation()}
|
|
1798
|
+
onmousedown={(event) => event.stopPropagation()}
|
|
1799
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
1800
|
+
onclick={(event) => {
|
|
1801
|
+
event.stopPropagation()
|
|
1802
|
+
datetime_select_open_col_id =
|
|
1803
|
+
datetime_select_open_col_id === col_id ? null : col_id
|
|
1804
|
+
}}
|
|
1805
|
+
{@attach tooltip({
|
|
1806
|
+
content: `Date/time format: ${datetime_format_labels[dt_mode]}`,
|
|
1807
|
+
placement: `top`,
|
|
1808
|
+
})}
|
|
1809
|
+
>
|
|
1810
|
+
<Icon icon="Calendar" />
|
|
1811
|
+
<span id={datetime_label_id} class="sr-only">
|
|
1812
|
+
Date/time format for {strip_html(col.label)}
|
|
1813
|
+
</span>
|
|
1814
|
+
</button>
|
|
1815
|
+
{#if datetime_select_open_col_id === col_id}
|
|
1816
|
+
<select
|
|
1817
|
+
class="datetime-format-select"
|
|
1818
|
+
aria-labelledby={datetime_label_id}
|
|
1819
|
+
value={dt_mode}
|
|
1820
|
+
size={datetime_format_options(col).length}
|
|
1821
|
+
onclick={(event) => {
|
|
1822
|
+
event.stopPropagation()
|
|
1823
|
+
if (event.currentTarget.value === dt_mode) {
|
|
1824
|
+
datetime_select_open_col_id = null
|
|
1825
|
+
}
|
|
1826
|
+
}}
|
|
1827
|
+
onkeydown={(event) => {
|
|
1828
|
+
event.stopPropagation()
|
|
1829
|
+
if (event.key === `Escape`) datetime_select_open_col_id = null
|
|
1830
|
+
}}
|
|
1831
|
+
onmousedown={(event) => event.stopPropagation()}
|
|
1832
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
1833
|
+
oninput={(event) => {
|
|
1834
|
+
event.stopPropagation()
|
|
1835
|
+
set_datetime_format(
|
|
1836
|
+
col,
|
|
1837
|
+
event.currentTarget.value as DateTimeFormatMode,
|
|
1838
|
+
)
|
|
1839
|
+
datetime_select_open_col_id = null
|
|
1840
|
+
}}
|
|
1841
|
+
>
|
|
1842
|
+
{#each datetime_format_options(col) as mode (mode)}
|
|
1843
|
+
<option value={mode}>{datetime_format_labels[mode]}</option>
|
|
1844
|
+
{/each}
|
|
1845
|
+
</select>
|
|
1846
|
+
{/if}
|
|
1847
|
+
</span>
|
|
1848
|
+
{/if}
|
|
1286
1849
|
<!-- Column resize handle -->
|
|
1287
1850
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
1288
1851
|
<span
|
|
@@ -1290,7 +1853,7 @@
|
|
|
1290
1853
|
onmousedown={(event) => start_resize(event, col)}
|
|
1291
1854
|
role="separator"
|
|
1292
1855
|
aria-orientation="vertical"
|
|
1293
|
-
aria-valuenow={
|
|
1856
|
+
aria-valuenow={col_width ?? 100}
|
|
1294
1857
|
aria-valuemin={50}
|
|
1295
1858
|
aria-valuemax={500}
|
|
1296
1859
|
></span>
|
|
@@ -1298,33 +1861,48 @@
|
|
|
1298
1861
|
{/each}
|
|
1299
1862
|
</tr>
|
|
1300
1863
|
</thead>
|
|
1301
|
-
|
|
1302
|
-
|
|
1864
|
+
{#snippet virtual_spacer(height: number)}
|
|
1865
|
+
<!-- preserves scroll geometry for the unrendered rows above/below the window -->
|
|
1866
|
+
{#if height > 0}
|
|
1867
|
+
<tr class="virtual-spacer" aria-hidden="true" style:height="{height}px">
|
|
1868
|
+
<td colspan={body_colspan}></td>
|
|
1869
|
+
</tr>
|
|
1870
|
+
{/if}
|
|
1871
|
+
{/snippet}
|
|
1872
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions (drag cell-range selection; keyboard copy handled on window) -->
|
|
1873
|
+
<tbody onpointermove={extend_cell_drag}>
|
|
1874
|
+
{@render virtual_spacer(spacer_top)}
|
|
1875
|
+
{#each display_rows as row, row_idx (get_row_id(row))}
|
|
1876
|
+
{@const abs_idx = display_range.start + row_idx}
|
|
1303
1877
|
{@const row_selected = show_row_select && is_row_selected(row)}
|
|
1304
1878
|
<tr
|
|
1305
|
-
animate:flip={{ duration: 500 }}
|
|
1879
|
+
animate:flip={{ duration: virtual_config ? 0 : 500 }}
|
|
1306
1880
|
style={row.style}
|
|
1307
|
-
class={row.class
|
|
1881
|
+
class={row.class}
|
|
1308
1882
|
class:selected={row_selected}
|
|
1883
|
+
data-title={row_title?.(row) || undefined}
|
|
1309
1884
|
tabindex={onrowclick ? 0 : undefined}
|
|
1885
|
+
onpointerdown={onrowpointerdown
|
|
1886
|
+
? (event) => onrowpointerdown(event, row)
|
|
1887
|
+
: undefined}
|
|
1310
1888
|
onclick={onrowclick ? (event) => onrowclick(event, row) : undefined}
|
|
1311
1889
|
ondblclick={onrowdblclick ? (event) => onrowdblclick(event, row) : undefined}
|
|
1312
1890
|
onkeydown={onrowclick
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1891
|
+
? (event) => {
|
|
1892
|
+
if (event.key === `Enter` || event.key === ` `) {
|
|
1893
|
+
event.preventDefault()
|
|
1894
|
+
onrowclick(event, row)
|
|
1895
|
+
} else if (event.key === `ArrowDown`) {
|
|
1896
|
+
event.preventDefault()
|
|
1897
|
+
const next = event.currentTarget.nextElementSibling
|
|
1898
|
+
if (next instanceof HTMLElement) next.focus()
|
|
1899
|
+
} else if (event.key === `ArrowUp`) {
|
|
1900
|
+
event.preventDefault()
|
|
1901
|
+
const prev = event.currentTarget.previousElementSibling
|
|
1902
|
+
if (prev instanceof HTMLElement) prev.focus()
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
: undefined}
|
|
1328
1906
|
>
|
|
1329
1907
|
{#if show_row_select}
|
|
1330
1908
|
<td class="select-col">
|
|
@@ -1336,36 +1914,45 @@
|
|
|
1336
1914
|
</td>
|
|
1337
1915
|
{/if}
|
|
1338
1916
|
{#if show_row_numbers}
|
|
1339
|
-
<td class="row-num-col">
|
|
1340
|
-
{(current_page - 1) * effective_page_size + row_idx + 1}
|
|
1341
|
-
</td>
|
|
1917
|
+
<td class="row-num-col">{abs_idx + 1}</td>
|
|
1342
1918
|
{/if}
|
|
1343
|
-
{#each visible_columns as col (get_col_id(col))}
|
|
1344
|
-
{@const
|
|
1919
|
+
{#each visible_columns as col, col_idx (get_col_id(col))}
|
|
1920
|
+
{@const col_id = get_col_id(col)}
|
|
1921
|
+
{@const val = row[col_id]}
|
|
1345
1922
|
{@const color = calc_color(val, col)}
|
|
1346
|
-
{@const col_width = column_widths[
|
|
1923
|
+
{@const col_width = column_widths[col_id]}
|
|
1924
|
+
{@const date_val = is_datetime_column(col)
|
|
1925
|
+
? format_datetime_cell(val, col)
|
|
1926
|
+
: null}
|
|
1347
1927
|
<td
|
|
1348
1928
|
data-col={col.label}
|
|
1349
|
-
data-sort-value={
|
|
1929
|
+
data-sort-value={get_cell_sort_attr(val)}
|
|
1930
|
+
data-row-idx={abs_idx}
|
|
1931
|
+
data-col-idx={col_idx}
|
|
1350
1932
|
class:sticky-col={col.sticky}
|
|
1933
|
+
class:cell-selected={selected_cell_keys.has(`${abs_idx}:${col_idx}`)}
|
|
1934
|
+
onpointerdown={(event) => start_cell_drag(event, abs_idx, col_idx)}
|
|
1935
|
+
oncontextmenu={(event) => {
|
|
1936
|
+
// keep the native context menu for links/buttons/inputs inside cells
|
|
1937
|
+
if (is_interactive_cell_target(event.target)) return
|
|
1938
|
+
open_column_context_menu(event, col_id)
|
|
1939
|
+
}}
|
|
1351
1940
|
style:--cell-bg={color.bg}
|
|
1352
1941
|
style:color={color.text}
|
|
1353
1942
|
style={`${col.cell_style ?? col.style ?? ``}${
|
|
1354
|
-
col_width
|
|
1355
|
-
? `; width: ${col_width}px; max-width: ${col_width}px`
|
|
1356
|
-
: ``
|
|
1943
|
+
col_width ? `; width: ${col_width}px; max-width: ${col_width}px` : ``
|
|
1357
1944
|
}`}
|
|
1358
1945
|
>
|
|
1359
1946
|
{#if special_cells?.[col.label]}
|
|
1360
1947
|
{@render special_cells[col.label]({ row, col, val })}
|
|
1361
1948
|
{:else if cell}
|
|
1362
1949
|
{@render cell({ row, col, val })}
|
|
1950
|
+
{:else if date_val != null}
|
|
1951
|
+
{date_val}
|
|
1363
1952
|
{:else if typeof val === `number` && !Number.isNaN(val)}
|
|
1364
1953
|
{format_num(val, col.format ?? default_num_format)}
|
|
1365
|
-
{:else if
|
|
1366
|
-
<span {@attach tooltip({ content: `Not available` })}>
|
|
1367
|
-
n/a
|
|
1368
|
-
</span>
|
|
1954
|
+
{:else if is_invalid(val)}
|
|
1955
|
+
<span {@attach tooltip({ content: `Not available` })}> n/a </span>
|
|
1369
1956
|
{:else}
|
|
1370
1957
|
{@html sanitize_html(val)}
|
|
1371
1958
|
{/if}
|
|
@@ -1375,15 +1962,11 @@
|
|
|
1375
1962
|
{:else}
|
|
1376
1963
|
{#if empty_message}
|
|
1377
1964
|
<tr class="empty-row">
|
|
1378
|
-
<td
|
|
1379
|
-
colspan={visible_columns.length + (show_row_select ? 1 : 0) +
|
|
1380
|
-
(show_row_numbers ? 1 : 0)}
|
|
1381
|
-
>
|
|
1382
|
-
{empty_message}
|
|
1383
|
-
</td>
|
|
1965
|
+
<td colspan={body_colspan}>{empty_message}</td>
|
|
1384
1966
|
</tr>
|
|
1385
1967
|
{/if}
|
|
1386
1968
|
{/each}
|
|
1969
|
+
{@render virtual_spacer(spacer_bottom)}
|
|
1387
1970
|
</tbody>
|
|
1388
1971
|
{#if footer}
|
|
1389
1972
|
<tfoot>
|
|
@@ -1395,24 +1978,22 @@
|
|
|
1395
1978
|
|
|
1396
1979
|
{@render sort_hint_element(`bottom`)}
|
|
1397
1980
|
|
|
1981
|
+
{#if virtual_config && sorted_data.length > display_rows.length}
|
|
1982
|
+
<div class="row-count-info">
|
|
1983
|
+
{display_rows.length} of {sorted_data.length} rows
|
|
1984
|
+
</div>
|
|
1985
|
+
{/if}
|
|
1986
|
+
|
|
1987
|
+
{#snippet page_btn(label: string, title: string, target_page: number, disabled: boolean)}
|
|
1988
|
+
<button class="page-btn" {disabled} onclick={() => (current_page = target_page)} {title}>
|
|
1989
|
+
{label}
|
|
1990
|
+
</button>
|
|
1991
|
+
{/snippet}
|
|
1992
|
+
|
|
1398
1993
|
{#if pagination_config && total_pages > 1}
|
|
1399
1994
|
<div class="pagination">
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
disabled={current_page === 1}
|
|
1403
|
-
onclick={() => current_page = 1}
|
|
1404
|
-
title="First page"
|
|
1405
|
-
>
|
|
1406
|
-
«
|
|
1407
|
-
</button>
|
|
1408
|
-
<button
|
|
1409
|
-
class="page-btn"
|
|
1410
|
-
disabled={current_page === 1}
|
|
1411
|
-
onclick={() => current_page--}
|
|
1412
|
-
title="Previous page"
|
|
1413
|
-
>
|
|
1414
|
-
‹
|
|
1415
|
-
</button>
|
|
1995
|
+
{@render page_btn(`«`, `First page`, 1, current_page === 1)}
|
|
1996
|
+
{@render page_btn(`‹`, `Previous page`, current_page - 1, current_page === 1)}
|
|
1416
1997
|
<span class="page-info">
|
|
1417
1998
|
Page
|
|
1418
1999
|
<input
|
|
@@ -1430,28 +2011,16 @@
|
|
|
1430
2011
|
of {total_pages}
|
|
1431
2012
|
<span class="row-count">({sorted_data.length} rows)</span>
|
|
1432
2013
|
</span>
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
disabled={current_page === total_pages}
|
|
1436
|
-
onclick={() => current_page++}
|
|
1437
|
-
title="Next page"
|
|
1438
|
-
>
|
|
1439
|
-
›
|
|
1440
|
-
</button>
|
|
1441
|
-
<button
|
|
1442
|
-
class="page-btn"
|
|
1443
|
-
disabled={current_page === total_pages}
|
|
1444
|
-
onclick={() => current_page = total_pages}
|
|
1445
|
-
title="Last page"
|
|
1446
|
-
>
|
|
1447
|
-
»
|
|
1448
|
-
</button>
|
|
2014
|
+
{@render page_btn(`›`, `Next page`, current_page + 1, current_page === total_pages)}
|
|
2015
|
+
{@render page_btn(`»`, `Last page`, total_pages, current_page === total_pages)}
|
|
1449
2016
|
{#if pagination_config.page_sizes}
|
|
1450
2017
|
<select
|
|
1451
2018
|
class="page-size-select"
|
|
1452
2019
|
onchange={(event) => {
|
|
1453
|
-
|
|
2020
|
+
const page_size = parseInt(event.currentTarget.value, 10)
|
|
2021
|
+
effective_page_size = page_size
|
|
1454
2022
|
current_page = 1
|
|
2023
|
+
pagination_config.on_page_size_change?.(page_size)
|
|
1455
2024
|
}}
|
|
1456
2025
|
>
|
|
1457
2026
|
{#each pagination_config.page_sizes as size (size)}
|
|
@@ -1465,11 +2034,13 @@
|
|
|
1465
2034
|
{/if}
|
|
1466
2035
|
|
|
1467
2036
|
<ContextMenu
|
|
1468
|
-
sections={
|
|
1469
|
-
selected_values={{
|
|
2037
|
+
sections={context_menu_sections}
|
|
2038
|
+
selected_values={{
|
|
2039
|
+
'Gradient direction': better_overrides.get(context_menu_col ?? ``) ?? ``,
|
|
2040
|
+
}}
|
|
1470
2041
|
position={context_menu_pos}
|
|
1471
2042
|
visible={context_menu_col !== null}
|
|
1472
|
-
on_close={() => context_menu_col = null}
|
|
2043
|
+
on_close={() => (context_menu_col = null)}
|
|
1473
2044
|
style={[
|
|
1474
2045
|
`--surface-bg: light-dark(#fff, #1e1e1e)`,
|
|
1475
2046
|
`--border-color: light-dark(rgba(0,0,0,0.15), rgba(255,255,255,0.15))`,
|
|
@@ -1479,13 +2050,7 @@
|
|
|
1479
2050
|
`--accent-color: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.15))`,
|
|
1480
2051
|
`z-index: 200`,
|
|
1481
2052
|
].join(`; `)}
|
|
1482
|
-
on_select={
|
|
1483
|
-
if (!context_menu_col) return
|
|
1484
|
-
const current = better_overrides.get(context_menu_col)
|
|
1485
|
-
if (current === option.value) better_overrides.delete(context_menu_col)
|
|
1486
|
-
else better_overrides.set(context_menu_col, option.value as `higher` | `lower`)
|
|
1487
|
-
context_menu_col = null
|
|
1488
|
-
}}
|
|
2053
|
+
on_select={handle_context_menu_select}
|
|
1489
2054
|
/>
|
|
1490
2055
|
</div>
|
|
1491
2056
|
|
|
@@ -1507,15 +2072,30 @@
|
|
|
1507
2072
|
.table-scroll.has-scroll {
|
|
1508
2073
|
border: 1px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.12));
|
|
1509
2074
|
border-radius: var(--border-radius, 3pt);
|
|
1510
|
-
overflow-x: hidden;
|
|
1511
|
-
overflow-y: auto;
|
|
1512
2075
|
}
|
|
1513
2076
|
table {
|
|
1514
2077
|
border-collapse: separate;
|
|
1515
2078
|
border-spacing: 0;
|
|
1516
2079
|
display: table; /* Override global display: block to enable sticky headers */
|
|
1517
2080
|
}
|
|
1518
|
-
|
|
2081
|
+
/* during a cell-range drag, native text selection would fight the
|
|
2082
|
+
rectangle highlight */
|
|
2083
|
+
.table-container.cell-dragging {
|
|
2084
|
+
cursor: cell;
|
|
2085
|
+
user-select: none;
|
|
2086
|
+
}
|
|
2087
|
+
/* background-image stacks on top of the per-cell heatmap background-color,
|
|
2088
|
+
so selected heatmap cells stay tinted underneath */
|
|
2089
|
+
td.cell-selected {
|
|
2090
|
+
background-image: linear-gradient(
|
|
2091
|
+
color-mix(in srgb, var(--accent-color, #4a9eff) 30%, transparent),
|
|
2092
|
+
color-mix(in srgb, var(--accent-color, #4a9eff) 30%, transparent)
|
|
2093
|
+
);
|
|
2094
|
+
box-shadow: inset 0 0 0 1px
|
|
2095
|
+
color-mix(in srgb, var(--accent-color, #4a9eff) 55%, transparent);
|
|
2096
|
+
}
|
|
2097
|
+
th,
|
|
2098
|
+
td {
|
|
1519
2099
|
padding: var(--heatmap-cell-padding, 1pt 5pt);
|
|
1520
2100
|
text-align: var(--heatmap-text-align, left);
|
|
1521
2101
|
border: var(--heatmap-cell-border, none);
|
|
@@ -1541,6 +2121,78 @@
|
|
|
1541
2121
|
th:hover {
|
|
1542
2122
|
background: var(--heatmap-header-hover-bg, var(--nav-bg));
|
|
1543
2123
|
}
|
|
2124
|
+
th.datetime-select-open {
|
|
2125
|
+
overflow: visible;
|
|
2126
|
+
z-index: 30;
|
|
2127
|
+
}
|
|
2128
|
+
.datetime-format-control {
|
|
2129
|
+
display: inline-flex;
|
|
2130
|
+
align-items: center;
|
|
2131
|
+
margin-left: 3px;
|
|
2132
|
+
position: relative;
|
|
2133
|
+
vertical-align: middle;
|
|
2134
|
+
}
|
|
2135
|
+
.datetime-format-trigger {
|
|
2136
|
+
display: inline-grid;
|
|
2137
|
+
place-items: center;
|
|
2138
|
+
width: 14px;
|
|
2139
|
+
height: 14px;
|
|
2140
|
+
padding: 0;
|
|
2141
|
+
border: 0;
|
|
2142
|
+
border-radius: 3px;
|
|
2143
|
+
background: transparent;
|
|
2144
|
+
color: inherit;
|
|
2145
|
+
cursor: pointer;
|
|
2146
|
+
line-height: 1;
|
|
2147
|
+
}
|
|
2148
|
+
.datetime-format-trigger:hover,
|
|
2149
|
+
.datetime-format-trigger[aria-expanded='true'] {
|
|
2150
|
+
background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.16));
|
|
2151
|
+
}
|
|
2152
|
+
.datetime-format-trigger :global(svg) {
|
|
2153
|
+
width: 10px;
|
|
2154
|
+
height: 10px;
|
|
2155
|
+
opacity: 0.75;
|
|
2156
|
+
transform: translateY(-1px);
|
|
2157
|
+
}
|
|
2158
|
+
.sr-only {
|
|
2159
|
+
position: absolute;
|
|
2160
|
+
width: 1px;
|
|
2161
|
+
height: 1px;
|
|
2162
|
+
padding: 0;
|
|
2163
|
+
margin: -1px;
|
|
2164
|
+
overflow: hidden;
|
|
2165
|
+
clip: rect(0, 0, 0, 0);
|
|
2166
|
+
white-space: nowrap;
|
|
2167
|
+
border: 0;
|
|
2168
|
+
}
|
|
2169
|
+
.datetime-format-select {
|
|
2170
|
+
position: absolute;
|
|
2171
|
+
top: calc(100% + 2px);
|
|
2172
|
+
right: 0;
|
|
2173
|
+
z-index: 20;
|
|
2174
|
+
min-width: max-content;
|
|
2175
|
+
max-width: 10em;
|
|
2176
|
+
padding: 2px;
|
|
2177
|
+
border: 1px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.18));
|
|
2178
|
+
border-radius: 3px;
|
|
2179
|
+
background: var(--heatmap-header-bg, var(--page-bg, Canvas));
|
|
2180
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
|
2181
|
+
color: inherit;
|
|
2182
|
+
cursor: pointer;
|
|
2183
|
+
font-size: 0.9em;
|
|
2184
|
+
line-height: 1.35;
|
|
2185
|
+
outline: none;
|
|
2186
|
+
}
|
|
2187
|
+
.datetime-format-select option {
|
|
2188
|
+
padding: 3px 8px;
|
|
2189
|
+
}
|
|
2190
|
+
.datetime-format-select option:checked {
|
|
2191
|
+
background: light-dark(rgba(74, 158, 255, 0.18), rgba(122, 179, 255, 0.28));
|
|
2192
|
+
box-shadow: 0 0 0 100vmax light-dark(rgba(74, 158, 255, 0.18), rgba(122, 179, 255, 0.28))
|
|
2193
|
+
inset;
|
|
2194
|
+
color: inherit;
|
|
2195
|
+
}
|
|
1544
2196
|
th.dragging {
|
|
1545
2197
|
opacity: 0.4;
|
|
1546
2198
|
cursor: grabbing;
|
|
@@ -1565,10 +2217,20 @@
|
|
|
1565
2217
|
td.sticky-col {
|
|
1566
2218
|
position: sticky;
|
|
1567
2219
|
left: 0;
|
|
1568
|
-
background: var(--page-bg, Canvas);
|
|
2220
|
+
background: var(--heatmap-sticky-cell-bg, var(--page-bg, Canvas));
|
|
1569
2221
|
z-index: 1;
|
|
1570
2222
|
border-right: 1px solid var(--border, #ddd);
|
|
1571
2223
|
}
|
|
2224
|
+
/* separate odd-row var so consumers with striped rows can composite their stripe
|
|
2225
|
+
color over the opaque sticky background (which must stay opaque to occlude columns
|
|
2226
|
+
scrolling beneath it), e.g.
|
|
2227
|
+
--heatmap-sticky-cell-odd-bg: linear-gradient(var(--stripe), var(--stripe)), var(--page-bg) */
|
|
2228
|
+
tbody tr:nth-child(odd) td.sticky-col {
|
|
2229
|
+
background: var(
|
|
2230
|
+
--heatmap-sticky-cell-odd-bg,
|
|
2231
|
+
var(--heatmap-sticky-cell-bg, var(--page-bg, Canvas))
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
1572
2234
|
tbody tr:hover {
|
|
1573
2235
|
filter: var(--heatmap-row-hover-filter, brightness(1.1));
|
|
1574
2236
|
}
|
|
@@ -1602,35 +2264,57 @@
|
|
|
1602
2264
|
transition: opacity 0.15s;
|
|
1603
2265
|
}
|
|
1604
2266
|
.table-container:hover .control-buttons,
|
|
1605
|
-
.control-buttons:focus-within
|
|
2267
|
+
.control-buttons:focus-within,
|
|
2268
|
+
/* keep visible while a dropdown/pane is open or when hosted in a panel
|
|
2269
|
+
toolbar (portaled out of the table, so container hover can't reveal it) */
|
|
2270
|
+
.control-buttons.force-visible,
|
|
2271
|
+
.control-buttons.portaled {
|
|
1606
2272
|
opacity: 1;
|
|
1607
2273
|
pointer-events: auto;
|
|
1608
2274
|
}
|
|
1609
|
-
.
|
|
1610
|
-
|
|
2275
|
+
.control-buttons.portaled {
|
|
2276
|
+
margin: 0;
|
|
2277
|
+
}
|
|
2278
|
+
/* .pane-toggle = the settings-pane gear, which sits in the control row and
|
|
2279
|
+
must match the other .icon-btn buttons: uniform square ghost buttons */
|
|
2280
|
+
.icon-btn,
|
|
2281
|
+
.control-buttons > :global(button.pane-toggle) {
|
|
2282
|
+
box-sizing: border-box;
|
|
2283
|
+
inline-size: 22px;
|
|
2284
|
+
block-size: 22px;
|
|
2285
|
+
padding: 0;
|
|
1611
2286
|
border: none;
|
|
1612
2287
|
border-radius: 3px;
|
|
1613
|
-
background:
|
|
1614
|
-
color
|
|
2288
|
+
background: transparent;
|
|
2289
|
+
/* dim resting color so the hover jump to full contrast reads clearly */
|
|
2290
|
+
color: light-dark(#6b7280, #98a0ae);
|
|
1615
2291
|
cursor: pointer;
|
|
1616
2292
|
display: flex;
|
|
1617
2293
|
align-items: center;
|
|
1618
2294
|
justify-content: center;
|
|
1619
2295
|
gap: 2px;
|
|
1620
2296
|
font-size: 0.8em;
|
|
2297
|
+
transition: color 0.02s linear;
|
|
1621
2298
|
}
|
|
1622
|
-
.icon-btn :global(svg)
|
|
1623
|
-
|
|
1624
|
-
|
|
2299
|
+
.icon-btn :global(svg),
|
|
2300
|
+
.control-buttons > :global(button.pane-toggle svg) {
|
|
2301
|
+
width: 14px;
|
|
2302
|
+
height: 14px;
|
|
1625
2303
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
2304
|
+
/* toolbar buttons give color-only hover feedback — no background shading */
|
|
2305
|
+
.icon-btn:hover,
|
|
2306
|
+
.control-buttons > :global(button.pane-toggle:hover) {
|
|
2307
|
+
background: transparent;
|
|
2308
|
+
color: light-dark(#000, #fff);
|
|
1628
2309
|
}
|
|
1629
2310
|
.icon-btn.active {
|
|
1630
|
-
|
|
2311
|
+
color: var(--active-color, #4a9eff);
|
|
1631
2312
|
}
|
|
1632
2313
|
.selection-badge {
|
|
1633
2314
|
position: relative;
|
|
2315
|
+
/* row-count badge next to the clear icon makes this one wider */
|
|
2316
|
+
inline-size: auto;
|
|
2317
|
+
padding: 0 4px;
|
|
1634
2318
|
}
|
|
1635
2319
|
.selection-badge .badge {
|
|
1636
2320
|
background: var(--highlight, #4a9eff);
|
|
@@ -1716,7 +2400,8 @@
|
|
|
1716
2400
|
tr.highlight {
|
|
1717
2401
|
background-color: var(--nav-bg) !important;
|
|
1718
2402
|
}
|
|
1719
|
-
tr.highlight,
|
|
2403
|
+
tr.highlight,
|
|
2404
|
+
tr.highlight :global(a) {
|
|
1720
2405
|
color: var(--highlight) !important;
|
|
1721
2406
|
}
|
|
1722
2407
|
|
|
@@ -1727,10 +2412,6 @@
|
|
|
1727
2412
|
vertical-align: middle;
|
|
1728
2413
|
padding: 2px !important;
|
|
1729
2414
|
}
|
|
1730
|
-
.select-col :global(svg) {
|
|
1731
|
-
display: block;
|
|
1732
|
-
margin: auto;
|
|
1733
|
-
}
|
|
1734
2415
|
tr.selected {
|
|
1735
2416
|
background: var(--highlight-bg, rgba(74, 158, 255, 0.15)) !important;
|
|
1736
2417
|
}
|
|
@@ -1738,6 +2419,18 @@
|
|
|
1738
2419
|
border-top: 1px solid var(--highlight, #4a9eff);
|
|
1739
2420
|
border-bottom: 1px solid var(--highlight, #4a9eff);
|
|
1740
2421
|
}
|
|
2422
|
+
/* Virtualized rows: spacers keep scroll geometry; the count line replaces
|
|
2423
|
+
pagination in infinite-scroll mode */
|
|
2424
|
+
.virtual-spacer td {
|
|
2425
|
+
padding: 0;
|
|
2426
|
+
border: none;
|
|
2427
|
+
}
|
|
2428
|
+
.row-count-info {
|
|
2429
|
+
padding: 4px 8px;
|
|
2430
|
+
font-size: 0.8em;
|
|
2431
|
+
text-align: right;
|
|
2432
|
+
opacity: 0.6;
|
|
2433
|
+
}
|
|
1741
2434
|
/* Pagination */
|
|
1742
2435
|
.pagination {
|
|
1743
2436
|
display: flex;
|
|
@@ -1857,11 +2550,13 @@
|
|
|
1857
2550
|
font-style: italic;
|
|
1858
2551
|
}
|
|
1859
2552
|
.row-num-col {
|
|
1860
|
-
text-align: right;
|
|
2553
|
+
text-align: var(--heatmap-row-num-align, right);
|
|
1861
2554
|
color: var(--text-muted, #888);
|
|
1862
2555
|
font-size: 0.85em;
|
|
1863
2556
|
width: 2em;
|
|
1864
|
-
padding
|
|
2557
|
+
/* left default matches the th,td --heatmap-cell-padding fallback */
|
|
2558
|
+
padding-left: var(--heatmap-row-num-padding-left, 5pt);
|
|
2559
|
+
padding-right: var(--heatmap-row-num-padding-right, 8px) !important;
|
|
1865
2560
|
}
|
|
1866
2561
|
.page-size-select {
|
|
1867
2562
|
padding: 2px 4px;
|