@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.
- package/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +20 -0
- package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +9 -0
- package/dist/components/ProgressCircle/ProgressCircle.d.ts +9 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/input-group.d.ts +1 -1
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/components/ui/tabs.d.ts +1 -1
- package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +136 -0
- package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +179 -0
- package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +190 -0
- package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +43 -0
- package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +111 -0
- package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +282 -0
- package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +283 -0
- package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +41 -0
- package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +139 -0
- package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +230 -0
- package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +33 -0
- package/dist/examples/data-visualization/cell-renderers.tsx +119 -0
- package/dist/examples/data-visualization/constants.ts +1251 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +65 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +40 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +41 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +61 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +32 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +32 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +133 -0
- package/dist/examples/data-visualization/hooks/use-filtered-data.ts +84 -0
- package/dist/examples/data-visualization/index.ts +10 -0
- package/dist/examples/data-visualization/types.ts +103 -0
- package/dist/fastnd-components.js +18759 -15519
- 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?: "
|
|
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?: "
|
|
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-
|
|
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?: "
|
|
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?: "
|
|
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 }
|