@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 +16 -0
- package/dist/style.css +39 -26
- package/dist/table/data-table.svelte +56 -20
- package/package.json +1 -1
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
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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-
|
|
662
|
-
|
|
663
|
-
|
|
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(
|
|
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
|
-
|
|
669
|
-
|
|
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
|
-
@
|
|
672
|
-
:where(.slean-table-
|
|
673
|
-
opacity:
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
|
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
|
|
884
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
905
|
+
if (last === undefined) continue;
|
|
906
|
+
cells.get(last)!.edge = true;
|
|
907
|
+
edges.push([side, at(offset - 1)]);
|
|
903
908
|
}
|
|
904
|
-
return
|
|
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
|
|
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
|
|
1273
|
-
|
|
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={
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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}
|
|
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"
|