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