@mdxui/terminal 2.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 (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,1225 @@
1
+ /**
2
+ * @mdxui/terminal Reactive Data Hooks for Data Components
3
+ *
4
+ * This module provides React hooks for connecting data components (Table, List,
5
+ * Card, Metrics) to TanStack DB collections with reactive updates and optimistic
6
+ * mutations.
7
+ *
8
+ * Key features:
9
+ * - Reactive data binding - UI updates automatically when collection data changes
10
+ * - Optimistic updates - UI updates immediately, rolls back on error
11
+ * - Type-safe queries - Full TypeScript support with Zod validation
12
+ * - Live subscriptions - Real-time updates from collection changes
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * import { useReactiveTable, useReactiveList } from '@mdxui/terminal'
17
+ *
18
+ * function UsersTable() {
19
+ * const { data, isLoading, mutate, sort, setSort } = useReactiveTable({
20
+ * collection: 'users',
21
+ * where: { status: 'active' },
22
+ * orderBy: { name: 'asc' },
23
+ * })
24
+ *
25
+ * // Data updates reactively when collection changes
26
+ * return (
27
+ * <Table
28
+ * columns={[
29
+ * { key: 'name', header: 'Name', sortable: true },
30
+ * { key: 'email', header: 'Email' },
31
+ * ]}
32
+ * data={data}
33
+ * sortBy={sort.field}
34
+ * sortDirection={sort.direction}
35
+ * onSort={(field) => setSort(field)}
36
+ * onRowAction={(action, row) => {
37
+ * if (action === 'delete') {
38
+ * mutate.delete({ id: row.id })
39
+ * }
40
+ * }}
41
+ * />
42
+ * )
43
+ * }
44
+ * ```
45
+ */
46
+
47
+ import { useState, useEffect, useCallback, useRef } from 'react'
48
+ import { useDBContext } from './context'
49
+ import type {
50
+ QueryOptions,
51
+ MutationResult,
52
+ WhereClause,
53
+ OrderByClause,
54
+ OrderDirection,
55
+ Collection,
56
+ } from './types'
57
+
58
+ // ============================================================================
59
+ // Types
60
+ // ============================================================================
61
+
62
+ /**
63
+ * Sort state for reactive data components
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const sort: SortState = {
68
+ * field: 'createdAt',
69
+ * direction: 'desc',
70
+ * }
71
+ * ```
72
+ */
73
+ export interface SortState {
74
+ /** Field to sort by */
75
+ field: string | null
76
+ /** Sort direction: 'asc' or 'desc' */
77
+ direction: OrderDirection
78
+ }
79
+
80
+ /**
81
+ * Selection state for reactive data components
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const selection: SelectionState = {
86
+ * mode: 'multi',
87
+ * selected: new Set(['user-1', 'user-3']),
88
+ * }
89
+ * ```
90
+ */
91
+ export interface SelectionState {
92
+ /** Selection mode: 'single', 'multi', or 'none' */
93
+ mode: 'single' | 'multi' | 'none'
94
+ /** Set of selected item IDs */
95
+ selected: Set<string>
96
+ }
97
+
98
+ /**
99
+ * Pagination state for reactive data components
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const pagination: PaginationState = {
104
+ * page: 1,
105
+ * pageSize: 20,
106
+ * total: 150,
107
+ * }
108
+ * ```
109
+ */
110
+ export interface PaginationState {
111
+ /** Current page (1-indexed) */
112
+ page: number
113
+ /** Items per page */
114
+ pageSize: number
115
+ /** Total item count (from query) */
116
+ total: number
117
+ }
118
+
119
+ /**
120
+ * Options for useReactiveData hook
121
+ *
122
+ * @template T - The document type
123
+ *
124
+ * @remarks
125
+ * Extends QueryOptions with reactive-specific options like selection, sorting,
126
+ * and pagination control.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const options: ReactiveDataOptions<User> = {
131
+ * collection: 'users',
132
+ * where: { role: 'admin' },
133
+ * orderBy: { createdAt: 'desc' },
134
+ * limit: 20,
135
+ * selectable: 'multi',
136
+ * optimistic: true,
137
+ * }
138
+ * ```
139
+ */
140
+ export interface ReactiveDataOptions<T> {
141
+ /** Collection name to query from */
142
+ collection: string
143
+ /** Filter conditions */
144
+ where?: WhereClause<T>
145
+ /** Sort order */
146
+ orderBy?: OrderByClause<T> | OrderByClause<T>[]
147
+ /** Maximum items to return */
148
+ limit?: number
149
+ /** Items to skip (for pagination) */
150
+ offset?: number
151
+ /** Enable selection: 'single', 'multi', or false */
152
+ selectable?: 'single' | 'multi' | boolean
153
+ /** Enable optimistic updates (default: true) */
154
+ optimistic?: boolean
155
+ /** Primary key field (default: 'id') */
156
+ primaryKey?: keyof T
157
+ }
158
+
159
+ /**
160
+ * Result of useReactiveData hook
161
+ *
162
+ * @template T - The document type
163
+ *
164
+ * @remarks
165
+ * Provides reactive data, loading/error states, mutation functions, and
166
+ * controls for sorting, selection, and pagination.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * const {
171
+ * data,
172
+ * isLoading,
173
+ * error,
174
+ * mutate,
175
+ * sort,
176
+ * setSort,
177
+ * selection,
178
+ * toggleSelect,
179
+ * pagination,
180
+ * setPage,
181
+ * } = useReactiveData<User>({ collection: 'users' })
182
+ * ```
183
+ */
184
+ export interface ReactiveDataResult<T> {
185
+ /** Reactive data array - updates when collection changes */
186
+ data: T[]
187
+ /** True while initial query is executing */
188
+ isLoading: boolean
189
+ /** Error if query or mutation failed */
190
+ error: Error | undefined
191
+ /** Manual refetch function */
192
+ refetch: () => void
193
+
194
+ // Mutation functions
195
+ /** Mutation functions for insert, update, delete */
196
+ mutate: {
197
+ /** Insert a new document */
198
+ insert: (data: T) => Promise<void>
199
+ /** Update documents matching filter */
200
+ update: (filter: Partial<T>, updates: Partial<T>) => Promise<void>
201
+ /** Delete a document by ID */
202
+ delete: (filter: { id: string } | Partial<T>) => Promise<void>
203
+ }
204
+ /** True while any mutation is pending */
205
+ isMutating: boolean
206
+
207
+ // Sort state
208
+ /** Current sort state */
209
+ sort: SortState
210
+ /** Set sort field (toggles direction if same field) */
211
+ setSort: (field: string) => void
212
+
213
+ // Selection state
214
+ /** Current selection state */
215
+ selection: SelectionState
216
+ /** Toggle selection for an item */
217
+ toggleSelect: (id: string) => void
218
+ /** Select all items */
219
+ selectAll: () => void
220
+ /** Clear all selections */
221
+ clearSelection: () => void
222
+
223
+ // Pagination state
224
+ /** Current pagination state */
225
+ pagination: PaginationState
226
+ /** Go to a specific page */
227
+ setPage: (page: number) => void
228
+ /** Change page size */
229
+ setPageSize: (size: number) => void
230
+ }
231
+
232
+ /**
233
+ * Options for useReactiveTable hook
234
+ *
235
+ * @template T - The document type
236
+ *
237
+ * @remarks
238
+ * Table-specific options extending ReactiveDataOptions with column definitions.
239
+ */
240
+ export interface ReactiveTableOptions<T> extends ReactiveDataOptions<T> {
241
+ /** Column definitions for the table */
242
+ columns?: Array<{
243
+ key: keyof T
244
+ header: string
245
+ sortable?: boolean
246
+ width?: number
247
+ align?: 'left' | 'center' | 'right'
248
+ }>
249
+ }
250
+
251
+ /**
252
+ * Options for useReactiveList hook
253
+ *
254
+ * @template T - The document type
255
+ *
256
+ * @remarks
257
+ * List-specific options extending ReactiveDataOptions.
258
+ */
259
+ export interface ReactiveListOptions<T> extends ReactiveDataOptions<T> {
260
+ /** Field to use as display text */
261
+ labelField?: keyof T
262
+ /** Field to use as icon */
263
+ iconField?: keyof T
264
+ }
265
+
266
+ /**
267
+ * Options for useReactiveMetrics hook
268
+ *
269
+ * @template T - The document type
270
+ *
271
+ * @remarks
272
+ * Metrics-specific options for aggregated data display.
273
+ */
274
+ export interface ReactiveMetricsOptions<T> extends Omit<ReactiveDataOptions<T>, 'limit' | 'offset'> {
275
+ /** Metrics to compute from collection data */
276
+ metrics: Array<{
277
+ /** Unique key for the metric */
278
+ key: string
279
+ /** Display label */
280
+ label: string
281
+ /** Field to aggregate */
282
+ field: keyof T
283
+ /** Aggregation function */
284
+ aggregate: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'latest'
285
+ /** Format for display */
286
+ format?: 'number' | 'currency' | 'percentage'
287
+ /** Unit suffix */
288
+ unit?: string
289
+ }>
290
+ }
291
+
292
+ /**
293
+ * Single metric result
294
+ */
295
+ export interface MetricValue {
296
+ /** Metric key */
297
+ key: string
298
+ /** Display label */
299
+ label: string
300
+ /** Computed value */
301
+ value: number | string
302
+ /** Trend direction (computed from previous values if available) */
303
+ trend?: 'up' | 'down' | 'neutral'
304
+ /** Trend percentage change */
305
+ trendValue?: number
306
+ /** Optional sparkline data */
307
+ sparkline?: number[]
308
+ }
309
+
310
+ /**
311
+ * Result of useReactiveMetrics hook
312
+ */
313
+ export interface ReactiveMetricsResult {
314
+ /** Computed metric values */
315
+ metrics: MetricValue[]
316
+ /** True while initial query is executing */
317
+ isLoading: boolean
318
+ /** Error if query failed */
319
+ error: Error | undefined
320
+ /** Manual refetch function */
321
+ refetch: () => void
322
+ }
323
+
324
+ /**
325
+ * Options for useReactiveCard hook
326
+ *
327
+ * @template T - The document type
328
+ *
329
+ * @remarks
330
+ * Card-specific options for single-record display.
331
+ */
332
+ export interface ReactiveCardOptions<T> {
333
+ /** Collection name */
334
+ collection: string
335
+ /** Filter to find single record */
336
+ where: WhereClause<T>
337
+ /** Fields to display as key-value pairs */
338
+ fields?: Array<{
339
+ key: keyof T
340
+ label: string
341
+ format?: 'string' | 'date' | 'currency' | 'badge'
342
+ }>
343
+ /** Enable optimistic updates */
344
+ optimistic?: boolean
345
+ }
346
+
347
+ /**
348
+ * Result of useReactiveCard hook
349
+ */
350
+ export interface ReactiveCardResult<T> {
351
+ /** Single record data */
352
+ data: T | null
353
+ /** True while loading */
354
+ isLoading: boolean
355
+ /** Error if query failed */
356
+ error: Error | undefined
357
+ /** Refetch function */
358
+ refetch: () => void
359
+ /** Update the record */
360
+ update: (updates: Partial<T>) => Promise<void>
361
+ /** True while updating */
362
+ isUpdating: boolean
363
+ }
364
+
365
+ // ============================================================================
366
+ // Hooks
367
+ // ============================================================================
368
+
369
+ /**
370
+ * Hook for reactive data binding with TanStack DB collections
371
+ *
372
+ * Provides reactive data updates, optimistic mutations, sorting, selection,
373
+ * and pagination for data components like Table and List.
374
+ *
375
+ * @template T - The document type (must extend Record<string, any>)
376
+ *
377
+ * @param options - Configuration for collection, filters, and behavior
378
+ * @returns ReactiveDataResult with data, mutations, and state controls
379
+ *
380
+ * @remarks
381
+ * - Data updates automatically when collection changes (via subscriptions)
382
+ * - Optimistic updates show changes immediately, roll back on error
383
+ * - Sort state toggles direction when same field is selected
384
+ * - Selection supports single and multi-select modes
385
+ * - Pagination is handled client-side with limit/offset
386
+ *
387
+ * @example
388
+ * ```tsx
389
+ * function UserList() {
390
+ * const {
391
+ * data,
392
+ * isLoading,
393
+ * mutate,
394
+ * sort,
395
+ * setSort,
396
+ * selection,
397
+ * toggleSelect,
398
+ * } = useReactiveData<User>({
399
+ * collection: 'users',
400
+ * where: { status: 'active' },
401
+ * orderBy: { name: 'asc' },
402
+ * selectable: 'multi',
403
+ * })
404
+ *
405
+ * if (isLoading) return <Spinner />
406
+ *
407
+ * return (
408
+ * <List
409
+ * items={data.map(user => ({
410
+ * id: user.id,
411
+ * text: user.name,
412
+ * selected: selection.selected.has(user.id),
413
+ * }))}
414
+ * onItemClick={(id) => toggleSelect(id)}
415
+ * onDelete={(id) => mutate.delete({ id })}
416
+ * />
417
+ * )
418
+ * }
419
+ * ```
420
+ *
421
+ * @example
422
+ * ```tsx
423
+ * // With optimistic updates
424
+ * function QuickAddUser() {
425
+ * const { mutate, isMutating } = useReactiveData<User>({
426
+ * collection: 'users',
427
+ * optimistic: true,
428
+ * })
429
+ *
430
+ * const handleAdd = async () => {
431
+ * // UI updates immediately, rolls back if server fails
432
+ * await mutate.insert({
433
+ * id: crypto.randomUUID(),
434
+ * name: 'New User',
435
+ * status: 'active',
436
+ * })
437
+ * }
438
+ *
439
+ * return (
440
+ * <button onClick={handleAdd} disabled={isMutating}>
441
+ * {isMutating ? 'Adding...' : 'Add User'}
442
+ * </button>
443
+ * )
444
+ * }
445
+ * ```
446
+ */
447
+ export function useReactiveData<T extends Record<string, any>>(
448
+ options: ReactiveDataOptions<T>
449
+ ): ReactiveDataResult<T> {
450
+ const db = useDBContext()
451
+ const {
452
+ collection: collectionName,
453
+ where,
454
+ orderBy: initialOrderBy,
455
+ limit,
456
+ offset,
457
+ selectable = false,
458
+ optimistic = true,
459
+ primaryKey = 'id' as keyof T,
460
+ } = options
461
+
462
+ // Data state
463
+ const [data, setData] = useState<T[]>([])
464
+ const [isLoading, setIsLoading] = useState(true)
465
+ const [error, setError] = useState<Error | undefined>(undefined)
466
+ const [isMutating, setIsMutating] = useState(false)
467
+
468
+ // Sort state
469
+ const [sort, setSortState] = useState<SortState>(() => {
470
+ if (!initialOrderBy) return { field: null, direction: 'asc' as OrderDirection }
471
+ const orderClauses = Array.isArray(initialOrderBy) ? initialOrderBy : [initialOrderBy]
472
+ if (orderClauses.length === 0) return { field: null, direction: 'asc' as OrderDirection }
473
+ const [field, direction] = Object.entries(orderClauses[0])[0]
474
+ return { field, direction: direction as OrderDirection }
475
+ })
476
+
477
+ // Selection state
478
+ const [selection, setSelection] = useState<SelectionState>({
479
+ mode: selectable === true ? 'single' : selectable === false ? 'none' : selectable,
480
+ selected: new Set<string>(),
481
+ })
482
+
483
+ // Pagination state
484
+ const [pagination, setPagination] = useState<PaginationState>({
485
+ page: 1,
486
+ pageSize: limit ?? 20,
487
+ total: 0,
488
+ })
489
+
490
+ // Refs for tracking rollback data
491
+ const rollbackDataRef = useRef<T[]>([])
492
+
493
+ // Get collection
494
+ const collection = db.collections[collectionName] as Collection<T> | undefined
495
+
496
+ // Build orderBy from sort state
497
+ const currentOrderBy = sort.field
498
+ ? { [sort.field]: sort.direction } as OrderByClause<T>
499
+ : initialOrderBy
500
+
501
+ // Execute query
502
+ const executeQuery = useCallback(async () => {
503
+ if (!collection) {
504
+ setError(new Error(`Collection '${collectionName}' not found`))
505
+ setIsLoading(false)
506
+ return
507
+ }
508
+
509
+ try {
510
+ const result = await collection.findMany({
511
+ where,
512
+ orderBy: currentOrderBy,
513
+ limit,
514
+ offset,
515
+ })
516
+ setData(result)
517
+ setPagination(prev => ({ ...prev, total: result.length }))
518
+ setError(undefined)
519
+ } catch (err) {
520
+ setError(err instanceof Error ? err : new Error(String(err)))
521
+ } finally {
522
+ setIsLoading(false)
523
+ }
524
+ }, [collection, collectionName, JSON.stringify(where), JSON.stringify(currentOrderBy), limit, offset])
525
+
526
+ // Initial fetch
527
+ useEffect(() => {
528
+ executeQuery()
529
+ }, [executeQuery])
530
+
531
+ // Subscribe to collection changes
532
+ useEffect(() => {
533
+ if (!collection) return
534
+
535
+ const unsubscribe = collection.subscribe((allData: T[]) => {
536
+ // Re-apply filters when data changes
537
+ let result = allData
538
+
539
+ if (where) {
540
+ result = result.filter(doc => matchesWhere(doc, where))
541
+ }
542
+
543
+ if (currentOrderBy) {
544
+ result = sortDocuments(result, currentOrderBy)
545
+ }
546
+
547
+ if (offset !== undefined) {
548
+ result = result.slice(offset)
549
+ }
550
+
551
+ if (limit !== undefined) {
552
+ result = result.slice(0, limit)
553
+ }
554
+
555
+ setData(result)
556
+ setPagination(prev => ({ ...prev, total: result.length }))
557
+ })
558
+
559
+ return unsubscribe
560
+ }, [collection, JSON.stringify(where), JSON.stringify(currentOrderBy), limit, offset])
561
+
562
+ // Mutation functions
563
+ const mutate = {
564
+ insert: useCallback(async (newData: T) => {
565
+ if (!collection) {
566
+ throw new Error(`Collection '${collectionName}' not found`)
567
+ }
568
+
569
+ setIsMutating(true)
570
+
571
+ if (optimistic) {
572
+ // Save for rollback
573
+ rollbackDataRef.current = [...data]
574
+ // Optimistically add to local state
575
+ setData(prev => [...prev, newData])
576
+ }
577
+
578
+ try {
579
+ await collection.insert(newData)
580
+ // If not optimistic, data will update via subscription
581
+ } catch (err) {
582
+ // Rollback on error
583
+ if (optimistic) {
584
+ setData(rollbackDataRef.current)
585
+ }
586
+ throw err
587
+ } finally {
588
+ setIsMutating(false)
589
+ }
590
+ }, [collection, collectionName, data, optimistic]),
591
+
592
+ update: useCallback(async (filter: Partial<T>, updates: Partial<T>) => {
593
+ if (!collection) {
594
+ throw new Error(`Collection '${collectionName}' not found`)
595
+ }
596
+
597
+ setIsMutating(true)
598
+
599
+ if (optimistic) {
600
+ // Save for rollback
601
+ rollbackDataRef.current = [...data]
602
+ // Optimistically update local state
603
+ setData(prev => prev.map(item => {
604
+ const matches = Object.entries(filter).every(([key, val]) => item[key] === val)
605
+ return matches ? { ...item, ...updates } : item
606
+ }))
607
+ }
608
+
609
+ try {
610
+ await collection.update(filter, updates)
611
+ } catch (err) {
612
+ if (optimistic) {
613
+ setData(rollbackDataRef.current)
614
+ }
615
+ throw err
616
+ } finally {
617
+ setIsMutating(false)
618
+ }
619
+ }, [collection, collectionName, data, optimistic]),
620
+
621
+ delete: useCallback(async (filter: { id: string } | Partial<T>) => {
622
+ if (!collection) {
623
+ throw new Error(`Collection '${collectionName}' not found`)
624
+ }
625
+
626
+ setIsMutating(true)
627
+
628
+ if (optimistic) {
629
+ // Save for rollback
630
+ rollbackDataRef.current = [...data]
631
+ // Optimistically remove from local state
632
+ setData(prev => prev.filter(item => {
633
+ const matches = Object.entries(filter).every(([key, val]) => item[key as keyof T] === val)
634
+ return !matches
635
+ }))
636
+ }
637
+
638
+ try {
639
+ await collection.delete(filter as Partial<T>)
640
+ } catch (err) {
641
+ if (optimistic) {
642
+ setData(rollbackDataRef.current)
643
+ }
644
+ throw err
645
+ } finally {
646
+ setIsMutating(false)
647
+ }
648
+ }, [collection, collectionName, data, optimistic]),
649
+ }
650
+
651
+ // Sort control
652
+ const setSort = useCallback((field: string) => {
653
+ setSortState(prev => ({
654
+ field,
655
+ direction: prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc',
656
+ }))
657
+ }, [])
658
+
659
+ // Selection controls
660
+ const toggleSelect = useCallback((id: string) => {
661
+ setSelection(prev => {
662
+ const newSelected = new Set(prev.selected)
663
+ if (prev.mode === 'single') {
664
+ // Single mode: toggle or replace
665
+ if (newSelected.has(id)) {
666
+ newSelected.clear()
667
+ } else {
668
+ newSelected.clear()
669
+ newSelected.add(id)
670
+ }
671
+ } else if (prev.mode === 'multi') {
672
+ // Multi mode: toggle
673
+ if (newSelected.has(id)) {
674
+ newSelected.delete(id)
675
+ } else {
676
+ newSelected.add(id)
677
+ }
678
+ }
679
+ return { ...prev, selected: newSelected }
680
+ })
681
+ }, [])
682
+
683
+ const selectAll = useCallback(() => {
684
+ if (selection.mode === 'none') return
685
+ const allIds = data.map(item => String(item[primaryKey]))
686
+ setSelection(prev => ({ ...prev, selected: new Set(allIds) }))
687
+ }, [data, primaryKey, selection.mode])
688
+
689
+ const clearSelection = useCallback(() => {
690
+ setSelection(prev => ({ ...prev, selected: new Set() }))
691
+ }, [])
692
+
693
+ // Pagination controls
694
+ const setPage = useCallback((page: number) => {
695
+ setPagination(prev => ({ ...prev, page }))
696
+ }, [])
697
+
698
+ const setPageSize = useCallback((size: number) => {
699
+ setPagination(prev => ({ ...prev, pageSize: size, page: 1 }))
700
+ }, [])
701
+
702
+ return {
703
+ data,
704
+ isLoading,
705
+ error,
706
+ refetch: executeQuery,
707
+ mutate,
708
+ isMutating,
709
+ sort,
710
+ setSort,
711
+ selection,
712
+ toggleSelect,
713
+ selectAll,
714
+ clearSelection,
715
+ pagination,
716
+ setPage,
717
+ setPageSize,
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Hook for reactive table data with TanStack DB integration
723
+ *
724
+ * Specialized version of useReactiveData for Table components with
725
+ * column-aware sorting and selection.
726
+ *
727
+ * @template T - The document type
728
+ *
729
+ * @param options - Table-specific options including column definitions
730
+ * @returns ReactiveDataResult with table-optimized behavior
731
+ *
732
+ * @example
733
+ * ```tsx
734
+ * function UsersTable() {
735
+ * const {
736
+ * data,
737
+ * sort,
738
+ * setSort,
739
+ * selection,
740
+ * toggleSelect,
741
+ * } = useReactiveTable<User>({
742
+ * collection: 'users',
743
+ * columns: [
744
+ * { key: 'name', header: 'Name', sortable: true },
745
+ * { key: 'email', header: 'Email' },
746
+ * { key: 'role', header: 'Role', sortable: true },
747
+ * ],
748
+ * selectable: 'multi',
749
+ * })
750
+ *
751
+ * return (
752
+ * <Table
753
+ * columns={columns}
754
+ * data={data}
755
+ * sortBy={sort.field}
756
+ * sortDirection={sort.direction}
757
+ * onSort={setSort}
758
+ * selectedRows={Array.from(selection.selected)}
759
+ * onRowSelect={toggleSelect}
760
+ * />
761
+ * )
762
+ * }
763
+ * ```
764
+ */
765
+ export function useReactiveTable<T extends Record<string, any>>(
766
+ options: ReactiveTableOptions<T>
767
+ ): ReactiveDataResult<T> {
768
+ return useReactiveData<T>(options)
769
+ }
770
+
771
+ /**
772
+ * Hook for reactive list data with TanStack DB integration
773
+ *
774
+ * Specialized version of useReactiveData for List components with
775
+ * item-level selection and actions.
776
+ *
777
+ * @template T - The document type
778
+ *
779
+ * @param options - List-specific options
780
+ * @returns ReactiveDataResult with list-optimized behavior
781
+ *
782
+ * @example
783
+ * ```tsx
784
+ * function TaskList() {
785
+ * const {
786
+ * data,
787
+ * mutate,
788
+ * toggleSelect,
789
+ * } = useReactiveList<Task>({
790
+ * collection: 'tasks',
791
+ * where: { completed: false },
792
+ * orderBy: { priority: 'desc' },
793
+ * labelField: 'title',
794
+ * selectable: 'single',
795
+ * })
796
+ *
797
+ * return (
798
+ * <List
799
+ * items={data.map(task => ({
800
+ * id: task.id,
801
+ * content: task.title,
802
+ * icon: task.priority === 'high' ? 'alert' : 'task',
803
+ * }))}
804
+ * onItemClick={(id) => toggleSelect(id)}
805
+ * onDelete={(id) => mutate.delete({ id })}
806
+ * />
807
+ * )
808
+ * }
809
+ * ```
810
+ */
811
+ export function useReactiveList<T extends Record<string, any>>(
812
+ options: ReactiveListOptions<T>
813
+ ): ReactiveDataResult<T> {
814
+ return useReactiveData<T>(options)
815
+ }
816
+
817
+ /**
818
+ * Hook for reactive metrics computed from TanStack DB collections
819
+ *
820
+ * Aggregates collection data into metric values with optional trend
821
+ * computation and sparkline data.
822
+ *
823
+ * @template T - The document type
824
+ *
825
+ * @param options - Metrics configuration with aggregation definitions
826
+ * @returns ReactiveMetricsResult with computed metric values
827
+ *
828
+ * @example
829
+ * ```tsx
830
+ * function DashboardMetrics() {
831
+ * const { metrics, isLoading } = useReactiveMetrics<Order>({
832
+ * collection: 'orders',
833
+ * where: { status: 'completed' },
834
+ * metrics: [
835
+ * {
836
+ * key: 'total-orders',
837
+ * label: 'Total Orders',
838
+ * field: 'id',
839
+ * aggregate: 'count',
840
+ * },
841
+ * {
842
+ * key: 'revenue',
843
+ * label: 'Revenue',
844
+ * field: 'total',
845
+ * aggregate: 'sum',
846
+ * format: 'currency',
847
+ * },
848
+ * {
849
+ * key: 'avg-order',
850
+ * label: 'Avg Order',
851
+ * field: 'total',
852
+ * aggregate: 'avg',
853
+ * format: 'currency',
854
+ * },
855
+ * ],
856
+ * })
857
+ *
858
+ * if (isLoading) return <Spinner />
859
+ *
860
+ * return (
861
+ * <Metrics
862
+ * metrics={metrics.map(m => ({
863
+ * label: m.label,
864
+ * value: m.value,
865
+ * trend: m.trend,
866
+ * trendValue: m.trendValue,
867
+ * }))}
868
+ * />
869
+ * )
870
+ * }
871
+ * ```
872
+ */
873
+ export function useReactiveMetrics<T extends Record<string, any>>(
874
+ options: ReactiveMetricsOptions<T>
875
+ ): ReactiveMetricsResult {
876
+ const db = useDBContext()
877
+ const { collection: collectionName, where, metrics: metricDefs } = options
878
+
879
+ const [metrics, setMetrics] = useState<MetricValue[]>([])
880
+ const [isLoading, setIsLoading] = useState(true)
881
+ const [error, setError] = useState<Error | undefined>(undefined)
882
+
883
+ // Historical data for sparklines (last N values per metric)
884
+ const historyRef = useRef<Map<string, number[]>>(new Map())
885
+
886
+ const collection = db.collections[collectionName] as Collection<T> | undefined
887
+
888
+ const computeMetrics = useCallback(async () => {
889
+ if (!collection) {
890
+ setError(new Error(`Collection '${collectionName}' not found`))
891
+ setIsLoading(false)
892
+ return
893
+ }
894
+
895
+ try {
896
+ const data = await collection.findMany({ where })
897
+
898
+ const computed: MetricValue[] = metricDefs.map(def => {
899
+ const values: number[] = data.map(doc => {
900
+ const val = doc[def.field]
901
+ return typeof val === 'number' ? val : 0
902
+ })
903
+
904
+ let value: number
905
+ switch (def.aggregate) {
906
+ case 'count':
907
+ value = data.length
908
+ break
909
+ case 'sum':
910
+ value = values.reduce((a: number, b: number) => a + b, 0)
911
+ break
912
+ case 'avg':
913
+ value = values.length > 0 ? values.reduce((a: number, b: number) => a + b, 0) / values.length : 0
914
+ break
915
+ case 'min':
916
+ value = values.length > 0 ? Math.min(...values) : 0
917
+ break
918
+ case 'max':
919
+ value = values.length > 0 ? Math.max(...values) : 0
920
+ break
921
+ case 'latest':
922
+ value = values.length > 0 ? values[values.length - 1] : 0
923
+ break
924
+ default:
925
+ value = 0
926
+ }
927
+
928
+ // Update history for sparkline
929
+ const history = historyRef.current.get(def.key) || []
930
+ history.push(value)
931
+ if (history.length > 20) history.shift() // Keep last 20 values
932
+ historyRef.current.set(def.key, history)
933
+
934
+ // Compute trend
935
+ let trend: 'up' | 'down' | 'neutral' = 'neutral'
936
+ let trendValue: number | undefined
937
+ if (history.length >= 2) {
938
+ const prev = history[history.length - 2]
939
+ const current = history[history.length - 1]
940
+ if (prev !== 0) {
941
+ trendValue = ((current - prev) / prev) * 100
942
+ trend = trendValue > 0 ? 'up' : trendValue < 0 ? 'down' : 'neutral'
943
+ }
944
+ }
945
+
946
+ // Format value
947
+ let formattedValue: string | number = value
948
+ if (def.format === 'currency') {
949
+ formattedValue = `$${value.toLocaleString()}`
950
+ } else if (def.format === 'percentage') {
951
+ formattedValue = `${value.toFixed(1)}%`
952
+ }
953
+ if (def.unit) {
954
+ formattedValue = `${formattedValue} ${def.unit}`
955
+ }
956
+
957
+ return {
958
+ key: def.key,
959
+ label: def.label,
960
+ value: formattedValue,
961
+ trend,
962
+ trendValue,
963
+ sparkline: [...history],
964
+ }
965
+ })
966
+
967
+ setMetrics(computed)
968
+ setError(undefined)
969
+ } catch (err) {
970
+ setError(err instanceof Error ? err : new Error(String(err)))
971
+ } finally {
972
+ setIsLoading(false)
973
+ }
974
+ }, [collection, collectionName, JSON.stringify(where), JSON.stringify(metricDefs)])
975
+
976
+ // Initial computation
977
+ useEffect(() => {
978
+ computeMetrics()
979
+ }, [computeMetrics])
980
+
981
+ // Subscribe to changes
982
+ useEffect(() => {
983
+ if (!collection) return
984
+ const unsubscribe = collection.subscribe(() => {
985
+ computeMetrics()
986
+ })
987
+ return unsubscribe
988
+ }, [collection, computeMetrics])
989
+
990
+ return {
991
+ metrics,
992
+ isLoading,
993
+ error,
994
+ refetch: computeMetrics,
995
+ }
996
+ }
997
+
998
+ /**
999
+ * Hook for reactive single-record card data with TanStack DB integration
1000
+ *
1001
+ * Fetches and subscribes to a single record from a collection for
1002
+ * Card component display.
1003
+ *
1004
+ * @template T - The document type
1005
+ *
1006
+ * @param options - Card-specific options
1007
+ * @returns ReactiveCardResult with single record and update function
1008
+ *
1009
+ * @example
1010
+ * ```tsx
1011
+ * function UserProfileCard({ userId }: { userId: string }) {
1012
+ * const {
1013
+ * data,
1014
+ * isLoading,
1015
+ * update,
1016
+ * isUpdating,
1017
+ * } = useReactiveCard<User>({
1018
+ * collection: 'users',
1019
+ * where: { id: userId },
1020
+ * fields: [
1021
+ * { key: 'name', label: 'Name' },
1022
+ * { key: 'email', label: 'Email' },
1023
+ * { key: 'role', label: 'Role', format: 'badge' },
1024
+ * ],
1025
+ * })
1026
+ *
1027
+ * if (isLoading) return <Spinner />
1028
+ * if (!data) return <Empty message="User not found" />
1029
+ *
1030
+ * return (
1031
+ * <Card
1032
+ * title={data.name}
1033
+ * subtitle={data.email}
1034
+ * pairs={[
1035
+ * { key: 'Role', value: data.role },
1036
+ * { key: 'Status', value: data.status },
1037
+ * ]}
1038
+ * actions={[
1039
+ * {
1040
+ * label: isUpdating ? 'Saving...' : 'Edit',
1041
+ * action: () => update({ status: 'updated' }),
1042
+ * },
1043
+ * ]}
1044
+ * />
1045
+ * )
1046
+ * }
1047
+ * ```
1048
+ */
1049
+ export function useReactiveCard<T extends Record<string, any>>(
1050
+ options: ReactiveCardOptions<T>
1051
+ ): ReactiveCardResult<T> {
1052
+ const db = useDBContext()
1053
+ const { collection: collectionName, where, optimistic = true } = options
1054
+
1055
+ const [data, setData] = useState<T | null>(null)
1056
+ const [isLoading, setIsLoading] = useState(true)
1057
+ const [error, setError] = useState<Error | undefined>(undefined)
1058
+ const [isUpdating, setIsUpdating] = useState(false)
1059
+
1060
+ const rollbackRef = useRef<T | null>(null)
1061
+
1062
+ const collection = db.collections[collectionName] as Collection<T> | undefined
1063
+
1064
+ const fetchData = useCallback(async () => {
1065
+ if (!collection) {
1066
+ setError(new Error(`Collection '${collectionName}' not found`))
1067
+ setIsLoading(false)
1068
+ return
1069
+ }
1070
+
1071
+ try {
1072
+ const result = await collection.findOne(where as Partial<T>)
1073
+ setData(result)
1074
+ setError(undefined)
1075
+ } catch (err) {
1076
+ setError(err instanceof Error ? err : new Error(String(err)))
1077
+ } finally {
1078
+ setIsLoading(false)
1079
+ }
1080
+ }, [collection, collectionName, JSON.stringify(where)])
1081
+
1082
+ // Initial fetch
1083
+ useEffect(() => {
1084
+ fetchData()
1085
+ }, [fetchData])
1086
+
1087
+ // Subscribe to changes
1088
+ useEffect(() => {
1089
+ if (!collection) return
1090
+
1091
+ const unsubscribe = collection.subscribe((allData: T[]) => {
1092
+ // Find matching record
1093
+ const found = allData.find(doc =>
1094
+ Object.entries(where).every(([key, val]) => doc[key as keyof T] === val)
1095
+ )
1096
+ setData(found ?? null)
1097
+ })
1098
+
1099
+ return unsubscribe
1100
+ }, [collection, JSON.stringify(where)])
1101
+
1102
+ const update = useCallback(async (updates: Partial<T>) => {
1103
+ if (!collection || !data) {
1104
+ throw new Error('Cannot update: no data or collection')
1105
+ }
1106
+
1107
+ setIsUpdating(true)
1108
+
1109
+ if (optimistic) {
1110
+ rollbackRef.current = data
1111
+ setData(prev => prev ? { ...prev, ...updates } : null)
1112
+ }
1113
+
1114
+ try {
1115
+ await collection.update(where as Partial<T>, updates)
1116
+ } catch (err) {
1117
+ if (optimistic && rollbackRef.current) {
1118
+ setData(rollbackRef.current)
1119
+ }
1120
+ throw err
1121
+ } finally {
1122
+ setIsUpdating(false)
1123
+ }
1124
+ }, [collection, data, where, optimistic])
1125
+
1126
+ return {
1127
+ data,
1128
+ isLoading,
1129
+ error,
1130
+ refetch: fetchData,
1131
+ update,
1132
+ isUpdating,
1133
+ }
1134
+ }
1135
+
1136
+ // ============================================================================
1137
+ // Helper Functions
1138
+ // ============================================================================
1139
+
1140
+ /**
1141
+ * Check if a value matches a field filter
1142
+ */
1143
+ function matchesFilter<T>(value: T, filter: any): boolean {
1144
+ if (filter === null || filter === undefined || typeof filter !== 'object') {
1145
+ return value === filter
1146
+ }
1147
+
1148
+ const ops = filter as Record<string, any>
1149
+
1150
+ if ('$eq' in ops && value !== ops.$eq) return false
1151
+ if ('$ne' in ops && value === ops.$ne) return false
1152
+ if ('$gt' in ops && !(value > ops.$gt)) return false
1153
+ if ('$gte' in ops && !(value >= ops.$gte)) return false
1154
+ if ('$lt' in ops && !(value < ops.$lt)) return false
1155
+ if ('$lte' in ops && !(value <= ops.$lte)) return false
1156
+ if ('$in' in ops && !ops.$in.includes(value)) return false
1157
+ if ('$nin' in ops && ops.$nin.includes(value)) return false
1158
+
1159
+ return true
1160
+ }
1161
+
1162
+ /**
1163
+ * Check if a document matches a where clause
1164
+ */
1165
+ function matchesWhere<T extends Record<string, any>>(doc: T, where: WhereClause<T>): boolean {
1166
+ if ('$or' in where && where.$or) {
1167
+ const orClauses = where.$or as WhereClause<T>[]
1168
+ const matchesOr = orClauses.some(clause => matchesWhere(doc, clause))
1169
+ if (!matchesOr) return false
1170
+
1171
+ const { $or, ...rest } = where
1172
+ if (Object.keys(rest).length === 0) return true
1173
+ return matchesWhere(doc, rest as WhereClause<T>)
1174
+ }
1175
+
1176
+ for (const [key, filter] of Object.entries(where)) {
1177
+ if (key === '$or') continue
1178
+ const value = doc[key]
1179
+ if (!matchesFilter(value, filter)) {
1180
+ return false
1181
+ }
1182
+ }
1183
+
1184
+ return true
1185
+ }
1186
+
1187
+ /**
1188
+ * Sort documents by orderBy clause
1189
+ */
1190
+ function sortDocuments<T extends Record<string, any>>(
1191
+ docs: T[],
1192
+ orderBy: OrderByClause<T> | OrderByClause<T>[]
1193
+ ): T[] {
1194
+ const orderClauses = Array.isArray(orderBy) ? orderBy : [orderBy]
1195
+
1196
+ return [...docs].sort((a, b) => {
1197
+ for (const clause of orderClauses) {
1198
+ const [field, direction] = Object.entries(clause)[0]
1199
+ const aVal = a[field]
1200
+ const bVal = b[field]
1201
+
1202
+ if (aVal === null || aVal === undefined) {
1203
+ if (bVal !== null && bVal !== undefined) return 1
1204
+ continue
1205
+ }
1206
+ if (bVal === null || bVal === undefined) return -1
1207
+
1208
+ let comparison = 0
1209
+ if (typeof aVal === 'string') {
1210
+ comparison = aVal.localeCompare(bVal)
1211
+ } else if (typeof aVal === 'number') {
1212
+ comparison = aVal - bVal
1213
+ } else if (aVal instanceof Date) {
1214
+ comparison = aVal.getTime() - bVal.getTime()
1215
+ } else {
1216
+ comparison = String(aVal).localeCompare(String(bVal))
1217
+ }
1218
+
1219
+ if (comparison !== 0) {
1220
+ return direction === 'desc' ? -comparison : comparison
1221
+ }
1222
+ }
1223
+ return 0
1224
+ })
1225
+ }