@svelte-lean/table 0.1.1 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @svelte-lean/table
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `layout="grid"` for long pages: the same elements laid out as CSS grid rows with explicit ARIA roles. Body rows stand in sections of 50 under `content-visibility: auto`, so the browser styles, lays out and paints only the sections near the screen; skipped sections assume the measured row height, and complete pages are laid out ahead in idle time. Column widths are measured once from hidden copies of the header and a sample of rows and kept; a declared `width` is a minimum and `maxWidth` (default 480) caps a measured width.
8
+
9
+ `progressive: true` now makes the first batch one screenful, slices are sized from the measured cost of a row, and a table first rendered in the browser mounts progressively too (server output and its hydration stay complete).
10
+
11
+ Rendering cost per row went down in both layouts: row nodes keep their identity across data updates with the same rows, body cells carry no per-cell `data-row` attribute or inline alignment style (alignment is `data-align`), row clicks, keys and drags are delegated to the table, and striping is a `data-even` attribute instead of a structural selector. Selectors that relied on `td[data-row]` should use `tr[data-row-id] > td[data-column]`.
12
+
13
+ A header in a region that scrolls sideways but not down (no `height`, a table wider than its container) is no longer `position: sticky`: it could never stick there, and a sticky box in a scroller made every row painted under it cost compositing work (about a tenth of a fast scroll through 1000 rows).
14
+
3
15
  ## 0.1.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -9,7 +9,7 @@ Svelte Lean Table: a data table for Svelte 5 that renders a native `<table>`, ke
9
9
  plain object and adds capabilities through separate entry points. Part of the
10
10
  [Svelte Lean](https://github.com/say16/svelte-lean) ecosystem.
11
11
 
12
- > Preview release (`0.1.0-next`).
12
+ > Pre-1.0: minor versions may change the API; see the changelog.
13
13
 
14
14
  ## Install
15
15
 
@@ -45,6 +45,11 @@ pinning, resizing, visibility and ordering, inline editing, cell ranges, row reo
45
45
  empty and error states are props of `<DataTable />`. State is a serializable `TableState` object:
46
46
  pass `bind:state` or `onStateChange` to own it.
47
47
 
48
+ Long pages: `progressive` mounts a large page in slices so the main thread never blocks on it, and
49
+ `layout="grid"` lays rows out as CSS grid rows in sections the browser skips while off screen
50
+ (`content-visibility: auto`), with column widths measured once from the content. Nothing is
51
+ virtualized; every row stays in the document.
52
+
48
53
  Keyboard: with `onEdit` or `onCellRangeChange` the body is one Tab stop (roving `tabindex` over
49
54
  cells, arrow keys move, Enter/F2 edits, focus returns to the cell after an edit). Text direction
50
55
  follows the nearest `dir` attribute unless the `dir` prop is given. `selection: 'single'` needs an
package/dist/style.css CHANGED
@@ -342,7 +342,9 @@
342
342
  ) {
343
343
  border-start-end-radius: calc(var(--slean-table-radius) - 1px);
344
344
  }
