@contember/bindx-dataview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/DataGrid.d.ts +38 -0
  2. package/dist/DataGrid.d.ts.map +1 -0
  3. package/dist/DataViewContext.d.ts +39 -0
  4. package/dist/DataViewContext.d.ts.map +1 -0
  5. package/dist/DataViewKeyProvider.d.ts +8 -0
  6. package/dist/DataViewKeyProvider.d.ts.map +1 -0
  7. package/dist/HasManyDataGrid.d.ts +51 -0
  8. package/dist/HasManyDataGrid.d.ts.map +1 -0
  9. package/dist/columnLeaf.d.ts +72 -0
  10. package/dist/columnLeaf.d.ts.map +1 -0
  11. package/dist/columnTypes.d.ts +35 -0
  12. package/dist/columnTypes.d.ts.map +1 -0
  13. package/dist/columns.d.ts +115 -0
  14. package/dist/columns.d.ts.map +1 -0
  15. package/dist/contextHooks.d.ts +45 -0
  16. package/dist/contextHooks.d.ts.map +1 -0
  17. package/dist/createColumn.d.ts +37 -0
  18. package/dist/createColumn.d.ts.map +1 -0
  19. package/dist/createRelationColumn.d.ts +57 -0
  20. package/dist/createRelationColumn.d.ts.map +1 -0
  21. package/dist/dataAttribute.d.ts +6 -0
  22. package/dist/dataAttribute.d.ts.map +1 -0
  23. package/dist/export.d.ts +46 -0
  24. package/dist/export.d.ts.map +1 -0
  25. package/dist/filterComponents.d.ts +127 -0
  26. package/dist/filterComponents.d.ts.map +1 -0
  27. package/dist/filterContext.d.ts +10 -0
  28. package/dist/filterContext.d.ts.map +1 -0
  29. package/dist/filterHooks.d.ts +74 -0
  30. package/dist/filterHooks.d.ts.map +1 -0
  31. package/dist/filterScope.d.ts +54 -0
  32. package/dist/filterScope.d.ts.map +1 -0
  33. package/dist/filterWrappers.d.ts +66 -0
  34. package/dist/filterWrappers.d.ts.map +1 -0
  35. package/dist/index.d.ts +28 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/infiniteLoad.d.ts +32 -0
  38. package/dist/infiniteLoad.d.ts.map +1 -0
  39. package/dist/loaderComponents.d.ts +75 -0
  40. package/dist/loaderComponents.d.ts.map +1 -0
  41. package/dist/markers.d.ts +36 -0
  42. package/dist/markers.d.ts.map +1 -0
  43. package/dist/pagingComponents.d.ts +50 -0
  44. package/dist/pagingComponents.d.ts.map +1 -0
  45. package/dist/renderCellValue.d.ts +9 -0
  46. package/dist/renderCellValue.d.ts.map +1 -0
  47. package/dist/select/MultiSelect.d.ts +27 -0
  48. package/dist/select/MultiSelect.d.ts.map +1 -0
  49. package/dist/select/Select.d.ts +27 -0
  50. package/dist/select/Select.d.ts.map +1 -0
  51. package/dist/select/SelectDataView.d.ts +46 -0
  52. package/dist/select/SelectDataView.d.ts.map +1 -0
  53. package/dist/select/SelectEachValue.d.ts +20 -0
  54. package/dist/select/SelectEachValue.d.ts.map +1 -0
  55. package/dist/select/SelectItemTrigger.d.ts +25 -0
  56. package/dist/select/SelectItemTrigger.d.ts.map +1 -0
  57. package/dist/select/SelectOption.d.ts +23 -0
  58. package/dist/select/SelectOption.d.ts.map +1 -0
  59. package/dist/select/SelectPlaceholder.d.ts +16 -0
  60. package/dist/select/SelectPlaceholder.d.ts.map +1 -0
  61. package/dist/select/index.d.ts +9 -0
  62. package/dist/select/index.d.ts.map +1 -0
  63. package/dist/select/selectContext.d.ts +20 -0
  64. package/dist/select/selectContext.d.ts.map +1 -0
  65. package/dist/selectionComponents.d.ts +62 -0
  66. package/dist/selectionComponents.d.ts.map +1 -0
  67. package/dist/sortingComponents.d.ts +39 -0
  68. package/dist/sortingComponents.d.ts.map +1 -0
  69. package/dist/stateStorage.d.ts +20 -0
  70. package/dist/stateStorage.d.ts.map +1 -0
  71. package/dist/useDataGridSetup.d.ts +53 -0
  72. package/dist/useDataGridSetup.d.ts.map +1 -0
  73. package/dist/useDataView.d.ts +57 -0
  74. package/dist/useDataView.d.ts.map +1 -0
  75. package/dist/useDataViewState.d.ts +89 -0
  76. package/dist/useDataViewState.d.ts.map +1 -0
  77. package/package.json +33 -0
  78. package/src/DataGrid.tsx +173 -0
  79. package/src/DataViewContext.tsx +56 -0
  80. package/src/DataViewKeyProvider.tsx +16 -0
  81. package/src/HasManyDataGrid.tsx +319 -0
  82. package/src/columnLeaf.ts +148 -0
  83. package/src/columnTypes.ts +160 -0
  84. package/src/columns.tsx +306 -0
  85. package/src/contextHooks.ts +157 -0
  86. package/src/createColumn.ts +128 -0
  87. package/src/createRelationColumn.tsx +203 -0
  88. package/src/dataAttribute.ts +7 -0
  89. package/src/export.tsx +219 -0
  90. package/src/filterComponents.tsx +617 -0
  91. package/src/filterContext.ts +27 -0
  92. package/src/filterHooks.ts +386 -0
  93. package/src/filterScope.tsx +89 -0
  94. package/src/filterWrappers.tsx +187 -0
  95. package/src/index.ts +356 -0
  96. package/src/infiniteLoad.tsx +188 -0
  97. package/src/loaderComponents.tsx +239 -0
  98. package/src/markers.ts +54 -0
  99. package/src/pagingComponents.tsx +149 -0
  100. package/src/select/MultiSelect.tsx +88 -0
  101. package/src/select/Select.tsx +88 -0
  102. package/src/select/SelectDataView.tsx +231 -0
  103. package/src/select/SelectEachValue.tsx +34 -0
  104. package/src/select/SelectItemTrigger.tsx +51 -0
  105. package/src/select/SelectOption.tsx +40 -0
  106. package/src/select/SelectPlaceholder.tsx +22 -0
  107. package/src/select/index.ts +17 -0
  108. package/src/select/selectContext.ts +82 -0
  109. package/src/selectionComponents.tsx +160 -0
  110. package/src/sortingComponents.tsx +104 -0
  111. package/src/stateStorage.ts +188 -0
  112. package/src/useDataGridSetup.ts +230 -0
  113. package/src/useDataView.ts +148 -0
  114. package/src/useDataViewState.ts +404 -0
