@umituz/web-design-system 3.1.7 → 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 +39 -88
- 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,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Footer Organism Component
|
|
3
|
-
* @description
|
|
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
|
|
|
10
11
|
export interface FooterProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
11
12
|
brand?: {
|
|
12
13
|
name: string;
|
|
13
|
-
description?: string;
|
|
14
14
|
};
|
|
15
|
-
sections?: Array<{
|
|
16
|
-
title: string;
|
|
17
|
-
links: Array<{ label: string; href: string }>;
|
|
18
|
-
}>;
|
|
19
15
|
social?: Array<{
|
|
20
16
|
name: string;
|
|
21
17
|
href: string;
|
|
@@ -24,106 +20,61 @@ export interface FooterProps extends HTMLAttributes<HTMLElement>, BaseProps {
|
|
|
24
20
|
copyright?: string;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
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>(
|
|
42
|
+
({ className, brand, social, copyright = '© 2026 UmitUZ. All rights reserved.', ...props }, ref) => {
|
|
29
43
|
return (
|
|
30
44
|
<footer
|
|
31
45
|
ref={ref}
|
|
32
|
-
className={cn('relative mt-
|
|
46
|
+
className={cn('relative mt-8 transition-theme', className)}
|
|
33
47
|
{...props}
|
|
34
48
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
50% { opacity: 0.5; transform: scale(1.1) translate(-10px, -10px); }
|
|
39
|
-
}
|
|
40
|
-
.animate-gradient-shift {
|
|
41
|
-
animation: gradient-shift 8s ease-in-out infinite;
|
|
42
|
-
}
|
|
43
|
-
`}</style>
|
|
44
|
-
|
|
45
|
-
{/* Animated Gradient Background */}
|
|
46
|
-
<div className="absolute inset-0 bg-gradient-to-br from-bg-card via-bg-secondary/20 to-bg-card"></div>
|
|
47
|
-
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-primary/8 via-transparent to-transparent animate-gradient-shift"></div>
|
|
48
|
-
|
|
49
|
-
{/* Gradient Top Border */}
|
|
50
|
-
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/50 to-transparent"></div>
|
|
51
|
-
|
|
52
|
-
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
|
|
53
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10 lg:gap-12">
|
|
54
|
-
{/* Brand Section - Prominent */}
|
|
49
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
50
|
+
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
|
51
|
+
{/* Brand */}
|
|
55
52
|
{brand && (
|
|
56
|
-
<div className="
|
|
57
|
-
|
|
58
|
-
<h3 className="text-2xl font-bold text-text-primary tracking-tight">
|
|
59
|
-
{brand.name}
|
|
60
|
-
</h3>
|
|
61
|
-
{brand.description && (
|
|
62
|
-
<p className="text-text-secondary text-sm leading-relaxed">{brand.description}</p>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
{/* Social Icons */}
|
|
67
|
-
{social && social.length > 0 && (
|
|
68
|
-
<div className="flex items-center gap-3">
|
|
69
|
-
{social.map((item, index) => (
|
|
70
|
-
<a
|
|
71
|
-
key={index}
|
|
72
|
-
href={item.href}
|
|
73
|
-
target="_blank"
|
|
74
|
-
rel="noopener noreferrer"
|
|
75
|
-
className="group relative flex items-center justify-center w-12 h-12 rounded-xl bg-bg-secondary/50 text-text-secondary hover:bg-primary-gradient hover:text-white border border-border/50 hover:border-transparent transition-all duration-300 hover:scale-110 hover:shadow-lg hover:shadow-primary/20"
|
|
76
|
-
aria-label={item.name}
|
|
77
|
-
title={item.name}
|
|
78
|
-
>
|
|
79
|
-
{/* Glow effect */}
|
|
80
|
-
<span className="absolute inset-0 rounded-xl bg-primary/15 blur-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
|
81
|
-
{/* Icon */}
|
|
82
|
-
<span className="relative">{item.icon}</span>
|
|
83
|
-
</a>
|
|
84
|
-
))}
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
53
|
+
<div className="text-xl font-bold text-text-primary">
|
|
54
|
+
{brand.name}
|
|
87
55
|
</div>
|
|
88
56
|
)}
|
|
89
57
|
|
|
90
|
-
{/*
|
|
91
|
-
|
|
92
|
-
<div className="
|
|
93
|
-
{
|
|
94
|
-
<
|
|
95
|
-
<h4 className="font-semibold text-text-primary text-sm uppercase tracking-wider">{section.title}</h4>
|
|
96
|
-
<ul className="space-y-2">
|
|
97
|
-
{section.links.map((link, linkIndex) => (
|
|
98
|
-
<li key={linkIndex}>
|
|
99
|
-
<a
|
|
100
|
-
href={link.href}
|
|
101
|
-
className="text-text-secondary text-sm hover:text-primary-light transition-colors duration-200 inline-flex items-center gap-2 group"
|
|
102
|
-
>
|
|
103
|
-
<span className="opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-primary-light text-xs">→</span>
|
|
104
|
-
<span>{link.label}</span>
|
|
105
|
-
</a>
|
|
106
|
-
</li>
|
|
107
|
-
))}
|
|
108
|
-
</ul>
|
|
109
|
-
</div>
|
|
58
|
+
{/* Social Icons */}
|
|
59
|
+
{social && social.length > 0 && (
|
|
60
|
+
<div className="flex items-center gap-3">
|
|
61
|
+
{social.map((item, index) => (
|
|
62
|
+
<SocialIcon key={`${item.name}-${index}`} item={item} />
|
|
110
63
|
))}
|
|
111
64
|
</div>
|
|
112
|
-
|
|
65
|
+
)}
|
|
113
66
|
</div>
|
|
114
67
|
|
|
115
|
-
{/* Copyright
|
|
68
|
+
{/* Copyright */}
|
|
116
69
|
{copyright && (
|
|
117
|
-
<div className="border-t border-border/
|
|
118
|
-
<
|
|
119
|
-
<p className="text-sm text-text-secondary/80">{copyright}</p>
|
|
120
|
-
</div>
|
|
70
|
+
<div className="text-center mt-6 pt-6 border-t border-border/20">
|
|
71
|
+
<p className="text-sm text-text-secondary/60">{copyright}</p>
|
|
121
72
|
</div>
|
|
122
73
|
)}
|
|
123
74
|
</div>
|
|
124
75
|
</footer>
|
|
125
76
|
);
|
|
126
77
|
}
|
|
127
|
-
);
|
|
78
|
+
));
|
|
128
79
|
|
|
129
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
|
|