@umituz/web-design-system 3.1.21 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -469
- package/dist/styles.css +2 -0
- package/package.json +30 -31
- package/src/__tests__/cn.test.ts +24 -0
- package/src/__tests__/components.smoke.test.tsx +78 -0
- package/src/__tests__/styles.contract.test.ts +60 -0
- package/src/domain/tokens/index.ts +8 -49
- package/src/index.ts +8 -10
- package/src/infrastructure/constants/component.constants.ts +3 -17
- package/src/infrastructure/constants/index.ts +4 -2
- package/src/infrastructure/constants/timing.constants.ts +46 -0
- package/src/infrastructure/utils/calculation.utils.ts +74 -0
- package/src/infrastructure/utils/index.ts +12 -0
- package/src/presentation/atoms/IconButton.tsx +55 -0
- package/src/presentation/atoms/Progress.tsx +2 -1
- package/src/presentation/atoms/Spinner.tsx +1 -1
- package/src/presentation/atoms/Switch.tsx +2 -2
- package/src/presentation/atoms/Text.tsx +15 -13
- package/src/presentation/atoms/Tooltip.tsx +2 -1
- package/src/presentation/atoms/index.ts +3 -0
- package/src/presentation/hooks/index.ts +0 -3
- package/src/presentation/hooks/useClickOutside.ts +14 -10
- package/src/presentation/hooks/useClipboard.ts +30 -12
- package/src/presentation/hooks/useDebounce.ts +8 -10
- package/src/presentation/hooks/useLocalStorage.ts +28 -18
- package/src/presentation/hooks/useMediaQuery.ts +2 -1
- package/src/presentation/hooks/useScrollLock.ts +40 -12
- package/src/presentation/hooks/useToggle.ts +8 -6
- package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
- package/src/presentation/molecules/Avatar.tsx +1 -1
- package/src/presentation/molecules/CodeBlock.tsx +51 -48
- package/src/presentation/molecules/ListItem.tsx +40 -29
- package/src/presentation/molecules/Select.tsx +1 -1
- package/src/presentation/molecules/Toggle.tsx +4 -4
- package/src/presentation/molecules/index.ts +0 -3
- package/src/presentation/organisms/Alert.tsx +1 -1
- package/src/presentation/organisms/DataTable.tsx +24 -11
- package/src/presentation/organisms/Dialog.tsx +1 -1
- package/src/presentation/organisms/DropdownMenu.tsx +2 -2
- package/src/presentation/organisms/HoverCard.tsx +1 -1
- package/src/presentation/organisms/LoadingState.tsx +1 -5
- package/src/presentation/organisms/Popover.tsx +1 -1
- package/src/presentation/organisms/StatCard.tsx +2 -2
- package/src/presentation/organisms/Table.tsx +2 -2
- package/src/presentation/organisms/index.ts +0 -60
- package/src/styles/compile.css +18 -0
- package/src/styles/theme.css +131 -0
- package/src/domain/tokens/animation.tokens.ts +0 -87
- package/src/domain/tokens/color.tokens.ts +0 -95
- package/src/domain/tokens/radius.tokens.ts +0 -27
- package/src/domain/tokens/shadow.tokens.ts +0 -25
- package/src/domain/tokens/spacing.tokens.ts +0 -65
- package/src/domain/tokens/typography.tokens.ts +0 -74
- package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
- package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
- package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
- package/src/infrastructure/error/index.ts +0 -5
- package/src/infrastructure/performance/index.ts +0 -92
- package/src/infrastructure/performance/useLazyLoading.ts +0 -342
- package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
- package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
- package/src/infrastructure/security/index.ts +0 -10
- package/src/infrastructure/security/security-config.ts +0 -171
- package/src/infrastructure/security/useFormValidation.ts +0 -216
- package/src/infrastructure/security/validation.ts +0 -246
- package/src/presentation/hooks/useLanguage.ts +0 -109
- package/src/presentation/molecules/Breadcrumb.tsx +0 -54
- package/src/presentation/organisms/AlertDialog.tsx +0 -181
- package/src/presentation/organisms/Calendar.tsx +0 -79
- package/src/presentation/organisms/Comments.tsx +0 -103
- package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
- package/src/presentation/organisms/FilterBar.tsx +0 -244
- package/src/presentation/organisms/FilterSidebar.tsx +0 -172
- package/src/presentation/organisms/Footer.tsx +0 -86
- package/src/presentation/organisms/FormModal.tsx +0 -126
- package/src/presentation/organisms/Grid.tsx +0 -128
- package/src/presentation/organisms/ImageLightbox.tsx +0 -252
- package/src/presentation/organisms/MainNavbar.tsx +0 -320
- package/src/presentation/organisms/Modal.tsx +0 -173
- package/src/presentation/organisms/Navbar.tsx +0 -75
- package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
- package/src/presentation/organisms/QuickActionCard.tsx +0 -93
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import React, { Suspense } from 'react';
|
|
2
|
-
import { ErrorBoundary } from './ErrorBoundary';
|
|
3
|
-
|
|
4
|
-
export interface SuspenseWrapperProps {
|
|
5
|
-
children: React.ReactNode;
|
|
6
|
-
fallback?: React.ReactNode;
|
|
7
|
-
errorFallback?: React.ReactNode;
|
|
8
|
-
loadingText?: string;
|
|
9
|
-
variant?: 'default' | 'minimal' | 'card';
|
|
10
|
-
className?: string;
|
|
11
|
-
style?: React.CSSProperties;
|
|
12
|
-
showErrorBoundary?: boolean;
|
|
13
|
-
errorBoundaryLevel?: 'page' | 'component' | 'feature';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const DefaultLoadingSpinner: React.FC<{ text?: string; className?: string }> = ({ text, className }) => (
|
|
17
|
-
<div className={className} style={{
|
|
18
|
-
display: 'flex',
|
|
19
|
-
flexDirection: 'column',
|
|
20
|
-
alignItems: 'center',
|
|
21
|
-
justifyContent: 'center',
|
|
22
|
-
gap: '16px',
|
|
23
|
-
padding: '32px'
|
|
24
|
-
}}>
|
|
25
|
-
<div style={{
|
|
26
|
-
width: '40px',
|
|
27
|
-
height: '40px',
|
|
28
|
-
border: '4px solid #e5e7eb',
|
|
29
|
-
borderTopColor: '#3b82f6',
|
|
30
|
-
borderRadius: '50%',
|
|
31
|
-
animation: 'spin 1s linear infinite'
|
|
32
|
-
}} />
|
|
33
|
-
{text && <p style={{ fontSize: '14px', color: '#666' }}>{text}</p>}
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
export const SuspenseWrapper: React.FC<SuspenseWrapperProps> = ({
|
|
38
|
-
children,
|
|
39
|
-
fallback,
|
|
40
|
-
errorFallback,
|
|
41
|
-
loadingText = 'Loading...',
|
|
42
|
-
variant = 'default',
|
|
43
|
-
className = '',
|
|
44
|
-
style,
|
|
45
|
-
showErrorBoundary = true,
|
|
46
|
-
errorBoundaryLevel = 'component'
|
|
47
|
-
}) => {
|
|
48
|
-
const defaultFallback = (
|
|
49
|
-
<DefaultLoadingSpinner
|
|
50
|
-
text={variant === 'card' ? loadingText : undefined}
|
|
51
|
-
className={className}
|
|
52
|
-
/>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const suspenseContent = (
|
|
56
|
-
<Suspense fallback={fallback || defaultFallback}>
|
|
57
|
-
{children}
|
|
58
|
-
</Suspense>
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const content = showErrorBoundary ? (
|
|
62
|
-
<ErrorBoundary
|
|
63
|
-
level={errorBoundaryLevel}
|
|
64
|
-
fallback={errorFallback}
|
|
65
|
-
>
|
|
66
|
-
{suspenseContent}
|
|
67
|
-
</ErrorBoundary>
|
|
68
|
-
) : suspenseContent;
|
|
69
|
-
|
|
70
|
-
// Wrap in div if className or style is provided
|
|
71
|
-
if (className || style) {
|
|
72
|
-
return <div className={className} style={style}>{content}</div>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return content;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Specialized Suspense wrappers
|
|
79
|
-
export const PageSuspense: React.FC<{
|
|
80
|
-
children: React.ReactNode;
|
|
81
|
-
loadingText?: string
|
|
82
|
-
}> = ({ children, loadingText = 'Loading page...' }) => (
|
|
83
|
-
<SuspenseWrapper
|
|
84
|
-
variant="card"
|
|
85
|
-
loadingText={loadingText}
|
|
86
|
-
errorBoundaryLevel="page"
|
|
87
|
-
className="min-h-[400px]"
|
|
88
|
-
>
|
|
89
|
-
{children}
|
|
90
|
-
</SuspenseWrapper>
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
export const ComponentSuspense: React.FC<{
|
|
94
|
-
children: React.ReactNode;
|
|
95
|
-
loadingText?: string;
|
|
96
|
-
className?: string;
|
|
97
|
-
}> = ({ children, loadingText = 'Loading component...', className }) => (
|
|
98
|
-
<SuspenseWrapper
|
|
99
|
-
variant="default"
|
|
100
|
-
loadingText={loadingText}
|
|
101
|
-
errorBoundaryLevel="component"
|
|
102
|
-
className={className}
|
|
103
|
-
>
|
|
104
|
-
{children}
|
|
105
|
-
</SuspenseWrapper>
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
export const FeatureSuspense: React.FC<{
|
|
109
|
-
children: React.ReactNode;
|
|
110
|
-
loadingText?: string
|
|
111
|
-
}> = ({ children, loadingText = 'Loading feature...' }) => (
|
|
112
|
-
<SuspenseWrapper
|
|
113
|
-
variant="card"
|
|
114
|
-
loadingText={loadingText}
|
|
115
|
-
errorBoundaryLevel="feature"
|
|
116
|
-
style={{ padding: '24px' }}
|
|
117
|
-
>
|
|
118
|
-
{children}
|
|
119
|
-
</SuspenseWrapper>
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
export const InlineSuspense: React.FC<{
|
|
123
|
-
children: React.ReactNode
|
|
124
|
-
}> = ({ children }) => (
|
|
125
|
-
<SuspenseWrapper
|
|
126
|
-
variant="minimal"
|
|
127
|
-
showErrorBoundary={false}
|
|
128
|
-
>
|
|
129
|
-
{children}
|
|
130
|
-
</SuspenseWrapper>
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
export default SuspenseWrapper;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { ErrorBoundary } from './ErrorBoundary';
|
|
2
|
-
export { ErrorDisplay, NetworkError, NotFoundError, PermissionError, ApiError } from './ErrorDisplay';
|
|
3
|
-
export type { ErrorDisplayProps } from './ErrorDisplay';
|
|
4
|
-
export { SuspenseWrapper, PageSuspense, ComponentSuspense, FeatureSuspense, InlineSuspense } from './SuspenseWrapper';
|
|
5
|
-
export type { SuspenseWrapperProps } from './SuspenseWrapper';
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
export { usePerformanceMonitor, useRenderPerformance } from './usePerformanceMonitor';
|
|
2
|
-
export { useLazyLoading } from './useLazyLoading';
|
|
3
|
-
export { useMemoryOptimization, useMemoryLeakDetector } from './useMemoryOptimization';
|
|
4
|
-
|
|
5
|
-
export type { PerformanceMetrics, PerformanceConfig } from './usePerformanceMonitor';
|
|
6
|
-
export type { MemoryOptimizationConfig, CleanupFunction } from './useMemoryOptimization';
|
|
7
|
-
|
|
8
|
-
// Performance monitoring utilities
|
|
9
|
-
export const performanceUtils = {
|
|
10
|
-
// Measure render performance
|
|
11
|
-
measureRender: (componentName: string, fn: () => void) => {
|
|
12
|
-
const start = performance.now();
|
|
13
|
-
fn();
|
|
14
|
-
const end = performance.now();
|
|
15
|
-
|
|
16
|
-
if (import.meta.env.DEV) {
|
|
17
|
-
const duration = end - start;
|
|
18
|
-
if (duration > 16) {
|
|
19
|
-
console.warn(`[Performance] ${componentName} took ${duration.toFixed(2)}ms to render`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return end - start;
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
// Measure async operation
|
|
27
|
-
measureAsync: async <T>(componentName: string, operation: string, fn: () => Promise<T>): Promise<T> => {
|
|
28
|
-
const start = performance.now();
|
|
29
|
-
try {
|
|
30
|
-
const result = await fn();
|
|
31
|
-
const duration = performance.now() - start;
|
|
32
|
-
|
|
33
|
-
if (import.meta.env.DEV) {
|
|
34
|
-
console.log(`[Performance] ${componentName} ${operation} completed in ${duration.toFixed(2)}ms`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return result;
|
|
38
|
-
} catch (error) {
|
|
39
|
-
const duration = performance.now() - start;
|
|
40
|
-
console.error(`[Performance] ${componentName} ${operation} failed after ${duration.toFixed(2)}ms:`, error);
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
// Get performance metrics
|
|
46
|
-
getMetrics: () => {
|
|
47
|
-
if (typeof performance === 'undefined') {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// performance.memory is not in standard TypeScript lib but available in Chrome-based browsers
|
|
52
|
-
interface PerformanceMemory {
|
|
53
|
-
usedJSHeapSize: number;
|
|
54
|
-
totalJSHeapSize: number;
|
|
55
|
-
jsHeapSizeLimit: number;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface PerformanceWithMemory extends Performance {
|
|
59
|
-
memory?: PerformanceMemory;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const mem = (performance as unknown as PerformanceWithMemory).memory;
|
|
63
|
-
if (!mem) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
memory: {
|
|
69
|
-
usedJSHeapSize: mem.usedJSHeapSize,
|
|
70
|
-
totalJSHeapSize: mem.totalJSHeapSize,
|
|
71
|
-
jsHeapSizeLimit: mem.jsHeapSizeLimit,
|
|
72
|
-
},
|
|
73
|
-
navigation: performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined,
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
// Log performance summary
|
|
78
|
-
logSummary: () => {
|
|
79
|
-
const metrics = performanceUtils.getMetrics();
|
|
80
|
-
if (!metrics) return;
|
|
81
|
-
|
|
82
|
-
if (import.meta.env.DEV && metrics.memory) {
|
|
83
|
-
const memoryUsage = ((metrics.memory.usedJSHeapSize / metrics.memory.jsHeapSizeLimit) * 100).toFixed(2);
|
|
84
|
-
console.log(`[Performance] Memory usage: ${memoryUsage}%`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (metrics.navigation) {
|
|
88
|
-
const loadTime = metrics.navigation.loadEventEnd - metrics.navigation.fetchStart;
|
|
89
|
-
console.log(`[Performance] Page load time: ${loadTime.toFixed(2)}ms`);
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
};
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
|
|
3
|
-
export const useIntersectionObserver = (
|
|
4
|
-
options: IntersectionObserverInit = {}
|
|
5
|
-
) => {
|
|
6
|
-
const [isIntersecting, setIsIntersecting] = useState(false);
|
|
7
|
-
const [hasIntersected, setHasIntersected] = useState(false);
|
|
8
|
-
const targetRef = useRef<HTMLElement>(null);
|
|
9
|
-
|
|
10
|
-
const defaultOptions = React.useMemo(() => ({
|
|
11
|
-
threshold: 0.1,
|
|
12
|
-
rootMargin: '50px',
|
|
13
|
-
...options,
|
|
14
|
-
}), [options]);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const target = targetRef.current;
|
|
18
|
-
if (!target) return;
|
|
19
|
-
|
|
20
|
-
const observer = new IntersectionObserver(
|
|
21
|
-
([entry]) => {
|
|
22
|
-
const isVisible = entry.isIntersecting;
|
|
23
|
-
setIsIntersecting(isVisible);
|
|
24
|
-
|
|
25
|
-
if (isVisible && !hasIntersected) {
|
|
26
|
-
setHasIntersected(true);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
defaultOptions
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
observer.observe(target);
|
|
33
|
-
|
|
34
|
-
return () => {
|
|
35
|
-
observer.unobserve(target);
|
|
36
|
-
};
|
|
37
|
-
}, [defaultOptions, hasIntersected]);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
targetRef,
|
|
41
|
-
isIntersecting,
|
|
42
|
-
hasIntersected,
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const useLazyImage = (src: string, placeholder?: string) => {
|
|
47
|
-
const [imageSrc, setImageSrc] = useState(placeholder || '');
|
|
48
|
-
const [isLoaded, setIsLoaded] = useState(false);
|
|
49
|
-
const [isError, setIsError] = useState(false);
|
|
50
|
-
const { targetRef, hasIntersected } = useIntersectionObserver();
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (!hasIntersected || !src) return;
|
|
54
|
-
|
|
55
|
-
const img = new Image();
|
|
56
|
-
|
|
57
|
-
img.onload = () => {
|
|
58
|
-
setImageSrc(src);
|
|
59
|
-
setIsLoaded(true);
|
|
60
|
-
setIsError(false);
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
img.onerror = () => {
|
|
64
|
-
setIsError(true);
|
|
65
|
-
setIsLoaded(false);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
img.src = src;
|
|
69
|
-
|
|
70
|
-
return () => {
|
|
71
|
-
img.onload = null;
|
|
72
|
-
img.onerror = null;
|
|
73
|
-
};
|
|
74
|
-
}, [hasIntersected, src]);
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
targetRef,
|
|
78
|
-
imageSrc,
|
|
79
|
-
isLoaded,
|
|
80
|
-
isError,
|
|
81
|
-
hasIntersected,
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export const useVirtualList = <T>(
|
|
86
|
-
items: T[],
|
|
87
|
-
options: {
|
|
88
|
-
itemHeight: number;
|
|
89
|
-
containerHeight: number;
|
|
90
|
-
overscan?: number;
|
|
91
|
-
}
|
|
92
|
-
) => {
|
|
93
|
-
const { itemHeight, containerHeight, overscan = 5 } = options;
|
|
94
|
-
const [scrollTop, setScrollTop] = useState(0);
|
|
95
|
-
const scrollElementRef = useRef<HTMLDivElement>(null);
|
|
96
|
-
|
|
97
|
-
const totalHeight = items.length * itemHeight;
|
|
98
|
-
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
|
|
99
|
-
const endIndex = Math.min(
|
|
100
|
-
items.length - 1,
|
|
101
|
-
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const visibleItems = items.slice(startIndex, endIndex + 1).map((item, index) => ({
|
|
105
|
-
item,
|
|
106
|
-
index: startIndex + index,
|
|
107
|
-
style: {
|
|
108
|
-
position: 'absolute' as const,
|
|
109
|
-
top: (startIndex + index) * itemHeight,
|
|
110
|
-
height: itemHeight,
|
|
111
|
-
width: '100%',
|
|
112
|
-
},
|
|
113
|
-
}));
|
|
114
|
-
|
|
115
|
-
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
|
116
|
-
setScrollTop(event.currentTarget.scrollTop);
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
scrollElementRef,
|
|
121
|
-
visibleItems,
|
|
122
|
-
totalHeight,
|
|
123
|
-
handleScroll,
|
|
124
|
-
startIndex,
|
|
125
|
-
endIndex,
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export const useResourcePreloader = () => {
|
|
130
|
-
const [preloadedResources, setPreloadedResources] = useState<Set<string>>(new Set());
|
|
131
|
-
const [loadingResources, setLoadingResources] = useState<Set<string>>(new Set());
|
|
132
|
-
|
|
133
|
-
const preloadImage = useCallback(async (src: string): Promise<void> => {
|
|
134
|
-
if (preloadedResources.has(src) || loadingResources.has(src)) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
setLoadingResources(prev => new Set(prev).add(src));
|
|
139
|
-
|
|
140
|
-
return new Promise((resolve, reject) => {
|
|
141
|
-
const img = new Image();
|
|
142
|
-
|
|
143
|
-
img.onload = () => {
|
|
144
|
-
setPreloadedResources(prev => new Set(prev).add(src));
|
|
145
|
-
setLoadingResources(prev => {
|
|
146
|
-
const newSet = new Set(prev);
|
|
147
|
-
newSet.delete(src);
|
|
148
|
-
return newSet;
|
|
149
|
-
});
|
|
150
|
-
resolve();
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
img.onerror = () => {
|
|
154
|
-
setLoadingResources(prev => {
|
|
155
|
-
const newSet = new Set(prev);
|
|
156
|
-
newSet.delete(src);
|
|
157
|
-
return newSet;
|
|
158
|
-
});
|
|
159
|
-
reject(new Error(`Failed to preload image: ${src}`));
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
img.src = src;
|
|
163
|
-
});
|
|
164
|
-
}, [preloadedResources, loadingResources]);
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
preloadImage,
|
|
168
|
-
preloadedResources: Array.from(preloadedResources),
|
|
169
|
-
loadingResources: Array.from(loadingResources),
|
|
170
|
-
isPreloaded: (src: string) => preloadedResources.has(src),
|
|
171
|
-
isLoading: (src: string) => loadingResources.has(src),
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
export const useLazyComponent = <TProps = Record<string, unknown>>(
|
|
176
|
-
importFunc: () => Promise<{ default: React.ComponentType<TProps> }>,
|
|
177
|
-
fallback?: React.ComponentType<TProps>
|
|
178
|
-
) => {
|
|
179
|
-
const [Component, setComponent] = useState<React.ComponentType<TProps> | null>(null);
|
|
180
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
181
|
-
const [error, setError] = useState<Error | null>(null);
|
|
182
|
-
const { targetRef, hasIntersected } = useIntersectionObserver();
|
|
183
|
-
|
|
184
|
-
const loadComponent = useCallback(async () => {
|
|
185
|
-
if (Component || isLoading) return;
|
|
186
|
-
|
|
187
|
-
setIsLoading(true);
|
|
188
|
-
setError(null);
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const module = await importFunc();
|
|
192
|
-
setComponent(() => module.default);
|
|
193
|
-
} catch (err) {
|
|
194
|
-
setError(err instanceof Error ? err : new Error('Failed to load component'));
|
|
195
|
-
} finally {
|
|
196
|
-
setIsLoading(false);
|
|
197
|
-
}
|
|
198
|
-
}, [importFunc, Component, isLoading]);
|
|
199
|
-
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
if (hasIntersected) {
|
|
202
|
-
loadComponent();
|
|
203
|
-
}
|
|
204
|
-
}, [hasIntersected, loadComponent]);
|
|
205
|
-
|
|
206
|
-
const LazyComponent = useCallback((props: TProps) => {
|
|
207
|
-
if (error) {
|
|
208
|
-
return fallback ? React.createElement(fallback as React.ElementType, props) : null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (isLoading || !Component) {
|
|
212
|
-
return fallback ? React.createElement(fallback as React.ElementType, props) : null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return React.createElement(Component as React.ElementType, props);
|
|
216
|
-
}, [Component, isLoading, error, fallback]);
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
targetRef,
|
|
220
|
-
LazyComponent,
|
|
221
|
-
isLoading,
|
|
222
|
-
error,
|
|
223
|
-
hasIntersected,
|
|
224
|
-
loadComponent,
|
|
225
|
-
};
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
export const useProgressiveEnhancement = () => {
|
|
229
|
-
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
230
|
-
const [connectionType, setConnectionType] = useState<'slow' | 'fast' | 'unknown'>('unknown');
|
|
231
|
-
|
|
232
|
-
const handleOnline = useCallback(() => {
|
|
233
|
-
setIsOnline(true);
|
|
234
|
-
}, []);
|
|
235
|
-
|
|
236
|
-
const handleOffline = useCallback(() => {
|
|
237
|
-
setIsOnline(false);
|
|
238
|
-
}, []);
|
|
239
|
-
|
|
240
|
-
useEffect(() => {
|
|
241
|
-
window.addEventListener('online', handleOnline);
|
|
242
|
-
window.addEventListener('offline', handleOffline);
|
|
243
|
-
|
|
244
|
-
interface NetworkInformation {
|
|
245
|
-
effectiveType?: string;
|
|
246
|
-
addEventListener?: (type: string, listener: () => void) => void;
|
|
247
|
-
removeEventListener?: (type: string, listener: () => void) => void;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function getConnection(): NetworkInformation | undefined {
|
|
251
|
-
const nav = navigator as unknown as {
|
|
252
|
-
connection?: unknown;
|
|
253
|
-
mozConnection?: unknown;
|
|
254
|
-
webkitConnection?: unknown;
|
|
255
|
-
};
|
|
256
|
-
const conn = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
257
|
-
if (
|
|
258
|
-
conn &&
|
|
259
|
-
(typeof conn === 'object') &&
|
|
260
|
-
('effectiveType' in conn)
|
|
261
|
-
) {
|
|
262
|
-
return conn as NetworkInformation;
|
|
263
|
-
}
|
|
264
|
-
return undefined;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const connection = getConnection();
|
|
268
|
-
|
|
269
|
-
if (connection) {
|
|
270
|
-
const updateConnectionType = () => {
|
|
271
|
-
const effectiveType = connection.effectiveType;
|
|
272
|
-
if (effectiveType === 'slow-2g' || effectiveType === '2g') {
|
|
273
|
-
setConnectionType('slow');
|
|
274
|
-
} else if (effectiveType === '3g' || effectiveType === '4g') {
|
|
275
|
-
setConnectionType('fast');
|
|
276
|
-
} else {
|
|
277
|
-
setConnectionType('unknown');
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
updateConnectionType();
|
|
282
|
-
connection.addEventListener?.('change', updateConnectionType);
|
|
283
|
-
|
|
284
|
-
return () => {
|
|
285
|
-
window.removeEventListener('online', handleOnline);
|
|
286
|
-
window.removeEventListener('offline', handleOffline);
|
|
287
|
-
connection.removeEventListener?.('change', updateConnectionType);
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return () => {
|
|
292
|
-
window.removeEventListener('online', handleOnline);
|
|
293
|
-
window.removeEventListener('offline', handleOffline);
|
|
294
|
-
};
|
|
295
|
-
}, [handleOnline, handleOffline]);
|
|
296
|
-
|
|
297
|
-
const shouldLoadHeavyContent = useCallback(() => {
|
|
298
|
-
return isOnline && connectionType !== 'slow';
|
|
299
|
-
}, [isOnline, connectionType]);
|
|
300
|
-
|
|
301
|
-
const shouldPreloadContent = useCallback(() => {
|
|
302
|
-
return isOnline && connectionType === 'fast';
|
|
303
|
-
}, [isOnline, connectionType]);
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
isOnline,
|
|
307
|
-
connectionType,
|
|
308
|
-
shouldLoadHeavyContent,
|
|
309
|
-
shouldPreloadContent,
|
|
310
|
-
};
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
export const useLazyLoading = (options: {
|
|
314
|
-
enableImageLazyLoading?: boolean;
|
|
315
|
-
enableComponentLazyLoading?: boolean;
|
|
316
|
-
enableVirtualization?: boolean;
|
|
317
|
-
enablePreloading?: boolean;
|
|
318
|
-
enableProgressiveEnhancement?: boolean;
|
|
319
|
-
} = {}) => {
|
|
320
|
-
const {
|
|
321
|
-
enableImageLazyLoading = true,
|
|
322
|
-
enableComponentLazyLoading = true,
|
|
323
|
-
enableVirtualization = true,
|
|
324
|
-
enablePreloading = true,
|
|
325
|
-
enableProgressiveEnhancement = true,
|
|
326
|
-
} = options;
|
|
327
|
-
|
|
328
|
-
const resourcePreloader = useResourcePreloader();
|
|
329
|
-
const progressiveEnhancement = useProgressiveEnhancement();
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
useIntersectionObserver,
|
|
333
|
-
useLazyImage: enableImageLazyLoading ? useLazyImage : null,
|
|
334
|
-
useLazyComponent: enableComponentLazyLoading ? useLazyComponent : null,
|
|
335
|
-
useVirtualList: enableVirtualization ? useVirtualList : null,
|
|
336
|
-
preloadImage: enablePreloading ? resourcePreloader.preloadImage : undefined,
|
|
337
|
-
shouldLoadHeavyContent: enableProgressiveEnhancement ? progressiveEnhancement.shouldLoadHeavyContent : undefined,
|
|
338
|
-
shouldPreloadContent: enableProgressiveEnhancement ? progressiveEnhancement.shouldPreloadContent : undefined,
|
|
339
|
-
isOnline: enableProgressiveEnhancement ? progressiveEnhancement.isOnline : undefined,
|
|
340
|
-
connectionType: enableProgressiveEnhancement ? progressiveEnhancement.connectionType : undefined,
|
|
341
|
-
};
|
|
342
|
-
};
|