@fastnd/components 1.0.30 → 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.
- 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/StatusDonutChart/StatusDonutChart.tsx +41 -56
- package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
- package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +175 -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 +246 -0
- package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +449 -0
- package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +140 -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 +313 -0
- package/dist/examples/data-visualization/index.ts +1 -0
- package/dist/examples/data-visualization/types.ts +99 -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
- package/dist/examples/data-explorer/CardCarouselPanel/CardCarouselPanel.tsx +0 -197
- package/dist/examples/data-explorer/CardView/CardView.tsx +0 -168
- package/dist/examples/data-explorer/ColumnConfigPopover/ColumnConfigPopover.tsx +0 -157
- package/dist/examples/data-explorer/DataExplorerEmpty/DataExplorerEmpty.tsx +0 -56
- package/dist/examples/data-explorer/DataExplorerPage/DataExplorerPage.tsx +0 -101
- package/dist/examples/data-explorer/DataExplorerPagination/DataExplorerPagination.tsx +0 -129
- package/dist/examples/data-explorer/DataExplorerToolbar/DataExplorerToolbar.tsx +0 -143
- package/dist/examples/data-explorer/DomainSwitcher/DomainSwitcher.tsx +0 -36
- package/dist/examples/data-explorer/ExpansionRows/ExpansionRows.tsx +0 -180
- package/dist/examples/data-explorer/FilterChip/FilterChip.tsx +0 -85
- package/dist/examples/data-explorer/FilterPopoverContent/FilterPopoverContent.tsx +0 -73
- package/dist/examples/data-explorer/ListView/ListView.tsx +0 -305
- package/dist/examples/data-explorer/MoreFiltersPopover/MoreFiltersPopover.tsx +0 -113
- package/dist/examples/data-explorer/TableView/TableView.tsx +0 -193
- package/dist/examples/data-explorer/cells/CellRenderer.tsx +0 -147
- package/dist/examples/data-explorer/cells/CurrencyCell.tsx +0 -31
- package/dist/examples/data-explorer/cells/DoubleTextCell.tsx +0 -27
- package/dist/examples/data-explorer/cells/ExpandButton.tsx +0 -67
- package/dist/examples/data-explorer/cells/InventoryBadgeCell.tsx +0 -52
- package/dist/examples/data-explorer/cells/LinkCell.tsx +0 -42
- package/dist/examples/data-explorer/cells/ScoreBar.tsx +0 -50
- package/dist/examples/data-explorer/cells/StatusBadgeCell.tsx +0 -39
- package/dist/examples/data-explorer/cells/TextCell.tsx +0 -35
- package/dist/examples/data-explorer/cells/index.ts +0 -26
- package/dist/examples/data-explorer/domains/applications.ts +0 -225
- package/dist/examples/data-explorer/domains/customers.ts +0 -267
- package/dist/examples/data-explorer/domains/index.ts +0 -26
- package/dist/examples/data-explorer/domains/products.ts +0 -1116
- package/dist/examples/data-explorer/domains/projects.ts +0 -205
- package/dist/examples/data-explorer/hooks/use-data-explorer-state.ts +0 -371
- package/dist/examples/data-explorer/index.ts +0 -3
- package/dist/examples/data-explorer/types.ts +0 -239
|
@@ -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 }
|