@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,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 }