@umituz/web-design-system 3.1.22 → 4.0.2

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 (84) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +31 -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/cn.util.ts +1 -1
  14. package/src/infrastructure/utils/index.ts +12 -0
  15. package/src/presentation/atoms/IconButton.tsx +55 -0
  16. package/src/presentation/atoms/Progress.tsx +2 -1
  17. package/src/presentation/atoms/Spinner.tsx +1 -1
  18. package/src/presentation/atoms/Switch.tsx +2 -2
  19. package/src/presentation/atoms/Text.tsx +15 -13
  20. package/src/presentation/atoms/Tooltip.tsx +2 -1
  21. package/src/presentation/atoms/index.ts +3 -0
  22. package/src/presentation/hooks/index.ts +0 -3
  23. package/src/presentation/hooks/useClickOutside.ts +16 -10
  24. package/src/presentation/hooks/useClipboard.ts +30 -12
  25. package/src/presentation/hooks/useDebounce.ts +9 -11
  26. package/src/presentation/hooks/useLocalStorage.ts +32 -20
  27. package/src/presentation/hooks/useMediaQuery.ts +20 -15
  28. package/src/presentation/hooks/useScrollLock.ts +40 -12
  29. package/src/presentation/hooks/useToggle.ts +8 -6
  30. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  31. package/src/presentation/molecules/Avatar.tsx +1 -1
  32. package/src/presentation/molecules/CodeBlock.tsx +50 -48
  33. package/src/presentation/molecules/InputGroup.tsx +1 -1
  34. package/src/presentation/molecules/ListItem.tsx +40 -29
  35. package/src/presentation/molecules/Select.tsx +1 -1
  36. package/src/presentation/molecules/Toggle.tsx +4 -4
  37. package/src/presentation/molecules/index.ts +0 -3
  38. package/src/presentation/organisms/Alert.tsx +1 -1
  39. package/src/presentation/organisms/DataTable.tsx +26 -15
  40. package/src/presentation/organisms/Dialog.tsx +1 -1
  41. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  42. package/src/presentation/organisms/HoverCard.tsx +1 -1
  43. package/src/presentation/organisms/LoadingState.tsx +1 -5
  44. package/src/presentation/organisms/Popover.tsx +1 -1
  45. package/src/presentation/organisms/StatCard.tsx +2 -2
  46. package/src/presentation/organisms/Table.tsx +2 -2
  47. package/src/presentation/organisms/index.ts +0 -60
  48. package/src/styles/compile.css +18 -0
  49. package/src/styles/theme.css +131 -0
  50. package/src/domain/tokens/animation.tokens.ts +0 -87
  51. package/src/domain/tokens/color.tokens.ts +0 -95
  52. package/src/domain/tokens/radius.tokens.ts +0 -27
  53. package/src/domain/tokens/shadow.tokens.ts +0 -25
  54. package/src/domain/tokens/spacing.tokens.ts +0 -65
  55. package/src/domain/tokens/typography.tokens.ts +0 -74
  56. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  57. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  58. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  59. package/src/infrastructure/error/index.ts +0 -5
  60. package/src/infrastructure/performance/index.ts +0 -92
  61. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  62. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  63. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  64. package/src/infrastructure/security/index.ts +0 -10
  65. package/src/infrastructure/security/security-config.ts +0 -171
  66. package/src/infrastructure/security/useFormValidation.ts +0 -216
  67. package/src/infrastructure/security/validation.ts +0 -246
  68. package/src/presentation/hooks/useLanguage.ts +0 -109
  69. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  70. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  71. package/src/presentation/organisms/Calendar.tsx +0 -79
  72. package/src/presentation/organisms/Comments.tsx +0 -103
  73. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  74. package/src/presentation/organisms/FilterBar.tsx +0 -244
  75. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  76. package/src/presentation/organisms/Footer.tsx +0 -86
  77. package/src/presentation/organisms/FormModal.tsx +0 -126
  78. package/src/presentation/organisms/Grid.tsx +0 -128
  79. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  80. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  81. package/src/presentation/organisms/Modal.tsx +0 -173
  82. package/src/presentation/organisms/Navbar.tsx +0 -75
  83. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  84. package/src/presentation/organisms/QuickActionCard.tsx +0 -93
