@svelte-lean/table 0.15.1 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @svelte-lean/table
2
2
 
3
+ ## 0.15.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
3
9
  ## 0.15.1
4
10
 
5
11
  ### 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
  }
@@ -878,32 +878,56 @@
878
878
  /**
879
879
  * Sticky offsets of the pinned columns, computed once per column change instead of per cell.
880
880
  * 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.
881
+ * first end-pinned column, where the stylesheet draws the edge line.
882
+ *
883
+ * `edges` places the shade of each pinned side: one element in the scroll region (style.css),
884
+ * not one per edge cell, at that side's pinned width less the edge line (1px, which the shade
885
+ * covers), so a resize or a pin change moves it.
882
886
  */
883
- const pins = $derived.by(() => {
884
- const result = new Map<string, { pin: Exclude<Pin, false>; style: string; edge?: true }>();
887
+ const pinning = $derived.by(() => {
888
+ const cells = new Map<string, { pin: Exclude<Pin, false>; style: string; edge?: true }>();
889
+ const edges: [Exclude<Pin, false>, string][] = [];
885
890
  const lead = leads;
886
891
  for (const side of ['left', 'right'] as const) {
887
892
  const ordered = side === 'left' ? cols : [...cols].reverse();
893
+ const at = (offset: number) =>
894
+ side === 'left' && lead.length ? `calc(${offset}px + ${lead.join(' + ')})` : `${offset}px`;
888
895
  let offset = 0;
889
896
  let last: string | undefined;
890
897
  for (const col of ordered) {
891
898
  if (table.pinOf(col) !== side) continue;
892
- const inset =
893
- side === 'left' && lead.length
894
- ? `calc(${offset}px + ${lead.join(' + ')})`
895
- : `${offset}px`;
896
899
  const property = side === 'left' ? 'inset-inline-start' : 'inset-inline-end';
897
- result.set(col.id, { pin: side, style: `${property}:${inset};` });
900
+ cells.set(col.id, { pin: side, style: `${property}:${at(offset)};` });
898
901
  offset += grid ? gridWidth(col) : table.columnWidth(col);
899
902
  last = col.id;
900
903
  }
901
- const edge = last === undefined ? undefined : result.get(last);
902
- if (edge) edge.edge = true;
904
+ if (last === undefined) continue;
905
+ cells.get(last)!.edge = true;
906
+ edges.push([side, at(offset - 1)]);
903
907
  }
904
- return result;
908
+ return { cells, edges };
909
+ });
910
+ const pins = $derived(pinning.cells);
911
+ /**
912
+ * Where scroll-state queries are missing, the shades show only while the rows overflow sideways
913
+ * (`data-overflows`, read when a size changes): a table stretched to fit widens its columns.
914
+ */
915
+ let overflows = $state(false);
916
+ $effect(() => {
917
+ if (
918
+ !pinning.edges.length ||
919
+ typeof ResizeObserver === 'undefined' ||
920
+ globalThis.CSS?.supports('container-type', 'scroll-state')
921
+ )
922
+ return;
923
+ const observer = new ResizeObserver(
924
+ () => (overflows = viewport.scrollWidth > viewport.clientWidth + 1)
925
+ );
926
+ observer.observe(viewport);
927
+ observer.observe(tableElement);
928
+ return () => observer.disconnect();
905
929
  });
906
- /** Pin attributes of a filter or footer cell. */
930
+ /** Pin attributes of a filter, footer or placeholder cell. */
907
931
  function pinned(col: Column<T>) {
908
932
  const pin = pins.get(col.id);
909
933
  return pin ? { 'data-pin': pin.pin, 'data-pin-edge': pin.edge, style: pin.style } : {};
@@ -1269,8 +1293,9 @@
1269
1293
  class="slean-table-skeleton-row"
1270
1294
  aria-hidden="true"
1271
1295
  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
1296
+ >{@render leading()}{#each cols as col (col.id)}<td
1297
+ role={grid ? 'cell' : undefined}
1298
+ {...pinned(col)}><span class="slean-table-skeleton"></span></td
1274
1299
  >{/each}</tr
1275
1300
  >{/each}
1276
1301
  {#if !model.rows.length && !skeleton.length}<tr role={grid ? 'row' : undefined}
@@ -1698,6 +1723,9 @@
1698
1723
  class="slean-table-scroll"
1699
1724
  data-page-sticky={fits || undefined}
1700
1725
  data-head-follows={(stickyHeader && !height && !fits && !scrollsY) || undefined}
1726
+ data-pinned={pinning.edges.length > 0 || undefined}
1727
+ data-overflows={overflows || undefined}
1728
+ style:--slean-table-min-width={tracks?.min}
1701
1729
  style:--slean-table-travel={stickyHeader && !height && !fits && !scrollsY
1702
1730
  ? `${travel}px`
1703
1731
  : undefined}
@@ -1724,7 +1752,6 @@
1724
1752
  class:slean-table-sticky={stickyHeader && (fits || scrollsY)}
1725
1753
  style:table-layout={grid ? undefined : tableLayout}
1726
1754
  style:--slean-table-tracks={tracks?.template}
1727
- style:--slean-table-min-width={tracks?.min}
1728
1755
  >
1729
1756
  {#if caption}<caption>{caption}</caption>{/if}
1730
1757
  {#if !grid}<colgroup
@@ -1870,6 +1897,12 @@
1870
1897
  ></tfoot
1871
1898
  >{/if}
1872
1899
  </table>
1900
+ {#each pinning.edges as [side, inset]}<div
1901
+ class="slean-table-pin-shade"
1902
+ data-pin={side}
1903
+ aria-hidden="true"
1904
+ style:--slean-table-pin-inset={inset}
1905
+ ></div>{/each}
1873
1906
  </div>
1874
1907
  {#if pagination}<div class="slean-table-pagination">
1875
1908
  <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.2",
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",