@fastnd/components 1.0.31 → 1.0.32

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 (25) hide show
  1. package/dist/components/QuickAccessCard/QuickAccessCard.d.ts +12 -0
  2. package/dist/components/ScoreBar/ScoreBar.d.ts +8 -0
  3. package/dist/components/index.d.ts +2 -0
  4. package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +41 -56
  5. package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
  6. package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +175 -0
  7. package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +124 -0
  8. package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +223 -0
  9. package/dist/examples/data-visualization/DataExplorerPagination/DataExplorerPagination.tsx +143 -0
  10. package/dist/examples/data-visualization/DataExplorerToolbar/DataExplorerToolbar.tsx +88 -0
  11. package/dist/examples/data-visualization/DataListView/DataListView.tsx +246 -0
  12. package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +449 -0
  13. package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +140 -0
  14. package/dist/examples/data-visualization/FilterChipGroup/FilterChipGroup.tsx +125 -0
  15. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +132 -0
  16. package/dist/examples/data-visualization/constants.ts +587 -0
  17. package/dist/examples/data-visualization/hooks/use-column-config.ts +76 -0
  18. package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +313 -0
  19. package/dist/examples/data-visualization/index.ts +1 -0
  20. package/dist/examples/data-visualization/types.ts +99 -0
  21. package/dist/examples/quickaccess/QuickAccess/QuickAccess.tsx +97 -0
  22. package/dist/examples/quickaccess/index.ts +2 -0
  23. package/dist/examples/quickaccess/types.ts +11 -0
  24. package/dist/fastnd-components.js +5708 -5590
  25. package/package.json +1 -1
