@fastnd/components 1.0.30 → 1.0.32

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 (57) hide show
  1. package/dist/components/QuickAccessCard/QuickAccessCard.d.ts +12 -0
  2. package/dist/components/ScoreBar/ScoreBar.d.ts +8 -0
  3. package/dist/components/index.d.ts +2 -0
  4. package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +41 -56
  5. package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
  6. package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +175 -0
  7. package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +124 -0
  8. package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +223 -0
  9. package/dist/examples/data-visualization/DataExplorerPagination/DataExplorerPagination.tsx +143 -0
  10. package/dist/examples/data-visualization/DataExplorerToolbar/DataExplorerToolbar.tsx +88 -0
  11. package/dist/examples/data-visualization/DataListView/DataListView.tsx +246 -0
  12. package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +449 -0
  13. package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +140 -0
  14. package/dist/examples/data-visualization/FilterChipGroup/FilterChipGroup.tsx +125 -0
  15. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +132 -0
  16. package/dist/examples/data-visualization/constants.ts +587 -0
  17. package/dist/examples/data-visualization/hooks/use-column-config.ts +76 -0
  18. package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +313 -0
  19. package/dist/examples/data-visualization/index.ts +1 -0
  20. package/dist/examples/data-visualization/types.ts +99 -0
  21. package/dist/examples/quickaccess/QuickAccess/QuickAccess.tsx +97 -0
  22. package/dist/examples/quickaccess/index.ts +2 -0
  23. package/dist/examples/quickaccess/types.ts +11 -0
  24. package/dist/fastnd-components.js +5708 -5590
  25. package/package.json +1 -1
  26. package/dist/examples/data-explorer/CardCarouselPanel/CardCarouselPanel.tsx +0 -197
  27. package/dist/examples/data-explorer/CardView/CardView.tsx +0 -168
  28. package/dist/examples/data-explorer/ColumnConfigPopover/ColumnConfigPopover.tsx +0 -157
  29. package/dist/examples/data-explorer/DataExplorerEmpty/DataExplorerEmpty.tsx +0 -56
  30. package/dist/examples/data-explorer/DataExplorerPage/DataExplorerPage.tsx +0 -101
  31. package/dist/examples/data-explorer/DataExplorerPagination/DataExplorerPagination.tsx +0 -129
  32. package/dist/examples/data-explorer/DataExplorerToolbar/DataExplorerToolbar.tsx +0 -143
  33. package/dist/examples/data-explorer/DomainSwitcher/DomainSwitcher.tsx +0 -36
  34. package/dist/examples/data-explorer/ExpansionRows/ExpansionRows.tsx +0 -180
  35. package/dist/examples/data-explorer/FilterChip/FilterChip.tsx +0 -85
  36. package/dist/examples/data-explorer/FilterPopoverContent/FilterPopoverContent.tsx +0 -73
  37. package/dist/examples/data-explorer/ListView/ListView.tsx +0 -305
  38. package/dist/examples/data-explorer/MoreFiltersPopover/MoreFiltersPopover.tsx +0 -113
  39. package/dist/examples/data-explorer/TableView/TableView.tsx +0 -193
  40. package/dist/examples/data-explorer/cells/CellRenderer.tsx +0 -147
  41. package/dist/examples/data-explorer/cells/CurrencyCell.tsx +0 -31
  42. package/dist/examples/data-explorer/cells/DoubleTextCell.tsx +0 -27
  43. package/dist/examples/data-explorer/cells/ExpandButton.tsx +0 -67
  44. package/dist/examples/data-explorer/cells/InventoryBadgeCell.tsx +0 -52
  45. package/dist/examples/data-explorer/cells/LinkCell.tsx +0 -42
  46. package/dist/examples/data-explorer/cells/ScoreBar.tsx +0 -50
  47. package/dist/examples/data-explorer/cells/StatusBadgeCell.tsx +0 -39
  48. package/dist/examples/data-explorer/cells/TextCell.tsx +0 -35
  49. package/dist/examples/data-explorer/cells/index.ts +0 -26
  50. package/dist/examples/data-explorer/domains/applications.ts +0 -225
  51. package/dist/examples/data-explorer/domains/customers.ts +0 -267
  52. package/dist/examples/data-explorer/domains/index.ts +0 -26
  53. package/dist/examples/data-explorer/domains/products.ts +0 -1116
  54. package/dist/examples/data-explorer/domains/projects.ts +0 -205
  55. package/dist/examples/data-explorer/hooks/use-data-explorer-state.ts +0 -371
  56. package/dist/examples/data-explorer/index.ts +0 -3
  57. package/dist/examples/data-explorer/types.ts +0 -239
