@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 +6 -0
- package/dist/style.css +39 -26
- package/dist/table/data-table.svelte +48 -15
- package/package.json +1 -1
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
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
884
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
904
|
+
if (last === undefined) continue;
|
|
905
|
+
cells.get(last)!.edge = true;
|
|
906
|
+
edges.push([side, at(offset - 1)]);
|
|
903
907
|
}
|
|
904
|
-
return
|
|
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
|
|
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
|
|
1273
|
-
|
|
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"
|