@fastnd/components 1.0.28 → 1.0.30

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 (68) hide show
  1. package/dist/components/FavoriteButton/FavoriteButton.d.ts +2 -1
  2. package/dist/components/index.d.ts +1 -3
  3. package/dist/components/ui/badge.d.ts +1 -1
  4. package/dist/components/ui/button.d.ts +1 -1
  5. package/dist/components/ui/data-table.d.ts +8 -0
  6. package/dist/components/ui/input-group.d.ts +1 -1
  7. package/dist/components/ui/item.d.ts +1 -1
  8. package/dist/components/ui/tabs.d.ts +1 -1
  9. package/dist/examples/data-explorer/CardCarouselPanel/CardCarouselPanel.tsx +197 -0
  10. package/dist/examples/data-explorer/CardView/CardView.tsx +168 -0
  11. package/dist/examples/data-explorer/ColumnConfigPopover/ColumnConfigPopover.tsx +157 -0
  12. package/dist/examples/data-explorer/DataExplorerEmpty/DataExplorerEmpty.tsx +56 -0
  13. package/dist/examples/data-explorer/DataExplorerPage/DataExplorerPage.tsx +101 -0
  14. package/dist/examples/data-explorer/DataExplorerPagination/DataExplorerPagination.tsx +129 -0
  15. package/dist/examples/data-explorer/DataExplorerToolbar/DataExplorerToolbar.tsx +143 -0
  16. package/dist/examples/data-explorer/DomainSwitcher/DomainSwitcher.tsx +36 -0
  17. package/dist/examples/data-explorer/ExpansionRows/ExpansionRows.tsx +180 -0
  18. package/dist/examples/data-explorer/FilterChip/FilterChip.tsx +85 -0
  19. package/dist/examples/data-explorer/FilterPopoverContent/FilterPopoverContent.tsx +73 -0
  20. package/dist/examples/data-explorer/ListView/ListView.tsx +305 -0
  21. package/dist/examples/data-explorer/MoreFiltersPopover/MoreFiltersPopover.tsx +113 -0
  22. package/dist/examples/data-explorer/TableView/TableView.tsx +193 -0
  23. package/dist/examples/data-explorer/cells/CellRenderer.tsx +147 -0
  24. package/dist/examples/data-explorer/cells/CurrencyCell.tsx +31 -0
  25. package/dist/examples/data-explorer/cells/DoubleTextCell.tsx +27 -0
  26. package/dist/examples/data-explorer/cells/ExpandButton.tsx +67 -0
  27. package/dist/examples/data-explorer/cells/InventoryBadgeCell.tsx +52 -0
  28. package/dist/examples/data-explorer/cells/LinkCell.tsx +42 -0
  29. package/dist/examples/data-explorer/cells/ScoreBar.tsx +50 -0
  30. package/dist/examples/data-explorer/cells/StatusBadgeCell.tsx +39 -0
  31. package/dist/examples/data-explorer/cells/TextCell.tsx +35 -0
  32. package/dist/examples/data-explorer/cells/index.ts +26 -0
  33. package/dist/examples/data-explorer/domains/applications.ts +225 -0
  34. package/dist/examples/data-explorer/domains/customers.ts +267 -0
  35. package/dist/examples/data-explorer/domains/index.ts +26 -0
  36. package/dist/examples/data-explorer/domains/products.ts +1116 -0
  37. package/dist/examples/data-explorer/domains/projects.ts +205 -0
  38. package/dist/examples/data-explorer/hooks/use-data-explorer-state.ts +371 -0
  39. package/dist/examples/data-explorer/index.ts +3 -0
  40. package/dist/examples/data-explorer/types.ts +239 -0
  41. package/dist/fastnd-components.js +16426 -17975
  42. package/package.json +1 -1
  43. package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +0 -20
  44. package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +0 -9
  45. package/dist/components/ProgressCircle/ProgressCircle.d.ts +0 -9
  46. package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +0 -136
  47. package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +0 -179
  48. package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +0 -190
  49. package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +0 -43
  50. package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +0 -111
  51. package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +0 -282
  52. package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +0 -283
  53. package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +0 -41
  54. package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +0 -139
  55. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +0 -230
  56. package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +0 -33
  57. package/dist/examples/data-visualization/cell-renderers.tsx +0 -119
  58. package/dist/examples/data-visualization/constants.ts +0 -1251
  59. package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +0 -65
  60. package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +0 -40
  61. package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +0 -41
  62. package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +0 -61
  63. package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +0 -32
  64. package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +0 -32
  65. package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +0 -133
  66. package/dist/examples/data-visualization/hooks/use-filtered-data.ts +0 -84
  67. package/dist/examples/data-visualization/index.ts +0 -10
  68. package/dist/examples/data-visualization/types.ts +0 -103
