@fastnd/components 1.0.32 → 1.0.34
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/examples/dashboard/DashboardPage/DashboardPage.tsx +1 -1
- package/dist/examples/dashboard/ProjectList/ProjectList.tsx +92 -52
- package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +4 -4
- package/dist/examples/dashboard/StatusFilterLegend/StatusFilterLegend.tsx +1 -1
- package/dist/examples/dashboard/StatusOverview/StatusOverview.tsx +1 -1
- package/dist/examples/dashboard/constants.ts +20 -6
- package/dist/examples/dashboard/types.ts +2 -0
- package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +46 -16
- package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +1 -1
- package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +30 -22
- package/dist/examples/data-visualization/DataListView/DataListView.tsx +101 -48
- package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +70 -17
- package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +12 -1
- package/dist/examples/data-visualization/constants.ts +8 -7
- package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +6 -1
- package/dist/examples/data-visualization/types.ts +19 -0
- package/dist/fastnd-components.js +4 -4
- package/package.json +1 -1
|
@@ -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,22 +1,25 @@
|
|
|
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
|
|
|
12
12
|
const STATUS_BADGE_CLASSES: Record<ProjectStatus, string> = {
|
|
13
|
-
'neu': '
|
|
14
|
-
'offen': '
|
|
15
|
-
'in-prufung': '
|
|
16
|
-
'validierung': '
|
|
17
|
-
'abgeschlossen': '
|
|
13
|
+
'neu': 'border-primary',
|
|
14
|
+
'offen': 'border-muted-foreground',
|
|
15
|
+
'in-prufung': 'border-[#ebbe0d]',
|
|
16
|
+
'validierung': 'border-[#e8a026]',
|
|
17
|
+
'abgeschlossen': 'border-[#1ec489]',
|
|
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="outline" 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>
|
|
@@ -58,7 +58,7 @@ const StatusDonutChart = React.forwardRef<HTMLDivElement, StatusDonutChartProps>
|
|
|
58
58
|
{...props}
|
|
59
59
|
>
|
|
60
60
|
{/* Relative wrapper so the center label overlay can be positioned absolutely */}
|
|
61
|
-
<div className="relative mx-auto aspect-square max-h-[
|
|
61
|
+
<div className="relative mx-auto aspect-square max-h-[160px]">
|
|
62
62
|
<ChartContainer config={chartConfig} className="size-full">
|
|
63
63
|
<PieChart>
|
|
64
64
|
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
@@ -66,8 +66,8 @@ const StatusDonutChart = React.forwardRef<HTMLDivElement, StatusDonutChartProps>
|
|
|
66
66
|
data={chartData}
|
|
67
67
|
dataKey="count"
|
|
68
68
|
nameKey="status"
|
|
69
|
-
innerRadius={
|
|
70
|
-
outerRadius={
|
|
69
|
+
innerRadius={48}
|
|
70
|
+
outerRadius={68}
|
|
71
71
|
strokeWidth={5}
|
|
72
72
|
className="cursor-pointer"
|
|
73
73
|
onClick={handlePieClick}
|
|
@@ -88,7 +88,7 @@ const StatusDonutChart = React.forwardRef<HTMLDivElement, StatusDonutChartProps>
|
|
|
88
88
|
{/* Center label — rendered as HTML so it is always in the DOM (recharts SVG labels are invisible in jsdom) */}
|
|
89
89
|
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-0.5">
|
|
90
90
|
<span
|
|
91
|
-
className="text-
|
|
91
|
+
className="text-2xl font-bold text-foreground"
|
|
92
92
|
style={{ fontFamily: 'var(--font-clash)' }}
|
|
93
93
|
>
|
|
94
94
|
{total.toLocaleString()}
|
|
@@ -43,7 +43,7 @@ const StatusFilterLegend = React.forwardRef<HTMLDivElement, StatusFilterLegendPr
|
|
|
43
43
|
data-filter={filter}
|
|
44
44
|
onClick={() => onFilterChange(filter)}
|
|
45
45
|
className={cn(
|
|
46
|
-
'flex items-center justify-between w-full px-3 py-
|
|
46
|
+
'flex items-center justify-between w-full px-3 py-1.5 rounded-md border border-transparent transition-colors cursor-pointer text-sm outline-none focus-visible:bg-muted',
|
|
47
47
|
isActive ? 'bg-muted border-border' : 'hover:bg-muted',
|
|
48
48
|
)}
|
|
49
49
|
>
|
|
@@ -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 & Tracking Platform', client: 'Bayerische LogiTrans Handelsgesellschaft mbH', application: 'Next.js 14 / tRPC / PostgreSQL', status: 'in-prufung', isFavorite: true, createdAt: '2024-12-03', updatedAt: d(2) },
|
|
25
|
+
{ id: '8', name: 'HR Self-Service & Workforce Management Portal', client: 'Personalwerk Süddeutschland AG', application: 'Angular 17 / .NET Core 8 / Azure AD', status: 'offen', isFavorite: false, createdAt: '2025-01-08', updatedAt: d(5) },
|
|
26
|
+
{ id: '9', name: 'Echtzeit IoT Monitoring & Alerting Dashboard', client: 'SmartFactory Automatisierungstechnik SE', application: 'Vue.js 3 / InfluxDB / Grafana', status: 'validierung', isFavorite: false, createdAt: '2024-10-14', updatedAt: d(9) },
|
|
27
|
+
{ id: '10', name: 'Digitales Dokumenten- & Archivierungssystem', client: 'Kanzlei Bauer, Hoffmann & Partner GbR', application: 'SharePoint Online / Power Automate', status: 'abgeschlossen', isFavorite: false, createdAt: '2024-06-01', updatedAt: d(30) },
|
|
28
|
+
{ id: '11', name: 'Multi-Channel Customer Loyalty & Rewards App', client: 'Retail Chain Deutschland GmbH & Co. KG', application: 'React Native / Expo / Firebase', status: 'neu', isFavorite: false, createdAt: '2025-03-20', updatedAt: d(0) },
|
|
29
|
+
{ id: '12', name: 'KI-gestützte Predictive Maintenance Engine', client: 'IndustrieWerk Maschinenbau KG', application: 'Python 3.12 / FastAPI / PyTorch / MLflow',status: 'in-prufung', isFavorite: true, createdAt: '2024-09-17', updatedAt: d(4) },
|
|
30
|
+
{ id: '13', name: 'Automatisierte Compliance & Audit Reporting Suite', client: 'RegulaCorp Financial Services AG', application: 'Power BI Embedded / Azure Synapse', status: 'offen', isFavorite: false, createdAt: '2025-02-25', updatedAt: d(11) },
|
|
31
|
+
{ id: '14', name: 'Mobile Field Service & Wartungsmanagement', client: 'TechService Außendienst GmbH', application: 'Salesforce Field Service / MuleSoft', status: 'validierung', isFavorite: true, createdAt: '2024-07-30', updatedAt: d(6) },
|
|
18
32
|
]
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { ChevronDown, ArrowLeftRight, Sparkles } from 'lucide-react'
|
|
2
|
+
import { ChevronDown, ArrowLeftRight, Sparkles, FolderPlus, BookmarkPlus, Trash2 } from 'lucide-react'
|
|
3
3
|
import { Badge } from '@/components/ui/badge'
|
|
4
4
|
import { Button } from '@/components/ui/button'
|
|
5
5
|
import { ScoreBar } from '@/components/ScoreBar/ScoreBar'
|
|
6
6
|
import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
|
|
7
7
|
import { cn } from '@/lib/utils'
|
|
8
|
-
import type { ColumnDef } from '../types'
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
mode?: 'default' | 'compact' | 'inventory-label'
|
|
12
|
-
isExpanded?: boolean
|
|
13
|
-
isFavorite?: boolean
|
|
14
|
-
onToggleExpand?: () => void
|
|
15
|
-
onToggleFavorite?: () => void
|
|
16
|
-
}
|
|
8
|
+
import type { ColumnDef, RenderCellOptions } from '../types'
|
|
9
|
+
|
|
10
|
+
export type { RenderCellOptions }
|
|
17
11
|
|
|
18
12
|
const STATUS_COLORS: Record<string, string> = {
|
|
19
|
-
active:
|
|
20
|
-
nrnd:
|
|
21
|
-
eol:
|
|
22
|
-
production: '
|
|
13
|
+
active: 'border-[var(--lifecycle-active)]',
|
|
14
|
+
nrnd: 'border-[var(--lifecycle-nrnd)]',
|
|
15
|
+
eol: 'border-[var(--lifecycle-eol)]',
|
|
16
|
+
production: 'border-[var(--lifecycle-production)]',
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
const INVENTORY_COLORS: Record<string, string> = {
|
|
@@ -39,9 +33,11 @@ export function renderCell(
|
|
|
39
33
|
row: Record<string, unknown>,
|
|
40
34
|
options: RenderCellOptions = {},
|
|
41
35
|
): React.ReactNode {
|
|
42
|
-
const { mode = 'default', isExpanded = false, isFavorite = false, onToggleExpand, onToggleFavorite } = options
|
|
36
|
+
const { mode = 'default', isExpanded = false, isFavorite = false, onToggleExpand, onToggleFavorite, onAddToProject, onAddToCollection, onDelete } = options
|
|
43
37
|
const val = row[colKey]
|
|
44
38
|
|
|
39
|
+
if (col.render) return col.render(val, row, options)
|
|
40
|
+
|
|
45
41
|
switch (col.type) {
|
|
46
42
|
case 'text': {
|
|
47
43
|
// col.rowLines drives the clamp; default 2 so columns can shrink without growing rows
|
|
@@ -83,7 +79,7 @@ export function renderCell(
|
|
|
83
79
|
if (val == null || !col.statusMap) return <span className="text-[13px]">{val != null ? String(val) : ''}</span>
|
|
84
80
|
const status = col.statusMap[String(val)] ?? 'active'
|
|
85
81
|
return (
|
|
86
|
-
<Badge className={STATUS_COLORS[status]}>
|
|
82
|
+
<Badge variant="outline" className={STATUS_COLORS[status]}>
|
|
87
83
|
{String(val)}
|
|
88
84
|
</Badge>
|
|
89
85
|
)
|
|
@@ -168,6 +164,40 @@ export function renderCell(
|
|
|
168
164
|
)
|
|
169
165
|
}
|
|
170
166
|
|
|
167
|
+
case 'actions': {
|
|
168
|
+
return (
|
|
169
|
+
<div className="flex items-center justify-end gap-0.5">
|
|
170
|
+
<Button
|
|
171
|
+
variant="ghost"
|
|
172
|
+
size="icon"
|
|
173
|
+
className="size-7 text-muted-foreground hover:text-foreground"
|
|
174
|
+
aria-label="Zu Projekt hinzufügen"
|
|
175
|
+
onClick={(e) => { e.stopPropagation(); onAddToProject?.() }}
|
|
176
|
+
>
|
|
177
|
+
<FolderPlus className="size-3.5" aria-hidden="true" />
|
|
178
|
+
</Button>
|
|
179
|
+
<Button
|
|
180
|
+
variant="ghost"
|
|
181
|
+
size="icon"
|
|
182
|
+
className="size-7 text-muted-foreground hover:text-foreground"
|
|
183
|
+
aria-label="Zu Sammlung hinzufügen"
|
|
184
|
+
onClick={(e) => { e.stopPropagation(); onAddToCollection?.() }}
|
|
185
|
+
>
|
|
186
|
+
<BookmarkPlus className="size-3.5" aria-hidden="true" />
|
|
187
|
+
</Button>
|
|
188
|
+
<Button
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="icon"
|
|
191
|
+
className="size-7 text-muted-foreground hover:text-destructive"
|
|
192
|
+
aria-label="Produkt löschen"
|
|
193
|
+
onClick={(e) => { e.stopPropagation(); onDelete?.() }}
|
|
194
|
+
>
|
|
195
|
+
<Trash2 className="size-3.5" aria-hidden="true" />
|
|
196
|
+
</Button>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
171
201
|
default: {
|
|
172
202
|
return <span className="text-[13px]">{val != null ? String(val) : ''}</span>
|
|
173
203
|
}
|
|
@@ -106,7 +106,7 @@ export function ColumnConfigPopover({
|
|
|
106
106
|
>
|
|
107
107
|
<SortableContext items={columnOrder} strategy={verticalListSortingStrategy}>
|
|
108
108
|
<div className="p-1">
|
|
109
|
-
{columnOrder.map((key) => (
|
|
109
|
+
{columnOrder.filter((key) => columns[key]?.configurable !== false).map((key) => (
|
|
110
110
|
<SortableColumnItem
|
|
111
111
|
key={key}
|
|
112
112
|
colKey={key}
|
|
@@ -86,6 +86,7 @@ function DataCardView({
|
|
|
86
86
|
}: DataCardViewProps) {
|
|
87
87
|
const gridRef = useRef<HTMLDivElement>(null)
|
|
88
88
|
const expandFields = getExpandFields(layout)
|
|
89
|
+
const actionsColKey = Object.keys(columns).find((k) => columns[k]?.type === 'actions')
|
|
89
90
|
|
|
90
91
|
useLayoutEffect(() => {
|
|
91
92
|
if (gridRef.current) {
|
|
@@ -97,7 +98,7 @@ function DataCardView({
|
|
|
97
98
|
<div
|
|
98
99
|
ref={gridRef}
|
|
99
100
|
data-slot="data-card-view"
|
|
100
|
-
className={cn('grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-
|
|
101
|
+
className={cn('grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4', className)}
|
|
101
102
|
>
|
|
102
103
|
{data.map((row) => {
|
|
103
104
|
const rowId = String(row.id ?? '')
|
|
@@ -111,7 +112,7 @@ function DataCardView({
|
|
|
111
112
|
<div className="flex flex-col">
|
|
112
113
|
<div
|
|
113
114
|
className={cn(
|
|
114
|
-
'flex flex-col border border-border rounded-lg overflow-hidden hover:shadow-md transition-shadow bg-card text-card-foreground',
|
|
115
|
+
'flex flex-col flex-1 border border-border rounded-lg overflow-hidden hover:shadow-md transition-shadow bg-card text-card-foreground',
|
|
115
116
|
isAnyExpanded && 'border-b-2 border-b-primary',
|
|
116
117
|
)}
|
|
117
118
|
>
|
|
@@ -136,7 +137,7 @@ function DataCardView({
|
|
|
136
137
|
|
|
137
138
|
{/* Badges */}
|
|
138
139
|
{layout.badgeFields.length > 0 && (
|
|
139
|
-
<div className="px-4 pt-2 flex gap-
|
|
140
|
+
<div className="px-4 pt-2 flex gap-1.5 flex-wrap">
|
|
140
141
|
{layout.badgeFields.map((bf) => {
|
|
141
142
|
const col = columns[bf]
|
|
142
143
|
if (!col || row[bf] == null) return null
|
|
@@ -163,31 +164,38 @@ function DataCardView({
|
|
|
163
164
|
)
|
|
164
165
|
: String(row[r.field] ?? '')
|
|
165
166
|
return (
|
|
166
|
-
<div key={r.field} className="flex justify-between items-
|
|
167
|
-
<span className="text-xs text-muted-foreground">{r.label}</span>
|
|
168
|
-
<span className="text-[13px] font-medium">{valueNode}</span>
|
|
167
|
+
<div key={r.field} className="flex justify-between items-start gap-4">
|
|
168
|
+
<span className="text-xs text-muted-foreground shrink-0">{r.label}</span>
|
|
169
|
+
<span className="text-[13px] font-medium min-w-0 text-right">{valueNode}</span>
|
|
169
170
|
</div>
|
|
170
171
|
)
|
|
171
172
|
})}
|
|
172
173
|
</div>
|
|
173
174
|
)}
|
|
174
175
|
|
|
175
|
-
{/* Footer — expand buttons */}
|
|
176
|
-
{expandFields.length > 0 && (
|
|
177
|
-
<div className="p-4 pt-3">
|
|
178
|
-
<div className="flex gap-2">
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
176
|
+
{/* Footer — expand buttons + actions */}
|
|
177
|
+
{(expandFields.length > 0 || actionsColKey) && (
|
|
178
|
+
<div className="mt-auto p-4 pt-3 border-t border-border">
|
|
179
|
+
<div className="flex items-center justify-between gap-2">
|
|
180
|
+
<div className="flex gap-2">
|
|
181
|
+
{expandFields.map((ef) => {
|
|
182
|
+
const col = columns[ef]
|
|
183
|
+
if (!col) return null
|
|
184
|
+
const expansionKey = `${rowId}::${ef}`
|
|
185
|
+
return (
|
|
186
|
+
<React.Fragment key={ef}>
|
|
187
|
+
{renderCell(ef, col, row, {
|
|
188
|
+
isExpanded: expandedRows.has(expansionKey),
|
|
189
|
+
onToggleExpand: () => onToggleExpansion(rowId, ef),
|
|
190
|
+
})}
|
|
191
|
+
</React.Fragment>
|
|
192
|
+
)
|
|
193
|
+
})}
|
|
194
|
+
</div>
|
|
195
|
+
{actionsColKey && renderCell(actionsColKey, columns[actionsColKey], row, {
|
|
196
|
+
onAddToProject: () => {},
|
|
197
|
+
onAddToCollection: () => {},
|
|
198
|
+
onDelete: () => {},
|
|
191
199
|
})}
|
|
192
200
|
</div>
|
|
193
201
|
</div>
|