@umituz/web-design-system 3.1.22 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +31 -31
  4. package/src/__tests__/cn.test.ts +24 -0
  5. package/src/__tests__/components.smoke.test.tsx +78 -0
  6. package/src/__tests__/styles.contract.test.ts +60 -0
  7. package/src/domain/tokens/index.ts +8 -49
  8. package/src/index.ts +8 -10
  9. package/src/infrastructure/constants/component.constants.ts +3 -17
  10. package/src/infrastructure/constants/index.ts +4 -2
  11. package/src/infrastructure/constants/timing.constants.ts +46 -0
  12. package/src/infrastructure/utils/calculation.utils.ts +74 -0
  13. package/src/infrastructure/utils/cn.util.ts +1 -1
  14. package/src/infrastructure/utils/index.ts +12 -0
  15. package/src/presentation/atoms/IconButton.tsx +55 -0
  16. package/src/presentation/atoms/Progress.tsx +2 -1
  17. package/src/presentation/atoms/Spinner.tsx +1 -1
  18. package/src/presentation/atoms/Switch.tsx +2 -2
  19. package/src/presentation/atoms/Text.tsx +15 -13
  20. package/src/presentation/atoms/Tooltip.tsx +2 -1
  21. package/src/presentation/atoms/index.ts +3 -0
  22. package/src/presentation/hooks/index.ts +0 -3
  23. package/src/presentation/hooks/useClickOutside.ts +16 -10
  24. package/src/presentation/hooks/useClipboard.ts +30 -12
  25. package/src/presentation/hooks/useDebounce.ts +9 -11
  26. package/src/presentation/hooks/useLocalStorage.ts +32 -20
  27. package/src/presentation/hooks/useMediaQuery.ts +20 -15
  28. package/src/presentation/hooks/useScrollLock.ts +40 -12
  29. package/src/presentation/hooks/useToggle.ts +8 -6
  30. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  31. package/src/presentation/molecules/Avatar.tsx +1 -1
  32. package/src/presentation/molecules/CodeBlock.tsx +50 -48
  33. package/src/presentation/molecules/InputGroup.tsx +1 -1
  34. package/src/presentation/molecules/ListItem.tsx +40 -29
  35. package/src/presentation/molecules/Select.tsx +1 -1
  36. package/src/presentation/molecules/Toggle.tsx +4 -4
  37. package/src/presentation/molecules/index.ts +0 -3
  38. package/src/presentation/organisms/Alert.tsx +1 -1
  39. package/src/presentation/organisms/DataTable.tsx +26 -15
  40. package/src/presentation/organisms/Dialog.tsx +1 -1
  41. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  42. package/src/presentation/organisms/HoverCard.tsx +1 -1
  43. package/src/presentation/organisms/LoadingState.tsx +1 -5
  44. package/src/presentation/organisms/Popover.tsx +1 -1
  45. package/src/presentation/organisms/StatCard.tsx +2 -2
  46. package/src/presentation/organisms/Table.tsx +2 -2
  47. package/src/presentation/organisms/index.ts +0 -60
  48. package/src/styles/compile.css +18 -0
  49. package/src/styles/theme.css +131 -0
  50. package/src/domain/tokens/animation.tokens.ts +0 -87
  51. package/src/domain/tokens/color.tokens.ts +0 -95
  52. package/src/domain/tokens/radius.tokens.ts +0 -27
  53. package/src/domain/tokens/shadow.tokens.ts +0 -25
  54. package/src/domain/tokens/spacing.tokens.ts +0 -65
  55. package/src/domain/tokens/typography.tokens.ts +0 -74
  56. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  57. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  58. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  59. package/src/infrastructure/error/index.ts +0 -5
  60. package/src/infrastructure/performance/index.ts +0 -92
  61. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  62. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  63. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  64. package/src/infrastructure/security/index.ts +0 -10
  65. package/src/infrastructure/security/security-config.ts +0 -171
  66. package/src/infrastructure/security/useFormValidation.ts +0 -216
  67. package/src/infrastructure/security/validation.ts +0 -246
  68. package/src/presentation/hooks/useLanguage.ts +0 -109
  69. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  70. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  71. package/src/presentation/organisms/Calendar.tsx +0 -79
  72. package/src/presentation/organisms/Comments.tsx +0 -103
  73. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  74. package/src/presentation/organisms/FilterBar.tsx +0 -244
  75. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  76. package/src/presentation/organisms/Footer.tsx +0 -86
  77. package/src/presentation/organisms/FormModal.tsx +0 -126
  78. package/src/presentation/organisms/Grid.tsx +0 -128
  79. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  80. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  81. package/src/presentation/organisms/Modal.tsx +0 -173
  82. package/src/presentation/organisms/Navbar.tsx +0 -75
  83. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  84. package/src/presentation/organisms/QuickActionCard.tsx +0 -93