@@ -0,0 +1,449 @@
1
+ import React, { useState, useRef, useCallback } from 'react'
2
+ import { ChevronUp, ChevronDown, ArrowLeftRight, Sparkles, Plus } from 'lucide-react'
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ } from '@/components/ui/table'
11
+ import {
12
+ HoverCard,
13
+ HoverCardContent,
14
+ HoverCardTrigger,
15
+ } from '@/components/ui/hover-card'
16
+ import { Button } from '@/components/ui/button'
17
+ import { Badge } from '@/components/ui/badge'
18
+ import { ScoreBar } from '@/components/ScoreBar/ScoreBar'
19
+ import { renderCell } from '../CellRenderers/CellRenderers'
20
+ import { cn } from '@/lib/utils'
21
+ import type { ColumnDef, ExpandColumnDef, SortState } from '../types'
22
+
23
+ // Icon mapping for expand column headers
24
+ const HEADER_ICONS: Record<string, React.ElementType> = {
25
+ arrowLeftRight: ArrowLeftRight,
26
+ sparkles: Sparkles,
27
+ }
28
+
29
+ // Default minimum widths per column type (px)
30
+ const DEFAULT_MIN_WIDTHS: Partial<Record<string, number>> = {
31
+ favorite: 48,
32
+ expand: 64,
33
+ 'status-badge': 100,
34
+ inventory: 100,
35
+ currency: 100,
36
+ 'double-text': 200,
37
+ link: 140,
38
+ }
39
+
40
+ // Column types that should wrap text (line-clamp handles truncation)
41
+ const WRAPPABLE_TYPES = new Set(['text', 'link', 'double-text'])
42
+
43
+ function computeRowHeight(visibleColumns: string[], columns: Record<string, ColumnDef>): string {
44
+ for (const key of visibleColumns) {
45
+ const col = columns[key]
46
+ if (!col) continue
47
+ if (WRAPPABLE_TYPES.has(col.type)) return 'min-h-[56px]'
48
+ }
49
+ return ''
50
+ }
51
+
52
+ interface ExpansionCellProps {
53
+ mainColKey: string
54
+ expandColKey: string
55
+ mainCol: ColumnDef
56
+ mapLookup: Record<string, ExpandColumnDef>
57
+ item: Record<string, unknown>
58
+ }
59
+
60
+ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: ExpansionCellProps) {
61
+ // Expand column itself → show "Hinzufügen" button
62
+ if (mainColKey === expandColKey) {
63
+ return (
64
+ <Button
65
+ variant="ghost"
66
+ size="icon"
67
+ className="size-7"
68
+ aria-label="Hinzufügen"
69
+ onClick={(e) => e.preventDefault()}
70
+ >
71
+ <Plus className="size-3.5" aria-hidden="true" />
72
+ </Button>
73
+ )
74
+ }
75
+
76
+ const ec = mapLookup[mainColKey]
77
+ if (!ec) return null
78
+
79
+ const val = item[ec.key]
80
+
81
+ // double-text with secondary key
82
+ if (mainCol.type === 'double-text' && ec.secondaryKey) {
83
+ const sec = item[ec.secondaryKey]
84
+ return (
85
+ <>
86
+ <span className={cn('font-semibold text-[13px] line-clamp-2', ec.bold && 'font-bold')}>
87
+ {val != null ? String(val) : ''}
88
+ </span>
89
+ <span className="text-muted-foreground text-xs line-clamp-1">
90
+ {sec != null ? String(sec) : ''}
91
+ </span>
92
+ </>
93
+ )
94
+ }
95
+
96
+ // link
97
+ if (mainCol.type === 'link') {
98
+ return (
99
+ <a
100
+ href="#"
101
+ className="text-primary hover:underline text-[13px]"
102
+ onClick={(e) => e.preventDefault()}
103
+ >
104
+ {val != null ? String(val) : ''}
105
+ </a>
106
+ )
107
+ }
108
+
109
+ // status-badge
110
+ if (mainCol.type === 'status-badge' && mainCol.statusMap && val != null) {
111
+ const status = mainCol.statusMap[String(val)] ?? 'active'
112
+ const STATUS_COLORS: Record<string, string> = {
113
+ active: 'bg-[var(--lifecycle-active-bg)] text-[var(--lifecycle-active)] border-transparent',
114
+ nrnd: 'bg-[var(--lifecycle-nrnd-bg)] text-[var(--lifecycle-nrnd)] border-transparent',
115
+ eol: 'bg-[var(--lifecycle-eol-bg)] text-[var(--lifecycle-eol)] border-transparent',
116
+ production: 'bg-[var(--lifecycle-production-bg)] text-[var(--lifecycle-production)] border-transparent',
117
+ }
118
+ return (
119
+ <Badge className={cn('gap-1.5', STATUS_COLORS[status])}>
120
+ <span
121
+ className={cn('size-1.5 rounded-full', `bg-[var(--lifecycle-${status})]`)}
122
+ aria-hidden="true"
123
+ />
124
+ {String(val)}
125
+ </Badge>
126
+ )
127
+ }
128
+
129
+ // score-bar (expand column own type)
130
+ if (ec.type === 'score-bar') {
131
+ return <ScoreBar value={val != null ? Number(val) : 0} />
132
+ }
133
+
134
+ // muted text
135
+ if (ec.muted) {
136
+ return <span className="text-muted-foreground text-xs line-clamp-3">{val != null ? String(val) : ''}</span>
137
+ }
138
+
139
+ // boolean
140
+ if (typeof val === 'boolean') {
141
+ return <span className="text-[13px]">{val ? 'Ja' : 'Nein'}</span>
142
+ }
143
+
144
+ // default text (ec.bold = semibold, always 3-line clamp per spec)
145
+ return (
146
+ <span className={cn('text-[13px] line-clamp-3', ec.bold && 'font-semibold')}>
147
+ {val != null ? String(val) : ''}
148
+ </span>
149
+ )
150
+ }
151
+
152
+ interface ExpansionSectionProps {
153
+ row: Record<string, unknown>
154
+ colKey: string
155
+ visibleColumns: string[]
156
+ columns: Record<string, ColumnDef>
157
+ }
158
+
159
+ function ExpansionSection({ row, colKey, visibleColumns, columns }: ExpansionSectionProps) {
160
+ const col = columns[colKey]
161
+ const items = row[colKey] as Record<string, unknown>[] | undefined
162
+ if (!items?.length || !col.expandColumns) return null
163
+
164
+ const mapLookup: Record<string, ExpandColumnDef> = {}
165
+ for (const ec of col.expandColumns) {
166
+ if (ec.mapTo) mapLookup[ec.mapTo] = ec
167
+ }
168
+
169
+ const title = col.expandTitleFn ? col.expandTitleFn(row) : col.expandLabel
170
+
171
+ return (
172
+ <>
173
+ {/* Title row */}
174
+ <TableRow className="bg-secondary hover:bg-secondary">
175
+ <TableCell
176
+ colSpan={visibleColumns.length}
177
+ className="py-2 text-xs uppercase tracking-wide font-semibold text-muted-foreground"
178
+ >
179
+ {title}
180
+ </TableCell>
181
+ </TableRow>
182
+
183
+ {/* Data rows */}
184
+ {items.map((item, i) => (
185
+ <TableRow
186
+ key={i}
187
+ className={cn(
188
+ 'bg-accent/50 hover:bg-primary/5',
189
+ i === items.length - 1 && 'border-b-2 border-b-border',
190
+ )}
191
+ >
192
+ {visibleColumns.map((mainKey) => {
193
+ const mainCol = columns[mainKey]
194
+ if (!mainCol) return <TableCell key={mainKey} />
195
+
196
+ return (
197
+ <TableCell
198
+ key={mainKey}
199
+ className={cn(
200
+ mainCol.hideTablet && 'hidden lg:table-cell',
201
+ mainCol.hideMobile && 'hidden sm:table-cell',
202
+ )}
203
+ >
204
+ <ExpansionCell
205
+ mainColKey={mainKey}
206
+ expandColKey={colKey}
207
+ mainCol={mainCol}
208
+ mapLookup={mapLookup}
209
+ item={item}
210
+ />
211
+ </TableCell>
212
+ )
213
+ })}
214
+ </TableRow>
215
+ ))}
216
+ </>
217
+ )
218
+ }
219
+
220
+ export interface DataTableViewProps {
221
+ data: Record<string, unknown>[]
222
+ columns: Record<string, ColumnDef>
223
+ visibleColumns: string[]
224
+ sort: SortState
225
+ onToggleSort: (column: string) => void
226
+ expandedRows: Set<string>
227
+ onToggleExpansion: (rowId: string, field: string) => void
228
+ favorites: Set<string>
229
+ onToggleFavorite: (id: string) => void
230
+ className?: string
231
+ }
232
+
233
+ export function DataTableView({
234
+ data,
235
+ columns,
236
+ visibleColumns,
237
+ sort,
238
+ onToggleSort,
239
+ expandedRows,
240
+ onToggleExpansion,
241
+ favorites,
242
+ onToggleFavorite,
243
+ className,
244
+ }: DataTableViewProps) {
245
+ // Column widths: keyed by column key, value in px
246
+ const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
247
+
248
+ // Resize tracking ref (not state — no re-render during drag)
249
+ const resizeRef = useRef<{
250
+ colKey: string
251
+ startX: number
252
+ startWidth: number
253
+ } | null>(null)
254
+
255
+ const handleResizeMouseDown = useCallback(
256
+ (e: React.MouseEvent, colKey: string) => {
257
+ e.preventDefault()
258
+ const th = (e.target as HTMLElement).closest('th')
259
+ if (!th) return
260
+ // Use actual rendered width so resize starts from wherever the browser laid things out
261
+ const startWidth = columnWidths[colKey] ?? th.getBoundingClientRect().width
262
+ resizeRef.current = { colKey, startX: e.clientX, startWidth }
263
+
264
+ const onMouseMove = (ev: MouseEvent) => {
265
+ if (!resizeRef.current) return
266
+ const delta = ev.clientX - resizeRef.current.startX
267
+ const newWidth = Math.max(60, resizeRef.current.startWidth + delta)
268
+ setColumnWidths((prev) => ({ ...prev, [resizeRef.current!.colKey]: newWidth }))
269
+ }
270
+
271
+ const onMouseUp = () => {
272
+ resizeRef.current = null
273
+ window.removeEventListener('mousemove', onMouseMove)
274
+ window.removeEventListener('mouseup', onMouseUp)
275
+ }
276
+
277
+ window.addEventListener('mousemove', onMouseMove)
278
+ window.addEventListener('mouseup', onMouseUp)
279
+ },
280
+ [columnWidths, columns],
281
+ )
282
+
283
+ const rowHeightClass = computeRowHeight(visibleColumns, columns)
284
+
285
+ // Collect expand column keys for the current domain
286
+ const expandColKeys = visibleColumns.filter((k) => columns[k]?.type === 'expand')
287
+
288
+ return (
289
+ <div
290
+ className={cn('overflow-x-auto', className)}
291
+ role="region"
292
+ aria-label="Datentabelle"
293
+ tabIndex={0}
294
+ >
295
+ <Table>
296
+ <TableHeader>
297
+ <TableRow className="bg-secondary hover:bg-secondary">
298
+ {visibleColumns.map((colKey) => {
299
+ const col = columns[colKey]
300
+ if (!col) return null
301
+
302
+ const isActiveSort = sort.column === colKey
303
+ // Only apply explicit width when user has dragged; otherwise let browser auto-size
304
+ const userWidth = columnWidths[colKey]
305
+
306
+ return (
307
+ <TableHead
308
+ key={colKey}
309
+ scope="col"
310
+ className={cn(
311
+ 'group/th relative select-none',
312
+ col.hideTablet && 'hidden lg:table-cell',
313
+ col.hideMobile && 'hidden sm:table-cell',
314
+ )}
315
+ style={userWidth != null ? { width: userWidth } : undefined}
316
+ aria-sort={
317
+ isActiveSort
318
+ ? sort.direction === 'asc'
319
+ ? 'ascending'
320
+ : 'descending'
321
+ : undefined
322
+ }
323
+ >
324
+ {col.sortable ? (
325
+ // Sortable headers render as ghost Button for proper hover/focus states
326
+ <Button
327
+ variant="ghost"
328
+ size="sm"
329
+ className="-mx-2 gap-1 px-2 font-medium text-xs uppercase tracking-[0.03em] text-foreground"
330
+ onClick={() => onToggleSort(colKey)}
331
+ >
332
+ {col.label}
333
+ <span aria-hidden="true" className="text-muted-foreground">
334
+ {isActiveSort ? (
335
+ sort.direction === 'asc' ? (
336
+ <ChevronUp className="size-3.5" />
337
+ ) : (
338
+ <ChevronDown className="size-3.5" />
339
+ )
340
+ ) : (
341
+ <ChevronDown className="size-3.5 opacity-40" />
342
+ )}
343
+ </span>
344
+ </Button>
345
+ ) : col.headerIcon ? (
346
+ // Icon-only header with hover card for expand columns
347
+ <HoverCard openDelay={200}>
348
+ <HoverCardTrigger asChild>
349
+ <span
350
+ className="inline-flex items-center justify-center cursor-help text-muted-foreground"
351
+ aria-label={col.headerTooltip ?? col.label}
352
+ >
353
+ {React.createElement(HEADER_ICONS[col.headerIcon] ?? ArrowLeftRight, {
354
+ className: 'size-4',
355
+ 'aria-hidden': true,
356
+ })}
357
+ </span>
358
+ </HoverCardTrigger>
359
+ <HoverCardContent side="top" className="w-auto p-2 text-sm">
360
+ {col.headerTooltip ?? col.label}
361
+ </HoverCardContent>
362
+ </HoverCard>
363
+ ) : (
364
+ <span className="text-xs font-medium uppercase tracking-[0.03em]">
365
+ {col.label}
366
+ </span>
367
+ )}
368
+
369
+ {/* Resize handle */}
370
+ <div
371
+ role="separator"
372
+ aria-orientation="vertical"
373
+ className={cn(
374
+ 'absolute right-0 top-0 h-full w-1 cursor-col-resize opacity-0 group-hover/th:opacity-100 transition-opacity',
375
+ 'hover:bg-primary active:bg-primary active:opacity-100',
376
+ )}
377
+ onMouseDown={(e) => handleResizeMouseDown(e, colKey)}
378
+ onClick={(e) => e.stopPropagation()}
379
+ />
380
+ </TableHead>
381
+ )
382
+ })}
383
+ </TableRow>
384
+ </TableHeader>
385
+
386
+ <TableBody>
387
+ {data.map((row, idx) => {
388
+ const rowId = String(row.id ?? idx)
389
+ const expandedColKeys = expandColKeys.filter((k) =>
390
+ expandedRows.has(`${rowId}::${k}`),
391
+ )
392
+ const isFavorite = favorites.has(rowId)
393
+
394
+ return (
395
+ <React.Fragment key={rowId}>
396
+ <TableRow
397
+ className={cn(
398
+ 'animate-in fade-in slide-in-from-bottom-1',
399
+ rowHeightClass,
400
+ expandedColKeys.length > 0 && 'border-b-0',
401
+ )}
402
+ style={{ animationDelay: `${idx * 30}ms` }}
403
+ >
404
+ {visibleColumns.map((colKey) => {
405
+ const col = columns[colKey]
406
+ if (!col) return null
407
+
408
+ const isExpanded = expandedRows.has(`${rowId}::${colKey}`)
409
+
410
+ return (
411
+ <TableCell
412
+ key={colKey}
413
+ className={cn(
414
+ col.hideTablet && 'hidden lg:table-cell',
415
+ col.hideMobile && 'hidden sm:table-cell',
416
+ // Allow wrappable columns to shrink; line-clamp handles truncation
417
+ WRAPPABLE_TYPES.has(col.type) && 'whitespace-normal',
418
+ )}
419
+ style={columnWidths[colKey] != null ? { width: columnWidths[colKey] } : undefined}
420
+ >
421
+ {renderCell(colKey, col, row, {
422
+ isExpanded,
423
+ isFavorite,
424
+ onToggleExpand: () => onToggleExpansion(rowId, colKey),
425
+ onToggleFavorite: () => onToggleFavorite(rowId),
426
+ })}
427
+ </TableCell>
428
+ )
429
+ })}
430
+ </TableRow>
431
+
432
+ {/* Expansion sections for each expanded column */}
433
+ {expandedColKeys.map((expColKey) => (
434
+ <ExpansionSection
435
+ key={`${rowId}::${expColKey}`}
436
+ row={row}
437
+ colKey={expColKey}
438
+ visibleColumns={visibleColumns}
439
+ columns={columns}
440
+ />
441
+ ))}
442
+ </React.Fragment>
443
+ )
444
+ })}
445
+ </TableBody>
446
+ </Table>
447
+ </div>
448
+ )
449
+ }
@@ -0,0 +1,140 @@
1
+ import { Search } from 'lucide-react'
2
+ import { cn } from '@/lib/utils'
3
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
4
+ import { useDataExplorerState } from '../hooks/use-data-explorer-state'
5
+ import { useColumnConfig } from '../hooks/use-column-config'
6
+ import { DOMAIN_KEYS, DATA_SOURCES } from '../constants'
7
+ import type { DomainKey } from '../types'
8
+ import { DataExplorerToolbar } from '../DataExplorerToolbar/DataExplorerToolbar'
9
+ import { FilterChipGroup } from '../FilterChipGroup/FilterChipGroup'
10
+ import { MoreFiltersPopover } from '../MoreFiltersPopover/MoreFiltersPopover'
11
+ import { ColumnConfigPopover } from '../ColumnConfigPopover/ColumnConfigPopover'
12
+ import { DataTableView } from '../DataTableView/DataTableView'
13
+ import { DataListView } from '../DataListView/DataListView'
14
+ import { DataCardView } from '../DataCardView/DataCardView'
15
+ import { DataExplorerPagination } from '../DataExplorerPagination/DataExplorerPagination'
16
+
17
+ export function DataVisualizationPage({ className }: { className?: string }) {
18
+ const state = useDataExplorerState()
19
+ const columnConfig = useColumnConfig({ activeDomain: state.activeDomain })
20
+
21
+ const visibleColumns = columnConfig.visibleColumns
22
+
23
+ return (
24
+ <div data-slot="data-visualization-page" className={cn('flex flex-col border border-border rounded-lg overflow-hidden bg-card', className)}>
25
+ <Tabs
26
+ value={state.activeDomain}
27
+ onValueChange={(v) => state.setActiveDomain(v as DomainKey)}
28
+ >
29
+ <TabsList variant="line" className="w-full justify-start border-b border-border bg-card px-6">
30
+ {DOMAIN_KEYS.map((dk) => (
31
+ <TabsTrigger key={dk} value={dk}>
32
+ {DATA_SOURCES[dk].label}
33
+ </TabsTrigger>
34
+ ))}
35
+ </TabsList>
36
+ </Tabs>
37
+
38
+ <DataExplorerToolbar
39
+ viewMode={state.viewMode}
40
+ onViewModeChange={state.setViewMode}
41
+ searchTerm={state.searchTerm}
42
+ onSearchChange={state.setSearchTerm}
43
+ hasActiveFilters={state.hasActiveFilters}
44
+ onResetAll={state.clearAllFilters}
45
+ filterSlot={
46
+ <>
47
+ <FilterChipGroup
48
+ columns={state.domainConfig.columns}
49
+ filters={state.filters}
50
+ onToggleFilter={state.toggleFilter}
51
+ onClearFilter={state.clearFilter}
52
+ getFilterOptions={state.getFilterOptions}
53
+ />
54
+ <MoreFiltersPopover
55
+ columns={state.domainConfig.columns}
56
+ filters={state.filters}
57
+ onToggleFilter={state.toggleFilter}
58
+ onClearSecondaryFilters={state.clearSecondaryFilters}
59
+ getFilterOptions={state.getFilterOptions}
60
+ />
61
+ </>
62
+ }
63
+ configSlot={
64
+ <ColumnConfigPopover
65
+ columns={state.domainConfig.columns}
66
+ columnOrder={columnConfig.columnOrder}
67
+ columnVisibility={columnConfig.columnVisibility}
68
+ onReorderColumn={columnConfig.reorderColumn}
69
+ onToggleVisibility={columnConfig.toggleColumnVisibility}
70
+ />
71
+ }
72
+ />
73
+
74
+ {state.totalFiltered === 0 ? (
75
+ <div
76
+ data-slot="empty-state"
77
+ className="flex flex-col items-center justify-center px-6 py-16 text-center"
78
+ >
79
+ <Search className="size-12 text-muted-foreground/50 mb-4" />
80
+ <h2 className="text-base font-semibold text-foreground mb-1">
81
+ Keine Ergebnisse
82
+ </h2>
83
+ <p className="text-[13px] text-muted-foreground max-w-[360px]">
84
+ Es wurden keine Eintr&auml;ge gefunden, die den aktuellen Filtern entsprechen.
85
+ </p>
86
+ </div>
87
+ ) : (
88
+ <>
89
+ {state.viewMode === 'table' && (
90
+ <DataTableView
91
+ data={state.paginatedData}
92
+ columns={state.domainConfig.columns}
93
+ visibleColumns={visibleColumns}
94
+ sort={state.sort}
95
+ onToggleSort={state.toggleSort}
96
+ expandedRows={state.expandedRows}
97
+ onToggleExpansion={state.toggleExpansion}
98
+ favorites={state.favorites}
99
+ onToggleFavorite={state.toggleFavorite}
100
+ />
101
+ )}
102
+ {state.viewMode === 'list' && (
103
+ <DataListView
104
+ data={state.paginatedData}
105
+ columns={state.domainConfig.columns}
106
+ layout={state.domainConfig.layout.list}
107
+ expandedRows={state.expandedRows}
108
+ onToggleExpansion={state.toggleExpansion}
109
+ favorites={state.favorites}
110
+ onToggleFavorite={state.toggleFavorite}
111
+ />
112
+ )}
113
+ {state.viewMode === 'card' && (
114
+ <DataCardView
115
+ data={state.paginatedData}
116
+ columns={state.domainConfig.columns}
117
+ layout={state.domainConfig.layout.card}
118
+ expandedRows={state.expandedRows}
119
+ onToggleExpansion={state.toggleExpansion}
120
+ favorites={state.favorites}
121
+ onToggleFavorite={state.toggleFavorite}
122
+ />
123
+ )}
124
+ </>
125
+ )}
126
+
127
+ {state.totalFiltered > 0 && (
128
+ <DataExplorerPagination
129
+ currentPage={state.currentPage}
130
+ totalPages={state.totalPages}
131
+ pageSize={state.pageSize}
132
+ totalFiltered={state.totalFiltered}
133
+ resultLabel={state.domainConfig.resultLabel}
134
+ onPageChange={state.setPage}
135
+ onPageSizeChange={state.setPageSize}
136
+ />
137
+ )}
138
+ </div>
139
+ )
140
+ }