@umituz/web-design-system 3.1.8 → 3.1.9
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 +1 -1
- package/src/infrastructure/utils/cn.util.ts +61 -2
- package/src/presentation/atoms/Button.tsx +2 -2
- package/src/presentation/atoms/Text.tsx +3 -2
- package/src/presentation/hooks/useDebounce.ts +47 -4
- package/src/presentation/hooks/useLocalStorage.ts +8 -4
- package/src/presentation/organisms/Card.tsx +11 -10
- package/src/presentation/organisms/FilterBar.tsx +100 -43
- package/src/presentation/organisms/Footer.tsx +24 -15
- package/src/presentation/organisms/Modal.tsx +40 -8
- package/src/presentation/organisms/Navbar.tsx +10 -9
- package/src/presentation/organisms/Table.tsx +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* cn Utility
|
|
3
|
-
* @description Conditional className utility using clsx + tailwind-merge for proper Tailwind class merging
|
|
3
|
+
* @description Conditional className utility using clsx + tailwind-merge for proper Tailwind class merging with LRU cache for performance
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { clsx, type ClassValue } from 'clsx';
|
|
@@ -8,6 +8,65 @@ import { twMerge } from 'tailwind-merge';
|
|
|
8
8
|
|
|
9
9
|
export type { ClassValue };
|
|
10
10
|
|
|
11
|
+
// Simple LRU Cache implementation for className caching
|
|
12
|
+
class LRUCache<K, V> {
|
|
13
|
+
private cache: Map<K, V>;
|
|
14
|
+
private maxSize: number;
|
|
15
|
+
|
|
16
|
+
constructor(maxSize: number = 128) {
|
|
17
|
+
this.cache = new Map();
|
|
18
|
+
this.maxSize = maxSize;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get(key: K): V | undefined {
|
|
22
|
+
const value = this.cache.get(key);
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
// Move to end (most recently used)
|
|
25
|
+
this.cache.delete(key);
|
|
26
|
+
this.cache.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
set(key: K, value: V): void {
|
|
32
|
+
// Remove existing entry to update position
|
|
33
|
+
if (this.cache.has(key)) {
|
|
34
|
+
this.cache.delete(key);
|
|
35
|
+
}
|
|
36
|
+
// Remove oldest entry if at capacity
|
|
37
|
+
else if (this.cache.size >= this.maxSize) {
|
|
38
|
+
const firstKey = this.cache.keys().next().value;
|
|
39
|
+
this.cache.delete(firstKey);
|
|
40
|
+
}
|
|
41
|
+
this.cache.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
this.cache.clear();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create cache instance
|
|
50
|
+
const classNameCache = new LRUCache<string, string>(256);
|
|
51
|
+
|
|
52
|
+
// Cache key generator
|
|
53
|
+
function generateCacheKey(inputs: ClassValue[]): string {
|
|
54
|
+
return JSON.stringify(inputs);
|
|
55
|
+
}
|
|
56
|
+
|
|
11
57
|
export function cn(...inputs: ClassValue[]): string {
|
|
12
|
-
|
|
58
|
+
const cacheKey = generateCacheKey(inputs);
|
|
59
|
+
const cached = classNameCache.get(cacheKey);
|
|
60
|
+
|
|
61
|
+
if (cached) {
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = twMerge(clsx(inputs));
|
|
66
|
+
classNameCache.set(cacheKey, result);
|
|
67
|
+
|
|
68
|
+
return result;
|
|
13
69
|
}
|
|
70
|
+
|
|
71
|
+
// Export cache for manual clearing if needed
|
|
72
|
+
export { classNameCache };
|
|
@@ -41,12 +41,12 @@ export interface ButtonProps
|
|
|
41
41
|
asChild?: boolean;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
44
|
+
const Button = React.memo(React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
45
45
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
46
46
|
const Comp = asChild ? Slot : 'button';
|
|
47
47
|
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
|
48
48
|
}
|
|
49
|
-
);
|
|
49
|
+
));
|
|
50
50
|
Button.displayName = 'Button';
|
|
51
51
|
|
|
52
52
|
export { Button, buttonVariants };
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import type { BaseProps } from '../../domain/types';
|
|
9
10
|
|
|
@@ -45,7 +46,7 @@ const weightStyles: Record<'normal' | 'medium' | 'semibold' | 'bold', string> =
|
|
|
45
46
|
|
|
46
47
|
// NOTE: "as any" is used here for the polymorphic ref, which is a necessary workaround
|
|
47
48
|
// for TypeScript's limitations in typing polymorphic components properly.
|
|
48
|
-
export const Text = forwardRef<HTMLElement, TextProps>(
|
|
49
|
+
export const Text = React.memo(forwardRef<HTMLElement, TextProps>(
|
|
49
50
|
({ className, as = 'p', variant = 'body', size = 'md', weight = 'normal', ...props }, ref) => {
|
|
50
51
|
const Tag = as as any;
|
|
51
52
|
return (
|
|
@@ -61,6 +62,6 @@ export const Text = forwardRef<HTMLElement, TextProps>(
|
|
|
61
62
|
/>
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
|
-
);
|
|
65
|
+
));
|
|
65
66
|
|
|
66
67
|
Text.displayName = 'Text';
|
|
@@ -1,22 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useDebounce Hook
|
|
3
|
-
* @description Debounce a value
|
|
3
|
+
* @description Debounce a value with optimized performance
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useEffect } from 'react';
|
|
6
|
+
import { useState, useEffect, useRef } from 'react';
|
|
7
7
|
|
|
8
8
|
export function useDebounce<T>(value: T, delay: number = 500): T {
|
|
9
9
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
10
|
+
const timeoutRef = useRef<number>();
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
+
// Clear previous timeout
|
|
14
|
+
if (timeoutRef.current) {
|
|
15
|
+
clearTimeout(timeoutRef.current);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Set new timeout
|
|
19
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
13
20
|
setDebouncedValue(value);
|
|
14
21
|
}, delay);
|
|
15
22
|
|
|
23
|
+
// Cleanup
|
|
16
24
|
return () => {
|
|
17
|
-
|
|
25
|
+
if (timeoutRef.current) {
|
|
26
|
+
clearTimeout(timeoutRef.current);
|
|
27
|
+
}
|
|
18
28
|
};
|
|
19
29
|
}, [value, delay]);
|
|
20
30
|
|
|
21
31
|
return debouncedValue;
|
|
22
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* useThrottle Hook
|
|
36
|
+
* @description Throttle a function to limit execution rate
|
|
37
|
+
*/
|
|
38
|
+
export function useThrottle<T extends (...args: any[]) => any>(
|
|
39
|
+
func: T,
|
|
40
|
+
delay: number = 300
|
|
41
|
+
): T {
|
|
42
|
+
const lastRun = useRef<Date>(new Date());
|
|
43
|
+
const timeoutRef = useRef<number>();
|
|
44
|
+
|
|
45
|
+
return useCallback((...args: Parameters<T>) => {
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const timeSinceLastRun = now.getTime() - lastRun.current.getTime();
|
|
48
|
+
|
|
49
|
+
if (timeSinceLastRun >= delay) {
|
|
50
|
+
lastRun.current = now;
|
|
51
|
+
return func(...args);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (timeoutRef.current) {
|
|
55
|
+
clearTimeout(timeoutRef.current);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
59
|
+
lastRun.current = new Date();
|
|
60
|
+
func(...args);
|
|
61
|
+
}, delay - timeSinceLastRun);
|
|
62
|
+
}, [func, delay]) as T;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
import { useCallback } from 'react';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useLocalStorage Hook
|
|
3
|
-
* @description LocalStorage state management
|
|
3
|
+
* @description LocalStorage state management with optimized re-renders
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useCallback, useState } from 'react';
|
|
6
|
+
import { useCallback, useState, useRef } from 'react';
|
|
7
7
|
|
|
8
8
|
export function useLocalStorage<T>(
|
|
9
9
|
key: string,
|
|
@@ -18,17 +18,21 @@ export function useLocalStorage<T>(
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
// Use ref to track latest value without causing re-renders
|
|
22
|
+
const valueRef = useRef(storedValue);
|
|
23
|
+
valueRef.current = storedValue;
|
|
24
|
+
|
|
21
25
|
const setValue = useCallback(
|
|
22
26
|
(value: T | ((prev: T) => T)) => {
|
|
23
27
|
try {
|
|
24
|
-
const valueToStore = value instanceof Function ? value(
|
|
28
|
+
const valueToStore = value instanceof Function ? value(valueRef.current) : value;
|
|
25
29
|
setStoredValue(valueToStore);
|
|
26
30
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
27
31
|
} catch (error) {
|
|
28
32
|
console.error(`Error setting localStorage key "${key}":`, error);
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
|
-
[key
|
|
35
|
+
[key] // Remove storedValue dependency
|
|
32
36
|
);
|
|
33
37
|
|
|
34
38
|
const removeValue = useCallback(() => {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import type { BaseProps } from '../../domain/types';
|
|
9
10
|
|
|
@@ -33,7 +34,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
|
33
34
|
|
|
34
35
|
Card.displayName = 'Card';
|
|
35
36
|
|
|
36
|
-
export const CardHeader = forwardRef<HTMLDivElement, CardProps>(
|
|
37
|
+
export const CardHeader = React.memo(forwardRef<HTMLDivElement, CardProps>(
|
|
37
38
|
({ className, ...props }, ref) => (
|
|
38
39
|
<div
|
|
39
40
|
ref={ref}
|
|
@@ -41,11 +42,11 @@ export const CardHeader = forwardRef<HTMLDivElement, CardProps>(
|
|
|
41
42
|
{...props}
|
|
42
43
|
/>
|
|
43
44
|
)
|
|
44
|
-
);
|
|
45
|
+
));
|
|
45
46
|
|
|
46
47
|
CardHeader.displayName = 'CardHeader';
|
|
47
48
|
|
|
48
|
-
export const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
49
|
+
export const CardTitle = React.memo(forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
49
50
|
({ className, ...props }, ref) => (
|
|
50
51
|
<h3
|
|
51
52
|
ref={ref}
|
|
@@ -53,11 +54,11 @@ export const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHea
|
|
|
53
54
|
{...props}
|
|
54
55
|
/>
|
|
55
56
|
)
|
|
56
|
-
);
|
|
57
|
+
));
|
|
57
58
|
|
|
58
59
|
CardTitle.displayName = 'CardTitle';
|
|
59
60
|
|
|
60
|
-
export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
61
|
+
export const CardDescription = React.memo(forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
61
62
|
({ className, ...props }, ref) => (
|
|
62
63
|
<p
|
|
63
64
|
ref={ref}
|
|
@@ -65,19 +66,19 @@ export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<H
|
|
|
65
66
|
{...props}
|
|
66
67
|
/>
|
|
67
68
|
)
|
|
68
|
-
);
|
|
69
|
+
));
|
|
69
70
|
|
|
70
71
|
CardDescription.displayName = 'CardDescription';
|
|
71
72
|
|
|
72
|
-
export const CardContent = forwardRef<HTMLDivElement, CardProps>(
|
|
73
|
+
export const CardContent = React.memo(forwardRef<HTMLDivElement, CardProps>(
|
|
73
74
|
({ className, ...props }, ref) => (
|
|
74
75
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
75
76
|
)
|
|
76
|
-
);
|
|
77
|
+
));
|
|
77
78
|
|
|
78
79
|
CardContent.displayName = 'CardContent';
|
|
79
80
|
|
|
80
|
-
export const CardFooter = forwardRef<HTMLDivElement, CardProps>(
|
|
81
|
+
export const CardFooter = React.memo(forwardRef<HTMLDivElement, CardProps>(
|
|
81
82
|
({ className, ...props }, ref) => (
|
|
82
83
|
<div
|
|
83
84
|
ref={ref}
|
|
@@ -85,6 +86,6 @@ export const CardFooter = forwardRef<HTMLDivElement, CardProps>(
|
|
|
85
86
|
{...props}
|
|
86
87
|
/>
|
|
87
88
|
)
|
|
88
|
-
);
|
|
89
|
+
));
|
|
89
90
|
|
|
90
91
|
CardFooter.displayName = 'CardFooter';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FilterBar Component (Organism)
|
|
3
|
-
* @description Mobile filter bar with search, categories, tags, and sort
|
|
3
|
+
* @description Mobile filter bar with search, categories, tags, and sort with optimized performance
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState } from 'react';
|
|
6
|
+
import { useState, useCallback, memo } from 'react';
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import type { BaseProps } from '../../domain/types';
|
|
9
9
|
|
|
@@ -35,7 +35,67 @@ export interface FilterBarProps extends BaseProps {
|
|
|
35
35
|
onClearFilters: () => void;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
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>(({
|
|
39
99
|
searchQuery,
|
|
40
100
|
setSearchQuery,
|
|
41
101
|
selectedCategory,
|
|
@@ -50,9 +110,25 @@ export const FilterBar = ({
|
|
|
50
110
|
hasActiveFilters,
|
|
51
111
|
onClearFilters,
|
|
52
112
|
className,
|
|
53
|
-
}
|
|
113
|
+
}) => {
|
|
54
114
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
|
55
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
|
+
|
|
56
132
|
return (
|
|
57
133
|
<div className={`lg:hidden mb-4 space-y-3 ${className || ''}`}>
|
|
58
134
|
{/* Search */}
|
|
@@ -75,7 +151,7 @@ export const FilterBar = ({
|
|
|
75
151
|
type="text"
|
|
76
152
|
placeholder="Search..."
|
|
77
153
|
value={searchQuery}
|
|
78
|
-
onChange={
|
|
154
|
+
onChange={handleSearchChange}
|
|
79
155
|
className="w-full pl-10 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
156
|
aria-label="Search"
|
|
81
157
|
/>
|
|
@@ -84,7 +160,7 @@ export const FilterBar = ({
|
|
|
84
160
|
{/* Filter Toggle + Sort + Clear Filters */}
|
|
85
161
|
<div className="flex items-center gap-2 flex-wrap">
|
|
86
162
|
<button
|
|
87
|
-
onClick={
|
|
163
|
+
onClick={handleToggleFilters}
|
|
88
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"
|
|
89
165
|
type="button"
|
|
90
166
|
aria-expanded={isFilterOpen}
|
|
@@ -104,7 +180,7 @@ export const FilterBar = ({
|
|
|
104
180
|
{/* Sort Dropdown */}
|
|
105
181
|
<select
|
|
106
182
|
value={sortBy}
|
|
107
|
-
onChange={
|
|
183
|
+
onChange={handleSortChange}
|
|
108
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"
|
|
109
185
|
aria-label="Sort by"
|
|
110
186
|
>
|
|
@@ -118,7 +194,7 @@ export const FilterBar = ({
|
|
|
118
194
|
{/* Clear Filters */}
|
|
119
195
|
{hasActiveFilters && (
|
|
120
196
|
<button
|
|
121
|
-
onClick={
|
|
197
|
+
onClick={handleClearFilters}
|
|
122
198
|
className="px-3 py-2.5 text-text-secondary hover:text-text-primary text-sm transition-colors"
|
|
123
199
|
type="button"
|
|
124
200
|
>
|
|
@@ -134,28 +210,14 @@ export const FilterBar = ({
|
|
|
134
210
|
<div>
|
|
135
211
|
<h4 className="text-sm font-semibold text-text-primary mb-3">Categories</h4>
|
|
136
212
|
<div className="grid grid-cols-2 gap-2">
|
|
137
|
-
{categories.map((category) =>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}}
|
|
146
|
-
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs transition-all transition-theme ${
|
|
147
|
-
isActive
|
|
148
|
-
? 'bg-primary-light text-text-primary font-medium'
|
|
149
|
-
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
|
|
150
|
-
}`}
|
|
151
|
-
type="button"
|
|
152
|
-
aria-pressed={isActive}
|
|
153
|
-
>
|
|
154
|
-
<Icon size={14} />
|
|
155
|
-
<span>{category.name}</span>
|
|
156
|
-
</button>
|
|
157
|
-
);
|
|
158
|
-
})}
|
|
213
|
+
{categories.map((category) => (
|
|
214
|
+
<CategoryButton
|
|
215
|
+
key={category.id}
|
|
216
|
+
category={category}
|
|
217
|
+
selectedCategory={selectedCategory}
|
|
218
|
+
onCategoryChange={onCategoryChange}
|
|
219
|
+
/>
|
|
220
|
+
))}
|
|
159
221
|
</div>
|
|
160
222
|
</div>
|
|
161
223
|
|
|
@@ -164,19 +226,12 @@ export const FilterBar = ({
|
|
|
164
226
|
<h4 className="text-sm font-semibold text-text-primary mb-3">Popular Tags</h4>
|
|
165
227
|
<div className="flex flex-wrap gap-2">
|
|
166
228
|
{popularTags.map((tag) => (
|
|
167
|
-
<
|
|
229
|
+
<TagButton
|
|
168
230
|
key={tag}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
: 'bg-bg-secondary text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
|
|
174
|
-
}`}
|
|
175
|
-
type="button"
|
|
176
|
-
aria-pressed={selectedTags.includes(tag)}
|
|
177
|
-
>
|
|
178
|
-
{tag}
|
|
179
|
-
</button>
|
|
231
|
+
tag={tag}
|
|
232
|
+
isSelected={selectedTags.includes(tag)}
|
|
233
|
+
onTagToggle={onTagToggle}
|
|
234
|
+
/>
|
|
180
235
|
))}
|
|
181
236
|
</div>
|
|
182
237
|
</div>
|
|
@@ -184,4 +239,6 @@ export const FilterBar = ({
|
|
|
184
239
|
)}
|
|
185
240
|
</div>
|
|
186
241
|
);
|
|
187
|
-
};
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
FilterBar.displayName = 'FilterBar';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Footer Organism Component
|
|
3
|
-
* @description Minimal footer with brand and social icons
|
|
3
|
+
* @description Minimal footer with brand and social icons - optimized for performance
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
6
|
+
import { forwardRef, type HTMLAttributes, type ReactNode, memo } from 'react';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import type { BaseProps } from '../../domain/types';
|
|
9
10
|
|
|
@@ -19,7 +20,25 @@ export interface FooterProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
|
19
20
|
copyright?: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// Memoize social icon component to prevent unnecessary re-renders
|
|
24
|
+
const SocialIcon = memo<{
|
|
25
|
+
item: FooterProps['social'][number];
|
|
26
|
+
}>(({ item }) => (
|
|
27
|
+
<a
|
|
28
|
+
href={item.href}
|
|
29
|
+
target="_blank"
|
|
30
|
+
rel="noopener noreferrer"
|
|
31
|
+
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"
|
|
32
|
+
aria-label={item.name}
|
|
33
|
+
title={item.name}
|
|
34
|
+
>
|
|
35
|
+
<span className="relative">{item.icon}</span>
|
|
36
|
+
</a>
|
|
37
|
+
));
|
|
38
|
+
|
|
39
|
+
SocialIcon.displayName = 'SocialIcon';
|
|
40
|
+
|
|
41
|
+
export const Footer = memo(forwardRef<HTMLElement, FooterProps>(
|
|
23
42
|
({ className, brand, social, copyright = '© 2026 UmitUZ. All rights reserved.', ...props }, ref) => {
|
|
24
43
|
return (
|
|
25
44
|
<footer
|
|
@@ -40,17 +59,7 @@ export const Footer = forwardRef<HTMLElement, FooterProps>(
|
|
|
40
59
|
{social && social.length > 0 && (
|
|
41
60
|
<div className="flex items-center gap-3">
|
|
42
61
|
{social.map((item, index) => (
|
|
43
|
-
<
|
|
44
|
-
key={index}
|
|
45
|
-
href={item.href}
|
|
46
|
-
target="_blank"
|
|
47
|
-
rel="noopener noreferrer"
|
|
48
|
-
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"
|
|
49
|
-
aria-label={item.name}
|
|
50
|
-
title={item.name}
|
|
51
|
-
>
|
|
52
|
-
<span className="relative">{item.icon}</span>
|
|
53
|
-
</a>
|
|
62
|
+
<SocialIcon key={`${item.name}-${index}`} item={item} />
|
|
54
63
|
))}
|
|
55
64
|
</div>
|
|
56
65
|
)}
|
|
@@ -66,6 +75,6 @@ export const Footer = forwardRef<HTMLElement, FooterProps>(
|
|
|
66
75
|
</footer>
|
|
67
76
|
);
|
|
68
77
|
}
|
|
69
|
-
);
|
|
78
|
+
));
|
|
70
79
|
|
|
71
80
|
Footer.displayName = 'Footer';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modal Component (Organism)
|
|
3
|
-
* @description Dialog/overlay container
|
|
3
|
+
* @description Dialog/overlay container with optimized transitions
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { forwardRef, type HTMLAttributes } from 'react';
|
|
6
|
+
import { forwardRef, type HTMLAttributes, useEffect, useState } from 'react';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import type { BaseProps } from '../../domain/types';
|
|
9
10
|
|
|
@@ -22,9 +23,35 @@ const sizeStyles: Record<'sm' | 'md' | 'lg' | 'xl' | 'full', string> = {
|
|
|
22
23
|
full: 'max-w-full mx-4',
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
26
|
+
export const Modal = React.memo(forwardRef<HTMLDivElement, ModalProps>(
|
|
26
27
|
({ open = false, onClose, showCloseButton = true, size = 'md', className, children, ...props }, ref) => {
|
|
27
|
-
|
|
28
|
+
const [shouldRender, setShouldRender] = useState(open);
|
|
29
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (open) {
|
|
33
|
+
setShouldRender(true);
|
|
34
|
+
// Small delay to trigger animation
|
|
35
|
+
requestAnimationFrame(() => {
|
|
36
|
+
setIsAnimating(true);
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
setIsAnimating(false);
|
|
40
|
+
// Wait for animation to complete before unmounting
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
setShouldRender(false);
|
|
43
|
+
}, 200); // Match animation duration
|
|
44
|
+
return () => clearTimeout(timer);
|
|
45
|
+
}
|
|
46
|
+
}, [open]);
|
|
47
|
+
|
|
48
|
+
if (!shouldRender) return null;
|
|
49
|
+
|
|
50
|
+
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
51
|
+
if (e.target === e.currentTarget && onClose) {
|
|
52
|
+
onClose();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
28
55
|
|
|
29
56
|
return (
|
|
30
57
|
<div
|
|
@@ -33,8 +60,11 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
|
33
60
|
>
|
|
34
61
|
{/* Backdrop */}
|
|
35
62
|
<div
|
|
36
|
-
className=
|
|
37
|
-
|
|
63
|
+
className={`fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-200 ${
|
|
64
|
+
isAnimating ? 'opacity-100' : 'opacity-0'
|
|
65
|
+
}`}
|
|
66
|
+
onClick={handleBackdropClick}
|
|
67
|
+
aria-hidden="true"
|
|
38
68
|
/>
|
|
39
69
|
|
|
40
70
|
{/* Modal */}
|
|
@@ -42,7 +72,8 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
|
42
72
|
ref={ref}
|
|
43
73
|
className={cn(
|
|
44
74
|
'relative z-50 w-full rounded-lg border bg-card p-6 shadow-lg',
|
|
45
|
-
'
|
|
75
|
+
'transition-all duration-200',
|
|
76
|
+
isAnimating ? 'opacity-100 scale-100' : 'opacity-0 scale-95',
|
|
46
77
|
sizeStyles[size],
|
|
47
78
|
className
|
|
48
79
|
)}
|
|
@@ -53,6 +84,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
|
53
84
|
<button
|
|
54
85
|
onClick={onClose}
|
|
55
86
|
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
87
|
+
aria-label="Close modal"
|
|
56
88
|
>
|
|
57
89
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
58
90
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
@@ -66,7 +98,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
|
66
98
|
</div>
|
|
67
99
|
);
|
|
68
100
|
}
|
|
69
|
-
);
|
|
101
|
+
));
|
|
70
102
|
|
|
71
103
|
Modal.displayName = 'Modal';
|
|
72
104
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Navbar Component (Organism)
|
|
3
|
-
* @description Navigation bar with logo and links
|
|
3
|
+
* @description Navigation bar with logo and links - optimized for performance
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import type { BaseProps } from '../../domain/types';
|
|
9
10
|
|
|
@@ -17,7 +18,7 @@ const variantStyles: Record<'default' | 'sticky' | 'fixed', string> = {
|
|
|
17
18
|
fixed: 'fixed top-0 left-0 right-0 z-50',
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export const Navbar = forwardRef<HTMLElement, NavbarProps>(
|
|
21
|
+
export const Navbar = React.memo(forwardRef<HTMLElement, NavbarProps>(
|
|
21
22
|
({ className, variant = 'default', children, ...props }, ref) => {
|
|
22
23
|
return (
|
|
23
24
|
<nav
|
|
@@ -33,11 +34,11 @@ export const Navbar = forwardRef<HTMLElement, NavbarProps>(
|
|
|
33
34
|
</nav>
|
|
34
35
|
);
|
|
35
36
|
}
|
|
36
|
-
);
|
|
37
|
+
));
|
|
37
38
|
|
|
38
39
|
Navbar.displayName = 'Navbar';
|
|
39
40
|
|
|
40
|
-
export const NavbarBrand = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
41
|
+
export const NavbarBrand = React.memo(forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
41
42
|
({ className, ...props }, ref) => (
|
|
42
43
|
<div
|
|
43
44
|
ref={ref}
|
|
@@ -45,11 +46,11 @@ export const NavbarBrand = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElem
|
|
|
45
46
|
{...props}
|
|
46
47
|
/>
|
|
47
48
|
)
|
|
48
|
-
);
|
|
49
|
+
));
|
|
49
50
|
|
|
50
51
|
NavbarBrand.displayName = 'NavbarBrand';
|
|
51
52
|
|
|
52
|
-
export const NavbarLinks = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
53
|
+
export const NavbarLinks = React.memo(forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
53
54
|
({ className, ...props }, ref) => (
|
|
54
55
|
<div
|
|
55
56
|
ref={ref}
|
|
@@ -57,11 +58,11 @@ export const NavbarLinks = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElem
|
|
|
57
58
|
{...props}
|
|
58
59
|
/>
|
|
59
60
|
)
|
|
60
|
-
);
|
|
61
|
+
));
|
|
61
62
|
|
|
62
63
|
NavbarLinks.displayName = 'NavbarLinks';
|
|
63
64
|
|
|
64
|
-
export const NavbarActions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
65
|
+
export const NavbarActions = React.memo(forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
65
66
|
({ className, ...props }, ref) => (
|
|
66
67
|
<div
|
|
67
68
|
ref={ref}
|
|
@@ -69,6 +70,6 @@ export const NavbarActions = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEl
|
|
|
69
70
|
{...props}
|
|
70
71
|
/>
|
|
71
72
|
)
|
|
72
|
-
);
|
|
73
|
+
));
|
|
73
74
|
|
|
74
75
|
NavbarActions.displayName = 'NavbarActions';
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { forwardRef, type HTMLAttributes } from 'react';
|
|
7
|
+
import React from 'react';
|
|
7
8
|
import { cn } from '../../infrastructure/utils';
|
|
8
9
|
import type { BaseProps } from '../../domain/types';
|
|
9
10
|
|
|
@@ -62,7 +63,7 @@ export const TableFooter = forwardRef<HTMLTableSectionElement, HTMLAttributes<HT
|
|
|
62
63
|
|
|
63
64
|
TableFooter.displayName = 'TableFooter';
|
|
64
65
|
|
|
65
|
-
export const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
|
|
66
|
+
export const TableRow = React.memo(forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
|
|
66
67
|
({ className, ...props }, ref) => (
|
|
67
68
|
<tr
|
|
68
69
|
ref={ref}
|
|
@@ -73,11 +74,11 @@ export const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTable
|
|
|
73
74
|
{...props}
|
|
74
75
|
/>
|
|
75
76
|
)
|
|
76
|
-
);
|
|
77
|
+
));
|
|
77
78
|
|
|
78
79
|
TableRow.displayName = 'TableRow';
|
|
79
80
|
|
|
80
|
-
export const TableHead = forwardRef<HTMLTableCellElement, HTMLAttributes<HTMLTableCellElement>>(
|
|
81
|
+
export const TableHead = React.memo(forwardRef<HTMLTableCellElement, HTMLAttributes<HTMLTableCellElement>>(
|
|
81
82
|
({ className, ...props }, ref) => (
|
|
82
83
|
<th
|
|
83
84
|
ref={ref}
|
|
@@ -88,11 +89,11 @@ export const TableHead = forwardRef<HTMLTableCellElement, HTMLAttributes<HTMLTab
|
|
|
88
89
|
{...props}
|
|
89
90
|
/>
|
|
90
91
|
)
|
|
91
|
-
);
|
|
92
|
+
));
|
|
92
93
|
|
|
93
94
|
TableHead.displayName = 'TableHead';
|
|
94
95
|
|
|
95
|
-
export const TableCell = forwardRef<HTMLTableCellElement, HTMLAttributes<HTMLTableCellElement> & { colSpan?: number }>(
|
|
96
|
+
export const TableCell = React.memo(forwardRef<HTMLTableCellElement, HTMLAttributes<HTMLTableCellElement> & { colSpan?: number }>(
|
|
96
97
|
({ className, ...props }, ref) => (
|
|
97
98
|
<td
|
|
98
99
|
ref={ref}
|
|
@@ -103,7 +104,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, HTMLAttributes<HTMLTab
|
|
|
103
104
|
{...props}
|
|
104
105
|
/>
|
|
105
106
|
)
|
|
106
|
-
);
|
|
107
|
+
));
|
|
107
108
|
|
|
108
109
|
TableCell.displayName = 'TableCell';
|
|
109
110
|
|