@umituz/web-design-system 1.3.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.
@@ -1,117 +1,59 @@
1
1
  /**
2
2
  * Accordion Component (Organism)
3
- * @description Collapsible content sections
3
+ * @description Collapsible content sections (Shadcn/ui compatible)
4
4
  */
5
5
 
6
- import { useState, useCallback, type ReactNode, type HTMLAttributes } from 'react';
6
+ import * as React from 'react';
7
+ import * as AccordionPrimitive from '@radix-ui/react-accordion';
8
+ import { ChevronDown } from 'lucide-react';
7
9
  import { cn } from '../../infrastructure/utils';
8
- import type { BaseProps } from '../../domain/types';
9
- import { Icon } from '../atoms/Icon';
10
10
 
11
- export interface AccordionItem {
12
- value: string;
13
- title: string;
14
- content: ReactNode;
15
- disabled?: boolean;
16
- }
17
-
18
- export interface AccordionProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
19
- items: AccordionItem[];
20
- allowMultiple?: boolean;
21
- defaultValue?: string[];
22
- variant?: 'default' | 'bordered' | 'ghost';
23
- }
24
-
25
- const variantStyles: Record<'default' | 'bordered' | 'ghost', string> = {
26
- default: 'border-b',
27
- bordered: 'border rounded-lg mb-2',
28
- ghost: 'border-0',
29
- };
30
-
31
- export function Accordion({
32
- items,
33
- allowMultiple = false,
34
- defaultValue = [],
35
- variant = 'default',
36
- className,
37
- ...props
38
- }: AccordionProps) {
39
- const [openItems, setOpenItems] = useState<string[]>(defaultValue);
40
-
41
- const toggleItem = useCallback((value: string) => {
42
- setOpenItems((prev) => {
43
- const isOpen = prev.includes(value);
44
-
45
- if (allowMultiple) {
46
- return isOpen
47
- ? prev.filter((v) => v !== value)
48
- : [...prev, value];
49
- } else {
50
- return isOpen ? [] : [value];
51
- }
52
- });
53
- }, [allowMultiple]);
54
-
55
- return (
56
- <div className={cn('w-full', className)} {...props}>
57
- {items.map((item, index) => {
58
- const isOpen = openItems.includes(item.value);
59
-
60
- return (
61
- <div
62
- key={item.value}
63
- className={cn(
64
- 'group',
65
- variantStyles[variant],
66
- variant === 'bordered' && isOpen && 'ring-1 ring-ring'
67
- )}
68
- >
69
- {/* Header */}
70
- <button
71
- onClick={() => toggleItem(item.value)}
72
- disabled={item.disabled}
73
- className={cn(
74
- 'flex w-full items-center justify-between py-4 font-medium transition-all',
75
- 'hover:text-foreground',
76
- item.disabled && 'opacity-50 cursor-not-allowed',
77
- variant === 'bordered' && 'px-4'
78
- )}
79
- >
80
- <span>{item.title}</span>
81
- <Icon
82
- className={cn(
83
- 'transition-transform duration-200',
84
- isOpen && 'rotate-180'
85
- )}
86
- size="sm"
87
- >
88
- <path
89
- strokeLinecap="round"
90
- strokeLinejoin="round"
91
- d="M19.5 8.25l-7.5 7.5-7.5-7.5"
92
- />
93
- </Icon>
94
- </button>
95
-
96
- {/* Content */}
97
- {isOpen && (
98
- <div
99
- className={cn(
100
- 'overflow-hidden',
101
- 'animate-accordion-down',
102
- variant === 'bordered' && 'px-4 pb-4'
103
- )}
104
- >
105
- <div className="pb-4 text-sm text-muted-foreground">
106
- {item.content}
107
- </div>
108
- </div>
109
- )}
110
- </div>
111
- );
112
- })}
113
- </div>
114
- );
115
- }
116
-
117
- Accordion.displayName = 'Accordion';
11
+ const Accordion = AccordionPrimitive.Root;
12
+
13
+ const AccordionItem = React.forwardRef<
14
+ React.ElementRef<typeof AccordionPrimitive.Item>,
15
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
16
+ >(({ className, ...props }, ref) => (
17
+ <AccordionPrimitive.Item
18
+ ref={ref}
19
+ className={cn('border-b', className)}
20
+ {...props}
21
+ />
22
+ ));
23
+ AccordionItem.displayName = 'AccordionItem';
24
+
25
+ const AccordionTrigger = React.forwardRef<
26
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
28
+ >(({ className, children, ...props }, ref) => (
29
+ <AccordionPrimitive.Header className="flex">
30
+ <AccordionPrimitive.Trigger
31
+ ref={ref}
32
+ className={cn(
33
+ 'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
34
+ className
35
+ )}
36
+ {...props}
37
+ >
38
+ {children}
39
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
40
+ </AccordionPrimitive.Trigger>
41
+ </AccordionPrimitive.Header>
42
+ ));
43
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
44
+
45
+ const AccordionContent = React.forwardRef<
46
+ React.ElementRef<typeof AccordionPrimitive.Content>,
47
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
48
+ >(({ className, children, ...props }, ref) => (
49
+ <AccordionPrimitive.Content
50
+ ref={ref}
51
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
52
+ {...props}
53
+ >
54
+ <div className={cn('pb-4 pt-0', className)}>{children}</div>
55
+ </AccordionPrimitive.Content>
56
+ ));
57
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
58
+
59
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -1,96 +1,54 @@
1
1
  /**
2
2
  * Alert Component (Organism)
3
- * @description Feedback message with icon
3
+ * @description Feedback message (Shadcn/ui compatible)
4
4
  */
