@zakkster/lite-table 1.0.0 → 1.1.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/llms.txt CHANGED
@@ -130,10 +130,107 @@ TableCore methods (focus):
130
130
  rowStart, rowEnd, pageUp, pageDown
131
131
  skips hidden columns; follows current sort order
132
132
 
133
+ TableCore methods (export, M1.1):
134
+ exportCsv(opts?) -> string
135
+ opts.rows: "visible"|"all"|"selected"|Array (default "visible")
136
+ opts.columns: "visible"|"all"|Array<key> (default "visible")
137
+ opts.delimiter: string (default ",")
138
+ opts.quote: string (default '"')
139
+ opts.headers: boolean (default true)
140
+ opts.newline: string (default "\r\n")
141
+ opts.bom: boolean (default false; UTF-8 BOM for Excel-on-Windows)
142
+ opts.formatter: (row, col) => unknown
143
+ RFC 4180 escaping; column accessor honored; numbers stringify naturally;
144
+ null/undefined become empty fields.
145
+
146
+ exportJson(opts?) -> string | object[]
147
+ opts.rows: same as exportCsv
148
+ opts.columns: same as exportCsv
149
+ opts.indent: number (default 0 = compact single-line)
150
+ opts.format: "string"|"array" (default "string"; "array" skips JSON.stringify)
151
+ opts.formatter: (row, col) => unknown
152
+ Fast path: columns:"all"+no formatter returns rows.slice() (shallow).
153
+
154
+ Paginated-getter pitfall: rows:"all" and rows:"selected" resolve against
155
+ rowsGetter(), which for a function-form rows IS the current page. To
156
+ export across the master, pass the master array:
157
+ table.exportCsv({rows: allRows})
158
+ table.exportCsv({rows: table.selectedRows(allRows)})
159
+
160
+ TableCore filtering (M2):
161
+ Column opt-in: { key, filterable: true,
162
+ filter?: (value, query, row) => boolean,
163
+ filterPlaceholder?: string }
164
+
165
+ Default predicate: case-insensitive substring on stringified value.
166
+ Empty / whitespace-only query: filter is treated as inactive (predicate
167
+ not invoked).
168
+
169
+ Reactive surface:
170
+ columnFilters() -> ReadonlyMap<columnKey, queryString>
171
+ filteredRows() -> Computed rows post-filter, pre-sort
172
+ visibleRows() -> Computed rows post-filter AND post-sort
173
+
174
+ Methods:
175
+ setColumnFilter(key, value) value=null/""/whitespace clears that column
176
+ clearColumnFilters() no-op + no notify if already empty
177
+
178
+ Pipeline: rowsGetter -> filteredRows -> visibleRows -> exports/mount
179
+ Multiple active filters apply as AND.
180
+
181
+ DOM (when mounted): a .lt-filter-row is mounted between header + viewport
182
+ if any column is filterable. One <input> per filterable column, two-way
183
+ bound. Escape on input clears that column's filter. Sticky top.
184
+
185
+ TableCore editing (M2):
186
+ Column opt-in: { key, editable: true }
187
+ Config: createTable({ ...,
188
+ onCellEdit: ({row, columnKey, oldValue, newValue}) => ... })
189
+
190
+ lite-table does NOT mutate rows. onCellEdit is the consumer's write hook.
191
+
192
+ Reactive surface:
193
+ editingCell() -> { rowId, columnKey } | null
194
+ editingDraft() -> current in-progress string
195
+
196
+ Methods:
197
+ startEdit(rowId, columnKey) no-op on non-editable; auto-commits
198
+ in-flight on a different cell;
199
+ idempotent on the same cell.
200
+ commitEdit() reads editingDraft
201
+ commitEdit("explicit") commits a specific value
202
+ cancelEdit() discard; no onCellEdit
203
+ isEditing(rowId, columnKey) O(1) predicate
204
+
205
+ Commit-skip: onCellEdit is NOT called when the value is unchanged.
206
+ DOM edits produce string newValue; guard compares
207
+ String(oldValue) !== newValue (so 100 vs "100" is unchanged).
208
+ Explicit commitEdit(explicitValue) falls back to strict ===.
209
+ newValue is ALWAYS a string from the DOM path -- consumers
210
+ coerce in their handler (Number(newValue) / new Date(newValue) /
211
+ newValue === "true" / etc.).
212
+ Handler errors are caught + logged; subsequent edits work.
213
+
214
+ DOM keyboard (when mounted):
215
+ dblclick on editable cell -> startEdit
216
+ F2 / Enter on focused cell with editable column -> startEdit
217
+ Enter -> commit + moveFocus down
218
+ Tab -> commit + moveFocus right (Shift+Tab = left)
219
+ Escape -> cancel + return focus to root
220
+ blur -> commit
221
+
222
+ Edit state survives scroll: keyed on rowId, so contenteditable rebinds
223
+ when the row scrolls back into view. Draft preserved across scroll.
224
+
133
225
  TableCore lifecycle:
134
226
  dispose() releases all reactive nodes
135
227
  cellId(rowId, columnKey) -> "lt_<rowId>__<columnKey>"
136
228
 
