@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
@@ -0,0 +1,617 @@
1
+ /**
2
+ * Composable filter UI components using Radix Slot pattern.
3
+ *
4
+ * All trigger components use `@radix-ui/react-slot` for proper prop merging
5
+ * and `forwardRef` for ref forwarding. Filter names can be inferred from
6
+ * context (via DataViewTextFilter, DataViewFilterScope, etc.) or passed explicitly.
7
+ *
8
+ * Usage:
9
+ * ```tsx
10
+ * // With explicit name
11
+ * <DataViewTextFilterInput name="title">
12
+ * <input placeholder="Search..." />
13
+ * </DataViewTextFilterInput>
14
+ *
15
+ * // With context-inferred name
16
+ * <DataViewTextFilter field="title">
17
+ * <DataViewTextFilterInput>
18
+ * <input placeholder="Search..." />
19
+ * </DataViewTextFilterInput>
20
+ * <DataViewTextFilterMatchModeTrigger mode="contains">
21
+ * <button>Contains</button>
22
+ * </DataViewTextFilterMatchModeTrigger>
23
+ * </DataViewTextFilter>
24
+ * ```
25
+ */
26
+
27
+ import React, { forwardRef, type ReactElement, type ReactNode, useCallback } from 'react'
28
+ import { Slot } from '@radix-ui/react-slot'
29
+ import { composeEventHandlers } from '@radix-ui/primitive'
30
+ import { useOptionalDataViewFilterName } from './filterContext.js'
31
+ import { dataAttribute } from './dataAttribute.js'
32
+ import {
33
+ useDataViewFilter,
34
+ useDataViewBooleanFilter,
35
+ useDataViewEnumFilter,
36
+ useDataViewRelationFilter,
37
+ useDataViewTextFilterInput,
38
+ useDataViewTextFilterMatchMode,
39
+ useDataViewNullFilter,
40
+ type UseDataViewFilterResult,
41
+ type DataViewSetBooleanFilterAction,
42
+ type DataViewBooleanFilterCurrent,
43
+ type DataViewSetEnumFilterAction,
44
+ type DataViewEnumFilterCurrent,
45
+ type DataViewSetRelationFilterAction,
46
+ type DataViewRelationFilterCurrent,
47
+ type DataViewSetNullFilterAction,
48
+ type DataViewNullFilterState,
49
+ } from './filterHooks.js'
50
+ import type {
51
+ FilterArtifact,
52
+ TextFilterArtifact,
53
+ NumberRangeFilterArtifact,
54
+ DateFilterArtifact,
55
+ EnumFilterArtifact,
56
+ } from '@contember/bindx'
57
+ import { useDataViewContext } from './DataViewContext.js'
58
+
59
+ // Re-export the hook and types from filterHooks (backwards compat)
60
+ export {
61
+ useDataViewFilter,
62
+ } from './filterHooks.js'
63
+ export type { UseDataViewFilterResult as DataViewFilterState } from './filterHooks.js'
64
+
65
+ function resolveFilterName(name: string | undefined): string {
66
+ if (name !== undefined) return name
67
+ // eslint-disable-next-line react-hooks/rules-of-hooks
68
+ const contextName = useOptionalDataViewFilterName()
69
+ if (contextName !== null) return contextName
70
+ throw new Error('Filter name must be provided via `name` prop or filter scope context')
71
+ }
72
+
73
+ // ============================================================================
74
+ // Text Filter Input — Slot-based
75
+ // ============================================================================
76
+
77
+ const SlotInput = Slot as React.ForwardRefExoticComponent<
78
+ React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>
79
+ >
80
+
81
+ export interface DataViewTextFilterInputProps {
82
+ name?: string
83
+ debounceMs?: number
84
+ children: ReactElement
85
+ }
86
+
87
+ export const DataViewTextFilterInput = forwardRef<HTMLInputElement, DataViewTextFilterInputProps>(
88
+ ({ name, debounceMs, ...props }, ref) => {
89
+ name = resolveFilterName(name)
90
+ return (
91
+ <SlotInput
92
+ {...useDataViewTextFilterInput({ name, debounceMs })}
93
+ {...props}
94
+ ref={ref}
95
+ />
96
+ )
97
+ },
98
+ )
99
+ DataViewTextFilterInput.displayName = 'DataViewTextFilterInput'
100
+
101
+ // ============================================================================
102
+ // Text Filter Match Mode Trigger — Slot-based
103
+ // ============================================================================
104
+
105
+ export interface DataViewTextFilterMatchModeTriggerProps {
106
+ name?: string
107
+ mode: TextFilterArtifact['mode']
108
+ children: ReactElement
109
+ }
110
+
111
+ export const DataViewTextFilterMatchModeTrigger = forwardRef<HTMLButtonElement, DataViewTextFilterMatchModeTriggerProps>(
112
+ ({ name, mode, ...props }, ref) => {
113
+ name = resolveFilterName(name)
114
+ const [active, cb] = useDataViewTextFilterMatchMode(name, mode)
115
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
116
+
117
+ return (
118
+ <Slot
119
+ ref={ref}
120
+ onClick={composeEventHandlers(onClick, cb)}
121
+ data-active={dataAttribute(active)}
122
+ {...otherProps}
123
+ />
124
+ )
125
+ },
126
+ )
127
+ DataViewTextFilterMatchModeTrigger.displayName = 'DataViewTextFilterMatchModeTrigger'
128
+
129
+ // ============================================================================
130
+ // Text Filter Match Mode Label
131
+ // ============================================================================
132
+
133
+ export interface DataViewTextFilterMatchModeLabelProps {
134
+ name?: string
135
+ }
136
+
137
+ const TEXT_MODE_LABELS: Record<TextFilterArtifact['mode'], string> = {
138
+ contains: 'Contains',
139
+ startsWith: 'Starts with',
140
+ endsWith: 'Ends with',
141
+ equals: 'Equals',
142
+ notContains: 'Not contains',
143
+ }
144
+
145
+ export function DataViewTextFilterMatchModeLabel({
146
+ name,
147
+ }: DataViewTextFilterMatchModeLabelProps): ReactElement {
148
+ name = resolveFilterName(name)
149
+ const [state] = useDataViewFilter<TextFilterArtifact>(name)
150
+ const mode = state?.mode ?? 'contains'
151
+ return <>{TEXT_MODE_LABELS[mode]}</>
152
+ }
153
+
154
+ // ============================================================================
155
+ // Text Filter Reset Trigger
156
+ // ============================================================================
157
+
158
+ export interface DataViewTextFilterResetTriggerProps {
159
+ name?: string
160
+ children: ReactElement
161
+ }
162
+
163
+ export const DataViewTextFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewTextFilterResetTriggerProps>(
164
+ ({ name, ...props }, ref) => {
165
+ name = resolveFilterName(name)
166
+ const [state, setFilter] = useDataViewFilter<TextFilterArtifact>(name)
167
+ const hasQuery = (state?.query?.length ?? 0) > 0
168
+
169
+ const handleReset = useCallback((): void => {
170
+ setFilter(it => ({ mode: 'contains' as const, ...it, query: '' }))
171
+ }, [setFilter])
172
+
173
+ if (!hasQuery) return null
174
+
175
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
176
+
177
+ return (
178
+ <Slot
179
+ ref={ref}
180
+ onClick={composeEventHandlers(onClick, handleReset)}
181
+ {...otherProps}
182
+ />
183
+ )
184
+ },
185
+ )
186
+ DataViewTextFilterResetTrigger.displayName = 'DataViewTextFilterResetTrigger'
187
+
188
+ // ============================================================================
189
+ // Number Filter Input — Slot-based
190
+ // ============================================================================
191
+
192
+ const SlotNumberInput = Slot as React.ForwardRefExoticComponent<
193
+ React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>
194
+ >
195
+
196
+ export interface DataViewNumberFilterInputProps {
197
+ name?: string
198
+ type: 'from' | 'to'
199
+ allowFloat?: boolean
200
+ children: ReactElement
201
+ }
202
+
203
+ export const DataViewNumberFilterInput = forwardRef<HTMLInputElement, DataViewNumberFilterInputProps>(
204
+ ({ name, type, allowFloat, ...props }, ref) => {
205
+ name = resolveFilterName(name)
206
+ const [artifact, setFilter] = useDataViewFilter<NumberRangeFilterArtifact>(name)
207
+ const min = artifact?.min ?? null
208
+ const max = artifact?.max ?? null
209
+ const value = type === 'from' ? min : max
210
+
211
+ const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
212
+ const raw = e.target.value
213
+ const num = raw === '' ? null : (allowFloat ? parseFloat(raw) : parseInt(raw, 10))
214
+ const parsed = num !== null && isNaN(num) ? null : num
215
+ if (type === 'from') {
216
+ setFilter(it => ({ ...it, min: parsed, max: it?.max ?? null }))
217
+ } else {
218
+ setFilter(it => ({ ...it, min: it?.min ?? null, max: parsed }))
219
+ }
220
+ }, [type, allowFloat, setFilter])
221
+
222
+ return (
223
+ <SlotNumberInput
224
+ type="number"
225
+ value={value ?? ''}
226
+ onChange={onChange}
227
+ {...props}
228
+ ref={ref}
229
+ />
230
+ )
231
+ },
232
+ )
233
+ DataViewNumberFilterInput.displayName = 'DataViewNumberFilterInput'
234
+
235
+ // ============================================================================
236
+ // Date Filter Input — Slot-based
237
+ // ============================================================================
238
+
239
+ const SlotDateInput = Slot as React.ForwardRefExoticComponent<
240
+ React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>
241
+ >
242
+
243
+ export interface DataViewDateFilterInputProps {
244
+ name?: string
245
+ type: 'start' | 'end'
246
+ children: ReactElement
247
+ }
248
+
249
+ export const DataViewDateFilterInput = forwardRef<HTMLInputElement, DataViewDateFilterInputProps>(
250
+ ({ name, type, ...props }, ref) => {
251
+ name = resolveFilterName(name)
252
+ const [artifact, setFilter] = useDataViewFilter<import('@contember/bindx').DateFilterArtifact>(name)
253
+ const start = artifact?.start ?? null
254
+ const end = artifact?.end ?? null
255
+ const value = type === 'start' ? start : end
256
+
257
+ const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
258
+ const val = e.target.value || null
259
+ if (type === 'start') {
260
+ setFilter(it => ({ ...it, start: val, end: it?.end ?? null }))
261
+ } else {
262
+ setFilter(it => ({ ...it, start: it?.start ?? null, end: val }))
263
+ }
264
+ }, [type, setFilter])
265
+
266
+ return (
267
+ <SlotDateInput
268
+ type="date"
269
+ value={value ?? ''}
270
+ onChange={onChange}
271
+ {...props}
272
+ ref={ref}
273
+ />
274
+ )
275
+ },
276
+ )
277
+ DataViewDateFilterInput.displayName = 'DataViewDateFilterInput'
278
+
279
+ // ============================================================================
280
+ // Boolean Filter Trigger — Slot-based with action
281
+ // ============================================================================
282
+
283
+ export interface DataViewBooleanFilterTriggerProps {
284
+ name?: string
285
+ value: boolean
286
+ action?: DataViewSetBooleanFilterAction
287
+ children: ReactElement
288
+ }
289
+
290
+ export const DataViewBooleanFilterTrigger = forwardRef<HTMLButtonElement, DataViewBooleanFilterTriggerProps>(
291
+ ({ name, action = 'include', value, ...props }, ref) => {
292
+ name = resolveFilterName(name)
293
+ const [current, setFilter] = useDataViewBooleanFilter(name, value)
294
+
295
+ const toggleFilter = useCallback((): void => {
296
+ setFilter(action)
297
+ }, [action, setFilter])
298
+
299
+ const active = current === actionToStateBool[action]
300
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
301
+
302
+ return (
303
+ <Slot
304
+ ref={ref}
305
+ onClick={composeEventHandlers(onClick, toggleFilter)}
306
+ data-active={dataAttribute(active)}
307
+ data-current={current}
308
+ {...otherProps}
309
+ />
310
+ )
311
+ },
312
+ )
313
+ DataViewBooleanFilterTrigger.displayName = 'DataViewBooleanFilterTrigger'
314
+
315
+ const actionToStateBool: Record<DataViewSetBooleanFilterAction, DataViewBooleanFilterCurrent> = {
316
+ include: 'include',
317
+ unset: 'none',
318
+ toggle: 'include',
319
+ }
320
+
321
+ // ============================================================================
322
+ // Enum Filter Trigger — Slot-based with action
323
+ // ============================================================================
324
+
325
+ export interface DataViewEnumFilterTriggerProps {
326
+ name?: string
327
+ value: string
328
+ action?: DataViewSetEnumFilterAction
329
+ children: ReactElement
330
+ }
331
+
332
+ export const DataViewEnumFilterTrigger = forwardRef<HTMLButtonElement, DataViewEnumFilterTriggerProps>(
333
+ ({ name, action = 'include', value, ...props }, ref) => {
334
+ name = resolveFilterName(name)
335
+ const [current, setFilter] = useDataViewEnumFilter(name, value)
336
+
337
+ const toggleFilter = useCallback((): void => {
338
+ setFilter(action)
339
+ }, [action, setFilter])
340
+
341
+ const active = current === actionToStateEnum[action]
342
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
343
+
344
+ return (
345
+ <Slot
346
+ ref={ref}
347
+ onClick={composeEventHandlers(onClick, toggleFilter)}
348
+ data-active={dataAttribute(active)}
349
+ data-current={current}
350
+ {...otherProps}
351
+ />
352
+ )
353
+ },
354
+ )
355
+ DataViewEnumFilterTrigger.displayName = 'DataViewEnumFilterTrigger'
356
+
357
+ const actionToStateEnum: Record<DataViewSetEnumFilterAction, DataViewEnumFilterCurrent> = {
358
+ exclude: 'exclude',
359
+ include: 'include',
360
+ unset: 'none',
361
+ toggleInclude: 'include',
362
+ toggleExclude: 'exclude',
363
+ }
364
+
365
+ // ============================================================================
366
+ // Enum Filter State — render prop
367
+ // ============================================================================
368
+
369
+ export interface DataViewEnumFilterStateProps {
370
+ name?: string
371
+ value: string
372
+ children: (state: { isIncluded: boolean; isExcluded: boolean }) => ReactNode
373
+ }
374
+
375
+ export function DataViewEnumFilterState({
376
+ name,
377
+ value,
378
+ children,
379
+ }: DataViewEnumFilterStateProps): ReactElement {
380
+ name = resolveFilterName(name)
381
+ const [artifact] = useDataViewFilter<EnumFilterArtifact>(name)
382
+ const includedValues = artifact?.values ?? []
383
+ const excludedValues = artifact?.notValues ?? []
384
+
385
+ return <>{children({
386
+ isIncluded: includedValues.includes(value),
387
+ isExcluded: excludedValues.includes(value),
388
+ })}</>
389
+ }
390
+
391
+ // ============================================================================
392
+ // Relation Filter Trigger — Slot-based with action
393
+ // ============================================================================
394
+
395
+ export interface DataViewRelationFilterTriggerProps {
396
+ name?: string
397
+ id: string
398
+ action?: DataViewSetRelationFilterAction
399
+ children: ReactElement
400
+ }
401
+
402
+ export const DataViewRelationFilterTrigger = forwardRef<HTMLButtonElement, DataViewRelationFilterTriggerProps>(
403
+ ({ name, action = 'include', id, ...props }, ref) => {
404
+ name = resolveFilterName(name)
405
+ const [current, setFilter] = useDataViewRelationFilter(name, id)
406
+
407
+ const toggleFilter = useCallback((): void => {
408
+ setFilter(action)
409
+ }, [action, setFilter])
410
+
411
+ const active = current === actionToStateRelation[action]
412
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
413
+
414
+ return (
415
+ <Slot
416
+ ref={ref}
417
+ onClick={composeEventHandlers(onClick, toggleFilter)}
418
+ data-active={dataAttribute(active)}
419
+ data-current={current}
420
+ {...otherProps}
421
+ />
422
+ )
423
+ },
424
+ )
425
+ DataViewRelationFilterTrigger.displayName = 'DataViewRelationFilterTrigger'
426
+
427
+ const actionToStateRelation: Record<DataViewSetRelationFilterAction, DataViewRelationFilterCurrent> = {
428
+ exclude: 'exclude',
429
+ include: 'include',
430
+ unset: 'none',
431
+ toggleInclude: 'include',
432
+ toggleExclude: 'exclude',
433
+ }
434
+
435
+ // ============================================================================
436
+ // Relation Filter State — render prop
437
+ // ============================================================================
438
+
439
+ export interface DataViewRelationFilterStateProps {
440
+ name?: string
441
+ id: string
442
+ children: (state: { isIncluded: boolean; isExcluded: boolean }) => ReactNode
443
+ }
444
+
445
+ export function DataViewRelationFilterState({
446
+ name,
447
+ id,
448
+ children,
449
+ }: DataViewRelationFilterStateProps): ReactElement {
450
+ name = resolveFilterName(name)
451
+ const [artifact] = useDataViewFilter<import('@contember/bindx').RelationFilterArtifact>(name)
452
+ const includedIds = artifact?.id ?? []
453
+ const excludedIds = artifact?.notId ?? []
454
+
455
+ return <>{children({
456
+ isIncluded: includedIds.includes(id),
457
+ isExcluded: excludedIds.includes(id),
458
+ })}</>
459
+ }
460
+
461
+ // ============================================================================
462
+ // Null Filter Trigger — Slot-based with action
463
+ // ============================================================================
464
+
465
+ export interface DataViewNullFilterTriggerProps {
466
+ name?: string
467
+ action: DataViewSetNullFilterAction
468
+ children: ReactElement
469
+ }
470
+
471
+ export const DataViewNullFilterTrigger = forwardRef<HTMLButtonElement, DataViewNullFilterTriggerProps>(
472
+ ({ name, action, ...props }, ref) => {
473
+ name = resolveFilterName(name)
474
+ const [current, setFilter] = useDataViewNullFilter(name)
475
+
476
+ const toggleFilter = useCallback((): void => {
477
+ setFilter(action)
478
+ }, [action, setFilter])
479
+
480
+ const active = current === actionToStateNull[action]
481
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
482
+
483
+ return (
484
+ <Slot
485
+ ref={ref}
486
+ onClick={composeEventHandlers(onClick, toggleFilter)}
487
+ data-active={dataAttribute(active)}
488
+ data-current={current}
489
+ {...otherProps}
490
+ />
491
+ )
492
+ },
493
+ )
494
+ DataViewNullFilterTrigger.displayName = 'DataViewNullFilterTrigger'
495
+
496
+ const actionToStateNull: Record<DataViewSetNullFilterAction, DataViewNullFilterState> = {
497
+ exclude: 'exclude',
498
+ include: 'include',
499
+ unset: 'none',
500
+ toggleInclude: 'include',
501
+ toggleExclude: 'exclude',
502
+ }
503
+
504
+ // ============================================================================
505
+ // Generic Filter Reset — Slot-based
506
+ // ============================================================================
507
+
508
+ export interface DataViewFilterResetTriggerProps {
509
+ name?: string
510
+ children: ReactElement
511
+ }
512
+
513
+ export const DataViewFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewFilterResetTriggerProps>(
514
+ ({ name, ...props }, ref) => {
515
+ name = resolveFilterName(name)
516
+ const { filtering } = useDataViewContext()
517
+ const filter = filtering.filters.get(name)
518
+ const isActive = filter ? filter.handler.isActive(filter.artifact) : false
519
+
520
+ const handleReset = useCallback((): void => {
521
+ filtering.resetFilter(name!)
522
+ }, [filtering, name])
523
+
524
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
525
+
526
+ return (
527
+ <Slot
528
+ ref={ref}
529
+ onClick={composeEventHandlers(onClick, handleReset)}
530
+ disabled={!isActive}
531
+ data-active={dataAttribute(isActive)}
532
+ {...otherProps}
533
+ />
534
+ )
535
+ },
536
+ )
537
+ DataViewFilterResetTrigger.displayName = 'DataViewFilterResetTrigger'
538
+
539
+ // ============================================================================
540
+ // Date Filter Reset Trigger — Slot-based
541
+ // ============================================================================
542
+
543
+ export interface DataViewDateFilterResetTriggerProps {
544
+ name?: string
545
+ /** Which part to reset: 'start', 'end', or undefined (both) */
546
+ type?: 'start' | 'end'
547
+ children: ReactElement
548
+ }
549
+
550
+ export const DataViewDateFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewDateFilterResetTriggerProps>(
551
+ ({ name, type, ...props }, ref) => {
552
+ name = resolveFilterName(name)
553
+ const [state, setFilter] = useDataViewFilter<import('@contember/bindx').DateFilterArtifact>(name)
554
+ const hasValue = type === 'start'
555
+ ? (state?.start ?? null) !== null
556
+ : type === 'end'
557
+ ? (state?.end ?? null) !== null
558
+ : (state?.start ?? null) !== null || (state?.end ?? null) !== null
559
+
560
+ const handleReset = useCallback((): void => {
561
+ if (type === 'start') {
562
+ setFilter(it => it ? { ...it, start: null } : undefined)
563
+ } else if (type === 'end') {
564
+ setFilter(it => it ? { ...it, end: null } : undefined)
565
+ } else {
566
+ setFilter(undefined)
567
+ }
568
+ }, [setFilter, type])
569
+
570
+ if (!hasValue) return null
571
+
572
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
573
+
574
+ return (
575
+ <Slot
576
+ ref={ref}
577
+ onClick={composeEventHandlers(onClick, handleReset)}
578
+ {...otherProps}
579
+ />
580
+ )
581
+ },
582
+ )
583
+ DataViewDateFilterResetTrigger.displayName = 'DataViewDateFilterResetTrigger'
584
+
585
+ // ============================================================================
586
+ // Number Filter Reset Trigger — Slot-based
587
+ // ============================================================================
588
+
589
+ export interface DataViewNumberFilterResetTriggerProps {
590
+ name?: string
591
+ children: ReactElement
592
+ }
593
+
594
+ export const DataViewNumberFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewNumberFilterResetTriggerProps>(
595
+ ({ name, ...props }, ref) => {
596
+ name = resolveFilterName(name)
597
+ const [state, setFilter] = useDataViewFilter<NumberRangeFilterArtifact>(name)
598
+ const hasValue = (state?.min ?? null) !== null || (state?.max ?? null) !== null
599
+
600
+ const handleReset = useCallback((): void => {
601
+ setFilter(undefined)
602
+ }, [setFilter])
603
+
604
+ if (!hasValue) return null
605
+
606
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
607
+
608
+ return (
609
+ <Slot
610
+ ref={ref}
611
+ onClick={composeEventHandlers(onClick, handleReset)}
612
+ {...otherProps}
613
+ />
614
+ )
615
+ },
616
+ )
617
+ DataViewNumberFilterResetTrigger.displayName = 'DataViewNumberFilterResetTrigger'
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Filter name context — provides the current filter name to descendant components.
3
+ *
4
+ * Used by DataViewTextFilter, DataViewBooleanFilter, etc. to scope
5
+ * trigger components so they don't need an explicit `name` prop.
6
+ */
7
+
8
+ import { createContext, useContext } from 'react'
9
+
10
+ const DataViewFilterNameContext = createContext<string | null>(null)
11
+
12
+ export function useDataViewFilterName(): string {
13
+ const name = useContext(DataViewFilterNameContext)
14
+ if (name === null) {
15
+ throw new Error(
16
+ 'useDataViewFilterName: no filter name in context. ' +
17
+ 'Wrap this component in a DataViewTextFilter, DataViewBooleanFilter, DataViewFilterScope, etc.',
18
+ )
19
+ }
20
+ return name
21
+ }
22
+
23
+ export function useOptionalDataViewFilterName(): string | null {
24
+ return useContext(DataViewFilterNameContext)
25
+ }
26
+
27
+ export const DataViewFilterNameProvider = DataViewFilterNameContext.Provider