@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,88 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { LayoutGrid, List, RotateCcw, Search, Table2 } from 'lucide-react'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Input } from '@/components/ui/input'
|
|
5
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
import type { ViewMode } from '../types'
|
|
8
|
+
|
|
9
|
+
interface DataExplorerToolbarProps {
|
|
10
|
+
viewMode: ViewMode
|
|
11
|
+
onViewModeChange: (mode: ViewMode) => void
|
|
12
|
+
searchTerm: string
|
|
13
|
+
onSearchChange: (term: string) => void
|
|
14
|
+
hasActiveFilters: boolean
|
|
15
|
+
onResetAll: () => void
|
|
16
|
+
filterSlot: React.ReactNode
|
|
17
|
+
configSlot: React.ReactNode
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DataExplorerToolbar({
|
|
22
|
+
viewMode,
|
|
23
|
+
onViewModeChange,
|
|
24
|
+
searchTerm,
|
|
25
|
+
onSearchChange,
|
|
26
|
+
hasActiveFilters,
|
|
27
|
+
onResetAll,
|
|
28
|
+
filterSlot,
|
|
29
|
+
configSlot,
|
|
30
|
+
className,
|
|
31
|
+
}: DataExplorerToolbarProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
role="toolbar"
|
|
35
|
+
aria-label="Daten-Toolbar"
|
|
36
|
+
className={cn('flex flex-col gap-3 px-6 py-4 border-b border-border', className)}
|
|
37
|
+
>
|
|
38
|
+
{/* Row 1: view switcher + config */}
|
|
39
|
+
<div className="flex items-center justify-between">
|
|
40
|
+
<Tabs value={viewMode} onValueChange={(v) => onViewModeChange(v as ViewMode)}>
|
|
41
|
+
<TabsList>
|
|
42
|
+
<TabsTrigger value="table">
|
|
43
|
+
<Table2 className="size-4" />
|
|
44
|
+
<span className="max-sm:hidden">Tabelle</span>
|
|
45
|
+
</TabsTrigger>
|
|
46
|
+
<TabsTrigger value="list">
|
|
47
|
+
<List className="size-4" />
|
|
48
|
+
<span className="max-sm:hidden">Liste</span>
|
|
49
|
+
</TabsTrigger>
|
|
50
|
+
<TabsTrigger value="card">
|
|
51
|
+
<LayoutGrid className="size-4" />
|
|
52
|
+
<span className="max-sm:hidden">Karten</span>
|
|
53
|
+
</TabsTrigger>
|
|
54
|
+
</TabsList>
|
|
55
|
+
</Tabs>
|
|
56
|
+
{configSlot}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Row 2: filter chips + search */}
|
|
60
|
+
<div className="flex items-center justify-between gap-3">
|
|
61
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
62
|
+
{filterSlot}
|
|
63
|
+
{hasActiveFilters && (
|
|
64
|
+
<Button
|
|
65
|
+
variant="ghost"
|
|
66
|
+
size="sm"
|
|
67
|
+
className="text-destructive"
|
|
68
|
+
onClick={onResetAll}
|
|
69
|
+
>
|
|
70
|
+
<RotateCcw className="size-4" />
|
|
71
|
+
Zurücksetzen
|
|
72
|
+
</Button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
<div className="relative">
|
|
76
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
|
77
|
+
<Input
|
|
78
|
+
placeholder="Suchen..."
|
|
79
|
+
value={searchTerm}
|
|
80
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
81
|
+
className="pl-9 min-w-[240px] max-sm:min-w-[160px]"
|
|
82
|
+
aria-label="Datensätze durchsuchen"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Plus } from 'lucide-react'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from '@/components/ui/table'
|
|
12
|
+
import { ScoreBar } from '@/components/ScoreBar/ScoreBar'
|
|
13
|
+
import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
|
|
14
|
+
import { renderCell } from '../CellRenderers/CellRenderers'
|
|
15
|
+
import { cn } from '@/lib/utils'
|
|
16
|
+
import type { ColumnDef, ExpandColumnDef, ListLayout } from '../types'
|
|
17
|
+
|
|
18
|
+
export interface DataListViewProps {
|
|
19
|
+
data: Record<string, unknown>[]
|
|
20
|
+
columns: Record<string, ColumnDef>
|
|
21
|
+
layout: ListLayout
|
|
22
|
+
expandedRows: Set<string>
|
|
23
|
+
onToggleExpansion: (rowId: string, field: string) => void
|
|
24
|
+
favorites: Set<string>
|
|
25
|
+
onToggleFavorite: (id: string) => void
|
|
26
|
+
showHeader?: boolean
|
|
27
|
+
className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ExpansionTableProps {
|
|
31
|
+
row: Record<string, unknown>
|
|
32
|
+
colKey: string
|
|
33
|
+
columns: Record<string, ColumnDef>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ExpansionTable({ row, colKey, columns }: ExpansionTableProps) {
|
|
37
|
+
const col = columns[colKey]
|
|
38
|
+
if (!col?.expandColumns) return null
|
|
39
|
+
|
|
40
|
+
const items = row[colKey]
|
|
41
|
+
if (!Array.isArray(items) || items.length === 0) return null
|
|
42
|
+
|
|
43
|
+
const title = col.expandTitleFn ? col.expandTitleFn(row) : colKey
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<div className="text-sm font-semibold mb-2">{title}</div>
|
|
48
|
+
<Table>
|
|
49
|
+
<TableHeader>
|
|
50
|
+
<TableRow>
|
|
51
|
+
{col.expandColumns.map((ec: ExpandColumnDef) => (
|
|
52
|
+
<TableHead key={ec.key} className="text-xs h-8 px-2">
|
|
53
|
+
{ec.label}
|
|
54
|
+
</TableHead>
|
|
55
|
+
))}
|
|
56
|
+
<TableHead className="w-10" />
|
|
57
|
+
</TableRow>
|
|
58
|
+
</TableHeader>
|
|
59
|
+
<TableBody>
|
|
60
|
+
{(items as Record<string, unknown>[]).map((item, index) => (
|
|
61
|
+
<TableRow key={index}>
|
|
62
|
+
{col.expandColumns!.map((ec: ExpandColumnDef) => {
|
|
63
|
+
const v = item[ec.key]
|
|
64
|
+
return (
|
|
65
|
+
<TableCell key={ec.key} className="px-2 py-1.5 text-xs">
|
|
66
|
+
<ExpansionCell ec={ec} value={v} />
|
|
67
|
+
</TableCell>
|
|
68
|
+
)
|
|
69
|
+
})}
|
|
70
|
+
<TableCell className="px-2 py-1.5">
|
|
71
|
+
<Button
|
|
72
|
+
variant="ghost"
|
|
73
|
+
size="icon-xs"
|
|
74
|
+
title="Hinzufügen"
|
|
75
|
+
aria-label="Hinzufügen"
|
|
76
|
+
onClick={(e) => e.preventDefault()}
|
|
77
|
+
>
|
|
78
|
+
<Plus className="size-3.5" aria-hidden="true" />
|
|
79
|
+
</Button>
|
|
80
|
+
</TableCell>
|
|
81
|
+
</TableRow>
|
|
82
|
+
))}
|
|
83
|
+
</TableBody>
|
|
84
|
+
</Table>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ExpansionCellProps {
|
|
90
|
+
ec: ExpandColumnDef
|
|
91
|
+
value: unknown
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ExpansionCell({ ec, value }: ExpansionCellProps) {
|
|
95
|
+
if (ec.type === 'score-bar') {
|
|
96
|
+
return <ScoreBar value={typeof value === 'number' ? value : 0} />
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const text = value != null ? String(value) : ''
|
|
100
|
+
|
|
101
|
+
if (ec.muted) {
|
|
102
|
+
const hasAI = text.includes('AI') || text.includes('Ki')
|
|
103
|
+
return (
|
|
104
|
+
<span className="text-muted-foreground text-xs line-clamp-3">
|
|
105
|
+
{text}
|
|
106
|
+
{hasAI && (
|
|
107
|
+
<span className="ml-1 text-[10px] font-medium bg-primary/10 text-primary rounded px-1">
|
|
108
|
+
KI
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</span>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ec.bold) {
|
|
116
|
+
return <span className="font-semibold text-[13px] line-clamp-3">{text}</span>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof value === 'boolean') {
|
|
120
|
+
return <span>{value ? 'Ja' : 'Nein'}</span>
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return <span>{text}</span>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ListHeaderProps {
|
|
127
|
+
columns: Record<string, ColumnDef>
|
|
128
|
+
layout: ListLayout
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ListHeader({ columns, layout }: ListHeaderProps) {
|
|
132
|
+
const titleCol = columns[layout.titleField]
|
|
133
|
+
const expandFields = layout.expandFields ?? (layout.expandField ? [layout.expandField] : [])
|
|
134
|
+
const secondaryLabel =
|
|
135
|
+
titleCol?.type === 'double-text' && titleCol.secondary
|
|
136
|
+
? columns[titleCol.secondary]?.label
|
|
137
|
+
: undefined
|
|
138
|
+
const titleLabel = secondaryLabel
|
|
139
|
+
? `${titleCol!.label} / ${secondaryLabel}`
|
|
140
|
+
: (titleCol?.label ?? '')
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex items-center gap-3 px-4 py-2 border-b border-border">
|
|
144
|
+
{/* Placeholder matching FavoriteButton size */}
|
|
145
|
+
<div className="size-[28px] flex-none" aria-hidden="true" />
|
|
146
|
+
<div className="flex-1 min-w-0 text-xs font-medium text-muted-foreground">
|
|
147
|
+
{titleLabel}
|
|
148
|
+
</div>
|
|
149
|
+
{layout.badgeFields.map((field) => (
|
|
150
|
+
<span key={field} className="shrink-0 text-xs font-medium text-muted-foreground">
|
|
151
|
+
{columns[field]?.label ?? ''}
|
|
152
|
+
</span>
|
|
153
|
+
))}
|
|
154
|
+
{expandFields.map((field) => (
|
|
155
|
+
<span key={field} className="shrink-0 text-xs font-medium text-muted-foreground">
|
|
156
|
+
{columns[field]?.expandLabel ?? columns[field]?.label ?? ''}
|
|
157
|
+
</span>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function DataListView({
|
|
164
|
+
data,
|
|
165
|
+
columns,
|
|
166
|
+
layout,
|
|
167
|
+
expandedRows,
|
|
168
|
+
onToggleExpansion,
|
|
169
|
+
favorites,
|
|
170
|
+
onToggleFavorite,
|
|
171
|
+
showHeader = false,
|
|
172
|
+
className,
|
|
173
|
+
}: DataListViewProps) {
|
|
174
|
+
const expandFields = layout.expandFields ?? (layout.expandField ? [layout.expandField] : [])
|
|
175
|
+
const titleCol = columns[layout.titleField]
|
|
176
|
+
const isDoubleText = titleCol?.type === 'double-text'
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div
|
|
180
|
+
data-slot="data-list-view"
|
|
181
|
+
className={cn('divide-y divide-border', className)}
|
|
182
|
+
>
|
|
183
|
+
{showHeader && <ListHeader columns={columns} layout={layout} />}
|
|
184
|
+
{data.map((row) => {
|
|
185
|
+
const rowId = String(row.id ?? '')
|
|
186
|
+
const titleValue = row[layout.titleField]
|
|
187
|
+
const subtitleValue = isDoubleText && titleCol.secondary ? row[titleCol.secondary] : undefined
|
|
188
|
+
const hasBadges = layout.badgeFields.length > 0
|
|
189
|
+
const hasExpand = expandFields.length > 0
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div key={rowId}>
|
|
193
|
+
{/* Single slim row: Fav | Title block | Badges | Expand */}
|
|
194
|
+
<div className="flex items-center gap-3 px-4 py-2 hover:bg-accent transition-colors">
|
|
195
|
+
<div className="flex-none">
|
|
196
|
+
<FavoriteButton
|
|
197
|
+
pressed={favorites.has(rowId)}
|
|
198
|
+
itemName={titleValue != null ? String(titleValue) : ''}
|
|
199
|
+
onPressedChange={() => onToggleFavorite(rowId)}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Title block grows to fill space — no fixed width */}
|
|
204
|
+
<div className="flex-1 min-w-0">
|
|
205
|
+
<div className="font-semibold text-sm truncate">
|
|
206
|
+
{titleValue != null ? String(titleValue) : ''}
|
|
207
|
+
</div>
|
|
208
|
+
{/* Secondary line: subtitle + meta chips inline */}
|
|
209
|
+
{(subtitleValue != null || layout.metaFields.length > 0) && (
|
|
210
|
+
<div className="flex items-center gap-1.5 mt-0.5 min-w-0">
|
|
211
|
+
{subtitleValue != null && (
|
|
212
|
+
<span className="text-xs text-muted-foreground truncate shrink">
|
|
213
|
+
{String(subtitleValue)}
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
{layout.metaFields.map((field) => {
|
|
217
|
+
const v = row[field]
|
|
218
|
+
if (v == null || v === '') return null
|
|
219
|
+
return (
|
|
220
|
+
<span
|
|
221
|
+
key={field}
|
|
222
|
+
className="shrink-0 bg-secondary rounded-md px-1.5 py-px text-xs truncate max-w-[140px]"
|
|
223
|
+
>
|
|
224
|
+
{String(v)}
|
|
225
|
+
</span>
|
|
226
|
+
)
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Badges — shrink-0, sizes to content, no fixed column */}
|
|
233
|
+
{layout.badgeFields.map((field) => {
|
|
234
|
+
const col = columns[field]
|
|
235
|
+
if (!col) return null
|
|
236
|
+
return (
|
|
237
|
+
<React.Fragment key={field}>
|
|
238
|
+
{renderCell(field, col, row, {
|
|
239
|
+
mode: col.type === 'inventory' ? 'inventory-label' : 'default',
|
|
240
|
+
})}
|
|
241
|
+
</React.Fragment>
|
|
242
|
+
)
|
|
243
|
+
})}
|
|
244
|
+
|
|
245
|
+
{/* Expand buttons — trailing edge */}
|
|
246
|
+
{expandFields.map((field) => {
|
|
247
|
+
const col = columns[field]
|
|
248
|
+
if (!col) return null
|
|
249
|
+
const expandKey = `${rowId}::${field}`
|
|
250
|
+
return (
|
|
251
|
+
<React.Fragment key={field}>
|
|
252
|
+
{renderCell(field, col, row, {
|
|
253
|
+
mode: 'compact',
|
|
254
|
+
isExpanded: expandedRows.has(expandKey),
|
|
255
|
+
onToggleExpand: () => onToggleExpansion(rowId, field),
|
|
256
|
+
})}
|
|
257
|
+
</React.Fragment>
|
|
258
|
+
)
|
|
259
|
+
})}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Expansion panels — full width, below the row */}
|
|
263
|
+
{expandFields.map((field) => {
|
|
264
|
+
const expandKey = `${rowId}::${field}`
|
|
265
|
+
if (!expandedRows.has(expandKey)) return null
|
|
266
|
+
return (
|
|
267
|
+
<div
|
|
268
|
+
key={field}
|
|
269
|
+
className="px-4 pb-3 pt-3 border-t border-border"
|
|
270
|
+
data-slot="expansion-content"
|
|
271
|
+
>
|
|
272
|
+
<ExpansionTable row={row} colKey={field} columns={columns} />
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
})}
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
})}
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export { DataListView }
|