@fastnd/components 1.0.27 → 1.0.28

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 (34) hide show
  1. package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +20 -0
  2. package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +9 -0
  3. package/dist/components/ProgressCircle/ProgressCircle.d.ts +9 -0
  4. package/dist/components/index.d.ts +3 -0
  5. package/dist/components/ui/badge.d.ts +1 -1
  6. package/dist/components/ui/button.d.ts +1 -1
  7. package/dist/components/ui/input-group.d.ts +1 -1
  8. package/dist/components/ui/item.d.ts +1 -1
  9. package/dist/components/ui/tabs.d.ts +1 -1
  10. package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +136 -0
  11. package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +179 -0
  12. package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +190 -0
  13. package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +43 -0
  14. package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +111 -0
  15. package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +282 -0
  16. package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +283 -0
  17. package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +41 -0
  18. package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +139 -0
  19. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +230 -0
  20. package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +33 -0
  21. package/dist/examples/data-visualization/cell-renderers.tsx +119 -0
  22. package/dist/examples/data-visualization/constants.ts +1251 -0
  23. package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +65 -0
  24. package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +40 -0
  25. package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +41 -0
  26. package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +61 -0
  27. package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +32 -0
  28. package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +32 -0
  29. package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +133 -0
  30. package/dist/examples/data-visualization/hooks/use-filtered-data.ts +84 -0
  31. package/dist/examples/data-visualization/index.ts +10 -0
  32. package/dist/examples/data-visualization/types.ts +103 -0
  33. package/dist/fastnd-components.js +18759 -15519
  34. package/package.json +1 -1
@@ -0,0 +1,20 @@
1
+ import * as React from 'react';
2
+ interface ColumnItem {
3
+ key: string;
4
+ label: string;
5
+ }
6
+ interface ColumnConfigPopoverProps {
7
+ columns: ColumnItem[];
8
+ visibleColumns: Set<string>;
9
+ onVisibilityChange: (columnKey: string, visible: boolean) => void;
10
+ onReorder: (newOrder: string[]) => void;
11
+ minVisible?: number;
12
+ className?: string;
13
+ children?: React.ReactNode;
14
+ }
15
+ declare function ColumnConfigPopover({ columns, visibleColumns, onVisibilityChange, onReorder, minVisible, className, children, }: ColumnConfigPopoverProps): import("react/jsx-runtime").JSX.Element;
16
+ declare namespace ColumnConfigPopover {
17
+ var displayName: string;
18
+ }
19
+ export { ColumnConfigPopover };
20
+ export type { ColumnConfigPopoverProps, ColumnItem };
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ interface DoubleTextCellProps extends React.ComponentProps<'div'> {
3
+ primary: string;
4
+ secondary?: string;
5
+ clampLines?: number;
6
+ }
7
+ declare const DoubleTextCell: React.ForwardRefExoticComponent<Omit<DoubleTextCellProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
8
+ export { DoubleTextCell };
9
+ export type { DoubleTextCellProps };
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ interface ProgressCircleProps extends React.ComponentProps<'div'> {
3
+ value: number;
4
+ size?: number;
5
+ strokeWidth?: number;
6
+ }
7
+ declare const ProgressCircle: React.ForwardRefExoticComponent<Omit<ProgressCircleProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
8
+ export { ProgressCircle };
9
+ export type { ProgressCircleProps };
@@ -1,4 +1,7 @@
1
+ export * from './ColumnConfigPopover/ColumnConfigPopover';
2
+ export * from './DoubleTextCell/DoubleTextCell';
1
3
  export * from './FavoriteButton/FavoriteButton';
4
+ export * from './ProgressCircle/ProgressCircle';
2
5
  export * from './ui/accordion';
3
6
  export * from './ui/alert';
