@zakkster/lite-table 1.1.0 → 1.2.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/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;
@@ -152,6 +167,69 @@ export interface CreateTableConfig<Row = any> {
152
167
  * optimistic update, etc.). Skipped when newValue === oldValue.
153
168
  * Errors thrown by the handler are caught + logged. */
154
169
  onCellEdit?: (payload: CellEditPayload<Row>) => void | Promise<unknown>;
170
+
171
+ /** M3: group rows by one or more column keys. `null` (default) or an
172
+ * empty array = no grouping. A bare string is normalized to `[string]`.
173
+ * Unknown column keys are silently dropped so persisted state that
174
+ * outlives a column config survives without crashes. */
175
+ groupBy?: string | readonly string[] | null;
176
+ /** M3: pre-seed the collapsed set. Each entry is a group path -- e.g.
177
+ * `[["Europe"], ["Asia", "Books"]]`. Unknown paths become no-ops when
178
+ * their groups don't exist. */
179
+ initialCollapsedGroups?: readonly (readonly string[])[];
180
+ /** M3: append a grand-total entry at the tail of `visibleEntries` when
181
+ * any column has an `aggregate` spec. Grand-total aggregates fold
182
+ * over `filteredRows()` and are stable across collapse operations. */
183
+ showGrandTotal?: boolean;
184
+ }
185
+
186
+ // --- M3: grouping + aggregation types ---------------------------------------
187
+
188
+ /** One node in the grouped-rows tree. Leaf nodes have `rows` populated and
189
+ * `subGroups: null`; internal nodes have `subGroups` populated and
190
+ * `rows: null`. Aggregates always fold over leaf rows recursively. */
191
+ export interface GroupNode<Row = any> {
192
+ readonly depth: number;
193
+ /** The column key this depth groups on. */
194
+ readonly key: string;
195
+ /** The group's identifying value (post-`accessor`). `null` for the
196
+ * null / undefined bucket. */
197
+ readonly value: unknown;
198
+ /** Path of ancestor values including this node. e.g. `["Europe", "Books"]`. */
199
+ readonly path: readonly string[];
200
+ /** Stringified path (U+001F separator) -- the storage key for the
201
+ * collapsed-groups Set. */
202
+ readonly pathStr: string;
203
+ /** Recursive leaf-row count. */
204
+ readonly count: number;
205
+ readonly aggregates: ReadonlyMap<string, unknown>;
206
+ readonly subGroups: readonly GroupNode<Row>[] | null;
207
+ readonly rows: readonly Row[] | null;
208
+ }
209
+
210
+ /** A row in `visibleEntries`. The mount layer dispatches per-slot rendering
211
+ * on `type`. */
212
+ export type Entry<Row = any> = DataEntry<Row> | GroupHeaderEntry | GrandTotalEntry;
213
+
214
+ export interface DataEntry<Row = any> {
215
+ readonly type: "data";
216
+ readonly row: Row;
217
+ }
218
+ export interface GroupHeaderEntry {
219
+ readonly type: "group-header";
220
+ readonly depth: number;
221
+ readonly key: string;
222
+ readonly value: unknown;
223
+ readonly path: readonly string[];
224
+ readonly pathStr: string;
225
+ readonly count: number;
226
+ readonly aggregates: ReadonlyMap<string, unknown>;
227
+ readonly isCollapsed: boolean;
228
+ }
229
+ export interface GrandTotalEntry {
230
+ readonly type: "grand-total";
231
+ readonly aggregates: ReadonlyMap<string, unknown>;
232
+ readonly count: number;
155
233
  }
156
234
 
157
235
  // --- Export option types ----------------------------------------------------
@@ -215,6 +293,28 @@ export interface TableCore<Row = any> {
215
293
  readonly visibleRows: Computed<readonly Row[]>;
216
294
  readonly rowCount: Computed<number>;
217
295
 
296
+ // ---- Reactive: grouping + aggregation (M3) ----
297
+ /** Current grouping keys. Empty array = no grouping (fast path).
298
+ * Assign via `setGroupBy`; the raw signal is exposed for read-only
299
+ * access + persistence with `@zakkster/lite-persist`. */
300
+ readonly groupBy: Signal<readonly string[]>;
301
+ /** Set of collapsed group path-strings (U+001F separator). Mutated via
302
+ * `toggleGroup`, `collapseGroup`, `expandGroup`, `collapseAllGroups`,
303
+ * `expandAllGroups`. `isGroupCollapsed(path)` is the O(1) predicate. */
304
+ readonly collapsedGroups: Signal<ReadonlySet<string>>;
305
+ /** Grouped tree of `filteredRows`, sorted within leaves per `sortChain`.
306
+ * `null` when `groupBy` is empty -- consumers can branch on this to
307
+ * render a flat list without walking the tree. */
308
+ readonly groupedRows: Computed<readonly GroupNode<Row>[] | null>;
309
+ /** Flat interleaved list of `{type: "data" | "group-header" | "grand-total"}`
310
+ * entries. Drives the mount layer's slot rendering. Length matches
311
+ * `entryCount()`. */
312
+ readonly visibleEntries: Computed<readonly Entry<Row>[]>;
313
+ /** Total entry count including group headers + grand total. Drives the
314
+ * virtual axis in the mount layer -- `rowCount` (data-only) is kept
315
+ * as a separate consumer stat. */
316
+ readonly entryCount: Computed<number>;
317
+
218
318
  // ---- Reactive: columns ----
219
319
  readonly columnOrder: Signal<readonly string[]>;
220
320
  readonly visibleColumns: Computed<readonly ColumnState<Row>[]>;
@@ -293,6 +393,30 @@ export interface TableCore<Row = any> {
293
393
  /** Clear all filters. No-op + no notify if the filter map is already empty. */
294
394
  clearColumnFilters(): void;
295
395
 
396
+ // ---- Methods: grouping (M3) ----
397
+ /** Set the grouping keys. Accepts a bare string, a string[], or
398
+ * null/empty (no grouping). Unknown column keys are silently dropped.
399
+ * Idempotent -- setting the same effective value is a no-op that
400
+ * doesn't churn the signal. */
401
+ setGroupBy(v: string | readonly string[] | null): void;
402
+ /** Flip the collapsed state for the given group path. */
403
+ toggleGroup(path: readonly string[]): void;
404
+ /** Expand a specific group (no-op if already expanded). */
405
+ expandGroup(path: readonly string[]): void;
406
+ /** Collapse a specific group (no-op if already collapsed). */
407
+ collapseGroup(path: readonly string[]): void;
408
+ /** Expand every group -- clears the collapsed set. */
409
+ expandAllGroups(): void;
410
+ /** Collapse every group in the current tree. Walks `groupedRows()` once. */
411
+ collapseAllGroups(): void;
412
+ /** O(1) predicate: is this exact group path currently collapsed? */
413
+ isGroupCollapsed(path: readonly string[]): boolean;
414
+ /** Given an entry index (typically `axis.start()`), return the group
415
+ * headers that CONTAIN it -- one per depth level, deepest last.
416
+ * Used by consumers implementing sticky group headers. Returns [] for
417
+ * the ungrouped fast path or when the target is above the first header. */
418
+ groupAncestryAt(entryIndex: number): readonly GroupHeaderEntry[];
419
+
296
420
  // ---- Methods: editing (M2) ----
297
421
  /** Start editing the (rowId, columnKey) cell. No-op on non-editable
298
422
  * columns. Auto-commits any in-flight edit of a different cell. Idempotent