229
+ Note: v1.1.0 fixed a latent leak in dispose() (handles were called,
230
+ not freed). After dispose() every reactive node the table allocated
231
+ returns to the registry pool. 50 createTable+dispose cycles round-trip
232
+ cleanly with activeNodes flat.
233
+
137
234
  ### mountTable(host, table, options?) -> TableMount
138
235
 
139
236
  options: { injectStyles?=true, initialViewportHeight?=480 }
@@ -163,15 +260,28 @@ Header: <div class="lt-header" role="row" aria-rowindex="1">
163
260
  <span class="lt-header-resize"></span> <!-- if resizable -->
164
261
  </div> ...
165
262
  </div>
263
+ Filters (M2; mounted only when any column has filterable: true):
264
+ <div class="lt-filter-row" role="row" aria-rowindex="2">
265
+ <div class="lt-filter-cell" data-key data-pin
266
+ style="grid-column; left|right (if pinned)">
267
+ <input class="lt-filter-input"
268
+ aria-label="Filter <header>"
269
+ placeholder="Filter…">
270
+ <!-- empty cells for non-filterable columns -->
271
+ </div> ...
272
+ </div>
166
273
  Body: <div class="lt-viewport"><div class="lt-inner">
167
274
  <div class="lt-row [lt-row-alt] [is-selected]"
168
275
  role="row"
169
276
  aria-rowindex aria-selected
170
277
  style="transform: translateY(<i*rowHeight>px)">
171
- <div class="lt-cell" role="gridcell"
278
+ <div class="lt-cell [is-focused] [is-editing]"
279
+ role="gridcell"
172
280
  id="lt_<rowId>__<columnKey>"
173
281
  aria-colindex
174
282
  data-pin
283
+ [data-editable="true"]
284
+ [contenteditable="true"] <!-- only the active edit cell -->
175
285
  style="grid-column; left|right (if pinned)">text</div>
176
286
  ...
177
287
  </div> ...
@@ -185,13 +295,22 @@ Ctrl+Home/End grid corners (0,0) / (last,last)
185
295
  PageUp/PageDown +/- floor(viewportHeight / rowHeight)
186
296
  Space selectRow(focused.rowId, "set")
187
297
  Ctrl+A selectAll()
188
- Escape clearSelection()
298
+ Escape clearSelection() (when not editing)
299
+ F2 / Enter (on editable) startEdit(focused) (M2)
300
+ Dblclick on editable cell startEdit (M2)
189
301
  Click cell selectRow(set) + focus
190
302
  Shift+click selectRow(range)
191
303
  Ctrl/Cmd+click selectRow(toggle)
192
304
  Drag header reorder
193
305
  Drag right edge of header resize
194
306
  Click header toggleSort
307
+ (while editing):
308
+ Enter commitEdit + moveFocus("down")
309
+ Tab / Shift+Tab commitEdit + moveFocus("right"/"left")
310
+ Escape cancelEdit + return focus to root
311
+ blur commitEdit
312
+ (on a filter input):
313
+ Escape setColumnFilter(key, "")
195
314
  Shift+click header toggleSort additive
196
315
 
197
316
  ## Performance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-table",
3
- "version": "1.0.0",
3
+ "version": "1.1.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.",
5
5
  "sideEffects": false,
6
6
  "keywords": [
@@ -24,8 +24,10 @@
24
24
  "types": "./Table.d.ts",
25
25
  "exports": {
26
26
  ".": {
27
+ "node": "./Table.js",
28
+ "import": "./Table.js",
27
29
  "types": "./Table.d.ts",
28
- "import": "./Table.js"
30
+ "default": "./Table.js"
29
31
  }
30
32
  },
31
33
  "files": [
@@ -42,24 +44,26 @@
42
44
  "bench": "node --expose-gc --max-old-space-size=4096 bench/run.js"
43
45
  },
44
46
  "peerDependencies": {
45
- "@zakkster/lite-signal": "^1.2.0",
47
+ "@zakkster/lite-signal": "^1.2.1",
46
48
  "@zakkster/lite-signal-dom": "^1.0.1",
47
49
  "@zakkster/lite-virtual": "^1.1.0"
48
50
  },
49
51
  "devDependencies": {
52
+ "@playwright/test": "^1.60.0",
50
53
  "clusterize.js": "^1.0.0",
51
- "happy-dom": "^15.11.7"
54
+ "happy-dom": "^15.11.7",
55
+ "playwright": "^1.60.0"
52
56
  },
53
57
  "engines": {
54
58
  "node": ">=20"
55
59
  },
56
- "homepage": "https://github.com/PeshoVurtoleta/lite-signal#readme",
60
+ "homepage": "https://github.com/PeshoVurtoleta/lite-table#readme",
57
61
  "repository": {
58
62
  "type": "git",
59
- "url": "git+https://github.com/PeshoVurtoleta/lite-signal.git"
63
+ "url": "git+https://github.com/PeshoVurtoleta/lite-table.git"
60
64
  },
61
65
  "bugs": {
62
- "url": "https://github.com/PeshoVurtoleta/lite-signal/issues",
66
+ "url": "https://github.com/PeshoVurtoleta/lite-table/issues",
63
67
  "email": "shinikchiev@yahoo.com"
64
68
  },
65
69
  "funding": {