@svelte-lean/table 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @svelte-lean/table
2
2
 
3
+ ## 0.15.3
4
+
5
+ ### Patch Changes
6
+
7
+ - b526e70: A header that follows the page keeps its travel to itself: `--slean-table-travel` is set on the
8
+ `<thead>`, not on the scroll region. On the region every row inherited it, and each section a fast
9
+ scroll drew for the first time changed the table's height, so the travel, so every row already
10
+ drawn was styled again. An admin list of 1000 bets at 4x CPU blocked for 8.2 s in its first fast
11
+ scroll and 2.2 s with the travel on the header (`apps/playground/tests/bench/head-travel.bench.spec.ts`).
12
+
13
+ ## 0.15.2
14
+
15
+ ### Patch Changes
16
+
17
+ - Pinned columns: the edge shade is drawn once per pinned side instead of once per row. 0.15.1 drew it as a `::after` on every edge cell, two pseudo-elements per row to paint and composite, and a fast scroll through a long page paid for them. The scroll region now holds one `.slean-table-pin-shade` element per pinned side (`data-pin="left|right"`, `aria-hidden`), stacked with the table in one grid cell (the region carries `data-pinned`; the table there is its own stacking context, `isolation: isolate`, so a page scroll reuses its paint instead of repainting every sticky cell), so it is as tall as the table, header to footer, sticky at the side's pinned width (`--slean-table-pin-inset`, set by the renderer, so a resize or a pin change moves it), above the sticky header and below the column panel and the top layer, and never takes the pointer. The look is the same: `--slean-table-pin-shadow` to transparent, `--slean-table-pin-shadow-size` (8px) wide, mirrored in RTL, over the edge cell's line, and shown only while content lies past that side (scroll-state queries). It is also continuous across the lines between rows, which the per-cell shade left out. Where scroll-state queries are missing it shows while the rows overflow sideways (`data-overflows`, read by a ResizeObserver only there), not on a table that fits. A table without pinned columns renders no shade and its region stays a plain block. Placeholder (skeleton) rows now carry the pin attributes too, so pinned columns stay in place while a table loads. Behavior changes: an edge cell's `::after` is no longer used (a theme that styled `[data-pin-edge]::after` styles `.slean-table-pin-shade` instead), and in a table with pinned columns a cell's `z-index` no longer reaches outside the table (menus and popovers are in the top layer and unaffected). Tested in `apps/playground/tests/table/pinned.spec.ts` (position, continuity by pixel sampling, visibility at both ends, RTL, resize and pin change, region and page scrolling, pointer, loading, empty, compact, no pins) and measured by `apps/playground/tests/bench/pin-shade.bench.spec.ts` (1000 rows, fast page scroll at 4x CPU, no shade, the 0.15.1 shade and this one interleaved; `apps/playground/results/pin-shade-bench.json`): in grid layout the per-cell shade added about a tenth to the rendering work and this one is within noise of no shade; in table layout, where the table's own stacking context lets a page scroll reuse its paint, the rendering work is about a third of either.
18
+
3
19
  ## 0.15.1
4
20
 
5
21
  ### Patch Changes
package/dist/style.css CHANGED
@@ -641,46 +641,58 @@
641
641
  :where(thead .slean-table-mover, thead .slean-table-selection, thead .slean-table-expander) {
642
642
  z-index: 4;
643
643
  }
644
- /* Pinned edges: a line and a shade as tall as the cell (a blurred shadow faded at each row's
645
- ends, a dashed line); with scroll-state queries, only while content lies past the edge. */
644
+ /* Pinned edges: a line on the edge cells, one shade per side. */
646
645
  :where(.slean-table-scroll > table > * > tr > [data-pin='left'][data-pin-edge]) {
647
646
  border-inline-end: 1px solid var(--slean-table-border);
648
647
  }
649
648
  :where(.slean-table-scroll > table > * > tr > [data-pin='right'][data-pin-edge]) {
650
649
  border-inline-start: 1px solid var(--slean-table-border);
651
650
  }
