@fastnd/components 1.0.28 → 1.0.30

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 (68) hide show
  1. package/dist/components/FavoriteButton/FavoriteButton.d.ts +2 -1
  2. package/dist/components/index.d.ts +1 -3
  3. package/dist/components/ui/badge.d.ts +1 -1
  4. package/dist/components/ui/button.d.ts +1 -1
  5. package/dist/components/ui/data-table.d.ts +8 -0
  6. package/dist/components/ui/input-group.d.ts +1 -1
  7. package/dist/components/ui/item.d.ts +1 -1
  8. package/dist/components/ui/tabs.d.ts +1 -1
  9. package/dist/examples/data-explorer/CardCarouselPanel/CardCarouselPanel.tsx +197 -0
  10. package/dist/examples/data-explorer/CardView/CardView.tsx +168 -0
  11. package/dist/examples/data-explorer/ColumnConfigPopover/ColumnConfigPopover.tsx +157 -0
  12. package/dist/examples/data-explorer/DataExplorerEmpty/DataExplorerEmpty.tsx +56 -0
  13. package/dist/examples/data-explorer/DataExplorerPage/DataExplorerPage.tsx +101 -0
  14. package/dist/examples/data-explorer/DataExplorerPagination/DataExplorerPagination.tsx +129 -0
  15. package/dist/examples/data-explorer/DataExplorerToolbar/DataExplorerToolbar.tsx +143 -0
  16. package/dist/examples/data-explorer/DomainSwitcher/DomainSwitcher.tsx +36 -0
  17. package/dist/examples/data-explorer/ExpansionRows/ExpansionRows.tsx +180 -0
  18. package/dist/examples/data-explorer/FilterChip/FilterChip.tsx +85 -0
  19. package/dist/examples/data-explorer/FilterPopoverContent/FilterPopoverContent.tsx +73 -0
  20. package/dist/examples/data-explorer/ListView/ListView.tsx +305 -0
  21. package/dist/examples/data-explorer/MoreFiltersPopover/MoreFiltersPopover.tsx +113 -0
  22. package/dist/examples/data-explorer/TableView/TableView.tsx +193 -0
  23. package/dist/examples/data-explorer/cells/CellRenderer.tsx +147 -0
  24. package/dist/examples/data-explorer/cells/CurrencyCell.tsx +31 -0
  25. package/dist/examples/data-explorer/cells/DoubleTextCell.tsx +27 -0
  26. package/dist/examples/data-explorer/cells/ExpandButton.tsx +67 -0
  27. package/dist/examples/data-explorer/cells/InventoryBadgeCell.tsx +52 -0
  28. package/dist/examples/data-explorer/cells/LinkCell.tsx +42 -0
  29. package/dist/examples/data-explorer/cells/ScoreBar.tsx +50 -0
  30. package/dist/examples/data-explorer/cells/StatusBadgeCell.tsx +39 -0
  31. package/dist/examples/data-explorer/cells/TextCell.tsx +35 -0
  32. package/dist/examples/data-explorer/cells/index.ts +26 -0
  33. package/dist/examples/data-explorer/domains/applications.ts +225 -0
  34. package/dist/examples/data-explorer/domains/customers.ts +267 -0
  35. package/dist/examples/data-explorer/domains/index.ts +26 -0
  36. package/dist/examples/data-explorer/domains/products.ts +1116 -0
  37. package/dist/examples/data-explorer/domains/projects.ts +205 -0
  38. package/dist/examples/data-explorer/hooks/use-data-explorer-state.ts +371 -0
  39. package/dist/examples/data-explorer/index.ts +3 -0
  40. package/dist/examples/data-explorer/types.ts +239 -0
  41. package/dist/fastnd-components.js +16426 -17975
  42. package/package.json +1 -1
  43. package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +0 -20
  44. package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +0 -9
  45. package/dist/components/ProgressCircle/ProgressCircle.d.ts +0 -9
  46. package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +0 -136
  47. package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +0 -179
  48. package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +0 -190
  49. package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +0 -43
  50. package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +0 -111
  51. package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +0 -282
  52. package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +0 -283
  53. package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +0 -41
  54. package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +0 -139
  55. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +0 -230
  56. package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +0 -33
  57. package/dist/examples/data-visualization/cell-renderers.tsx +0 -119
  58. package/dist/examples/data-visualization/constants.ts +0 -1251
  59. package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +0 -65
  60. package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +0 -40
  61. package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +0 -41
  62. package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +0 -61
  63. package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +0 -32
  64. package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +0 -32
  65. package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +0 -133
  66. package/dist/examples/data-visualization/hooks/use-filtered-data.ts +0 -84
  67. package/dist/examples/data-visualization/index.ts +0 -10
  68. package/dist/examples/data-visualization/types.ts +0 -103