345
- :where(.slean-table-scroll > table > tbody > tr:last-child > td) {
345
+ /* The last body row (`data-last`, set by the renderer) leaves the line to the region's border.
346
+ An attribute, not \`:last-child\`: appending a row then restyles two rows, not every cell. */
347
+ :where(.slean-table-scroll > table > tbody > tr[data-last] > td) {
346
348
  border-bottom: 0;
347
349
  }
348
350
  :where(.slean-table-heading) {
@@ -426,6 +428,14 @@
426
428
  min-width: 3rem;
427
429
  font-weight: 400;
428
430
  }
431
+ /* Alignment is an attribute, not an inline style: an inline style per cell defeats the
432
+ browser's style sharing between cells, which is most of the styling cost of a long table. */
433
+ :where(.slean-table-scroll > table > * > tr > td[data-align='end']) {
434
+ text-align: end;
435
+ }
436
+ :where(.slean-table-scroll > table > * > tr > td[data-align='center']) {
437
+ text-align: center;
438
+ }
429
439
  :where(.slean-table-cell) {
430
440
  display: flex;
431
441
  align-items: center;
@@ -476,6 +486,28 @@
476
486
  ) {
477
487
  color: var(--slean-table-fg);
478
488
  }
489
+ /* The expand toggle of a plain grid row: the chevron is drawn by the button itself (two borders
490
+ of a turned square), one element per row instead of an SVG. It points down when open, and the
491
+ other way in right-to-left tables. */
492
+ :where(.slean-table-chevron)::before {
493
+ content: '';
494
+ display: block;
495
+ inline-size: 0.4375rem;
496
+ block-size: 0.4375rem;
497
+ margin: auto;
498
+ border: solid currentColor;
499
+ border-width: 0 1.5px 1.5px 0;
500
+ rotate: -45deg;
501
+ translate: -1px 0;
502
+ }
503
+ :where(.slean-table-chevron[aria-expanded='true'])::before {
504
+ rotate: 45deg;
505
+ translate: 0 -1px;
506
+ }
507
+ :where(.slean-table-chevron[aria-expanded='false']):dir(rtl)::before {
508
+ rotate: 135deg;
509
+ translate: 1px 0;
510
+ }
479
511
  /* Keeps the text of rows without a toggle aligned with the rows that have one. */
480
512
  :where(.slean-table-expand-spacer) {
481
513
  inline-size: 1.5rem;
@@ -502,14 +534,9 @@
502
534
  }
503
535
 
504
536
  /* Row states write one variable on the row; cells paint it (pinned cells included). Stripes
505
- count data rows only, so an open detail row does not shift them. */
506
- :where(
507
- .slean-table[data-striped='true']
508
- > .slean-table-scroll
509
- > table
510
- > tbody
511
- > tr:nth-child(even of [data-row-id])
512
- ) {
537
+ follow the row's position on the page (`data-even`, set by the renderer), so an open detail
538
+ row does not shift them and inserting rows restyles nothing else. */
539
+ :where(.slean-table[data-striped='true'] > .slean-table-scroll > table > tbody > tr[data-even]) {
513
540
  --slean-table-row-bg: var(--slean-table-row-stripe);
514
541
  }
515
542
  :where(
@@ -773,6 +800,184 @@
773
800
  }
774
801
  }
775
802
  /* Print: inline sticky offsets and the scroll limit are inline styles, hence !important. */
803
+ /*
804
+ * Grid layout (`layout="grid"`). The native elements stay, with explicit ARIA roles; every row
805
+ * is a grid over the renderer's track list (`--slean-table-tracks`), and a body section off
806
+ * screen is skipped by the browser (`content-visibility: auto`), so a long page costs the layout
807
+ * and paint of what is on screen. `--slean-table-min-width` is the sum of the tracks' minimums:
808
+ * below it the rows scroll sideways. `data-measuring` marks the hidden copy of a few rows the
809
+ * renderer reads the content widths from, and removes before the frame ends.
810
+ */
811
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table) {
812
+ display: block;
813
+ width: max(100%, var(--slean-table-min-width, 0px));
814
+ min-width: 0;
815
+ }
816
+ :where(
817
+ .slean-table[data-layout='grid']
818
+ > .slean-table-scroll
819
+ > table
820
+ > :is(thead, tbody, tfoot, caption)
821
+ ) {
822
+ display: block;
823
+ }
824
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > * > tr) {
825
+ display: grid;
826
+ grid-template-columns: var(--slean-table-tracks);
827
+ }
828
+ /* Each body row its own stacking context: sticky pinned cells then layerize per row, not per
829
+ cell, which keeps compositing cheap while a long page scrolls. */
830
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > tbody > tr) {
831
+ isolation: isolate;
832
+ }
833
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > tbody.slean-table-chunk) {
834
+ content-visibility: auto;
835
+ contain-intrinsic-block-size: auto
836
+ calc(var(--slean-table-chunk-rows, 50) * var(--slean-table-row-estimate, 2.75rem));
837
+ }
838
+ /* A section whose rows are still being mounted holds the height they are expected to stand. */
839
+ :where(
840
+ .slean-table[data-layout='grid']
841
+ > .slean-table-scroll
842
+ > table
843
+ > tbody.slean-table-chunk[data-pending]
844
+ ) {
845
+ min-block-size: calc(
846
+ var(--slean-table-chunk-rows, 50) * var(--slean-table-row-estimate, 2.75rem)
847
+ );
848
+ }
849
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > * > tr > :is(th, td)) {
850
+ display: flex;
851
+ align-items: center;
852
+ min-width: 0;
853
+ }
854
+ :where(
855
+ .slean-table[data-layout='grid'] > .slean-table-scroll > table > * > tr > :is(th, td) > *
856
+ ) {
857
+ flex: 1 1 auto;
858
+ min-width: 0;
859
+ }
860
+ :where(
861
+ .slean-table[data-layout='grid'] > .slean-table-scroll > table > * > tr > .slean-table-full
862
+ ) {
863
+ grid-column: 1 / -1;
864
+ display: block;
865
+ }
866
+ :where(
867
+ .slean-table[data-layout='grid'] > .slean-table-scroll > table > tbody > tr > td[data-clip]
868
+ ) {
869
+ overflow: hidden;
870
+ }
871
+ :where(
872
+ .slean-table[data-layout='grid']
873
+ > .slean-table-scroll
874
+ > table
875
+ > tbody
876
+ > tr
877
+ > td[data-clip]
878
+ > .slean-table-cell
879
+ ) {
880
+ overflow: hidden;
881
+ white-space: nowrap;
882
+ text-overflow: ellipsis;
883
+ display: block;
884
+ }
885
+ :where(.slean-table-scroll > table[data-measuring]) {
886
+ display: block;
887
+ width: max-content;
888
+ }
889
+ :where(.slean-table-scroll > table[data-measuring] > *) {
890
+ display: block;
891
+ }
892
+ :where(.slean-table-scroll > table[data-measuring] > * > tr) {
893
+ display: grid;
894
+ content-visibility: visible;
895
+ }
896
+ :where(.slean-table-scroll > table[data-measuring] > * > tr > :is(th, td)) {
897
+ display: flex;
898
+ align-items: center;
899
+ position: static;
900
+ }
901
+ :where(.slean-table-scroll > table[data-measuring] > * > tr) {
902
+ grid-template-columns: none;
903
+ grid-auto-flow: column;
904
+ grid-auto-columns: max-content;
905
+ width: max-content;
906
+ }
907
+ :where(.slean-table-scroll > table[data-measuring] > * > tr > :is(th, td) > *) {
908
+ flex: none;
909
+ }
910
+ :where(.slean-table-scroll > table[data-measuring] > thead > tr) {
911
+ grid-auto-columns: min-content;
912
+ }
913
+ /* A plain grid cell is its own content box: the td carries `slean-table-cell` (no wrapper). */
914
+ :where(
915
+ .slean-table[data-layout='grid']
916
+ > .slean-table-scroll
917
+ > table
918
+ > tbody
919
+ > tr
920
+ > td.slean-table-cell
921
+ ) {
922
+ gap: 0.5rem;
923
+ }
924
+ :where(
925
+ .slean-table[data-layout='grid']
926
+ > .slean-table-scroll
927
+ > table
928
+ > tbody
929
+ > tr
930
+ > td.slean-table-cell[data-align='end']
931
+ ) {
932
+ justify-content: flex-end;
933
+ }
934
+ :where(
935
+ .slean-table[data-layout='grid']
936
+ > .slean-table-scroll
937
+ > table
938
+ > tbody
939
+ > tr
940
+ > td.slean-table-cell[data-align='center']
941
+ ) {
942
+ justify-content: center;
943
+ }
944
+ :where(
945
+ .slean-table[data-layout='grid']
946
+ > .slean-table-scroll
947
+ > table
948
+ > tbody
949
+ > tr
950
+ > td.slean-table-cell:is([data-clip], .slean-table-ellipsis)
951
+ ) {
952
+ display: block;
953
+ overflow: hidden;
954
+ white-space: nowrap;
955
+ text-overflow: ellipsis;
956
+ line-height: 1.5rem;
957
+ }
958
+ /* A grid body row paints its own ground once; its cells are transparent, except the sticky
959
+ ones (pinned, selection, expand), which must stay opaque over what scrolls beneath them. */
960
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > tbody > tr) {
961
+ background: var(--slean-table-row-bg, var(--slean-table-bg));
962
+ }
963
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > tbody > tr > td) {
964
+ background: none;
965
+ }
966
+ :where(
967
+ .slean-table[data-layout='grid']
968
+ > .slean-table-scroll
969
+ > table
970
+ > tbody
971
+ > tr
972
+ > :is([data-pin], .slean-table-selection, .slean-table-expander)
973
+ ) {
974
+ background: var(--slean-table-row-bg, var(--slean-table-bg));
975
+ }
976
+ /* Grid header labels wrap at word boundaries rather than widen their column. */
977
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll > table > thead > tr > th) {
978
+ white-space: normal;
979
+ overflow-wrap: normal;
980
+ }
776
981
  @media print {
777
982
  :where(
778
983
  .slean-table-toolbar,