@fastnd/components 1.0.26 → 1.0.28

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 (43) hide show
  1. package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +20 -0
  2. package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +9 -0
  3. package/dist/components/ProgressCircle/ProgressCircle.d.ts +9 -0
  4. package/dist/components/index.d.ts +3 -0
  5. package/dist/components/ui/badge.d.ts +1 -1
  6. package/dist/components/ui/button.d.ts +1 -1
  7. package/dist/components/ui/input-group.d.ts +1 -1
  8. package/dist/components/ui/item.d.ts +1 -1
  9. package/dist/components/ui/tabs.d.ts +1 -1
  10. package/dist/examples/dashboard/DashboardPage/DashboardPage.tsx +54 -0
  11. package/dist/examples/dashboard/ProjectList/ProjectList.tsx +120 -0
  12. package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +123 -0
  13. package/dist/examples/dashboard/StatusFilterLegend/StatusFilterLegend.tsx +70 -0
  14. package/dist/examples/dashboard/StatusOverview/StatusOverview.tsx +51 -0
  15. package/dist/examples/dashboard/constants.ts +18 -0
  16. package/dist/examples/dashboard/hooks/use-dashboard-state.ts +98 -0
  17. package/dist/examples/dashboard/index.ts +6 -0
  18. package/dist/examples/dashboard/types.ts +19 -0
  19. package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +136 -0
  20. package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +179 -0
  21. package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +190 -0
  22. package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +43 -0
  23. package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +111 -0
  24. package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +282 -0
  25. package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +283 -0
  26. package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +41 -0
  27. package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +139 -0
  28. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +230 -0
  29. package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +33 -0
  30. package/dist/examples/data-visualization/cell-renderers.tsx +119 -0
  31. package/dist/examples/data-visualization/constants.ts +1251 -0
  32. package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +65 -0
  33. package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +40 -0
  34. package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +41 -0
  35. package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +61 -0
  36. package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +32 -0
  37. package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +32 -0
  38. package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +133 -0
  39. package/dist/examples/data-visualization/hooks/use-filtered-data.ts +84 -0
  40. package/dist/examples/data-visualization/index.ts +10 -0
  41. package/dist/examples/data-visualization/types.ts +103 -0
  42. package/dist/fastnd-components.js +18759 -15519
  43. package/package.json +2 -1
