@zakkster/lite-table 1.1.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.
Files changed (6) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +223 -18
  3. package/Table.d.ts +178 -0
  4. package/Table.js +1136 -148
  5. package/llms.txt +212 -4
  6. package/package.json +5 -3
package/Table.d.ts CHANGED
@@ -47,12 +47,27 @@ export interface ColumnDef<Row = any, Value = unknown> {
47
47
  filter?: (value: Value, query: string, row: Row) => boolean;
48
48
  /** M2: placeholder text for the filter input. Default "Filter…". */
49
49
  filterPlaceholder?: string;
50
+ /** M3: aggregate reducer for group-header + grand-total cells. One of the
51
+ * built-in strings, or a custom `(rows, col) => value` function. `null`
52
+ * or omitted means "no aggregate" -- the column shows blank in header
53
+ * rows. Aggregates always fold over LEAF rows at every depth (safe
54
+ * for non-associative reducers like median or last-value-wins). */
55
+ aggregate?: AggregateSpec<Row> | null;
56
+ /** M3: format the aggregate for display. Only affects rendered text --
57
+ * `entry.aggregates.get(key)` still returns the raw value so consumers
58
+ * can format independently for export etc. Errors are caught + logged. */
59
+ aggregateFormat?: (value: unknown, col: ColumnState<Row>, count: number) => string;
50
60
  /** Computed value override; if absent, `row[key]` is used. */
51
61
  accessor?: (row: Row) => Value;
52
62
  /** Sort comparator; default is null-safe number-or-string compare. */
53
63
  compare?: (a: Value, b: Value) => number;
54
64
  }
55
65
 
66
+ /** M3: aggregate spec on a column. */
67
+ export type AggregateSpec<Row = any> =
68
+ | "sum" | "avg" | "min" | "max" | "count"
69
+ | ((rows: readonly Row[], col: ColumnState<Row>) => unknown);
70
+
56
71
  /** Live reactive state for a column, exposed on `table.columns[i]`. */
57
72
  export interface ColumnState<Row = any> {
58
73
  readonly key: string;
@@ -117,6 +132,42 @@ export interface SortEntry {
117
132
  dir: SortDir;
118
133
  }
119
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
+
120
171
  export type SelectMode = "set" | "add" | "toggle" | "range";
121
172
 
122
173
  /** Selection state is a PREDICATE, not a list of IDs.
@@ -152,6 +203,69 @@ export interface CreateTableConfig<Row = any> {
152
203
  * optimistic update, etc.). Skipped when newValue === oldValue.
153
204
  * Errors thrown by the handler are caught + logged. */
154
205
  onCellEdit?: (payload: CellEditPayload<Row>) => void | Promise<unknown>;
206
+
207
+ /** M3: group rows by one or more column keys. `null` (default) or an
208
+ * empty array = no grouping. A bare string is normalized to `[string]`.
209
+ * Unknown column keys are silently dropped so persisted state that
210
+ * outlives a column config survives without crashes. */
211
+ groupBy?: string | readonly string[] | null;
212
+ /** M3: pre-seed the collapsed set. Each entry is a group path -- e.g.
213
+ * `[["Europe"], ["Asia", "Books"]]`. Unknown paths become no-ops when
214
+ * their groups don't exist. */
215
+ initialCollapsedGroups?: readonly (readonly string[])[];
216
+ /** M3: append a grand-total entry at the tail of `visibleEntries` when
217
+ * any column has an `aggregate` spec. Grand-total aggregates fold
218
+ * over `filteredRows()` and are stable across collapse operations. */
219
+ showGrandTotal?: boolean;
220
+ }
221
+
222
+ // --- M3: grouping + aggregation types ---------------------------------------
223
+
224
+ /** One node in the grouped-rows tree. Leaf nodes have `rows` populated and
225
+ * `subGroups: null`; internal nodes have `subGroups` populated and
226
+ * `rows: null`. Aggregates always fold over leaf rows recursively. */
227
+ export interface GroupNode<Row = any> {
228
+ readonly depth: number;
229
+ /** The column key this depth groups on. */
230
+ readonly key: string;
231
+ /** The group's identifying value (post-`accessor`). `null` for the
232
+ * null / undefined bucket. */
233
+ readonly value: unknown;
234
+ /** Path of ancestor values including this node. e.g. `["Europe", "Books"]`. */
235
+ readonly path: readonly string[];
236
+ /** Stringified path (U+001F separator) -- the storage key for the
237
+ * collapsed-groups Set. */
238
+ readonly pathStr: string;
239
+ /** Recursive leaf-row count. */
240
+ readonly count: number;
241
+ readonly aggregates: ReadonlyMap<string, unknown>;
242
+ readonly subGroups: readonly GroupNode<Row>[] | null;
243
+ readonly rows: readonly Row[] | null;
244
+ }
245
+
246
+ /** A row in `visibleEntries`. The mount layer dispatches per-slot rendering
247
+ * on `type`. */
248
+ export type Entry<Row = any> = DataEntry<Row> | GroupHeaderEntry | GrandTotalEntry;
249
+
250
+ export interface DataEntry<Row = any> {
251
+ readonly type: "data";
252
+ readonly row: Row;
253
+ }
254
+ export interface GroupHeaderEntry {
255
+ readonly type: "group-header";
256
+ readonly depth: number;
257
+ readonly key: string;
258
+ readonly value: unknown;
259
+ readonly path: readonly string[];
260
+ readonly pathStr: string;
261
+ readonly count: number;
262
+ readonly aggregates: ReadonlyMap<string, unknown>;
263
+ readonly isCollapsed: boolean;
264
+ }
265
+ export interface GrandTotalEntry {
266
+ readonly type: "grand-total";
267
+ readonly aggregates: ReadonlyMap<string, unknown>;
268
+ readonly count: number;
155
269
  }
