@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,231 @@
1
+ /**
2
+ * SelectDataView — provides a DataView context for select option lists.
3
+ *
4
+ * Analyzes the children render function for field selection, loads data
5
+ * from the options entity, and provides a DataView context for rendering.
6
+ * Children are rendered directly inside the DataView — use DataViewEachRow
7
+ * or DataViewLoaderState to iterate items and show loading states.
8
+ *
9
+ * Usage:
10
+ * ```tsx
11
+ * <Select relation={article.category} options={schema.Category}>
12
+ * <SelectDataView queryField={['name']}>
13
+ * <DataViewEachRow>
14
+ * {(item) => (
15
+ * <SelectOption entity={item}>
16
+ * <SelectItemTrigger entity={item}>
17
+ * <button>{item.name.value}</button>
18
+ * </SelectItemTrigger>
19
+ * </SelectOption>
20
+ * )}
21
+ * </DataViewEachRow>
22
+ * </SelectDataView>
23
+ * </Select>
24
+ * ```
25
+ */
26
+
27
+ import { memo, type ReactElement, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
28
+ import type {
29
+ EntityAccessor,
30
+ FilterArtifact,
31
+ FilterHandler,
32
+ OrderDirection,
33
+ SortingDirections,
34
+ } from '@contember/bindx'
35
+ import { SelectionScope, buildQueryFromSelection, createFullTextFilterHandler, FIELD_REF_META } from '@contember/bindx'
36
+ import type { FieldRefBase } from '@contember/bindx'
37
+ import {
38
+ useBindxContext,
39
+ useEntityList,
40
+ createCollectorProxy,
41
+ mergeSelections,
42
+ collectSelection,
43
+ } from '@contember/bindx-react'
44
+ import { useSelectOptions } from './selectContext.js'
45
+ import { DataViewProvider, type DataViewContextValue, type DataViewLoaderState } from '../DataViewContext.js'
46
+ import { useFilteringState, useSortingState, usePagingState, useSelectionState } from '../useDataViewState.js'
47
+
48
+ export interface SelectDataViewProps {
49
+ /** Children rendered inside the DataView context */
50
+ children: ReactNode
51
+ /** Selection definer — called with collector proxy to discover fields */
52
+ selection?: (it: EntityAccessor<object>) => ReactNode
53
+ /** Field(s) to search across */
54
+ queryField?: FieldRefBase<unknown> | FieldRefBase<unknown>[] | string[]
55
+ /** Initial sort order */
56
+ initialSorting?: Partial<Record<string, OrderDirection>>
57
+ /** Static filter for the options list */
58
+ filter?: Record<string, unknown>
59
+ /** Items per page (null = all). Default: 50 */
60
+ itemsPerPage?: number | null
61
+ }
62
+
63
+ const QUERY_FILTER_NAME = '__query'
64
+
65
+ function SelectDataViewImpl({
66
+ children,
67
+ selection: selectionDefiner,
68
+ queryField,
69
+ initialSorting,
70
+ filter: staticFilter,
71
+ itemsPerPage = 50,
72
+ }: SelectDataViewProps): ReactElement | null {
73
+ const options = useSelectOptions()
74
+ const { schema: schemaRegistry } = useBindxContext()
75
+ const entityType = options.$name
76
+
77
+ // ---- Loader state tracking ----
78
+ const hasLoadedOnce = useRef(false)
79
+ const [loaderState, setLoaderState] = useState<DataViewLoaderState>('initial')
80
+
81
+ // ---- Build selection from definer ----
82
+ // Use ref for selectionDefiner to avoid re-running on every render
83
+ // (inline arrow functions create new refs each render)
84
+ const selectionDefinerRef = useRef(selectionDefiner)
85
+ selectionDefinerRef.current = selectionDefiner
86
+
87
+ const { selection, queryKey } = useMemo(() => {
88
+ const scope = new SelectionScope()
89
+ const collector = createCollectorProxy<object>(scope, entityType, schemaRegistry ?? undefined) as unknown as EntityAccessor<object>
90
+
91
+ if (selectionDefinerRef.current) {
92
+ const jsx = selectionDefinerRef.current(collector)
93
+ const jsxSel = collectSelection(jsx)
94
+ const sel = scope.toSelectionMeta()
95
+ mergeSelections(sel, jsxSel)
96
+ }
97
+
98
+ const sel = scope.toSelectionMeta()
99
+ const query = buildQueryFromSelection(sel)
100
+ const key = JSON.stringify({ entityType, query })
101
+
102
+ return { selection: sel, queryKey: key }
103
+ }, [entityType, schemaRegistry])
104
+
105
+ // ---- Filter setup ----
106
+ const filterDefs = useMemo((): ReadonlyMap<string, { handler: FilterHandler<FilterArtifact> }> => {
107
+ const map = new Map<string, { handler: FilterHandler<FilterArtifact> }>()
108
+
109
+ // Resolve query fields: explicit prop → auto-derive from selection scalars
110
+ let resolvedPaths: string[] | undefined
111
+ if (queryField) {
112
+ const fields = Array.isArray(queryField) ? queryField : [queryField]
113
+ resolvedPaths = fields.map(f => typeof f === 'string' ? f : f[FIELD_REF_META].fieldName)
114
+ } else if (selection.fields.size > 0) {
115
+ // Auto-derive: use all scalar fields from selection as searchable fields
116
+ resolvedPaths = []
117
+ for (const [name, field] of selection.fields) {
118
+ if (!field.nested) resolvedPaths.push(name)
119
+ }
120
+ }
121
+
122
+ if (resolvedPaths && resolvedPaths.length > 0) {
123
+ map.set(QUERY_FILTER_NAME, { handler: createFullTextFilterHandler(resolvedPaths) })
124
+ }
125
+ return map
126
+ }, [queryField, selection])
127
+
128
+ const sortableFields = useMemo((): ReadonlySet<string> => new Set(), [])
129
+
130
+ const initialSortingDirs = useMemo((): SortingDirections | undefined => {
131
+ if (!initialSorting) return undefined
132
+ const dirs: Record<string, OrderDirection> = {}
133
+ for (const [field, dir] of Object.entries(initialSorting)) {
134
+ if (dir) dirs[field] = dir
135
+ }
136
+ return Object.keys(dirs).length > 0 ? dirs : undefined
137
+ }, [initialSorting])
138
+
139
+ // ---- State (no persistence for select dropdowns) ----
140
+ const filtering = useFilteringState({ filters: filterDefs })
141
+ const sorting = useSortingState({ sortableFields, initialSorting: initialSortingDirs })
142
+ const paging = usePagingState({ initialItemsPerPage: itemsPerPage })
143
+ const selectionState = useSelectionState({})
144
+
145
+ // ---- Build combined filter ----
146
+ const combinedFilter = useMemo((): Record<string, unknown> | undefined => {
147
+ const parts: Record<string, unknown>[] = []
148
+ if (staticFilter) parts.push(staticFilter)
149
+ if (filtering.resolvedWhere) parts.push(filtering.resolvedWhere)
150
+ if (parts.length === 0) return undefined
151
+ if (parts.length === 1) return parts[0]
152
+ return { and: parts }
153
+ }, [staticFilter, filtering.resolvedWhere])
154
+
155
+ // ---- Load data ----
156
+ const result = useEntityList(options, {
157
+ filter: combinedFilter,
158
+ orderBy: sorting.resolvedOrderBy,
159
+ limit: paging.queryLimit,
160
+ offset: paging.queryOffset,
161
+ selection,
162
+ queryKey,
163
+ })
164
+
165
+ const items = result.status === 'ready' ? result.items : []
166
+ const itemCount = items.length
167
+
168
+ // Update loader state
169
+ useEffect(() => {
170
+ if (result.status === 'ready') {
171
+ hasLoadedOnce.current = true
172
+ setLoaderState('loaded')
173
+ } else if (result.status === 'error') {
174
+ setLoaderState('failed')
175
+ } else if (result.status === 'loading') {
176
+ setLoaderState(hasLoadedOnce.current ? 'refreshing' : 'initial')
177
+ }
178
+ }, [result.status])
179
+
180
+ // Update total count for paging
181
+ useEffect(() => {
182
+ if (result.status === 'ready' && paging.queryLimit !== undefined && paging.queryOffset !== undefined && itemCount < paging.queryLimit) {
183
+ paging.setTotalCount(paging.queryOffset + itemCount)
184
+ }
185
+ }, [result.status, itemCount, paging.queryLimit, paging.queryOffset, paging.setTotalCount])
186
+
187
+ // ---- Reload ----
188
+ const [, setReloadCounter] = useState(0)
189
+ const reload = useCallback((): void => {
190
+ setReloadCounter(c => c + 1)
191
+ }, [])
192
+
193
+ // ---- Row highlighting ----
194
+ const [highlightIndex, setHighlightIndex] = useState<number | null>(null)
195
+
196
+ // Reset highlight when filter/sort changes (not on every items reference change)
197
+ const filterKey = filtering.resolvedWhere ? JSON.stringify(filtering.resolvedWhere) : ''
198
+ const sortKey = sorting.resolvedOrderBy ? JSON.stringify(sorting.resolvedOrderBy) : ''
199
+ useEffect(() => {
200
+ setHighlightIndex(null)
201
+ }, [filterKey, sortKey])
202
+
203
+ const emptyMap = useMemo(() => new Map(), [])
204
+
205
+ const contextValue = useMemo((): DataViewContextValue => ({
206
+ filtering,
207
+ sorting,
208
+ paging,
209
+ selection: selectionState,
210
+ columns: [],
211
+ entityType,
212
+ items,
213
+ itemCount,
214
+ loaderState,
215
+ reload,
216
+ highlightIndex,
217
+ setHighlightIndex,
218
+ selectionMeta: selection,
219
+ toolbarContent: undefined,
220
+ layoutRenders: emptyMap,
221
+ layoutElements: emptyMap,
222
+ }), [filtering, sorting, paging, selectionState, entityType, items, itemCount, loaderState, reload, highlightIndex, selection, emptyMap])
223
+
224
+ return (
225
+ <DataViewProvider value={contextValue}>
226
+ {children}
227
+ </DataViewProvider>
228
+ )
229
+ }
230
+
231
+ export const SelectDataView = memo(SelectDataViewImpl) as unknown as typeof SelectDataViewImpl
@@ -0,0 +1,34 @@
1
+ /**
2
+ * SelectEachValue — iterates over currently selected entities.
3
+ *
4
+ * Renders its children once for each selected entity,
5
+ * providing the entity accessor and index.
6
+ *
7
+ * Usage:
8
+ * ```tsx
9
+ * <SelectEachValue>
10
+ * {(entity) => <span>{entity.name.value}</span>}
11
+ * </SelectEachValue>
12
+ * ```
13
+ */
14
+
15
+ import React, { type ReactNode } from 'react'
16
+ import type { EntityAccessor } from '@contember/bindx'
17
+ import { useSelectCurrentEntities } from './selectContext.js'
18
+
19
+ export interface SelectEachValueProps {
20
+ children: (entity: EntityAccessor<object>, index: number) => ReactNode
21
+ }
22
+
23
+ export function SelectEachValue({ children }: SelectEachValueProps): ReactNode {
24
+ const entities = useSelectCurrentEntities()
25
+ return (
26
+ <>
27
+ {entities.map((entity, index) => (
28
+ <React.Fragment key={entity.id}>
29
+ {children(entity, index)}
30
+ </React.Fragment>
31
+ ))}
32
+ </>
33
+ )
34
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * SelectItemTrigger — clickable handler for option items.
3
+ *
4
+ * Slot-based: wraps its single child element, adding an onClick handler
5
+ * that calls the select handler with the specified action.
6
+ *
7
+ * Usage:
8
+ * ```tsx
9
+ * <SelectItemTrigger action="toggle">
10
+ * <button><Field field={it.name} /></button>
11
+ * </SelectItemTrigger>
12
+ * ```
13
+ */
14
+
15
+ import React, { forwardRef, type ReactElement, useCallback } from 'react'
16
+ import { Slot } from '@radix-ui/react-slot'
17
+ import { composeEventHandlers } from '@radix-ui/primitive'
18
+ import { useSelectHandleSelect, type SelectAction } from './selectContext.js'
19
+ import type { EntityAccessor } from '@contember/bindx'
20
+
21
+ export interface SelectItemTriggerProps {
22
+ /** Entity accessor for this option */
23
+ entity: EntityAccessor<object>
24
+ /** Action to perform on click. Default: 'toggle' */
25
+ action?: SelectAction
26
+ children: ReactElement
27
+ }
28
+
29
+ export const SelectItemTrigger = forwardRef<HTMLElement, SelectItemTriggerProps>(
30
+ ({ entity, action = 'toggle', ...props }, ref) => {
31
+ const handleSelect = useSelectHandleSelect()
32
+
33
+ const handleClick = useCallback(
34
+ (): void => {
35
+ handleSelect(entity, action)
36
+ },
37
+ [entity, action, handleSelect],
38
+ )
39
+
40
+ const { onClick, ...otherProps } = props as React.HTMLAttributes<HTMLElement> & { children: ReactElement }
41
+
42
+ return (
43
+ <Slot
44
+ ref={ref}
45
+ onClick={composeEventHandlers(onClick, handleClick)}
46
+ {...otherProps}
47
+ />
48
+ )
49
+ },
50
+ )
51
+ SelectItemTrigger.displayName = 'SelectItemTrigger'
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SelectOption — marks an option item with data-selected attribute.
3
+ *
4
+ * Slot-based: wraps its single child element, adding `data-selected`
5
+ * when the current entity is selected.
6
+ *
7
+ * Usage:
8
+ * ```tsx
9
+ * <SelectOption>
10
+ * <button>Option label</button>
11
+ * </SelectOption>
12
+ * ```
13
+ */
14
+
15
+ import { forwardRef, type ReactElement } from 'react'
16
+ import { Slot } from '@radix-ui/react-slot'
17
+ import { useSelectIsSelected } from './selectContext.js'
18
+ import { dataAttribute } from '../dataAttribute.js'
19
+
20
+ export interface SelectOptionProps {
21
+ /** Entity accessor for this option (passed automatically by DataViewEachRow) */
22
+ entity: { id: string }
23
+ children: ReactElement
24
+ }
25
+
26
+ export const SelectOption = forwardRef<HTMLElement, SelectOptionProps>(
27
+ ({ entity, children, ...rest }, ref) => {
28
+ const isSelected = useSelectIsSelected()
29
+ return (
30
+ <Slot
31
+ ref={ref}
32
+ data-selected={dataAttribute(isSelected({ id: entity.id } as Parameters<typeof isSelected>[0]))}
33
+ {...rest}
34
+ >
35
+ {children}
36
+ </Slot>
37
+ )
38
+ },
39
+ )
40
+ SelectOption.displayName = 'SelectOption'
@@ -0,0 +1,22 @@
1
+ /**
2
+ * SelectPlaceholder — renders children only when no entities are selected.
3
+ *
4
+ * Usage:
5
+ * ```tsx
6
+ * <SelectPlaceholder>
7
+ * <span>Select an option...</span>
8
+ * </SelectPlaceholder>
9
+ * ```
10
+ */
11
+
12
+ import type { ReactNode } from 'react'
13
+ import { useSelectCurrentEntities } from './selectContext.js'
14
+
15
+ export interface SelectPlaceholderProps {
16
+ children: ReactNode
17
+ }
18
+
19
+ export function SelectPlaceholder({ children }: SelectPlaceholderProps): ReactNode {
20
+ const entities = useSelectCurrentEntities()
21
+ return entities.length > 0 ? null : <>{children}</>
22
+ }
@@ -0,0 +1,17 @@
1
+ export { Select, type SelectProps } from './Select.js'
2
+ export { MultiSelect, type MultiSelectProps } from './MultiSelect.js'
3
+ export { SelectDataView, type SelectDataViewProps } from './SelectDataView.js'
4
+ export { SelectOption, type SelectOptionProps } from './SelectOption.js'
5
+ export { SelectItemTrigger, type SelectItemTriggerProps } from './SelectItemTrigger.js'
6
+ export { SelectEachValue, type SelectEachValueProps } from './SelectEachValue.js'
7
+ export { SelectPlaceholder, type SelectPlaceholderProps } from './SelectPlaceholder.js'
8
+ export {
9
+ useSelectCurrentEntities,
10
+ useSelectIsSelected,
11
+ useSelectHandleSelect,
12
+ useSelectOptions,
13
+ SelectOptionsContext,
14
+ type SelectHandler,
15
+ type SelectAction,
16
+ type SelectEvents,
17
+ } from './selectContext.js'
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Select context — provides selection state and handlers to descendant components.
3
+ */
4
+
5
+ import { createContext, useContext } from 'react'
6
+ import type { EntityAccessor, EntityDef } from '@contember/bindx'
7
+
8
+ // ============================================================================
9
+ // Types
10
+ // ============================================================================
11
+
12
+ export type SelectAction = 'select' | 'unselect' | 'toggle'
13
+
14
+ export type SelectHandler = (entity: EntityAccessor<object>, action?: SelectAction) => void
15
+
16
+ export interface SelectEvents {
17
+ onSelect?: (entity: EntityAccessor<object>) => void
18
+ onUnselect?: (entity: EntityAccessor<object>) => void
19
+ }
20
+
21
+ // ============================================================================
22
+ // Contexts
23
+ // ============================================================================
24
+
25
+ const SelectCurrentEntitiesContext = createContext<readonly EntityAccessor<object>[] | null>(null)
26
+ SelectCurrentEntitiesContext.displayName = 'SelectCurrentEntitiesContext'
27
+
28
+ const SelectIsSelectedContext = createContext<((entity: EntityAccessor<object>) => boolean) | null>(null)
29
+ SelectIsSelectedContext.displayName = 'SelectIsSelectedContext'
30
+
31
+ const SelectHandleSelectContext = createContext<SelectHandler | null>(null)
32
+ SelectHandleSelectContext.displayName = 'SelectHandleSelectContext'
33
+
34
+ const SelectOptionsContext = createContext<EntityDef | null>(null)
35
+ SelectOptionsContext.displayName = 'SelectOptionsContext'
36
+
37
+ // ============================================================================
38
+ // Hooks
39
+ // ============================================================================
40
+
41
+ export function useSelectCurrentEntities(): readonly EntityAccessor<object>[] {
42
+ const ctx = useContext(SelectCurrentEntitiesContext)
43
+ if (ctx === null) {
44
+ throw new Error('useSelectCurrentEntities must be used within a Select or MultiSelect')
45
+ }
46
+ return ctx
47
+ }
48
+
49
+ export function useSelectIsSelected(): (entity: EntityAccessor<object>) => boolean {
50
+ const ctx = useContext(SelectIsSelectedContext)
51
+ if (ctx === null) {
52
+ throw new Error('useSelectIsSelected must be used within a Select or MultiSelect')
53
+ }
54
+ return ctx
55
+ }
56
+
57
+ export function useSelectHandleSelect(): SelectHandler {
58
+ const ctx = useContext(SelectHandleSelectContext)
59
+ if (ctx === null) {
60
+ throw new Error('useSelectHandleSelect must be used within a Select or MultiSelect')
61
+ }
62
+ return ctx
63
+ }
64
+
65
+ export function useSelectOptions(): EntityDef {
66
+ const ctx = useContext(SelectOptionsContext)
67
+ if (ctx === null) {
68
+ throw new Error('useSelectOptions must be used within a Select or MultiSelect')
69
+ }
70
+ return ctx
71
+ }
72
+
73
+ // ============================================================================
74
+ // Providers (exported for internal use by Select/MultiSelect)
75
+ // ============================================================================
76
+
77
+ export {
78
+ SelectCurrentEntitiesContext,
79
+ SelectIsSelectedContext,
80
+ SelectHandleSelectContext,
81
+ SelectOptionsContext,
82
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Selection components — column visibility + layout switching using Radix Slot.
3
+ *
4
+ * Usage:
5
+ * ```tsx
6
+ * <DataViewVisibilityTrigger name="description">
7
+ * <button>Toggle Description</button>
8
+ * </DataViewVisibilityTrigger>
9
+ *
10
+ * <DataViewElement name="description">
11
+ * <DescriptionColumn />
12
+ * </DataViewElement>
13
+ *
14
+ * <DataViewLayout name="table">
15
+ * <TableView />
16
+ * </DataViewLayout>
17
+ *
18
+ * <DataViewLayoutTrigger name="table">
19
+ * <button>Table View</button>
20
+ * </DataViewLayoutTrigger>
21
+ * ```
22
+ */
23
+
24
+ import React, { forwardRef, type ReactElement, useCallback } from 'react'
25
+ import { Slot } from '@radix-ui/react-slot'
26
+ import { composeEventHandlers } from '@radix-ui/primitive'
27
+ import { useDataViewContext } from './DataViewContext.js'
28
+ import { dataAttribute } from './dataAttribute.js'
29
+
30
+ // ============================================================================
31
+ // Column Visibility
32
+ // ============================================================================
33
+
34
+ export interface DataViewVisibilityTriggerProps {
35
+ /** Element name to toggle */
36
+ name: string
37
+ /** Value to set. Can be boolean or function. Default: toggle */
38
+ value?: boolean | ((current: boolean) => boolean)
39
+ /** Fallback visibility when not explicitly set. Default: true */
40
+ fallbackValue?: boolean
41
+ /** Button element */
42
+ children: ReactElement
43
+ }
44
+
45
+ export const DataViewVisibilityTrigger = forwardRef<HTMLButtonElement, DataViewVisibilityTriggerProps>(
46
+ ({ name, value, fallbackValue = true, ...props }, ref) => {
47
+ const { selection } = useDataViewContext()
48
+ const isVisible = selection.isVisible(name, fallbackValue)
49
+
50
+ const handleClick = useCallback((): void => {
51
+ if (value === undefined) {
52
+ selection.setVisibility(name, !isVisible)
53
+ } else if (typeof value === 'function') {
54
+ selection.setVisibility(name, value(isVisible))
55
+ } else {
56
+ selection.setVisibility(name, value)
57
+ }
58
+ }, [selection, name, value, isVisible])
59
+
60
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
61
+
62
+ return (
63
+ <Slot
64
+ ref={ref}
65
+ onClick={composeEventHandlers(onClick, handleClick)}
66
+ data-active={dataAttribute(isVisible)}
67
+ data-current={String(isVisible)}
68
+ {...otherProps}
69
+ />
70
+ )
71
+ },
72
+ )
73
+ DataViewVisibilityTrigger.displayName = 'DataViewVisibilityTrigger'
74
+
75
+ // ============================================================================
76
+ // Conditional Element (visibility-aware)
77
+ // ============================================================================
78
+
79
+ export interface DataViewElementProps {
80
+ /** Element name (used for visibility toggle) */
81
+ name: string
82
+ /** Label for the element (for UI display) */
83
+ label?: React.ReactNode
84
+ /** Default visibility when not explicitly set. Default: true */
85
+ fallback?: boolean
86
+ children: React.ReactNode
87
+ }
88
+
89
+ export function DataViewElement({
90
+ name,
91
+ fallback = true,
92
+ children,
93
+ }: DataViewElementProps): ReactElement | null {
94
+ const { selection } = useDataViewContext()
95
+ const isVisible = selection.isVisible(name, fallback)
96
+
97
+ if (!isVisible) return null
98
+ return <>{children}</>
99
+ }
100
+
101
+ /** @deprecated Use DataViewElement instead */
102
+ export const DataViewIsVisible = DataViewElement
103
+
104
+ // ============================================================================
105
+ // Layout Switching
106
+ // ============================================================================
107
+
108
+ export interface DataViewLayoutProps {
109
+ /** Layout name */
110
+ name: string
111
+ /** Label for the layout */
112
+ label?: React.ReactNode
113
+ children: React.ReactNode
114
+ }
115
+
116
+ export function DataViewLayout({
117
+ name,
118
+ children,
119
+ }: DataViewLayoutProps): ReactElement | null {
120
+ const { selection } = useDataViewContext()
121
+ const currentLayout = selection.currentLayout
122
+ const layouts = selection.state.layouts
123
+ const isActive = currentLayout === name || (currentLayout === undefined && layouts[0]?.name === name)
124
+
125
+ if (!isActive) return null
126
+ return <>{children}</>
127
+ }
128
+
129
+ export interface DataViewLayoutTriggerProps {
130
+ /** Layout name to switch to */
131
+ name: string
132
+ /** Button element */
133
+ children: ReactElement
134
+ }
135
+
136
+ export const DataViewLayoutTrigger = forwardRef<HTMLButtonElement, DataViewLayoutTriggerProps>(
137
+ ({ name, ...props }, ref) => {
138
+ const { selection } = useDataViewContext()
139
+ const currentLayout = selection.currentLayout
140
+ const layouts = selection.state.layouts
141
+ const isActive = currentLayout === name || (currentLayout === undefined && layouts[0]?.name === name)
142
+
143
+ const handleClick = useCallback((): void => {
144
+ selection.setLayout(name)
145
+ }, [selection, name])
146
+
147
+ const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
148
+
149
+ return (
150
+ <Slot
151
+ ref={ref}
152
+ onClick={composeEventHandlers(onClick, handleClick)}
153
+ data-active={dataAttribute(isActive)}
154
+ data-current={currentLayout ?? layouts[0]?.name ?? 'none'}
155
+ {...otherProps}
156
+ />
157
+ )
158
+ },
159
+ )
160
+ DataViewLayoutTrigger.displayName = 'DataViewLayoutTrigger'