@umituz/web-design-system 3.1.22 → 4.0.0

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 (82) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +30 -31
  4. package/src/__tests__/cn.test.ts +24 -0
  5. package/src/__tests__/components.smoke.test.tsx +78 -0
  6. package/src/__tests__/styles.contract.test.ts +60 -0
  7. package/src/domain/tokens/index.ts +8 -49
  8. package/src/index.ts +8 -10
  9. package/src/infrastructure/constants/component.constants.ts +3 -17
  10. package/src/infrastructure/constants/index.ts +4 -2
  11. package/src/infrastructure/constants/timing.constants.ts +46 -0
  12. package/src/infrastructure/utils/calculation.utils.ts +74 -0
  13. package/src/infrastructure/utils/index.ts +12 -0
  14. package/src/presentation/atoms/IconButton.tsx +55 -0
  15. package/src/presentation/atoms/Progress.tsx +2 -1
  16. package/src/presentation/atoms/Spinner.tsx +1 -1
  17. package/src/presentation/atoms/Switch.tsx +2 -2
  18. package/src/presentation/atoms/Text.tsx +15 -13
  19. package/src/presentation/atoms/Tooltip.tsx +2 -1
  20. package/src/presentation/atoms/index.ts +3 -0
  21. package/src/presentation/hooks/index.ts +0 -3
  22. package/src/presentation/hooks/useClickOutside.ts +14 -10
  23. package/src/presentation/hooks/useClipboard.ts +30 -12
  24. package/src/presentation/hooks/useDebounce.ts +8 -10
  25. package/src/presentation/hooks/useLocalStorage.ts +28 -18
  26. package/src/presentation/hooks/useMediaQuery.ts +2 -1
  27. package/src/presentation/hooks/useScrollLock.ts +40 -12
  28. package/src/presentation/hooks/useToggle.ts +8 -6
  29. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  30. package/src/presentation/molecules/Avatar.tsx +1 -1
  31. package/src/presentation/molecules/CodeBlock.tsx +51 -48
  32. package/src/presentation/molecules/ListItem.tsx +40 -29
  33. package/src/presentation/molecules/Select.tsx +1 -1
  34. package/src/presentation/molecules/Toggle.tsx +4 -4
  35. package/src/presentation/molecules/index.ts +0 -3
  36. package/src/presentation/organisms/Alert.tsx +1 -1
  37. package/src/presentation/organisms/DataTable.tsx +24 -11
  38. package/src/presentation/organisms/Dialog.tsx +1 -1
  39. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  40. package/src/presentation/organisms/HoverCard.tsx +1 -1
  41. package/src/presentation/organisms/LoadingState.tsx +1 -5
  42. package/src/presentation/organisms/Popover.tsx +1 -1
  43. package/src/presentation/organisms/StatCard.tsx +2 -2
  44. package/src/presentation/organisms/Table.tsx +2 -2
  45. package/src/presentation/organisms/index.ts +0 -60
  46. package/src/styles/compile.css +18 -0
  47. package/src/styles/theme.css +131 -0
  48. package/src/domain/tokens/animation.tokens.ts +0 -87
  49. package/src/domain/tokens/color.tokens.ts +0 -95
  50. package/src/domain/tokens/radius.tokens.ts +0 -27
  51. package/src/domain/tokens/shadow.tokens.ts +0 -25
  52. package/src/domain/tokens/spacing.tokens.ts +0 -65
  53. package/src/domain/tokens/typography.tokens.ts +0 -74
  54. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  55. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  56. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  57. package/src/infrastructure/error/index.ts +0 -5
  58. package/src/infrastructure/performance/index.ts +0 -92
  59. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  60. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  61. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  62. package/src/infrastructure/security/index.ts +0 -10
  63. package/src/infrastructure/security/security-config.ts +0 -171
  64. package/src/infrastructure/security/useFormValidation.ts +0 -216
  65. package/src/infrastructure/security/validation.ts +0 -246
  66. package/src/presentation/hooks/useLanguage.ts +0 -109
  67. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  68. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  69. package/src/presentation/organisms/Calendar.tsx +0 -79
  70. package/src/presentation/organisms/Comments.tsx +0 -103
  71. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  72. package/src/presentation/organisms/FilterBar.tsx +0 -244
  73. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  74. package/src/presentation/organisms/Footer.tsx +0 -86
  75. package/src/presentation/organisms/FormModal.tsx +0 -126
  76. package/src/presentation/organisms/Grid.tsx +0 -128
  77. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  78. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  79. package/src/presentation/organisms/Modal.tsx +0 -173
  80. package/src/presentation/organisms/Navbar.tsx +0 -75
  81. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  82. package/src/presentation/organisms/QuickActionCard.tsx +0 -93