4
7
  export * from './ui/alert-dialog';
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const badgeVariants: (props?: ({
4
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
4
+ variant?: "link" | "secondary" | "default" | "destructive" | "outline" | "ghost" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
6
  declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
7
7
  asChild?: boolean;
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | null | undefined;
4
+ variant?: "link" | "secondary" | "default" | "destructive" | "outline" | "ghost" | null | undefined;
5
5
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import { Button } from "@/components/ui/button";
4
4
  declare function InputGroup({ className, ...props }: React.ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
5
5
  declare const inputGroupAddonVariants: (props?: ({
6
- align?: "inline-start" | "inline-end" | "block-start" | "block-end" | null | undefined;
6
+ align?: "inline-end" | "inline-start" | "block-end" | "block-start" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function InputGroupAddon({ className, align, ...props }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>): import("react/jsx-runtime").JSX.Element;
9
9
  declare const inputGroupButtonVariants: (props?: ({
@@ -11,7 +11,7 @@ declare function Item({ className, variant, size, asChild, ...props }: React.Com
11
11
  asChild?: boolean;
12
12
  }): import("react/jsx-runtime").JSX.Element;
13
13
  declare const itemMediaVariants: (props?: ({
14
- variant?: "default" | "icon" | "image" | null | undefined;
14
+ variant?: "image" | "default" | "icon" | null | undefined;
15
15
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
16
  declare function ItemMedia({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>): import("react/jsx-runtime").JSX.Element;
17
17
  declare function ItemContent({ className, ...props }: React.ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import { Tabs as TabsPrimitive } from "radix-ui";
4
4
  declare function Tabs({ className, orientation, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
5
5
  declare const tabsListVariants: (props?: ({
6
- variant?: "default" | "line" | null | undefined;
6
+ variant?: "line" | "default" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  declare function TabsList({ className, variant, ...props }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>): import("react/jsx-runtime").JSX.Element;
9
9
  declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,136 @@
1
+ import * as React from 'react'
2
+ import { useMemo } from 'react'
3
+ import { SearchX } from 'lucide-react'
4
+ import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
5
+ import { cn } from '@/lib/utils'
6
+ import { useDataGridState } from '../hooks/use-data-grid-state'
7
+ import { DataGridToolbar } from '../DataGridToolbar/DataGridToolbar'
8
+ import { DataGridTableView } from '../DataGridTableView/DataGridTableView'
9
+ import { DataGridListView } from '../DataGridListView/DataGridListView'
10
+ import { DataGridCardView } from '../DataGridCardView/DataGridCardView'
11
+ import { DataGridPagination } from '../DataGridPagination/DataGridPagination'
12
+ import type { DomainConfig } from '../types'
13
+
14
+ interface DataGridProps extends React.ComponentProps<'div'> {
15
+ domainConfig: DomainConfig
16
+ }
17
+
18
+ const DataGrid = React.forwardRef<HTMLDivElement, DataGridProps>(
19
+ ({ domainConfig, className, ...props }, ref) => {
20
+ const state = useDataGridState(domainConfig)
21
+
22
+ const filterOptions = useMemo(() => {
23
+ const opts: Record<string, string[]> = {}
24
+ for (const [key, config] of Object.entries(domainConfig.columns)) {
25
+ if (!config.filterable) continue
26
+ const uniqueValues = [
27
+ ...new Set(
28
+ domainConfig.data
29
+ .map((row) => String(row[key] ?? ''))
30
+ .filter(Boolean)
31
+ ),
32
+ ].sort()
33
+ opts[key] = uniqueValues
34
+ }
35
+ return opts
36
+ }, [domainConfig])
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ data-slot="data-grid"
42
+ className={cn('flex flex-col border border-border rounded-lg', className)}
43
+ {...props}
44
+ >
45
+ <DataGridToolbar
46
+ currentView={state.currentView}
47
+ onViewChange={state.setCurrentView}
48
+ columns={state.columnConfig}
49
+ columnOrder={state.columnOrder}
50
+ visibleColumns={state.visibleColumns}
51
+ filters={state.filters}
52
+ filterOptions={filterOptions}
53
+ onFilterChange={state.setFilter}
54
+ searchQuery={state.searchQuery}
55
+ onSearchChange={state.setSearchQuery}
56
+ hasActiveFilters={state.hasActiveFilters}
57
+ onResetFilters={state.resetFilters}
58
+ onToggleColumnVisibility={state.toggleColumnVisibility}
59
+ onReorderColumns={state.reorderColumns}
60
+ />
61
+
62
+ {state.filteredData.length === 0 ? (
63
+ <Empty className="py-16">
64
+ <EmptyHeader>
65
+ <EmptyMedia variant="icon">
66
+ <SearchX />
67
+ </EmptyMedia>
68
+ <EmptyTitle>Keine Ergebnisse</EmptyTitle>
69
+ <EmptyDescription>
70
+ Es wurden keine Einträge gefunden, die den aktuellen Filtern entsprechen.
71
+ </EmptyDescription>
72
+ </EmptyHeader>
73
+ </Empty>
74
+ ) : (
75
+ <>
76
+ {state.currentView === 'table' && (
77
+ <DataGridTableView
78
+ data={state.pagedData}
79
+ columns={state.orderedVisibleColumns}
80
+ sortColumn={state.sortColumn}
81
+ sortDirection={state.sortDirection}
82
+ onToggleSort={state.toggleSort}
83
+ expandedRows={state.expandedRows}
84
+ onToggleExpand={state.toggleExpand}
85
+ favorites={state.favorites}
86
+ onToggleFavorite={state.toggleFavorite}
87
+ columnWidths={state.columnWidths}
88
+ onResizeColumn={state.resizeColumn}
89
+ />
90
+ )}
91
+ {state.currentView === 'list' && (
92
+ <DataGridListView
93
+ data={state.pagedData}
94
+ columnConfig={state.columnConfig}
95
+ layout={state.layout.list}
96
+ visibleColumns={state.visibleColumns}
97
+ columnOrder={state.columnOrder}
98
+ favorites={state.favorites}
99
+ onToggleFavorite={state.toggleFavorite}
100
+ expandedRows={state.expandedRows}
101
+ onToggleExpand={state.toggleExpand}
102
+ />
103
+ )}
104
+ {state.currentView === 'cards' && (
105
+ <DataGridCardView
106
+ data={state.pagedData}
107
+ columnConfig={state.columnConfig}
108
+ layout={state.layout.card}
109
+ visibleColumns={state.visibleColumns}
110
+ favorites={state.favorites}
111
+ onToggleFavorite={state.toggleFavorite}
112
+ expandedRows={state.expandedRows}
113
+ onToggleExpand={state.toggleExpand}
114
+ />
115
+ )}
116
+ </>
117
+ )}
118
+
119
+ <DataGridPagination
120
+ currentPage={state.currentPage}
121
+ totalPages={state.totalPages}
122
+ pageSize={state.pageSize}
123
+ totalFiltered={state.filteredCount}
124
+ label={domainConfig.resultLabel}
125
+ onPageChange={state.setCurrentPage}
126
+ onPageSizeChange={state.setPageSize}
127
+ />
128
+ </div>
129
+ )
130
+ }
131
+ )
132
+
133
+ DataGrid.displayName = 'DataGrid'
134
+
135
+ export { DataGrid }
136
+ export type { DataGridProps }
@@ -0,0 +1,179 @@
1
+ import * as React from 'react'
2
+ import { ChevronDown } from 'lucide-react'
3
+ import { Card } from '@/components/ui/card'
4
+ import { Button } from '@/components/ui/button'
5
+ import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
6
+ import { cellRenderers } from '../cell-renderers'
7
+ import { cn } from '@/lib/utils'
8
+ import type { CardLayoutConfig, CellType, ColumnConfig, DataRow } from '../types'
9
+
10
+ const BADGE_TYPES = new Set<CellType>(['status-badge', 'inventory', 'inventory-label'])
11
+ const SKIP_TYPES = new Set<CellType>(['favorite'])
12
+
13
+ interface DataGridCardViewProps extends React.ComponentProps<'div'> {
14
+ data: DataRow[]
15
+ columnConfig: Record<string, ColumnConfig>
16
+ layout: CardLayoutConfig
17
+ visibleColumns: Set<string>
18
+ favorites: Set<string>
19
+ onToggleFavorite: (rowId: string) => void
20
+ expandedRows: Set<string>
21
+ onToggleExpand: (rowId: string, expandKey: string) => void
22
+ }
23
+
24
+ // Finds extra visible badge columns not already listed in layout.badgeFields.
25
+ function deriveExtraBadgeFields(
26
+ columnConfig: Record<string, ColumnConfig>,
27
+ visibleColumns: Set<string>,
28
+ layout: CardLayoutConfig
29
+ ): string[] {
30
+ const layoutFields = new Set([
31
+ layout.titleField,
32
+ ...(layout.subtitleField ? [layout.subtitleField] : []),
33
+ ...layout.badgeFields,
34
+ ...layout.rows.map((r) => r.field),
35
+ ...(layout.footerField ? [layout.footerField] : []),
36
+ ...(layout.expandField ? [layout.expandField] : []),
37
+ ])
38
+
39
+ const extra: string[] = []
40
+ for (const key of visibleColumns) {
41
+ if (layoutFields.has(key)) continue
42
+ const config = columnConfig[key]
43
+ if (!config) continue
44
+ if (SKIP_TYPES.has(config.type)) continue
45
+ if (BADGE_TYPES.has(config.type)) {
46
+ extra.push(key)
47
+ }
48
+ }
49
+ return extra
50
+ }
51
+
52
+ const DataGridCardView = React.forwardRef<HTMLDivElement, DataGridCardViewProps>(
53
+ (
54
+ {
55
+ data,
56
+ columnConfig,
57
+ layout,
58
+ visibleColumns,
59
+ favorites,
60
+ onToggleFavorite,
61
+ expandedRows,
62
+ onToggleExpand,
63
+ className,
64
+ ...props
65
+ },
66
+ ref
67
+ ) => {
68
+ const extraBadgeFields = deriveExtraBadgeFields(columnConfig, visibleColumns, layout)
69
+ const allBadgeFields = [...layout.badgeFields, ...extraBadgeFields]
70
+
71
+ return (
72
+ <div
73
+ ref={ref}
74
+ data-slot="data-grid-card-view"
75
+ className={cn(
76
+ 'grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 p-5',
77
+ className
78
+ )}
79
+ {...props}
80
+ >
81
+ {data.map((row) => {
82
+ const titleValue = String(row[layout.titleField] ?? '')
83
+ const expandKey = layout.expandField
84
+ const expandItems = expandKey && Array.isArray(row[expandKey])
85
+ ? (row[expandKey] as unknown[])
86
+ : []
87
+ const compositeKey = expandKey ? `${row.id}::${expandKey}` : ''
88
+ const isExpanded = compositeKey ? expandedRows.has(compositeKey) : false
89
+
90
+ return (
91
+ <Card
92
+ key={row.id}
93
+ className="flex flex-col gap-3 p-4 hover:shadow-md transition-shadow"
94
+ >
95
+ {/* Header */}
96
+ <div className="flex items-start justify-between">
97
+ <div className="min-w-0">
98
+ <div className="font-medium text-sm truncate">{titleValue}</div>
99
+ {layout.subtitleField && (
100
+ <div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
101
+ {String(row[layout.subtitleField] ?? '')}
102
+ </div>
103
+ )}
104
+ </div>
105
+ <FavoriteButton
106
+ pressed={favorites.has(row.id)}
107
+ projectName={titleValue}
108
+ onPressedChange={() => onToggleFavorite(row.id)}
109
+ />
110
+ </div>
111
+
112
+ {/* Badges */}
113
+ {allBadgeFields.length > 0 && (
114
+ <div className="flex gap-1.5 flex-wrap">
115
+ {allBadgeFields.map((field) => {
116
+ const config = columnConfig[field]
117
+ if (!config) return null
118
+ const rendererType: CellType =
119
+ config.type === 'inventory' ? 'inventory-label' : config.type
120
+ return (
121
+ <React.Fragment key={field}>
122
+ {cellRenderers[rendererType]?.(row[field], row, config)}
123
+ </React.Fragment>
124
+ )
125
+ })}
126
+ </div>
127
+ )}
128
+
129
+ {/* Data rows */}
130
+ {layout.rows.map((rowConfig) => {
131
+ const config = columnConfig[rowConfig.field]
132
+ if (!config) return null
133
+ const rendererType: CellType = rowConfig.rendererOverride ?? config.type
134
+ return (
135
+ <div key={rowConfig.field} className="flex justify-between items-center">
136
+ <span className="text-xs uppercase tracking-wider text-muted-foreground">
137
+ {rowConfig.label}
138
+ </span>
139
+ <span className="text-sm font-medium font-[family-name:var(--font-grotesk)]">
140
+ {cellRenderers[rendererType]?.(row[rowConfig.field], row, config)}
141
+ </span>
142
+ </div>
143
+ )
144
+ })}
145
+
146
+ {/* Footer */}
147
+ {layout.footerField && (
148
+ <div className="flex items-center justify-between pt-2.5 border-t border-border">
149
+ <span className="text-xs text-primary font-medium">
150
+ {String(row[layout.footerField] ?? '')}
151
+ </span>
152
+ {expandKey && (
153
+ <Button
154
+ variant="ghost"
155
+ size="sm"
156
+ onClick={() => onToggleExpand(row.id, expandKey)}
157
+ aria-expanded={isExpanded}
158
+ >
159
+ <ChevronDown
160
+ className={cn('transition-transform', isExpanded && 'rotate-180')}
161
+ size={14}
162
+ />
163
+ {expandItems.length}
164
+ </Button>
165
+ )}
166
+ </div>
167
+ )}
168
+ </Card>
169
+ )
170
+ })}
171
+ </div>
172
+ )
173
+ }
174
+ )
175
+
176
+ DataGridCardView.displayName = 'DataGridCardView'
177
+
178
+ export { DataGridCardView }
179
+ export type { DataGridCardViewProps }
@@ -0,0 +1,190 @@
1
+ import * as React from 'react'
2
+ import { ChevronDown } from 'lucide-react'
3
+ import { Button } from '@/components/ui/button'
4
+ import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
5
+ import { cellRenderers } from '../cell-renderers'
6
+ import { cn } from '@/lib/utils'
7
+ import type { ColumnConfig, DataRow, ListLayoutConfig } from '../types'
8
+
9
+ const BADGE_TYPES = new Set(['status-badge', 'inventory', 'inventory-label'])
10
+ const SKIP_TYPES = new Set(['favorite'])
11
+
12
+ interface DataGridListViewProps extends React.ComponentProps<'div'> {
13
+ data: DataRow[]
14
+ columnConfig: Record<string, ColumnConfig>
15
+ layout: ListLayoutConfig
16
+ visibleColumns: Set<string>
17
+ columnOrder: string[]
18
+ favorites: Set<string>
19
+ onToggleFavorite: (rowId: string) => void
20
+ expandedRows: Set<string>
21
+ onToggleExpand: (rowId: string, expandKey: string) => void
22
+ }
23
+
24
+ // Derives the extra visible columns not already covered by the layout fields.
25
+ function deriveExtraFields(
26
+ columnConfig: Record<string, ColumnConfig>,
27
+ visibleColumns: Set<string>,
28
+ columnOrder: string[],
29
+ layout: ListLayoutConfig
30
+ ): { extraMeta: string[]; extraBadge: string[]; extraExpand: string[] } {
31
+ const layoutFields = new Set([
32
+ layout.titleField,
33
+ ...layout.metaFields,
34
+ ...layout.badgeFields,
35
+ ...(layout.valueField ? [layout.valueField] : []),
36
+ ...(layout.expandField ? [layout.expandField] : []),
37
+ ])
38
+
39
+ const extraMeta: string[] = []
40
+ const extraBadge: string[] = []
41
+ const extraExpand: string[] = []
42
+
43
+ for (const key of columnOrder) {
44
+ if (!visibleColumns.has(key) || layoutFields.has(key)) continue
45
+ const config = columnConfig[key]
46
+ if (!config) continue
47
+ if (SKIP_TYPES.has(config.type)) continue
48
+ if (BADGE_TYPES.has(config.type)) {
49
+ extraBadge.push(key)
50
+ } else if (config.type === 'expand') {
51
+ extraExpand.push(key)
52
+ } else {
53
+ extraMeta.push(key)
54
+ }
55
+ }
56
+
57
+ return { extraMeta, extraBadge, extraExpand }
58
+ }
59
+
60
+ const DataGridListView = React.forwardRef<HTMLDivElement, DataGridListViewProps>(
61
+ (
62
+ {
63
+ data,
64
+ columnConfig,
65
+ layout,
66
+ visibleColumns,
67
+ columnOrder,
68
+ favorites,
69
+ onToggleFavorite,
70
+ expandedRows,
71
+ onToggleExpand,
72
+ className,
73
+ ...props
74
+ },
75
+ ref
76
+ ) => {
77
+ const { extraMeta, extraBadge, extraExpand } = deriveExtraFields(
78
+ columnConfig,
79
+ visibleColumns,
80
+ columnOrder,
81
+ layout
82
+ )
83
+
84
+ const allMetaFields = [...layout.metaFields, ...extraMeta]
85
+ const allBadgeFields = [...layout.badgeFields, ...extraBadge]
86
+ const allExpandFields = [
87
+ ...(layout.expandField ? [layout.expandField] : []),
88
+ ...extraExpand,
89
+ ]
90
+
91
+ return (
92
+ <div
93
+ ref={ref}
94
+ data-slot="data-grid-list-view"
95
+ className={cn('divide-y divide-border', className)}
96
+ {...props}
97
+ >
98
+ {data.map((row) => {
99
+ const titleValue = String(row[layout.titleField] ?? '')
100
+
101
+ return (
102
+ <div
103
+ key={row.id}
104
+ className="flex items-center gap-3 px-5 py-3 hover:bg-muted/50 transition-colors"
105
+ >
106
+ <FavoriteButton
107
+ pressed={favorites.has(row.id)}
108
+ projectName={titleValue}
109
+ onPressedChange={() => onToggleFavorite(row.id)}
110
+ />
111
+
112
+ <div className="flex-1 min-w-0">
113
+ <div className="font-medium text-sm truncate">{titleValue}</div>
114
+
115
+ {allMetaFields.length > 0 && (
116
+ <div className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap">
117
+ {allMetaFields.map((field, index) => {
118
+ const config = columnConfig[field]
119
+ if (!config) return null
120
+ const rendered = cellRenderers[config.type]?.(
121
+ row[field],
122
+ row,
123
+ config
124
+ )
125
+ return (
126
+ <React.Fragment key={field}>
127
+ {index > 0 && <span aria-hidden="true">·</span>}
128
+ <span>{rendered}</span>
129
+ </React.Fragment>
130
+ )
131
+ })}
132
+ </div>
133
+ )}
134
+ </div>
135
+
136
+ {allBadgeFields.length > 0 && (
137
+ <div className="flex items-center gap-1.5 shrink-0">
138
+ {allBadgeFields.map((field) => {
139
+ const config = columnConfig[field]
140
+ if (!config) return null
141
+ // Use inventory-label instead of inventory in list view for readability
142
+ const rendererType =
143
+ config.type === 'inventory' ? 'inventory-label' : config.type
144
+ return (
145
+ <React.Fragment key={field}>
146
+ {cellRenderers[rendererType]?.(row[field], row, config)}
147
+ </React.Fragment>
148
+ )
149
+ })}
150
+ </div>
151
+ )}
152
+
153
+ {allExpandFields.length > 0 && (
154
+ <div className="shrink-0 flex items-center gap-1">
155
+ {allExpandFields.map((expandKey) => {
156
+ const config = columnConfig[expandKey]
157
+ if (!config) return null
158
+ const items = Array.isArray(row[expandKey]) ? (row[expandKey] as unknown[]) : []
159
+ const compositeKey = `${row.id}::${expandKey}`
160
+ const isExpanded = expandedRows.has(compositeKey)
161
+ return (
162
+ <Button
163
+ key={expandKey}
164
+ variant="ghost"
165
+ size="sm"
166
+ onClick={() => onToggleExpand(row.id, expandKey)}
167
+ aria-expanded={isExpanded}
168
+ >
169
+ <ChevronDown
170
+ className={cn('transition-transform', isExpanded && 'rotate-180')}
171
+ size={14}
172
+ />
173
+ {items.length}
174
+ </Button>
175
+ )
176
+ })}
177
+ </div>
178
+ )}
179
+ </div>
180
+ )
181
+ })}
182
+ </div>
183
+ )
184
+ }
185
+ )
186
+
187
+ DataGridListView.displayName = 'DataGridListView'
188
+
189
+ export { DataGridListView }
190
+ export type { DataGridListViewProps }
@@ -0,0 +1,43 @@
1
+ import * as React from 'react'
2
+ import { useState } from 'react'
3
+ import { cn } from '@/lib/utils'
4
+ import { DomainSwitcher } from '../DomainSwitcher/DomainSwitcher'
5
+ import { DataGrid } from '../DataGrid/DataGrid'
6
+ import type { DomainConfig } from '../types'
7
+
8
+ interface DataGridPageProps extends React.ComponentProps<'div'> {
9
+ domains: DomainConfig[]
10
+ }
11
+
12
+ const DataGridPage = React.forwardRef<HTMLDivElement, DataGridPageProps>(
13
+ ({ domains, className, ...props }, ref) => {
14
+ const [activeDomain, setActiveDomain] = useState(domains[0]?.key ?? '')
15
+ const activeDomainConfig = domains.find((d) => d.key === activeDomain) ?? domains[0]
16
+
17
+ return (
18
+ <div
19
+ ref={ref}
20
+ data-slot="data-grid-page"
21
+ className={cn('flex flex-col gap-4', className)}
22
+ {...props}
23
+ >
24
+ <DomainSwitcher
25
+ domains={domains.map((d) => ({ key: d.key, label: d.label }))}
26
+ activeDomain={activeDomain}
27
+ onDomainChange={setActiveDomain}
28
+ />
29
+ {activeDomainConfig && (
30
+ <DataGrid
31
+ key={activeDomainConfig.key}
32
+ domainConfig={activeDomainConfig}
33
+ />
34
+ )}
35
+ </div>
36
+ )
37
+ }
38
+ )
39
+
40
+ DataGridPage.displayName = 'DataGridPage'
41
+
42
+ export { DataGridPage }
43
+ export type { DataGridPageProps }