ajo-ui 0.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.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
@@ -0,0 +1,760 @@
1
+ import {
2
+ columnFilteringFeature,
3
+ columnVisibilityFeature,
4
+ constructTable,
5
+ createFilteredRowModel,
6
+ createPaginatedRowModel,
7
+ createSortedRowModel,
8
+ globalFilteringFeature,
9
+ rowPaginationFeature,
10
+ rowSelectionFeature,
11
+ rowSortingFeature,
12
+ sortFn_alphanumeric,
13
+ tableFeatures,
14
+ type ColumnDef,
15
+ type RowSelectionState,
16
+ type SortingState,
17
+ } from '@tanstack/table-core'
18
+ import type { TableReactivityBindings } from '@tanstack/table-core/reactivity'
19
+ import { storeReactivityBindings } from '@tanstack/table-core/store-reactivity-bindings'
20
+ import type { Children } from 'ajo'
21
+ import type { Host } from 'ajo-cloves'
22
+ import type {
23
+ DataTableArgs,
24
+ DataTableColumn,
25
+ DataTableData,
26
+ DataTableKey,
27
+ DataTableLabels,
28
+ DataTablePagination,
29
+ } from './data-table-contract'
30
+
31
+ export const dataTableStrategy = tableFeatures({
32
+ columnFilteringFeature,
33
+ columnVisibilityFeature,
34
+ globalFilteringFeature,
35
+ rowPaginationFeature,
36
+ rowSelectionFeature,
37
+ rowSortingFeature,
38
+ filteredRowModel: createFilteredRowModel(),
39
+ paginatedRowModel: createPaginatedRowModel(),
40
+ sortedRowModel: createSortedRowModel(),
41
+ })
42
+
43
+ const DEFAULT_SIZES = [10, 25, 50] as const
44
+
45
+ export const dataTableDefaultLabels: DataTableLabels = {
46
+ columns: 'Columns',
47
+ deselectPage: 'Deselect page',
48
+ deselectResults: 'Deselect filtered results',
49
+ deselectRow: row => `Deselect ${row}`,
50
+ firstPage: 'First page',
51
+ lastPage: 'Last page',
52
+ nextPage: 'Next page',
53
+ page: (page, pages) => `Page ${page} of ${pages}`,
54
+ pagination: table => `${table} pagination`,
55
+ previousPage: 'Previous page',
56
+ reset: 'Reset',
57
+ results: count => `${count} result${count === 1 ? '' : 's'}`,
58
+ rowsPerPage: 'Rows per page',
59
+ search: 'Search',
60
+ selectPage: 'Select page',
61
+ selectResults: 'Select filtered results',
62
+ selectRow: row => `Select ${row}`,
63
+ selected: (selected, total) => `${selected} of ${total} ${total === 1 ? 'row' : 'rows'} selected.`,
64
+ sort: (column, next) => `Sort ${column} ${next}`,
65
+ toolbar: table => `${table} controls`,
66
+ }
67
+
68
+ type Subscription = { unsubscribe(): void }
69
+
70
+ type ColumnModel<T extends DataTableData> = {
71
+ column: DataTableColumn<T>
72
+ id: string
73
+ read?: (row: T, sourceIndex: number) => unknown
74
+ }
75
+
76
+ export type DataTableColumnView<T extends DataTableData> = ColumnModel<T> & {
77
+ active: readonly string[]
78
+ sorted: false | 'asc' | 'desc'
79
+ visible: boolean
80
+ }
81
+
82
+ export type DataTableCellView<T extends DataTableData> = {
83
+ column: ColumnModel<T>
84
+ value: unknown
85
+ }
86
+
87
+ export type DataTableRowView<T extends DataTableData, Key extends DataTableKey> = {
88
+ cells: readonly DataTableCellView<T>[]
89
+ id: string
90
+ key: Key
91
+ original: T
92
+ selected: boolean
93
+ sourceIndex: number
94
+ }
95
+
96
+ export type DataTableView<T extends DataTableData, Key extends DataTableKey> = {
97
+ columns: readonly DataTableColumnView<T>[]
98
+ filteredCount: number
99
+ hasFilters: boolean
100
+ page: {
101
+ count: number
102
+ enabled: boolean
103
+ index: number
104
+ size: number
105
+ sizes: readonly number[]
106
+ }
107
+ query: string
108
+ rows: readonly DataTableRowView<T, Key>[]
109
+ selectedCount: number
110
+ selection: {
111
+ enabled: boolean
112
+ all: boolean
113
+ some: boolean
114
+ }
115
+ sourceCount: number
116
+ visibility: boolean
117
+ }
118
+
119
+ export type DataTableModel<T extends DataTableData, Key extends DataTableKey> = {
120
+ cell(cell: DataTableCellView<T>, row: DataTableRowView<T, Key>): Children
121
+ firstPage(): void
122
+ lastPage(): void
123
+ nextPage(): void
124
+ previousPage(): void
125
+ reset(event?: Event): void
126
+ selected(rowId: string): boolean
127
+ selection(): { all: boolean; some: boolean }
128
+ setFacet(columnId: string, value: string, checked: boolean, event?: Event): void
129
+ setPageSize(size: number): void
130
+ setQuery(query: string, event?: Event): void
131
+ sort(columnId: string, event?: Event): void
132
+ sync(args: DataTableArgs<T, Key>): DataTableView<T, Key>
133
+ toggleColumn(columnId: string, visible: boolean): void
134
+ togglePage(checked: boolean, event?: Event): void
135
+ toggleRow(rowId: string, checked: boolean, event?: Event): void
136
+ }
137
+
138
+ const assertText = (value: unknown, name: string) => {
139
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`DataTable invalid ${name}`)
140
+ return value
141
+ }
142
+
143
+ const encodeKey = (key: DataTableKey, index?: number) => {
144
+ if (typeof key === 'string') {
145
+ if (!key) throw new TypeError(`DataTable invalid row key${index === undefined ? '' : ` at ${index}`}`)
146
+ return `s:${key}`
147
+ }
148
+ if (!Number.isFinite(key)) throw new TypeError(`DataTable invalid row key${index === undefined ? '' : ` at ${index}`}`)
149
+ return `n:${Object.is(key, -0) ? 0 : key}`
150
+ }
151
+
152
+ const columnId = <T extends DataTableData,>(column: DataTableColumn<T>) => {
153
+ const id = column.id ?? (typeof column.value === 'string' ? column.value : undefined)
154
+ return assertText(id, 'column id')
155
+ }
156
+
157
+ const scalarText = (value: unknown, column: string, index: number) => {
158
+ if (value == null) return ''
159
+ if (typeof value === 'string' || typeof value === 'boolean') return String(value)
160
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value)
161
+ throw new TypeError(`DataTable invalid searchable value ${column} at ${index}`)
162
+ }
163
+
164
+ const facetValues = <T extends DataTableData,>(model: ColumnModel<T>, row: T, index: number) => {
165
+ const facet = model.column.facet
166
+ const raw = facet?.values?.(row, index)
167
+ if (raw !== undefined) return (Array.isArray(raw) ? raw : [raw]).map(value => assertText(value, `facet value ${model.id}`))
168
+ return [scalarText(model.read?.(row, index), model.id, index)]
169
+ }
170
+
171
+ const compareValues = (
172
+ left: unknown,
173
+ right: unknown,
174
+ leftRow: { index: number },
175
+ rightRow: { index: number },
176
+ columnId: string,
177
+ alphanumeric: () => number,
178
+ ) => {
179
+ const leftMissing = left == null
180
+ const rightMissing = right == null
181
+ if (leftMissing || rightMissing) return leftMissing === rightMissing ? 0 : leftMissing ? 1 : -1
182
+ if (typeof left !== typeof right) throw new TypeError(`DataTable mixed sort values ${columnId}`)
183
+ if (typeof left === 'string') return alphanumeric()
184
+ if (typeof left === 'number' && typeof right === 'number') {
185
+ if (!Number.isFinite(left) || !Number.isFinite(right)) {
186
+ throw new TypeError(`DataTable invalid sort value ${columnId} at ${!Number.isFinite(left) ? leftRow.index : rightRow.index}`)
187
+ }
188
+ return left === right ? 0 : left < right ? -1 : 1
189
+ }
190
+ if (typeof left === 'boolean' && typeof right === 'boolean') return left === right ? 0 : left ? 1 : -1
191
+ throw new TypeError(`DataTable invalid sort value ${columnId}`)
192
+ }
193
+
194
+ const paginationConfig = (pagination: false | DataTablePagination | undefined) => {
195
+ const sizes = pagination === false ? DEFAULT_SIZES : pagination?.sizes ?? DEFAULT_SIZES
196
+ if (!sizes.length) throw new RangeError('DataTable empty page sizes')
197
+ const seen = new Set<number>()
198
+ for (const size of sizes) {
199
+ if (!Number.isInteger(size) || size <= 0 || seen.has(size)) throw new RangeError('DataTable invalid page sizes')
200
+ seen.add(size)
201
+ }
202
+ const size = pagination === false ? sizes[0]! : pagination?.defaultSize ?? sizes[0]!
203
+ if (!seen.has(size)) throw new RangeError('DataTable default page size is unavailable')
204
+ return { enabled: pagination !== false, size, sizes }
205
+ }
206
+
207
+ export const dataTableReactivity = (host: Host) => {
208
+ const subscriptions = new Set<Subscription>()
209
+ let disposed = false
210
+ const dispose = () => {
211
+ if (disposed) return
212
+ disposed = true
213
+ for (const subscription of subscriptions) subscription.unsubscribe()
214
+ subscriptions.clear()
215
+ }
216
+ const add = (subscription: Subscription) => {
217
+ if (disposed) subscription.unsubscribe()
218
+ else subscriptions.add(subscription)
219
+ return subscription
220
+ }
221
+ const bindings: TableReactivityBindings = {
222
+ ...storeReactivityBindings(),
223
+ addSubscription: subscription => { add(subscription) },
224
+ createOptionsStore: false,
225
+ schedule: fn => queueMicrotask(() => {
226
+ if (disposed || host.signal.aborted) return
227
+ try { fn() } catch (error) { host.throw(error) }
228
+ }),
229
+ unmount: dispose,
230
+ wrapExternalAtoms: false,
231
+ }
232
+ host.signal.addEventListener('abort', dispose, { once: true })
233
+ return { add, bindings }
234
+ }
235
+
236
+ export const createDataTableModel = <T extends DataTableData, Key extends DataTableKey>(
237
+ host: Host,
238
+ initialArgs: DataTableArgs<T, Key>,
239
+ ): DataTableModel<T, Key> => {
240
+ const reactive = dataTableReactivity(host)
241
+ const features = tableFeatures({ ...dataTableStrategy, coreReactivityFeature: reactive.bindings })
242
+ let args = initialArgs
243
+ let actionEvent: Event | undefined
244
+ let syncing = false
245
+ let stateVersion = 0
246
+ let renderedVersion = 0
247
+ let requestedVersion = 0
248
+ let queued = false
249
+ let optionsInitialized = false
250
+
251
+ let rowsRef: readonly T[] | undefined
252
+ let rowsLength = 0
253
+ let keyGetter: DataTableArgs<T, Key>['getRowKey'] | undefined
254
+ let tableData: readonly T[] = initialArgs.rows
255
+ let rowIds: string[] = []
256
+ let rowKeys: Key[] = []
257
+ let rowById = new Map<string, number>()
258
+
259
+ let columnsRef: readonly DataTableColumn<T>[] | undefined
260
+ let columnsLength = 0
261
+ let models: ColumnModel<T>[] = []
262
+ let modelById = new Map<string, ColumnModel<T>>()
263
+ let definitions: ReadonlyArray<ColumnDef<typeof features, T, unknown>> = []
264
+ let knownColumns = new Map<string, ColumnModel<T>>()
265
+
266
+ let selectionEnabled = Boolean(initialArgs.selection)
267
+ let selectionControlled = initialArgs.selection?.value !== undefined
268
+ let searchEnabled = Boolean(initialArgs.search)
269
+ let searchValue = ''
270
+ let pageConfig = paginationConfig(initialArgs.pagination)
271
+
272
+ const snapshotRows = (current: DataTableArgs<T, Key>) => {
273
+ if (current.rows === rowsRef && current.getRowKey === keyGetter) {
274
+ if (current.rows.length !== rowsLength) throw new TypeError('DataTable mutated rows')
275
+ if (rowsLength) {
276
+ const last = rowsLength - 1
277
+ if (
278
+ encodeKey(current.getRowKey(current.rows[0]!, 0)) !== rowIds[0]
279
+ || encodeKey(current.getRowKey(current.rows[last]!, last)) !== rowIds[last]
280
+ ) throw new TypeError('DataTable mutated rows')
281
+ }
282
+ return false
283
+ }
284
+ const replaced = current.rows !== rowsRef
285
+ rowsRef = current.rows
286
+ rowsLength = current.rows.length
287
+ keyGetter = current.getRowKey
288
+ // TanStack memoizes core rows by data identity. A new key function needs a
289
+ // fresh identity even when the logical collection itself is unchanged.
290
+ tableData = replaced ? current.rows : [...current.rows]
291
+ rowIds = []
292
+ rowKeys = []
293
+ rowById = new Map()
294
+ current.rows.forEach((row, index) => {
295
+ const key = current.getRowKey(row, index)
296
+ const id = encodeKey(key, index)
297
+ const previous = rowById.get(id)
298
+ if (previous !== undefined) throw new TypeError(`DataTable duplicate row key ${JSON.stringify(key)} ${previous}/${index}`)
299
+ rowById.set(id, index)
300
+ rowIds.push(id)
301
+ rowKeys.push(key)
302
+ })
303
+ return true
304
+ }
305
+
306
+ const snapshotColumns = (current: DataTableArgs<T, Key>) => {
307
+ if (current.columns === columnsRef) {
308
+ if (current.columns.length !== columnsLength) throw new TypeError('DataTable mutated columns')
309
+ return false
310
+ }
311
+ if (!current.columns.length) throw new TypeError('DataTable needs columns')
312
+ columnsRef = current.columns
313
+ columnsLength = current.columns.length
314
+ models = []
315
+ modelById = new Map()
316
+ for (const column of current.columns) {
317
+ assertText(column.label, 'column label')
318
+ const id = columnId(column)
319
+ if (modelById.has(id)) throw new TypeError(`DataTable duplicate column ${JSON.stringify(id)}`)
320
+ const read = column.value === undefined
321
+ ? undefined
322
+ : typeof column.value === 'function'
323
+ ? column.value
324
+ : (row: T) => row[column.value as keyof T]
325
+ if (!read && !column.cell) throw new TypeError(`DataTable display column ${JSON.stringify(id)} needs a cell`)
326
+ if (!read && column.facet) throw new TypeError(`DataTable display column ${JSON.stringify(id)} cannot define a facet`)
327
+ if (column.facet) {
328
+ assertText(column.facet.label, `facet label ${id}`)
329
+ const options = new Set<string>()
330
+ for (const option of column.facet.options) {
331
+ assertText(option.label, `facet option label ${id}`)
332
+ assertText(option.value, `facet option value ${id}`)
333
+ if (options.has(option.value)) throw new TypeError(`DataTable duplicate facet option ${id}/${option.value}`)
334
+ options.add(option.value)
335
+ }
336
+ }
337
+ const model = { column, id, read }
338
+ models.push(model)
339
+ modelById.set(id, model)
340
+ }
341
+ if (models.every(({ column }) => column.defaultHidden)) throw new TypeError('DataTable needs a visible column')
342
+
343
+ definitions = models.map(model => {
344
+ const { column, id, read } = model
345
+ const definition = {
346
+ enableGlobalFilter: Boolean(read && column.search !== false),
347
+ enableHiding: column.hideable !== false,
348
+ enableSorting: Boolean(read && column.sort !== false),
349
+ filterFn: (row, _columnId, active) => {
350
+ const selected = active as readonly string[]
351
+ return !selected.length || facetValues(model, row.original, row.index).some(value => selected.includes(value))
352
+ },
353
+ header: column.label,
354
+ id,
355
+ sortFn: (left, right, columnId) => {
356
+ if (typeof column.sort === 'function') {
357
+ const result = column.sort(left.original, right.original)
358
+ if (!Number.isFinite(result)) throw new TypeError(`DataTable invalid comparator ${columnId}`)
359
+ return result
360
+ }
361
+ return compareValues(
362
+ left.getValue(columnId),
363
+ right.getValue(columnId),
364
+ left,
365
+ right,
366
+ columnId,
367
+ () => sortFn_alphanumeric(left, right, columnId),
368
+ )
369
+ },
370
+ sortUndefined: false,
371
+ } satisfies ColumnDef<typeof features, T, unknown>
372
+ return read
373
+ ? { ...definition, accessorFn: read } satisfies ColumnDef<typeof features, T, unknown>
374
+ : definition
375
+ })
376
+ return true
377
+ }
378
+
379
+ const selectionState = (keys: readonly Key[] | undefined) => {
380
+ const state: RowSelectionState = Object.create(null)
381
+ const seen = new Set<string>()
382
+ for (const key of keys ?? []) {
383
+ const id = encodeKey(key)
384
+ if (seen.has(id)) throw new TypeError(`DataTable duplicate selection key ${JSON.stringify(key)}`)
385
+ seen.add(id)
386
+ if (rowById.has(id)) state[id] = true
387
+ }
388
+ return state
389
+ }
390
+ const selectionInput = (selection: DataTableArgs<T, Key>['selection']) => {
391
+ const fallback = selectionState(selection?.defaultValue)
392
+ return selection?.value === undefined ? fallback : selectionState(selection.value)
393
+ }
394
+ const sameSelection = (left: RowSelectionState, right: RowSelectionState) => {
395
+ for (const id of Object.keys(left)) if (Boolean(left[id]) !== Boolean(right[id])) return false
396
+ for (const id of Object.keys(right)) if (Boolean(left[id]) !== Boolean(right[id])) return false
397
+ return true
398
+ }
399
+
400
+ snapshotRows(initialArgs)
401
+ snapshotColumns(initialArgs)
402
+ knownColumns = new Map(modelById)
403
+ assertText(initialArgs.label, 'label')
404
+
405
+ const initialSelection = selectionInput(initialArgs.selection)
406
+ const initialVisibility = Object.fromEntries(models.map(({ column, id }) => [
407
+ id,
408
+ column.hideable === false || !column.defaultHidden,
409
+ ]))
410
+
411
+ const table = constructTable<typeof features, T>({
412
+ autoResetPageIndex: false,
413
+ columns: definitions,
414
+ data: tableData,
415
+ enableMultiSort: false,
416
+ enableRowRangeSelection: false,
417
+ enableRowSelection: selectionEnabled,
418
+ features,
419
+ getColumnCanGlobalFilter: column => (column.columnDef as { enableGlobalFilter?: boolean }).enableGlobalFilter === true,
420
+ getRowId: (_row, index) => rowIds[index]!,
421
+ globalFilterFn: (row, columnId) => {
422
+ const model = modelById.get(columnId)
423
+ if (!model?.read || model.column.search === false) return false
424
+ const raw = typeof model.column.search === 'function'
425
+ ? model.column.search(row.original, row.index)
426
+ : row.getValue(columnId)
427
+ return scalarText(raw, columnId, row.index).toLowerCase().includes(searchValue)
428
+ },
429
+ initialState: {
430
+ columnFilters: [],
431
+ columnVisibility: initialVisibility,
432
+ globalFilter: '',
433
+ pagination: { pageIndex: 0, pageSize: pageConfig.size },
434
+ rowSelection: initialSelection,
435
+ sorting: [],
436
+ },
437
+ })
438
+
439
+ const defaultSelectionUpdater = table.options.onRowSelectionChange!
440
+
441
+ const selectedKeys = (state = table.atoms.rowSelection.get()) => {
442
+ const indexes: number[] = []
443
+ for (const id of Object.keys(state)) {
444
+ const index = rowById.get(id)
445
+ if (state[id] && index !== undefined) indexes.push(index)
446
+ }
447
+ indexes.sort((left, right) => left - right)
448
+ return indexes.map(index => rowKeys[index]!)
449
+ }
450
+
451
+ const countSelected = (state = table.atoms.rowSelection.get()) => {
452
+ let count = 0
453
+ for (const id of Object.keys(state)) if (state[id] && rowById.has(id)) count++
454
+ return count
455
+ }
456
+ const pageSelection = () => {
457
+ const rows = pageConfig.enabled ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows
458
+ let selected = 0
459
+ for (const row of rows) if (row.getIsSelected()) selected++
460
+ const all = rows.length > 0 && selected === rows.length
461
+ return { all, some: selected > 0 && !all }
462
+ }
463
+
464
+ const invalidate = (version: number) => {
465
+ requestedVersion = Math.max(requestedVersion, version)
466
+ if (queued || host.signal.aborted) return
467
+ queued = true
468
+ queueMicrotask(() => {
469
+ queued = false
470
+ if (host.signal.aborted || renderedVersion >= requestedVersion) return
471
+ try { host.next() } catch (error) { if (!host.signal.aborted) host.throw(error) }
472
+ })
473
+ }
474
+
475
+ const selectionUpdater = (updater: RowSelectionState | ((state: RowSelectionState) => RowSelectionState)) => {
476
+ const selection = args.selection
477
+ if (!selection) return
478
+ if (selection.value !== undefined) {
479
+ const current = selectionState(selection.value)
480
+ const next = typeof updater === 'function' ? updater(current) : updater
481
+ selection.onValueChange(selectedKeys(next), actionEvent)
482
+ return
483
+ }
484
+ defaultSelectionUpdater(updater)
485
+ selection.onValueChange?.(selectedKeys(), actionEvent)
486
+ }
487
+
488
+ reactive.add(table.store.subscribe(() => {
489
+ const version = ++stateVersion
490
+ if (!syncing) invalidate(version)
491
+ }))
492
+
493
+ const withAction = (event: Event | undefined, action: () => void) => {
494
+ if (host.signal.aborted) return
495
+ const previous = actionEvent
496
+ actionEvent = event
497
+ try { action() } finally { actionEvent = previous }
498
+ }
499
+
500
+ const firstPage = () => { if (!host.signal.aborted && pageConfig.enabled) table.firstPage() }
501
+ const resetPage = () => { if (pageConfig.enabled) table.setPageIndex(0) }
502
+
503
+ const reconcileColumns = (changed: boolean) => {
504
+ if (!changed) return
505
+ const visibility = table.atoms.columnVisibility.get()
506
+ const nextVisibility: Record<string, boolean> = {}
507
+ for (const model of models) {
508
+ const previous = knownColumns.get(model.id)
509
+ nextVisibility[model.id] = model.column.hideable === false
510
+ ? true
511
+ : previous ? visibility[model.id] !== false : !model.column.defaultHidden
512
+ }
513
+ if (!Object.values(nextVisibility).some(Boolean)) nextVisibility[models[0]!.id] = true
514
+ table.baseAtoms.columnVisibility.set(nextVisibility)
515
+
516
+ let reset = false
517
+ const sorting = table.atoms.sorting.get()
518
+ if (sorting.some(sort => {
519
+ const model = modelById.get(sort.id)
520
+ return !model?.read || model.column.sort === false
521
+ })) {
522
+ table.baseAtoms.sorting.set([])
523
+ reset = true
524
+ }
525
+ const currentFilters = table.atoms.columnFilters.get()
526
+ const filters = currentFilters.flatMap(filter => {
527
+ const facet = modelById.get(filter.id)?.column.facet
528
+ if (!facet) return []
529
+ const allowed = new Set(facet.options.map(option => option.value))
530
+ const values = (filter.value as readonly string[]).filter(value => allowed.has(value))
531
+ return values.length ? [{ id: filter.id, value: values }] : []
532
+ })
533
+ const filtersChanged = filters.length !== currentFilters.length || filters.some((filter, index) => {
534
+ const current = currentFilters[index]
535
+ const values = filter.value as readonly string[]
536
+ const currentValues = current?.value as readonly string[] | undefined
537
+ return !current || current.id !== filter.id || values.length !== currentValues?.length
538
+ || values.some((value, valueIndex) => value !== currentValues[valueIndex])
539
+ })
540
+ if (filtersChanged) {
541
+ table.baseAtoms.columnFilters.set(filters)
542
+ reset = true
543
+ }
544
+ knownColumns = new Map(modelById)
545
+ if (reset) resetPage()
546
+ }
547
+
548
+ const sync = (nextArgs: DataTableArgs<T, Key>): DataTableView<T, Key> => {
549
+ args = nextArgs
550
+ assertText(args.label, 'label')
551
+ const rowsChanged = snapshotRows(args)
552
+ const columnsChanged = snapshotColumns(args)
553
+ // Rows cache accessor results by column ID. Rebuild them when a schema is
554
+ // replaced over the same collection; stable renders keep the same data ref.
555
+ if (columnsChanged && !rowsChanged) tableData = [...args.rows]
556
+ const nextPage = paginationConfig(args.pagination)
557
+ const nextSelection = Boolean(args.selection)
558
+ const nextSelectionState = selectionInput(args.selection)
559
+ const nextSelectionControlled = args.selection?.value !== undefined
560
+ const nextSearch = Boolean(args.search)
561
+ const optionsChanged = !optionsInitialized
562
+ || rowsChanged
563
+ || columnsChanged
564
+ || selectionEnabled !== nextSelection
565
+ || searchEnabled !== nextSearch
566
+ || selectionControlled !== nextSelectionControlled
567
+ || (nextSelectionControlled && !sameSelection(table.atoms.rowSelection.get(), nextSelectionState))
568
+
569
+ syncing = true
570
+ try {
571
+ if (selectionEnabled !== nextSelection) {
572
+ table.baseAtoms.rowSelection.set(nextSelection
573
+ ? nextSelectionState
574
+ : {})
575
+ }
576
+ if (searchEnabled !== nextSearch) {
577
+ searchValue = ''
578
+ table.baseAtoms.globalFilter.set('')
579
+ }
580
+ if (pageConfig.enabled !== nextPage.enabled) {
581
+ table.baseAtoms.pagination.set({ pageIndex: 0, pageSize: nextPage.size })
582
+ } else if (!nextPage.sizes.includes(table.atoms.pagination.get().pageSize)) {
583
+ table.baseAtoms.pagination.set({ pageIndex: 0, pageSize: nextPage.size })
584
+ }
585
+
586
+ selectionEnabled = nextSelection
587
+ selectionControlled = nextSelectionControlled
588
+ searchEnabled = nextSearch
589
+ pageConfig = nextPage
590
+
591
+ if (optionsChanged) {
592
+ table.setOptions(previous => ({
593
+ ...previous,
594
+ columns: definitions,
595
+ data: tableData,
596
+ enableGlobalFilter: searchEnabled,
597
+ enableRowSelection: selectionEnabled,
598
+ onRowSelectionChange: selectionUpdater,
599
+ state: selectionControlled ? { rowSelection: nextSelectionState } : undefined,
600
+ }))
601
+ optionsInitialized = true
602
+ }
603
+
604
+ reconcileColumns(columnsChanged)
605
+ if (rowsChanged && args.selection?.value === undefined) {
606
+ const current = table.atoms.rowSelection.get()
607
+ const pruned: RowSelectionState = Object.create(null)
608
+ for (const id of Object.keys(current)) if (rowById.has(id)) pruned[id] = true
609
+ if (Object.keys(pruned).length !== Object.keys(current).length) table.baseAtoms.rowSelection.set(pruned)
610
+ }
611
+
612
+ if (pageConfig.enabled) {
613
+ const pageCount = Math.max(1, table.getPageCount())
614
+ const current = table.atoms.pagination.get()
615
+ if (current.pageIndex >= pageCount) table.baseAtoms.pagination.set({ ...current, pageIndex: pageCount - 1 })
616
+ }
617
+ } finally {
618
+ syncing = false
619
+ }
620
+
621
+ const pagination = table.atoms.pagination.get()
622
+ const pageCount = pageConfig.enabled ? Math.max(1, table.getPageCount()) : 1
623
+ const sourceRows = pageConfig.enabled ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows
624
+ const allColumns = models.map(model => ({
625
+ ...model,
626
+ active: (table.getColumn(model.id)!.getFilterValue() as readonly string[] | undefined) ?? [],
627
+ sorted: table.getColumn(model.id)!.getIsSorted(),
628
+ visible: table.getColumn(model.id)!.getIsVisible(),
629
+ }))
630
+ const visibleRows = sourceRows.map(row => ({
631
+ cells: row.getVisibleCells().map(cell => ({ column: modelById.get(cell.column.id)!, value: cell.getValue() })),
632
+ id: row.id,
633
+ key: rowKeys[row.index]!,
634
+ original: row.original,
635
+ selected: row.getIsSelected(),
636
+ sourceIndex: row.index,
637
+ }))
638
+ const selected = countSelected()
639
+ const pageSelected = pageSelection()
640
+ const filters = table.atoms.columnFilters.get()
641
+ const query = searchEnabled ? String(table.atoms.globalFilter.get() ?? '') : ''
642
+
643
+ renderedVersion = stateVersion
644
+ return {
645
+ columns: allColumns,
646
+ filteredCount: table.getFilteredRowModel().rows.length,
647
+ hasFilters: Boolean(query || filters.length),
648
+ page: {
649
+ count: pageCount,
650
+ enabled: pageConfig.enabled,
651
+ index: pageConfig.enabled ? pagination.pageIndex : 0,
652
+ size: pagination.pageSize,
653
+ sizes: pageConfig.sizes,
654
+ },
655
+ query,
656
+ rows: visibleRows,
657
+ selectedCount: selected,
658
+ selection: {
659
+ enabled: selectionEnabled,
660
+ ...pageSelected,
661
+ },
662
+ sourceCount: args.rows.length,
663
+ visibility: models.length > 1 && models.some(model => model.column.hideable !== false),
664
+ }
665
+ }
666
+
667
+ return {
668
+ cell(cell, row) {
669
+ if (cell.column.column.cell) return cell.column.column.cell(row.original, {
670
+ columnId: cell.column.id,
671
+ sourceIndex: row.sourceIndex,
672
+ value: cell.value,
673
+ })
674
+ if (cell.value == null) return ''
675
+ if (['bigint', 'boolean', 'number', 'string'].includes(typeof cell.value)) return String(cell.value)
676
+ throw new TypeError(`DataTable cannot render ${cell.column.id} at ${row.sourceIndex}`)
677
+ },
678
+ firstPage,
679
+ lastPage: () => { if (!host.signal.aborted && pageConfig.enabled) table.lastPage() },
680
+ nextPage: () => { if (!host.signal.aborted && pageConfig.enabled) table.nextPage() },
681
+ previousPage: () => { if (!host.signal.aborted && pageConfig.enabled) table.previousPage() },
682
+ reset(event) {
683
+ withAction(event, () => {
684
+ searchValue = ''
685
+ table.setGlobalFilter('')
686
+ table.setColumnFilters([])
687
+ resetPage()
688
+ })
689
+ },
690
+ selected: id => table.atoms.rowSelection.get()[id] === true,
691
+ selection: pageSelection,
692
+ setFacet(id, value, checked, event) {
693
+ const facet = modelById.get(id)?.column.facet
694
+ if (!facet?.options.some(option => option.value === value)) return
695
+ withAction(event, () => {
696
+ const column = table.getColumn(id)
697
+ const active = new Set(column?.getFilterValue() as readonly string[] | undefined)
698
+ if (active.has(value) === checked) return
699
+ if (checked) active.add(value)
700
+ else active.delete(value)
701
+ column?.setFilterValue(active.size ? [...active] : undefined)
702
+ resetPage()
703
+ })
704
+ },
705
+ setPageSize(size) {
706
+ if (host.signal.aborted || !pageConfig.enabled || !pageConfig.sizes.includes(size)) return
707
+ table.setPagination({ pageIndex: 0, pageSize: size })
708
+ },
709
+ setQuery(query, event) {
710
+ if (!searchEnabled) return
711
+ withAction(event, () => {
712
+ const value = query.trim()
713
+ searchValue = value.toLowerCase()
714
+ table.setGlobalFilter(value)
715
+ resetPage()
716
+ })
717
+ },
718
+ sort(id, event) {
719
+ const model = modelById.get(id)
720
+ if (!model?.read || model.column.sort === false) return
721
+ withAction(event, () => {
722
+ const current = table.atoms.sorting.get()[0]
723
+ const next: SortingState = current?.id !== id
724
+ ? [{ id, desc: false }]
725
+ : !current.desc ? [{ id, desc: true }] : []
726
+ table.setSorting(next)
727
+ resetPage()
728
+ })
729
+ },
730
+ sync,
731
+ toggleColumn(id, visible) {
732
+ if (host.signal.aborted) return
733
+ const column = table.getColumn(id)
734
+ if (!column?.getCanHide()) return
735
+ if (!visible && table.getVisibleLeafColumns().length <= 1) return
736
+ column.toggleVisibility(visible)
737
+ },
738
+ togglePage(checked, event) {
739
+ if (!selectionEnabled) return
740
+ withAction(event, () => {
741
+ const state = { ...table.atoms.rowSelection.get() }
742
+ const rows = pageConfig.enabled ? table.getRowModel().rows : table.getPrePaginatedRowModel().rows
743
+ for (const row of rows) {
744
+ if (checked) state[row.id] = true
745
+ else delete state[row.id]
746
+ }
747
+ table.setRowSelection(state)
748
+ })
749
+ },
750
+ toggleRow(id, checked, event) {
751
+ if (!selectionEnabled || !rowById.has(id)) return
752
+ withAction(event, () => {
753
+ const state = { ...table.atoms.rowSelection.get() }
754
+ if (checked) state[id] = true
755
+ else delete state[id]
756
+ table.setRowSelection(state)
757
+ })
758
+ },
759
+ }
760
+ }