@@ -1,346 +0,0 @@
1
- import React from 'react';
2
-
3
- export interface ErrorDisplayProps {
4
- error?: Error | string | null;
5
- title?: string;
6
- description?: string;
7
- variant?: 'default' | 'minimal' | 'card' | 'inline';
8
- severity?: 'error' | 'warning' | 'info';
9
- showDetails?: boolean;
10
- showRetry?: boolean;
11
- showHome?: boolean;
12
- onRetry?: () => void;
13
- onHome?: () => void;
14
- className?: string;
15
- errorId?: string;
16
- }
17
-
18
- const severityConfig = {
19
- error: {
20
- icon: '❌',
21
- color: '#dc2626',
22
- bgColor: '#fef2f2',
23
- borderColor: '#fecaca'
24
- },
25
- warning: {
26
- icon: '⚠️',
27
- color: '#d97706',
28
- bgColor: '#fffbeb',
29
- borderColor: '#fde68a'
30
- },
31
- info: {
32
- icon: 'ℹ️',
33
- color: '#2563eb',
34
- bgColor: '#eff6ff',
35
- borderColor: '#bfdbfe'
36
- }
37
- };
38
-
39
- export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
40
- error,
41
- title = 'Something went wrong',
42
- description = 'An unexpected error occurred. Please try again.',
43
- variant = 'default',
44
- severity = 'error',
45
- showDetails = false,
46
- showRetry = true,
47
- showHome = false,
48
- onRetry,
49
- onHome,
50
- className = '',
51
- errorId
52
- }) => {
53
- const config = severityConfig[severity];
54
-
55
- const errorMessage = error instanceof Error ? error.message : (error || 'Unknown error');
56
- const errorStack = error instanceof Error ? error.stack : undefined;
57
-
58
- if (variant === 'minimal') {
59
- return (
60
- <div className={className} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: config.color }}>
61
- <span>{config.icon}</span>
62
- <span>{errorMessage}</span>
63
- </div>
64
- );
65
- }
66
-
67
- if (variant === 'inline') {
68
- return (
69
- <div className={className} style={{
70
- padding: '12px 16px',
71
- background: config.bgColor,
72
- border: `1px solid ${config.borderColor}`,
73
- borderRadius: '6px'
74
- }}>
75
- <span style={{ marginRight: '8px' }}>{config.icon}</span>
76
- <span style={{ fontWeight: 500 }}>{title}:</span> {errorMessage}
77
- </div>
78
- );
79
- }
80
-
81
- if (variant === 'card') {
82
- return (
83
- <div className={className} style={{
84
- background: 'white',
85
- borderRadius: '8px',
86
- boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
87
- border: 'none'
88
- }}>
89
- <div style={{ padding: '24px' }}>
90
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
91
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
92
- <div style={{
93
- width: '48px',
94
- height: '48px',
95
- borderRadius: '50%',
96
- display: 'flex',
97
- alignItems: 'center',
98
- justifyContent: 'center',
99
- background: config.bgColor,
100
- fontSize: '24px'
101
- }}>
102
- {config.icon}
103
- </div>
104
- <div>
105
- <h3 style={{ color: config.color, margin: 0, fontSize: '18px', fontWeight: 600 }}>
106
- {title}
107
- </h3>
108
- <p style={{ color: '#666', margin: 0, fontSize: '14px' }}>
109
- {description}
110
- </p>
111
- </div>
112
- </div>
113
- {errorId && (
114
- <span style={{
115
- background: severity === 'error' ? '#dc2626' : '#6b7280',
116
- color: 'white',
117
- padding: '4px 8px',
118
- borderRadius: '4px',
119
- fontSize: '12px',
120
- fontWeight: 500
121
- }}>
122
- ID: {errorId.slice(-8)}
123
- </span>
124
- )}
125
- </div>
126
-
127
- <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
128
- {showDetails && errorStack && (
129
- <div style={{ padding: '16px', background: '#f5f5f5', borderRadius: '8px' }}>
130
- <h4 style={{ fontWeight: 600, fontSize: '14px', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}>
131
- 🐛 Error Details
132
- </h4>
133
- <pre style={{ fontSize: '12px', color: '#666', overflow: 'auto', maxHeight: '128px' }}>
134
- {errorStack}
135
- </pre>
136
- </div>
137
- )}
138
-
139
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
140
- {showRetry && onRetry && (
141
- <button
142
- onClick={onRetry}
143
- style={{
144
- padding: '8px 16px',
145
- background: 'white',
146
- border: '1px solid #d1d5db',
147
- borderRadius: '6px',
148
- fontSize: '14px',
149
- fontWeight: 500,
150
- cursor: 'pointer',
151
- display: 'flex',
152
- alignItems: 'center',
153
- gap: '8px'
154
- }}
155
- >
156
- 🔄 Try Again
157
- </button>
158
- )}
159
- <button
160
- onClick={() => window.location.reload()}
161
- style={{
162
- padding: '8px 16px',
163
- background: '#3b82f6',
164
- color: 'white',
165
- border: 'none',
166
- borderRadius: '6px',
167
- fontSize: '14px',
168
- fontWeight: 500,
169
- cursor: 'pointer',
170
- display: 'flex',
171
- alignItems: 'center',
172
- gap: '8px'
173
- }}
174
- >
175
- 🔄 Refresh Page
176
- </button>
177
- {showHome && onHome && (
178
- <button
179
- onClick={onHome}
180
- style={{
181
- padding: '8px 16px',
182
- background: '#6b7280',
183
- color: 'white',
184
- border: 'none',
185
- borderRadius: '6px',
186
- fontSize: '14px',
187
- fontWeight: 500,
188
- cursor: 'pointer',
189
- display: 'flex',
190
- alignItems: 'center',
191
- gap: '8px'
192
- }}
193
- >
194
- 🏠 Go Home
195
- </button>
196
- )}
197
- </div>
198
- </div>
199
- </div>
200
- </div>
201
- );
202
- }
203
-
204
- // Default variant
205
- return (
206
- <div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '32px' }}>
207
- <div style={{
208
- width: '100%',
209
- maxWidth: '448px',
210
- background: 'white',
211
- borderRadius: '8px',
212
- boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
213
- overflow: 'hidden'
214
- }}>
215
- <div style={{ padding: '24px' }}>
216
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
217
- <div style={{
218
- width: '40px',
219
- height: '40px',
220
- borderRadius: '50%',
221
- display: 'flex',
222
- alignItems: 'center',
223
- justifyContent: 'center',
224
- background: config.bgColor,
225
- fontSize: '20px'
226
- }}>
227
- {config.icon}
228
- </div>
229
- <div>
230
- <h3 style={{ color: config.color, margin: 0, fontSize: '16px', fontWeight: 600 }}>
231
- {title}
232
- </h3>
233
- <p style={{ color: '#666', margin: 0, fontSize: '14px' }}>
234
- {description}
235
- </p>
236
- </div>
237
- </div>
238
- </div>
239
-
240
- <div style={{ padding: '0 24px 24px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
241
- <p style={{ fontSize: '14px', color: '#666', margin: 0 }}>{errorMessage}</p>
242
- <div style={{ display: 'flex', gap: '8px' }}>
243
- {showRetry && onRetry && (
244
- <button
245
- onClick={onRetry}
246
- style={{
247
- padding: '8px 16px',
248
- background: 'white',
249
- border: '1px solid #d1d5db',
250
- borderRadius: '6px',
251
- fontSize: '14px',
252
- fontWeight: 500,
253
- cursor: 'pointer',
254
- display: 'flex',
255
- alignItems: 'center',
256
- gap: '8px'
257
- }}
258
- >
259
- 🔄 Retry
260
- </button>
261
- )}
262
- {showHome && onHome && (
263
- <button
264
- onClick={onHome}
265
- style={{
266
- padding: '8px 16px',
267
- background: '#6b7280',
268
- color: 'white',
269
- border: 'none',
270
- borderRadius: '6px',
271
- fontSize: '14px',
272
- fontWeight: 500,
273
- cursor: 'pointer',
274
- display: 'flex',
275
- alignItems: 'center',
276
- gap: '8px'
277
- }}
278
- >
279
- 🏠 Home
280
- </button>
281
- )}
282
- </div>
283
- </div>
284
- </div>
285
- </div>
286
- );
287
- };
288
-
289
- // Specialized error components
290
- export const NetworkError: React.FC<{ onRetry?: () => void; className?: string }> = ({
291
- onRetry,
292
- className
293
- }) => (
294
- <ErrorDisplay
295
- title="Network Error"
296
- description="Unable to connect to the server. Please check your internet connection."
297
- severity="warning"
298
- showRetry={true}
299
- onRetry={onRetry}
300
- className={className}
301
- />
302
- );
303
-
304
- export const NotFoundError: React.FC<{ onHome?: () => void; className?: string }> = ({
305
- onHome,
306
- className
307
- }) => (
308
- <ErrorDisplay
309
- title="Page Not Found"
310
- description="The page you're looking for doesn't exist or has been moved."
311
- severity="info"
312
- showHome={true}
313
- showRetry={false}
314
- onHome={onHome}
315
- className={className}
316
- />
317
- );
318
-
319
- export const PermissionError: React.FC<{ className?: string }> = ({ className }) => (
320
- <ErrorDisplay
321
- title="Access Denied"
322
- description="You don't have permission to access this resource."
323
- severity="error"
324
- showRetry={false}
325
- showHome={true}
326
- className={className}
327
- />
328
- );
329
-
330
- export const ApiError: React.FC<{
331
- error?: Error | string;
332
- onRetry?: () => void;
333
- className?: string
334
- }> = ({ error, onRetry, className }) => (
335
- <ErrorDisplay
336
- error={error}
337
- title="API Error"
338
- description="Failed to load data from the server."
339
- severity="error"
340
- showRetry={true}
341
- onRetry={onRetry}
342
- className={className}
343
- />
344
- );
345
-
346
- export default ErrorDisplay;
@@ -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
- };