156
270
 
157
271
  // --- Export option types ----------------------------------------------------
@@ -215,6 +329,28 @@ export interface TableCore<Row = any> {
215
329
  readonly visibleRows: Computed<readonly Row[]>;
216
330
  readonly rowCount: Computed<number>;
217
331
 
332
+ // ---- Reactive: grouping + aggregation (M3) ----
333
+ /** Current grouping keys. Empty array = no grouping (fast path).
334
+ * Assign via `setGroupBy`; the raw signal is exposed for read-only
335
+ * access + persistence with `@zakkster/lite-persist`. */
336
+ readonly groupBy: Signal<readonly string[]>;
337
+ /** Set of collapsed group path-strings (U+001F separator). Mutated via
338
+ * `toggleGroup`, `collapseGroup`, `expandGroup`, `collapseAllGroups`,
339
+ * `expandAllGroups`. `isGroupCollapsed(path)` is the O(1) predicate. */
340
+ readonly collapsedGroups: Signal<ReadonlySet<string>>;
341
+ /** Grouped tree of `filteredRows`, sorted within leaves per `sortChain`.
342
+ * `null` when `groupBy` is empty -- consumers can branch on this to
343
+ * render a flat list without walking the tree. */
344
+ readonly groupedRows: Computed<readonly GroupNode<Row>[] | null>;
345
+ /** Flat interleaved list of `{type: "data" | "group-header" | "grand-total"}`
346
+ * entries. Drives the mount layer's slot rendering. Length matches
347
+ * `entryCount()`. */
348
+ readonly visibleEntries: Computed<readonly Entry<Row>[]>;
349
+ /** Total entry count including group headers + grand total. Drives the
350
+ * virtual axis in the mount layer -- `rowCount` (data-only) is kept
351
+ * as a separate consumer stat. */
352
+ readonly entryCount: Computed<number>;
353
+
218
354
  // ---- Reactive: columns ----
219
355
  readonly columnOrder: Signal<readonly string[]>;
220
356
  readonly visibleColumns: Computed<readonly ColumnState<Row>[]>;
@@ -293,6 +429,30 @@ export interface TableCore<Row = any> {
293
429
  /** Clear all filters. No-op + no notify if the filter map is already empty. */
294
430
  clearColumnFilters(): void;
295
431
 
432
+ // ---- Methods: grouping (M3) ----
433
+ /** Set the grouping keys. Accepts a bare string, a string[], or
434
+ * null/empty (no grouping). Unknown column keys are silently dropped.
435
+ * Idempotent -- setting the same effective value is a no-op that
436
+ * doesn't churn the signal. */
437
+ setGroupBy(v: string | readonly string[] | null): void;
438
+ /** Flip the collapsed state for the given group path. */
439
+ toggleGroup(path: readonly string[]): void;
440
+ /** Expand a specific group (no-op if already expanded). */
441
+ expandGroup(path: readonly string[]): void;
442
+ /** Collapse a specific group (no-op if already collapsed). */
443
+ collapseGroup(path: readonly string[]): void;
444
+ /** Expand every group -- clears the collapsed set. */
445
+ expandAllGroups(): void;
446
+ /** Collapse every group in the current tree. Walks `groupedRows()` once. */
447
+ collapseAllGroups(): void;
448
+ /** O(1) predicate: is this exact group path currently collapsed? */
449
+ isGroupCollapsed(path: readonly string[]): boolean;
450
+ /** Given an entry index (typically `axis.start()`), return the group
451
+ * headers that CONTAIN it -- one per depth level, deepest last.
452
+ * Used by consumers implementing sticky group headers. Returns [] for
453
+ * the ungrouped fast path or when the target is above the first header. */
454
+ groupAncestryAt(entryIndex: number): readonly GroupHeaderEntry[];
455
+
296
456
  // ---- Methods: editing (M2) ----
297
457
  /** Start editing the (rowId, columnKey) cell. No-op on non-editable
298
458
  * columns. Auto-commits any in-flight edit of a different cell. Idempotent
@@ -314,6 +474,24 @@ export interface TableCore<Row = any> {
314
474
  // ---- Methods: focus ----
315
475
  moveFocus(direction: FocusDirection, opts?: { pageSize?: number }): void;
316
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
+
317
495
  // ---- Lifecycle ----
318
496
  dispose(): void;
319
497