@fastnd/components 1.0.31 → 1.0.32

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 (25) 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/StatusDonutChart/StatusDonutChart.tsx +41 -56
  5. package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
  6. package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +175 -0
  7. package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +124 -0
  8. package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +223 -0
  9. package/dist/examples/data-visualization/DataExplorerPagination/DataExplorerPagination.tsx +143 -0
  10. package/dist/examples/data-visualization/DataExplorerToolbar/DataExplorerToolbar.tsx +88 -0
  11. package/dist/examples/data-visualization/DataListView/DataListView.tsx +246 -0
  12. package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +449 -0
  13. package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +140 -0
  14. package/dist/examples/data-visualization/FilterChipGroup/FilterChipGroup.tsx +125 -0
  15. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +132 -0
  16. package/dist/examples/data-visualization/constants.ts +587 -0
  17. package/dist/examples/data-visualization/hooks/use-column-config.ts +76 -0
  18. package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +313 -0
  19. package/dist/examples/data-visualization/index.ts +1 -0
  20. package/dist/examples/data-visualization/types.ts +99 -0
  21. package/dist/examples/quickaccess/QuickAccess/QuickAccess.tsx +97 -0
  22. package/dist/examples/quickaccess/index.ts +2 -0
  23. package/dist/examples/quickaccess/types.ts +11 -0
  24. package/dist/fastnd-components.js +5708 -5590
  25. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react'