@@ -0,0 +1,205 @@
1
+ import type { ColumnsConfig, DomainConfig, DomainLayout, Project } from '../types'
2
+
3
+ export const PROJECT_COLUMNS: ColumnsConfig<Project> = {
4
+ favorite: {
5
+ label: 'Favorit',
6
+ type: 'favorite',
7
+ sortable: false,
8
+ filterable: false,
9
+ visible: true,
10
+ },
11
+ name: {
12
+ label: 'Projektname',
13
+ type: 'double-text',
14
+ sortable: true,
15
+ filterable: false,
16
+ visible: true,
17
+ secondary: 'customer_name',
18
+ searchable: true,
19
+ },
20
+ customer_name: {
21
+ label: 'Kunde',
22
+ type: 'link',
23
+ sortable: true,
24
+ filterable: true,
25
+ primaryFilter: true,
26
+ visible: true,
27
+ searchable: true,
28
+ },
29
+ status: {
30
+ label: 'Status',
31
+ type: 'status-badge',
32
+ sortable: true,
33
+ filterable: true,
34
+ primaryFilter: true,
35
+ visible: true,
36
+ hideTablet: true,
37
+ statusMap: {
38
+ Open: 'active',
39
+ Won: 'production',
40
+ Lost: 'eol',
41
+ Negotiation: 'nrnd',
42
+ Qualified: 'active',
43
+ },
44
+ },
45
+ expected_closing: {
46
+ label: 'Closing',
47
+ type: 'text',
48
+ sortable: true,
49
+ filterable: false,
50
+ visible: true,
51
+ hideMobile: true,
52
+ },
53
+ project_lifetime_quantity: {
54
+ label: 'Lifetime Qty',
55
+ type: 'inventory',
56
+ sortable: true,
57
+ filterable: true,
58
+ primaryFilter: true,
59
+ visible: true,
60
+ hideMobile: true,
61
+ levelFn: (v: number) => (v >= 100000 ? 'high' : v >= 10000 ? 'medium' : 'low'),
62
+ formatFn: (v: number) =>
63
+ v >= 1000 ? (v / 1000).toFixed(v >= 100000 ? 0 : 1) + 'k' : String(v),
64
+ labelMap: { high: 'Hoch', medium: 'Mittel', low: 'Gering' },
65
+ filterOptions: ['Hoch (\u2265 100k)', 'Mittel (10k\u2013100k)', 'Gering (< 10k)'],
66
+ filterFn: (row: Project, val: string) => {
67
+ if (val.startsWith('Hoch')) return row.project_lifetime_quantity >= 100000
68
+ if (val.startsWith('Mittel'))
69
+ return (
70
+ row.project_lifetime_quantity >= 10000 && row.project_lifetime_quantity < 100000
71
+ )
72
+ return row.project_lifetime_quantity < 10000
73
+ },
74
+ },
75
+ }
76
+
77
+ export const PROJECT_LAYOUT: DomainLayout = {
78
+ list: {
79
+ titleField: 'name',
80
+ metaFields: ['expected_closing'],
81
+ badgeFields: ['status', 'project_lifetime_quantity'],
82
+ valueField: null,
83
+ },
84
+ card: {
85
+ titleField: 'name',
86
+ subtitleField: 'customer_name',
87
+ badgeFields: ['status'],
88
+ rows: [
89
+ { label: 'Closing', field: 'expected_closing' },
90
+ { label: 'Lifetime Qty', field: 'project_lifetime_quantity', rendererOverride: 'inventory-label' },
91
+ ],
92
+ footerField: 'customer_name',
93
+ },
94
+ }
95
+
96
+ export const MOCK_PROJECTS: Project[] = [
97
+ {
98
+ id: 'pj1',
99
+ name: 'ADAS Kameramodul Gen3',
100
+ customer_name: 'Continental AG',
101
+ status: 'Open',
102
+ expected_closing: '2025-09-30',
103
+ project_lifetime_quantity: 250000,
104
+ favorite: true,
105
+ },
106
+ {
107
+ id: 'pj2',
108
+ name: 'EV Ladesteuerung 22kW',
109
+ customer_name: 'Webasto SE',
110
+ status: 'Won',
111
+ expected_closing: '2025-03-15',
112
+ project_lifetime_quantity: 80000,
113
+ favorite: false,
114
+ },
115
+ {
116
+ id: 'pj3',
117
+ name: 'Industrie-Gateway IoT Protokollkonverter',
118
+ customer_name: 'Siemens AG',
119
+ status: 'Negotiation',
120
+ expected_closing: '2025-12-01',
121
+ project_lifetime_quantity: 15000,
122
+ favorite: false,
123
+ },
124
+ {
125
+ id: 'pj4',
126
+ name: 'Patientenmonitor V5',
127
+ customer_name: 'Dräger AG',
128
+ status: 'Qualified',
129
+ expected_closing: '2026-02-28',
130
+ project_lifetime_quantity: 5000,
131
+ favorite: true,
132
+ },
133
+ {
134
+ id: 'pj5',
135
+ name: 'Smart Meter Modul',
136
+ customer_name: 'Landis+Gyr AG',
137
+ status: 'Open',
138
+ expected_closing: '2025-11-15',
139
+ project_lifetime_quantity: 500000,
140
+ favorite: false,
141
+ },
142
+ {
143
+ id: 'pj6',
144
+ name: 'Robotersteuerung R200',
145
+ customer_name: 'KUKA AG',
146
+ status: 'Won',
147
+ expected_closing: '2025-01-20',
148
+ project_lifetime_quantity: 12000,
149
+ favorite: false,
150
+ },
151
+ {
152
+ id: 'pj7',
153
+ name: 'BMS 48V Mild-Hybrid Batteriesystem für Nutzfahrzeuge',
154
+ customer_name: 'ZF Friedrichshafen AG',
155
+ status: 'Lost',
156
+ expected_closing: '2025-06-30',
157
+ project_lifetime_quantity: 180000,
158
+ favorite: false,
159
+ },
160
+ {
161
+ id: 'pj8',
162
+ name: 'Sensorknoten Agrar',
163
+ customer_name: 'CLAAS KGaA',
164
+ status: 'Open',
165
+ expected_closing: '2026-03-31',
166
+ project_lifetime_quantity: 8000,
167
+ favorite: false,
168
+ },
169
+ {
170
+ id: 'pj9',
171
+ name: 'Radar-Frontend 77GHz',
172
+ customer_name: 'Continental AG',
173
+ status: 'Negotiation',
174
+ expected_closing: '2025-10-15',
175
+ project_lifetime_quantity: 320000,
176
+ favorite: true,
177
+ },
178
+ {
179
+ id: 'pj10',
180
+ name: 'Wallbox Premium 11kW',
181
+ customer_name: 'Webasto SE',
182
+ status: 'Qualified',
183
+ expected_closing: '2025-08-30',
184
+ project_lifetime_quantity: 45000,
185
+ favorite: false,
186
+ },
187
+ {
188
+ id: 'pj11',
189
+ name: 'Autonomes Fahrzeug-Plattform Generation 4',
190
+ customer_name: 'BMW Group',
191
+ status: 'Negotiation',
192
+ expected_closing: '2027-06-30',
193
+ project_lifetime_quantity: 1750000,
194
+ favorite: false,
195
+ },
196
+ ]
197
+
198
+ export const PROJECTS_CONFIG: DomainConfig<Project> = {
199
+ key: 'projects',
200
+ label: 'Projekte',
201
+ resultLabel: 'Projekten',
202
+ columns: PROJECT_COLUMNS,
203
+ layout: PROJECT_LAYOUT,
204
+ data: MOCK_PROJECTS,
205
+ }
@@ -0,0 +1,371 @@
1
+ import { useReducer, useMemo, useCallback } from 'react'
2
+ import type {
3
+ DataExplorerState,
4
+ DataExplorerActions,
5
+ DataExplorerDerived,
6
+ DomainKey,
7
+ ViewMode,
8
+ ColumnDef,
9
+ DomainConfig,
10
+ } from '../types'
11
+ import { DATA_SOURCES } from '../domains'
12
+
13
+ // --- Action types ---
14
+
15
+ type Action =
16
+ | { type: 'SWITCH_DOMAIN'; domain: DomainKey }
17
+ | { type: 'SWITCH_VIEW'; mode: ViewMode }
18
+ | { type: 'TOGGLE_SORT'; column: string }
19
+ | { type: 'SET_FILTER'; column: string; values: string[] }
20
+ | { type: 'TOGGLE_FILTER_OPTION'; column: string; value: string }
21
+ | { type: 'CLEAR_FILTER'; column: string }
22
+ | { type: 'RESET_ALL_FILTERS' }
23
+ | { type: 'RESET_SECONDARY_FILTERS' }
24
+ | { type: 'SET_SEARCH_TERM'; term: string }
25
+ | { type: 'TOGGLE_EXPAND'; rowId: string }
26
+ | { type: 'TOGGLE_FAVORITE'; rowId: string }
27
+ | { type: 'TOGGLE_COLUMN_VISIBILITY'; column: string }
28
+ | { type: 'REORDER_COLUMNS'; newOrder: string[] }
29
+ | { type: 'SET_CURRENT_PAGE'; page: number }
30
+ | { type: 'SET_PAGE_SIZE'; size: number }
31
+
32
+ // --- Helpers ---
33
+
34
+ function initDomainState(domain: DomainKey): DataExplorerState {
35
+ const source = DATA_SOURCES[domain]
36
+ const columns = source.columns
37
+ const columnOrder = Object.keys(columns)
38
+ const columnVisibility: Record<string, boolean> = {}
39
+ for (const [key, col] of Object.entries(columns)) {
40
+ columnVisibility[key] = col.visible !== false
41
+ }
42
+ const favorites = new Set<string>()
43
+ for (const row of source.data) {
44
+ const r = row as Record<string, unknown>
45
+ if (r.favorite) favorites.add(r.id as string)
46
+ }
47
+ return {
48
+ activeDomain: domain,
49
+ viewMode: 'table',
50
+ sortColumn: null,
51
+ sortDirection: 'asc',
52
+ filters: {},
53
+ searchTerm: '',
54
+ columnOrder,
55
+ columnVisibility,
56
+ expandedRows: new Set(),
57
+ favorites,
58
+ currentPage: 1,
59
+ pageSize: 25,
60
+ }
61
+ }
62
+
63
+ function reducer(state: DataExplorerState, action: Action): DataExplorerState {
64
+ switch (action.type) {
65
+ case 'SWITCH_DOMAIN': {
66
+ const newState = initDomainState(action.domain)
67
+ newState.viewMode = state.viewMode
68
+ return newState
69
+ }
70
+
71
+ case 'SWITCH_VIEW':
72
+ return { ...state, viewMode: action.mode }
73
+
74
+ case 'TOGGLE_SORT': {
75
+ if (state.sortColumn === action.column) {
76
+ if (state.sortDirection === 'asc') {
77
+ return { ...state, sortDirection: 'desc', currentPage: 1 }
78
+ }
79
+ return { ...state, sortColumn: null, sortDirection: 'asc', currentPage: 1 }
80
+ }
81
+ return { ...state, sortColumn: action.column, sortDirection: 'asc', currentPage: 1 }
82
+ }
83
+
84
+ case 'SET_FILTER':
85
+ return {
86
+ ...state,
87
+ filters: { ...state.filters, [action.column]: action.values },
88
+ currentPage: 1,
89
+ }
90
+
91
+ case 'TOGGLE_FILTER_OPTION': {
92
+ const current = state.filters[action.column] ?? []
93
+ const has = current.includes(action.value)
94
+ const next = has
95
+ ? current.filter((v) => v !== action.value)
96
+ : [...current, action.value]
97
+ return {
98
+ ...state,
99
+ filters: { ...state.filters, [action.column]: next },
100
+ currentPage: 1,
101
+ }
102
+ }
103
+
104
+ case 'CLEAR_FILTER': {
105
+ const { [action.column]: _, ...rest } = state.filters
106
+ return { ...state, filters: rest, currentPage: 1 }
107
+ }
108
+
109
+ case 'RESET_ALL_FILTERS':
110
+ return { ...state, filters: {}, searchTerm: '', currentPage: 1 }
111
+
112
+ case 'RESET_SECONDARY_FILTERS': {
113
+ const source = DATA_SOURCES[state.activeDomain]
114
+ const cols = source.columns
115
+ const primaryKeys = new Set(
116
+ Object.entries(cols)
117
+ .filter(([, c]) => c.primaryFilter)
118
+ .map(([k]) => k),
119
+ )
120
+ const newFilters: Record<string, string[]> = {}
121
+ for (const [key, values] of Object.entries(state.filters)) {
122
+ if (primaryKeys.has(key)) newFilters[key] = values
123
+ }
124
+ return { ...state, filters: newFilters, currentPage: 1 }
125
+ }
126
+
127
+ case 'SET_SEARCH_TERM':
128
+ return { ...state, searchTerm: action.term, currentPage: 1 }
129
+
130
+ case 'TOGGLE_EXPAND': {
131
+ const next = new Set(state.expandedRows)
132
+ if (next.has(action.rowId)) next.delete(action.rowId)
133
+ else next.add(action.rowId)
134
+ return { ...state, expandedRows: next }
135
+ }
136
+
137
+ case 'TOGGLE_FAVORITE': {
138
+ const next = new Set(state.favorites)
139
+ if (next.has(action.rowId)) next.delete(action.rowId)
140
+ else next.add(action.rowId)
141
+ return { ...state, favorites: next }
142
+ }
143
+
144
+ case 'TOGGLE_COLUMN_VISIBILITY':
145
+ return {
146
+ ...state,
147
+ columnVisibility: {
148
+ ...state.columnVisibility,
149
+ [action.column]: !state.columnVisibility[action.column],
150
+ },
151
+ }
152
+
153
+ case 'REORDER_COLUMNS':
154
+ return { ...state, columnOrder: action.newOrder }
155
+
156
+ case 'SET_CURRENT_PAGE':
157
+ return { ...state, currentPage: action.page }
158
+
159
+ case 'SET_PAGE_SIZE':
160
+ return { ...state, pageSize: action.size, currentPage: 1 }
161
+ }
162
+ }
163
+
164
+ // --- Hook ---
165
+
166
+ export interface UseDataExplorerReturn {
167
+ state: DataExplorerState
168
+ derived: DataExplorerDerived
169
+ actions: DataExplorerActions
170
+ }
171
+
172
+ export function useDataExplorerState(
173
+ initialDomain: DomainKey = 'products',
174
+ ): UseDataExplorerReturn {
175
+ const [state, dispatch] = useReducer(reducer, initialDomain, initDomainState)
176
+
177
+ const domainConfig = useMemo<DomainConfig>(
178
+ () => DATA_SOURCES[state.activeDomain],
179
+ [state.activeDomain],
180
+ )
181
+
182
+ const visibleColumns = useMemo(
183
+ () =>
184
+ state.columnOrder.filter(
185
+ (k) => state.columnVisibility[k] && domainConfig.columns[k],
186
+ ),
187
+ [state.columnOrder, state.columnVisibility, domainConfig],
188
+ )
189
+
190
+ const filteredData = useMemo(() => {
191
+ const columns = domainConfig.columns
192
+ const term = state.searchTerm.toLowerCase()
193
+
194
+ return domainConfig.data.filter((row) => {
195
+ const r = row as Record<string, unknown>
196
+
197
+ // Search filter
198
+ if (term) {
199
+ let matchesSearch = false
200
+ for (const [key, col] of Object.entries(columns)) {
201
+ if (!col.searchable) continue
202
+ const v = r[key]
203
+ if (v != null && String(v).toLowerCase().includes(term)) {
204
+ matchesSearch = true
205
+ break
206
+ }
207
+ if (col.secondary) {
208
+ const sv = r[col.secondary]
209
+ if (sv != null && String(sv).toLowerCase().includes(term)) {
210
+ matchesSearch = true
211
+ break
212
+ }
213
+ }
214
+ }
215
+ if (!matchesSearch) return false
216
+ }
217
+
218
+ // Column filters
219
+ for (const [colKey, selected] of Object.entries(state.filters)) {
220
+ if (!selected || selected.length === 0) continue
221
+ const col = columns[colKey]
222
+ if (!col) continue
223
+ if (col.filterFn) {
224
+ const match = selected.some((val) =>
225
+ (col.filterFn as (row: Record<string, unknown>, val: string) => boolean)(r, val),
226
+ )
227
+ if (!match) return false
228
+ } else {
229
+ const val = String(r[colKey] ?? '')
230
+ if (!selected.includes(val)) return false
231
+ }
232
+ }
233
+
234
+ return true
235
+ })
236
+ }, [domainConfig, state.searchTerm, state.filters])
237
+
238
+ const sortedData = useMemo(() => {
239
+ const data = [...filteredData]
240
+ if (!state.sortColumn) return data
241
+ const col = domainConfig.columns[state.sortColumn]
242
+ if (!col) return data
243
+ const key = state.sortColumn
244
+ const dir = state.sortDirection === 'asc' ? 1 : -1
245
+ data.sort((a, b) => {
246
+ const ra = a as Record<string, unknown>
247
+ const rb = b as Record<string, unknown>
248
+ let va = ra[key] as string | number | null | undefined
249
+ let vb = rb[key] as string | number | null | undefined
250
+ if (va == null) va = ''
251
+ if (vb == null) vb = ''
252
+ if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir
253
+ return String(va).localeCompare(String(vb), 'de') * dir
254
+ })
255
+ return data
256
+ }, [filteredData, state.sortColumn, state.sortDirection, domainConfig])
257
+
258
+ const totalFiltered = filteredData.length
259
+
260
+ const totalPages = useMemo(
261
+ () => Math.max(1, Math.ceil(totalFiltered / state.pageSize)),
262
+ [totalFiltered, state.pageSize],
263
+ )
264
+
265
+ const paginatedData = useMemo(() => {
266
+ const start = (state.currentPage - 1) * state.pageSize
267
+ return sortedData.slice(start, start + state.pageSize)
268
+ }, [sortedData, state.currentPage, state.pageSize])
269
+
270
+ const hasActiveFilters = useMemo(() => {
271
+ if (state.searchTerm) return true
272
+ return Object.values(state.filters).some((v) => v && v.length > 0)
273
+ }, [state.searchTerm, state.filters])
274
+
275
+ const totalActiveFilterCount = useMemo(() => {
276
+ let count = 0
277
+ for (const v of Object.values(state.filters)) {
278
+ if (v) count += v.length
279
+ }
280
+ return count
281
+ }, [state.filters])
282
+
283
+ const primaryFilterColumns = useMemo<[string, ColumnDef][]>(
284
+ () =>
285
+ Object.entries(domainConfig.columns).filter(
286
+ ([, c]) => c.filterable && c.primaryFilter,
287
+ ) as [string, ColumnDef][],
288
+ [domainConfig],
289
+ )
290
+
291
+ const secondaryFilterColumns = useMemo<[string, ColumnDef][]>(
292
+ () =>
293
+ Object.entries(domainConfig.columns).filter(
294
+ ([, c]) => c.filterable && !c.primaryFilter,
295
+ ) as [string, ColumnDef][],
296
+ [domainConfig],
297
+ )
298
+
299
+ const getFilterOptions = useCallback(
300
+ (columnKey: string): string[] => {
301
+ const col = domainConfig.columns[columnKey]
302
+ if (!col) return []
303
+ if (col.filterOptions) return col.filterOptions
304
+ const vals = new Set<string>()
305
+ for (const row of domainConfig.data) {
306
+ const v = (row as Record<string, unknown>)[columnKey]
307
+ if (v != null && v !== '') vals.add(String(v))
308
+ }
309
+ return [...vals].sort((a, b) => a.localeCompare(b, 'de'))
310
+ },
311
+ [domainConfig],
312
+ )
313
+
314
+ // --- Actions ---
315
+
316
+ const actions = useMemo<DataExplorerActions>(
317
+ () => ({
318
+ switchDomain: (domain) => dispatch({ type: 'SWITCH_DOMAIN', domain }),
319
+ switchView: (mode) => dispatch({ type: 'SWITCH_VIEW', mode }),
320
+ toggleSort: (column) => dispatch({ type: 'TOGGLE_SORT', column }),
321
+ setFilter: (column, values) => dispatch({ type: 'SET_FILTER', column, values }),
322
+ toggleFilterOption: (column, value) =>
323
+ dispatch({ type: 'TOGGLE_FILTER_OPTION', column, value }),
324
+ clearFilter: (column) => dispatch({ type: 'CLEAR_FILTER', column }),
325
+ resetAllFilters: () => dispatch({ type: 'RESET_ALL_FILTERS' }),
326
+ resetSecondaryFilters: () => dispatch({ type: 'RESET_SECONDARY_FILTERS' }),
327
+ setSearchTerm: (term) => dispatch({ type: 'SET_SEARCH_TERM', term }),
328
+ toggleExpand: (rowId) => dispatch({ type: 'TOGGLE_EXPAND', rowId }),
329
+ toggleFavorite: (rowId) => dispatch({ type: 'TOGGLE_FAVORITE', rowId }),
330
+ toggleColumnVisibility: (column) =>
331
+ dispatch({ type: 'TOGGLE_COLUMN_VISIBILITY', column }),
332
+ reorderColumns: (newOrder) => dispatch({ type: 'REORDER_COLUMNS', newOrder }),
333
+ setCurrentPage: (page) => dispatch({ type: 'SET_CURRENT_PAGE', page }),
334
+ setPageSize: (size) => dispatch({ type: 'SET_PAGE_SIZE', size }),
335
+ }),
336
+ [],
337
+ )
338
+
339
+ const derived = useMemo<DataExplorerDerived>(
340
+ () => ({
341
+ domainConfig,
342
+ visibleColumns,
343
+ filteredData: filteredData as Record<string, unknown>[],
344
+ sortedData: sortedData as Record<string, unknown>[],
345
+ paginatedData: paginatedData as Record<string, unknown>[],
346
+ totalFiltered,
347
+ totalPages,
348
+ hasActiveFilters,
349
+ totalActiveFilterCount,
350
+ primaryFilterColumns,
351
+ secondaryFilterColumns,
352
+ getFilterOptions,
353
+ }),
354
+ [
355
+ domainConfig,
356
+ visibleColumns,
357
+ filteredData,
358
+ sortedData,
359
+ paginatedData,
360
+ totalFiltered,
361
+ totalPages,
362
+ hasActiveFilters,
363
+ totalActiveFilterCount,
364
+ primaryFilterColumns,
365
+ secondaryFilterColumns,
366
+ getFilterOptions,
367
+ ],
368
+ )
369
+
370
+ return { state, derived, actions }
371
+ }
@@ -0,0 +1,3 @@
1
+ export { DataExplorerPage } from './DataExplorerPage/DataExplorerPage'
2
+ export type { DomainKey, ViewMode, DataExplorerState } from './types'
3
+ export { useDataExplorerState } from './hooks/use-data-explorer-state'