5
5
 
6
- import { forwardRef, type HTMLAttributes } from 'react';
6
+ import * as React from 'react';
7
7
  import { cn } from '../../infrastructure/utils';
8
- import type { BaseProps, ChildrenProps, ColorVariant } from '../../domain/types';
9
- import { Icon } from '../atoms/Icon';
10
8
 
11
- export type AlertVariant = Extract<ColorVariant, 'success' | 'warning' | 'destructive'> | 'info';
12
-
13
- export interface AlertProps extends HTMLAttributes<HTMLDivElement>, BaseProps {
14
- variant?: AlertVariant;
15
- showIcon?: boolean;
9
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ variant?: 'default' | 'destructive';
16
11
  }
17
12
 
18
- const variantStyles: Record<AlertVariant, { container: string; iconColor: string }> = {
19
- success: {
20
- container: 'border-success bg-success/10 text-success',
21
- iconColor: 'text-success',
22
- },
23
- warning: {
24
- container: 'border-warning bg-warning/10 text-warning',
25
- iconColor: 'text-warning',
26
- },
27
- destructive: {
28
- container: 'border-destructive bg-destructive/10 text-destructive',
29
- iconColor: 'text-destructive',
30
- },
31
- info: {
32
- container: 'border-primary bg-primary/10 text-primary',
33
- iconColor: 'text-primary',
34
- },
35
- };
36
-
37
- const icons: Record<AlertVariant, React.ReactNode> = {
38
- success: (
39
- <path
40
- strokeLinecap="round"
41
- strokeLinejoin="round"
42
- d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
43
- />
44
- ),
45
- warning: (
46
- <path
47
- strokeLinecap="round"
48
- strokeLinejoin="round"
49
- d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
50
- />
51
- ),
52
- destructive: (
53
- <path
54
- strokeLinecap="round"
55
- strokeLinejoin="round"
56
- d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
57
- />
58
- ),
59
- info: (
60
- <path
61
- strokeLinecap="round"
62
- strokeLinejoin="round"
63
- d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
13
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
14
+ ({ className, variant = 'default', ...props }, ref) => (
15
+ <div
16
+ ref={ref}
17
+ role="alert"
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
+ )}
24
+ {...props}
64
25
  />
65
- ),
66
- };
67
-
68
- export const Alert = forwardRef<HTMLDivElement, AlertProps>(
69
- ({ className, variant = 'info', showIcon = true, children, ...props }, ref) => {
70
- const styles = variantStyles[variant];
71
-
72
- return (
73
- <div
74
- ref={ref}
75
- role="alert"
76
- className={cn(
77
- 'relative w-full rounded-lg border p-4',
78
- styles.container,
79
- className
80
- )}
81
- {...props}
82
- >
83
- <div className="flex items-start gap-3">
84
- {showIcon && (
85
- <Icon className={cn('shrink-0 mt-0.5', styles.iconColor)} size="sm">
86
- {icons[variant]}
87
- </Icon>
88
- )}
89
- <div className="flex-1">{children}</div>
90
- </div>
91
- </div>
92
- );
93
- }
26
+ )
94
27
  );
95
-
96
28
  Alert.displayName = 'Alert';
29
+
30
+ const AlertTitle = React.forwardRef<
31
+ HTMLParagraphElement,
32
+ React.HTMLAttributes<HTMLHeadingElement>
33
+ >(({ className, ...props }, ref) => (
34
+ <h5
35
+ ref={ref}
36
+ className={cn('mb-1 font-medium leading-none tracking-tight', className)}
37
+ {...props}
38
+ />
39
+ ));
40
+ AlertTitle.displayName = 'AlertTitle';
41
+
42
+ const AlertDescription = React.forwardRef<
43
+ HTMLParagraphElement,
44
+ React.HTMLAttributes<HTMLParagraphElement>
45
+ >(({ className, ...props }, ref) => (
46
+ <div
47
+ ref={ref}
48
+ className={cn('text-sm [&_p]:leading-relaxed', className)}
49
+ {...props}
50
+ />
51
+ ));
52
+ AlertDescription.displayName = 'AlertDescription';
53
+
54
+ export { Alert, AlertTitle, AlertDescription };
@@ -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
+ }