@@ -1,103 +0,0 @@
1
- /**
2
- * Comments Component (Organism)
3
- * @description Giscus-based GitHub Discussions comment widget
4
- */
5
-
6
- import { useEffect, useRef } from 'react';
7
- import { cn } from '../../infrastructure/utils';
8
- import type { BaseProps } from '../../domain/types';
9
-
10
- export interface GiscusConfig {
11
- repo: string;
12
- repoId: string;
13
- category?: string;
14
- mapping?: 'pathname' | 'url' | 'title' | 'og:title' | 'custom';
15
- term?: string;
16
- strict?: '0' | '1';
17
- reactionsEnabled?: '0' | '1';
18
- emitMetadata?: '0' | '1';
19
- inputPosition?: 'top' | 'bottom';
20
- theme?: 'light' | 'dark' | 'dark_dimmed' | 'transparent_dark' | 'preferred_color_scheme';
21
- lang?: string;
22
- }
23
-
24
- export interface CommentsProps extends BaseProps {
25
- slug: string;
26
- config?: Partial<GiscusConfig>;
27
- title?: string;
28
- description?: string;
29
- }
30
-
31
- declare global {
32
- interface Window {
33
- giscus?: {
34
- render: (element: HTMLElement, config: Record<string, unknown>) => void;
35
- };
36
- }
37
- }
38
-
39
- export const Comments = ({
40
- slug,
41
- config,
42
- title = 'Comments',
43
- description = 'Join the discussion! Share your thoughts and questions below.',
44
- className,
45
- }: CommentsProps) => {
46
- const rootRef = useRef<HTMLDivElement>(null);
47
-
48
- useEffect(() => {
49
- // Load Giscus script dynamically
50
- const script = document.createElement('script');
51
- script.src = 'https://giscus.app/client.js';
52
- script.async = true;
53
- script.crossOrigin = 'anonymous';
54
-
55
- script.addEventListener('load', () => {
56
- if (window.giscus && rootRef.current) {
57
- const defaultConfig: GiscusConfig = {
58
- repo: 'umituz/umituz-apps',
59
- repoId: 'R_kgDONJ7RJw',
60
- category: 'Announcements',
61
- mapping: 'pathname',
62
- term: slug,
63
- strict: '0',
64
- reactionsEnabled: '1',
65
- emitMetadata: '0',
66
- inputPosition: 'top',
67
- theme: 'dark',
68
- lang: 'en',
69
- ...config,
70
- };
71
-
72
- window.giscus.render(rootRef.current, defaultConfig as unknown as Record<string, unknown>);
73
- }
74
- });
75
-
76
- document.head.appendChild(script);
77
-
78
- return () => {
79
- // Only remove if script exists and is in DOM
80
- if (script.parentNode === document.head) {
81
- document.head.removeChild(script);
82
- }
83
- };
84
- }, [slug, config]);
85
-
86
- return (
87
- <div className={cn('mt-8', className)}>
88
- <div className="bg-bg-card rounded-xl p-6 border border-border transition-theme">
89
- <h3 className="text-xl font-bold text-text-primary mb-4">{title}</h3>
90
- <p className="text-text-secondary text-sm mb-4">
91
- {description}
92
- </p>
93
- <div
94
- ref={rootRef}
95
- id="giscus-comments"
96
- className="min-h-[200px]"
97
- />
98
- </div>
99
- </div>
100
- );
101
- };
102
-
103
- Comments.displayName = 'Comments';
@@ -1,97 +0,0 @@
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';
@@ -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';