@@ -1,244 +0,0 @@
1
- /**
2
- * FilterBar Component (Organism)
3
- * @description Mobile filter bar with search, categories, tags, and sort with optimized performance
4
- */
5
-
6
- import { useState, useCallback, memo } from 'react';
7
- import React from 'react';
8
- import type { BaseProps } from '../../domain/types';
9
-
10
- export interface Category {
11
- id: string;
12
- name: string;
13
- count: number;
14
- icon: React.ComponentType<{ className?: string; size?: number }>;
15
- }
16
-
17
- export interface SortOption {
18
- id: string;
19
- name: string;
20
- }
21
-
22
- export interface FilterBarProps extends BaseProps {
23
- searchQuery: string;
24
- setSearchQuery: (query: string) => void;
25
- selectedCategory: string | null;
26
- onCategoryChange: (category: string) => void;
27
- categories: Category[];
28
- popularTags: string[];
29
- selectedTags: string[];
30
- onTagToggle: (tag: string) => void;
31
- sortBy: string;
32
- onSortChange: (sort: string) => void;
33
- sortOptions: readonly SortOption[] | SortOption[];
34
- hasActiveFilters: boolean;
35
- onClearFilters: () => void;
36
- }
37
-
38
- // Memoize category button component
39
- const CategoryButton = memo<{
40
- category: Category;
41
- selectedCategory: string | null;
42
- onCategoryChange: (category: string) => void;
43
- }>(({ category, selectedCategory, onCategoryChange }) => {
44
- const Icon = category.icon;
45
- const isActive = selectedCategory === category.id;
46
-
47
- const handleClick = useCallback(() => {
48
- onCategoryChange(category.id);
49
- }, [category.id, onCategoryChange]);
50
-
51
- return (
52
- <button
53
- onClick={handleClick}
54
- className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs transition-all transition-theme ${
55
- isActive
56
- ? 'bg-primary-light text-text-primary font-medium'
57
- : 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
58
- }`}
59
- type="button"
60
- aria-pressed={isActive}
61
- >
62
- <Icon size={14} />
63
- <span>{category.name}</span>
64
- </button>
65
- );
66
- });
67
-
68
- CategoryButton.displayName = 'CategoryButton';
69
-
70
- // Memoize tag button component
71
- const TagButton = memo<{
72
- tag: string;
73
- isSelected: boolean;
74
- onTagToggle: (tag: string) => void;
75
- }>(({ tag, isSelected, onTagToggle }) => {
76
- const handleClick = useCallback(() => {
77
- onTagToggle(tag);
78
- }, [tag, onTagToggle]);
79
-
80
- return (
81
- <button
82
- onClick={handleClick}
83
- className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all transition-theme ${
84
- isSelected
85
- ? 'bg-primary-light text-text-primary'
86
- : 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
87
- }`}
88
- type="button"
89
- aria-pressed={isSelected}
90
- >
91
- {tag}
92
- </button>
93
- );
94
- });
95
-
96
- TagButton.displayName = 'TagButton';
97
-
98
- export const FilterBar = memo<FilterBarProps>(({
99
- searchQuery,
100
- setSearchQuery,
101
- selectedCategory,
102
- onCategoryChange,
103
- categories,
104
- popularTags,
105
- selectedTags,
106
- onTagToggle,
107
- sortBy,
108
- onSortChange,
109
- sortOptions,
110
- hasActiveFilters,
111
- onClearFilters,
112
- className,
113
- }) => {
114
- const [isFilterOpen, setIsFilterOpen] = useState(false);
115
-
116
- const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
117
- setSearchQuery(e.target.value);
118
- }, [setSearchQuery]);
119
-
120
- const handleToggleFilters = useCallback(() => {
121
- setIsFilterOpen(prev => !prev);
122
- }, []);
123
-
124
- const handleSortChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
125
- onSortChange(e.target.value);
126
- }, [onSortChange]);
127
-
128
- const handleClearFilters = useCallback(() => {
129
- onClearFilters();
130
- }, [onClearFilters]);
131
-
132
- return (
133
- <div className={`lg:hidden mb-4 space-y-3 ${className || ''}`}>
134
- {/* Search */}
135
- <div className="relative">
136
- <svg
137
- className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
138
- width="18"
139
- height="18"
140
- viewBox="0 0 24 24"
141
- fill="none"
142
- stroke="currentColor"
143
- strokeWidth="2"
144
- strokeLinecap="round"
145
- strokeLinejoin="round"
146
- >
147
- <circle cx="11" cy="11" r="8" />
148
- <path d="m21 21-4.35-4.35" />
149
- </svg>
150
- <input
151
- type="text"
152
- placeholder="Search..."
153
- value={searchQuery}
154
- onChange={handleSearchChange}
155
- className="w-full pl-9 pr-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
156
- aria-label="Search"
157
- />
158
- </div>
159
-
160
- {/* Filter Toggle + Sort + Clear Filters */}
161
- <div className="flex items-center gap-2 flex-wrap">
162
- <button
163
- onClick={handleToggleFilters}
164
- className="flex items-center gap-2 px-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border hover:border-primary-light transition-all text-sm font-medium transition-theme"
165
- type="button"
166
- aria-expanded={isFilterOpen}
167
- aria-label="Toggle filters"
168
- >
169
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
170
- <path d="M12 20v-6M6 20V10M18 20V4" />
171
- </svg>
172
- Filters
173
- {(selectedCategory || selectedTags.length > 0) && (
174
- <span className="ml-1 px-2 py-0.5 bg-primary-light text-text-primary text-xs rounded-full">
175
- {[selectedCategory, ...selectedTags].filter(Boolean).length}
176
- </span>
177
- )}
178
- </button>
179
-
180
- {/* Sort Dropdown */}
181
- <select
182
- value={sortBy}
183
- onChange={handleSortChange}
184
- className="px-3 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none transition-theme text-sm"
185
- aria-label="Sort by"
186
- >
187
- {sortOptions.map((option) => (
188
- <option key={option.id} value={option.id}>
189
- {option.name}
190
- </option>
191
- ))}
192
- </select>
193
-
194
- {/* Clear Filters */}
195
- {hasActiveFilters && (
196
- <button
197
- onClick={handleClearFilters}
198
- className="px-3 py-2.5 text-text-secondary hover:text-text-primary text-sm transition-colors"
199
- type="button"
200
- >
201
- Clear All
202
- </button>
203
- )}
204
- </div>
205
-
206
- {/* Mobile Filter Drawer */}
207
- {isFilterOpen && (
208
- <div className="bg-bg-card rounded-xl p-4 border border-border space-y-4 transition-theme">
209
- {/* Categories */}
210
- <div>
211
- <h4 className="text-sm font-semibold text-text-primary mb-3">Categories</h4>
212
- <div className="grid grid-cols-2 gap-2">
213
- {categories.map((category) => (
214
- <CategoryButton
215
- key={category.id}
216
- category={category}
217
- selectedCategory={selectedCategory}
218
- onCategoryChange={onCategoryChange}
219
- />
220
- ))}
221
- </div>
222
- </div>
223
-
224
- {/* Popular Tags */}
225
- <div>
226
- <h4 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h4>
227
- <div className="flex flex-wrap gap-2">
228
- {popularTags.map((tag) => (
229
- <TagButton
230
- key={tag}
231
- tag={tag}
232
- isSelected={selectedTags.includes(tag)}
233
- onTagToggle={onTagToggle}
234
- />
235
- ))}
236
- </div>
237
- </div>
238
- </div>
239
- )}
240
- </div>
241
- );
242
- });
243
-
244
- FilterBar.displayName = 'FilterBar';
@@ -1,172 +0,0 @@
1
- /**
2
- * FilterSidebar Component (Organism)
3
- * @description Desktop sidebar with search, categories, tags, and sort options
4
- */
5
-
6
- import React from 'react';
7
- import type { BaseProps } from '../../domain/types';
8
-
9
- export interface Category {
10
- id: string;
11
- name: string;
12
- count: number;
13
- icon: React.ComponentType<{ className?: string; size?: number }>;
14
- }
15
-
16
- export interface SortOption {
17
- id: string;
18
- name: string;
19
- }
20
-
21
- export interface FilterSidebarProps extends BaseProps {
22
- searchQuery: string;
23
- setSearchQuery: (query: string) => void;
24
- selectedCategory: string | null;
25
- onCategoryChange: (category: string) => void;
26
- categories: Category[];
27
- popularTags: string[];
28
- selectedTags: string[];
29
- onTagToggle: (tag: string) => void;
30
- sortBy: string;
31
- onSortChange: (sort: string) => void;
32
- sortOptions: readonly SortOption[] | SortOption[];
33
- hasActiveFilters: boolean;
34
- onClearFilters: () => void;
35
- children?: React.ReactNode;
36
- }
37
-
38
- export const FilterSidebar = ({
39
- searchQuery,
40
- setSearchQuery,
41
- selectedCategory,
42
- onCategoryChange,
43
- categories,
44
- popularTags,
45
- selectedTags,
46
- onTagToggle,
47
- sortBy,
48
- onSortChange,
49
- sortOptions,
50
- hasActiveFilters,
51
- onClearFilters,
52
- children,
53
- className,
54
- }: FilterSidebarProps) => {
55
- return (
56
- <aside className={`hidden lg:block w-64 flex-shrink-0 ${className || ''}`}>
57
- <div className="sticky top-24 space-y-6">
58
- {/* Search */}
59
- <div className="relative">
60
- <svg
61
- className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
62
- width="18"
63
- height="18"
64
- viewBox="0 0 24 24"
65
- fill="none"
66
- stroke="currentColor"
67
- strokeWidth="2"
68
- strokeLinecap="round"
69
- strokeLinejoin="round"
70
- >
71
- <circle cx="11" cy="11" r="8" />
72
- <path d="m21 21-4.35-4.35" />
73
- </svg>
74
- <input
75
- type="text"
76
- placeholder="Search..."
77
- value={searchQuery}
78
- onChange={(e) => setSearchQuery(e.target.value)}
79
- className="w-full pl-9 pr-4 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none placeholder-text-secondary/50 transition-theme text-sm"
80
- aria-label="Search"
81
- />
82
- </div>
83
-
84
- {/* Categories */}
85
- <div>
86
- <h3 className="text-sm font-semibold text-text-primary mb-3">Categories</h3>
87
- <div className="space-y-1">
88
- {categories.map((category) => {
89
- const Icon = category.icon;
90
- const isActive = selectedCategory === category.id;
91
- return (
92
- <button
93
- key={category.id}
94
- onClick={() => onCategoryChange(category.id)}
95
- className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all transition-theme ${
96
- isActive
97
- ? 'bg-primary-light text-text-primary font-medium'
98
- : 'text-text-secondary hover:bg-bg-secondary hover:text-text-primary'
99
- }`}
100
- type="button"
101
- aria-pressed={isActive}
102
- >
103
- <Icon size={16} />
104
- <span className="flex-1 text-left">{category.name}</span>
105
- <span className={`text-xs ${isActive ? 'text-text-primary' : 'text-text-secondary'}`}>
106
- {category.count}
107
- </span>
108
- </button>
109
- );
110
- })}
111
- </div>
112
- </div>
113
-
114
- {/* Popular Tags */}
115
- <div>
116
- <h3 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h3>
117
- <div className="flex flex-wrap gap-2">
118
- {popularTags.map((tag) => (
119
- <button
120
- key={tag}
121
- onClick={() => onTagToggle(tag)}
122
- className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all transition-theme ${
123
- selectedTags.includes(tag)
124
- ? 'bg-primary-light text-text-primary'
125
- : 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
126
- }`}
127
- type="button"
128
- aria-pressed={selectedTags.includes(tag)}
129
- >
130
- {tag}
131
- </button>
132
- ))}
133
- </div>
134
- </div>
135
-
136
- {/* Sort */}
137
- <div>
138
- <h3 className="text-sm font-semibold text-text-primary mb-3">Sort By</h3>
139
- <select
140
- value={sortBy}
141
- onChange={(e) => onSortChange(e.target.value)}
142
- className="w-full px-3 py-2.5 bg-bg-secondary text-text-primary rounded-lg border border-border focus:border-primary-light focus:outline-none transition-theme text-sm"
143
- aria-label="Sort by"
144
- >
145
- {sortOptions.map((option) => (
146
- <option key={option.id} value={option.id}>
147
- {option.name}
148
- </option>
149
- ))}
150
- </select>
151
- </div>
152
-
153
- {/* Additional Content */}
154
- {children}
155
-
156
- {/* Clear Filters */}
157
- {hasActiveFilters && (
158
- <button
159
- onClick={onClearFilters}
160
- className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-bg-secondary text-text-secondary rounded-lg border border-border hover:border-primary-light hover:text-text-primary transition-all text-sm font-medium transition-theme"
161
- type="button"
162
- >
163
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
164
- <path d="M18 6L6 18M6 6l12 12" />
165
- </svg>
166
- Clear All Filters
167
- </button>
168
- )}
169
- </div>
170
- </aside>
171
- );
172
- };
@@ -1,86 +0,0 @@
1
- /**
2
- * Footer Organism Component
3
- * @description Minimal footer with brand and social icons - optimized for performance
4
- */
5
-
6
- import { forwardRef, type HTMLAttributes, type ReactNode, memo } from 'react';
7
- import React from 'react';
8
- import { cn } from '../../infrastructure/utils';
9
- import type { BaseProps } from '../../domain/types';
10
-
11
- export interface FooterProps extends HTMLAttributes<HTMLElement>, BaseProps {
12
- brand?: {
13
- name: string;
14
- };
15
- social?: Array<{
16
- name: string;
17
- href: string;
18
- icon: ReactNode;
19
- }>;
20
- copyright?: string;
21
- }
22
-
23
- type SocialItem = {
24
- name: string;
25
- href: string;
26
- icon: ReactNode;
27
- };
28
-
29
- // Memoize social icon component to prevent unnecessary re-renders
30
- const SocialIcon = memo<{
31
- item: SocialItem;
32
- }>(({ item }) => (
33
- <a
34
- href={item.href}
35
- target="_blank"
36
- rel="noopener noreferrer"
37
- className="flex items-center justify-center w-10 h-10 rounded-lg bg-bg-secondary/50 text-text-secondary hover:bg-primary-gradient hover:text-white transition-all duration-200"
38
- aria-label={item.name}
39
- title={item.name}
40
- >
41
- <span className="relative">{item.icon}</span>
42
- </a>
43
- ));
44
-
45
- SocialIcon.displayName = 'SocialIcon';
46
-
47
- export const Footer = memo(forwardRef<HTMLElement, FooterProps>(
48
- ({ className, brand, social, copyright = '© 2026 UmitUZ. All rights reserved.', ...props }, ref) => {
49
- return (
50
- <footer
51
- ref={ref}
52
- className={cn('relative mt-8 transition-theme', className)}
53
- {...props}
54
- >
55
- <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
56
- <div className="flex flex-col md:flex-row items-center justify-between gap-6">
57
- {/* Brand */}
58
- {brand && (
59
- <div className="text-xl font-bold text-text-primary">
60
- {brand.name}
61
- </div>
62
- )}
63
-
64
- {/* Social Icons */}
65
- {social && social.length > 0 && (
66
- <div className="flex items-center gap-3">
67
- {social.map((item, index) => (
68
- <SocialIcon key={`${item.name}-${index}`} item={item} />
69
- ))}
70
- </div>
71
- )}
72
- </div>
73
-
74
- {/* Copyright */}
75
- {copyright && (
76
- <div className="text-center mt-6 pt-6 border-t border-border/20">
77
- <p className="text-sm text-text-secondary/60">{copyright}</p>
78
- </div>
79
- )}
80
- </div>
81
- </footer>
82
- );
83
- }
84
- ));
85
-
86
- Footer.displayName = 'Footer';
@@ -1,126 +0,0 @@
1
- /**
2
- * FormModal Component (Template)
3
- * @description Pre-configured modal with form wrapper and loading state
4
- * Reduces boilerplate in modal components like CampaignModal, ProjectListModal
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 { Loader2 } from 'lucide-react';
19
- import type { BaseProps } from '../../domain/types';
20
-
21
- export interface FormModalProps extends BaseProps {
22
- open: boolean;
23
- onOpenChange: (open: boolean) => void;
24
- title: string;
25
- description?: string;
26
- children: ReactNode;
27
- onSubmit?: (e: React.FormEvent) => void | Promise<void>;
28
- loading?: boolean;
29
- submitText?: string;
30
- cancelText?: string;
31
- showFooter?: boolean;
32
- footer?: ReactNode;
33
- size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
34
- preventCloseOnSubmit?: boolean;
35
- }
36
-
37
- const sizeStyles = {
38
- sm: 'sm:max-w-[400px]',
39
- md: 'sm:max-w-[425px]',
40
- lg: 'sm:max-w-[600px]',
41
- xl: 'sm:max-w-[800px]',
42
- full: 'sm:max-w-[95vw]',
43
- };
44
-
45
- export const FormModal = forwardRef<HTMLDivElement, FormModalProps>(
46
- (
47
- {
48
- className,
49
- open,
50
- onOpenChange,
51
- title,
52
- description,
53
- children,
54
- onSubmit,
55
- loading = false,
56
- submitText = 'Submit',
57
- cancelText = 'Cancel',
58
- showFooter = true,
59
- footer,
60
- size = 'md',
61
- preventCloseOnSubmit = false,
62
- ...props
63
- },
64
- ref
65
- ) => {
66
- const handleSubmit = async (e: React.FormEvent) => {
67
- e.preventDefault();
68
- if (onSubmit && !loading) {
69
- await onSubmit(e);
70
- if (!preventCloseOnSubmit) {
71
- onOpenChange(false);
72
- }
73
- }
74
- };
75
-
76
- return (
77
- <Dialog open={open} onOpenChange={onOpenChange}>
78
- <DialogContent className={cn(sizeStyles[size], className)} ref={ref} {...props}>
79
- {onSubmit ? (
80
- <form onSubmit={handleSubmit}>
81
- <DialogHeader>
82
- <DialogTitle>{title}</DialogTitle>
83
- {description && <DialogDescription>{description}</DialogDescription>}
84
- </DialogHeader>
85
- <div className="grid gap-4 py-4">{children}</div>
86
- {showFooter && (
87
- <DialogFooter>
88
- {footer || (
89
- <>
90
- <Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
91
- {cancelText}
92
- </Button>
93
- <Button type="submit" disabled={loading}>
94
- {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
95
- {submitText}
96
- </Button>
97
- </>
98
- )}
99
- </DialogFooter>
100
- )}
101
- </form>
102
- ) : (
103
- <>
104
- <DialogHeader>
105
- <DialogTitle>{title}</DialogTitle>
106
- {description && <DialogDescription>{description}</DialogDescription>}
107
- </DialogHeader>
108
- <div className="grid gap-4 py-4">{children}</div>
109
- {showFooter && (
110
- <DialogFooter>
111
- {footer || (
112
- <Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
113
- {cancelText}
114
- </Button>
115
- )}
116
- </DialogFooter>
117
- )}
118
- </>
119
- )}
120
- </DialogContent>
121
- </Dialog>
122
- );
123
- }
124
- );
125
-
126
- FormModal.displayName = 'FormModal';