@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.
- package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +20 -0
- package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +9 -0
- package/dist/components/ProgressCircle/ProgressCircle.d.ts +9 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/input-group.d.ts +1 -1
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/components/ui/tabs.d.ts +1 -1
- package/dist/examples/dashboard/DashboardPage/DashboardPage.tsx +54 -0
- package/dist/examples/dashboard/ProjectList/ProjectList.tsx +120 -0
- package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +123 -0
- package/dist/examples/dashboard/StatusFilterLegend/StatusFilterLegend.tsx +70 -0
- package/dist/examples/dashboard/StatusOverview/StatusOverview.tsx +51 -0
- package/dist/examples/dashboard/constants.ts +18 -0
- package/dist/examples/dashboard/hooks/use-dashboard-state.ts +98 -0
- package/dist/examples/dashboard/index.ts +6 -0
- package/dist/examples/dashboard/types.ts +19 -0
- package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +136 -0
- package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +179 -0
- package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +190 -0
- package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +43 -0
- package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +111 -0
- package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +282 -0
- package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +283 -0
- package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +41 -0
- package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +139 -0
- package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +230 -0
- package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +33 -0
- package/dist/examples/data-visualization/cell-renderers.tsx +119 -0
- package/dist/examples/data-visualization/constants.ts +1251 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +65 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +40 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +41 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +61 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +32 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +32 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +133 -0
- package/dist/examples/data-visualization/hooks/use-filtered-data.ts +84 -0
- package/dist/examples/data-visualization/index.ts +10 -0
- package/dist/examples/data-visualization/types.ts +103 -0
- package/dist/fastnd-components.js +18759 -15519
- 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 }
|