@svgrid/grid 1.0.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 (213) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +39 -0
  3. package/dist/FlexRender.svelte +96 -0
  4. package/dist/FlexRender.svelte.d.ts +49 -0
  5. package/dist/SvGrid.svelte +8742 -0
  6. package/dist/SvGrid.svelte.d.ts +381 -0
  7. package/dist/SvGridChart.svelte +653 -0
  8. package/dist/SvGridChart.svelte.d.ts +25 -0
  9. package/dist/SvGridChart.test.d.ts +1 -0
  10. package/dist/SvGridChart.test.js +62 -0
  11. package/dist/SvGridDropdown.svelte +675 -0
  12. package/dist/SvGridDropdown.svelte.d.ts +40 -0
  13. package/dist/a11y.contract.test.d.ts +1 -0
  14. package/dist/a11y.contract.test.js +39 -0
  15. package/dist/a11y.d.ts +38 -0
  16. package/dist/a11y.js +37 -0
  17. package/dist/a11y.test.d.ts +1 -0
  18. package/dist/a11y.test.js +44 -0
  19. package/dist/cell-formatting.d.ts +17 -0
  20. package/dist/cell-formatting.js +141 -0
  21. package/dist/cell-formatting.test.d.ts +1 -0
  22. package/dist/cell-formatting.test.js +234 -0
  23. package/dist/chart-export.d.ts +14 -0
  24. package/dist/chart-export.js +112 -0
  25. package/dist/chart.d.ts +226 -0
  26. package/dist/chart.js +732 -0
  27. package/dist/chart.test.d.ts +1 -0
  28. package/dist/chart.test.js +289 -0
  29. package/dist/collaboration.d.ts +74 -0
  30. package/dist/collaboration.js +98 -0
  31. package/dist/collaboration.test.d.ts +1 -0
  32. package/dist/collaboration.test.js +65 -0
  33. package/dist/conditional-formatting.d.ts +91 -0
  34. package/dist/conditional-formatting.js +170 -0
  35. package/dist/conditional-formatting.test.d.ts +1 -0
  36. package/dist/conditional-formatting.test.js +87 -0
  37. package/dist/core.coverage.test.d.ts +1 -0
  38. package/dist/core.coverage.test.js +186 -0
  39. package/dist/core.d.ts +370 -0
  40. package/dist/core.js +568 -0
  41. package/dist/core.performance.test.d.ts +1 -0
  42. package/dist/core.performance.test.js +29 -0
  43. package/dist/createGrid.svelte.d.ts +6 -0
  44. package/dist/createGrid.svelte.js +17 -0
  45. package/dist/createGrid.test.d.ts +1 -0
  46. package/dist/createGrid.test.js +9 -0
  47. package/dist/createGridState.svelte.d.ts +3 -0
  48. package/dist/createGridState.svelte.js +13 -0
  49. package/dist/editors/cell-editors.d.ts +23 -0
  50. package/dist/editors/cell-editors.js +97 -0
  51. package/dist/editors/cell-editors.test.d.ts +1 -0
  52. package/dist/editors/cell-editors.test.js +75 -0
  53. package/dist/fill-patterns.d.ts +30 -0
  54. package/dist/fill-patterns.js +207 -0
  55. package/dist/fill-patterns.test.d.ts +1 -0
  56. package/dist/fill-patterns.test.js +81 -0
  57. package/dist/filtering/excel-filters.d.ts +26 -0
  58. package/dist/filtering/excel-filters.js +64 -0
  59. package/dist/filtering/excel-filters.test.d.ts +1 -0
  60. package/dist/filtering/excel-filters.test.js +116 -0
  61. package/dist/filtering/locale-filter.test.d.ts +13 -0
  62. package/dist/filtering/locale-filter.test.js +189 -0
  63. package/dist/flex-render.d.ts +1 -0
  64. package/dist/flex-render.js +2 -0
  65. package/dist/flex-render.test.d.ts +1 -0
  66. package/dist/flex-render.test.js +142 -0
  67. package/dist/group-aggregate.test.d.ts +1 -0
  68. package/dist/group-aggregate.test.js +32 -0
  69. package/dist/index.d.ts +29 -0
  70. package/dist/index.js +27 -0
  71. package/dist/keyboard.d.ts +8 -0
  72. package/dist/keyboard.js +91 -0
  73. package/dist/keyboard.test.d.ts +1 -0
  74. package/dist/keyboard.test.js +27 -0
  75. package/dist/merge-objects.d.ts +9 -0
  76. package/dist/merge-objects.js +28 -0
  77. package/dist/named-views.d.ts +42 -0
  78. package/dist/named-views.js +80 -0
  79. package/dist/named-views.test.d.ts +1 -0
  80. package/dist/named-views.test.js +57 -0
  81. package/dist/new-features.test.d.ts +1 -0
  82. package/dist/new-features.test.js +183 -0
  83. package/dist/render-component.d.ts +13 -0
  84. package/dist/render-component.js +14 -0
  85. package/dist/render-component.test.d.ts +1 -0
  86. package/dist/render-component.test.js +41 -0
  87. package/dist/server-data-source.d.ts +76 -0
  88. package/dist/server-data-source.js +83 -0
  89. package/dist/server-data-source.test.d.ts +1 -0
  90. package/dist/server-data-source.test.js +81 -0
  91. package/dist/sparkline.d.ts +56 -0
  92. package/dist/sparkline.js +97 -0
  93. package/dist/sparkline.test.d.ts +1 -0
  94. package/dist/sparkline.test.js +52 -0
  95. package/dist/static-functions.d.ts +1 -0
  96. package/dist/static-functions.js +1 -0
  97. package/dist/subscribe.d.ts +9 -0
  98. package/dist/subscribe.js +31 -0
  99. package/dist/subscribe.test.d.ts +1 -0
  100. package/dist/subscribe.test.js +93 -0
  101. package/dist/sv-grid-scrollbar.d.ts +1 -0
  102. package/dist/sv-grid-scrollbar.js +310 -0
  103. package/dist/svgrid-wrapper.types.d.ts +341 -0
  104. package/dist/svgrid-wrapper.types.js +1 -0
  105. package/dist/svgrid.api-extensions.test.d.ts +14 -0
  106. package/dist/svgrid.api-extensions.test.js +334 -0
  107. package/dist/svgrid.api.test.d.ts +15 -0
  108. package/dist/svgrid.api.test.js +169 -0
  109. package/dist/svgrid.behavior.test.d.ts +15 -0
  110. package/dist/svgrid.behavior.test.js +605 -0
  111. package/dist/svgrid.column-reorder.test.d.ts +15 -0
  112. package/dist/svgrid.column-reorder.test.js +224 -0
  113. package/dist/svgrid.features.test.d.ts +1 -0
  114. package/dist/svgrid.features.test.js +129 -0
  115. package/dist/svgrid.interaction.test.d.ts +9 -0
  116. package/dist/svgrid.interaction.test.js +318 -0
  117. package/dist/svgrid.locale-filtering.test.d.ts +15 -0
  118. package/dist/svgrid.locale-filtering.test.js +261 -0
  119. package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
  120. package/dist/svgrid.new-features.wrapper.test.js +136 -0
  121. package/dist/svgrid.row-pinning.test.d.ts +14 -0
  122. package/dist/svgrid.row-pinning.test.js +250 -0
  123. package/dist/svgrid.set-filter.test.d.ts +17 -0
  124. package/dist/svgrid.set-filter.test.js +268 -0
  125. package/dist/svgrid.wrapper.test.d.ts +1 -0
  126. package/dist/svgrid.wrapper.test.js +33 -0
  127. package/dist/test-setup.d.ts +1 -0
  128. package/dist/test-setup.js +29 -0
  129. package/dist/transaction.test.d.ts +1 -0
  130. package/dist/transaction.test.js +89 -0
  131. package/dist/virtualization/column-virtualizer.d.ts +20 -0
  132. package/dist/virtualization/column-virtualizer.js +19 -0
  133. package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
  134. package/dist/virtualization/column-virtualizer.test.js +25 -0
  135. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
  136. package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
  137. package/dist/virtualization/types.d.ts +28 -0
  138. package/dist/virtualization/types.js +1 -0
  139. package/dist/virtualization/virtualizer.d.ts +13 -0
  140. package/dist/virtualization/virtualizer.js +232 -0
  141. package/dist/virtualization/virtualizer.test.d.ts +1 -0
  142. package/dist/virtualization/virtualizer.test.js +41 -0
  143. package/package.json +69 -0
  144. package/src/FlexRender.svelte +96 -0
  145. package/src/SvGrid.svelte +8742 -0
  146. package/src/SvGridChart.svelte +653 -0
  147. package/src/SvGridChart.test.ts +69 -0
  148. package/src/SvGridDropdown.svelte +675 -0
  149. package/src/a11y.contract.test.ts +49 -0
  150. package/src/a11y.test.ts +59 -0
  151. package/src/a11y.ts +59 -0
  152. package/src/cell-formatting.test.ts +273 -0
  153. package/src/cell-formatting.ts +169 -0
  154. package/src/chart-export.ts +144 -0
  155. package/src/chart.test.ts +311 -0
  156. package/src/chart.ts +934 -0
  157. package/src/collaboration.test.ts +74 -0
  158. package/src/collaboration.ts +161 -0
  159. package/src/conditional-formatting.test.ts +107 -0
  160. package/src/conditional-formatting.ts +239 -0
  161. package/src/core.coverage.test.ts +230 -0
  162. package/src/core.performance.test.ts +30 -0
  163. package/src/core.ts +991 -0
  164. package/src/createGrid.svelte.ts +42 -0
  165. package/src/createGrid.test.ts +10 -0
  166. package/src/createGridState.svelte.ts +17 -0
  167. package/src/editors/cell-editors.test.ts +80 -0
  168. package/src/editors/cell-editors.ts +127 -0
  169. package/src/fill-patterns.test.ts +93 -0
  170. package/src/fill-patterns.ts +229 -0
  171. package/src/filtering/excel-filters.test.ts +131 -0
  172. package/src/filtering/excel-filters.ts +101 -0
  173. package/src/filtering/locale-filter.test.ts +220 -0
  174. package/src/flex-render.test.ts +143 -0
  175. package/src/flex-render.ts +3 -0
  176. package/src/group-aggregate.test.ts +48 -0
  177. package/src/index.ts +159 -0
  178. package/src/keyboard.test.ts +59 -0
  179. package/src/keyboard.ts +97 -0
  180. package/src/merge-objects.ts +48 -0
  181. package/src/named-views.test.ts +66 -0
  182. package/src/named-views.ts +120 -0
  183. package/src/new-features.test.ts +217 -0
  184. package/src/render-component.test.ts +51 -0
  185. package/src/render-component.ts +28 -0
  186. package/src/server-data-source.test.ts +88 -0
  187. package/src/server-data-source.ts +163 -0
  188. package/src/sparkline.test.ts +59 -0
  189. package/src/sparkline.ts +164 -0
  190. package/src/static-functions.ts +11 -0
  191. package/src/subscribe.test.ts +103 -0
  192. package/src/subscribe.ts +38 -0
  193. package/src/sv-grid-scrollbar.ts +347 -0
  194. package/src/svgrid-wrapper.types.ts +382 -0
  195. package/src/svgrid.api-extensions.test.ts +362 -0
  196. package/src/svgrid.api.test.ts +192 -0
  197. package/src/svgrid.behavior.test.ts +657 -0
  198. package/src/svgrid.column-reorder.test.ts +234 -0
  199. package/src/svgrid.features.test.ts +157 -0
  200. package/src/svgrid.interaction.test.ts +355 -0
  201. package/src/svgrid.locale-filtering.test.ts +259 -0
  202. package/src/svgrid.new-features.wrapper.test.ts +164 -0
  203. package/src/svgrid.row-pinning.test.ts +266 -0
  204. package/src/svgrid.set-filter.test.ts +270 -0
  205. package/src/svgrid.wrapper.test.ts +35 -0
  206. package/src/test-setup.ts +37 -0
  207. package/src/transaction.test.ts +100 -0
  208. package/src/virtualization/column-virtualizer.test.ts +27 -0
  209. package/src/virtualization/column-virtualizer.ts +30 -0
  210. package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
  211. package/src/virtualization/types.ts +30 -0
  212. package/src/virtualization/virtualizer.test.ts +47 -0
  213. package/src/virtualization/virtualizer.ts +270 -0
