@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.
@@ -23,6 +23,7 @@ export interface DataListViewProps {
23
23
  onToggleExpansion: (rowId: string, field: string) => void
24
24
  favorites: Set<string>
25
25
  onToggleFavorite: (id: string) => void
26
+ showHeader?: boolean
26
27
  className?: string
27
28
  }
28
29
 
@@ -122,6 +123,52 @@ function ExpansionCell({ ec, value }: ExpansionCellProps) {
122
123
  return <span>{text}</span>
123
124
  }
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-3 border-b border-border bg-secondary">
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-foreground uppercase tracking-[0.03em]">
147
+ {titleLabel}
148
+ </div>
149
+ {layout.badgeFields.map((field) => (
150
+ <span
151
+ key={field}
152
+ style={{ minWidth: columns[field]?.badgeColumnWidth ?? 96 }}
153
+ className="shrink-0 text-xs font-medium text-foreground uppercase tracking-[0.03em] text-right"
154
+ >
155
+ {columns[field]?.label ?? ''}
156
+ </span>
157
+ ))}
158
+ {expandFields.map((field) => (
159
+ <span key={field} className="shrink-0 text-xs font-medium text-foreground uppercase tracking-[0.03em]">
160
+ {columns[field]?.expandLabel ?? columns[field]?.label ?? ''}
161
+ </span>
162
+ ))}
163
+ {Object.values(columns).some((c) => c.type === 'actions') && (
164
+ <span className="shrink-0 text-xs font-medium text-foreground uppercase tracking-[0.03em]">
165
+ {Object.values(columns).find((c) => c.type === 'actions')?.label ?? 'Aktionen'}
166
+ </span>
167
+ )}
168
+ </div>
169
+ )
170
+ }
171
+
125
172
  function DataListView({
126
173
  data,
127
174
  columns,
@@ -130,82 +177,81 @@ function DataListView({
130
177
  onToggleExpansion,
131
178
  favorites,
132
179
  onToggleFavorite,
180
+ showHeader = false,
133
181
  className,
134
182
  }: DataListViewProps) {
135
183
  const expandFields = layout.expandFields ?? (layout.expandField ? [layout.expandField] : [])
136
184
  const titleCol = columns[layout.titleField]
137
185
  const isDoubleText = titleCol?.type === 'double-text'
186
+ const actionsColKey = Object.keys(columns).find((k) => columns[k]?.type === 'actions')
138
187
 
139
188
  return (
140
189
  <div
141
190
  data-slot="data-list-view"
142
- className={cn(
143
- 'border border-border rounded-lg overflow-hidden divide-y divide-border',
144
- className,
145
- )}
191
+ className={cn('divide-y divide-border', className)}
146
192
  >
193
+ {showHeader && <ListHeader columns={columns} layout={layout} />}
147
194
  {data.map((row) => {
148
195
  const rowId = String(row.id ?? '')
149
196
  const titleValue = row[layout.titleField]
150
197
  const subtitleValue = isDoubleText && titleCol.secondary ? row[titleCol.secondary] : undefined
198
+ const hasBadges = layout.badgeFields.length > 0
199
+ const hasExpand = expandFields.length > 0
151
200
 
152
201
  return (
153
- <div
154
- key={rowId}
155
- className="grid grid-cols-[36px_1fr_auto_auto] sm:grid-cols-[36px_1fr_auto_auto] items-start gap-x-3 px-4 py-3 hover:bg-accent transition-colors"
156
- >
157
- {/* Col 1: Favorite */}
158
- <div className="flex items-center justify-center pt-0.5">
159
- <FavoriteButton
160
- pressed={favorites.has(rowId)}
161
- itemName={titleValue != null ? String(titleValue) : ''}
162
- onPressedChange={() => onToggleFavorite(rowId)}
163
- />
164
- </div>
165
-
166
- {/* Col 2: Title block */}
167
- <div className="min-w-0">
168
- <div className="font-semibold text-sm truncate max-w-[280px] sm:max-w-none">
169
- {titleValue != null ? String(titleValue) : ''}
202
+ <div key={rowId}>
203
+ {/* Row: Fav | Title + dot-meta | Fixed-width badge columns | Expand */}
204
+ <div className="flex items-start gap-3 px-4 py-3 hover:bg-accent transition-colors">
205
+ <div className="flex-none mt-0.5">
206
+ <FavoriteButton
207
+ pressed={favorites.has(rowId)}
208
+ itemName={titleValue != null ? String(titleValue) : ''}
209
+ onPressedChange={() => onToggleFavorite(rowId)}
210
+ />
170
211
  </div>
171
- <div className="flex flex-wrap items-center gap-2 mt-0.5">
172
- {subtitleValue != null && (
173
- <span className="text-xs text-muted-foreground truncate max-w-[320px] sm:max-w-none">
174
- {String(subtitleValue)}
175
- </span>
212
+
213
+ {/* Title block grows, no fixed width */}
214
+ <div className="flex-1 min-w-0">
215
+ <div className="font-semibold text-sm truncate">
216
+ {titleValue != null ? String(titleValue) : ''}
217
+ </div>
218
+ {/* Meta line: subtitle + metaFields, dot-separated, each segment truncated */}
219
+ {(subtitleValue != null || layout.metaFields.some(f => row[f] != null && row[f] !== '')) && (
220
+ <p className="text-xs text-muted-foreground mt-0.5 flex min-w-0">
221
+ {[
222
+ ...(subtitleValue != null ? [String(subtitleValue)] : []),
223
+ ...layout.metaFields
224
+ .map(f => row[f])
225
+ .filter(v => v != null && v !== '')
226
+ .map(String),
227
+ ].map((item, i) => (
228
+ <React.Fragment key={i}>
229
+ {i > 0 && <span className="shrink-0 px-1">·</span>}
230
+ <span className="truncate max-w-[160px]">{item}</span>
231
+ </React.Fragment>
232
+ ))}
233
+ </p>
176
234
  )}
177
- {layout.metaFields.map((field) => {
178
- const v = row[field]
179
- if (v == null || v === '') return null
180
- return (
181
- <span
182
- key={field}
183
- className="shrink-0 bg-secondary rounded-md px-2 py-0.5 text-xs max-w-[180px] truncate"
184
- >
185
- {String(v)}
186
- </span>
187
- )
188
- })}
189
235
  </div>
190
- </div>
191
236
 
192
- {/* Col 3: Badges */}
193
- <div className="flex items-center gap-2 pt-0.5 max-sm:col-start-2 max-sm:col-end-3 max-sm:row-start-2">
237
+ {/* Badge columns fixed min-width per column for vertical alignment */}
194
238
  {layout.badgeFields.map((field) => {
195
239
  const col = columns[field]
196
240
  if (!col) return null
197
241
  return (
198
- <React.Fragment key={field}>
242
+ <div
243
+ key={field}
244
+ style={{ minWidth: col.badgeColumnWidth ?? 96 }}
245
+ className="flex justify-end items-start"
246
+ >
199
247
  {renderCell(field, col, row, {
200
248
  mode: col.type === 'inventory' ? 'inventory-label' : 'default',
201
249
  })}
202
- </React.Fragment>
250
+ </div>
203
251
  )
204
252
  })}
205
- </div>
206
253
 
207
- {/* Col 4: Expand buttons */}
208
- <div className="flex items-center gap-2 pt-0.5 max-sm:col-start-2 max-sm:col-end-3 max-sm:row-start-3">
254
+ {/* Expand buttons — trailing edge */}
209
255
  {expandFields.map((field) => {
210
256
  const col = columns[field]
211
257
  if (!col) return null
@@ -220,16 +266,23 @@ function DataListView({
220
266
  </React.Fragment>
221
267
  )
222
268
  })}
269
+
270
+ {/* Actions */}
271
+ {actionsColKey && renderCell(actionsColKey, columns[actionsColKey], row, {
272
+ onAddToProject: () => {},
273
+ onAddToCollection: () => {},
274
+ onDelete: () => {},
275
+ })}
223
276
  </div>
224
277
 
225
- {/* Expansion content — full width */}
278
+ {/* Expansion panels — full width, below the row */}
226
279
  {expandFields.map((field) => {
227
280
  const expandKey = `${rowId}::${field}`
228
281
  if (!expandedRows.has(expandKey)) return null
229
282
  return (
230
283
  <div
231
284
  key={field}
232
- className="col-span-full border-t border-border pt-3 mt-1 max-sm:col-start-1 max-sm:row-start-4"
285
+ className="px-4 pb-3 pt-3 border-t border-border"
233
286
  data-slot="expansion-content"
234
287
  >
235
288
  <ExpansionTable row={row} colKey={field} columns={columns} />
@@ -1,5 +1,5 @@
1
- import React, { useState, useRef, useCallback } from 'react'
2
- import { ChevronUp, ChevronDown, ArrowLeftRight, Sparkles, Plus } from 'lucide-react'
1
+ import React, { useState, useRef, useCallback, useLayoutEffect } from 'react'
2
+ import { ChevronUp, ChevronDown, ArrowLeftRight, Sparkles, Plus, Star } from 'lucide-react'
3
3
  import {
4
4
  Table,
5
5
  TableBody,
@@ -24,6 +24,7 @@ import type { ColumnDef, ExpandColumnDef, SortState } from '../types'
24
24
  const HEADER_ICONS: Record<string, React.ElementType> = {
25
25
  arrowLeftRight: ArrowLeftRight,
26
26
  sparkles: Sparkles,
27
+ star: Star,
27
28
  }
28
29
 
29
30
  // Default minimum widths per column type (px)
@@ -35,6 +36,7 @@ const DEFAULT_MIN_WIDTHS: Partial<Record<string, number>> = {
35
36
  currency: 100,
36
37
  'double-text': 200,
37
38
  link: 140,
39
+ actions: 112,
38
40
  }
39
41
 
40
42
  // Column types that should wrap text (line-clamp handles truncation)
@@ -62,7 +64,7 @@ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: E
62
64
  if (mainColKey === expandColKey) {
63
65
  return (
64
66
  <Button
65
- variant="ghost"
67
+ variant="outline"
66
68
  size="icon"
67
69
  className="size-7"
68
70
  aria-label="Hinzufügen"
@@ -83,7 +85,7 @@ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: E
83
85
  const sec = item[ec.secondaryKey]
84
86
  return (
85
87
  <>
86
- <span className={cn('font-semibold text-[13px] line-clamp-2', ec.bold && 'font-bold')}>
88
+ <span className={cn('font-medium text-[13px] line-clamp-2', ec.bold && 'font-bold')}>
87
89
  {val != null ? String(val) : ''}
88
90
  </span>
89
91
  <span className="text-muted-foreground text-xs line-clamp-1">
@@ -110,17 +112,13 @@ function ExpansionCell({ mainColKey, expandColKey, mainCol, mapLookup, item }: E
110
112
  if (mainCol.type === 'status-badge' && mainCol.statusMap && val != null) {
111
113
  const status = mainCol.statusMap[String(val)] ?? 'active'
112
114
  const STATUS_COLORS: Record<string, string> = {
113
- active: 'bg-[var(--lifecycle-active-bg)] text-[var(--lifecycle-active)] border-transparent',
114
- nrnd: 'bg-[var(--lifecycle-nrnd-bg)] text-[var(--lifecycle-nrnd)] border-transparent',
115
- eol: 'bg-[var(--lifecycle-eol-bg)] text-[var(--lifecycle-eol)] border-transparent',
116
- production: 'bg-[var(--lifecycle-production-bg)] text-[var(--lifecycle-production)] border-transparent',
115
+ active: 'border-[var(--lifecycle-active)]',
116
+ nrnd: 'border-[var(--lifecycle-nrnd)]',
117
+ eol: 'border-[var(--lifecycle-eol)]',
118
+ production: 'border-[var(--lifecycle-production)]',
117
119
  }
118
120
  return (
119
- <Badge className={cn('gap-1.5', STATUS_COLORS[status])}>
120
- <span
121
- className={cn('size-1.5 rounded-full', `bg-[var(--lifecycle-${status})]`)}
122
- aria-hidden="true"
123
- />
121
+ <Badge variant="outline" className={STATUS_COLORS[status]}>
124
122
  {String(val)}
125
123
  </Badge>
126
124
  )
@@ -154,9 +152,10 @@ interface ExpansionSectionProps {
154
152
  colKey: string
155
153
  visibleColumns: string[]
156
154
  columns: Record<string, ColumnDef>
155
+ columnWidths: Record<string, number>
157
156
  }
158
157
 
159
- function ExpansionSection({ row, colKey, visibleColumns, columns }: ExpansionSectionProps) {
158
+ function ExpansionSection({ row, colKey, visibleColumns, columns, columnWidths }: ExpansionSectionProps) {
160
159
  const col = columns[colKey]
161
160
  const items = row[colKey] as Record<string, unknown>[] | undefined
162
161
  if (!items?.length || !col.expandColumns) return null
@@ -200,6 +199,7 @@ function ExpansionSection({ row, colKey, visibleColumns, columns }: ExpansionSec
200
199
  mainCol.hideTablet && 'hidden lg:table-cell',
201
200
  mainCol.hideMobile && 'hidden sm:table-cell',
202
201
  )}
202
+ style={columnWidths[mainKey] != null ? { width: columnWidths[mainKey] } : undefined}
203
203
  >
204
204
  <ExpansionCell
205
205
  mainColKey={mainKey}
@@ -227,6 +227,7 @@ export interface DataTableViewProps {
227
227
  onToggleExpansion: (rowId: string, field: string) => void
228
228
  favorites: Set<string>
229
229
  onToggleFavorite: (id: string) => void
230
+ footer?: React.ReactNode
230
231
  className?: string
231
232
  }
232
233
 
@@ -240,11 +241,18 @@ export function DataTableView({
240
241
  onToggleExpansion,
241
242
  favorites,
242
243
  onToggleFavorite,
244
+ footer,
243
245
  className,
244
246
  }: DataTableViewProps) {
245
247
  // Column widths: keyed by column key, value in px
246
248
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
247
249
 
250
+ // Ref to the header <tr> for snapshotting rendered column widths
251
+ const headerRowRef = useRef<HTMLTableRowElement>(null)
252
+
253
+ // Tracks the previous visibleColumns reference to detect changes
254
+ const prevVisibleRef = useRef<string[]>(visibleColumns)
255
+
248
256
  // Resize tracking ref (not state — no re-render during drag)
249
257
  const resizeRef = useRef<{
250
258
  colKey: string
@@ -252,6 +260,31 @@ export function DataTableView({
252
260
  startWidth: number
253
261
  } | null>(null)
254
262
 
263
+ // Measure-then-lock: snapshot <th> widths while table is still table-layout:auto,
264
+ // then switch to table-fixed via colgroup so expansion rows can never widen columns.
265
+ useLayoutEffect(() => {
266
+ const prev = prevVisibleRef.current
267
+ const changed =
268
+ prev.length !== visibleColumns.length || prev.some((k, i) => k !== visibleColumns[i])
269
+ if (changed) {
270
+ // Column set changed — clear all widths so the table re-snapshots with natural auto sizing
271
+ prevVisibleRef.current = visibleColumns
272
+ setColumnWidths({})
273
+ return
274
+ }
275
+ if (!headerRowRef.current) return
276
+ const ths = Array.from(headerRowRef.current.querySelectorAll('th')) as HTMLTableCellElement[]
277
+ setColumnWidths((prev) => {
278
+ const next = { ...prev }
279
+ visibleColumns.forEach((colKey, i) => {
280
+ if (next[colKey] == null && ths[i]) {
281
+ next[colKey] = ths[i].getBoundingClientRect().width
282
+ }
283
+ })
284
+ return next
285
+ })
286
+ }, [visibleColumns])
287
+
255
288
  const handleResizeMouseDown = useCallback(
256
289
  (e: React.MouseEvent, colKey: string) => {
257
290
  e.preventDefault()
@@ -285,16 +318,30 @@ export function DataTableView({
285
318
  // Collect expand column keys for the current domain
286
319
  const expandColKeys = visibleColumns.filter((k) => columns[k]?.type === 'expand')
287
320
 
321
+ // Switch to table-fixed only after all columns have been measured — prevents content in
322
+ // expansion rows from ever forcing a column to widen.
323
+ const isFixed = visibleColumns.every((k) => columnWidths[k] != null)
324
+
288
325
  return (
326
+ <div className={cn('mx-4 mb-4 border border-border rounded-md overflow-hidden', className)}>
289
327
  <div
290
- className={cn('overflow-x-auto', className)}
328
+ className="overflow-x-auto"
291
329
  role="region"
292
330
  aria-label="Datentabelle"
293
331
  tabIndex={0}
294
332
  >
295
- <Table>
333
+ <Table className={isFixed ? 'table-fixed' : undefined}>
334
+ {/* colgroup enforces widths on ALL rows (including expansion rows) */}
335
+ <colgroup>
336
+ {visibleColumns.map((colKey) => (
337
+ <col
338
+ key={colKey}
339
+ style={columnWidths[colKey] != null ? { width: columnWidths[colKey] } : undefined}
340
+ />
341
+ ))}
342
+ </colgroup>
296
343
  <TableHeader>
297
- <TableRow className="bg-secondary hover:bg-secondary">
344
+ <TableRow ref={headerRowRef} className="bg-secondary hover:bg-secondary">
298
345
  {visibleColumns.map((colKey) => {
299
346
  const col = columns[colKey]
300
347
  if (!col) return null
@@ -423,6 +470,9 @@ export function DataTableView({
423
470
  isFavorite,
424
471
  onToggleExpand: () => onToggleExpansion(rowId, colKey),
425
472
  onToggleFavorite: () => onToggleFavorite(rowId),
473
+ onAddToProject: () => {},
474
+ onAddToCollection: () => {},
475
+ onDelete: () => {},
426
476
  })}
427
477
  </TableCell>
428
478
  )
@@ -437,6 +487,7 @@ export function DataTableView({
437
487
  colKey={expColKey}
438
488
  visibleColumns={visibleColumns}
439
489
  columns={columns}
490
+ columnWidths={columnWidths}
440
491
  />
441
492
  ))}
442
493
  </React.Fragment>
@@ -445,5 +496,7 @@ export function DataTableView({
445
496
  </TableBody>
446
497
  </Table>
447
498
  </div>
499
+ {footer && <div className="border-t border-border">{footer}</div>}
500
+ </div>
448
501
  )
449
502
  }
@@ -97,6 +97,17 @@ export function DataVisualizationPage({ className }: { className?: string }) {
97
97
  onToggleExpansion={state.toggleExpansion}
98
98
  favorites={state.favorites}
99
99
  onToggleFavorite={state.toggleFavorite}
100
+ footer={
101
+ <DataExplorerPagination
102
+ currentPage={state.currentPage}
103
+ totalPages={state.totalPages}
104
+ pageSize={state.pageSize}
105
+ totalFiltered={state.totalFiltered}
106
+ resultLabel={state.domainConfig.resultLabel}
107
+ onPageChange={state.setPage}
108
+ onPageSizeChange={state.setPageSize}
109
+ />
110
+ }
100
111
  />
101
112
  )}
102
113
  {state.viewMode === 'list' && (
@@ -124,7 +135,7 @@ export function DataVisualizationPage({ className }: { className?: string }) {
124
135
  </>
125
136
  )}
126
137
 
127
- {state.totalFiltered > 0 && (
138
+ {state.totalFiltered > 0 && state.viewMode !== 'table' && (
128
139
  <DataExplorerPagination
129
140
  currentPage={state.currentPage}
130
141
  totalPages={state.totalPages}
@@ -3,8 +3,8 @@ import type { ColumnDef, DomainConfig, DomainKey, DomainLayout } from './types'
3
3
  // ===== PRODUCT DOMAIN =====
4
4
 
5
5
  const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
6
- product_category: { label: 'Produktkategorie', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
7
- product_group: { label: 'Produktgruppe', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
6
+ product_category: { label: 'Kategorie', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
7
+ product_group: { label: 'Gruppe', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
8
8
  part_number: { label: 'Produkt', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'product_family_name', searchable: true },
9
9
  manufacturer_name: { label: 'Hersteller', type: 'link', sortable: true, filterable: true, primaryFilter: false, visible: true, searchable: true },
10
10
  description: { label: 'Beschreibung', type: 'text', sortable: false, filterable: false, visible: true, searchable: true, rowLines: 2 },
@@ -32,7 +32,7 @@ const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
32
32
  expandColumns: [
33
33
  { key: 'alt_product_category', label: 'Kategorie', mapTo: 'product_category' },
34
34
  { key: 'alt_family_name', label: 'Familie', mapTo: 'product_group' },
35
- { key: 'alt_part_number', label: 'Teilenummer', bold: true, mapTo: 'part_number', secondaryKey: 'alt_product_family_name' },
35
+ { key: 'alt_part_number', label: 'Teilenummer', mapTo: 'part_number', secondaryKey: 'alt_product_family_name' },
36
36
  { key: 'alt_manufacturer', label: 'Hersteller', mapTo: 'manufacturer_name' },
37
37
  { key: 'alt_description', label: 'Beschreibung', mapTo: 'description' },
38
38
  { key: 'alt_lifecycle', label: 'Status', mapTo: 'lifecycle' },
@@ -54,7 +54,7 @@ const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
54
54
  expandColumns: [
55
55
  { key: 'cross_sell_product_category', label: 'Kategorie', mapTo: 'product_category' },
56
56
  { key: 'cross_sell_family_name', label: 'Familie', mapTo: 'product_group' },
57
- { key: 'cross_sell_part_number', label: 'Teilenummer', bold: true, mapTo: 'part_number', secondaryKey: 'cross_sell_product_family_name' },
57
+ { key: 'cross_sell_part_number', label: 'Teilenummer', mapTo: 'part_number', secondaryKey: 'cross_sell_product_family_name' },
58
58
  { key: 'cross_sell_manufacturer', label: 'Hersteller', mapTo: 'manufacturer_name' },
59
59
  { key: 'cross_sell_description', label: 'Beschreibung', mapTo: 'description' },
60
60
  { key: 'recommendation_source', label: 'Quelle', muted: true, mapTo: 'lifecycle' },
@@ -83,6 +83,7 @@ const PRODUCT_COLUMNS: Record<string, ColumnDef> = {
83
83
  return v < 1000
84
84
  },
85
85
  },
86
+ actions: { label: 'Aktionen', type: 'actions', sortable: false, filterable: false, visible: true, configurable: false },
86
87
  }
87
88
 
88
89
  const PRODUCT_LAYOUT: DomainLayout = {
@@ -283,7 +284,7 @@ export const MOCK_PRODUCTS: Record<string, unknown>[] = [
283
284
  // ===== PROJECT DOMAIN =====
284
285
 
285
286
  const PROJECT_COLUMNS: Record<string, ColumnDef> = {
286
- favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true },
287
+ favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true, headerIcon: 'star', headerTooltip: 'Favorit' },
287
288
  name: { label: 'Projektname', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'customer_name', searchable: true },
288
289
  customer_name: { label: 'Kunde', type: 'link', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
289
290
  status: {
@@ -354,7 +355,7 @@ export const MOCK_PROJECTS: Record<string, unknown>[] = [
354
355
  // ===== CUSTOMER DOMAIN =====
355
356
 
356
357
  const CUSTOMER_COLUMNS: Record<string, ColumnDef> = {
357
- favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true },
358
+ favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true, headerIcon: 'star', headerTooltip: 'Favorit' },
358
359
  name: { label: 'Kunde', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'main_customer', searchable: true },
359
360
  category: { label: 'Kategorie', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, searchable: true },
360
361
  region: { label: 'Region', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, hideTablet: true },
@@ -418,7 +419,7 @@ export const MOCK_CUSTOMERS: Record<string, unknown>[] = [
418
419
  // ===== APPLICATION DOMAIN =====
419
420
 
420
421
  const APPLICATION_COLUMNS: Record<string, ColumnDef> = {
421
- favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true },
422
+ favorite: { label: 'Favorit', type: 'favorite', sortable: false, filterable: false, visible: true, headerIcon: 'star', headerTooltip: 'Favorit' },
422
423
  name: { label: 'Applikation', type: 'double-text', sortable: true, filterable: false, visible: true, secondary: 'description', searchable: true },
423
424
  url: { label: 'Referenz', type: 'link', sortable: false, filterable: false, visible: true, hideMobile: true },
424
425
  trends: { label: 'Trend', type: 'text', sortable: true, filterable: true, primaryFilter: true, visible: true, hideTablet: true, searchable: true },
@@ -63,7 +63,7 @@ export function useDataExplorerState(
63
63
  initialDomain: DomainKey = 'products',
64
64
  ): DataExplorerState {
65
65
  const [activeDomain, setActiveDomainRaw] = useState<DomainKey>(initialDomain)
66
- const [viewMode, setViewMode] = useState<ViewMode>('table')
66
+ const [viewMode, setViewModeRaw] = useState<ViewMode>('table')
67
67
  const [sort, setSort] = useState<SortState>({ column: null, direction: 'asc' })
68
68
  const [filters, setFilters] = useState<FilterState>({})
69
69
  const [searchTerm, setSearchTermRaw] = useState('')
@@ -79,6 +79,11 @@ export function useDataExplorerState(
79
79
  [activeDomain],
80
80
  )
81
81
 
82
+ const setViewMode = useCallback((mode: ViewMode) => {
83
+ setViewModeRaw(mode)
84
+ setSort({ column: null, direction: 'asc' })
85
+ }, [])
86
+
82
87
  const setActiveDomain = useCallback((domain: DomainKey) => {
83
88
  setActiveDomainRaw(domain)
84
89
  setSort({ column: null, direction: 'asc' })
@@ -1,3 +1,5 @@
1
+ import type React from 'react'
2
+
1
3
  export type DomainKey = 'products' | 'projects' | 'customers' | 'applications'
2
4
 
3
5
  export type CellType =
@@ -10,6 +12,7 @@ export type CellType =
10
12
  | 'favorite'
11
13
  | 'expand'
12
14
  | 'score-bar'
15
+ | 'actions'
13
16
 
14
17
  export type ViewMode = 'table' | 'list' | 'card'
15
18
 
@@ -32,6 +35,17 @@ export interface ExpandColumnDef {
32
35
  type?: CellType
33
36
  }
34
37
 
38
+ export interface RenderCellOptions {
39
+ mode?: 'default' | 'compact' | 'inventory-label'
40
+ isExpanded?: boolean
41
+ isFavorite?: boolean
42
+ onToggleExpand?: () => void
43
+ onToggleFavorite?: () => void
44
+ onAddToProject?: () => void
45
+ onAddToCollection?: () => void
46
+ onDelete?: () => void
47
+ }
48
+
35
49
  export interface ColumnDef {
36
50
  label: string
37
51
  type: CellType
@@ -39,6 +53,8 @@ export interface ColumnDef {
39
53
  filterable: boolean
40
54
  primaryFilter?: boolean
41
55
  visible: boolean
56
+ /** When false, column is excluded from the column-config popover (always visible, not reorderable) */
57
+ configurable?: boolean
42
58
  searchable?: boolean
43
59
  secondary?: string
44
60
  currencyField?: string
@@ -57,6 +73,9 @@ export interface ColumnDef {
57
73
  hideTablet?: boolean
58
74
  hideMobile?: boolean
59
75
  rowLines?: number
76
+ /** Min-width in px for badge column alignment in DataListView. Default: 96 */
77
+ badgeColumnWidth?: number
78
+ render?: (val: unknown, row: Record<string, unknown>, opts: RenderCellOptions) => React.ReactNode
60
79
  }
61
80
 
62
81
  export interface ListLayout {