2
+ import { DATA_SOURCES } from '../constants'
3
+ import type { DomainKey } from '../types'
4
+
5
+ interface UseColumnConfigOptions {
6
+ activeDomain: DomainKey
7
+ }
8
+
9
+ interface ColumnConfigState {
10
+ columnOrder: string[]
11
+ columnVisibility: Record<string, boolean>
12
+ visibleColumns: string[]
13
+ reorderColumn: (activeId: string, overId: string) => void
14
+ toggleColumnVisibility: (key: string) => void
15
+ resetColumns: () => void
16
+ }
17
+
18
+ function buildInitialState(domain: DomainKey) {
19
+ const columns = DATA_SOURCES[domain].columns
20
+ const columnOrder = Object.keys(columns)
21
+ const columnVisibility: Record<string, boolean> = {}
22
+ for (const [k, col] of Object.entries(columns)) {
23
+ columnVisibility[k] = col.visible !== false
24
+ }
25
+ return { columnOrder, columnVisibility }
26
+ }
27
+
28
+ export function useColumnConfig({ activeDomain }: UseColumnConfigOptions): ColumnConfigState {
29
+ const [columnOrder, setColumnOrder] = useState<string[]>(
30
+ () => buildInitialState(activeDomain).columnOrder
31
+ )
32
+ const [columnVisibility, setColumnVisibility] = useState<Record<string, boolean>>(
33
+ () => buildInitialState(activeDomain).columnVisibility
34
+ )
35
+
36
+ useEffect(() => {
37
+ const { columnOrder: order, columnVisibility: visibility } = buildInitialState(activeDomain)
38
+ setColumnOrder(order)
39
+ setColumnVisibility(visibility)
40
+ }, [activeDomain])
41
+
42
+ const columns = DATA_SOURCES[activeDomain].columns
43
+
44
+ const visibleColumns = useMemo(
45
+ () => columnOrder.filter((k) => columnVisibility[k] && columns[k] !== undefined),
46
+ [columnOrder, columnVisibility, columns]
47
+ )
48
+
49
+ const reorderColumn = useCallback((activeId: string, overId: string) => {
50
+ setColumnOrder((prev) => {
51
+ const result = prev.filter((k) => k !== activeId)
52
+ const overIndex = result.indexOf(overId)
53
+ result.splice(overIndex, 0, activeId)
54
+ return result
55
+ })
56
+ }, [])
57
+
58
+ const toggleColumnVisibility = useCallback((key: string) => {
59
+ setColumnVisibility((prev) => ({ ...prev, [key]: !prev[key] }))
60
+ }, [])
61
+
62
+ const resetColumns = useCallback(() => {
63
+ const { columnOrder: order, columnVisibility: visibility } = buildInitialState(activeDomain)
64
+ setColumnOrder(order)
65
+ setColumnVisibility(visibility)
66
+ }, [activeDomain])
67
+
68
+ return {
69
+ columnOrder,
70
+ columnVisibility,
71
+ visibleColumns,
72
+ reorderColumn,
73
+ toggleColumnVisibility,
74
+ resetColumns,
75
+ }
76
+ }
@@ -0,0 +1,313 @@
1
+ import { useState, useMemo, useCallback } from 'react'
2
+ import type { DomainKey, DomainConfig, ViewMode, SortState, FilterState } from '../types'
3
+ import { DATA_SOURCES } from '../constants'
4
+
5
+ export interface DataExplorerState {
6
+ // Domain
7
+ activeDomain: DomainKey
8
+ setActiveDomain: (domain: DomainKey) => void
9
+ domainConfig: DomainConfig
10
+
11
+ // View
12
+ viewMode: ViewMode
13
+ setViewMode: (mode: ViewMode) => void
14
+
15
+ // Sorting
16
+ sort: SortState
17
+ toggleSort: (column: string) => void
18
+
19
+ // Filtering
20
+ filters: FilterState
21
+ toggleFilter: (column: string, value: string) => void
22
+ clearFilter: (column: string) => void
23
+ clearAllFilters: () => void
24
+ clearSecondaryFilters: () => void
25
+ hasActiveFilters: boolean
26
+ activeFilterCount: number
27
+ getFilterOptions: (colKey: string) => string[]
28
+
29
+ // Search
30
+ searchTerm: string
31
+ setSearchTerm: (term: string) => void
32
+
33
+ // Expansion
34
+ expandedRows: Set<string>
35
+ toggleExpansion: (rowId: string, field: string) => void
36
+
37
+ // Favorites
38
+ favorites: Set<string>
39
+ toggleFavorite: (id: string) => void
40
+
41
+ // Pagination
42
+ currentPage: number
43
+ pageSize: number
44
+ setPage: (page: number) => void
45
+ setPageSize: (size: number) => void
46
+ totalFiltered: number
47
+ totalPages: number
48
+
49
+ // Derived data
50
+ paginatedData: Record<string, unknown>[]
51
+ visibleColumns: string[]
52
+ }
53
+
54
+ function initFavorites(data: Record<string, unknown>[]): Set<string> {
55
+ const favs = new Set<string>()
56
+ for (const row of data) {
57
+ if (row.favorite) favs.add(row.id as string)
58
+ }
59
+ return favs
60
+ }
61
+
62
+ export function useDataExplorerState(
63
+ initialDomain: DomainKey = 'products',
64
+ ): DataExplorerState {
65
+ const [activeDomain, setActiveDomainRaw] = useState<DomainKey>(initialDomain)
66
+ const [viewMode, setViewMode] = useState<ViewMode>('table')
67
+ const [sort, setSort] = useState<SortState>({ column: null, direction: 'asc' })
68
+ const [filters, setFilters] = useState<FilterState>({})
69
+ const [searchTerm, setSearchTermRaw] = useState('')
70
+ const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
71
+ const [favorites, setFavorites] = useState<Set<string>>(() =>
72
+ initFavorites(DATA_SOURCES[initialDomain].data),
73
+ )
74
+ const [currentPage, setCurrentPage] = useState(1)
75
+ const [pageSize, setPageSizeRaw] = useState(25)
76
+
77
+ const domainConfig = useMemo<DomainConfig>(
78
+ () => DATA_SOURCES[activeDomain],
79
+ [activeDomain],
80
+ )
81
+
82
+ const setActiveDomain = useCallback((domain: DomainKey) => {
83
+ setActiveDomainRaw(domain)
84
+ setSort({ column: null, direction: 'asc' })
85
+ setFilters({})
86
+ setSearchTermRaw('')
87
+ setExpandedRows(new Set())
88
+ setCurrentPage(1)
89
+ setFavorites(initFavorites(DATA_SOURCES[domain].data))
90
+ }, [])
91
+
92
+ const toggleSort = useCallback((column: string) => {
93
+ setSort((prev) => {
94
+ if (prev.column === column) {
95
+ return { column, direction: prev.direction === 'asc' ? 'desc' : 'asc' }
96
+ }
97
+ return { column, direction: 'asc' }
98
+ })
99
+ setCurrentPage(1)
100
+ }, [])
101
+
102
+ const toggleFilter = useCallback((column: string, value: string) => {
103
+ setFilters((prev) => {
104
+ const current = prev[column] ?? []
105
+ const next = current.includes(value)
106
+ ? current.filter((v) => v !== value)
107
+ : [...current, value]
108
+ return { ...prev, [column]: next }
109
+ })
110
+ setCurrentPage(1)
111
+ }, [])
112
+
113
+ const clearFilter = useCallback((column: string) => {
114
+ setFilters((prev) => {
115
+ const next = { ...prev }
116
+ delete next[column]
117
+ return next
118
+ })
119
+ setCurrentPage(1)
120
+ }, [])
121
+
122
+ const clearAllFilters = useCallback(() => {
123
+ setFilters({})
124
+ setSearchTermRaw('')
125
+ setCurrentPage(1)
126
+ }, [])
127
+
128
+ const clearSecondaryFilters = useCallback(() => {
129
+ setFilters((prev) => {
130
+ const cols = DATA_SOURCES[activeDomain].columns
131
+ const next: FilterState = {}
132
+ for (const [k, v] of Object.entries(prev)) {
133
+ if (cols[k]?.primaryFilter) next[k] = v
134
+ }
135
+ return next
136
+ })
137
+ setCurrentPage(1)
138
+ // activeDomain is stable within a single render; callback captures it via closure
139
+ }, [activeDomain])
140
+
141
+ const setSearchTerm = useCallback((term: string) => {
142
+ setSearchTermRaw(term)
143
+ setCurrentPage(1)
144
+ }, [])
145
+
146
+ const toggleExpansion = useCallback((rowId: string, field: string) => {
147
+ const key = `${rowId}::${field}`
148
+ setExpandedRows((prev) => {
149
+ const next = new Set(prev)
150
+ if (next.has(key)) {
151
+ next.delete(key)
152
+ } else {
153
+ next.add(key)
154
+ }
155
+ return next
156
+ })
157
+ }, [])
158
+
159
+ const toggleFavorite = useCallback((id: string) => {
160
+ setFavorites((prev) => {
161
+ const next = new Set(prev)
162
+ if (next.has(id)) {
163
+ next.delete(id)
164
+ } else {
165
+ next.add(id)
166
+ }
167
+ return next
168
+ })
169
+ }, [])
170
+
171
+ const setPage = useCallback((page: number) => {
172
+ setCurrentPage(page)
173
+ }, [])
174
+
175
+ const setPageSize = useCallback((size: number) => {
176
+ setPageSizeRaw(size)
177
+ setCurrentPage(1)
178
+ }, [])
179
+
180
+ const getFilterOptions = useCallback(
181
+ (colKey: string): string[] => {
182
+ const col = DATA_SOURCES[activeDomain].columns[colKey]
183
+ if (!col) return []
184
+ if (col.filterOptions) return col.filterOptions
185
+ const vals = new Set<string>()
186
+ for (const row of DATA_SOURCES[activeDomain].data) {
187
+ const v = row[colKey]
188
+ if (v != null && v !== '') vals.add(String(v))
189
+ }
190
+ return [...vals].sort((a, b) => a.localeCompare(b, 'de'))
191
+ },
192
+ [activeDomain],
193
+ )
194
+
195
+ const filteredData = useMemo(() => {
196
+ const { data, columns } = DATA_SOURCES[activeDomain]
197
+ const term = searchTerm.toLowerCase()
198
+
199
+ return data.filter((row) => {
200
+ // search
201
+ if (term) {
202
+ let matched = false
203
+ for (const [k, col] of Object.entries(columns)) {
204
+ if (!col.searchable) continue
205
+ const v = row[k]
206
+ if (v != null && String(v).toLowerCase().includes(term)) {
207
+ matched = true
208
+ break
209
+ }
210
+ if (col.secondary) {
211
+ const sv = row[col.secondary]
212
+ if (sv != null && String(sv).toLowerCase().includes(term)) {
213
+ matched = true
214
+ break
215
+ }
216
+ }
217
+ }
218
+ if (!matched) return false
219
+ }
220
+
221
+ // filters
222
+ for (const [colKey, selected] of Object.entries(filters)) {
223
+ if (!selected || selected.length === 0) continue
224
+ const col = columns[colKey]
225
+ if (!col) continue
226
+ if (col.filterFn) {
227
+ if (!selected.some((val) => col.filterFn!(row, val))) return false
228
+ } else {
229
+ if (!selected.includes(String(row[colKey] ?? ''))) return false
230
+ }
231
+ }
232
+
233
+ return true
234
+ })
235
+ }, [activeDomain, filters, searchTerm])
236
+
237
+ const sortedData = useMemo(() => {
238
+ if (!sort.column) return filteredData
239
+ const col = DATA_SOURCES[activeDomain].columns[sort.column]
240
+ if (!col) return filteredData
241
+
242
+ const key = sort.column
243
+ const dir = sort.direction === 'asc' ? 1 : -1
244
+
245
+ return [...filteredData].sort((a, b) => {
246
+ let va = a[key] ?? ''
247
+ let vb = b[key] ?? ''
248
+ if (typeof va === 'number' && typeof vb === 'number') {
249
+ return (va - vb) * dir
250
+ }
251
+ return String(va).localeCompare(String(vb), 'de') * dir
252
+ })
253
+ }, [filteredData, sort, activeDomain])
254
+
255
+ const totalFiltered = useMemo(() => filteredData.length, [filteredData])
256
+
257
+ const totalPages = useMemo(
258
+ () => Math.max(1, Math.ceil(totalFiltered / pageSize)),
259
+ [totalFiltered, pageSize],
260
+ )
261
+
262
+ const paginatedData = useMemo(() => {
263
+ const start = (currentPage - 1) * pageSize
264
+ return sortedData.slice(start, start + pageSize)
265
+ }, [sortedData, currentPage, pageSize])
266
+
267
+ const visibleColumns = useMemo(() => {
268
+ const { columns } = DATA_SOURCES[activeDomain]
269
+ return Object.keys(columns).filter((k) => columns[k].visible !== false)
270
+ }, [activeDomain])
271
+
272
+ const hasActiveFilters = useMemo(() => {
273
+ if (searchTerm) return true
274
+ return Object.values(filters).some((v) => v && v.length > 0)
275
+ }, [filters, searchTerm])
276
+
277
+ const activeFilterCount = useMemo(
278
+ () => Object.values(filters).reduce((acc, v) => acc + (v?.length ?? 0), 0),
279
+ [filters],
280
+ )
281
+
282
+ return {
283
+ activeDomain,
284
+ setActiveDomain,
285
+ domainConfig,
286
+ viewMode,
287
+ setViewMode,
288
+ sort,
289
+ toggleSort,
290
+ filters,
291
+ toggleFilter,
292
+ clearFilter,
293
+ clearAllFilters,
294
+ clearSecondaryFilters,
295
+ hasActiveFilters,
296
+ activeFilterCount,
297
+ getFilterOptions,
298
+ searchTerm,
299
+ setSearchTerm,
300
+ expandedRows,
301
+ toggleExpansion,
302
+ favorites,
303
+ toggleFavorite,
304
+ currentPage,
305
+ pageSize,
306
+ setPage,
307
+ setPageSize,
308
+ totalFiltered,
309
+ totalPages,
310
+ paginatedData,
311
+ visibleColumns,
312
+ }
313
+ }
@@ -0,0 +1 @@
1
+ export { DataVisualizationPage } from './DataVisualizationPage/DataVisualizationPage'
@@ -0,0 +1,99 @@
1
+ export type DomainKey = 'products' | 'projects' | 'customers' | 'applications'
2
+
3
+ export type CellType =
4
+ | 'text'
5
+ | 'double-text'
6
+ | 'link'
7
+ | 'status-badge'
8
+ | 'currency'
9
+ | 'inventory'
10
+ | 'favorite'
11
+ | 'expand'
12
+ | 'score-bar'
13
+
14
+ export type ViewMode = 'table' | 'list' | 'card'
15
+
16
+ export type SortDirection = 'asc' | 'desc'
17
+
18
+ export interface SortState {
19
+ column: string | null
20
+ direction: SortDirection
21
+ }
22
+
23
+ export type FilterState = Record<string, string[]>
24
+
25
+ export interface ExpandColumnDef {
26
+ key: string
27
+ label: string
28
+ mapTo?: string
29
+ secondaryKey?: string
30
+ bold?: boolean
31
+ muted?: boolean
32
+ type?: CellType
33
+ }
34
+
35
+ export interface ColumnDef {
36
+ label: string
37
+ type: CellType
38
+ sortable: boolean
39
+ filterable: boolean
40
+ primaryFilter?: boolean
41
+ visible: boolean
42
+ searchable?: boolean
43
+ secondary?: string
44
+ currencyField?: string
45
+ statusMap?: Record<string, string>
46
+ levelFn?: (v: number) => 'high' | 'medium' | 'low'
47
+ formatFn?: (v: number) => string
48
+ labelMap?: Record<string, string>
49
+ filterOptions?: string[]
50
+ filterFn?: (row: Record<string, unknown>, val: string) => boolean
51
+ expandColumns?: ExpandColumnDef[]
52
+ expandTitleFn?: (row: Record<string, unknown>) => string
53
+ expandLabel?: string
54
+ expandIcon?: string
55
+ headerIcon?: string
56
+ headerTooltip?: string
57
+ hideTablet?: boolean
58
+ hideMobile?: boolean
59
+ rowLines?: number
60
+ }
61
+
62
+ export interface ListLayout {
63
+ titleField: string
64
+ metaFields: string[]
65
+ badgeFields: string[]
66
+ valueField: string | null
67
+ expandFields?: string[]
68
+ expandField?: string
69
+ }
70
+
71
+ export interface CardRow {
72
+ label: string
73
+ field: string
74
+ rendererOverride?: string
75
+ }
76
+
77
+ export interface CardLayout {
78
+ titleField: string
79
+ subtitleField: string
80
+ badgeFields: string[]
81
+ rows: CardRow[]
82
+ footerField: string
83
+ expandFields?: string[]
84
+ expandField?: string
85
+ }
86
+
87
+ export interface DomainLayout {
88
+ list: ListLayout
89
+ card: CardLayout
90
+ }
91
+
92
+ export interface DomainConfig {
93
+ key: DomainKey
94
+ label: string
95
+ resultLabel: string
96
+ columns: Record<string, ColumnDef>
97
+ layout: DomainLayout
98
+ data: Record<string, unknown>[]
99
+ }
@@ -0,0 +1,97 @@
1
+ import * as React from 'react'
2
+ import { ChevronDown } from 'lucide-react'
3
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
4
+ import { QuickAccessCard } from '@/components/QuickAccessCard/QuickAccessCard'
5
+ import { cn } from '@/lib/utils'
6
+ import type { QuickAccessItem } from '../types'
7
+
8
+ interface QuickAccessProps extends React.ComponentProps<'section'> {
9
+ items: QuickAccessItem[]
10
+ defaultOpen?: boolean
11
+ activeItemId?: string
12
+ onSelect?: (item: QuickAccessItem) => void
13
+ }
14
+
15
+ const QuickAccess = React.forwardRef<HTMLElement, QuickAccessProps>(
16
+ ({ items, defaultOpen = true, activeItemId, onSelect, className, ...props }, ref) => {
17
+ const titleId = React.useId()
18
+
19
+ return (
20
+ <section
21
+ ref={ref}
22
+ data-slot="quick-access"
23
+ aria-labelledby={titleId}
24
+ className={cn(
25
+ 'bg-background border border-border rounded-lg overflow-hidden shadow-[var(--shadow-card)]',
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ <Collapsible defaultOpen={defaultOpen}>
31
+ {/* Header */}
32
+ <div className="flex items-center gap-2 px-5 py-3 border-b border-border select-none">
33
+ <CollapsibleTrigger
34
+ className={cn(
35
+ 'group inline-flex items-center justify-center h-7 w-7 rounded-sm',
36
+ 'text-muted-foreground bg-transparent border-none cursor-pointer',
37
+ 'transition-[background-color,color] duration-150',
38
+ 'hover:bg-muted hover:text-foreground',
39
+ 'focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2',
40
+ )}
41
+ aria-label="Schnellzugriffe ein-/ausklappen"
42
+ >
43
+ <ChevronDown
44
+ size={16}
45
+ className="transition-transform duration-300 group-data-[state=closed]:-rotate-90"
46
+ />
47
+ </CollapsibleTrigger>
48
+
49
+ <h2
50
+ id={titleId}
51
+ className="text-[0.8125rem] font-medium tracking-[0.06em] uppercase text-foreground"
52
+ style={{ fontFamily: 'var(--font-clash)' }}
53
+ >
54
+ Schnellzugriffe
55
+ </h2>
56
+ </div>
57
+
58
+ {/* Cards */}
59
+ <CollapsibleContent>
60
+ <div
61
+ role="list"
62
+ className="flex flex-wrap gap-3 p-4 md:flex-nowrap md:gap-4 md:p-5"
63
+ >
64
+ {items.map((item) => (
65
+ <QuickAccessCard
66
+ key={item.id}
67
+ role="listitem"
68
+ label={item.label}
69
+ count={item.count}
70
+ countLabel={item.countLabel}
71
+ icon={item.icon}
72
+ variant={item.variant}
73
+ isActive={activeItemId === item.id}
74
+ href={item.href ?? `#${item.id}`}
75
+ className="flex-[1_1_100%] min-[480px]:flex-[1_1_calc(50%_-_0.375rem)] md:flex-1"
76
+ onClick={
77
+ onSelect
78
+ ? (e) => {
79
+ e.preventDefault()
80
+ onSelect(item)
81
+ }
82
+ : undefined
83
+ }
84
+ />
85
+ ))}
86
+ </div>
87
+ </CollapsibleContent>
88
+ </Collapsible>
89
+ </section>
90
+ )
91
+ },
92
+ )
93
+
94
+ QuickAccess.displayName = 'QuickAccess'
95
+
96
+ export { QuickAccess }
97
+ export type { QuickAccessProps }
@@ -0,0 +1,2 @@
1
+ export * from './QuickAccess/QuickAccess'
2
+ export type * from './types'
@@ -0,0 +1,11 @@
1
+ import type * as React from 'react'
2
+
3
+ export interface QuickAccessItem {
4
+ id: string
5
+ label: string
6
+ count: number
7
+ countLabel?: string // defaults to 'Projekte'
8
+ href?: string
9
+ variant?: 'default' | 'overdue'
10
+ icon: React.ReactNode
11
+ }