652
- :where(.slean-table-scroll > table > * > tr > [data-pin-edge])::after {
653
- content: '';
654
- position: absolute;
655
- inset-block: 0;
656
- inset-inline-start: 100%;
651
+ /* One grid cell the table's size (a grid table's width is known); the table is a stacking
652
+ context, so a page scroll reuses its paint. */
653
+ :where(.slean-table-scroll[data-pinned]) {
654
+ display: grid;
655
+ }
656
+ :where(.slean-table[data-layout='grid'] > .slean-table-scroll[data-pinned]) {
657
+ grid-template-columns: max(100%, var(--slean-table-min-width, 0px));
658
+ }
659
+ :where(.slean-table-scroll[data-pinned] > *) {
660
+ grid-area: 1 / 1;
661
+ isolation: isolate;
662
+ }
663
+ /* Sticky at the pinned width, over the edge line; above the sticky header (5). */
664
+ :where(.slean-table-pin-shade) {
665
+ position: sticky;
666
+ z-index: 6;
667
+ inset-inline-start: var(--slean-table-pin-inset);
668
+ margin-inline-start: var(--slean-table-pin-inset);
657
669
  inline-size: var(--slean-table-pin-shadow-size);
658
670
  background: linear-gradient(to right, var(--slean-table-pin-shadow), transparent);
659
671
  pointer-events: none;
660
672
  }
661
- :where(.slean-table-scroll > table > * > tr > [data-pin='right'][data-pin-edge])::after {
662
- inset-inline: auto 100%;
663
- scale: -1 1;
673
+ :where(.slean-table-pin-shade[data-pin='right']) {
674
+ justify-self: end;
675
+ inset-inline: auto var(--slean-table-pin-inset);
676
+ margin-inline: 0 var(--slean-table-pin-inset);
664
677
  }
665
- :where(.slean-table-scroll > table > * > tr > [data-pin-edge]:dir(rtl))::after {
678
+ :where(
679
+ .slean-table-pin-shade[data-pin='right']:dir(ltr),
680
+ .slean-table-pin-shade[data-pin='left']:dir(rtl)
681
+ ) {
666
682
  scale: -1 1;
667
683
  }
668
- :where(.slean-table-scroll > table > * > tr > [data-pin='right'][data-pin-edge]:dir(rtl))::after {
669
- scale: none;
684
+ /* Shown while content lies past that side; without scroll-state, while rows overflow. */
685
+ :where(.slean-table-scroll:not([data-overflows]) > .slean-table-pin-shade) {
686
+ opacity: 0;
670
687
  }
671
- @supports (container-type: scroll-state) {
672
- :where(.slean-table-scroll > table > * > tr > [data-pin-edge])::after {
673
- opacity: 0;
674
- }
675
- @container scroll-state(scrollable: inline-start) {
676
- :where(.slean-table-scroll > table > * > tr > [data-pin='left'][data-pin-edge])::after {
677
- opacity: 1;
678
- }
688
+ @container scroll-state(scrollable: inline-start) {
689
+ :where(.slean-table-pin-shade[data-pin='left']) {
690
+ opacity: 1;
679
691
  }
680
- @container scroll-state(scrollable: inline-end) {
681
- :where(.slean-table-scroll > table > * > tr > [data-pin='right'][data-pin-edge])::after {
682
- opacity: 1;
683
- }
692
+ }
693
+ @container scroll-state(scrollable: inline-end) {
694
+ :where(.slean-table-pin-shade[data-pin='right']) {
695
+ opacity: 1;
684
696
  }
685
697
  }
686
698
  :where(.slean-table-scroll > table > tbody > tr > td[data-range='true']) {
@@ -1186,7 +1198,8 @@
1186
1198
  .slean-table-pagination,
1187
1199
  .slean-table-resize,
1188
1200
  .slean-table-edit,
1189
- .slean-table-grip
1201
+ .slean-table-grip,
1202
+ .slean-table-pin-shade
1190
1203
  ) {
1191
1204
  display: none !important;
1192
1205
  }
@@ -840,6 +840,7 @@
840
840
  let fits = $state(false);
841
841
  let scrollsY = $state(true);
842
842
  let travel = $state(0);
843
+ const headFollows = $derived(stickyHeader && !height && !fits && !scrollsY);
843
844
  $effect(() => {
844
845
  if (!stickyHeader || height || typeof ResizeObserver === 'undefined') {
845
846
  fits = false;
@@ -878,32 +879,56 @@
878
879
  /**
879
880
  * Sticky offsets of the pinned columns, computed once per column change instead of per cell.
880
881
  * Logical insets follow the table's own direction. `edge` marks the last start-pinned and the
881
- * first end-pinned column, where the stylesheet draws the pin shadow.
882
+ * first end-pinned column, where the stylesheet draws the edge line.
883
+ *
884
+ * `edges` places the shade of each pinned side: one element in the scroll region (style.css),
885
+ * not one per edge cell, at that side's pinned width less the edge line (1px, which the shade
886
+ * covers), so a resize or a pin change moves it.
882
887
  */
883
- const pins = $derived.by(() => {
884
- const result = new Map<string, { pin: Exclude<Pin, false>; style: string; edge?: true }>();
888
+ const pinning = $derived.by(() => {
889
+ const cells = new Map<string, { pin: Exclude<Pin, false>; style: string; edge?: true }>();
890
+ const edges: [Exclude<Pin, false>, string][] = [];
885
891
  const lead = leads;
886
892
  for (const side of ['left', 'right'] as const) {
887
893
  const ordered = side === 'left' ? cols : [...cols].reverse();
894
+ const at = (offset: number) =>
895
+ side === 'left' && lead.length ? `calc(${offset}px + ${lead.join(' + ')})` : `${offset}px`;
888
896
  let offset = 0;
889
897
  let last: string | undefined;
890
898
  for (const col of ordered) {
891
899
  if (table.pinOf(col) !== side) continue;
892
- const inset =
893
- side === 'left' && lead.length
894
- ? `calc(${offset}px + ${lead.join(' + ')})`
895
- : `${offset}px`;
896
900
  const property = side === 'left' ? 'inset-inline-start' : 'inset-inline-end';
897
- result.set(col.id, { pin: side, style: `${property}:${inset};` });
901
+ cells.set(col.id, { pin: side, style: `${property}:${at(offset)};` });
898
902
  offset += grid ? gridWidth(col) : table.columnWidth(col);
899
903
  last = col.id;
900
904
  }
901
- const edge = last === undefined ? undefined : result.get(last);
902
- if (edge) edge.edge = true;
905
+ if (last === undefined) continue;
906
+ cells.get(last)!.edge = true;
907
+ edges.push([side, at(offset - 1)]);
903
908
  }
904
- return result;
909
+ return { cells, edges };
910
+ });
911
+ const pins = $derived(pinning.cells);
912
+ /**
913
+ * Where scroll-state queries are missing, the shades show only while the rows overflow sideways
914
+ * (`data-overflows`, read when a size changes): a table stretched to fit widens its columns.
915
+ */
916
+ let overflows = $state(false);
917
+ $effect(() => {
918
+ if (
919
+ !pinning.edges.length ||
920
+ typeof ResizeObserver === 'undefined' ||
921
+ globalThis.CSS?.supports('container-type', 'scroll-state')
922
+ )
923
+ return;
924
+ const observer = new ResizeObserver(
925
+ () => (overflows = viewport.scrollWidth > viewport.clientWidth + 1)
926
+ );
927
+ observer.observe(viewport);
928
+ observer.observe(tableElement);
929
+ return () => observer.disconnect();
905
930
  });
906
- /** Pin attributes of a filter or footer cell. */
931
+ /** Pin attributes of a filter, footer or placeholder cell. */
907
932
  function pinned(col: Column<T>) {
908
933
  const pin = pins.get(col.id);
909
934
  return pin ? { 'data-pin': pin.pin, 'data-pin-edge': pin.edge, style: pin.style } : {};
@@ -1269,8 +1294,9 @@
1269
1294
  class="slean-table-skeleton-row"
1270
1295
  aria-hidden="true"
1271
1296
  role={grid ? 'row' : undefined}
1272
- >{@render leading()}{#each cols as col (col.id)}<td role={grid ? 'cell' : undefined}
1273
- ><span class="slean-table-skeleton"></span></td
1297
+ >{@render leading()}{#each cols as col (col.id)}<td
1298
+ role={grid ? 'cell' : undefined}
1299
+ {...pinned(col)}><span class="slean-table-skeleton"></span></td
1274
1300
  >{/each}</tr
1275
1301
  >{/each}
1276
1302
  {#if !model.rows.length && !skeleton.length}<tr role={grid ? 'row' : undefined}
@@ -1697,10 +1723,10 @@
1697
1723
  bind:this={viewport}
1698
1724
  class="slean-table-scroll"
1699
1725
  data-page-sticky={fits || undefined}
1700
- data-head-follows={(stickyHeader && !height && !fits && !scrollsY) || undefined}
1701
- style:--slean-table-travel={stickyHeader && !height && !fits && !scrollsY
1702
- ? `${travel}px`
1703
- : undefined}
1726
+ data-head-follows={headFollows || undefined}
1727
+ data-pinned={pinning.edges.length > 0 || undefined}
1728
+ data-overflows={overflows || undefined}
1729
+ style:--slean-table-min-width={tracks?.min}
1704
1730
  style:max-height={height}
1705
1731
  tabindex={height ? 0 : undefined}
1706
1732
  role="region"
@@ -1724,7 +1750,6 @@
1724
1750
  class:slean-table-sticky={stickyHeader && (fits || scrollsY)}
1725
1751
  style:table-layout={grid ? undefined : tableLayout}
1726
1752
  style:--slean-table-tracks={tracks?.template}
1727
- style:--slean-table-min-width={tracks?.min}
1728
1753
  >
1729
1754
  {#if caption}<caption>{caption}</caption>{/if}
1730
1755
  {#if !grid}<colgroup
@@ -1737,7 +1762,12 @@
1737
1762
  style:width={colWidth(col)}
1738
1763
  />{/each}</colgroup
1739
1764
  >{/if}
1740
- {#if showHeader}<thead role={grid ? 'rowgroup' : undefined}>
1765
+ {#if showHeader}<!-- The travel is the header's own: set on the region, every row under it
1766
+ inherited it, and each section a fast scroll drew changed the table's height,
1767
+ the travel, and so restyled every row already drawn. --><thead
1768
+ role={grid ? 'rowgroup' : undefined}
1769
+ style:--slean-table-travel={headFollows ? `${travel}px` : undefined}
1770
+ >
1741
1771
  {#each headerLevels as row, level}<tr
1742
1772
  role={grid ? 'row' : undefined}
1743
1773
  aria-rowindex={ariaRowCount === undefined ? undefined : level + 1}
@@ -1870,6 +1900,12 @@
1870
1900
  ></tfoot
1871
1901
  >{/if}
1872
1902
  </table>
1903
+ {#each pinning.edges as [side, inset]}<div
1904
+ class="slean-table-pin-shade"
1905
+ data-pin={side}
1906
+ aria-hidden="true"
1907
+ style:--slean-table-pin-inset={inset}
1908
+ ></div>{/each}
1873
1909
  </div>
1874
1910
  {#if pagination}<div class="slean-table-pagination">
1875
1911
  <span class="slean-table-page-status" aria-live="polite"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-lean/table",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "A Svelte-native table and data-grid engine that ships only the features you use.",
5
5
  "license": "MIT",
6
6
  "author": "Ömer Say",