@fastnd/components 1.0.31 → 1.0.33
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/QuickAccessCard/QuickAccessCard.d.ts +12 -0
- package/dist/components/ScoreBar/ScoreBar.d.ts +8 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/examples/dashboard/DashboardPage/DashboardPage.tsx +1 -1
- package/dist/examples/dashboard/ProjectList/ProjectList.tsx +87 -47
- package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +41 -56
- package/dist/examples/dashboard/constants.ts +20 -6
- package/dist/examples/dashboard/types.ts +2 -0
- package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
- package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +171 -0
- package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +124 -0
- package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +223 -0
- package/dist/examples/data-visualization/DataExplorerPagination/DataExplorerPagination.tsx +143 -0
- package/dist/examples/data-visualization/DataExplorerToolbar/DataExplorerToolbar.tsx +88 -0
- package/dist/examples/data-visualization/DataListView/DataListView.tsx +283 -0
- package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +455 -0
- package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +151 -0
- package/dist/examples/data-visualization/FilterChipGroup/FilterChipGroup.tsx +125 -0
- package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +132 -0
- package/dist/examples/data-visualization/constants.ts +587 -0
- package/dist/examples/data-visualization/hooks/use-column-config.ts +76 -0
- package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +318 -0
- package/dist/examples/data-visualization/index.ts +1 -0
- package/dist/examples/data-visualization/types.ts +110 -0
- package/dist/examples/quickaccess/QuickAccess/QuickAccess.tsx +97 -0
- package/dist/examples/quickaccess/index.ts +2 -0
- package/dist/examples/quickaccess/types.ts +11 -0
- package/dist/fastnd-components.js +5708 -5590
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
interface QuickAccessCardProps extends React.ComponentProps<'a'> {
|
|
3
|
+
label: string;
|
|
4
|
+
count: number;
|
|
5
|
+
countLabel?: string;
|
|
6
|
+
icon: React.ReactNode;
|
|
7
|
+
variant?: 'default' | 'overdue';
|
|
8
|
+
isActive?: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare const QuickAccessCard: React.ForwardRefExoticComponent<Omit<QuickAccessCardProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
|
|
11
|
+
export { QuickAccessCard };
|
|
12
|
+
export type { QuickAccessCardProps };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
interface ScoreBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
3
|
+
value: number;
|
|
4
|
+
showLabel?: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare const ScoreBar: React.ForwardRefExoticComponent<ScoreBarProps & React.RefAttributes<HTMLDivElement>>;
|
|
7
|
+
export { ScoreBar };
|
|
8
|
+
export type { ScoreBarProps };
|
|
@@ -25,7 +25,7 @@ const DashboardPage = React.forwardRef<HTMLElement, DashboardPageProps>(
|
|
|
25
25
|
ref={ref}
|
|
26
26
|
data-slot="dashboard-page"
|
|
27
27
|
className={cn(
|
|
28
|
-
'grid grid-cols-1 lg:grid-cols-[
|
|
28
|
+
'grid grid-cols-1 lg:grid-cols-[280px_1fr] gap-6 max-w-[1400px] mx-auto p-8',
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
{...props}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { Star } from 'lucide-react'
|
|
3
2
|
import { Card, CardContent } from '@/components/ui/card'
|
|
4
3
|
import { Input } from '@/components/ui/input'
|
|
5
|
-
import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from '@/components/ui/table'
|
|
6
4
|
import { Badge } from '@/components/ui/badge'
|
|
7
|
-
import {
|
|
5
|
+
import { DataTableView } from '@/features/data-visualization/DataTableView/DataTableView'
|
|
6
|
+
import { formatRelativeDate } from '@/utils/date'
|
|
8
7
|
import { cn } from '@/lib/utils'
|
|
8
|
+
import type { ColumnDef, SortState } from '@/features/data-visualization/types'
|
|
9
9
|
import type { Project, ProjectStatus } from '../types'
|
|
10
10
|
import { STATUS_CONFIG } from '../constants'
|
|
11
11
|
|
|
@@ -17,6 +17,9 @@ const STATUS_BADGE_CLASSES: Record<ProjectStatus, string> = {
|
|
|
17
17
|
'abgeschlossen': 'bg-[#1ec489]/15 text-[#0a6e44] border-transparent',
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const STATIC_SORT: SortState = { column: null, direction: 'asc' }
|
|
21
|
+
const EMPTY_ROWS = new Set<string>()
|
|
22
|
+
|
|
20
23
|
interface ProjectListProps extends React.ComponentProps<'section'> {
|
|
21
24
|
projects: Project[]
|
|
22
25
|
filterLabel: string
|
|
@@ -38,6 +41,75 @@ const ProjectList = React.forwardRef<HTMLElement, ProjectListProps>(
|
|
|
38
41
|
},
|
|
39
42
|
ref,
|
|
40
43
|
) => {
|
|
44
|
+
const columns: Record<string, ColumnDef> = React.useMemo(() => ({
|
|
45
|
+
isFavorite: {
|
|
46
|
+
label: 'Favorit',
|
|
47
|
+
type: 'favorite',
|
|
48
|
+
sortable: false,
|
|
49
|
+
filterable: false,
|
|
50
|
+
visible: true,
|
|
51
|
+
headerIcon: 'star',
|
|
52
|
+
headerTooltip: 'Favorit',
|
|
53
|
+
},
|
|
54
|
+
name: {
|
|
55
|
+
label: 'Projekt / Applikation',
|
|
56
|
+
type: 'double-text',
|
|
57
|
+
secondary: 'application',
|
|
58
|
+
sortable: false,
|
|
59
|
+
filterable: false,
|
|
60
|
+
visible: true,
|
|
61
|
+
},
|
|
62
|
+
client: {
|
|
63
|
+
label: 'Kunde',
|
|
64
|
+
type: 'text',
|
|
65
|
+
sortable: false,
|
|
66
|
+
filterable: false,
|
|
67
|
+
visible: true,
|
|
68
|
+
},
|
|
69
|
+
status: {
|
|
70
|
+
label: 'Status',
|
|
71
|
+
type: 'text',
|
|
72
|
+
sortable: false,
|
|
73
|
+
filterable: false,
|
|
74
|
+
visible: true,
|
|
75
|
+
render: (val) => {
|
|
76
|
+
const s = val as ProjectStatus
|
|
77
|
+
return (
|
|
78
|
+
<Badge variant="secondary" className={STATUS_BADGE_CLASSES[s]}>
|
|
79
|
+
{STATUS_CONFIG[s].label}
|
|
80
|
+
</Badge>
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
updatedAt: {
|
|
85
|
+
label: 'Aktivität',
|
|
86
|
+
type: 'text',
|
|
87
|
+
sortable: false,
|
|
88
|
+
filterable: false,
|
|
89
|
+
visible: true,
|
|
90
|
+
render: (val, row) => (
|
|
91
|
+
<div className="flex flex-col">
|
|
92
|
+
<span className="font-medium text-[13px]">{formatRelativeDate(String(val))}</span>
|
|
93
|
+
<span className="text-muted-foreground text-xs">
|
|
94
|
+
Erstellt: {new Date(String(row.createdAt)).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
}), [])
|
|
100
|
+
|
|
101
|
+
const visibleColumns = ['isFavorite', 'name', 'client', 'status', 'updatedAt']
|
|
102
|
+
|
|
103
|
+
const rowData = React.useMemo(
|
|
104
|
+
() => projects.map(p => ({ ...p } as Record<string, unknown>)),
|
|
105
|
+
[projects],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const favorites = React.useMemo(
|
|
109
|
+
() => new Set(projects.filter(p => p.isFavorite).map(p => p.id)),
|
|
110
|
+
[projects],
|
|
111
|
+
)
|
|
112
|
+
|
|
41
113
|
return (
|
|
42
114
|
<section
|
|
43
115
|
ref={ref}
|
|
@@ -63,50 +135,18 @@ const ProjectList = React.forwardRef<HTMLElement, ProjectListProps>(
|
|
|
63
135
|
onChange={e => onSearchChange(e.target.value)}
|
|
64
136
|
/>
|
|
65
137
|
</div>
|
|
66
|
-
<CardContent>
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
</TableHeader>
|
|
79
|
-
<TableBody>
|
|
80
|
-
{projects.map(project => (
|
|
81
|
-
<TableRow key={project.id}>
|
|
82
|
-
<TableCell>
|
|
83
|
-
<span className="font-medium">{project.name}</span>
|
|
84
|
-
</TableCell>
|
|
85
|
-
<TableCell>
|
|
86
|
-
<span className="text-muted-foreground text-[0.8125rem]">
|
|
87
|
-
{project.client}
|
|
88
|
-
</span>
|
|
89
|
-
</TableCell>
|
|
90
|
-
<TableCell>{project.application}</TableCell>
|
|
91
|
-
<TableCell>
|
|
92
|
-
<Badge
|
|
93
|
-
variant="secondary"
|
|
94
|
-
className={cn(STATUS_BADGE_CLASSES[project.status])}
|
|
95
|
-
>
|
|
96
|
-
{STATUS_CONFIG[project.status].label}
|
|
97
|
-
</Badge>
|
|
98
|
-
</TableCell>
|
|
99
|
-
<TableCell>
|
|
100
|
-
<FavoriteButton
|
|
101
|
-
pressed={project.isFavorite}
|
|
102
|
-
projectName={project.name}
|
|
103
|
-
onPressedChange={() => onToggleFavorite(project.id)}
|
|
104
|
-
/>
|
|
105
|
-
</TableCell>
|
|
106
|
-
</TableRow>
|
|
107
|
-
))}
|
|
108
|
-
</TableBody>
|
|
109
|
-
</Table>
|
|
138
|
+
<CardContent className="p-0">
|
|
139
|
+
<DataTableView
|
|
140
|
+
data={rowData}
|
|
141
|
+
columns={columns}
|
|
142
|
+
visibleColumns={visibleColumns}
|
|
143
|
+
sort={STATIC_SORT}
|
|
144
|
+
onToggleSort={() => {}}
|
|
145
|
+
expandedRows={EMPTY_ROWS}
|
|
146
|
+
onToggleExpansion={() => {}}
|
|
147
|
+
favorites={favorites}
|
|
148
|
+
onToggleFavorite={onToggleFavorite}
|
|
149
|
+
/>
|
|
110
150
|
</CardContent>
|
|
111
151
|
</Card>
|
|
112
152
|
</section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { Pie, PieChart,
|
|
2
|
+
import { Pie, PieChart, Sector } from 'recharts'
|
|
3
3
|
import type { PieSectorShapeProps } from 'recharts/types/polar/Pie'
|
|
4
4
|
import {
|
|
5
5
|
ChartContainer,
|
|
@@ -57,62 +57,47 @@ const StatusDonutChart = React.forwardRef<HTMLDivElement, StatusDonutChartProps>
|
|
|
57
57
|
className={cn(className)}
|
|
58
58
|
{...props}
|
|
59
59
|
>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
content={({ viewBox }) => {
|
|
85
|
-
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
|
86
|
-
return (
|
|
87
|
-
<text
|
|
88
|
-
x={viewBox.cx}
|
|
89
|
-
y={viewBox.cy}
|
|
90
|
-
textAnchor="middle"
|
|
91
|
-
dominantBaseline="middle"
|
|
92
|
-
>
|
|
93
|
-
<tspan
|
|
94
|
-
x={viewBox.cx}
|
|
95
|
-
y={viewBox.cy}
|
|
96
|
-
className="fill-foreground text-3xl font-bold"
|
|
97
|
-
style={{ fontFamily: 'var(--font-clash)' }}
|
|
98
|
-
>
|
|
99
|
-
{total.toLocaleString()}
|
|
100
|
-
</tspan>
|
|
101
|
-
<tspan
|
|
102
|
-
x={viewBox.cx}
|
|
103
|
-
y={(viewBox.cy || 0) + 24}
|
|
104
|
-
className="fill-muted-foreground text-xs uppercase tracking-wider"
|
|
105
|
-
>
|
|
106
|
-
Gesamt
|
|
107
|
-
</tspan>
|
|
108
|
-
</text>
|
|
109
|
-
)
|
|
110
|
-
}
|
|
111
|
-
}}
|
|
60
|
+
{/* Relative wrapper so the center label overlay can be positioned absolutely */}
|
|
61
|
+
<div className="relative mx-auto aspect-square max-h-[200px]">
|
|
62
|
+
<ChartContainer config={chartConfig} className="size-full">
|
|
63
|
+
<PieChart>
|
|
64
|
+
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
65
|
+
<Pie
|
|
66
|
+
data={chartData}
|
|
67
|
+
dataKey="count"
|
|
68
|
+
nameKey="status"
|
|
69
|
+
innerRadius={60}
|
|
70
|
+
outerRadius={85}
|
|
71
|
+
strokeWidth={5}
|
|
72
|
+
className="cursor-pointer"
|
|
73
|
+
onClick={handlePieClick}
|
|
74
|
+
shape={({
|
|
75
|
+
index,
|
|
76
|
+
outerRadius: or = 0,
|
|
77
|
+
...sectorProps
|
|
78
|
+
}: PieSectorShapeProps) => (
|
|
79
|
+
<Sector
|
|
80
|
+
{...sectorProps}
|
|
81
|
+
outerRadius={index === activeIndex ? or + 10 : or}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
112
84
|
/>
|
|
113
|
-
</
|
|
114
|
-
</
|
|
115
|
-
|
|
85
|
+
</PieChart>
|
|
86
|
+
</ChartContainer>
|
|
87
|
+
|
|
88
|
+
{/* Center label — rendered as HTML so it is always in the DOM (recharts SVG labels are invisible in jsdom) */}
|
|
89
|
+
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-0.5">
|
|
90
|
+
<span
|
|
91
|
+
className="text-3xl font-bold text-foreground"
|
|
92
|
+
style={{ fontFamily: 'var(--font-clash)' }}
|
|
93
|
+
>
|
|
94
|
+
{total.toLocaleString()}
|
|
95
|
+
</span>
|
|
96
|
+
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
97
|
+
Gesamt
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
116
101
|
</div>
|
|
117
102
|
)
|
|
118
103
|
}
|
|
@@ -8,11 +8,25 @@ export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color: string
|
|
|
8
8
|
'abgeschlossen': { label: 'Abgeschlossen', color: '#1ec489' },
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const d = (daysAgo: number) => new Date(Date.now() - daysAgo * 86_400_000).toISOString()
|
|
12
|
+
|
|
11
13
|
export const MOCK_PROJECTS: Project[] = [
|
|
12
|
-
{ id: '1', name: 'E-Commerce Relaunch',
|
|
13
|
-
{ id: '2', name: 'CRM Integration MVP',
|
|
14
|
-
{ id: '3', name: 'Data Analytics Dashboard', client: 'DataViz AG', application: 'React / Supabase',
|
|
15
|
-
{ id: '4', name: 'Mobile App Onboarding',
|
|
16
|
-
{ id: '5', name: 'Payment Gateway Update',
|
|
17
|
-
{ id: '6', name: 'Legacy System Migration',
|
|
14
|
+
{ id: '1', name: 'E-Commerce Relaunch', client: 'Müller GmbH', application: 'Shopify CMS', status: 'neu', isFavorite: false, createdAt: '2025-03-15', updatedAt: d(0) },
|
|
15
|
+
{ id: '2', name: 'CRM Integration MVP', client: 'TechCorp Inc.', application: 'Salesforce API', status: 'offen', isFavorite: true, createdAt: '2025-02-10', updatedAt: d(1) },
|
|
16
|
+
{ id: '3', name: 'Data Analytics Dashboard', client: 'DataViz AG', application: 'React / Supabase', status: 'in-prufung', isFavorite: false, createdAt: '2025-01-20', updatedAt: d(3) },
|
|
17
|
+
{ id: '4', name: 'Mobile App Onboarding', client: 'FitHealth', application: 'Flutter', status: 'neu', isFavorite: false, createdAt: '2025-03-01', updatedAt: d(7) },
|
|
18
|
+
{ id: '5', name: 'Payment Gateway Update', client: 'FinTech Solutions', application: 'Stripe API', status: 'validierung', isFavorite: false, createdAt: '2024-11-05', updatedAt: d(14) },
|
|
19
|
+
{ id: '6', name: 'Legacy System Migration', client: 'OldBank Corp.', application: 'Node.js / PostgreSQL', status: 'abgeschlossen', isFavorite: true, createdAt: '2024-08-20', updatedAt: d(60) },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const MOCK_PROJECTS_EXTENDED: Project[] = [
|
|
23
|
+
...MOCK_PROJECTS,
|
|
24
|
+
{ id: '7', name: 'Supply Chain Visibility Platform', client: 'LogiTrans GmbH', application: 'Next.js / GraphQL', status: 'in-prufung', isFavorite: true, createdAt: '2024-12-03', updatedAt: d(2) },
|
|
25
|
+
{ id: '8', name: 'HR Self-Service Portal', client: 'Personalwerk AG', application: 'Angular / .NET Core', status: 'offen', isFavorite: false, createdAt: '2025-01-08', updatedAt: d(5) },
|
|
26
|
+
{ id: '9', name: 'IoT Monitoring Dashboard', client: 'SmartFactory SE', application: 'Vue.js / InfluxDB', status: 'validierung', isFavorite: false, createdAt: '2024-10-14', updatedAt: d(9) },
|
|
27
|
+
{ id: '10', name: 'Document Management System', client: 'Kanzlei Bauer & Co', application: 'SharePoint / Power Automate', status: 'abgeschlossen', isFavorite: false, createdAt: '2024-06-01', updatedAt: d(30) },
|
|
28
|
+
{ id: '11', name: 'Customer Loyalty App', client: 'Retail Chain GmbH', application: 'React Native', status: 'neu', isFavorite: false, createdAt: '2025-03-20', updatedAt: d(0) },
|
|
29
|
+
{ id: '12', name: 'Predictive Maintenance Engine', client: 'IndustrieWerk KG', application: 'Python / FastAPI / ML',status: 'in-prufung', isFavorite: true, createdAt: '2024-09-17', updatedAt: d(4) },
|
|
30
|
+
{ id: '13', name: 'Compliance Reporting Suite', client: 'RegulaCorp AG', application: 'Power BI / Azure', status: 'offen', isFavorite: false, createdAt: '2025-02-25', updatedAt: d(11) },
|
|
31
|
+
{ id: '14', name: 'Field Service Management', client: 'TechService GmbH', application: 'Salesforce Field Srv', status: 'validierung', isFavorite: true, createdAt: '2024-07-30', updatedAt: d(6) },
|
|
18
32
|
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Plus } from 'lucide-react'
|
|
3
|
+
import { Badge } from '@/components/ui/badge'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import {
|
|
6
|
+
Carousel,
|
|
7
|
+
CarouselContent,
|
|
8
|
+
CarouselItem,
|
|
9
|
+
CarouselPrevious,
|
|
10
|
+
CarouselNext,
|
|
11
|
+
} from '@/components/ui/carousel'
|
|
12
|
+
import { ScoreBar } from '@/components/ScoreBar/ScoreBar'
|
|
13
|
+
import { renderCell } from '../CellRenderers/CellRenderers'
|
|
14
|
+
import { cn } from '@/lib/utils'
|
|
15
|
+
import type { CardLayout, ColumnDef, ExpandColumnDef } from '../types'
|
|
16
|
+
|
|
17
|
+
export interface CardCarouselPanelProps {
|
|
18
|
+
title: string
|
|
19
|
+
items: Record<string, unknown>[]
|
|
20
|
+
expandColumns: ExpandColumnDef[]
|
|
21
|
+
columns: Record<string, ColumnDef>
|
|
22
|
+
layout: CardLayout
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildVirtualRow(
|
|
27
|
+
item: Record<string, unknown>,
|
|
28
|
+
expandColumns: ExpandColumnDef[],
|
|
29
|
+
columns: Record<string, ColumnDef>,
|
|
30
|
+
): Record<string, unknown> {
|
|
31
|
+
const virtualRow: Record<string, unknown> = {}
|
|
32
|
+
for (const ec of expandColumns) {
|
|
33
|
+
if (ec.mapTo) {
|
|
34
|
+
virtualRow[ec.mapTo] = item[ec.key]
|
|
35
|
+
if (ec.secondaryKey) {
|
|
36
|
+
const mainCol = columns[ec.mapTo]
|
|
37
|
+
if (mainCol?.secondary) {
|
|
38
|
+
virtualRow[mainCol.secondary] = item[ec.secondaryKey]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return virtualRow
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CardCarouselPanel({
|
|
47
|
+
title,
|
|
48
|
+
items,
|
|
49
|
+
expandColumns,
|
|
50
|
+
columns,
|
|
51
|
+
layout,
|
|
52
|
+
className,
|
|
53
|
+
}: CardCarouselPanelProps) {
|
|
54
|
+
const scoreEc = expandColumns.find((ec) => ec.type === 'score-bar')
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
data-slot="card-carousel-panel"
|
|
59
|
+
className={cn('border border-border rounded-lg bg-card', className)}
|
|
60
|
+
>
|
|
61
|
+
<Carousel opts={{ align: 'start' }}>
|
|
62
|
+
<div className="flex items-center justify-between p-3 border-b border-border">
|
|
63
|
+
<div className="flex items-center gap-2">
|
|
64
|
+
<span className="text-sm font-semibold">{title}</span>
|
|
65
|
+
<Badge variant="secondary" className="text-xs">
|
|
66
|
+
{items.length}
|
|
67
|
+
</Badge>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex items-center gap-1">
|
|
70
|
+
<CarouselPrevious className="static translate-y-0 size-7 rounded-md" />
|
|
71
|
+
<CarouselNext className="static translate-y-0 size-7 rounded-md" />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<CarouselContent className="p-3 -ml-3">
|
|
76
|
+
{items.map((item, index) => {
|
|
77
|
+
const virtualRow = buildVirtualRow(item, expandColumns, columns)
|
|
78
|
+
const titleValue = virtualRow[layout.titleField]
|
|
79
|
+
const subtitleValue = layout.subtitleField
|
|
80
|
+
? virtualRow[layout.subtitleField]
|
|
81
|
+
: undefined
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<CarouselItem
|
|
85
|
+
key={index}
|
|
86
|
+
className="basis-1/3 pl-3 max-lg:basis-1/2 max-sm:basis-[85%]"
|
|
87
|
+
>
|
|
88
|
+
<div className="border border-border rounded-lg p-4 flex flex-col gap-3 h-full">
|
|
89
|
+
{/* Header — single-line truncate for carousel mini-card per spec */}
|
|
90
|
+
<div>
|
|
91
|
+
<div className="text-[13px] font-semibold truncate">
|
|
92
|
+
{titleValue != null ? String(titleValue) : ''}
|
|
93
|
+
</div>
|
|
94
|
+
{subtitleValue != null && (
|
|
95
|
+
<div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1">
|
|
96
|
+
{String(subtitleValue)}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Badges */}
|
|
102
|
+
{layout.badgeFields.length > 0 && (
|
|
103
|
+
<div className="flex flex-wrap gap-1">
|
|
104
|
+
{layout.badgeFields.map((bf) => {
|
|
105
|
+
const col = columns[bf]
|
|
106
|
+
if (!col || virtualRow[bf] == null) return null
|
|
107
|
+
return (
|
|
108
|
+
<React.Fragment key={bf}>
|
|
109
|
+
{renderCell(bf, col, virtualRow)}
|
|
110
|
+
</React.Fragment>
|
|
111
|
+
)
|
|
112
|
+
})}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Rows */}
|
|
117
|
+
{layout.rows.length > 0 && (
|
|
118
|
+
<div className="flex flex-col gap-1.5 flex-1">
|
|
119
|
+
{layout.rows.map((r) => {
|
|
120
|
+
if (virtualRow[r.field] == null) return null
|
|
121
|
+
const col = columns[r.field]
|
|
122
|
+
const valueNode = col
|
|
123
|
+
? renderCell(
|
|
124
|
+
r.field,
|
|
125
|
+
col,
|
|
126
|
+
virtualRow,
|
|
127
|
+
r.rendererOverride === 'inventory-label'
|
|
128
|
+
? { mode: 'inventory-label' }
|
|
129
|
+
: {},
|
|
130
|
+
)
|
|
131
|
+
: String(virtualRow[r.field] ?? '')
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
key={r.field}
|
|
135
|
+
className="flex justify-between items-center text-[13px]"
|
|
136
|
+
>
|
|
137
|
+
<span className="text-muted-foreground">{r.label}</span>
|
|
138
|
+
<span className="font-medium">{valueNode}</span>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
})}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Score bar */}
|
|
146
|
+
{scoreEc && (
|
|
147
|
+
<ScoreBar
|
|
148
|
+
value={
|
|
149
|
+
typeof item[scoreEc.key] === 'number'
|
|
150
|
+
? (item[scoreEc.key] as number)
|
|
151
|
+
: 0
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Footer */}
|
|
157
|
+
<Button variant="outline" size="sm" className="w-full gap-1.5">
|
|
158
|
+
<Plus className="size-3.5" aria-hidden="true" />
|
|
159
|
+
Hinzufügen
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</CarouselItem>
|
|
163
|
+
)
|
|
164
|
+
})}
|
|
165
|
+
</CarouselContent>
|
|
166
|
+
</Carousel>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { CardCarouselPanel }
|