@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,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 }