@zakkster/lite-table 1.2.0 → 1.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.
- package/CHANGELOG.md +12 -0
- package/Table.d.ts +54 -0
- package/Table.js +163 -1
- package/llms.txt +42 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to `@zakkster/lite-table` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] — 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added — View-state persistence seam
|
|
11
|
+
|
|
12
|
+
Two additive `TableCore` methods that snapshot and restore the table's layout and query as plain JSON. This is the seam `@zakkster/lite-headless` `createSavedViews` (G-03) consumes; the named-view manager lives there, not here. Purely additive: the default path and every existing setter/signal/computed are byte-identical.
|
|
13
|
+
|
|
14
|
+
- **`table.getViewState(): ViewState`** — a JSON-serializable snapshot of the layout + query: `v` (schema version, always `1`), `sort`, `columnOrder`, full per-column `columns` layout (`width` / `hidden` / `pin` / `flex` for every current column, not deltas), active `filters` (empty/whitespace queries omitted), `groupBy`, and `collapsedGroups`. No live signal refs, no `Map`/`Set` instances — `filters` is a plain object, `collapsedGroups` an array. Selection/focus/edit state are transient and excluded.
|
|
15
|
+
- **`table.setViewState(view, opts?): void`** — restore a `ViewState` with REPLACE semantics, applied atomically inside a single `batch()` so downstream computeds recompute once. Throws `TypeError` before any mutation on a non-object, a missing `v`, or a `v` other than `1`. Within a v1 view a single malformed entry (bad sort dir, non-number width, unknown key) is skipped and the rest applied. `columnOrder` is reconciled against the live column set — saved keys still present are kept in order, new columns appended, dead keys dropped — so a stale order never trips `setColumnOrder`'s non-permutation guard. Columns absent from `view.columns` keep their current values. `opts` is reserved (no merge mode in v1) and ignored.
|
|
16
|
+
- **`ViewState` / `ViewColumnState` / `SetViewStateOptions`** — exported type declarations.
|
|
17
|
+
|
|
18
|
+
`setViewState(getViewState())` is an identity round-trip. Views are keyed (not positional), so they survive column-set drift. `batch` is now imported from `@zakkster/lite-signal`.
|
|
19
|
+
|
|
8
20
|
## [1.2.0] — 2026-07-03
|
|
9
21
|
|
|
10
22
|
Row grouping, per-column aggregation, sticky group headers, and a sticky grand-total row. All feature additions are opt-in; consumers of 1.1.0 who don't pass `groupBy` are on the identical fast path they had in 1.1.0.
|
package/Table.d.ts
CHANGED
|
@@ -132,6 +132,42 @@ export interface SortEntry {
|
|
|
132
132
|
dir: SortDir;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/** Per-column layout snapshot inside a {@link ViewState}. */
|
|
136
|
+
export interface ViewColumnState {
|
|
137
|
+
width: number;
|
|
138
|
+
hidden: boolean;
|
|
139
|
+
pin: PinSide;
|
|
140
|
+
flex: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A JSON-serializable snapshot of the table's LAYOUT + QUERY -- sort,
|
|
145
|
+
* per-column layout, order, filters, grouping. Produced by
|
|
146
|
+
* `getViewState()` and consumed by `setViewState()`. No live signal refs,
|
|
147
|
+
* no `Map`/`Set` instances: `filters` is a plain object, `collapsedGroups`
|
|
148
|
+
* an array. This is the persistence seam lite-headless `createSavedViews`
|
|
149
|
+
* consumes; selection/focus/edit state are transient and excluded.
|
|
150
|
+
*/
|
|
151
|
+
export interface ViewState {
|
|
152
|
+
/** Schema version. Always `1` in this release. */
|
|
153
|
+
v: 1;
|
|
154
|
+
/** Sort chain, outermost key first. */
|
|
155
|
+
sort: SortEntry[];
|
|
156
|
+
/** Column order by key. */
|
|
157
|
+
columnOrder: string[];
|
|
158
|
+
/** Full per-column layout for every current column, keyed by column key. */
|
|
159
|
+
columns: { [key: string]: ViewColumnState };
|
|
160
|
+
/** Active column filters by key; empty/whitespace queries are omitted. */
|
|
161
|
+
filters: { [key: string]: string };
|
|
162
|
+
/** Group-by keys, outermost first. */
|
|
163
|
+
groupBy: string[];
|
|
164
|
+
/** Collapsed group path-strings (U+001F separator). */
|
|
165
|
+
collapsedGroups: string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Reserved options for {@link TableCore.setViewState}. Ignored in v1. */
|
|
169
|
+
export interface SetViewStateOptions {}
|
|
170
|
+
|
|
135
171
|
export type SelectMode = "set" | "add" | "toggle" | "range";
|
|
136
172
|
|
|
137
173
|
/** Selection state is a PREDICATE, not a list of IDs.
|
|
@@ -438,6 +474,24 @@ export interface TableCore<Row = any> {
|
|
|
438
474
|
// ---- Methods: focus ----
|
|
439
475
|
moveFocus(direction: FocusDirection, opts?: { pageSize?: number }): void;
|
|
440
476
|
|
|
477
|
+
// ---- Methods: view state (persistence seam) ----
|
|
478
|
+
/**
|
|
479
|
+
* Snapshot the full layout + query as a JSON-serializable {@link ViewState}
|
|
480
|
+
* -- deep-copied, no live signal refs, no `Map`/`Set` instances. Persist
|
|
481
|
+
* it, then restore it with {@link TableCore.setViewState}.
|
|
482
|
+
*/
|
|
483
|
+
getViewState(): ViewState;
|
|
484
|
+
/**
|
|
485
|
+
* Restore a {@link ViewState} with REPLACE semantics, applied atomically
|
|
486
|
+
* inside a single batch. Throws `TypeError` before any mutation on a
|
|
487
|
+
* non-object, a missing `v`, or a `v` other than `1`. Within a v1 view a
|
|
488
|
+
* single malformed entry (bad sort dir, non-number width, unknown key) is
|
|
489
|
+
* skipped and the rest applied. Stale column orders are reconciled, so a
|
|
490
|
+
* dropped/added column never rejects the restore. `opts` is reserved and
|
|
491
|
+
* ignored in v1.
|
|
492
|
+
*/
|
|
493
|
+
setViewState(view: ViewState, opts?: SetViewStateOptions): void;
|
|
494
|
+
|
|
441
495
|
// ---- Lifecycle ----
|
|
442
496
|
dispose(): void;
|
|
443
497
|
|
package/Table.js
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
60
|
import {
|
|
61
|
-
signal, computed, effect, untrack,
|
|
61
|
+
signal, computed, effect, untrack, batch,
|
|
62
62
|
dispose as disposeNode
|
|
63
63
|
} from "@zakkster/lite-signal";
|
|
64
64
|
import { virtualAxis } from "@zakkster/lite-virtual";
|
|
@@ -1624,6 +1624,165 @@ export function createTable(config) {
|
|
|
1624
1624
|
return indent > 0 ? JSON.stringify(out, null, indent) : JSON.stringify(out);
|
|
1625
1625
|
}
|
|
1626
1626
|
|
|
1627
|
+
// =========================================================================
|
|
1628
|
+
// --- View state (persistence seam, v1.3.0) -------------------------------
|
|
1629
|
+
// =========================================================================
|
|
1630
|
+
//
|
|
1631
|
+
// getViewState() snapshots the LAYOUT + QUERY (sort, per-column layout,
|
|
1632
|
+
// order, filters, grouping) to plain JSON-safe data -- no live signal
|
|
1633
|
+
// refs, no Map/Set instances. setViewState() restores it with REPLACE
|
|
1634
|
+
// semantics, atomically inside batch(), fail-closed on garbage. This is
|
|
1635
|
+
// the seam lite-headless createSavedViews (G-03) consumes; the named-view
|
|
1636
|
+
// MANAGER lives there, not here. Cold user-gesture path, not a hot path.
|
|
1637
|
+
|
|
1638
|
+
// Reconcile a saved column order against the CURRENT column set: keep saved
|
|
1639
|
+
// keys still present (in saved order), append current keys absent from the
|
|
1640
|
+
// saved order, drop saved keys no longer present. Always returns a
|
|
1641
|
+
// permutation of the current columns, so setColumnOrder's non-permutation
|
|
1642
|
+
// guard never trips on a stale-but-valid order.
|
|
1643
|
+
function _reconcileOrder(savedOrder) {
|
|
1644
|
+
const result = [];
|
|
1645
|
+
const used = new Set();
|
|
1646
|
+
if (Array.isArray(savedOrder)) {
|
|
1647
|
+
for (let i = 0; i < savedOrder.length; i++) {
|
|
1648
|
+
const k = savedOrder[i];
|
|
1649
|
+
if (columnsByKey.has(k) && !used.has(k)) {
|
|
1650
|
+
result.push(k);
|
|
1651
|
+
used.add(k);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
const cur = columnOrder();
|
|
1656
|
+
for (let i = 0; i < cur.length; i++) {
|
|
1657
|
+
const k = cur[i];
|
|
1658
|
+
if (!used.has(k)) {
|
|
1659
|
+
result.push(k);
|
|
1660
|
+
used.add(k);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return result;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function getViewState() {
|
|
1667
|
+
// Full per-column layout for EVERY current column -- restore is
|
|
1668
|
+
// default-independent (never a delta against unknown defaults).
|
|
1669
|
+
const cols = {};
|
|
1670
|
+
for (let i = 0; i < columns.length; i++) {
|
|
1671
|
+
const c = columns[i];
|
|
1672
|
+
cols[c.key] = {
|
|
1673
|
+
width: c.width(),
|
|
1674
|
+
hidden: c.hidden(),
|
|
1675
|
+
pin: c.pin(),
|
|
1676
|
+
flex: c.flex()
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
// Map -> object; skip empty/whitespace queries (they carry no state).
|
|
1680
|
+
const filters = {};
|
|
1681
|
+
for (const [key, q] of columnFilters()) {
|
|
1682
|
+
if (typeof q === "string" && q.trim() !== "") filters[key] = q;
|
|
1683
|
+
}
|
|
1684
|
+
// Copy each sort entry -- no shared refs to the live chain.
|
|
1685
|
+
const sort = [];
|
|
1686
|
+
const chain = sortChain();
|
|
1687
|
+
for (let i = 0; i < chain.length; i++) {
|
|
1688
|
+
sort.push({ key: chain[i].key, dir: chain[i].dir });
|
|
1689
|
+
}
|
|
1690
|
+
// Set -> array of pathStr.
|
|
1691
|
+
const collapsed = [];
|
|
1692
|
+
for (const p of collapsedGroups()) collapsed.push(p);
|
|
1693
|
+
return {
|
|
1694
|
+
v: 1,
|
|
1695
|
+
sort,
|
|
1696
|
+
columnOrder: columnOrder().slice(),
|
|
1697
|
+
columns: cols,
|
|
1698
|
+
filters,
|
|
1699
|
+
groupBy: groupBy().slice(),
|
|
1700
|
+
collapsedGroups: collapsed
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function setViewState(view, opts) {
|
|
1705
|
+
// Whole-view fail-closed BEFORE any mutation: garbage must never brick
|
|
1706
|
+
// the table, and a future major version must not partially apply.
|
|
1707
|
+
// null is not zero.
|
|
1708
|
+
if (view === null || typeof view !== "object" || Array.isArray(view)) {
|
|
1709
|
+
throw new TypeError("setViewState: view must be a plain object");
|
|
1710
|
+
}
|
|
1711
|
+
if (view.v !== 1) {
|
|
1712
|
+
throw new TypeError(
|
|
1713
|
+
"setViewState: unsupported view version " + String(view.v) +
|
|
1714
|
+
" (expected 1)"
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
// `opts` is accepted and reserved for future use (e.g. a merge mode);
|
|
1718
|
+
// ignored in v1. REPLACE semantics only.
|
|
1719
|
+
void opts;
|
|
1720
|
+
// Atomic: one batch so downstream computeds (visibleRows,
|
|
1721
|
+
// visibleColumns, colTemplate, offsets) recompute ONCE, not per field.
|
|
1722
|
+
batch(() => {
|
|
1723
|
+
// 1. Column order -- reconcile first so the guard never trips.
|
|
1724
|
+
setColumnOrder(_reconcileOrder(view.columnOrder));
|
|
1725
|
+
// 2. Per-column layout via the four public setters. Skip malformed
|
|
1726
|
+
// entries; columns absent from view.columns keep current values.
|
|
1727
|
+
const cols = view.columns;
|
|
1728
|
+
if (cols !== null && typeof cols === "object" && !Array.isArray(cols)) {
|
|
1729
|
+
const keys = Object.keys(cols);
|
|
1730
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1731
|
+
const key = keys[i];
|
|
1732
|
+
if (!columnsByKey.has(key)) continue;
|
|
1733
|
+
const c = cols[key];
|
|
1734
|
+
if (c === null || typeof c !== "object") continue;
|
|
1735
|
+
if (typeof c.width === "number" && Number.isFinite(c.width)) {
|
|
1736
|
+
setColumnWidth(key, c.width);
|
|
1737
|
+
}
|
|
1738
|
+
if (typeof c.hidden === "boolean") setColumnHidden(key, c.hidden);
|
|
1739
|
+
if (c.pin === "left" || c.pin === "right" || c.pin === "none") {
|
|
1740
|
+
setColumnPin(key, c.pin);
|
|
1741
|
+
}
|
|
1742
|
+
if (typeof c.flex === "number" && Number.isFinite(c.flex)) {
|
|
1743
|
+
setColumnFlex(key, c.flex);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
// 3. Filters (REPLACE) -- clear, then set each named entry.
|
|
1748
|
+
clearColumnFilters();
|
|
1749
|
+
const filters = view.filters;
|
|
1750
|
+
if (filters !== null && typeof filters === "object" && !Array.isArray(filters)) {
|
|
1751
|
+
const keys = Object.keys(filters);
|
|
1752
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1753
|
+
const key = keys[i];
|
|
1754
|
+
const q = filters[key];
|
|
1755
|
+
if (typeof q === "string") setColumnFilter(key, q);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
// 4. Sort (REPLACE) -- clear, then add each valid entry.
|
|
1759
|
+
clearSort();
|
|
1760
|
+
const sort = view.sort;
|
|
1761
|
+
if (Array.isArray(sort)) {
|
|
1762
|
+
for (let i = 0; i < sort.length; i++) {
|
|
1763
|
+
const e = sort[i];
|
|
1764
|
+
if (e === null || typeof e !== "object") continue;
|
|
1765
|
+
if (e.dir !== "asc" && e.dir !== "desc") continue;
|
|
1766
|
+
addSort(e.key, e.dir);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
// 5. Grouping (REPLACE) -- _normalizeGroupBy drops unknown keys.
|
|
1770
|
+
setGroupBy(view.groupBy);
|
|
1771
|
+
// 6. Collapsed groups (REPLACE) -- expand all, then collapse each
|
|
1772
|
+
// saved path. Saved pathStr strings split back to path arrays;
|
|
1773
|
+
// collapseGroup re-joins them to the identical pathStr.
|
|
1774
|
+
expandAllGroups();
|
|
1775
|
+
const collapsed = view.collapsedGroups;
|
|
1776
|
+
if (Array.isArray(collapsed)) {
|
|
1777
|
+
for (let i = 0; i < collapsed.length; i++) {
|
|
1778
|
+
const p = collapsed[i];
|
|
1779
|
+
if (typeof p === "string") collapseGroup(p.split(GROUP_PATH_SEP));
|
|
1780
|
+
else if (Array.isArray(p)) collapseGroup(p);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1627
1786
|
return {
|
|
1628
1787
|
// Static
|
|
1629
1788
|
columns,
|
|
@@ -1695,6 +1854,9 @@ export function createTable(config) {
|
|
|
1695
1854
|
// Methods: focus
|
|
1696
1855
|
moveFocus,
|
|
1697
1856
|
|
|
1857
|
+
// Methods: view state (persistence seam)
|
|
1858
|
+
getViewState, setViewState,
|
|
1859
|
+
|
|
1698
1860
|
// Lifecycle
|
|
1699
1861
|
dispose,
|
|
1700
1862
|
_scope: scope
|
package/llms.txt
CHANGED
|
@@ -351,6 +351,48 @@ TableCore lifecycle:
|
|
|
351
351
|
returns to the registry pool. 50 createTable+dispose cycles round-trip
|
|
352
352
|
cleanly with activeNodes flat.
|
|
353
353
|
|
|
354
|
+
View state (persistence seam):
|
|
355
|
+
Two TableCore methods (v1.3.0+) snapshot and restore the LAYOUT + QUERY
|
|
356
|
+
as plain JSON. Selection/focus/edit state are transient and excluded.
|
|
357
|
+
This is the seam @zakkster/lite-headless createSavedViews (G-03) consumes;
|
|
358
|
+
the named-view MANAGER lives there, not here. Cold user-gesture path.
|
|
359
|
+
|
|
360
|
+
getViewState() -> ViewState JSON-safe snapshot; no live signal
|
|
361
|
+
refs, no Map/Set. Map -> object,
|
|
362
|
+
Set -> array. Full per-column layout
|
|
363
|
+
for EVERY current column (not deltas)
|
|
364
|
+
so restore is default-independent.
|
|
365
|
+
setViewState(view, opts?) REPLACE semantics, atomic inside one
|
|
366
|
+
batch() (computeds recompute once).
|
|
367
|
+
opts reserved (no merge mode in v1),
|
|
368
|
+
ignored. Throws TypeError BEFORE any
|
|
369
|
+
mutation on a non-object, missing v,
|
|
370
|
+
or v !== 1. Within a v1 view a single
|
|
371
|
+
malformed entry is skipped, the rest
|
|
372
|
+
applied. columnOrder is reconciled
|
|
373
|
+
against the live column set (keep saved
|
|
374
|
+
keys still present, append new columns,
|
|
375
|
+
drop dead keys) so a stale order never
|
|
376
|
+
trips setColumnOrder's guard. Filters
|
|
377
|
+
and sort are cleared then re-applied;
|
|
378
|
+
groupBy replaced (unknown keys dropped);
|
|
379
|
+
collapsed groups expanded then
|
|
380
|
+
re-collapsed per saved path.
|
|
381
|
+
|
|
382
|
+
ViewState shape:
|
|
383
|
+
{
|
|
384
|
+
v: 1, // schema version (integer)
|
|
385
|
+
sort: [{ key, dir }], // outermost key first
|
|
386
|
+
columnOrder: [key, ...],
|
|
387
|
+
columns: { [key]: { width, hidden, pin, flex } }, // full layout
|
|
388
|
+
filters: { [key]: query }, // empty/whitespace omitted
|
|
389
|
+
groupBy: [key, ...],
|
|
390
|
+
collapsedGroups: [pathStr, ...] // U+001F separator
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
Round-trip: setViewState(getViewState()) is identity. Keyed (not
|
|
394
|
+
positional) so a view survives column-set drift.
|
|
395
|
+
|
|
354
396
|
### mountTable(host, table, options?) -> TableMount
|
|
355
397
|
|
|
356
398
|
options: { injectStyles?=true, initialViewportHeight?=480 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-table",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Headless reactive data tables on @zakkster/lite-signal. CSS Grid (no <table>), pooled slots that never reparent, aria-activedescendant focus model. Zero-GC scroll path on @zakkster/lite-virtual. Grouping, aggregation, sticky group headers, and grand totals in v1.2.",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"keywords": [
|