@@ -1,129 +0,0 @@
1
- import {
2
- Pagination,
3
- PaginationContent,
4
- PaginationEllipsis,
5
- PaginationItem,
6
- PaginationLink,
7
- PaginationNext,
8
- PaginationPrevious,
9
- } from '@/components/ui/pagination'
10
- import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
11
- import { cn } from '@/lib/utils'
12
-
13
- export interface DataExplorerPaginationProps {
14
- currentPage: number
15
- totalPages: number
16
- pageSize: number
17
- totalFiltered: number
18
- totalData: number
19
- onPageChange: (page: number) => void
20
- onPageSizeChange: (size: number) => void
21
- className?: string
22
- }
23
-
24
- const PAGE_SIZES = [25, 50, 100] as const
25
-
26
- function getPageRange(current: number, total: number): (number | '...')[] {
27
- if (total <= 7) {
28
- return Array.from({ length: total }, (_, i) => i + 1)
29
- }
30
- if (current <= 4) {
31
- return [1, 2, 3, 4, 5, '...', total]
32
- }
33
- if (current >= total - 3) {
34
- return [1, '...', total - 4, total - 3, total - 2, total - 1, total]
35
- }
36
- return [1, '...', current - 1, current, current + 1, '...', total]
37
- }
38
-
39
- export function DataExplorerPagination({
40
- currentPage,
41
- totalPages,
42
- pageSize,
43
- totalFiltered,
44
- onPageChange,
45
- onPageSizeChange,
46
- className,
47
- }: DataExplorerPaginationProps) {
48
- const startItem = totalFiltered === 0 ? 0 : (currentPage - 1) * pageSize + 1
49
- const endItem = Math.min(currentPage * pageSize, totalFiltered)
50
- const pageRange = getPageRange(currentPage, totalPages)
51
- const atFirst = currentPage <= 1
52
- const atLast = currentPage >= totalPages
53
-
54
- return (
55
- <nav
56
- data-slot="data-explorer-pagination"
57
- aria-label="Seitennavigation"
58
- className={cn('flex flex-wrap items-center justify-between gap-3 px-4 py-3', className)}
59
- >
60
- {/* Left: result range + page size select */}
61
- <div className="flex items-center gap-3 text-sm text-muted-foreground">
62
- <span>
63
- {startItem}–{endItem} von {totalFiltered} Ergebnissen
64
- </span>
65
- <div className="flex items-center gap-1.5">
66
- <span className="text-xs">Zeilen:</span>
67
- <NativeSelect
68
- size="sm"
69
- value={pageSize}
70
- onChange={(e) => onPageSizeChange(Number(e.target.value))}
71
- aria-label="Einträge pro Seite"
72
- className="w-auto"
73
- >
74
- {PAGE_SIZES.map((s) => (
75
- <NativeSelectOption key={s} value={s}>
76
- {s}
77
- </NativeSelectOption>
78
- ))}
79
- </NativeSelect>
80
- </div>
81
- </div>
82
-
83
- {/* Right: page navigation */}
84
- <Pagination className="w-auto mx-0 justify-end">
85
- <PaginationContent>
86
- <PaginationItem>
87
- <PaginationPrevious
88
- role="button"
89
- aria-disabled={atFirst}
90
- className={cn(atFirst && 'pointer-events-none opacity-50')}
91
- onClick={atFirst ? undefined : () => onPageChange(currentPage - 1)}
92
- />
93
- </PaginationItem>
94
-
95
- {pageRange.map((page, idx) =>
96
- page === '...' ? (
97
- <PaginationItem key={`ellipsis-${idx}`}>
98
- <PaginationEllipsis />
99
- </PaginationItem>
100
- ) : (
101
- <PaginationItem key={page}>
102
- <PaginationLink
103
- role="button"
104
- isActive={page === currentPage}
105
- onClick={() => onPageChange(page)}
106
- aria-label={`Seite ${page}`}
107
- className={page === currentPage ? 'bg-primary text-primary-foreground border-primary hover:bg-primary/90 hover:text-primary-foreground' : ''}
108
- >
109
- {page}
110
- </PaginationLink>
111
- </PaginationItem>
112
- ),
113
- )}
114
-
115
- <PaginationItem>
116
- <PaginationNext
117
- role="button"
118
- aria-disabled={atLast}
119
- className={cn(atLast && 'pointer-events-none opacity-50')}
120
- onClick={atLast ? undefined : () => onPageChange(currentPage + 1)}
121
- />
122
- </PaginationItem>
123
- </PaginationContent>
124
- </Pagination>
125
- </nav>
126
- )
127
- }
128
-
129
- DataExplorerPagination.displayName = 'DataExplorerPagination'
@@ -1,143 +0,0 @@
1
- import * as React from 'react'
2
- import { LayoutGrid, List, RotateCcw, Search, Table } from 'lucide-react'
3
- import { Button } from '@/components/ui/button'
4
- import { Input } from '@/components/ui/input'
5
- import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
6
- import { ColumnConfigPopover } from '../ColumnConfigPopover/ColumnConfigPopover'
7
- import { FilterChip } from '../FilterChip/FilterChip'
8
- import { MoreFiltersPopover } from '../MoreFiltersPopover/MoreFiltersPopover'
9
- import type { DataExplorerActions, DataExplorerDerived, DataExplorerState } from '../types'
10
- import { cn } from '@/lib/utils'
11
-
12
- export interface DataExplorerToolbarProps {
13
- state: DataExplorerState
14
- derived: DataExplorerDerived
15
- actions: DataExplorerActions
16
- className?: string
17
- }
18
-
19
- export function DataExplorerToolbar({
20
- state,
21
- derived,
22
- actions,
23
- className,
24
- }: DataExplorerToolbarProps) {
25
- const secondaryFilterCount = React.useMemo(
26
- () =>
27
- derived.secondaryFilterColumns.reduce(
28
- (acc, [key]) => acc + (state.filters[key]?.length ?? 0),
29
- 0,
30
- ),
31
- [derived.secondaryFilterColumns, state.filters],
32
- )
33
-
34
- return (
35
- <div
36
- data-slot="data-explorer-toolbar"
37
- role="toolbar"
38
- aria-label="Daten-Toolbar"
39
- className={cn('flex flex-col gap-3 border-b bg-card px-6 py-4', className)}
40
- >
41
- {/* Row 1: View switcher + column config */}
42
- <div className="flex items-center justify-between gap-2">
43
- <ToggleGroup
44
- type="single"
45
- value={state.viewMode}
46
- onValueChange={(v) => v && actions.switchView(v as DataExplorerState['viewMode'])}
47
- variant="outline"
48
- size="sm"
49
- >
50
- <ToggleGroupItem
51
- value="table"
52
- aria-label="Tabellenansicht"
53
- className="gap-1.5 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground data-[state=on]:border-primary"
54
- >
55
- <Table className="size-3.5" />
56
- <span className="hidden sm:inline">Tabelle</span>
57
- </ToggleGroupItem>
58
- <ToggleGroupItem
59
- value="list"
60
- aria-label="Listenansicht"
61
- className="gap-1.5 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground data-[state=on]:border-primary"
62
- >
63
- <List className="size-3.5" />
64
- <span className="hidden sm:inline">Liste</span>
65
- </ToggleGroupItem>
66
- <ToggleGroupItem
67
- value="card"
68
- aria-label="Kartenansicht"
69
- className="gap-1.5 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground data-[state=on]:border-primary"
70
- >
71
- <LayoutGrid className="size-3.5" />
72
- <span className="hidden sm:inline">Karten</span>
73
- </ToggleGroupItem>
74
- </ToggleGroup>
75
-
76
- <ColumnConfigPopover
77
- columns={derived.domainConfig.columns}
78
- columnOrder={state.columnOrder}
79
- columnVisibility={state.columnVisibility}
80
- onToggleVisibility={actions.toggleColumnVisibility}
81
- onReorder={actions.reorderColumns}
82
- />
83
- </div>
84
-
85
- {/* Row 2: Filter chips + search */}
86
- <div className="flex flex-wrap items-center gap-2">
87
- {/* Primary filter chips */}
88
- {derived.primaryFilterColumns.map(([key, col]) => (
89
- <FilterChip
90
- key={key}
91
- columnKey={key}
92
- label={col.label}
93
- selectedValues={state.filters[key] ?? []}
94
- options={derived.getFilterOptions(key)}
95
- onToggleOption={(value) => actions.toggleFilterOption(key, value)}
96
- onClear={() => actions.clearFilter(key)}
97
- />
98
- ))}
99
-
100
- {/* More filters */}
101
- {derived.secondaryFilterColumns.length > 0 && (
102
- <MoreFiltersPopover
103
- columns={derived.secondaryFilterColumns}
104
- filters={state.filters}
105
- getFilterOptions={derived.getFilterOptions}
106
- onToggleOption={actions.toggleFilterOption}
107
- onResetSecondary={actions.resetSecondaryFilters}
108
- totalSecondaryCount={secondaryFilterCount}
109
- />
110
- )}
111
-
112
- {/* Reset all */}
113
- {derived.hasActiveFilters && (
114
- <Button
115
- variant="ghost"
116
- size="sm"
117
- onClick={actions.resetAllFilters}
118
- aria-label="Alle Filter zurücksetzen"
119
- className="h-8 gap-1.5 text-sm text-muted-foreground hover:text-destructive"
120
- >
121
- <RotateCcw className="size-3.5" />
122
- <span>Zurücksetzen</span>
123
- </Button>
124
- )}
125
-
126
- {/* Search input — pushed to the right */}
127
- <div className="relative ml-auto min-w-[180px] max-w-xs flex-1">
128
- <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
129
- <Input
130
- type="search"
131
- value={state.searchTerm}
132
- onChange={(e) => actions.setSearchTerm(e.target.value)}
133
- placeholder="Suchen..."
134
- aria-label="Datensätze durchsuchen"
135
- className="h-8 pl-8 text-sm"
136
- />
137
- </div>
138
- </div>
139
- </div>
140
- )
141
- }
142
-
143
- DataExplorerToolbar.displayName = 'DataExplorerToolbar'
@@ -1,36 +0,0 @@
1
- import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
2
- import { DOMAIN_KEYS, DOMAIN_LABELS } from '../domains'
3
- import type { DomainKey } from '../types'
4
- import { cn } from '@/lib/utils'
5
-
6
- export interface DomainSwitcherProps {
7
- activeDomain: DomainKey
8
- onDomainChange: (domain: DomainKey) => void
9
- className?: string
10
- }
11
-
12
- export function DomainSwitcher({ activeDomain, onDomainChange, className }: DomainSwitcherProps) {
13
- return (
14
- <nav data-slot="domain-switcher" className={cn('border-b bg-card', className)}>
15
- <Tabs
16
- value={activeDomain}
17
- onValueChange={(v) => onDomainChange(v as DomainKey)}
18
- className="px-6"
19
- >
20
- <TabsList variant="line" className="h-auto gap-0 rounded-none p-0">
21
- {DOMAIN_KEYS.map((key) => (
22
- <TabsTrigger
23
- key={key}
24
- value={key}
25
- className="rounded-none px-5 py-3 text-sm font-medium data-[state=active]:font-semibold data-[state=active]:text-primary data-[state=active]:after:bg-primary"
26
- >
27
- {DOMAIN_LABELS[key]}
28
- </TabsTrigger>
29
- ))}
30
- </TabsList>
31
- </Tabs>
32
- </nav>
33
- )
34
- }
35
-
36
- DomainSwitcher.displayName = 'DomainSwitcher'
@@ -1,180 +0,0 @@
1
- import { Plus } from 'lucide-react'
2
- import { cn } from '@/lib/utils'
3
- import { Badge } from '@/components/ui/badge'
4
- import { Button } from '@/components/ui/button'
5
- import { TableCell, TableRow } from '@/components/ui/table'
6
- import { ScoreBar } from '../cells'
7
- import type { ColumnDef } from '../types'
8
-
9
- interface ExpansionRowsProps {
10
- row: Record<string, unknown>
11
- columnKey: string
12
- column: ColumnDef
13
- visibleColumns: string[]
14
- columns: Record<string, ColumnDef>
15
- colSpan: number
16
- className?: string
17
- }
18
-
19
- const ExpansionRows = ({
20
- row,
21
- columnKey,
22
- column,
23
- visibleColumns,
24
- columns,
25
- colSpan,
26
- className,
27
- }: ExpansionRowsProps) => {
28
- const items = row[columnKey]
29
-
30
- if (!Array.isArray(items) || items.length === 0 || !column.expandColumns) {
31
- return null
32
- }
33
-
34
- // Build a lookup: mainColumnKey -> expandColumnDef for all expand columns that map to a visible column
35
- const mapLookup: Record<string, (typeof column.expandColumns)[number]> = {}
36
- for (const ec of column.expandColumns) {
37
- if (ec.mapTo) {
38
- mapLookup[ec.mapTo] = ec
39
- }
40
- }
41
-
42
- const title = column.expandTitleFn ? column.expandTitleFn(row) : (column.expandLabel ?? columnKey)
43
-
44
- return (
45
- <>
46
- {/* Title row spanning all columns */}
47
- <TableRow
48
- data-slot="expansion-title-row"
49
- className={cn('bg-secondary hover:bg-secondary border-b-0', className)}
50
- >
51
- <TableCell colSpan={colSpan} className="text-[11px] font-semibold uppercase tracking-wide px-3 py-2">
52
- <div className="flex items-center gap-2">
53
- <span>{title}</span>
54
- <Badge variant="secondary" className="text-xs tabular-nums">
55
- {items.length}
56
- </Badge>
57
- </div>
58
- </TableCell>
59
- </TableRow>
60
-
61
- {/* One data row per nested item */}
62
- {(items as Record<string, unknown>[]).map((item, idx) => (
63
- <TableRow
64
- key={idx}
65
- data-slot="expansion-data-row"
66
- className="bg-accent hover:bg-primary/[0.04]"
67
- >
68
- {visibleColumns.map((mainKey) => {
69
- const mainCol = columns[mainKey]
70
- const cellClasses = cn(
71
- mainCol?.hideMobile && 'hidden sm:table-cell',
72
- mainCol?.hideTablet && 'hidden lg:table-cell',
73
- )
74
-
75
- // The expand column itself: render the add button
76
- if (mainKey === columnKey) {
77
- return (
78
- <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)}>
79
- <Button
80
- variant="ghost"
81
- size="icon-xs"
82
- aria-label="Hinzufügen"
83
- onClick={(e) => e.preventDefault()}
84
- >
85
- <Plus />
86
- </Button>
87
- </TableCell>
88
- )
89
- }
90
-
91
- // A column that an expand column maps into
92
- const ec = mapLookup[mainKey]
93
- if (ec) {
94
- const value = item[ec.key]
95
-
96
- if (ec.type === 'score-bar') {
97
- return (
98
- <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)}>
99
- <ScoreBar value={typeof value === 'number' ? value : 0} />
100
- </TableCell>
101
- )
102
- }
103
-
104
- // Delegate rendering to the main column type for structural fidelity
105
- if (mainCol?.type === 'double-text' && ec.secondaryKey) {
106
- const secondary = item[ec.secondaryKey]
107
- return (
108
- <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)}>
109
- <div className={cn('flex flex-col', ec.bold && 'font-medium')}>
110
- <span className="text-sm">{value != null ? String(value) : ''}</span>
111
- {secondary != null && (
112
- <span className="text-xs text-muted-foreground">{String(secondary)}</span>
113
- )}
114
- </div>
115
- </TableCell>
116
- )
117
- }
118
-
119
- if (mainCol?.type === 'status-badge' && mainCol.statusMap && value != null) {
120
- const statusKey = mainCol.statusMap[String(value)] ?? 'default'
121
- return (
122
- <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)}>
123
- <Badge variant="secondary" data-status={statusKey}>
124
- {String(value)}
125
- </Badge>
126
- </TableCell>
127
- )
128
- }
129
-
130
- if (mainCol?.type === 'link') {
131
- return (
132
- <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)}>
133
- <a
134
- href="#"
135
- className="text-sm text-primary underline-offset-2 hover:underline"
136
- onClick={(e) => e.preventDefault()}
137
- >
138
- {value != null ? String(value) : ''}
139
- </a>
140
- </TableCell>
141
- )
142
- }
143
-
144
- const displayValue =
145
- typeof value === 'boolean'
146
- ? value
147
- ? 'Ja'
148
- : 'Nein'
149
- : value != null
150
- ? String(value)
151
- : ''
152
-
153
- return (
154
- <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)}>
155
- <span
156
- className={cn(
157
- 'text-sm',
158
- ec.bold && 'font-medium',
159
- ec.muted && 'text-muted-foreground',
160
- )}
161
- >
162
- {displayValue}
163
- </span>
164
- </TableCell>
165
- )
166
- }
167
-
168
- // No mapping — render empty cell to maintain column alignment
169
- return <TableCell key={mainKey} className={cn('px-3 py-2 text-[13px]', cellClasses)} />
170
- })}
171
- </TableRow>
172
- ))}
173
- </>
174
- )
175
- }
176
-
177
- ExpansionRows.displayName = 'ExpansionRows'
178
-
179
- export { ExpansionRows }
180
- export type { ExpansionRowsProps }
@@ -1,85 +0,0 @@
1
- import { ChevronDown, X } from 'lucide-react'
2
- import { Badge } from '@/components/ui/badge'
3
- import { Button } from '@/components/ui/button'
4
- import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
5
- import { FilterPopoverContent } from '../FilterPopoverContent/FilterPopoverContent'
6
- import { cn } from '@/lib/utils'
7
-
8
- export interface FilterChipProps {
9
- columnKey: string
10
- label: string
11
- selectedValues: string[]
12
- options: string[]
13
- onToggleOption: (value: string) => void
14
- onClear: () => void
15
- className?: string
16
- }
17
-
18
- export function FilterChip({
19
- columnKey,
20
- label,
21
- selectedValues,
22
- options,
23
- onToggleOption,
24
- onClear,
25
- className,
26
- }: FilterChipProps) {
27
- const hasSelection = selectedValues.length > 0
28
-
29
- return (
30
- <Popover>
31
- <div data-slot="filter-chip" className={cn('inline-flex items-center', className)}>
32
- <PopoverTrigger asChild>
33
- <Button
34
- variant="outline"
35
- size="sm"
36
- aria-haspopup="dialog"
37
- aria-label={`${label} Filter`}
38
- className={cn(
39
- 'h-8 gap-1.5 text-sm font-medium',
40
- hasSelection && 'rounded-r-none border-r-0 border-primary bg-primary/5 text-primary hover:bg-primary/10',
41
- )}
42
- >
43
- <span>{label}</span>
44
- {hasSelection ? (
45
- <Badge variant="secondary" className="h-4 min-w-4 px-1 py-0 text-[10px]">
46
- {selectedValues.length}
47
- </Badge>
48
- ) : (
49
- <ChevronDown className="size-3.5 text-muted-foreground" />
50
- )}
51
- </Button>
52
- </PopoverTrigger>
53
-
54
- {hasSelection && (
55
- <Button
56
- variant="outline"
57
- size="sm"
58
- onClick={onClear}
59
- aria-label={`${label} Filter entfernen`}
60
- className="h-8 rounded-l-none border-primary bg-primary/5 px-1.5 text-primary hover:bg-primary/10 hover:text-destructive"
61
- >
62
- <X className="size-3.5" />
63
- </Button>
64
- )}
65
- </div>
66
-
67
- <PopoverContent
68
- align="start"
69
- sideOffset={4}
70
- role="dialog"
71
- aria-label={`${label} Filter`}
72
- className="w-64 p-3"
73
- data-column-key={columnKey}
74
- >
75
- <FilterPopoverContent
76
- options={options}
77
- selectedValues={selectedValues}
78
- onToggleOption={onToggleOption}
79
- />
80
- </PopoverContent>
81
- </Popover>
82
- )
83
- }
84
-
85
- FilterChip.displayName = 'FilterChip'
@@ -1,73 +0,0 @@
1
- import * as React from 'react'
2
- import { Search } from 'lucide-react'
3
- import { Checkbox } from '@/components/ui/checkbox'
4
- import { Input } from '@/components/ui/input'
5
- import { cn } from '@/lib/utils'
6
-
7
- export interface FilterPopoverContentProps {
8
- options: string[]
9
- selectedValues: string[]
10
- onToggleOption: (value: string) => void
11
- labelMap?: Record<string, string>
12
- className?: string
13
- }
14
-
15
- export function FilterPopoverContent({
16
- options,
17
- selectedValues,
18
- onToggleOption,
19
- labelMap,
20
- className,
21
- }: FilterPopoverContentProps) {
22
- const [search, setSearch] = React.useState('')
23
-
24
- const filtered = React.useMemo(() => {
25
- if (!search) return options
26
- const lower = search.toLowerCase()
27
- return options.filter((opt) => {
28
- const display = labelMap?.[opt] ?? opt
29
- return display.toLowerCase().includes(lower)
30
- })
31
- }, [options, search, labelMap])
32
-
33
- return (
34
- <div data-slot="filter-popover-content" className={cn('flex flex-col gap-2', className)}>
35
- <div className="relative">
36
- <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
37
- <Input
38
- value={search}
39
- onChange={(e) => setSearch(e.target.value)}
40
- placeholder="Suchen..."
41
- className="h-8 pl-8 text-sm"
42
- />
43
- </div>
44
- <div className="max-h-60 overflow-y-auto" role="listbox">
45
- {filtered.length === 0 ? (
46
- <p className="py-4 text-center text-xs text-muted-foreground">Keine Treffer</p>
47
- ) : (
48
- filtered.map((opt) => {
49
- const label = labelMap?.[opt] ?? opt
50
- const checked = selectedValues.includes(opt)
51
- return (
52
- <label
53
- key={opt}
54
- role="option"
55
- aria-selected={checked}
56
- className="flex cursor-pointer items-center gap-2.5 rounded px-1 py-1.5 text-sm hover:bg-accent"
57
- >
58
- <Checkbox
59
- checked={checked}
60
- onCheckedChange={() => onToggleOption(opt)}
61
- aria-label={label}
62
- />
63
- <span className="leading-snug">{label}</span>
64
- </label>
65
- )
66
- })
67
- )}
68
- </div>
69
- </div>
70
- )
71
- }
72
-
73
- FilterPopoverContent.displayName = 'FilterPopoverContent'