@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.
- package/dist/components/FavoriteButton/FavoriteButton.d.ts +2 -1
- package/dist/components/index.d.ts +1 -3
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/data-table.d.ts +8 -0
- 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/data-explorer/CardCarouselPanel/CardCarouselPanel.tsx +197 -0
- package/dist/examples/data-explorer/CardView/CardView.tsx +168 -0
- package/dist/examples/data-explorer/ColumnConfigPopover/ColumnConfigPopover.tsx +157 -0
- package/dist/examples/data-explorer/DataExplorerEmpty/DataExplorerEmpty.tsx +56 -0
- package/dist/examples/data-explorer/DataExplorerPage/DataExplorerPage.tsx +101 -0
- package/dist/examples/data-explorer/DataExplorerPagination/DataExplorerPagination.tsx +129 -0
- package/dist/examples/data-explorer/DataExplorerToolbar/DataExplorerToolbar.tsx +143 -0
- package/dist/examples/data-explorer/DomainSwitcher/DomainSwitcher.tsx +36 -0
- package/dist/examples/data-explorer/ExpansionRows/ExpansionRows.tsx +180 -0
- package/dist/examples/data-explorer/FilterChip/FilterChip.tsx +85 -0
- package/dist/examples/data-explorer/FilterPopoverContent/FilterPopoverContent.tsx +73 -0
- package/dist/examples/data-explorer/ListView/ListView.tsx +305 -0
- package/dist/examples/data-explorer/MoreFiltersPopover/MoreFiltersPopover.tsx +113 -0
- package/dist/examples/data-explorer/TableView/TableView.tsx +193 -0
- package/dist/examples/data-explorer/cells/CellRenderer.tsx +147 -0
- package/dist/examples/data-explorer/cells/CurrencyCell.tsx +31 -0
- package/dist/examples/data-explorer/cells/DoubleTextCell.tsx +27 -0
- package/dist/examples/data-explorer/cells/ExpandButton.tsx +67 -0
- package/dist/examples/data-explorer/cells/InventoryBadgeCell.tsx +52 -0
- package/dist/examples/data-explorer/cells/LinkCell.tsx +42 -0
- package/dist/examples/data-explorer/cells/ScoreBar.tsx +50 -0
- package/dist/examples/data-explorer/cells/StatusBadgeCell.tsx +39 -0
- package/dist/examples/data-explorer/cells/TextCell.tsx +35 -0
- package/dist/examples/data-explorer/cells/index.ts +26 -0
- package/dist/examples/data-explorer/domains/applications.ts +225 -0
- package/dist/examples/data-explorer/domains/customers.ts +267 -0
- package/dist/examples/data-explorer/domains/index.ts +26 -0
- package/dist/examples/data-explorer/domains/products.ts +1116 -0
- package/dist/examples/data-explorer/domains/projects.ts +205 -0
- package/dist/examples/data-explorer/hooks/use-data-explorer-state.ts +371 -0
- package/dist/examples/data-explorer/index.ts +3 -0
- package/dist/examples/data-explorer/types.ts +239 -0
- package/dist/fastnd-components.js +16426 -17975
- package/package.json +1 -1
- package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +0 -20
- package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +0 -9
- package/dist/components/ProgressCircle/ProgressCircle.d.ts +0 -9
- package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +0 -136
- package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +0 -179
- package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +0 -190
- package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +0 -43
- package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +0 -111
- package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +0 -282
- package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +0 -283
- package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +0 -41
- package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +0 -139
- package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +0 -230
- package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +0 -33
- package/dist/examples/data-visualization/cell-renderers.tsx +0 -119
- package/dist/examples/data-visualization/constants.ts +0 -1251
- package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +0 -65
- package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +0 -40
- package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +0 -41
- package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +0 -61
- package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +0 -32
- package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +0 -32
- package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +0 -133
- package/dist/examples/data-visualization/hooks/use-filtered-data.ts +0 -84
- package/dist/examples/data-visualization/index.ts +0 -10
- 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
|
-
|
|
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?: "
|
|
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?: "
|
|
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-
|
|
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?: "
|
|
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?: "
|
|
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'
|