@fastnd/components 1.0.31 → 1.0.33

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