package/src/core.ts ADDED
@@ -0,0 +1,991 @@
1
+ import type { SparklineConfig } from './sparkline'
2
+
3
+ export type RowData = Record<string, unknown>
4
+ export type Updater<T> = T | ((prev: T) => T)
5
+ export type SortingState = Array<{ id: string; desc: boolean }>
6
+ export type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }
7
+ export type ColumnFiltersState = Array<ColumnFilter>
8
+ export type PaginationState = { pageIndex: number; pageSize: number }
9
+ export type GroupingState = Array<string>
10
+ export type ExpandedState = Record<string, boolean>
11
+ export type RowSelectionState = Record<string, boolean>
12
+ export type ActiveCellState = {
13
+ rowIndex: number
14
+ colIndex: number
15
+ cellId: string | null
16
+ }
17
+ export type TableFeatures = Record<string, unknown>
18
+
19
+ export type CellData = unknown
20
+
21
+ export type HeaderContext<TData extends RowData> = {
22
+ header: Header<TData>
23
+ column: Column<TData>
24
+ table: SvGrid<TData>
25
+ }
26
+
27
+ export type CellContext<TData extends RowData> = {
28
+ cell: Cell<TData>
29
+ row: Row<TData>
30
+ column: Column<TData>
31
+ table: SvGrid<TData>
32
+ getValue: () => unknown
33
+ }
34
+
35
+ /**
36
+ * Context passed to a custom `cellEditor` snippet/component. Three write
37
+ * helpers cover the lifecycle:
38
+ *
39
+ * - `update(next)` - stage `next` as the draft, keep the editor open.
40
+ * Use this for live-preview controls (sliders,
41
+ * color pickers) so the user can keep adjusting.
42
+ * - `commit(next?)` - write the value AND close the editor. The
43
+ * argument is optional; when omitted, the most
44
+ * recently `update()`d value is saved. Use this
45
+ * for "done" gestures (Enter, picking an option).
46
+ * - `cancel()` - discard the draft and close the editor.
47
+ */
48
+ export type EditorContext<TData extends RowData> = CellContext<TData> & {
49
+ value: unknown
50
+ update: (next: unknown) => void
51
+ commit: (next?: unknown) => void
52
+ cancel: () => void
53
+ }
54
+
55
+ export type CellFormatConfig =
56
+ | {
57
+ type: 'number'
58
+ locales?: string | Array<string>
59
+ options?: Intl.NumberFormatOptions
60
+ }
61
+ | {
62
+ type: 'currency'
63
+ /** ISO 4217 (default USD) */
64
+ currency?: string
65
+ locales?: string | Array<string>
66
+ options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>
67
+ }
68
+ | {
69
+ type: 'percent'
70
+ locales?: string | Array<string>
71
+ options?: Omit<Intl.NumberFormatOptions, 'style'>
72
+ /**
73
+ * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).
74
+ * Default false.
75
+ */
76
+ valueIsPercentPoints?: boolean
77
+ }
78
+ | {
79
+ type: 'date' | 'datetime'
80
+ locales?: string | Array<string>
81
+ /**
82
+ * Shortcut patterns merged with `options`:
83
+ * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,
84
+ * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.
85
+ */
86
+ pattern?: string
87
+ options?: Intl.DateTimeFormatOptions
88
+ }
89
+
90
+ export type CellFormatter<TData extends RowData> = (context: {
91
+ value: unknown
92
+ row: Row<TData>
93
+ column: Column<TData>
94
+ table: SvGrid<TData>
95
+ }) => string
96
+
97
+ export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)
98
+
99
+ /**
100
+ * How a column's value is aggregated for a group row when `columnGrouping`
101
+ * is active. Built-in reducers cover the common cases; pass a function for
102
+ * anything custom (weighted average, median, percentile, distinct count).
103
+ * The function receives the finite numeric values AND the raw leaf rows.
104
+ */
105
+ export type GroupAggregator<TData = any> =
106
+ | 'sum'
107
+ | 'avg'
108
+ | 'min'
109
+ | 'max'
110
+ | 'count'
111
+ | 'countDistinct'
112
+ | 'extent'
113
+ | 'first'
114
+ | ((values: number[], rows: Array<TData>) => unknown)
115
+
116
+ /** Apply a group aggregator over a bucket's leaf rows for one column. */
117
+ export function applyGroupAggregate<TData extends RowData>(
118
+ agg: GroupAggregator<TData>,
119
+ columnId: string,
120
+ rows: ReadonlyArray<Row<TData>>,
121
+ ): unknown {
122
+ const raw = rows.map((r) => r.getCellValueByColumnId(columnId))
123
+ if (typeof agg === 'function') {
124
+ const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
125
+ return agg(nums, rows.map((r) => r.original))
126
+ }
127
+ if (agg === 'count') return rows.length
128
+ if (agg === 'countDistinct') return new Set(raw.map((v) => String(v ?? ''))).size
129
+ if (agg === 'first') return raw[0]
130
+ const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
131
+ if (!nums.length) return undefined
132
+ switch (agg) {
133
+ case 'sum':
134
+ return nums.reduce((a, b) => a + b, 0)
135
+ case 'avg':
136
+ return nums.reduce((a, b) => a + b, 0) / nums.length
137
+ case 'min':
138
+ return Math.min(...nums)
139
+ case 'max':
140
+ return Math.max(...nums)
141
+ case 'extent':
142
+ return `${Math.min(...nums)} – ${Math.max(...nums)}`
143
+ default:
144
+ return undefined
145
+ }
146
+ }
147
+
148
+ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
149
+ id?: string
150
+ field?: keyof TData & string
151
+ accessorFn?: (row: TData) => unknown
152
+ header?: ColumnDefTemplate<HeaderContext<TData>>
153
+ footer?: ColumnDefTemplate<HeaderContext<TData>>
154
+ cell?: ColumnDefTemplate<CellContext<TData>>
155
+ columns?: Array<ColumnDef<TFeatures, TData>>
156
+ editorType?:
157
+ | 'text'
158
+ | 'number'
159
+ | 'date'
160
+ | 'datetime'
161
+ | 'time' // native <input type="time"> - HH:MM or HH:MM:SS
162
+ | 'password' // native <input type="password"> with masked rendering
163
+ | 'checkbox'
164
+ | 'list'
165
+ | 'chips'
166
+ | 'select' // custom dropdown - single value, no typeahead
167
+ | 'rich-select' // custom dropdown with a typeahead search input
168
+ | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline
169
+ | 'color' // native <input type="color"> swatch
170
+ | 'rating' // 5-star rating control
171
+ /**
172
+ * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`
173
+ * and `cancel()` helper. Use when none of the built-in `editorType`s fit;
174
+ * the snippet's outer element is mounted inside the editing cell and
175
+ * inherits keyboard handling (Esc cancels, Enter commits unless your
176
+ * snippet preventDefaults it).
177
+ *
178
+ * Coexists with `editorType`: when both are set, `cellEditor` wins and
179
+ * `editorType` is treated as a hint for parsing the saved value.
180
+ */
181
+ cellEditor?: ColumnDefTemplate<EditorContext<TData>>
182
+ /**
183
+ * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`
184
+ * runs per cell so the tooltip can reflect the value. Returning an empty
185
+ * string skips the tooltip.
186
+ */
187
+ tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)
188
+ /**
189
+ * Gate editing per column or per cell.
190
+ *
191
+ * - `true` (or omitted): the column is fully editable.
192
+ * - `false`: the column is read-only - double-click, type-to-edit,
193
+ * fill-handle drag, Delete, and clipboard paste all skip it.
194
+ * - `(ctx) => boolean`: evaluated for each cell, so you can lock
195
+ * individual rows (e.g. by role, status, ownership). Returning
196
+ * `false` opts the cell out of every editing path, identical to
197
+ * setting `editable: false` on the whole column for that row.
198
+ *
199
+ * The grid-wide `enableInlineEditing` prop still wins when set to
200
+ * `false`.
201
+ */
202
+ editable?: boolean | ((context: CellContext<TData>) => boolean)
203
+ /**
204
+ * When `false`, this column never shows a sort indicator and clicking
205
+ * its header is a no-op - `api.setSort(thisColumn, ...)` is also
206
+ * ignored. Defaults to `true` (the column participates in sorting as
207
+ * long as `rowSortingFeature` is registered).
208
+ */
209
+ sortable?: boolean
210
+ /**
211
+ * When `false`, this column never shows a filter funnel / menu and
212
+ * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the
213
+ * column is filterable as long as `columnFilteringFeature` is
214
+ * registered).
215
+ */
216
+ filterable?: boolean
217
+ /**
218
+ * Options for `editorType: 'list' | 'chips'`. Either bare values (the
219
+ * string is both value and label) or `{ value, label }` objects.
220
+ * For `chips` this is optional - when omitted, the chips editor becomes
221
+ * free-form (user types and presses Enter to commit a chip).
222
+ *
223
+ * Pass a function `(row) => options` for row-dependent (cascading)
224
+ * options - e.g. City options that depend on Country in the same row.
225
+ */
226
+ editorOptions?:
227
+ | ReadonlyArray<
228
+ string | number | { value: string | number; label?: string; color?: string }
229
+ >
230
+ | ((
231
+ row: TData,
232
+ ) => ReadonlyArray<
233
+ string | number | { value: string | number; label?: string; color?: string }
234
+ >)
235
+ /** When true, list/chips allow multiple selections. Cell value becomes an array. */
236
+ editorMultiple?: boolean
237
+ /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
238
+ editorSeparator?: string
239
+ format?: CellFormatConfig
240
+ formatter?: CellFormatter<TData>
241
+ /**
242
+ * Aggregate this column's values into the group row when grouping is
243
+ * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |
244
+ * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result
245
+ * is formatted with this column's `format` and shown in the group header.
246
+ */
247
+ aggregate?: GroupAggregator<TData>
248
+ /**
249
+ * Render the cell as an in-cell sparkline chart. The cell value should be
250
+ * an array of numbers (or a comma/space separated string). Mutually
251
+ * exclusive with a custom `cell` renderer (a `cell` wins if both are set).
252
+ *
253
+ * { sparkline: { type: 'line' } } // default line
254
+ * { sparkline: { type: 'bar', color: '#16a34a' } }
255
+ * { sparkline: { type: 'winloss' } } // sign-only up/down
256
+ *
257
+ * See `SparklineConfig` for the full option set (type, color,
258
+ * negativeColor, width, height, fixed min/max).
259
+ */
260
+ sparkline?: SparklineConfig
261
+ /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
262
+ width?: number
263
+ /**
264
+ * Horizontal alignment for header and body cells. When omitted, the
265
+ * default is inferred from `editorType`:
266
+ * - `'number' | 'date' | 'datetime'` → `'right'`
267
+ * - `'checkbox'` → `'center'`
268
+ * - everything else → `'left'`
269
+ */
270
+ align?: 'left' | 'center' | 'right'
271
+ /**
272
+ * Per-cell conditional CSS. Two shapes:
273
+ *
274
+ * - **String** (or array of strings): class name(s) added to the
275
+ * cell's `<td>` for every row in this column.
276
+ * - **Function**: invoked per cell with the same `CellContext` shape
277
+ * the `cell` renderer receives. Return a string, an array of
278
+ * strings, or an object mapping class names to booleans.
279
+ *
280
+ * Use it for status tinting, conditional bold, "negative number"
281
+ * coloring - anything that's a function of the row's value. Cells
282
+ * still receive their format / cell renderer; the class just
283
+ * augments the rendered `<td>`.
284
+ */
285
+ cellClass?:
286
+ | string
287
+ | ReadonlyArray<string>
288
+ | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)
289
+ }
290
+
291
+ export type Column<TData extends RowData> = {
292
+ id: string
293
+ columnDef: ColumnDef<any, TData>
294
+ depth: number
295
+ parentId?: string
296
+ getCanSort: () => boolean
297
+ getCanFilter: () => boolean
298
+ getIsSorted: () => false | 'asc' | 'desc'
299
+ getToggleSortingHandler: () => () => void
300
+ }
301
+
302
+ export type Header<TData extends RowData> = {
303
+ id: string
304
+ isPlaceholder: boolean
305
+ colSpan: number
306
+ column: Column<TData>
307
+ getContext: () => HeaderContext<TData>
308
+ }
309
+
310
+ export type HeaderGroup<TData extends RowData> = {
311
+ id: string
312
+ headers: Array<Header<TData>>
313
+ }
314
+
315
+ export type Cell<TData extends RowData> = {
316
+ id: string
317
+ row: Row<TData>
318
+ column: Column<TData>
319
+ getValue: () => unknown
320
+ getContext: () => CellContext<TData>
321
+ }
322
+
323
+ export type Row<TData extends RowData> = {
324
+ id: string
325
+ index: number
326
+ original: TData
327
+ depth: number
328
+ subRows?: Array<Row<TData>>
329
+ /** Total leaf (data) rows under this group row. Undefined for data rows. */
330
+ leafCount?: number
331
+ getCanExpand: () => boolean
332
+ getIsExpanded: () => boolean
333
+ toggleExpanded: () => void
334
+ getIsSelected: () => boolean
335
+ toggleSelected: () => void
336
+ getAllCells: () => Array<Cell<TData>>
337
+ getCellValueByColumnId: (columnId: string) => unknown
338
+ }
339
+
340
+ export type RowModel<TData extends RowData> = {
341
+ rows: Array<Row<TData>>
342
+ }
343
+
344
+ export type Store<T> = {
345
+ readonly state: T
346
+ setState: (updater: (prev: T) => T) => void
347
+ subscribe: (listener: () => void) => () => void
348
+ }
349
+
350
+ function createStore<T>(initial: T): Store<T> {
351
+ let value = initial
352
+ const listeners = new Set<() => void>()
353
+ return {
354
+ get state() {
355
+ return value
356
+ },
357
+ setState(updater) {
358
+ value = updater(value)
359
+ listeners.forEach((listener) => listener())
360
+ },
361
+ subscribe(listener) {
362
+ listeners.add(listener)
363
+ return () => listeners.delete(listener)
364
+ },
365
+ }
366
+ }
367
+
368
+ export const rowSortingFeature = { key: 'rowSortingFeature' }
369
+ export const columnFilteringFeature = { key: 'columnFilteringFeature' }
370
+ export const rowPaginationFeature = { key: 'rowPaginationFeature' }
371
+ export const columnGroupingFeature = { key: 'columnGroupingFeature' }
372
+ export const rowSelectionFeature = { key: 'rowSelectionFeature' }
373
+ export const rowExpandingFeature = { key: 'rowExpandingFeature' }
374
+
375
+ export function tableFeatures<T extends TableFeatures>(features: T): T {
376
+ return features
377
+ }
378
+
379
+ export const sortFns = {
380
+ auto: (a: unknown, b: unknown) => String(a).localeCompare(String(b)),
381
+ number: (a: unknown, b: unknown) => Number(a ?? 0) - Number(b ?? 0),
382
+ date: (a: unknown, b: unknown) => {
383
+ const aa = new Date(a as any).getTime()
384
+ const bb = new Date(b as any).getTime()
385
+ return aa - bb
386
+ },
387
+ }
388
+
389
+ export const filterFns = {
390
+ includesString: (value: unknown, query: string) =>
391
+ String(value).toLowerCase().includes(query.toLowerCase()),
392
+ equals: (value: unknown, query: unknown) => value === query,
393
+ }
394
+
395
+ export type RowModelFactory<TData extends RowData> = (args: {
396
+ table: SvGrid<TData>
397
+ rows: Array<Row<TData>>
398
+ }) => Array<Row<TData>>
399
+
400
+ export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {
401
+ return ({ rows }) => rows
402
+ }
403
+ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
404
+ return ({ table, rows }) => {
405
+ const filters: ColumnFiltersState = table.getState().columnFilters ?? []
406
+ if (!filters.length) return rows
407
+ return rows.filter((row) => {
408
+ return filters.every((filter) => {
409
+ const cellValue = row
410
+ .getAllCells()
411
+ .find((cell) => cell.column.id === filter.id)
412
+ ?.getValue()
413
+ const filterFn = filter.fn ? filterFns[filter.fn] : filterFns.includesString
414
+ return filterFn(cellValue, filter.value as any)
415
+ })
416
+ })
417
+ }
418
+ }
419
+ export function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {
420
+ return ({ table, rows }) => {
421
+ const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }
422
+ const start = pagination.pageIndex * pagination.pageSize
423
+ return rows.slice(start, start + pagination.pageSize)
424
+ }
425
+ }
426
+ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {
427
+ return ({ table, rows }) => {
428
+ const grouping: GroupingState = table.getState().grouping ?? []
429
+ if (!grouping.length) return rows
430
+ const columns = table.getAllColumns()
431
+
432
+ // Recursively bucket rows by each grouping column in turn. At every level a
433
+ // group row is built that stands in for its children - a non-group column
434
+ // resolves to the value shared by every leaf row, or to undefined when the
435
+ // leaves disagree.
436
+ function buildGroups(
437
+ input: Array<Row<TData>>,
438
+ levelIndex: number,
439
+ depth: number,
440
+ idPrefix: string,
441
+ ): Array<Row<TData>> {
442
+ if (levelIndex >= grouping.length) {
443
+ // Leaves: actual data rows, with their nesting depth recorded.
444
+ return input.map((row) => ({ ...row, depth }))
445
+ }
446
+ const groupKey = grouping[levelIndex]
447
+ if (!groupKey) return input
448
+
449
+ const buckets = new Map<string, Array<Row<TData>>>()
450
+ for (const row of input) {
451
+ const value = row.getCellValueByColumnId(groupKey)
452
+ const key = String(value ?? '')
453
+ const list = buckets.get(key) ?? []
454
+ list.push(row)
455
+ buckets.set(key, list)
456
+ }
457
+
458
+ const groupRows: Array<Row<TData>> = []
459
+ let index = 0
460
+ buckets.forEach((children, key) => {
461
+ const id = `${idPrefix}_${groupKey}_${key}`
462
+ const subRows = buildGroups(children, levelIndex + 1, depth + 1, id)
463
+ const isDeepest = levelIndex + 1 >= grouping.length
464
+ const leafCount = isDeepest
465
+ ? subRows.length
466
+ : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)
467
+
468
+ const resolveColumnValue = (columnId: string): unknown => {
469
+ if (columnId === groupKey) return key
470
+ let resolved: unknown
471
+ let hasResolved = false
472
+ for (const child of children) {
473
+ const childValue = child.getCellValueByColumnId(columnId)
474
+ if (!hasResolved) {
475
+ resolved = childValue
476
+ hasResolved = true
477
+ } else if (childValue !== resolved) {
478
+ return undefined
479
+ }
480
+ }
481
+ return resolved
482
+ }
483
+
484
+ const groupOriginal: Record<string, unknown> = {}
485
+ columns.forEach((column) => {
486
+ const field = column.columnDef.field
487
+ if (!field) return
488
+ const agg = column.columnDef.aggregate
489
+ groupOriginal[field] = agg
490
+ ? applyGroupAggregate(agg, column.id, children)
491
+ : resolveColumnValue(column.id)
492
+ })
493
+
494
+ const groupRow: Row<TData> = {
495
+ id,
496
+ index: index++,
497
+ original: groupOriginal as TData,
498
+ depth,
499
+ subRows,
500
+ leafCount,
501
+ getCanExpand: () => true,
502
+ getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
503
+ toggleExpanded: () => {
504
+ table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
505
+ },
506
+ getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),
507
+ toggleSelected: () => {
508
+ table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
509
+ },
510
+ getAllCells: () => [],
511
+ // Prefer the precomputed group value (which carries aggregates)
512
+ // and fall back to the shared-value resolver for columns without
513
+ // a field.
514
+ getCellValueByColumnId: (columnId: string) => {
515
+ const col = columns.find((c) => c.id === columnId)
516
+ const field = col?.columnDef.field
517
+ if (field && field in groupOriginal) return groupOriginal[field]
518
+ return resolveColumnValue(columnId)
519
+ },
520
+ }
521
+ groupRows.push(groupRow)
522
+ })
523
+ return groupRows
524
+ }
525
+
526
+ return buildGroups(rows, 0, 0, 'group')
527
+ }
528
+ }
529
+ export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
530
+ return ({ table, rows }) => {
531
+ const expanded: ExpandedState = table.getState().expanded ?? {}
532
+ const flattened: Array<Row<TData>> = []
533
+ const visit = (row: Row<TData>) => {
534
+ flattened.push(row)
535
+ if (row.subRows?.length && expanded[row.id]) {
536
+ for (const sub of row.subRows) visit(sub)
537
+ }
538
+ }
539
+ for (const row of rows) visit(row)
540
+ return flattened
541
+ }
542
+ }
543
+ export function createSortedRowModel<TData extends RowData>(
544
+ localSortFns: typeof sortFns = sortFns,
545
+ ): RowModelFactory<TData> {
546
+ return ({ table, rows }) => {
547
+ const sorting = table.getState().sorting ?? []
548
+ if (!sorting.length) return rows
549
+
550
+ const sorted = [...rows].sort((a, b) => {
551
+ for (const clause of sorting) {
552
+ const column = table.getAllColumns().find((col) => col.id === clause.id)
553
+ if (!column) continue
554
+ const editorType = column.columnDef.editorType
555
+ const comparator =
556
+ editorType === 'number'
557
+ ? localSortFns.number
558
+ : editorType === 'date' || editorType === 'datetime'
559
+ ? localSortFns.date
560
+ : localSortFns.auto
561
+ const result = comparator(
562
+ a.getCellValueByColumnId(column.id),
563
+ b.getCellValueByColumnId(column.id),
564
+ )
565
+ if (result !== 0) return clause.desc ? -result : result
566
+ }
567
+ return 0
568
+ })
569
+ return sorted
570
+ }
571
+ }
572
+
573
+ export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
574
+ _features: TFeatures
575
+ _rowModels?: {
576
+ coreRowModel?: RowModelFactory<TData>
577
+ filteredRowModel?: RowModelFactory<TData>
578
+ sortedRowModel?: RowModelFactory<TData>
579
+ paginatedRowModel?: RowModelFactory<TData>
580
+ groupedRowModel?: RowModelFactory<TData>
581
+ expandedRowModel?: RowModelFactory<TData>
582
+ }
583
+ columns: Array<ColumnDef<TFeatures, TData>>
584
+ data: ReadonlyArray<TData>
585
+ /**
586
+ * Optional row-id resolver. When set, the value it returns becomes
587
+ * `row.id` (and therefore the selection / expansion / edit key). When
588
+ * omitted, ids fall back to the row's array index as a string. Use a
589
+ * stable id (database PK, UUID, etc.) so selection survives reorders.
590
+ */
591
+ getRowId?: (row: TData, index: number) => string
592
+ state?: Partial<Record<string, any>>
593
+ onSortingChange?: (updater: Updater<SortingState>) => void
594
+ onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void
595
+ onPaginationChange?: (updater: Updater<PaginationState>) => void
596
+ onGroupingChange?: (updater: Updater<GroupingState>) => void
597
+ onExpandedChange?: (updater: Updater<ExpandedState>) => void
598
+ onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void
599
+ onActiveCellChange?: (updater: Updater<ActiveCellState>) => void
600
+ }
601
+
602
+ export type SvGrid<TData extends RowData> = {
603
+ store: Store<Record<string, any>>
604
+ optionsStore: Store<Record<string, any>>
605
+ state: Record<string, any>
606
+ getState: () => Record<string, any>
607
+ setOptions: (updater: Updater<Record<string, any>>) => void
608
+ setColumnFilters: (updater: Updater<ColumnFiltersState>) => void
609
+ setPagination: (updater: Updater<PaginationState>) => void
610
+ setGrouping: (updater: Updater<GroupingState>) => void
611
+ setExpanded: (updater: Updater<ExpandedState>) => void
612
+ setRowSelection: (updater: Updater<RowSelectionState>) => void
613
+ setActiveCell: (updater: Updater<ActiveCellState>) => void
614
+ moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void
615
+ getAllColumns: () => Array<Column<TData>>
616
+ getHeaderGroups: () => Array<HeaderGroup<TData>>
617
+ getFooterGroups: () => Array<HeaderGroup<TData>>
618
+ getRowModel: () => RowModel<TData>
619
+ }
620
+
621
+ type InternalGrid<TData extends RowData> = SvGrid<TData> & {
622
+ getAllColumns: () => Array<Column<TData>>
623
+ }
624
+
625
+ export function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(
626
+ options: SvGridOptions<TFeatures, TData>,
627
+ ): SvGrid<TData> {
628
+ const internalState: Record<string, any> = {
629
+ sorting: [],
630
+ columnFilters: [],
631
+ pagination: { pageIndex: 0, pageSize: options.data.length || 10 },
632
+ grouping: [],
633
+ expanded: {},
634
+ rowSelection: {},
635
+ activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
636
+ ...(options.state ?? {}),
637
+ }
638
+ const store = createStore(internalState)
639
+ const optionsStore = createStore(options as Record<string, any>)
640
+ let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null
641
+ let cachedColumns: Array<Column<TData>> = []
642
+ let cachedHeaderGroups: Array<HeaderGroup<TData>> = []
643
+ let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
644
+ let cachedBaseRowsColumns: Array<Column<TData>> | null = null
645
+ let cachedBaseRows: Array<Row<TData>> = []
646
+ let cachedRowModel: RowModel<TData> | null = null
647
+ let cachedRowModelBaseRows: Array<Row<TData>> | null = null
648
+ let cachedPipeline = options._rowModels
649
+ let cachedSlices: {
650
+ sorting: SortingState | undefined
651
+ columnFilters: ColumnFiltersState | undefined
652
+ pagination: PaginationState | undefined
653
+ grouping: GroupingState | undefined
654
+ expanded: ExpandedState | undefined
655
+ rowSelection: RowSelectionState | undefined
656
+ } | null = null
657
+
658
+ const grid = {
659
+ store,
660
+ optionsStore,
661
+ get state() {
662
+ return store.state
663
+ },
664
+ getState() {
665
+ return store.state
666
+ },
667
+ setOptions(updater: Updater<Record<string, any>>) {
668
+ optionsStore.setState((prev) =>
669
+ typeof updater === 'function' ? (updater as any)(prev) : updater,
670
+ )
671
+ },
672
+ setColumnFilters(updater: Updater<ColumnFiltersState>) {
673
+ store.setState((prev) => ({
674
+ ...prev,
675
+ columnFilters:
676
+ typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,
677
+ }))
678
+ options.onColumnFiltersChange?.(updater)
679
+ },
680
+ setPagination(updater: Updater<PaginationState>) {
681
+ store.setState((prev) => ({
682
+ ...prev,
683
+ pagination:
684
+ typeof updater === 'function'
685
+ ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })
686
+ : updater,
687
+ }))
688
+ options.onPaginationChange?.(updater)
689
+ },
690
+ setGrouping(updater: Updater<GroupingState>) {
691
+ store.setState((prev) => ({
692
+ ...prev,
693
+ grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,
694
+ }))
695
+ options.onGroupingChange?.(updater)
696
+ },
697
+ setExpanded(updater: Updater<ExpandedState>) {
698
+ store.setState((prev) => ({
699
+ ...prev,
700
+ expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,
701
+ }))
702
+ options.onExpandedChange?.(updater)
703
+ },
704
+ setRowSelection(updater: Updater<RowSelectionState>) {
705
+ store.setState((prev) => ({
706
+ ...prev,
707
+ rowSelection:
708
+ typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,
709
+ }))
710
+ options.onRowSelectionChange?.(updater)
711
+ },
712
+ setActiveCell(updater: Updater<ActiveCellState>) {
713
+ store.setState((prev) => {
714
+ const previous: ActiveCellState = prev.activeCell ?? {
715
+ rowIndex: 0,
716
+ colIndex: 0,
717
+ cellId: null,
718
+ }
719
+ const nextActive =
720
+ typeof updater === 'function' ? updater(previous) : updater
721
+ return {
722
+ ...prev,
723
+ activeCell: nextActive,
724
+ }
725
+ })
726
+ options.onActiveCellChange?.(updater)
727
+ },
728
+ moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {
729
+ const rows = grid.getRowModel().rows
730
+ const columns = grid.getAllColumns()
731
+ const maxRow = Math.max(rows.length - 1, 0)
732
+ const maxCol = Math.max(columns.length - 1, 0)
733
+ const current: ActiveCellState = grid.getState().activeCell ?? {
734
+ rowIndex: 0,
735
+ colIndex: 0,
736
+ cellId: null,
737
+ }
738
+
739
+ const rowIndex = Math.min(
740
+ Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),
741
+ maxRow,
742
+ )
743
+ const colIndex = Math.min(
744
+ Math.max(current.colIndex + (next.colDelta ?? 0), 0),
745
+ maxCol,
746
+ )
747
+ const columnId = columns[colIndex]?.id ?? 'col_0'
748
+ grid.setActiveCell({
749
+ rowIndex,
750
+ colIndex,
751
+ cellId: `${rowIndex}_${columnId}`,
752
+ })
753
+ },
754
+ getAllColumns() {
755
+ // Cache hit: referentially identical columns array.
756
+ if (cachedColumnsInput === options.columns && cachedColumns.length) {
757
+ return cachedColumns
758
+ }
759
+ // Soft cache hit: consumers commonly recreate the columns array
760
+ // inline on every render (e.g. `columns={[...]}`). If the new
761
+ // array has the same length AND each entry has the same `field` /
762
+ // `id` / `header` (the visibility-affecting structure of a
763
+ // column), trust the previous build. Mutable inner fields like
764
+ // `cell` and `editorOptions` are still picked up on the next real
765
+ // render that bumps an actual data dep - they're read at cell-
766
+ // render time, not at this top-level cache.
767
+ if (
768
+ cachedColumnsInput &&
769
+ options.columns.length === cachedColumnsInput.length &&
770
+ cachedColumns.length === options.columns.length &&
771
+ options.columns.every((c, i) => {
772
+ const prev = cachedColumnsInput![i]!
773
+ return (
774
+ c.field === prev.field &&
775
+ c.id === prev.id &&
776
+ c.header === prev.header &&
777
+ c.editorType === prev.editorType
778
+ )
779
+ })
780
+ ) {
781
+ // Update the stored input reference so the strict check hits
782
+ // next time, but reuse the built column model.
783
+ cachedColumnsInput = options.columns
784
+ return cachedColumns
785
+ }
786
+
787
+ cachedColumnsInput = options.columns
788
+ cachedHeaderGroups = []
789
+ const build = (
790
+ defs: Array<ColumnDef<TFeatures, TData>>,
791
+ depth: number,
792
+ parentId?: string,
793
+ ): Array<Column<TData>> => {
794
+ const leaves: Array<Column<TData>> = []
795
+ defs.forEach((columnDef, index) => {
796
+ const id = columnDef.id ?? columnDef.field ?? `${parentId ?? 'col'}_${depth}_${index}`
797
+ if (columnDef.columns?.length) {
798
+ leaves.push(...build(columnDef.columns, depth + 1, id))
799
+ return
800
+ }
801
+ leaves.push({
802
+ id,
803
+ depth,
804
+ parentId,
805
+ columnDef,
806
+ getCanSort: () =>
807
+ Boolean((options._features as any).rowSortingFeature) &&
808
+ columnDef.sortable !== false,
809
+ getCanFilter: () =>
810
+ Boolean((options._features as any).columnFilteringFeature) &&
811
+ columnDef.filterable !== false,
812
+ getIsSorted: () => {
813
+ const entry = store.state.sorting?.find((s: any) => s.id === id)
814
+ if (!entry) return false
815
+ return entry.desc ? 'desc' : 'asc'
816
+ },
817
+ getToggleSortingHandler: () => () => {
818
+ const clauses: SortingState = store.state.sorting ?? []
819
+ const current = clauses.find((s: any) => s.id === id)
820
+ const nextClause: SortingState = !current
821
+ ? [...clauses, { id, desc: false }]
822
+ : current.desc
823
+ ? clauses.filter((s) => s.id !== id)
824
+ : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))
825
+ store.setState((prev) => ({ ...prev, sorting: nextClause }))
826
+ options.onSortingChange?.(nextClause)
827
+ },
828
+ })
829
+ })
830
+ return leaves
831
+ }
832
+ cachedColumns = build(options.columns, 0)
833
+ return cachedColumns
834
+ },
835
+ getHeaderGroups() {
836
+ if (cachedHeaderGroups.length) return cachedHeaderGroups
837
+ const headers = grid.getAllColumns().map((column) => {
838
+ const header: Header<TData> = {
839
+ id: column.id,
840
+ isPlaceholder: false,
841
+ colSpan: 1,
842
+ column,
843
+ getContext: () => ({ header, column, table: grid }),
844
+ }
845
+ return header
846
+ })
847
+ cachedHeaderGroups = [{ id: 'header_group_0', headers }]
848
+ return cachedHeaderGroups
849
+ },
850
+ getFooterGroups() {
851
+ return grid.getHeaderGroups()
852
+ },
853
+ getRowModel() {
854
+ const columns = grid.getAllColumns()
855
+ if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
856
+ cachedBaseRowsInput = options.data
857
+ cachedBaseRowsColumns = columns
858
+ // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
859
+ // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
860
+ const columnIndexById = new Map<string, number>()
861
+ for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
862
+ const columnCount = columns.length
863
+
864
+ cachedBaseRows = new Array(options.data.length)
865
+ const getRowId = options.getRowId
866
+ for (let index = 0; index < options.data.length; index++) {
867
+ const original = options.data[index]!
868
+ const id = getRowId ? getRowId(original, index) : String(index)
869
+ // `values` and `cells` are computed lazily - for a 100k-row grid
870
+ // with only ~20 visible rows we don't want to materialise every
871
+ // row's full value array or cell objects up front.
872
+ let cachedValues: Array<unknown> | null = null
873
+ let cachedCells: Array<Cell<TData>> | null = null
874
+
875
+ function computeValues(): Array<unknown> {
876
+ const values = new Array<unknown>(columnCount)
877
+ for (let i = 0; i < columnCount; i++) {
878
+ const column = columns[i]!
879
+ if (column.columnDef.accessorFn) {
880
+ values[i] = column.columnDef.accessorFn(original)
881
+ } else if (column.columnDef.field) {
882
+ values[i] = (original as any)[column.columnDef.field]
883
+ } else {
884
+ values[i] = undefined
885
+ }
886
+ }
887
+ return values
888
+ }
889
+
890
+ const row: Row<TData> = {
891
+ id,
892
+ index,
893
+ original,
894
+ depth: 0,
895
+ getCanExpand: () => false,
896
+ getIsExpanded: () => Boolean((store.state.expanded ?? {})[id]),
897
+ toggleExpanded: () => {
898
+ grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
899
+ },
900
+ getIsSelected: () => Boolean((store.state.rowSelection ?? {})[id]),
901
+ toggleSelected: () => {
902
+ grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
903
+ },
904
+ getAllCells: () => {
905
+ if (cachedCells) return cachedCells
906
+ const built = new Array<Cell<TData>>(columnCount)
907
+ for (let i = 0; i < columnCount; i++) {
908
+ const column = columns[i]!
909
+ const colIndex = i
910
+ const cell: Cell<TData> = {
911
+ id: `${id}_${column.id}`,
912
+ row,
913
+ column,
914
+ getValue: () => {
915
+ if (!cachedValues) cachedValues = computeValues()
916
+ return cachedValues[colIndex]
917
+ },
918
+ getContext: () => ({
919
+ cell,
920
+ row,
921
+ column,
922
+ table: grid,
923
+ getValue: () => cell.getValue(),
924
+ }),
925
+ }
926
+ built[i] = cell
927
+ }
928
+ cachedCells = built
929
+ return built
930
+ },
931
+ getCellValueByColumnId: (columnId: string) => {
932
+ const idx = columnIndexById.get(columnId)
933
+ if (idx === undefined) return undefined
934
+ if (!cachedValues) cachedValues = computeValues()
935
+ return cachedValues[idx]
936
+ },
937
+ }
938
+ cachedBaseRows[index] = row
939
+ }
940
+ }
941
+
942
+ const currentSlices = {
943
+ sorting: store.state.sorting,
944
+ columnFilters: store.state.columnFilters,
945
+ pagination: store.state.pagination,
946
+ grouping: store.state.grouping,
947
+ expanded: store.state.expanded,
948
+ rowSelection: store.state.rowSelection,
949
+ }
950
+ if (
951
+ cachedRowModel &&
952
+ cachedRowModelBaseRows === cachedBaseRows &&
953
+ cachedPipeline === options._rowModels &&
954
+ cachedSlices?.sorting === currentSlices.sorting &&
955
+ cachedSlices?.columnFilters === currentSlices.columnFilters &&
956
+ cachedSlices?.pagination === currentSlices.pagination &&
957
+ cachedSlices?.grouping === currentSlices.grouping &&
958
+ cachedSlices?.expanded === currentSlices.expanded &&
959
+ cachedSlices?.rowSelection === currentSlices.rowSelection
960
+ ) {
961
+ return cachedRowModel
962
+ }
963
+
964
+ let rows: Array<Row<TData>> = cachedBaseRows
965
+
966
+ const pipeline = options._rowModels ?? {}
967
+ const ordered: Array<RowModelFactory<TData> | undefined> = [
968
+ pipeline.coreRowModel,
969
+ pipeline.filteredRowModel,
970
+ pipeline.sortedRowModel,
971
+ pipeline.groupedRowModel,
972
+ pipeline.expandedRowModel,
973
+ pipeline.paginatedRowModel,
974
+ ]
975
+ ordered.forEach((fn) => {
976
+ if (fn) rows = fn({ table: grid, rows })
977
+ })
978
+ cachedPipeline = options._rowModels
979
+ cachedSlices = currentSlices
980
+ cachedRowModelBaseRows = cachedBaseRows
981
+ cachedRowModel = { rows }
982
+ return cachedRowModel
983
+ },
984
+ } as InternalGrid<TData>
985
+
986
+ return grid
987
+ }
988
+
989
+ export function isFunction(value: unknown): value is (...args: Array<any>) => any {
990
+ return typeof value === 'function'
991
+ }