package/src/index.ts ADDED
@@ -0,0 +1,356 @@
1
+ // Core DataGrid
2
+ export { DataGrid, type DataGridProps, QUERY_FILTER_NAME } from './DataGrid.js'
3
+ export { HasManyDataGrid, type HasManyDataGridProps } from './HasManyDataGrid.js'
4
+
5
+ // Column type definitions (Layer 1: headless behavior)
6
+ export {
7
+ defineColumnType,
8
+ type ColumnTypeDef,
9
+ textColumnDef,
10
+ numberColumnDef,
11
+ dateColumnDef,
12
+ dateTimeColumnDef,
13
+ booleanColumnDef,
14
+ enumColumnDef,
15
+ enumListColumnDef,
16
+ uuidColumnDef,
17
+ isDefinedColumnDef,
18
+ hasOneColumnDef,
19
+ hasManyColumnDef,
20
+ accessField,
21
+ } from './columnTypes.js'
22
+
23
+ // ColumnLeaf, extraction, and children analysis
24
+ export {
25
+ ColumnLeaf,
26
+ type ColumnLeafProps,
27
+ extractColumnLeaves,
28
+ analyzeChildren,
29
+ type ChildrenAnalysisResult,
30
+ } from './columnLeaf.js'
31
+
32
+ // Marker components (toolbar content, layout)
33
+ export {
34
+ DataGridToolbarContent,
35
+ type DataGridToolbarContentProps,
36
+ DataGridLayout,
37
+ type DataGridLayoutProps,
38
+ } from './markers.js'
39
+
40
+ // createColumn factory (Layer 2: UI wrapping)
41
+ export {
42
+ createColumn,
43
+ type ColumnRenderProps,
44
+ type FilterRenderProps,
45
+ type CreateColumnConfig,
46
+ type ColumnComponentProps,
47
+ type ColumnComponent,
48
+ } from './createColumn.js'
49
+
50
+ // createRelationColumn factory (Layer 2: relation columns)
51
+ export {
52
+ createRelationColumn,
53
+ hasOneCellConfig,
54
+ hasManyCellConfig,
55
+ type RelationColumnConfig,
56
+ type RelationFilterContext,
57
+ type RelationCellWrapperContext,
58
+ type RelationColumnProps,
59
+ type RelationColumnComponent,
60
+ } from './createRelationColumn.jsx'
61
+
62
+ // Column components
63
+ export {
64
+ DataGridTextColumn,
65
+ DataGridNumberColumn,
66
+ DataGridDateColumn,
67
+ DataGridDateTimeColumn,
68
+ DataGridBooleanColumn,
69
+ DataGridEnumColumn,
70
+ DataGridEnumListColumn,
71
+ DataGridUuidColumn,
72
+ DataGridIsDefinedColumn,
73
+ DataGridHasOneColumn,
74
+ DataGridHasManyColumn,
75
+ DataGridActionColumn,
76
+ DataGridColumn,
77
+ type DataGridTextColumnProps,
78
+ type DataGridNumberColumnProps,
79
+ type DataGridDateColumnProps,
80
+ type DataGridDateTimeColumnProps,
81
+ type DataGridBooleanColumnProps,
82
+ type DataGridEnumColumnProps,
83
+ type DataGridEnumListColumnProps,
84
+ type DataGridUuidColumnProps,
85
+ type DataGridIsDefinedColumnProps,
86
+ type DataGridHasOneColumnProps,
87
+ type DataGridHasManyColumnProps,
88
+ type DataGridActionColumnProps,
89
+ type DataGridColumnProps,
90
+ type ColumnMeta,
91
+ extractFieldName,
92
+ extractRelatedEntityName,
93
+ hasFieldRefMeta,
94
+ getRelatedAccessor,
95
+ } from './columns.js'
96
+
97
+ // State hooks
98
+ export {
99
+ useFilteringState,
100
+ useSortingState,
101
+ usePagingState,
102
+ useSelectionState,
103
+ type FilteringState,
104
+ type RegisteredFilter,
105
+ type UseFilteringOptions,
106
+ type UseSortingOptions,
107
+ type SortingStateResult,
108
+ type UsePagingOptions,
109
+ type PagingStateResult,
110
+ type UseSelectionOptions,
111
+ type SelectionStateResult,
112
+ } from './useDataViewState.js'
113
+
114
+ // Context
115
+ export {
116
+ useDataViewContext,
117
+ useOptionalDataViewContext,
118
+ DataViewProvider,
119
+ type DataViewContextValue,
120
+ type DataViewItem,
121
+ type DataViewLoaderState as DataViewLoaderStateType,
122
+ type DataViewElementData,
123
+ } from './DataViewContext.js'
124
+
125
+ // Low-level hook
126
+ export { useDataView, type UseDataViewOptions, type DataViewResult } from './useDataView.js'
127
+
128
+ // Data attribute helper
129
+ export { dataAttribute } from './dataAttribute.js'
130
+
131
+ // Filter name context
132
+ export {
133
+ useDataViewFilterName,
134
+ useOptionalDataViewFilterName,
135
+ DataViewFilterNameProvider,
136
+ } from './filterContext.js'
137
+
138
+ // Filter scope and registration components
139
+ export {
140
+ DataViewFilterScope,
141
+ type DataViewFilterScopeProps,
142
+ DataViewFilter,
143
+ type DataViewFilterProps,
144
+ DataViewHasFilterType,
145
+ type DataViewHasFilterTypeProps,
146
+ } from './filterScope.js'
147
+
148
+ // Filter wrapper components (typed name scoping)
149
+ export {
150
+ DataViewTextFilter,
151
+ type DataViewTextFilterProps,
152
+ DataViewBooleanFilter,
153
+ type DataViewBooleanFilterProps,
154
+ DataViewNumberFilter,
155
+ type DataViewNumberFilterProps,
156
+ DataViewDateFilter,
157
+ type DataViewDateFilterProps,
158
+ DataViewEnumFilter,
159
+ type DataViewEnumFilterProps,
160
+ DataViewEnumListFilter,
161
+ type DataViewEnumListFilterProps,
162
+ DataViewHasOneFilter,
163
+ type DataViewHasOneFilterProps,
164
+ DataViewHasManyFilter,
165
+ type DataViewHasManyFilterProps,
166
+ DataViewIsDefinedFilter,
167
+ type DataViewIsDefinedFilterProps,
168
+ DataViewUnionTextFilter,
169
+ type DataViewUnionTextFilterProps,
170
+ } from './filterWrappers.js'
171
+
172
+ // Filter hooks
173
+ export {
174
+ useDataViewFilter,
175
+ type UseDataViewFilterResult,
176
+ useDataViewBooleanFilter,
177
+ useDataViewBooleanFilterFactory,
178
+ type DataViewSetBooleanFilterAction,
179
+ type DataViewBooleanFilterCurrent,
180
+ type UseDataViewBooleanFilterResult,
181
+ useDataViewEnumFilter,
182
+ useDataViewEnumFilterFactory,
183
+ type DataViewSetEnumFilterAction,
184
+ type DataViewEnumFilterCurrent,
185
+ type UseDataViewEnumFilterResult,
186
+ useDataViewRelationFilter,
187
+ useDataViewRelationFilterFactory,
188
+ type DataViewSetRelationFilterAction,
189
+ type DataViewRelationFilterCurrent,
190
+ type UseDataViewRelationFilterResult,
191
+ useDataViewTextFilterInput,
192
+ type UseDataViewTextFilterInputResult,
193
+ useDataViewTextFilterMatchMode,
194
+ type UseDataViewTextFilterMatchModeResult,
195
+ useDataViewNumberFilterInput,
196
+ type UseDataViewNumberFilterInputResult,
197
+ type UseDataViewNumberFilterInputProps,
198
+ useDataViewNullFilter,
199
+ type DataViewSetNullFilterAction,
200
+ type DataViewNullFilterState,
201
+ type UseDataViewNullFilterResult,
202
+ } from './filterHooks.js'
203
+
204
+ // Composable filter components
205
+ export {
206
+ type DataViewFilterState,
207
+ DataViewTextFilterInput,
208
+ type DataViewTextFilterInputProps,
209
+ DataViewTextFilterMatchModeTrigger,
210
+ type DataViewTextFilterMatchModeTriggerProps,
211
+ DataViewTextFilterMatchModeLabel,
212
+ type DataViewTextFilterMatchModeLabelProps,
213
+ DataViewTextFilterResetTrigger,
214
+ type DataViewTextFilterResetTriggerProps,
215
+ DataViewNumberFilterInput,
216
+ type DataViewNumberFilterInputProps,
217
+ DataViewDateFilterInput,
218
+ type DataViewDateFilterInputProps,
219
+ DataViewBooleanFilterTrigger,
220
+ type DataViewBooleanFilterTriggerProps,
221
+ DataViewEnumFilterTrigger,
222
+ type DataViewEnumFilterTriggerProps,
223
+ DataViewEnumFilterState,
224
+ type DataViewEnumFilterStateProps,
225
+ DataViewRelationFilterTrigger,
226
+ type DataViewRelationFilterTriggerProps,
227
+ DataViewRelationFilterState,
228
+ type DataViewRelationFilterStateProps,
229
+ DataViewNullFilterTrigger,
230
+ type DataViewNullFilterTriggerProps,
231
+ DataViewFilterResetTrigger,
232
+ type DataViewFilterResetTriggerProps,
233
+ DataViewDateFilterResetTrigger,
234
+ type DataViewDateFilterResetTriggerProps,
235
+ DataViewNumberFilterResetTrigger,
236
+ type DataViewNumberFilterResetTriggerProps,
237
+ } from './filterComponents.js'
238
+
239
+ // Sorting components
240
+ export {
241
+ DataViewSortingTrigger,
242
+ type DataViewSortingTriggerProps,
243
+ DataViewSortingSwitch,
244
+ type DataViewSortingSwitchProps,
245
+ } from './sortingComponents.js'
246
+
247
+ // Paging components
248
+ export {
249
+ DataViewChangePageTrigger,
250
+ type DataViewChangePageTriggerProps,
251
+ DataViewSetItemsPerPageTrigger,
252
+ type DataViewSetItemsPerPageTriggerProps,
253
+ DataViewPagingStateView,
254
+ type DataViewPagingStateViewProps,
255
+ DataViewPagingInfo,
256
+ type DataViewPagingInfoProps,
257
+ } from './pagingComponents.js'
258
+
259
+ // Selection (visibility + layout) components
260
+ export {
261
+ DataViewVisibilityTrigger,
262
+ type DataViewVisibilityTriggerProps,
263
+ DataViewElement,
264
+ DataViewIsVisible,
265
+ type DataViewElementProps,
266
+ DataViewLayout,
267
+ type DataViewLayoutProps,
268
+ DataViewLayoutTrigger,
269
+ type DataViewLayoutTriggerProps,
270
+ } from './selectionComponents.js'
271
+
272
+ // Export
273
+ export {
274
+ DataViewExportTrigger,
275
+ type DataViewExportTriggerProps,
276
+ CsvExportFactory,
277
+ type ExportFactory,
278
+ type ExportResult,
279
+ type ExportFactoryArgs,
280
+ useDataViewFetchAllData,
281
+ type FetchAllDataResult,
282
+ } from './export.js'
283
+
284
+ // Infinite scroll
285
+ export {
286
+ DataViewInfiniteLoadProvider,
287
+ type DataViewInfiniteLoadProviderProps,
288
+ DataViewInfiniteLoadTrigger,
289
+ type DataViewInfiniteLoadTriggerProps,
290
+ DataViewInfiniteLoadScrollObserver,
291
+ type DataViewInfiniteLoadScrollObserverProps,
292
+ useDataViewInfiniteLoadTrigger,
293
+ } from './infiniteLoad.js'
294
+
295
+ // Loader state, reload, and conditional rendering
296
+ export {
297
+ DataViewReloadTrigger,
298
+ type DataViewReloadTriggerProps,
299
+ DataViewLoaderState,
300
+ type DataViewLoaderStateProps,
301
+ DataViewLoaderStateSwitch,
302
+ type DataViewLoaderStateSwitchProps,
303
+ useDataViewLoaderState,
304
+ useDataViewReload,
305
+ useDataViewHighlightIndex,
306
+ DataViewEmpty,
307
+ type DataViewEmptyProps,
308
+ DataViewNonEmpty,
309
+ type DataViewNonEmptyProps,
310
+ DataViewEachRow,
311
+ type DataViewEachRowProps,
312
+ DataViewHighlightRow,
313
+ type DataViewHighlightRowProps,
314
+ DataViewKeyboardEventHandler,
315
+ type DataViewKeyboardEventHandlerProps,
316
+ } from './loaderComponents.js'
317
+
318
+ // Fine-grained context hooks
319
+ export {
320
+ useDataViewSortingState,
321
+ useDataViewSortingMethods,
322
+ useDataViewSortingDirection,
323
+ type DataViewSortingMethods,
324
+ useDataViewPagingState,
325
+ useDataViewPagingInfo,
326
+ useDataViewPagingMethods,
327
+ type DataViewPagingMethods,
328
+ useDataViewFilteringState,
329
+ useDataViewFilteringMethods,
330
+ useDataViewFilterHandlerRegistry,
331
+ type DataViewFilteringMethods,
332
+ useDataViewSelectionState,
333
+ useDataViewSelectionMethods,
334
+ type DataViewSelectionState,
335
+ type DataViewSelectionMethods,
336
+ useDataViewElements,
337
+ } from './contextHooks.js'
338
+
339
+ // State storage
340
+ export {
341
+ type StateStorage,
342
+ type StateStorageOrName,
343
+ type SetStoredState,
344
+ useStoredState,
345
+ buildDataViewKey,
346
+ } from './stateStorage.js'
347
+
348
+ // DataView key context
349
+ export {
350
+ DataViewKeyProvider,
351
+ type DataViewKeyProviderProps,
352
+ useDataViewKey,
353
+ } from './DataViewKeyProvider.js'
354
+
355
+ // Select
356
+ export * from './select/index.js'
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Infinite scroll / load-more components using Radix Slot.
3
+ *
4
+ * Usage:
5
+ * ```tsx
6
+ * <DataViewInfiniteLoadProvider>
7
+ * <DataViewEachRow>
8
+ * {(item) => <div>{item.title}</div>}
9
+ * </DataViewEachRow>
10
+ * <DataViewInfiniteLoadTrigger>
11
+ * <button>Load More</button>
12
+ * </DataViewInfiniteLoadTrigger>
13
+ * <DataViewInfiniteLoadScrollObserver />
14
+ * </DataViewInfiniteLoadProvider>
15
+ * ```
16
+ */
17
+
18
+ import React, { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
19
+ import { Slot } from '@radix-ui/react-slot'
20
+ import { composeEventHandlers } from '@radix-ui/primitive'
21
+ import { useDataViewContext } from './DataViewContext.js'
22
+
23
+ // ============================================================================
24
+ // Context
25
+ // ============================================================================
26
+
27
+ interface InfiniteLoadContextValue {
28
+ readonly pages: readonly PageData[]
29
+ readonly loadMore: (() => void) | undefined
30
+ readonly isLoadingMore: boolean
31
+ }
32
+
33
+ interface PageData {
34
+ readonly items: readonly { id: string; data: Record<string, unknown> }[]
35
+ }
36
+
37
+ const InfiniteLoadContext = createContext<InfiniteLoadContextValue | null>(null)
38
+
39
+ function useInfiniteLoadContext(): InfiniteLoadContextValue {
40
+ const ctx = useContext(InfiniteLoadContext)
41
+ if (!ctx) {
42
+ throw new Error('useInfiniteLoadContext must be used within DataViewInfiniteLoadProvider')
43
+ }
44
+ return ctx
45
+ }
46
+
47
+ // ============================================================================
48
+ // Provider
49
+ // ============================================================================
50
+
51
+ export interface DataViewInfiniteLoadProviderProps {
52
+ children: React.ReactNode
53
+ }
54
+
55
+ export function DataViewInfiniteLoadProvider({
56
+ children,
57
+ }: DataViewInfiniteLoadProviderProps): ReactElement {
58
+ const { paging, filtering, sorting } = useDataViewContext()
59
+ const [pages, setPages] = useState<PageData[]>([])
60
+ const [isLoadingMore, setIsLoadingMore] = useState(false)
61
+
62
+ const filterKey = JSON.stringify(filtering.resolvedWhere ?? null)
63
+ const sortKey = JSON.stringify(sorting.resolvedOrderBy ?? null)
64
+
65
+ const previousParams = useRef<{
66
+ filterKey: string
67
+ sortKey: string
68
+ limit: number | undefined
69
+ offset: number | undefined
70
+ } | null>(null)
71
+
72
+ const currentParams = useMemo(() => ({
73
+ filterKey,
74
+ sortKey,
75
+ limit: paging.queryLimit,
76
+ offset: paging.queryOffset,
77
+ }), [filterKey, sortKey, paging.queryLimit, paging.queryOffset])
78
+
79
+ useEffect(() => {
80
+ const prev = previousParams.current
81
+ if (!prev) {
82
+ setPages([])
83
+ } else if (
84
+ prev.filterKey === currentParams.filterKey
85
+ && prev.sortKey === currentParams.sortKey
86
+ && prev.limit === currentParams.limit
87
+ && prev.limit !== undefined
88
+ && prev.offset !== undefined
89
+ && currentParams.offset === prev.offset + prev.limit
90
+ ) {
91
+ setIsLoadingMore(true)
92
+ } else {
93
+ setPages([])
94
+ }
95
+ previousParams.current = currentParams
96
+ }, [currentParams])
97
+
98
+ const hasNext = paging.hasNext
99
+
100
+ const loadMore = useMemo((): (() => void) | undefined => {
101
+ if (!hasNext) return undefined
102
+ return () => paging.next()
103
+ }, [hasNext, paging])
104
+
105
+ const contextValue = useMemo((): InfiniteLoadContextValue => ({
106
+ pages,
107
+ loadMore,
108
+ isLoadingMore,
109
+ }), [pages, loadMore, isLoadingMore])
110
+
111
+ return (
112
+ <InfiniteLoadContext.Provider value={contextValue}>
113
+ {children}
114
+ </InfiniteLoadContext.Provider>
115
+ )
116
+ }
117
+
118
+ // ============================================================================
119
+ // Load More Trigger — Slot-based
120
+ // ============================================================================
121
+
122
+ export interface DataViewInfiniteLoadTriggerProps {
123
+ children: ReactElement
124
+ }
125
+
126
+ export const DataViewInfiniteLoadTrigger = forwardRef<HTMLButtonElement, DataViewInfiniteLoadTriggerProps>(
127
+ (props, ref) => {
128
+ const { loadMore, isLoadingMore } = useInfiniteLoadContext()
129
+
130
+ if (!loadMore) return null
131
+
132
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement> & { children: ReactElement }
133
+
134
+ return (
135
+ <Slot
136
+ ref={ref}
137
+ onClick={composeEventHandlers(onClick, loadMore)}
138
+ disabled={isLoadingMore}
139
+ data-state={isLoadingMore ? 'loading' : 'idle'}
140
+ {...otherProps}
141
+ />
142
+ )
143
+ },
144
+ )
145
+ DataViewInfiniteLoadTrigger.displayName = 'DataViewInfiniteLoadTrigger'
146
+
147
+ // ============================================================================
148
+ // Scroll Observer (auto-trigger)
149
+ // ============================================================================
150
+
151
+ export interface DataViewInfiniteLoadScrollObserverProps {
152
+ /** Root margin for IntersectionObserver. Default: '200px' */
153
+ rootMargin?: string
154
+ }
155
+
156
+ export function DataViewInfiniteLoadScrollObserver({
157
+ rootMargin = '200px',
158
+ }: DataViewInfiniteLoadScrollObserverProps = {}): ReactElement {
159
+ const { loadMore, isLoadingMore } = useInfiniteLoadContext()
160
+ const sentinelRef = useRef<HTMLSpanElement>(null)
161
+
162
+ useEffect(() => {
163
+ const el = sentinelRef.current
164
+ if (!el || !loadMore || isLoadingMore) return
165
+
166
+ const observer = new IntersectionObserver(
167
+ ([entry]) => {
168
+ if (entry?.isIntersecting && loadMore) {
169
+ loadMore()
170
+ }
171
+ },
172
+ { rootMargin },
173
+ )
174
+
175
+ observer.observe(el)
176
+ return () => observer.disconnect()
177
+ }, [loadMore, isLoadingMore, rootMargin])
178
+
179
+ return <span ref={sentinelRef} data-testid="infinite-load-sentinel" />
180
+ }
181
+
182
+ // ============================================================================
183
+ // Hooks
184
+ // ============================================================================
185
+
186
+ export function useDataViewInfiniteLoadTrigger(): (() => void) | undefined {
187
+ return useInfiniteLoadContext().loadMore
188
+ }