@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,282 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { ArrowUp, ArrowDown, ArrowUpDown, ChevronDown } from 'lucide-react'
|
|
3
|
+
import {
|
|
4
|
+
Table,
|
|
5
|
+
TableHeader,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableRow,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableCell,
|
|
10
|
+
} from '@/components/ui/table'
|
|
11
|
+
import { Button } from '@/components/ui/button'
|
|
12
|
+
import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
|
|
13
|
+
import { ExpansionDrawer } from '../ExpansionDrawer/ExpansionDrawer'
|
|
14
|
+
import { cellRenderers } from '../cell-renderers'
|
|
15
|
+
import { cn } from '@/lib/utils'
|
|
16
|
+
import type { ColumnConfig, DataRow, SortDirection } from '../types'
|
|
17
|
+
|
|
18
|
+
interface DataGridTableViewProps extends React.ComponentProps<'div'> {
|
|
19
|
+
data: DataRow[]
|
|
20
|
+
columns: Array<[string, ColumnConfig]>
|
|
21
|
+
sortColumn: string | null
|
|
22
|
+
sortDirection: SortDirection
|
|
23
|
+
onToggleSort: (columnKey: string) => void
|
|
24
|
+
expandedRows: Set<string>
|
|
25
|
+
onToggleExpand: (rowId: string, expandKey: string) => void
|
|
26
|
+
favorites: Set<string>
|
|
27
|
+
onToggleFavorite: (rowId: string) => void
|
|
28
|
+
columnWidths: Record<string, number>
|
|
29
|
+
onResizeColumn: (columnKey: string, width: number) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LINE_CLASS: Record<1 | 2 | 3, string> = {
|
|
33
|
+
1: 'truncate',
|
|
34
|
+
2: 'line-clamp-2',
|
|
35
|
+
3: 'line-clamp-3',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// text-sm (14px) × 1.5 line-height × N + 2×8px padding
|
|
39
|
+
const ROW_HEIGHT: Record<2 | 3, string> = {
|
|
40
|
+
2: '4rem',
|
|
41
|
+
3: '5.5rem',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function responsiveClass(config: ColumnConfig): string {
|
|
45
|
+
if (config.hideMobile) return 'hidden md:table-cell'
|
|
46
|
+
if (config.hideTablet) return 'hidden lg:table-cell'
|
|
47
|
+
return ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getAriaSortValue(
|
|
51
|
+
key: string,
|
|
52
|
+
sortColumn: string | null,
|
|
53
|
+
sortDirection: SortDirection
|
|
54
|
+
): 'ascending' | 'descending' | undefined {
|
|
55
|
+
if (sortColumn !== key) return undefined
|
|
56
|
+
return sortDirection === 'asc' ? 'ascending' : 'descending'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DataGridTableView = React.forwardRef<HTMLDivElement, DataGridTableViewProps>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
data,
|
|
63
|
+
columns,
|
|
64
|
+
sortColumn,
|
|
65
|
+
sortDirection,
|
|
66
|
+
onToggleSort,
|
|
67
|
+
expandedRows,
|
|
68
|
+
onToggleExpand,
|
|
69
|
+
favorites,
|
|
70
|
+
onToggleFavorite,
|
|
71
|
+
columnWidths,
|
|
72
|
+
onResizeColumn,
|
|
73
|
+
className,
|
|
74
|
+
...props
|
|
75
|
+
},
|
|
76
|
+
ref
|
|
77
|
+
) => {
|
|
78
|
+
const resizeRef = React.useRef<{
|
|
79
|
+
key: string
|
|
80
|
+
startX: number
|
|
81
|
+
startWidth: number
|
|
82
|
+
} | null>(null)
|
|
83
|
+
|
|
84
|
+
const headerRefs = React.useRef<Record<string, HTMLTableCellElement | null>>({})
|
|
85
|
+
|
|
86
|
+
function handleResizeStart(e: React.MouseEvent, key: string) {
|
|
87
|
+
e.preventDefault()
|
|
88
|
+
const thEl = headerRefs.current[key]
|
|
89
|
+
const startWidth = columnWidths[key] || thEl?.offsetWidth || 120
|
|
90
|
+
resizeRef.current = { key, startX: e.clientX, startWidth }
|
|
91
|
+
|
|
92
|
+
function onMouseMove(ev: MouseEvent) {
|
|
93
|
+
if (!resizeRef.current) return
|
|
94
|
+
const diff = ev.clientX - resizeRef.current.startX
|
|
95
|
+
const newWidth = Math.max(60, resizeRef.current.startWidth + diff)
|
|
96
|
+
onResizeColumn(resizeRef.current.key, newWidth)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function onMouseUp() {
|
|
100
|
+
resizeRef.current = null
|
|
101
|
+
document.removeEventListener('mousemove', onMouseMove)
|
|
102
|
+
document.removeEventListener('mouseup', onMouseUp)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
document.addEventListener('mousemove', onMouseMove)
|
|
106
|
+
document.addEventListener('mouseup', onMouseUp)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderCell(row: DataRow, colKey: string, config: ColumnConfig) {
|
|
110
|
+
if (config.type === 'favorite') {
|
|
111
|
+
const projectName = String(row[columns[0]?.[0] ?? ''] ?? row.id)
|
|
112
|
+
return (
|
|
113
|
+
<FavoriteButton
|
|
114
|
+
pressed={favorites.has(row.id)}
|
|
115
|
+
onPressedChange={() => onToggleFavorite(row.id)}
|
|
116
|
+
projectName={projectName}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (config.type === 'expand') {
|
|
122
|
+
const items = row[colKey] as unknown[]
|
|
123
|
+
if (!items?.length) {
|
|
124
|
+
return <span className="text-xs text-muted-foreground">—</span>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const compositeKey = `${row.id}::${colKey}`
|
|
128
|
+
const isExpanded = expandedRows.has(compositeKey)
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Button
|
|
132
|
+
variant="ghost"
|
|
133
|
+
size="sm"
|
|
134
|
+
onClick={() => onToggleExpand(row.id, colKey)}
|
|
135
|
+
aria-expanded={isExpanded}
|
|
136
|
+
aria-label={`${items.length} ${config.expandLabel ?? 'Einträge'} anzeigen`}
|
|
137
|
+
className="gap-1 text-xs"
|
|
138
|
+
>
|
|
139
|
+
<ChevronDown
|
|
140
|
+
size={14}
|
|
141
|
+
className={cn('transition-transform', isExpanded && 'rotate-180')}
|
|
142
|
+
/>
|
|
143
|
+
{items.length} {config.expandLabel ?? 'Einträge'}
|
|
144
|
+
</Button>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return cellRenderers[config.type](row[colKey], row, config)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Collect expand-type columns for drawer rendering
|
|
152
|
+
const expandColumns = columns.filter(([, config]) => config.type === 'expand')
|
|
153
|
+
|
|
154
|
+
// Max rowLines across all visible columns → determines unified row height
|
|
155
|
+
const maxRowLines = columns.reduce(
|
|
156
|
+
(max, [, config]) => Math.max(max, config.rowLines ?? 1),
|
|
157
|
+
1
|
|
158
|
+
) as 1 | 2 | 3
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div
|
|
162
|
+
ref={ref}
|
|
163
|
+
data-slot="data-grid-table-view"
|
|
164
|
+
role="region"
|
|
165
|
+
aria-label="Scrollbare Datentabelle"
|
|
166
|
+
tabIndex={0}
|
|
167
|
+
className={cn('overflow-x-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', className)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
<Table className="table-fixed" aria-label="Datentabelle">
|
|
171
|
+
<TableHeader>
|
|
172
|
+
<TableRow>
|
|
173
|
+
{columns.map(([key, config]) => {
|
|
174
|
+
const ariaSort = getAriaSortValue(key, sortColumn, sortDirection)
|
|
175
|
+
const widthStyle = columnWidths[key]
|
|
176
|
+
? { width: columnWidths[key] }
|
|
177
|
+
: undefined
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<TableHead
|
|
181
|
+
key={key}
|
|
182
|
+
ref={(el) => {
|
|
183
|
+
headerRefs.current[key] = el
|
|
184
|
+
}}
|
|
185
|
+
scope="col"
|
|
186
|
+
aria-sort={ariaSort}
|
|
187
|
+
className={cn('relative select-none overflow-hidden max-w-0', responsiveClass(config))}
|
|
188
|
+
style={widthStyle}
|
|
189
|
+
>
|
|
190
|
+
{config.sortable ? (
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => onToggleSort(key)}
|
|
194
|
+
className="flex items-center gap-1 text-left w-full min-w-0"
|
|
195
|
+
>
|
|
196
|
+
<span className="truncate flex-1 min-w-0">{config.label}</span>
|
|
197
|
+
{sortColumn === key ? (
|
|
198
|
+
sortDirection === 'asc' ? (
|
|
199
|
+
<ArrowUp size={14} className="shrink-0" />
|
|
200
|
+
) : (
|
|
201
|
+
<ArrowDown size={14} className="shrink-0" />
|
|
202
|
+
)
|
|
203
|
+
) : (
|
|
204
|
+
<ArrowUpDown size={14} className="opacity-30 shrink-0" />
|
|
205
|
+
)}
|
|
206
|
+
</button>
|
|
207
|
+
) : (
|
|
208
|
+
<span className="truncate block">{config.label}</span>
|
|
209
|
+
)}
|
|
210
|
+
<div
|
|
211
|
+
className="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-primary/20 active:bg-primary/40"
|
|
212
|
+
onMouseDown={(e) => handleResizeStart(e, key)}
|
|
213
|
+
role="separator"
|
|
214
|
+
aria-orientation="vertical"
|
|
215
|
+
/>
|
|
216
|
+
</TableHead>
|
|
217
|
+
)
|
|
218
|
+
})}
|
|
219
|
+
</TableRow>
|
|
220
|
+
</TableHeader>
|
|
221
|
+
<TableBody>
|
|
222
|
+
{data.map((row, index) => (
|
|
223
|
+
<React.Fragment key={row.id}>
|
|
224
|
+
<TableRow
|
|
225
|
+
className="animate-in fade-in-0 slide-in-from-bottom-1"
|
|
226
|
+
style={{
|
|
227
|
+
animationDelay: `${index * 20}ms`,
|
|
228
|
+
animationFillMode: 'backwards',
|
|
229
|
+
...(maxRowLines > 1 ? { height: ROW_HEIGHT[maxRowLines] } : {}),
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{columns.map(([key, config]) => (
|
|
233
|
+
<TableCell
|
|
234
|
+
key={key}
|
|
235
|
+
className={cn(
|
|
236
|
+
'overflow-hidden max-w-0 whitespace-normal',
|
|
237
|
+
maxRowLines > 1 && 'align-top',
|
|
238
|
+
responsiveClass(config)
|
|
239
|
+
)}
|
|
240
|
+
>
|
|
241
|
+
<div className={LINE_CLASS[config.rowLines ?? 1]}>
|
|
242
|
+
{renderCell(row, key, config)}
|
|
243
|
+
</div>
|
|
244
|
+
</TableCell>
|
|
245
|
+
))}
|
|
246
|
+
</TableRow>
|
|
247
|
+
|
|
248
|
+
{expandColumns.map(([expandKey, expandConfig]) => {
|
|
249
|
+
const compositeKey = `${row.id}::${expandKey}`
|
|
250
|
+
const isExpanded = expandedRows.has(compositeKey)
|
|
251
|
+
const items = row[expandKey] as Record<string, unknown>[] | undefined
|
|
252
|
+
|
|
253
|
+
if (!items?.length) return null
|
|
254
|
+
|
|
255
|
+
const title =
|
|
256
|
+
expandConfig.expandTitleFn?.(row) ??
|
|
257
|
+
`${expandConfig.expandLabel ?? expandKey} (${row.id})`
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<ExpansionDrawer
|
|
261
|
+
key={compositeKey}
|
|
262
|
+
items={items}
|
|
263
|
+
expandColumns={expandConfig.expandColumns ?? []}
|
|
264
|
+
title={title}
|
|
265
|
+
colSpan={columns.length}
|
|
266
|
+
isOpen={isExpanded}
|
|
267
|
+
/>
|
|
268
|
+
)
|
|
269
|
+
})}
|
|
270
|
+
</React.Fragment>
|
|
271
|
+
))}
|
|
272
|
+
</TableBody>
|
|
273
|
+
</Table>
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
DataGridTableView.displayName = 'DataGridTableView'
|
|
280
|
+
|
|
281
|
+
export { DataGridTableView }
|
|
282
|
+
export type { DataGridTableViewProps }
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Table2, List, LayoutGrid, Settings, Search, ChevronDown, Check, X } from 'lucide-react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Badge } from '@/components/ui/badge'
|
|
6
|
+
import { InputGroup, InputGroupAddon } from '@/components/ui/input-group'
|
|
7
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
|
8
|
+
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
|
9
|
+
import {
|
|
10
|
+
Command,
|
|
11
|
+
CommandInput,
|
|
12
|
+
CommandList,
|
|
13
|
+
CommandEmpty,
|
|
14
|
+
CommandItem,
|
|
15
|
+
} from '@/components/ui/command'
|
|
16
|
+
import { ColumnConfigPopover } from '@/components/ColumnConfigPopover/ColumnConfigPopover'
|
|
17
|
+
import { MoreFiltersPopover } from '../MoreFiltersPopover/MoreFiltersPopover'
|
|
18
|
+
import type { ViewMode, ColumnConfig, DataGridFilters } from '../types'
|
|
19
|
+
|
|
20
|
+
interface FilterChipProps {
|
|
21
|
+
label: string
|
|
22
|
+
values: string[]
|
|
23
|
+
options: string[]
|
|
24
|
+
onChange: (values: string[] | null) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function FilterChip({ label, values, options, onChange }: FilterChipProps) {
|
|
28
|
+
const [open, setOpen] = React.useState(false)
|
|
29
|
+
const hasSelection = values.length > 0
|
|
30
|
+
|
|
31
|
+
const buttonLabel =
|
|
32
|
+
values.length === 0
|
|
33
|
+
? label
|
|
34
|
+
: values.length === 1
|
|
35
|
+
? values[0]
|
|
36
|
+
: `${label} (${values.length})`
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
40
|
+
<PopoverTrigger asChild>
|
|
41
|
+
<Button
|
|
42
|
+
variant="outline"
|
|
43
|
+
size="sm"
|
|
44
|
+
className={cn(
|
|
45
|
+
'h-8 gap-1 pr-2 text-xs font-normal',
|
|
46
|
+
hasSelection && 'border-primary/40 bg-primary/5 text-primary'
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<span className="truncate max-w-[120px]">{buttonLabel}</span>
|
|
50
|
+
{hasSelection ? (
|
|
51
|
+
<X
|
|
52
|
+
size={12}
|
|
53
|
+
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
54
|
+
onClick={(e) => {
|
|
55
|
+
e.stopPropagation()
|
|
56
|
+
onChange(null)
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
) : (
|
|
60
|
+
<ChevronDown size={12} className="shrink-0 text-muted-foreground" />
|
|
61
|
+
)}
|
|
62
|
+
</Button>
|
|
63
|
+
</PopoverTrigger>
|
|
64
|
+
<PopoverContent align="start" className="p-0 min-w-[180px] w-[--radix-popover-trigger-width]">
|
|
65
|
+
<Command>
|
|
66
|
+
<CommandInput placeholder="Suchen..." />
|
|
67
|
+
<CommandList>
|
|
68
|
+
<CommandEmpty>Keine Ergebnisse</CommandEmpty>
|
|
69
|
+
{options.map((option) => {
|
|
70
|
+
const isSelected = values.includes(option)
|
|
71
|
+
return (
|
|
72
|
+
<CommandItem
|
|
73
|
+
key={option}
|
|
74
|
+
value={option}
|
|
75
|
+
onSelect={() => {
|
|
76
|
+
const next = isSelected
|
|
77
|
+
? values.filter((v) => v !== option)
|
|
78
|
+
: [...values, option]
|
|
79
|
+
onChange(next.length > 0 ? next : null)
|
|
80
|
+
// Popup bleibt offen für weitere Auswahl
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Check
|
|
84
|
+
size={14}
|
|
85
|
+
className={cn('shrink-0', isSelected ? 'opacity-100' : 'opacity-0')}
|
|
86
|
+
/>
|
|
87
|
+
{option}
|
|
88
|
+
</CommandItem>
|
|
89
|
+
)
|
|
90
|
+
})}
|
|
91
|
+
</CommandList>
|
|
92
|
+
</Command>
|
|
93
|
+
</PopoverContent>
|
|
94
|
+
</Popover>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface DataGridToolbarProps extends React.ComponentProps<'div'> {
|
|
99
|
+
currentView: ViewMode
|
|
100
|
+
onViewChange: (view: ViewMode) => void
|
|
101
|
+
columns: Record<string, ColumnConfig>
|
|
102
|
+
columnOrder: string[]
|
|
103
|
+
visibleColumns: Set<string>
|
|
104
|
+
filters: DataGridFilters
|
|
105
|
+
filterOptions: Record<string, string[]>
|
|
106
|
+
onFilterChange: (columnKey: string, value: string[] | null) => void
|
|
107
|
+
searchQuery: string
|
|
108
|
+
onSearchChange: (query: string) => void
|
|
109
|
+
hasActiveFilters: boolean
|
|
110
|
+
onResetFilters: () => void
|
|
111
|
+
onToggleColumnVisibility: (columnKey: string) => void
|
|
112
|
+
onReorderColumns: (newOrder: string[]) => void
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const DataGridToolbar = React.forwardRef<HTMLDivElement, DataGridToolbarProps>(
|
|
116
|
+
(
|
|
117
|
+
{
|
|
118
|
+
currentView,
|
|
119
|
+
onViewChange,
|
|
120
|
+
columns,
|
|
121
|
+
columnOrder,
|
|
122
|
+
visibleColumns,
|
|
123
|
+
filters,
|
|
124
|
+
filterOptions,
|
|
125
|
+
onFilterChange,
|
|
126
|
+
searchQuery,
|
|
127
|
+
onSearchChange,
|
|
128
|
+
hasActiveFilters,
|
|
129
|
+
onResetFilters,
|
|
130
|
+
onToggleColumnVisibility,
|
|
131
|
+
onReorderColumns,
|
|
132
|
+
className,
|
|
133
|
+
...props
|
|
134
|
+
},
|
|
135
|
+
ref
|
|
136
|
+
) => {
|
|
137
|
+
const primaryFilterColumns = columnOrder
|
|
138
|
+
.filter((key) => {
|
|
139
|
+
const col = columns[key]
|
|
140
|
+
return col?.filterable && col.primaryFilter && visibleColumns.has(key)
|
|
141
|
+
})
|
|
142
|
+
.map((key) => [key, columns[key]] as [string, ColumnConfig])
|
|
143
|
+
|
|
144
|
+
const secondaryActiveCount = columnOrder.filter((key) => {
|
|
145
|
+
const col = columns[key]
|
|
146
|
+
return (
|
|
147
|
+
col?.filterable &&
|
|
148
|
+
!col.primaryFilter &&
|
|
149
|
+
visibleColumns.has(key) &&
|
|
150
|
+
filters[key] != null
|
|
151
|
+
)
|
|
152
|
+
}).length
|
|
153
|
+
|
|
154
|
+
const columnConfigItems = columnOrder.map((key) => ({
|
|
155
|
+
key,
|
|
156
|
+
label: columns[key]?.label ?? key,
|
|
157
|
+
}))
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
ref={ref}
|
|
162
|
+
data-slot="data-grid-toolbar"
|
|
163
|
+
role="toolbar"
|
|
164
|
+
aria-label="DataGrid Steuerung"
|
|
165
|
+
className={cn('flex flex-col gap-2', className)}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
{/* Row 1: View toggle + Density toggle + Column config */}
|
|
169
|
+
<div className="flex items-center justify-between gap-2">
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<ToggleGroup
|
|
172
|
+
type="single"
|
|
173
|
+
variant="outline"
|
|
174
|
+
value={currentView}
|
|
175
|
+
onValueChange={(value) => {
|
|
176
|
+
if (value) onViewChange(value as ViewMode)
|
|
177
|
+
}}
|
|
178
|
+
aria-label="Ansicht wählen"
|
|
179
|
+
>
|
|
180
|
+
<ToggleGroupItem value="table" aria-label="Tabellenansicht">
|
|
181
|
+
<Table2 className="size-4" aria-hidden="true" />
|
|
182
|
+
<span className="hidden sm:inline">Tabelle</span>
|
|
183
|
+
</ToggleGroupItem>
|
|
184
|
+
<ToggleGroupItem value="list" aria-label="Listenansicht">
|
|
185
|
+
<List className="size-4" aria-hidden="true" />
|
|
186
|
+
<span className="hidden sm:inline">Liste</span>
|
|
187
|
+
</ToggleGroupItem>
|
|
188
|
+
<ToggleGroupItem value="cards" aria-label="Kartenansicht">
|
|
189
|
+
<LayoutGrid className="size-4" aria-hidden="true" />
|
|
190
|
+
<span className="hidden sm:inline">Karten</span>
|
|
191
|
+
</ToggleGroupItem>
|
|
192
|
+
</ToggleGroup>
|
|
193
|
+
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<ColumnConfigPopover
|
|
197
|
+
columns={columnConfigItems}
|
|
198
|
+
visibleColumns={visibleColumns}
|
|
199
|
+
onVisibilityChange={(key, visible) => {
|
|
200
|
+
if (visible !== visibleColumns.has(key)) {
|
|
201
|
+
onToggleColumnVisibility(key)
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
204
|
+
onReorder={onReorderColumns}
|
|
205
|
+
>
|
|
206
|
+
<Button variant="outline" size="sm" aria-label="Spalten konfigurieren">
|
|
207
|
+
<Settings className="size-4" aria-hidden="true" />
|
|
208
|
+
<span className="hidden sm:inline">Konfigurieren</span>
|
|
209
|
+
</Button>
|
|
210
|
+
</ColumnConfigPopover>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Row 2: Filter chips + Search */}
|
|
214
|
+
<div className="flex items-center justify-between gap-2 flex-wrap">
|
|
215
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
216
|
+
{primaryFilterColumns.map(([key, config]) => (
|
|
217
|
+
<FilterChip
|
|
218
|
+
key={key}
|
|
219
|
+
label={config.label}
|
|
220
|
+
values={filters[key] ?? []}
|
|
221
|
+
options={filterOptions[key] ?? []}
|
|
222
|
+
onChange={(vals) => onFilterChange(key, vals)}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
|
|
226
|
+
{hasActiveFilters && (
|
|
227
|
+
<Button
|
|
228
|
+
variant="ghost"
|
|
229
|
+
size="sm"
|
|
230
|
+
onClick={onResetFilters}
|
|
231
|
+
aria-label="Alle Filter zurücksetzen"
|
|
232
|
+
>
|
|
233
|
+
Filter zurücksetzen
|
|
234
|
+
</Button>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
<MoreFiltersPopover
|
|
238
|
+
columns={columns}
|
|
239
|
+
columnOrder={columnOrder}
|
|
240
|
+
visibleColumns={visibleColumns}
|
|
241
|
+
filters={filters}
|
|
242
|
+
filterOptions={filterOptions}
|
|
243
|
+
onFilterChange={onFilterChange}
|
|
244
|
+
>
|
|
245
|
+
<Button variant="ghost" size="sm" aria-label="Weitere Filter öffnen">
|
|
246
|
+
+ mehr Filter
|
|
247
|
+
{secondaryActiveCount > 0 && (
|
|
248
|
+
<Badge
|
|
249
|
+
variant="default"
|
|
250
|
+
className="ml-1 size-4 min-w-0 rounded-full p-0 text-[10px] leading-none"
|
|
251
|
+
aria-label={`${secondaryActiveCount} aktive sekundäre Filter`}
|
|
252
|
+
>
|
|
253
|
+
{secondaryActiveCount}
|
|
254
|
+
</Badge>
|
|
255
|
+
)}
|
|
256
|
+
</Button>
|
|
257
|
+
</MoreFiltersPopover>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<InputGroup className="w-auto max-w-[220px]">
|
|
261
|
+
<InputGroupAddon align="inline-start">
|
|
262
|
+
<Search className="size-4" aria-hidden="true" />
|
|
263
|
+
</InputGroupAddon>
|
|
264
|
+
<input
|
|
265
|
+
data-slot="input-group-control"
|
|
266
|
+
type="search"
|
|
267
|
+
placeholder="Suche..."
|
|
268
|
+
value={searchQuery}
|
|
269
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
270
|
+
aria-label="Datensätze durchsuchen"
|
|
271
|
+
className="flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 h-full px-2 text-sm outline-none"
|
|
272
|
+
/>
|
|
273
|
+
</InputGroup>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
DataGridToolbar.displayName = 'DataGridToolbar'
|
|
281
|
+
|
|
282
|
+
export { DataGridToolbar }
|
|
283
|
+
export type { DataGridToolbarProps }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
interface DomainSwitcherProps extends React.ComponentProps<'div'> {
|
|
6
|
+
domains: Array<{ key: string; label: string }>
|
|
7
|
+
activeDomain: string
|
|
8
|
+
onDomainChange: (key: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DomainSwitcher = React.forwardRef<HTMLDivElement, DomainSwitcherProps>(
|
|
12
|
+
({ domains, activeDomain, onDomainChange, className, ...props }, ref) => {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
ref={ref}
|
|
16
|
+
data-slot="domain-switcher"
|
|
17
|
+
aria-label="Datenquelle wechseln"
|
|
18
|
+
className={cn('flex items-center gap-3', className)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
22
|
+
Datenquelle
|
|
23
|
+
</span>
|
|
24
|
+
<Tabs value={activeDomain} onValueChange={onDomainChange}>
|
|
25
|
+
<TabsList>
|
|
26
|
+
{domains.map((domain) => (
|
|
27
|
+
<TabsTrigger key={domain.key} value={domain.key}>
|
|
28
|
+
{domain.label}
|
|
29
|
+
</TabsTrigger>
|
|
30
|
+
))}
|
|
31
|
+
</TabsList>
|
|
32
|
+
</Tabs>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
DomainSwitcher.displayName = 'DomainSwitcher'
|
|
39
|
+
|
|
40
|
+
export { DomainSwitcher }
|
|
41
|
+
export type { DomainSwitcherProps }
|