@umituz/web-design-system 1.5.1 → 1.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "1.5.1",
3
+ "version": "1.7.1",
4
4
  "private": false,
5
5
  "description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
6
6
  "main": "./src/index.ts",
@@ -53,14 +53,27 @@
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@radix-ui/react-accordion": ">=1.0.0",
56
+ "@radix-ui/react-collapsible": ">=1.0.0",
57
+ "@radix-ui/react-dialog": ">=1.0.0",
58
+ "@radix-ui/react-hover-card": ">=1.0.0",
59
+ "@radix-ui/react-label": ">=2.0.0",
60
+ "@radix-ui/react-popover": ">=1.0.0",
61
+ "@radix-ui/react-scroll-area": ">=1.0.0",
56
62
  "@radix-ui/react-select": ">=2.0.0",
57
63
  "clsx": ">=2.0.0",
64
+ "lucide-react": ">=0.400.0",
58
65
  "react": ">=18.0.0",
59
66
  "react-dom": ">=18.0.0",
60
67
  "tailwind-merge": ">=2.0.0"
61
68
  },
62
69
  "devDependencies": {
63
70
  "@radix-ui/react-accordion": "^1.2.12",
71
+ "@radix-ui/react-collapsible": "^1.1.12",
72
+ "@radix-ui/react-dialog": "^1.1.15",
73
+ "@radix-ui/react-hover-card": "^1.1.8",
74
+ "@radix-ui/react-label": "^2.1.8",
75
+ "@radix-ui/react-popover": "^1.1.15",
76
+ "@radix-ui/react-scroll-area": "^1.2.10",
64
77
  "@radix-ui/react-select": "^2.2.6",
65
78
  "@types/react": "^18.0.0",
66
79
  "@types/react-dom": "^18.0.0",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Label Component (Atom)
3
+ * @description Form label (Shadcn/ui compatible)
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as LabelPrimitive from '@radix-ui/react-label';
8
+ import { cn } from '../../infrastructure/utils';
9
+
10
+ const Label = React.forwardRef<
11
+ React.ElementRef<typeof LabelPrimitive.Root>,
12
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
13
+ >(({ className, ...props }, ref) => (
14
+ <LabelPrimitive.Root
15
+ ref={ref}
16
+ className={cn(
17
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ));
23
+ Label.displayName = LabelPrimitive.Root.displayName;
24
+
25
+ export { Label };
@@ -45,3 +45,5 @@ export type { TooltipProps } from './Tooltip';
45
45
 
46
46
  export { Progress } from './Progress';
47
47
  export type { ProgressProps } from './Progress';
48
+
49
+ export { Label } from './Label';
@@ -0,0 +1,128 @@
1
+ /**
2
+ * ListItem Component (Molecule)
3
+ * @description Reusable list item with icon, content, and actions
4
+ * Reduces boilerplate in list components throughout the app
5
+ */
6
+
7
+ import { forwardRef, type ReactNode, type MouseEvent } from 'react';
8
+ import { cn } from '../../infrastructure/utils';
9
+ import { Button } from '../atoms';
10
+ import type { BaseProps } from '../../domain/types';
11
+
12
+ export interface ListItemProps extends BaseProps {
13
+ title: string;
14
+ description?: string;
15
+ icon?: ReactNode;
16
+ actions?: ReactNode;
17
+ leftContent?: ReactNode;
18
+ rightContent?: ReactNode;
19
+ onClick?: () => void;
20
+ href?: string;
21
+ disabled?: boolean;
22
+ selected?: boolean;
23
+ size?: 'sm' | 'md' | 'lg';
24
+ variant?: 'default' | 'bordered' | 'ghost';
25
+ }
26
+
27
+ const sizeStyles = {
28
+ sm: 'p-3 gap-3',
29
+ md: 'p-4 gap-4',
30
+ lg: 'p-5 gap-5',
31
+ };
32
+
33
+ const titleSizeStyles = {
34
+ sm: 'text-sm',
35
+ md: 'text-base',
36
+ lg: 'text-lg',
37
+ };
38
+
39
+ const descriptionSizeStyles = {
40
+ sm: 'text-xs',
41
+ md: 'text-sm',
42
+ lg: 'text-base',
43
+ };
44
+
45
+ const iconSizeStyles = {
46
+ sm: 'h-4 w-4',
47
+ md: 'h-5 w-5',
48
+ lg: 'h-6 w-6',
49
+ };
50
+
51
+ export const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
52
+ (
53
+ {
54
+ className,
55
+ title,
56
+ description,
57
+ icon,
58
+ actions,
59
+ leftContent,
60
+ rightContent,
61
+ onClick,
62
+ href,
63
+ disabled = false,
64
+ selected = false,
65
+ size = 'md',
66
+ variant = 'default',
67
+ ...props
68
+ },
69
+ ref
70
+ ) => {
71
+ const baseClasses = cn(
72
+ 'flex items-center justify-between w-full transition-all duration-200',
73
+ sizeStyles[size],
74
+ !disabled && onClick && 'cursor-pointer hover:bg-muted/50 active:scale-[0.98]',
75
+ selected && 'bg-muted/50 border-primary',
76
+ variant === 'bordered' && 'border border-border rounded-lg',
77
+ variant === 'ghost' && 'hover:bg-muted/30',
78
+ disabled && 'opacity-50 cursor-not-allowed',
79
+ className
80
+ );
81
+
82
+ const content = (
83
+ <>
84
+ {leftContent || icon ? (
85
+ <div className="flex items-center gap-3 flex-shrink-0">
86
+ {icon && <div className={cn(iconSizeStyles[size], 'text-muted-foreground')}>{icon}</div>}
87
+ {!icon && leftContent}
88
+ </div>
89
+ ) : null}
90
+
91
+ <div className="flex-1 min-w-0">
92
+ <p className={cn('font-medium text-foreground truncate', titleSizeStyles[size])}>{title}</p>
93
+ {description && (
94
+ <p className={cn('text-muted-foreground truncate', descriptionSizeStyles[size])}>{description}</p>
95
+ )}
96
+ </div>
97
+
98
+ {rightContent || actions ? (
99
+ <div className="flex items-center gap-2 flex-shrink-0">
100
+ {rightContent}
101
+ {actions}
102
+ </div>
103
+ ) : null}
104
+ </>
105
+ );
106
+
107
+ if (href && !disabled) {
108
+ return (
109
+ <a href={href} ref={ref as any} className={baseClasses} {...(props as any)}>
110
+ {content}
111
+ </a>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <div
117
+ ref={ref}
118
+ className={baseClasses}
119
+ onClick={disabled ? undefined : onClick}
120
+ {...props}
121
+ >
122
+ {content}
123
+ </div>
124
+ );
125
+ }
126
+ );
127
+
128
+ ListItem.displayName = 'ListItem';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ScrollArea Component (Molecule)
3
+ * @description Custom scrollable area (Shadcn/ui compatible)
4
+ */
5
+
6
+ import * as React from 'react';
7
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
8
+ import { cn } from '../../infrastructure/utils';
9
+
10
+ const ScrollArea = React.forwardRef<
11
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
12
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
13
+ >(({ className, children, ...props }, ref) => (
14
+ <ScrollAreaPrimitive.Root
15
+ ref={ref}
16
+ className={cn('relative overflow-hidden', className)}
17
+ {...props}
18
+ >
19
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
20
+ {children}
21
+ </ScrollAreaPrimitive.Viewport>
22
+ <ScrollBar />
23
+ <ScrollAreaPrimitive.Corner />
24
+ </ScrollAreaPrimitive.Root>
25
+ ));
26
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
27
+
28
+ const ScrollBar = React.forwardRef<
29
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
30
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
31
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
32
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
33
+ ref={ref}
34
+ orientation={orientation}
35
+ className={cn(
36
+ 'flex touch-none select-none transition-colors',
37
+ orientation === 'vertical' &&
38
+ 'h-full w-2.5 border-l border-l-transparent p-[1px]',
39
+ orientation === 'horizontal' &&
40
+ 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
46
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
47
+ ));
48
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
49
+
50
+ export { ScrollArea, ScrollBar };
@@ -33,3 +33,8 @@ export type { CheckboxGroupProps, CheckboxOption } from './CheckboxGroup';
33
33
 
34
34
  export { InputGroup, GroupedInput } from './InputGroup';
35
35
  export type { InputGroupProps } from './InputGroup';
36
+
37
+ export { ScrollArea, ScrollBar } from './ScrollArea';
38
+
39
+ export { ListItem } from './ListItem';
40
+ export type { ListItemProps } from './ListItem';
@@ -4,32 +4,23 @@
4
4
  */
5
5
 
6
6
  import * as React from 'react';
7
- import { cva, type VariantProps } from 'class-variance-authority';
8
7
  import { cn } from '../../infrastructure/utils';
9
8
 
10
- const alertVariants = cva(
11
- 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
12
- {
13
- variants: {
14
- variant: {
15
- default: 'bg-background text-foreground',
16
- destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
17
- },
18
- },
19
- defaultVariants: {
20
- variant: 'default',
21
- },
22
- }
23
- );
24
-
25
- export interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {}
9
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ variant?: 'default' | 'destructive';
11
+ }
26
12
 
27
13
  const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
28
- ({ className, variant, ...props }, ref) => (
14
+ ({ className, variant = 'default', ...props }, ref) => (
29
15
  <div
30
16
  ref={ref}
31
17
  role="alert"
32
- className={cn(alertVariants({ variant }), className)}
18
+ className={cn(
19
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
20
+ variant === 'default' && 'bg-background text-foreground',
21
+ variant === 'destructive' && 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
22
+ className
23
+ )}
33
24
  {...props}
34
25
  />
35
26
  )
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Collapsible Component (Organism)
3
+ * @description Collapsible content (Shadcn/ui compatible)
4
+ */
5
+
6
+ import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
7
+
8
+ const Collapsible = CollapsiblePrimitive.Root;
9
+
10
+ const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
11
+
12
+ const CollapsibleContent = CollapsiblePrimitive.Content;
13
+
14
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ConfirmDialog Component (Organism)
3
+ * @description Confirmation dialog for destructive actions
4
+ * Reduces boilerplate in delete/remove confirmation modals
5
+ */
6
+
7
+ import { forwardRef, type ReactNode } from 'react';
8
+ import { cn } from '../../infrastructure/utils';
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from './Dialog';
17
+ import { Button } from '../atoms';
18
+ import { AlertTriangle } from 'lucide-react';
19
+ import type { BaseProps } from '../../domain/types';
20
+
21
+ export interface ConfirmDialogProps extends BaseProps {
22
+ open: boolean;
23
+ onOpenChange: (open: boolean) => void;
24
+ title: string;
25
+ message?: string;
26
+ description?: string;
27
+ onConfirm: () => void | Promise<void>;
28
+ loading?: boolean;
29
+ confirmText?: string;
30
+ cancelText?: string;
31
+ variant?: 'default' | 'destructive';
32
+ icon?: ReactNode;
33
+ }
34
+
35
+ export const ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(
36
+ (
37
+ {
38
+ className,
39
+ open,
40
+ onOpenChange,
41
+ title,
42
+ message,
43
+ description,
44
+ onConfirm,
45
+ loading = false,
46
+ confirmText = 'Confirm',
47
+ cancelText = 'Cancel',
48
+ variant = 'destructive',
49
+ icon,
50
+ ...props
51
+ },
52
+ ref
53
+ ) => {
54
+ const handleConfirm = async () => {
55
+ if (!loading) {
56
+ await onConfirm();
57
+ onOpenChange(false);
58
+ }
59
+ };
60
+
61
+ return (
62
+ <Dialog open={open} onOpenChange={onOpenChange}>
63
+ <DialogContent className={cn('sm:max-w-[425px]', className)} ref={ref} {...props}>
64
+ <DialogHeader>
65
+ <div className="flex items-center gap-3">
66
+ {icon || (
67
+ variant === 'destructive' && (
68
+ <div className="w-10 h-10 rounded-full bg-destructive/10 flex items-center justify-center">
69
+ <AlertTriangle className="h-5 w-5 text-destructive" />
70
+ </div>
71
+ )
72
+ )}
73
+ <DialogTitle>{title}</DialogTitle>
74
+ </div>
75
+ {description && <DialogDescription>{description}</DialogDescription>}
76
+ </DialogHeader>
77
+ {message && (
78
+ <div className="py-4">
79
+ <p className="text-sm text-muted-foreground">{message}</p>
80
+ </div>
81
+ )}
82
+ <DialogFooter>
83
+ <Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
84
+ {cancelText}
85
+ </Button>
86
+ <Button variant={variant} onClick={handleConfirm} disabled={loading}>
87
+ {loading && <span className="mr-2 h-4 w-4 animate-spin">⟳</span>}
88
+ {confirmText}
89
+ </Button>
90
+ </DialogFooter>
91
+ </DialogContent>
92
+ </Dialog>
93
+ );
94
+ }
95
+ );
96
+
97
+ ConfirmDialog.displayName = 'ConfirmDialog';
@@ -0,0 +1,233 @@
1
+ /**
2
+ * DataTable Component (Organism)
3
+ * @description Enhanced table component for displaying data with sorting and pagination
4
+ */
5
+
6
+ import { useState, useMemo } from 'react';
7
+ import { cn } from '../../infrastructure/utils';
8
+ import {
9
+ Table,
10
+ TableHeader,
11
+ TableBody,
12
+ TableFooter,
13
+ TableRow,
14
+ TableHead,
15
+ TableCell,
16
+ TableCaption,
17
+ } from './Table';
18
+ import { Button } from '../atoms/Button';
19
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
20
+ import type { BaseProps, SizeVariant, ColorVariant } from '../../domain/types';
21
+
22
+ export interface Column<T> {
23
+ id: string;
24
+ header: string;
25
+ accessor: keyof T | ((row: T) => React.ReactNode);
26
+ cell?: (row: T) => React.ReactNode;
27
+ sortable?: boolean;
28
+ className?: string;
29
+ }
30
+
31
+ export interface DataTableProps<T> extends BaseProps {
32
+ data: T[];
33
+ columns: Column<T>[];
34
+ caption?: string;
35
+ size?: Extract<SizeVariant, 'sm' | 'md' | 'lg'>;
36
+ variant?: ColorVariant;
37
+ sortable?: boolean;
38
+ paginated?: boolean;
39
+ pageSize?: number;
40
+ emptyState?: {
41
+ title: string;
42
+ description?: string;
43
+ };
44
+ onRowClick?: (row: T) => void;
45
+ }
46
+
47
+ export function DataTable<T extends Record<string, unknown>>({
48
+ className,
49
+ data,
50
+ columns,
51
+ caption,
52
+ size = 'md',
53
+ variant = 'primary',
54
+ sortable = false,
55
+ paginated = false,
56
+ pageSize = 10,
57
+ emptyState,
58
+ onRowClick,
59
+ ...props
60
+ }: DataTableProps<T>) {
61
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
62
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
63
+ const [currentPage, setCurrentPage] = useState(1);
64
+
65
+ const handleSort = (columnId: string) => {
66
+ if (!sortable) return;
67
+
68
+ if (sortColumn === columnId) {
69
+ setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
70
+ } else {
71
+ setSortColumn(columnId);
72
+ setSortDirection('asc');
73
+ }
74
+ };
75
+
76
+ const sortedData = useMemo(() => {
77
+ if (!sortColumn || !sortable) return data;
78
+
79
+ return [...data].sort((a, b) => {
80
+ const column = columns.find((col) => col.id === sortColumn);
81
+ if (!column) return 0;
82
+
83
+ const aValue = typeof column.accessor === 'function' ? column.accessor(a) : a[column.accessor];
84
+ const bValue = typeof column.accessor === 'function' ? column.accessor(b) : b[column.accessor];
85
+
86
+ if (aValue === bValue) return 0;
87
+
88
+ const comparison = aValue < bValue ? -1 : 1;
89
+ return sortDirection === 'asc' ? comparison : -comparison;
90
+ });
91
+ }, [data, sortColumn, sortDirection, columns, sortable]);
92
+
93
+ const paginatedData = useMemo(() => {
94
+ if (!paginated) return sortedData;
95
+
96
+ const start = (currentPage - 1) * pageSize;
97
+ const end = start + pageSize;
98
+ return sortedData.slice(start, end);
99
+ }, [sortedData, currentPage, paginated, pageSize]);
100
+
101
+ const totalPages = Math.ceil(data.length / pageSize);
102
+
103
+ const renderCell = (row: T, column: Column<T>) => {
104
+ if (column.cell) {
105
+ return column.cell(row);
106
+ }
107
+
108
+ const value = typeof column.accessor === 'function' ? column.accessor(row) : row[column.accessor];
109
+ return value as React.ReactNode;
110
+ };
111
+
112
+ const sizeStyles = {
113
+ sm: 'text-xs',
114
+ md: 'text-sm',
115
+ lg: 'text-base',
116
+ };
117
+
118
+ const paddingStyles = {
119
+ sm: 'px-2 py-2',
120
+ md: 'px-4 py-3',
121
+ lg: 'px-6 py-4',
122
+ };
123
+
124
+ if (data.length === 0 && emptyState) {
125
+ return (
126
+ <div className={cn('p-8 text-center', className)}>
127
+ <p className="font-semibold text-foreground">{emptyState.title}</p>
128
+ {emptyState.description && (
129
+ <p className="text-sm text-muted-foreground mt-1">{emptyState.description}</p>
130
+ )}
131
+ </div>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div className={cn('space-y-4', className)} {...props}>
137
+ <Table>
138
+ {caption && <TableCaption>{caption}</TableCaption>}
139
+ <TableHeader>
140
+ <TableRow>
141
+ {columns.map((column) => (
142
+ <TableHead
143
+ key={column.id}
144
+ className={cn(
145
+ sizeStyles[size],
146
+ paddingStyles[size],
147
+ column.sortable && sortable && 'cursor-pointer hover:bg-muted/50',
148
+ column.className
149
+ )}
150
+ onClick={() => column.sortable && handleSort(column.id)}
151
+ >
152
+ <div className="flex items-center gap-2">
153
+ {column.header}
154
+ {column.sortable && sortable && sortColumn === column.id && (
155
+ <span className="text-xs">{sortDirection === 'asc' ? '↑' : '↓'}</span>
156
+ )}
157
+ </div>
158
+ </TableHead>
159
+ ))}
160
+ </TableRow>
161
+ </TableHeader>
162
+ <TableBody>
163
+ {paginatedData.map((row, rowIndex) => (
164
+ <TableRow
165
+ key={rowIndex}
166
+ className={cn(onRowClick && 'cursor-pointer hover:bg-muted/50')}
167
+ onClick={() => onRowClick?.(row)}
168
+ >
169
+ {columns.map((column) => (
170
+ <TableCell
171
+ key={column.id}
172
+ className={cn(paddingStyles[size], column.className)}
173
+ >
174
+ {renderCell(row, column)}
175
+ </TableCell>
176
+ ))}
177
+ </TableRow>
178
+ ))}
179
+ </TableBody>
180
+ {paginated && (
181
+ <TableFooter>
182
+ <TableRow>
183
+ <TableCell colSpan={columns.length}>
184
+ <div className="flex items-center justify-between">
185
+ <p className={cn('text-muted-foreground', sizeStyles[size])}>
186
+ Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, data.length)} of {data.length} results
187
+ </p>
188
+ <div className="flex items-center gap-2">
189
+ <Button
190
+ size="sm"
191
+ variant="outline"
192
+ onClick={() => setCurrentPage(1)}
193
+ disabled={currentPage === 1}
194
+ >
195
+ <ChevronsLeft className="h-4 w-4" />
196
+ </Button>
197
+ <Button
198
+ size="sm"
199
+ variant="outline"
200
+ onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
201
+ disabled={currentPage === 1}
202
+ >
203
+ <ChevronLeft className="h-4 w-4" />
204
+ </Button>
205
+ <span className={cn('text-sm font-medium', sizeStyles[size])}>
206
+ Page {currentPage} of {totalPages}
207
+ </span>
208
+ <Button
209
+ size="sm"
210
+ variant="outline"
211
+ onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
212
+ disabled={currentPage === totalPages}
213
+ >
214
+ <ChevronRight className="h-4 w-4" />
215
+ </Button>
216
+ <Button
217
+ size="sm"
218
+ variant="outline"
219
+ onClick={() => setCurrentPage(totalPages)}
220
+ disabled={currentPage === totalPages}
221
+ >
222
+ <ChevronsRight className="h-4 w-4" />
223
+ </Button>
224
+ </div>
225
+ </div>
226
+ </TableCell>
227
+ </TableRow>
228
+ </TableFooter>
229
+ )}
230
+ </Table>
231
+ </div>
232
+ );
233
+ }