@@ -0,0 +1,136 @@
1
+ import * as React from 'react'
2
+ import { useMemo } from 'react'
3
+ import { SearchX } from 'lucide-react'
4
+ import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
5
+ import { cn } from '@/lib/utils'
6
+ import { useDataGridState } from '../hooks/use-data-grid-state'
7
+ import { DataGridToolbar } from '../DataGridToolbar/DataGridToolbar'
8
+ import { DataGridTableView } from '../DataGridTableView/DataGridTableView'
9
+ import { DataGridListView } from '../DataGridListView/DataGridListView'
10
+ import { DataGridCardView } from '../DataGridCardView/DataGridCardView'
11
+ import { DataGridPagination } from '../DataGridPagination/DataGridPagination'
12
+ import type { DomainConfig } from '../types'
13
+
14
+ interface DataGridProps extends React.ComponentProps<'div'> {
15
+ domainConfig: DomainConfig
16
+ }
17
+
18
+ const DataGrid = React.forwardRef<HTMLDivElement, DataGridProps>(
19
+ ({ domainConfig, className, ...props }, ref) => {
20
+ const state = useDataGridState(domainConfig)
21
+
22
+ const filterOptions = useMemo(() => {
23
+ const opts: Record<string, string[]> = {}
24
+ for (const [key, config] of Object.entries(domainConfig.columns)) {
25
+ if (!config.filterable) continue
26
+ const uniqueValues = [
27
+ ...new Set(
28
+ domainConfig.data
29
+ .map((row) => String(row[key] ?? ''))
30
+ .filter(Boolean)
31
+ ),
32
+ ].sort()
33
+ opts[key] = uniqueValues
34
+ }
35
+ return opts
36
+ }, [domainConfig])
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ data-slot="data-grid"
42
+ className={cn('flex flex-col border border-border rounded-lg', className)}
43
+ {...props}
44
+ >
45
+ <DataGridToolbar
46
+ currentView={state.currentView}
47
+ onViewChange={state.setCurrentView}
48
+ columns={state.columnConfig}
49
+ columnOrder={state.columnOrder}
50
+ visibleColumns={state.visibleColumns}
51
+ filters={state.filters}
52
+ filterOptions={filterOptions}
53
+ onFilterChange={state.setFilter}
54
+ searchQuery={state.searchQuery}
55
+ onSearchChange={state.setSearchQuery}
56
+ hasActiveFilters={state.hasActiveFilters}
57
+ onResetFilters={state.resetFilters}
58
+ onToggleColumnVisibility={state.toggleColumnVisibility}
59
+ onReorderColumns={state.reorderColumns}
60
+ />
61
+
62
+ {state.filteredData.length === 0 ? (
63
+ <Empty className="py-16">
64
+ <EmptyHeader>
65
+ <EmptyMedia variant="icon">
66
+ <SearchX />
67
+ </EmptyMedia>
68
+ <EmptyTitle>Keine Ergebnisse</EmptyTitle>
69
+ <EmptyDescription>
70
+ Es wurden keine Einträge gefunden, die den aktuellen Filtern entsprechen.
71
+ </EmptyDescription>
72
+ </EmptyHeader>
73
+ </Empty>
74
+ ) : (
75
+ <>
76
+ {state.currentView === 'table' && (
77
+ <DataGridTableView
78
+ data={state.pagedData}
79
+ columns={state.orderedVisibleColumns}
80
+ sortColumn={state.sortColumn}
81
+ sortDirection={state.sortDirection}
82
+ onToggleSort={state.toggleSort}
83
+ expandedRows={state.expandedRows}
84
+ onToggleExpand={state.toggleExpand}
85
+ favorites={state.favorites}
86
+ onToggleFavorite={state.toggleFavorite}
87
+ columnWidths={state.columnWidths}
88
+ onResizeColumn={state.resizeColumn}
89
+ />
90
+ )}
91
+ {state.currentView === 'list' && (
92
+ <DataGridListView
93
+ data={state.pagedData}
94
+ columnConfig={state.columnConfig}
95
+ layout={state.layout.list}
96
+ visibleColumns={state.visibleColumns}
97
+ columnOrder={state.columnOrder}
98
+ favorites={state.favorites}
99
+ onToggleFavorite={state.toggleFavorite}
100
+ expandedRows={state.expandedRows}
101
+ onToggleExpand={state.toggleExpand}
102
+ />
103
+ )}
104
+ {state.currentView === 'cards' && (
105
+ <DataGridCardView
106
+ data={state.pagedData}
107
+ columnConfig={state.columnConfig}
108
+ layout={state.layout.card}
109
+ visibleColumns={state.visibleColumns}
110
+ favorites={state.favorites}
111
+ onToggleFavorite={state.toggleFavorite}
112
+ expandedRows={state.expandedRows}
113
+ onToggleExpand={state.toggleExpand}
114
+ />
115
+ )}
116
+ </>
117
+ )}
118
+
119
+ <DataGridPagination
120
+ currentPage={state.currentPage}
121
+ totalPages={state.totalPages}
122
+ pageSize={state.pageSize}
123
+ totalFiltered={state.filteredCount}
124
+ label={domainConfig.resultLabel}
125
+ onPageChange={state.setCurrentPage}
126
+ onPageSizeChange={state.setPageSize}
127
+ />
128
+ </div>
129
+ )
130
+ }
131
+ )
132
+
133
+ DataGrid.displayName = 'DataGrid'
134
+
135
+ export { DataGrid }
136
+ export type { DataGridProps }
@@ -0,0 +1,179 @@
1
+ import * as React from 'react'
2
+ import { ChevronDown } from 'lucide-react'
3
+ import { Card } from '@/components/ui/card'
4
+ import { Button } from '@/components/ui/button'
5
+ import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
6
+ import { cellRenderers } from '../cell-renderers'
7
+ import { cn } from '@/lib/utils'
8
+ import type { CardLayoutConfig, CellType, ColumnConfig, DataRow } from '../types'
9
+
10
+ const BADGE_TYPES = new Set<CellType>(['status-badge', 'inventory', 'inventory-label'])
11
+ const SKIP_TYPES = new Set<CellType>(['favorite'])
12
+
13
+ interface DataGridCardViewProps extends React.ComponentProps<'div'> {
14
+ data: DataRow[]
15
+ columnConfig: Record<string, ColumnConfig>
16
+ layout: CardLayoutConfig
17
+ visibleColumns: Set<string>
18
+ favorites: Set<string>
19
+ onToggleFavorite: (rowId: string) => void
20
+ expandedRows: Set<string>
21
+ onToggleExpand: (rowId: string, expandKey: string) => void
22
+ }
23
+
24
+ // Finds extra visible badge columns not already listed in layout.badgeFields.
25
+ function deriveExtraBadgeFields(
26
+ columnConfig: Record<string, ColumnConfig>,
27
+ visibleColumns: Set<string>,
28
+ layout: CardLayoutConfig
29
+ ): string[] {
30
+ const layoutFields = new Set([
31
+ layout.titleField,
32
+ ...(layout.subtitleField ? [layout.subtitleField] : []),
33
+ ...layout.badgeFields,
34
+ ...layout.rows.map((r) => r.field),
35
+ ...(layout.footerField ? [layout.footerField] : []),
36
+ ...(layout.expandField ? [layout.expandField] : []),
37
+ ])
38
+
39
+ const extra: string[] = []
40
+ for (const key of visibleColumns) {
41
+ if (layoutFields.has(key)) continue
42
+ const config = columnConfig[key]
43
+ if (!config) continue
44
+ if (SKIP_TYPES.has(config.type)) continue
45
+ if (BADGE_TYPES.has(config.type)) {
46
+ extra.push(key)
47
+ }
48
+ }
49
+ return extra
50
+ }
51
+
52
+ const DataGridCardView = React.forwardRef<HTMLDivElement, DataGridCardViewProps>(
53
+ (
54
+ {
55
+ data,
56
+ columnConfig,
57
+ layout,
58
+ visibleColumns,
59
+ favorites,
60
+ onToggleFavorite,
61
+ expandedRows,
62
+ onToggleExpand,
63
+ className,
64
+ ...props
65
+ },
66
+ ref
67
+ ) => {
68
+ const extraBadgeFields = deriveExtraBadgeFields(columnConfig, visibleColumns, layout)
69
+ const allBadgeFields = [...layout.badgeFields, ...extraBadgeFields]
70
+
71
+ return (
72
+ <div
73
+ ref={ref}
74
+ data-slot="data-grid-card-view"
75
+ className={cn(
76
+ 'grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 p-5',
77
+ className
78
+ )}
79
+ {...props}
80
+ >
81
+ {data.map((row) => {
82
+ const titleValue = String(row[layout.titleField] ?? '')
83
+ const expandKey = layout.expandField
84
+ const expandItems = expandKey && Array.isArray(row[expandKey])
85
+ ? (row[expandKey] as unknown[])
86
+ : []
87
+ const compositeKey = expandKey ? `${row.id}::${expandKey}` : ''
88
+ const isExpanded = compositeKey ? expandedRows.has(compositeKey) : false
89
+
90
+ return (
91
+ <Card
92
+ key={row.id}
93
+ className="flex flex-col gap-3 p-4 hover:shadow-md transition-shadow"
94
+ >
95
+ {/* Header */}
96
+ <div className="flex items-start justify-between">
97
+ <div className="min-w-0">
98
+ <div className="font-medium text-sm truncate">{titleValue}</div>
99
+ {layout.subtitleField && (
100
+ <div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
101
+ {String(row[layout.subtitleField] ?? '')}
102
+ </div>
103
+ )}
104
+ </div>
105
+ <FavoriteButton
106
+ pressed={favorites.has(row.id)}
107
+ projectName={titleValue}
108
+ onPressedChange={() => onToggleFavorite(row.id)}
109
+ />
110
+ </div>
111
+
112
+ {/* Badges */}
113
+ {allBadgeFields.length > 0 && (
114
+ <div className="flex gap-1.5 flex-wrap">
115
+ {allBadgeFields.map((field) => {
116
+ const config = columnConfig[field]
117
+ if (!config) return null
118
+ const rendererType: CellType =
119
+ config.type === 'inventory' ? 'inventory-label' : config.type
120
+ return (
121
+ <React.Fragment key={field}>
122
+ {cellRenderers[rendererType]?.(row[field], row, config)}
123
+ </React.Fragment>
124
+ )
125
+ })}
126
+ </div>
127
+ )}
128
+
129
+ {/* Data rows */}
130
+ {layout.rows.map((rowConfig) => {
131
+ const config = columnConfig[rowConfig.field]
132
+ if (!config) return null
133
+ const rendererType: CellType = rowConfig.rendererOverride ?? config.type
134
+ return (
135
+ <div key={rowConfig.field} className="flex justify-between items-center">
136
+ <span className="text-xs uppercase tracking-wider text-muted-foreground">
137
+ {rowConfig.label}
138
+ </span>
139
+ <span className="text-sm font-medium font-[family-name:var(--font-grotesk)]">
140
+ {cellRenderers[rendererType]?.(row[rowConfig.field], row, config)}
141
+ </span>
142
+ </div>
143
+ )
144
+ })}
145
+
146
+ {/* Footer */}
147
+ {layout.footerField && (
148
+ <div className="flex items-center justify-between pt-2.5 border-t border-border">
149
+ <span className="text-xs text-primary font-medium">
150
+ {String(row[layout.footerField] ?? '')}
151
+ </span>
152
+ {expandKey && (
153
+ <Button
154
+ variant="ghost"
155
+ size="sm"
156
+ onClick={() => onToggleExpand(row.id, expandKey)}
157
+ aria-expanded={isExpanded}
158
+ >
159
+ <ChevronDown
160
+ className={cn('transition-transform', isExpanded && 'rotate-180')}
161
+ size={14}
162
+ />
163
+ {expandItems.length}
164
+ </Button>
165
+ )}
166
+ </div>
167
+ )}
168
+ </Card>
169
+ )
170
+ })}
171
+ </div>
172
+ )
173
+ }
174
+ )
175
+
176
+ DataGridCardView.displayName = 'DataGridCardView'
177
+
178
+ export { DataGridCardView }
179
+ export type { DataGridCardViewProps }
@@ -0,0 +1,190 @@
1
+ import * as React from 'react'
2
+ import { ChevronDown } from 'lucide-react'
3
+ import { Button } from '@/components/ui/button'
4
+ import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
5
+ import { cellRenderers } from '../cell-renderers'
6
+ import { cn } from '@/lib/utils'
7
+ import type { ColumnConfig, DataRow, ListLayoutConfig } from '../types'
8
+
9
+ const BADGE_TYPES = new Set(['status-badge', 'inventory', 'inventory-label'])
10
+ const SKIP_TYPES = new Set(['favorite'])
11
+
12
+ interface DataGridListViewProps extends React.ComponentProps<'div'> {
13
+ data: DataRow[]
14
+ columnConfig: Record<string, ColumnConfig>
15
+ layout: ListLayoutConfig
16
+ visibleColumns: Set<string>
17
+ columnOrder: string[]
18
+ favorites: Set<string>
19
+ onToggleFavorite: (rowId: string) => void
20
+ expandedRows: Set<string>
21
+ onToggleExpand: (rowId: string, expandKey: string) => void
22
+ }
23
+
24
+ // Derives the extra visible columns not already covered by the layout fields.
25
+ function deriveExtraFields(
26
+ columnConfig: Record<string, ColumnConfig>,
27
+ visibleColumns: Set<string>,
28
+ columnOrder: string[],
29
+ layout: ListLayoutConfig
30
+ ): { extraMeta: string[]; extraBadge: string[]; extraExpand: string[] } {
31
+ const layoutFields = new Set([
32
+ layout.titleField,
33
+ ...layout.metaFields,
34
+ ...layout.badgeFields,
35
+ ...(layout.valueField ? [layout.valueField] : []),
36
+ ...(layout.expandField ? [layout.expandField] : []),
37
+ ])
38
+
39
+ const extraMeta: string[] = []
40
+ const extraBadge: string[] = []
41
+ const extraExpand: string[] = []
42
+
43
+ for (const key of columnOrder) {
44
+ if (!visibleColumns.has(key) || layoutFields.has(key)) continue
45
+ const config = columnConfig[key]
46
+ if (!config) continue
47
+ if (SKIP_TYPES.has(config.type)) continue
48
+ if (BADGE_TYPES.has(config.type)) {
49
+ extraBadge.push(key)
50
+ } else if (config.type === 'expand') {
51
+ extraExpand.push(key)
52
+ } else {
53
+ extraMeta.push(key)
54
+ }
55
+ }
56
+
57
+ return { extraMeta, extraBadge, extraExpand }
58
+ }
59
+
60
+ const DataGridListView = React.forwardRef<HTMLDivElement, DataGridListViewProps>(
61
+ (
62
+ {
63
+ data,
64
+ columnConfig,
65
+ layout,
66
+ visibleColumns,
67
+ columnOrder,
68
+ favorites,
69
+ onToggleFavorite,
70
+ expandedRows,
71
+ onToggleExpand,
72
+ className,
73
+ ...props
74
+ },
75
+ ref
76
+ ) => {
77
+ const { extraMeta, extraBadge, extraExpand } = deriveExtraFields(
78
+ columnConfig,
79
+ visibleColumns,
80
+ columnOrder,
81
+ layout
82
+ )
83
+
84
+ const allMetaFields = [...layout.metaFields, ...extraMeta]
85
+ const allBadgeFields = [...layout.badgeFields, ...extraBadge]
86
+ const allExpandFields = [
87
+ ...(layout.expandField ? [layout.expandField] : []),
88
+ ...extraExpand,
89
+ ]
90
+
91
+ return (
92
+ <div
93
+ ref={ref}
94
+ data-slot="data-grid-list-view"
95
+ className={cn('divide-y divide-border', className)}
96
+ {...props}
97
+ >
98
+ {data.map((row) => {
99
+ const titleValue = String(row[layout.titleField] ?? '')
100
+
101
+ return (
102
+ <div
103
+ key={row.id}
104
+ className="flex items-center gap-3 px-5 py-3 hover:bg-muted/50 transition-colors"
105
+ >
106
+ <FavoriteButton
107
+ pressed={favorites.has(row.id)}
108
+ projectName={titleValue}
109
+ onPressedChange={() => onToggleFavorite(row.id)}
110
+ />
111
+
112
+ <div className="flex-1 min-w-0">
113
+ <div className="font-medium text-sm truncate">{titleValue}</div>
114
+
115
+ {allMetaFields.length > 0 && (
116
+ <div className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap">
117
+ {allMetaFields.map((field, index) => {
118
+ const config = columnConfig[field]
119
+ if (!config) return null
120
+ const rendered = cellRenderers[config.type]?.(
121
+ row[field],
122
+ row,
123
+ config
124
+ )
125
+ return (
126
+ <React.Fragment key={field}>
127
+ {index > 0 && <span aria-hidden="true">·</span>}
128
+ <span>{rendered}</span>
129
+ </React.Fragment>
130
+ )
131
+ })}
132
+ </div>
133
+ )}
134
+ </div>
135
+
136
+ {allBadgeFields.length > 0 && (
137
+ <div className="flex items-center gap-1.5 shrink-0">
138
+ {allBadgeFields.map((field) => {
139
+ const config = columnConfig[field]
140
+ if (!config) return null
141
+ // Use inventory-label instead of inventory in list view for readability
142
+ const rendererType =
143
+ config.type === 'inventory' ? 'inventory-label' : config.type
144
+ return (
145
+ <React.Fragment key={field}>
146
+ {cellRenderers[rendererType]?.(row[field], row, config)}
147
+ </React.Fragment>
148
+ )
149
+ })}
150
+ </div>
151
+ )}
152
+
153
+ {allExpandFields.length > 0 && (
154
+ <div className="shrink-0 flex items-center gap-1">
155
+ {allExpandFields.map((expandKey) => {
156
+ const config = columnConfig[expandKey]
157
+ if (!config) return null
158
+ const items = Array.isArray(row[expandKey]) ? (row[expandKey] as unknown[]) : []
159
+ const compositeKey = `${row.id}::${expandKey}`
160
+ const isExpanded = expandedRows.has(compositeKey)
161
+ return (
162
+ <Button
163
+ key={expandKey}
164
+ variant="ghost"
165
+ size="sm"
166
+ onClick={() => onToggleExpand(row.id, expandKey)}
167
+ aria-expanded={isExpanded}
168
+ >
169
+ <ChevronDown
170
+ className={cn('transition-transform', isExpanded && 'rotate-180')}
171
+ size={14}
172
+ />
173
+ {items.length}
174
+ </Button>
175
+ )
176
+ })}
177
+ </div>
178
+ )}
179
+ </div>
180
+ )
181
+ })}
182
+ </div>
183
+ )
184
+ }
185
+ )
186
+
187
+ DataGridListView.displayName = 'DataGridListView'
188
+
189
+ export { DataGridListView }
190
+ export type { DataGridListViewProps }
@@ -0,0 +1,43 @@
1
+ import * as React from 'react'
2
+ import { useState } from 'react'
3
+ import { cn } from '@/lib/utils'
4
+ import { DomainSwitcher } from '../DomainSwitcher/DomainSwitcher'
5
+ import { DataGrid } from '../DataGrid/DataGrid'
6
+ import type { DomainConfig } from '../types'
7
+
8
+ interface DataGridPageProps extends React.ComponentProps<'div'> {
9
+ domains: DomainConfig[]
10
+ }
11
+
12
+ const DataGridPage = React.forwardRef<HTMLDivElement, DataGridPageProps>(
13
+ ({ domains, className, ...props }, ref) => {
14
+ const [activeDomain, setActiveDomain] = useState(domains[0]?.key ?? '')
15
+ const activeDomainConfig = domains.find((d) => d.key === activeDomain) ?? domains[0]
16
+
17
+ return (
18
+ <div
19
+ ref={ref}
20
+ data-slot="data-grid-page"
21
+ className={cn('flex flex-col gap-4', className)}
22
+ {...props}
23
+ >
24
+ <DomainSwitcher
25
+ domains={domains.map((d) => ({ key: d.key, label: d.label }))}
26
+ activeDomain={activeDomain}
27
+ onDomainChange={setActiveDomain}
28
+ />
29
+ {activeDomainConfig && (
30
+ <DataGrid
31
+ key={activeDomainConfig.key}
32
+ domainConfig={activeDomainConfig}
33
+ />
34
+ )}
35
+ </div>
36
+ )
37
+ }
38
+ )
39
+
40
+ DataGridPage.displayName = 'DataGridPage'
41
+
42
+ export { DataGridPage }
43
+ export type { DataGridPageProps }
@@ -0,0 +1,111 @@
1
+ import * as React from 'react'
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { Button } from '@/components/ui/button'
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from '@/components/ui/select'
11
+ import { cn } from '@/lib/utils'
12
+ import type { PageSize } from '../hooks/use-data-grid-pagination'
13
+
14
+ interface DataGridPaginationProps extends React.ComponentProps<'div'> {
15
+ currentPage: number
16
+ totalPages: number
17
+ pageSize: PageSize
18
+ totalFiltered: number
19
+ label: string
20
+ onPageChange: (page: number) => void
21
+ onPageSizeChange: (size: PageSize) => void
22
+ }
23
+
24
+ const DataGridPagination = React.forwardRef<HTMLDivElement, DataGridPaginationProps>(
25
+ (
26
+ {
27
+ currentPage,
28
+ totalPages,
29
+ pageSize,
30
+ totalFiltered,
31
+ label,
32
+ onPageChange,
33
+ onPageSizeChange,
34
+ className,
35
+ ...props
36
+ },
37
+ ref
38
+ ) => {
39
+ return (
40
+ <div
41
+ ref={ref}
42
+ data-slot="data-grid-pagination"
43
+ role="navigation"
44
+ aria-label="Pagination"
45
+ className={cn(
46
+ 'flex items-center justify-between border-t border-border px-5 py-2 text-xs text-muted-foreground',
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ {/* Left: page size selector */}
52
+ <div className="flex items-center gap-2">
53
+ <span>Zeilen pro Seite</span>
54
+ <Select
55
+ value={String(pageSize)}
56
+ onValueChange={(val) => onPageSizeChange(Number(val) as PageSize)}
57
+ >
58
+ <SelectTrigger
59
+ className="h-7 w-[70px] text-xs"
60
+ aria-label="Zeilen pro Seite"
61
+ >
62
+ <SelectValue />
63
+ </SelectTrigger>
64
+ <SelectContent>
65
+ <SelectItem value="25">25</SelectItem>
66
+ <SelectItem value="50">50</SelectItem>
67
+ <SelectItem value="100">100</SelectItem>
68
+ </SelectContent>
69
+ </Select>
70
+ </div>
71
+
72
+ {/* Center: page info */}
73
+ <span aria-live="polite">
74
+ Seite {currentPage} von {totalPages}
75
+ {' · '}
76
+ <span className="font-[family-name:var(--font-grotesk)] font-semibold">{totalFiltered}</span>
77
+ {' '}{label}
78
+ </span>
79
+
80
+ {/* Right: prev / next */}
81
+ <div className="flex items-center gap-1">
82
+ <Button
83
+ variant="outline"
84
+ size="sm"
85
+ className="h-7 px-2"
86
+ disabled={currentPage <= 1}
87
+ onClick={() => onPageChange(currentPage - 1)}
88
+ aria-label="Vorherige Seite"
89
+ >
90
+ <ChevronLeft className="size-4" aria-hidden="true" />
91
+ </Button>
92
+ <Button
93
+ variant="outline"
94
+ size="sm"
95
+ className="h-7 px-2"
96
+ disabled={currentPage >= totalPages}
97
+ onClick={() => onPageChange(currentPage + 1)}
98
+ aria-label="Nächste Seite"
99
+ >
100
+ <ChevronRight className="size-4" aria-hidden="true" />
101
+ </Button>
102
+ </div>
103
+ </div>
104
+ )
105
+ }
106
+ )
107
+
108
+ DataGridPagination.displayName = 'DataGridPagination'
109
+
110
+ export { DataGridPagination }
111
+ export type { DataGridPaginationProps }