@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.
Files changed (29) 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/DashboardPage/DashboardPage.tsx +1 -1
  5. package/dist/examples/dashboard/ProjectList/ProjectList.tsx +87 -47
  6. package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +41 -56
  7. package/dist/examples/dashboard/constants.ts +20 -6
  8. package/dist/examples/dashboard/types.ts +2 -0
  9. package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
  10. package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +171 -0
  11. package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +124 -0
  12. package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +223 -0
  13. package/dist/examples/data-visualization/DataExplorerPagination/DataExplorerPagination.tsx +143 -0
  14. package/dist/examples/data-visualization/DataExplorerToolbar/DataExplorerToolbar.tsx +88 -0
  15. package/dist/examples/data-visualization/DataListView/DataListView.tsx +283 -0
  16. package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +455 -0
  17. package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +151 -0
  18. package/dist/examples/data-visualization/FilterChipGroup/FilterChipGroup.tsx +125 -0
  19. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +132 -0
  20. package/dist/examples/data-visualization/constants.ts +587 -0
  21. package/dist/examples/data-visualization/hooks/use-column-config.ts +76 -0
  22. package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +318 -0
  23. package/dist/examples/data-visualization/index.ts +1 -0
  24. package/dist/examples/data-visualization/types.ts +110 -0
  25. package/dist/examples/quickaccess/QuickAccess/QuickAccess.tsx +97 -0
  26. package/dist/examples/quickaccess/index.ts +2 -0
  27. package/dist/examples/quickaccess/types.ts +11 -0
  28. package/dist/fastnd-components.js +5708 -5590
  29. 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 };
@@ -1,4 +1,6 @@
1
1
  export * from './FavoriteButton/FavoriteButton';
2
+ export * from './QuickAccessCard/QuickAccessCard';
3
+ export * from './ScoreBar/ScoreBar';
2
4
  export * from './ui/accordion';
3
5
  export * from './ui/alert';
4
6
  export * from './ui/alert-dialog';
@@ -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-[350px_1fr] gap-6 max-w-[1400px] mx-auto p-8',
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 { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
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
- <Table>
68
- <TableHeader>
69
- <TableRow>
70
- <TableHead scope="col">Projektname</TableHead>
71
- <TableHead scope="col">Kunde</TableHead>
72
- <TableHead scope="col">Applikation</TableHead>
73
- <TableHead scope="col">Status</TableHead>
74
- <TableHead scope="col" aria-label="Favorit">
75
- <Star className="size-3.5" />
76
- </TableHead>
77
- </TableRow>
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, Label, Sector } from 'recharts'
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
- <ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[200px]">
61
- <PieChart>
62
- <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
63
- <Pie
64
- data={chartData}
65
- dataKey="count"
66
- nameKey="status"
67
- innerRadius={60}
68
- outerRadius={85}
69
- strokeWidth={5}
70
- className="cursor-pointer"
71
- onClick={handlePieClick}
72
- shape={({
73
- index,
74
- outerRadius: or = 0,
75
- ...sectorProps
76
- }: PieSectorShapeProps) => (
77
- <Sector
78
- {...sectorProps}
79
- outerRadius={index === activeIndex ? or + 10 : or}
80
- />
81
- )}
82
- >
83
- <Label
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
- </Pie>
114
- </PieChart>
115
- </ChartContainer>
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', client: 'Müller GmbH', application: 'Shopify CMS', status: 'neu', isFavorite: false },
13
- { id: '2', name: 'CRM Integration MVP', client: 'TechCorp Inc.', application: 'Salesforce API', status: 'offen', isFavorite: true },
14
- { id: '3', name: 'Data Analytics Dashboard', client: 'DataViz AG', application: 'React / Supabase', status: 'in-prufung', isFavorite: false },
15
- { id: '4', name: 'Mobile App Onboarding', client: 'FitHealth', application: 'Flutter', status: 'neu', isFavorite: false },
16
- { id: '5', name: 'Payment Gateway Update', client: 'FinTech Solutions', application: 'Stripe API', status: 'validierung', isFavorite: false },
17
- { id: '6', name: 'Legacy System Migration', client: 'OldBank Corp.', application: 'Node.js / PostgreSQL', status: 'abgeschlossen', isFavorite: true },
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
  ]
@@ -9,6 +9,8 @@ export interface Project {
9
9
  application: string
10
10
  status: ProjectStatus
11
11
  isFavorite: boolean
12
+ createdAt: string
13
+ updatedAt: string
12
14
  }
13
15
 
14
16
  export interface StatusCount {
@@ -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 }