@@ -2,7 +2,8 @@ import * as React from 'react';
2
2
  import { Button } from '@/components/ui/button';
3
3
  interface FavoriteButtonProps extends Omit<React.ComponentProps<typeof Button>, 'children' | 'onToggle'> {
4
4
  pressed: boolean;
5
- projectName: string;
5
+ itemName?: string;
6
+ projectName?: string;
6
7
  onPressedChange?: (newState: boolean) => void;
7
8
  }
8
9
  declare const FavoriteButton: React.ForwardRefExoticComponent<Omit<FavoriteButtonProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
@@ -1,7 +1,4 @@
1
- export * from './ColumnConfigPopover/ColumnConfigPopover';
2
- export * from './DoubleTextCell/DoubleTextCell';
3
1
  export * from './FavoriteButton/FavoriteButton';
4
- export * from './ProgressCircle/ProgressCircle';
5
2
  export * from './ui/accordion';
6
3
  export * from './ui/alert';
7
4
  export * from './ui/alert-dialog';
@@ -20,6 +17,7 @@ export * from './ui/collapsible';
20
17
  export * from './ui/combobox';
21
18
  export * from './ui/command';
22
19
  export * from './ui/context-menu';
20
+ export * from './ui/data-table';
23
21
  export * from './ui/dialog';
24
22
  export * from './ui/direction';
