@svelte-lean/table 0.1.1 → 0.3.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.
@@ -13,11 +13,29 @@ export function horizontalCell(cell, delta) {
13
13
  const cells = dataCells(row);
14
14
  return cells[cells.indexOf(cell) + delta];
15
15
  }
16
+ /**
17
+ * The row after or before `row` in the table body, crossing from one `<tbody>` to the next (grid
18
+ * layout renders the body in sections).
19
+ */
20
+ function adjacentRow(row, delta) {
21
+ const sibling = delta > 0 ? 'nextElementSibling' : 'previousElementSibling';
22
+ const next = row[sibling];
23
+ if (next)
24
+ return next;
25
+ for (let section = row.parentElement?.[sibling]; section; section = section[sibling]) {
26
+ if (section.tagName !== 'TBODY')
27
+ continue;
28
+ const edge = delta > 0 ? section.firstElementChild : section.lastElementChild;
29
+ if (edge)
30
+ return edge;
31
+ }
32
+ return null;
33
+ }
16
34
  /** The nearest cell with the same `data-column` in a following or preceding row. */
17
35
  export function verticalCell(cell, delta) {
18
36
  const column = cell.dataset.column;
19
- const sibling = delta > 0 ? 'nextElementSibling' : 'previousElementSibling';
20
- for (let row = cell.parentElement?.[sibling]; row; row = row[sibling]) {
37
+ const start = cell.parentElement;
38
+ for (let row = start && adjacentRow(start, delta); row; row = adjacentRow(row, delta)) {
21
39
  const target = dataCells(row).find((candidate) => candidate.dataset.column === column);
22
40
  if (target)
23
41
  return target;
@@ -30,8 +30,41 @@ export function createTable(options) {
30
30
  searchText: options.searchText,
31
31
  locale: resolve(options.locale)
32
32
  };
33
- return (options.getRowModel ?? createRowModel)(context);
33
+ return stabilize((options.getRowModel ?? createRowModel)(context));
34
34
  });
35
+ /**
36
+ * Row node identity across models. Every state change builds a new model; a node whose row,
37
+ * depth and children are unchanged is handed back as the same object, so a renderer keyed by
38
+ * row id sees nothing to redraw for it after a sort, a selection or an expansion. Only
39
+ * bookkeeping: the previous nodes by id, replaced by each model.
40
+ */
41
+ let previous = new Map();
42
+ function same(a, b) {
43
+ return (a.kind === 'row' &&
44
+ a.data === b.data &&
45
+ a.depth === b.depth &&
46
+ a.children.length === b.children.length &&
47
+ a.children.every((child, i) => child === b.children[i]));
48
+ }
49
+ function stabilize(next) {
50
+ const kept = new Map();
51
+ const reuse = (node) => {
52
+ const known = kept.get(node.id);
53
+ if (known)
54
+ return known;
55
+ // Children first: a parent is the same only when its children are the same objects.
56
+ const children = node.children.length ? node.children.map(reuse) : node.children;
57
+ const candidate = children === node.children ? node : { ...node, children };
58
+ const old = previous.get(node.id);
59
+ const result = old && same(old, candidate) ? old : candidate;
60
+ kept.set(node.id, result);
61
+ return result;
62
+ };
63
+ const rows = next.rows.map(reuse);
64
+ const all = next.all.map(reuse);
65
+ previous = kept;
66
+ return { ...next, rows, all };
67
+ }
35
68
  const selected = $derived(new Set(state.selection));
36
69
  const expanded = $derived(new Set(state.expanded));
37
70
  const mode = $derived(resolve(options.selection) ?? 'multiple');
package/dist/types.d.ts CHANGED
@@ -236,6 +236,15 @@ export interface TableProps<T> {
236
236
  title?: Snippet<[RowModel<T>]>;
237
237
  showHeader?: boolean;
238
238
  tableLayout?: 'auto' | 'fixed';
239
+ /**
240
+ * `table` (default): native table layout. `grid`: the same elements laid out as CSS grid rows
241
+ * with explicit ARIA roles; body rows stand in sections the browser skips while off screen
242
+ * (`content-visibility: auto`), and column widths are measured from the content once and kept.
243
+ * For long pages (hundreds of rows) where layout and paint of every row would dominate. In grid
244
+ * layout `width` is a column's minimum, `maxWidth` caps a measured width (default 480), `span`
245
+ * is not applied, and `tableLayout` has no effect.
246
+ */
247
+ layout?: 'table' | 'grid';
239
248
  searchable?: boolean;
240
249
  /** Milliseconds between the last keystroke in the search box or a filter input and the state change. */
241
250
  searchDelay?: number;
@@ -273,20 +282,31 @@ export interface TableProps<T> {
273
282
  /**
274
283
  * Keeps the header row in view. With `height` it sticks inside the scroll region; without, it
275
284
  * sticks to the page while the table fits its width (`--slean-table-sticky-top` offsets it below
276
- * a fixed application bar) and inside the region once the table scrolls sideways.
285
+ * a fixed application bar). A table wider than its container scrolls sideways in the region,
286
+ * which captures sticky positioning: the header then scrolls with the rows, unless the region
287
+ * also scrolls down (a `height`, or a `max-height` from CSS).
277
288
  */
278
289
  stickyHeader?: boolean;
279
290
  height?: string;
280
291
  /**
281
- * Progressive mounting for large pages. When at least this many new rows arrive after the table
282
- * has mounted (a fetch, a page change), the first batch renders at once and the rest follow in
283
- * slices of about one frame each, so the main thread never blocks on the whole set. `true` means
284
- * 100. The first render (server output, hydration) is always complete. Default `false`.
292
+ * Progressive mounting for large pages. When at least a first batch of rows the table has not
293
+ * drawn arrives (the first render in the browser, a fetch, a page change), that batch renders at
294
+ * once and the rest follow in slices sized from the measured cost of a row, one macrotask apart,
295
+ * so the main thread never blocks on the whole set. `true` makes the first batch one screenful; a
296
+ * number sets it. Server output and its hydration are always complete. Default `false`.
285
297
  */
286
298
  progressive?: boolean | number;
287
299
  pagination?: boolean;
288
300
  /** Page sizes offered by the pager; the current size is always included. */
289
301
  pageSizeOptions?: number[];
302
+ /**
303
+ * The page-size control. `native` (default): a native `<select>`. `custom`: the Select primitive
304
+ * of `@svelte-lean/primitives` (a trigger button and a listbox popup drawn the same in every
305
+ * engine), around the same native `<select>`, which keeps the value and works without scripts.
306
+ * `custom` needs the select behavior (`import '@svelte-lean/primitives/select/register'`) and
307
+ * its styles (`@svelte-lean/styles/select.css`) in the application.
308
+ */
309
+ pageSizeSelect?: 'native' | 'custom';
290
310
  /** Text of an empty value (`null`, `undefined`, `''`, or `format` returning `null`/`''`). */
291
311
  placeholder?: string;
292
312
  rowHoverable?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-lean/table",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
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",
@@ -58,7 +58,7 @@
58
58
  "access": "public"
59
59
  },
60
60
  "peerDependencies": {
61
- "svelte": "^5.0.0"
61
+ "svelte": "^5.20.0"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "yarn run -T svelte-kit sync && yarn run -T svelte-package && node scripts/types.mjs && yarn run -T publint && yarn run -T attw --pack . --profile esm-only --exclude-entrypoints ./style.css --format table-flipped --no-emoji",
@@ -89,5 +89,8 @@
89
89
  "pagination",
90
90
  "typescript",
91
91
  "headless"
92
- ]
92
+ ],
93
+ "dependencies": {
94
+ "esm-env": "^1.2.2"
95
+ }
93
96
  }