25
23
  export * from './ui/drawer';
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const badgeVariants: (props?: ({
4
- variant?: "link" | "secondary" | "default" | "destructive" | "outline" | "ghost" | null | undefined;
4
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
6
  declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
7
7
  asChild?: boolean;
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "link" | "secondary" | "default" | "destructive" | "outline" | "ghost" | null | undefined;
4
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
5
5
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
@@ -0,0 +1,8 @@
1
+ import { type ColumnDef } from '@tanstack/react-table';
2
+ interface DataTableProps<TData, TValue = unknown> {
3
+ columns: ColumnDef<TData, TValue>[];
4
+ data: TData[];
5
+ }
6
+ declare function DataTable<TData, TValue = unknown>({ columns, data, }: DataTableProps<TData, TValue>): import("react/jsx-runtime").JSX.Element;
7
+ export { DataTable };
8
+ export type { DataTableProps };
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import { Button } from "@/components/ui/button";
4
4
  declare function InputGroup({ className, ...props }: React.ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
5
5
  declare const inputGroupAddonVariants: (props?: ({
6
- align?: "inline-end" | "inline-start" | "block-end" | "block-start" | null | undefined;
6
+ align?: "inline-start" | "inline-end" | "block-start" | "block-end" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function InputGroupAddon({ className, align, ...props }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>): import("react/jsx-runtime").JSX.Element;
9
9
  declare const inputGroupButtonVariants: (props?: ({
@@ -11,7 +11,7 @@ declare function Item({ className, variant, size, asChild, ...props }: React.Com
11
11
  asChild?: boolean;
12
12
  }): import("react/jsx-runtime").JSX.Element;
13
13
  declare const itemMediaVariants: (props?: ({
14
- variant?: "image" | "default" | "icon" | null | undefined;
14
+ variant?: "default" | "icon" | "image" | null | undefined;
15
15
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
16
  declare function ItemMedia({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>): import("react/jsx-runtime").JSX.Element;
17
17
  declare function ItemContent({ className, ...props }: React.ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import { Tabs as TabsPrimitive } from "radix-ui";
4
4
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
5
5
  declare const tabsListVariants: (props?: ({
6
- variant?: "line" | "default" | null | undefined;
6
+ variant?: "default" | "line" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>): import("react/jsx-runtime").JSX.Element;
9
9
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,197 @@
1
+ import * as React from 'react'
2
+ import { X, Plus } from 'lucide-react'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import {
6
+ Carousel,
7
+ CarouselContent,
8
+ CarouselItem,
9
+ CarouselPrevious,
10
+ CarouselNext,
11
+ } from '@/components/ui/carousel'
12
+ import { cn } from '@/lib/utils'
13
+ import type { ColumnDef, CardLayout } from '../types'
14
+ import { CellRenderer } from '../cells'
15
+ import { ScoreBar } from '../cells/ScoreBar'
16
+
17
+ interface CardCarouselPanelProps {
18
+ row: Record<string, unknown>
19
+ columnKey: string
20
+ column: ColumnDef
21
+ cardLayout: CardLayout
22
+ columns: Record<string, ColumnDef>
23
+ onClose: () => void
24
+ className?: string
25
+ }
26
+
27
+ const CardCarouselPanel = ({
28
+ row,
29
+ columnKey,
30
+ column,
31
+ cardLayout,
32
+ columns,
33
+ onClose,
34
+ className,
35
+ }: CardCarouselPanelProps) => {
36
+ const items = row[columnKey]
37
+ if (!Array.isArray(items) || items.length === 0) return null
38
+ if (!column.expandColumns) return null
39
+
40
+ const title = column.expandTitleFn ? column.expandTitleFn(row) : column.expandLabel ?? columnKey
41
+ const scoreEc = column.expandColumns.find((ec) => ec.type === 'score-bar')
42
+
43
+ return (
44
+ <div
45
+ data-slot="card-carousel-panel"
46
+ className={cn(
47
+ 'col-span-full bg-accent border border-border rounded-lg p-0 overflow-hidden',
48
+ className
49
+ )}
50
+ >
51
+ {/* Header */}
52
+ <div className="flex justify-between items-center px-4 py-3 border-b border-border">
53
+ <div className="flex items-center gap-2">
54
+ <span className="font-semibold text-sm">{title}</span>
55
+ <Badge variant="secondary" className="text-xs">{items.length}</Badge>
56
+ </div>
57
+ <div className="flex items-center gap-2">
58
+ <Button
59
+ variant="ghost"
60
+ size="icon-xs"
61
+ aria-label="Schließen"
62
+ onClick={onClose}
63
+ >
64
+ <X className="size-4" aria-hidden="true" />
65
+ </Button>
66
+ </div>
67
+ </div>
68
+
69
+ {/* Carousel */}
70
+ <div className="px-4 py-3">
71
+ <Carousel
72
+ opts={{ align: 'start', dragFree: true }}
73
+ className="w-full px-10"
74
+ >
75
+ <CarouselContent className="-ml-3">
76
+ {items.map((item, index) => {
77
+ // Build virtual row by mapping expandColumns → main column keys via mapTo
78
+ const vRow: Record<string, unknown> = {}
79
+ for (const ec of column.expandColumns!) {
80
+ if (ec.mapTo) {
81
+ vRow[ec.mapTo] = (item as Record<string, unknown>)[ec.key]
82
+ }
83
+ if (ec.secondaryKey && ec.mapTo) {
84
+ const mainCol = columns[ec.mapTo]
85
+ if (mainCol?.secondary) {
86
+ vRow[mainCol.secondary] = (item as Record<string, unknown>)[ec.secondaryKey]
87
+ }
88
+ }
89
+ }
90
+
91
+ return (
92
+ <CarouselItem
93
+ key={index}
94
+ className="pl-3 basis-[280px] shrink-0"
95
+ >
96
+ {/* Mini card */}
97
+ <div className="flex flex-col gap-2 rounded-lg border border-border bg-card p-3 h-full text-[13px]">
98
+ {/* Mini card header */}
99
+ <div>
100
+ <div className="font-semibold text-xs line-clamp-2">
101
+ {String(vRow[cardLayout.titleField] ?? '')}
102
+ </div>
103
+ {cardLayout.subtitleField && vRow[cardLayout.subtitleField] != null && (
104
+ <div className="text-xs text-muted-foreground line-clamp-1 mt-0.5">
105
+ {String(vRow[cardLayout.subtitleField])}
106
+ </div>
107
+ )}
108
+ </div>
109
+
110
+ {/* Badges */}
111
+ {cardLayout.badgeFields && cardLayout.badgeFields.length > 0 && (
112
+ <div className="flex flex-wrap gap-1.5">
113
+ {cardLayout.badgeFields.map((bf) => {
114
+ const bc = columns[bf]
115
+ if (!bc || vRow[bf] == null) return null
116
+ return (
117
+ <CellRenderer
118
+ key={bf}
119
+ column={bc}
120
+ columnKey={bf}
121
+ row={vRow}
122
+ />
123
+ )
124
+ })}
125
+ </div>
126
+ )}
127
+
128
+ {/* Key-value rows */}
129
+ {cardLayout.rows && cardLayout.rows.length > 0 && (
130
+ <div className="flex flex-col gap-1">
131
+ {cardLayout.rows.map((r) => {
132
+ if (vRow[r.field] == null) return null
133
+ const rc = columns[r.field]
134
+ return (
135
+ <div key={r.field} className="flex justify-between">
136
+ <span className="text-muted-foreground shrink-0">{r.label}</span>
137
+ <span className="font-medium text-right">
138
+ {r.rendererOverride && rc ? (
139
+ <CellRenderer
140
+ column={{ ...rc, type: r.rendererOverride as ColumnDef['type'] }}
141
+ columnKey={r.field}
142
+ row={vRow}
143
+ />
144
+ ) : rc ? (
145
+ <CellRenderer column={rc} columnKey={r.field} row={vRow} />
146
+ ) : (
147
+ String(vRow[r.field] ?? '')
148
+ )}
149
+ </span>
150
+ </div>
151
+ )
152
+ })}
153
+ </div>
154
+ )}
155
+
156
+ {/* Score bar (expand-specific) */}
157
+ {scoreEc && (
158
+ <ScoreBar
159
+ value={
160
+ typeof (item as Record<string, unknown>)[scoreEc.key] === 'number'
161
+ ? Math.round(
162
+ ((item as Record<string, unknown>)[scoreEc.key] as number) * 100
163
+ )
164
+ : 0
165
+ }
166
+ />
167
+ )}
168
+
169
+ {/* Add button */}
170
+ <div className="mt-auto pt-1">
171
+ <Button
172
+ variant="outline"
173
+ size="sm"
174
+ className="gap-1.5 text-xs h-7 font-normal w-full"
175
+ onClick={(e) => e.preventDefault()}
176
+ >
177
+ <Plus className="size-3 shrink-0" aria-hidden="true" />
178
+ Hinzufügen
179
+ </Button>
180
+ </div>
181
+ </div>
182
+ </CarouselItem>
183
+ )
184
+ })}
185
+ </CarouselContent>
186
+ <CarouselPrevious className="-left-10" />
187
+ <CarouselNext className="-right-10" />
188
+ </Carousel>
189
+ </div>
190
+ </div>
191
+ )
192
+ }
193
+
194
+ CardCarouselPanel.displayName = 'CardCarouselPanel'
195
+
196
+ export { CardCarouselPanel }
197
+ export type { CardCarouselPanelProps }
@@ -0,0 +1,168 @@
1
+ import * as React from 'react'
2
+ import { Card } from '@/components/ui/card'
3
+ import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
4
+ import { cn } from '@/lib/utils'
5
+ import type { ColumnDef, CardLayout } from '../types'
6
+ import { CellRenderer } from '../cells'
7
+ import { CardCarouselPanel } from '../CardCarouselPanel/CardCarouselPanel'
8
+
9
+ interface CardViewProps {
10
+ data: Record<string, unknown>[]
11
+ columns: Record<string, ColumnDef>
12
+ layout: CardLayout
13
+ expandedRows: Set<string>
14
+ favorites: Set<string>
15
+ onToggleExpand: (rowId: string) => void
16
+ onToggleFavorite: (rowId: string) => void
17
+ className?: string
18
+ }
19
+
20
+ const CardView = ({
21
+ data,
22
+ columns,
23
+ layout,
24
+ expandedRows,
25
+ favorites,
26
+ onToggleExpand,
27
+ onToggleFavorite,
28
+ className,
29
+ }: CardViewProps) => {
30
+ const expandFields = layout.expandFields ?? (layout.expandField ? [layout.expandField] : [])
31
+
32
+ return (
33
+ <div
34
+ data-slot="card-view"
35
+ className={cn(
36
+ 'grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 px-6',
37
+ className
38
+ )}
39
+ >
40
+ {data.map((row) => {
41
+ const rowId = row['id'] as string
42
+ const isFavorite = favorites.has(rowId)
43
+ const isExpanded = expandFields.some((f) => expandedRows.has(`${rowId}::${f}`))
44
+
45
+ const activeExpandField =
46
+ expandFields.find((f) => expandedRows.has(`${rowId}::${f}`)) ?? null
47
+
48
+ return (
49
+ <React.Fragment key={rowId}>
50
+ <Card
51
+ className={cn(
52
+ 'rounded-lg p-0 gap-0 overflow-hidden hover:shadow-md transition-shadow',
53
+ isExpanded && 'border-b-2 border-b-primary shadow-md'
54
+ )}
55
+ >
56
+ {/* Inner content padding wrapper */}
57
+ <div className="p-4 flex flex-col gap-3">
58
+ {/* Header: title + favorite */}
59
+ <div className="flex justify-between items-start gap-2">
60
+ <div className="min-w-0">
61
+ <div className="font-semibold text-sm min-h-[2.8em] line-clamp-2">
62
+ {String(row[layout.titleField] ?? '')}
63
+ </div>
64
+ {layout.subtitleField && row[layout.subtitleField] != null && (
65
+ <div className="text-xs text-muted-foreground mt-0.5">
66
+ {String(row[layout.subtitleField])}
67
+ </div>
68
+ )}
69
+ </div>
70
+ <FavoriteButton
71
+ pressed={isFavorite}
72
+ itemName={rowId}
73
+ onPressedChange={() => onToggleFavorite(rowId)}
74
+ className="shrink-0 -mt-0.5 -mr-1"
75
+ />
76
+ </div>
77
+
78
+ {/* Badge fields */}
79
+ {layout.badgeFields && layout.badgeFields.length > 0 && (
80
+ <div className="flex flex-wrap gap-1.5">
81
+ {layout.badgeFields.map((bf) => {
82
+ const bc = columns[bf]
83
+ if (!bc) return null
84
+ return (
85
+ <CellRenderer
86
+ key={bf}
87
+ column={bc}
88
+ columnKey={bf}
89
+ row={row}
90
+ />
91
+ )
92
+ })}
93
+ </div>
94
+ )}
95
+
96
+ {/* Key-value rows */}
97
+ {layout.rows && layout.rows.length > 0 && (
98
+ <div className="flex flex-col gap-1.5">
99
+ {layout.rows.map((r) => {
100
+ const rc = columns[r.field]
101
+ return (
102
+ <div key={r.field} className="flex justify-between text-[13px]">
103
+ <span className="text-muted-foreground">{r.label}</span>
104
+ <span className="font-medium text-right">
105
+ {r.rendererOverride && rc ? (
106
+ <CellRenderer
107
+ column={{ ...rc, type: r.rendererOverride as ColumnDef['type'] }}
108
+ columnKey={r.field}
109
+ row={row}
110
+ />
111
+ ) : rc ? (
112
+ <CellRenderer column={rc} columnKey={r.field} row={row} />
113
+ ) : (
114
+ String(row[r.field] ?? '')
115
+ )}
116
+ </span>
117
+ </div>
118
+ )
119
+ })}
120
+ </div>
121
+ )}
122
+ </div>
123
+
124
+ {/* Footer — outside padding div, with border-top */}
125
+ {expandFields.length > 0 && (
126
+ <div className="mt-auto border-t border-border px-4 py-3 flex gap-2">
127
+ {expandFields.map((ef) => {
128
+ const ec = columns[ef]
129
+ if (!ec) return null
130
+ const expandKey = `${rowId}::${ef}`
131
+ return (
132
+ <CellRenderer
133
+ key={ef}
134
+ column={ec}
135
+ columnKey={ef}
136
+ row={row}
137
+ expanded={expandedRows.has(expandKey)}
138
+ onToggleExpand={() => onToggleExpand(expandKey)}
139
+ className="flex-1 justify-center"
140
+ />
141
+ )
142
+ })}
143
+ </div>
144
+ )}
145
+ </Card>
146
+
147
+ {/* Expansion carousel panel — spans full grid width */}
148
+ {activeExpandField && (
149
+ <CardCarouselPanel
150
+ row={row}
151
+ columnKey={activeExpandField}
152
+ column={columns[activeExpandField]}
153
+ cardLayout={layout}
154
+ columns={columns}
155
+ onClose={() => onToggleExpand(`${rowId}::${activeExpandField}`)}
156
+ />
157
+ )}
158
+ </React.Fragment>
159
+ )
160
+ })}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ CardView.displayName = 'CardView'
166
+
167
+ export { CardView }
168
+ export type { CardViewProps }
@@ -0,0 +1,157 @@
1
+ import * as React from 'react'
2
+ import { GripVertical, SlidersHorizontal } from 'lucide-react'
3
+ import {
4
+ DndContext,
5
+ closestCenter,
6
+ KeyboardSensor,
7
+ PointerSensor,
8
+ useSensor,
9
+ useSensors,
10
+ type DragEndEvent,
11
+ } from '@dnd-kit/core'
12
+ import {
13
+ SortableContext,
14
+ sortableKeyboardCoordinates,
15
+ useSortable,
16
+ verticalListSortingStrategy,
17
+ arrayMove,
18
+ } from '@dnd-kit/sortable'
19
+ import { CSS } from '@dnd-kit/utilities'
20
+ import { Button } from '@/components/ui/button'
21
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
22
+ import { Switch } from '@/components/ui/switch'
23
+ import type { ColumnDef } from '../types'
24
+ import { cn } from '@/lib/utils'
25
+
26
+ // --- Sortable item ---
27
+
28
+ interface SortableColumnItemProps {
29
+ id: string
30
+ label: string
31
+ visible: boolean
32
+ onToggleVisibility: () => void
33
+ }
34
+
35
+ function SortableColumnItem({ id, label, visible, onToggleVisibility }: SortableColumnItemProps) {
36
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
37
+
38
+ const style: React.CSSProperties = {
39
+ transform: CSS.Transform.toString(transform),
40
+ transition,
41
+ opacity: isDragging ? 0.5 : 1,
42
+ }
43
+
44
+ return (
45
+ <div
46
+ ref={setNodeRef}
47
+ style={style}
48
+ className="flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-accent"
49
+ >
50
+ <button
51
+ {...attributes}
52
+ {...listeners}
53
+ className="cursor-grab touch-none text-muted-foreground hover:text-foreground focus-visible:outline-none active:cursor-grabbing"
54
+ aria-label={`${label} verschieben`}
55
+ tabIndex={0}
56
+ >
57
+ <GripVertical className="size-4" />
58
+ </button>
59
+ <span className="flex-1 leading-snug">{label}</span>
60
+ <Switch
61
+ checked={visible}
62
+ onCheckedChange={onToggleVisibility}
63
+ size="sm"
64
+ aria-label={visible ? `${label} ausblenden` : `${label} einblenden`}
65
+ />
66
+ </div>
67
+ )
68
+ }
69
+
70
+ // --- Main component ---
71
+
72
+ export interface ColumnConfigPopoverProps {
73
+ columns: Record<string, ColumnDef>
74
+ columnOrder: string[]
75
+ columnVisibility: Record<string, boolean>
76
+ onToggleVisibility: (columnKey: string) => void
77
+ onReorder: (newOrder: string[]) => void
78
+ className?: string
79
+ }
80
+
81
+ export function ColumnConfigPopover({
82
+ columns,
83
+ columnOrder,
84
+ columnVisibility,
85
+ onToggleVisibility,
86
+ onReorder,
87
+ className,
88
+ }: ColumnConfigPopoverProps) {
89
+ const sensors = useSensors(
90
+ useSensor(PointerSensor),
91
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
92
+ )
93
+
94
+ // Exclude 'favorite' columns from config list
95
+ const configurableKeys = columnOrder.filter(
96
+ (key) => columns[key] && columns[key].type !== 'favorite',
97
+ )
98
+
99
+ function handleDragEnd(event: DragEndEvent) {
100
+ const { active, over } = event
101
+ if (!over || active.id === over.id) return
102
+
103
+ const oldIndex = columnOrder.indexOf(active.id as string)
104
+ const newIndex = columnOrder.indexOf(over.id as string)
105
+ if (oldIndex === -1 || newIndex === -1) return
106
+
107
+ onReorder(arrayMove(columnOrder, oldIndex, newIndex))
108
+ }
109
+
110
+ return (
111
+ <Popover>
112
+ <PopoverTrigger asChild>
113
+ <Button
114
+ data-slot="column-config-popover"
115
+ variant="outline"
116
+ size="sm"
117
+ aria-haspopup="dialog"
118
+ aria-label="Spalten konfigurieren"
119
+ className={cn('h-8 gap-1.5 text-sm', className)}
120
+ >
121
+ <SlidersHorizontal className="size-3.5" />
122
+ <span>Konfigurieren</span>
123
+ </Button>
124
+ </PopoverTrigger>
125
+
126
+ <PopoverContent
127
+ align="end"
128
+ sideOffset={4}
129
+ role="dialog"
130
+ aria-label="Spalten konfigurieren"
131
+ className="w-72 p-0"
132
+ >
133
+ <div className="border-b px-3 py-2">
134
+ <span className="text-sm font-medium">Spalten konfigurieren</span>
135
+ </div>
136
+
137
+ <div className="max-h-[400px] overflow-y-auto p-2">
138
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
139
+ <SortableContext items={configurableKeys} strategy={verticalListSortingStrategy}>
140
+ {configurableKeys.map((key) => (
141
+ <SortableColumnItem
142
+ key={key}
143
+ id={key}
144
+ label={columns[key].label}
145
+ visible={columnVisibility[key] ?? true}
146
+ onToggleVisibility={() => onToggleVisibility(key)}
147
+ />
148
+ ))}
149
+ </SortableContext>
150
+ </DndContext>
151
+ </div>
152
+ </PopoverContent>
153
+ </Popover>
154
+ )
155
+ }
156
+
157
+ ColumnConfigPopover.displayName = 'ColumnConfigPopover'
@@ -0,0 +1,56 @@
1
+ import { Search } from 'lucide-react'
2
+ import { Button } from '@/components/ui/button'
3
+ import {
4
+ Empty,
5
+ EmptyHeader,
6
+ EmptyMedia,
7
+ EmptyTitle,
8
+ EmptyDescription,
9
+ } from '@/components/ui/empty'
10
+ import { cn } from '@/lib/utils'
11
+
12
+ export interface DataExplorerEmptyProps {
13
+ searchTerm: string
14
+ hasActiveFilters: boolean
15
+ onResetAll: () => void
16
+ className?: string
17
+ }
18
+
19
+ function buildDescription(searchTerm: string, hasActiveFilters: boolean): string {
20
+ const hasTerm = searchTerm.length > 0
21
+ if (hasTerm && hasActiveFilters) {
22
+ return `Für '${searchTerm}' mit den aktiven Filtern wurden keine Ergebnisse gefunden.`
23
+ }
24
+ if (hasTerm) {
25
+ return `Für '${searchTerm}' wurden keine Ergebnisse gefunden.`
26
+ }
27
+ return 'Die aktiven Filter liefern keine Ergebnisse.'
28
+ }
29
+
30
+ export function DataExplorerEmpty({
31
+ searchTerm,
32
+ hasActiveFilters,
33
+ onResetAll,
34
+ className,
35
+ }: DataExplorerEmptyProps) {
36
+ const showReset = hasActiveFilters || searchTerm.length > 0
37
+
38
+ return (
39
+ <Empty data-slot="data-explorer-empty" className={cn(className)}>
40
+ <EmptyHeader>
41
+ <EmptyMedia variant="icon">
42
+ <Search />
43
+ </EmptyMedia>
44
+ <EmptyTitle>Keine Ergebnisse gefunden</EmptyTitle>
45
+ <EmptyDescription>{buildDescription(searchTerm, hasActiveFilters)}</EmptyDescription>
46
+ </EmptyHeader>
47
+ {showReset && (
48
+ <Button variant="outline" size="sm" onClick={onResetAll}>
49
+ Filter zurücksetzen
50
+ </Button>
51
+ )}
52
+ </Empty>
53
+ )
54
+ }
55
+
56
+ DataExplorerEmpty.displayName = 'DataExplorerEmpty'