@umituz/web-design-system 1.0.4 → 1.3.1

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 (41) hide show
  1. package/README.md +181 -0
  2. package/package.json +18 -5
  3. package/src/global.d.ts +20 -0
  4. package/src/index.ts +3 -0
  5. package/src/infrastructure/error/ErrorBoundary.tsx +258 -0
  6. package/src/infrastructure/error/ErrorDisplay.tsx +346 -0
  7. package/src/infrastructure/error/SuspenseWrapper.tsx +134 -0
  8. package/src/infrastructure/error/index.ts +5 -0
  9. package/src/infrastructure/performance/index.ts +6 -0
  10. package/src/infrastructure/performance/useLazyLoading.ts +342 -0
  11. package/src/infrastructure/performance/useMemoryOptimization.ts +293 -0
  12. package/src/infrastructure/performance/usePerformanceMonitor.ts +158 -0
  13. package/src/infrastructure/security/index.ts +10 -0
  14. package/src/infrastructure/security/security-config.ts +171 -0
  15. package/src/infrastructure/security/useFormValidation.ts +216 -0
  16. package/src/infrastructure/security/validation.ts +242 -0
  17. package/src/infrastructure/utils/cn.util.ts +7 -7
  18. package/src/infrastructure/utils/index.ts +1 -1
  19. package/src/presentation/atoms/Input.tsx +1 -1
  20. package/src/presentation/atoms/Slider.tsx +1 -1
  21. package/src/presentation/atoms/Text.tsx +1 -1
  22. package/src/presentation/atoms/Tooltip.tsx +2 -2
  23. package/src/presentation/hooks/index.ts +2 -1
  24. package/src/presentation/hooks/useClickOutside.ts +1 -1
  25. package/src/presentation/molecules/CheckboxGroup.tsx +1 -1
  26. package/src/presentation/molecules/FormField.tsx +2 -2
  27. package/src/presentation/molecules/InputGroup.tsx +1 -1
  28. package/src/presentation/molecules/RadioGroup.tsx +1 -1
  29. package/src/presentation/organisms/Alert.tsx +1 -1
  30. package/src/presentation/organisms/Card.tsx +1 -1
  31. package/src/presentation/organisms/Footer.tsx +99 -0
  32. package/src/presentation/organisms/Navbar.tsx +1 -1
  33. package/src/presentation/organisms/Table.tsx +1 -1
  34. package/src/presentation/organisms/index.ts +3 -0
  35. package/src/presentation/templates/Form.tsx +2 -2
  36. package/src/presentation/templates/List.tsx +1 -1
  37. package/src/presentation/templates/PageHeader.tsx +78 -0
  38. package/src/presentation/templates/PageLayout.tsx +38 -0
  39. package/src/presentation/templates/ProjectSkeleton.tsx +35 -0
  40. package/src/presentation/templates/Section.tsx +1 -1
  41. package/src/presentation/templates/index.ts +9 -0
@@ -0,0 +1,158 @@
1
+ import { useEffect, useRef, useState, useCallback } from 'react';
2
+
3
+ export interface PerformanceMetrics {
4
+ renderTime: number;
5
+ mountTime: number;
6
+ updateCount: number;
7
+ lastRenderTime: number;
8
+ averageRenderTime: number;
9
+ memoryUsage?: number;
10
+ componentName?: string;
11
+ }
12
+
13
+ export interface PerformanceConfig {
14
+ trackRenders?: boolean;
15
+ trackMemory?: boolean;
16
+ sampleRate?: number;
17
+ componentName?: string;
18
+ onMetricsUpdate?: (metrics: PerformanceMetrics) => void;
19
+ }
20
+
21
+ export const usePerformanceMonitor = (config: PerformanceConfig = {}) => {
22
+ const {
23
+ trackRenders = true,
24
+ trackMemory = false,
25
+ sampleRate = 1.0,
26
+ componentName = 'Unknown',
27
+ onMetricsUpdate
28
+ } = config;
29
+
30
+ const [metrics, setMetrics] = useState<PerformanceMetrics>({
31
+ renderTime: 0,
32
+ mountTime: 0,
33
+ updateCount: 0,
34
+ lastRenderTime: 0,
35
+ averageRenderTime: 0,
36
+ componentName
37
+ });
38
+
39
+ const mountTimeRef = useRef<number>(0);
40
+ const renderStartRef = useRef<number>(0);
41
+ const renderTimesRef = useRef<number[]>([]);
42
+ const updateCountRef = useRef<number>(0);
43
+
44
+ useEffect(() => {
45
+ mountTimeRef.current = performance.now();
46
+ renderStartRef.current = performance.now();
47
+
48
+ return () => {
49
+ // Component unmount
50
+ };
51
+ }, [componentName]);
52
+
53
+ useEffect(() => {
54
+ if (!trackRenders || Math.random() > sampleRate) return;
55
+
56
+ const renderEndTime = performance.now();
57
+ const renderTime = renderEndTime - renderStartRef.current;
58
+
59
+ renderTimesRef.current.push(renderTime);
60
+ updateCountRef.current += 1;
61
+
62
+ if (renderTimesRef.current.length > 50) {
63
+ renderTimesRef.current = renderTimesRef.current.slice(-50);
64
+ }
65
+
66
+ const averageRenderTime = renderTimesRef.current.reduce((sum, time) => sum + time, 0) / renderTimesRef.current.length;
67
+
68
+ let memoryUsage: number | undefined;
69
+ if (trackMemory && 'memory' in performance) {
70
+ memoryUsage = (performance as { memory?: { usedJSHeapSize: number } }).memory?.usedJSHeapSize;
71
+ }
72
+
73
+ const newMetrics: PerformanceMetrics = {
74
+ renderTime,
75
+ mountTime: renderEndTime - mountTimeRef.current,
76
+ updateCount: updateCountRef.current,
77
+ lastRenderTime: renderTime,
78
+ averageRenderTime,
79
+ memoryUsage,
80
+ componentName
81
+ };
82
+
83
+ setMetrics(newMetrics);
84
+ onMetricsUpdate?.(newMetrics);
85
+
86
+ renderStartRef.current = performance.now();
87
+
88
+ if (import.meta.env.DEV) {
89
+ if (renderTime > 16) {
90
+ console.warn(`[Performance] Slow render detected: ${renderTime.toFixed(2)}ms`);
91
+ }
92
+ if (updateCountRef.current % 10 === 0) {
93
+ console.log({
94
+ renders: updateCountRef.current,
95
+ avgRenderTime: averageRenderTime.toFixed(2) + 'ms',
96
+ lastRenderTime: renderTime.toFixed(2) + 'ms'
97
+ });
98
+ }
99
+ }
100
+ }, [trackRenders, sampleRate, trackMemory, componentName, onMetricsUpdate]);
101
+
102
+ const resetMetrics = useCallback(() => {
103
+ renderTimesRef.current = [];
104
+ updateCountRef.current = 0;
105
+ mountTimeRef.current = performance.now();
106
+ renderStartRef.current = performance.now();
107
+
108
+ setMetrics({
109
+ renderTime: 0,
110
+ mountTime: 0,
111
+ updateCount: 0,
112
+ lastRenderTime: 0,
113
+ averageRenderTime: 0,
114
+ componentName
115
+ });
116
+ }, [componentName]);
117
+
118
+ const getPerformanceReport = useCallback(() => {
119
+ return {
120
+ ...metrics,
121
+ renderTimes: [...renderTimesRef.current],
122
+ isSlowComponent: metrics.averageRenderTime > 16,
123
+ performanceGrade: metrics.averageRenderTime < 5 ? 'A' :
124
+ metrics.averageRenderTime < 10 ? 'B' :
125
+ metrics.averageRenderTime < 16 ? 'C' : 'D'
126
+ };
127
+ }, [metrics]);
128
+
129
+ return {
130
+ metrics,
131
+ resetMetrics,
132
+ getPerformanceReport
133
+ };
134
+ };
135
+
136
+ export const useRenderPerformance = (componentName?: string) => {
137
+ const renderStartTime = useRef<number>(0);
138
+ const [renderStats, setRenderStats] = useState<{
139
+ lastRenderTime: number;
140
+ renderCount: number;
141
+ }>({
142
+ lastRenderTime: 0,
143
+ renderCount: 0
144
+ });
145
+
146
+ const startTime = performance.now();
147
+
148
+ useEffect(() => {
149
+ setRenderStats(prev => ({
150
+ lastRenderTime: performance.now() - startTime,
151
+ renderCount: prev.renderCount + 1
152
+ }));
153
+
154
+ renderStartTime.current = performance.now();
155
+ }, [componentName, renderStats, startTime]);
156
+
157
+ return renderStats;
158
+ };
@@ -0,0 +1,10 @@
1
+ export * from './security-config';
2
+ export * from './validation';
3
+ export {
4
+ sanitizeInput as FormSanitizeInput,
5
+ useFormValidation,
6
+ COMMON_RULES,
7
+ VALIDATION_PATTERNS,
8
+ type ValidationRule,
9
+ type ValidationRules
10
+ } from './useFormValidation';
@@ -0,0 +1,171 @@
1
+ import type { ValidationConfig } from './validation';
2
+
3
+ /**
4
+ * Content Security Policy configuration
5
+ * Helps prevent XSS attacks by controlling resource loading
6
+ */
7
+ export const CSP_CONFIG = {
8
+ directives: {
9
+ 'default-src': ["'self'"],
10
+ 'script-src': ["'self'", "'unsafe-inline'"],
11
+ 'style-src': ["'self'", "'unsafe-inline'"],
12
+ 'img-src': ["'self'", "data:", "https:"],
13
+ 'connect-src': ["'self'", "https:", "wss:"],
14
+ 'font-src': ["'self'", "https:", "data:"],
15
+ 'object-src': ["'none'"],
16
+ 'media-src': ["'self'", "https:"],
17
+ 'frame-src': ["'none'"],
18
+ 'worker-src': ["'self'"],
19
+ 'child-src': ["'none'"],
20
+ 'form-action': ["'self'"],
21
+ 'base-uri': ["'self'"],
22
+ 'manifest-src': ["'self'"
23
+ ]
24
+ }
25
+ } as const;
26
+
27
+ /**
28
+ * Validation configurations for different content types
29
+ */
30
+ export const VALIDATION_CONFIGS: Record<string, ValidationConfig> = {
31
+ PLAIN_TEXT: {
32
+ allowHtml: false,
33
+ stripTags: true,
34
+ allowedTags: [],
35
+ allowedAttributes: []
36
+ },
37
+
38
+ RICH_TEXT: {
39
+ allowHtml: true,
40
+ stripTags: false,
41
+ allowedTags: ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
42
+ allowedAttributes: ['class']
43
+ },
44
+
45
+ LINK_CONTENT: {
46
+ allowHtml: true,
47
+ stripTags: false,
48
+ allowedTags: ['a', 'p', 'br', 'strong', 'em'],
49
+ allowedAttributes: ['href', 'title', 'target']
50
+ },
51
+
52
+ USER_PROFILE: {
53
+ allowHtml: false,
54
+ stripTags: true,
55
+ allowedTags: [],
56
+ allowedAttributes: []
57
+ },
58
+
59
+ COMMENT: {
60
+ allowHtml: true,
61
+ stripTags: false,
62
+ allowedTags: ['p', 'br', 'strong', 'em'],
63
+ allowedAttributes: []
64
+ }
65
+ } as const;
66
+
67
+ /**
68
+ * File upload security settings
69
+ */
70
+ export const FILE_SECURITY_CONFIG = {
71
+ allowedMimeTypes: {
72
+ image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
73
+ audio: ['audio/mpeg', 'audio/wav', 'audio/mp4', 'audio/flac'],
74
+ video: ['video/mp4', 'video/webm', 'video/quicktime'],
75
+ document: ['application/pdf', 'text/plain'],
76
+ avatar: ['image/jpeg', 'image/png', 'image/webp']
77
+ },
78
+
79
+ maxFileSizes: {
80
+ image: 10 * 1024 * 1024,
81
+ audio: 100 * 1024 * 1024,
82
+ video: 500 * 1024 * 1024,
83
+ document: 5 * 1024 * 1024,
84
+ avatar: 2 * 1024 * 1024
85
+ },
86
+
87
+ blockedExtensions: [
88
+ '.exe', '.bat', '.cmd', '.com', '.pif', '.scr', '.vbs', '.js', '.jar',
89
+ '.app', '.deb', '.pkg', '.dmg', '.rpm', '.msi', '.apk', '.ipa'
90
+ ],
91
+
92
+ blockedPatterns: [
93
+ /^\./,
94
+ /\.\.$/,
95
+ /[<>:"|?*]/,
96
+ /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i
97
+ ]
98
+ } as const;
99
+
100
+ /**
101
+ * Rate limiting configuration
102
+ */
103
+ export const RATE_LIMIT_CONFIG = {
104
+ api: {
105
+ default: 100,
106
+ auth: 10,
107
+ upload: 20,
108
+ generation: 30,
109
+ export: 10
110
+ },
111
+
112
+ actions: {
113
+ login: 5,
114
+ registration: 3,
115
+ passwordReset: 3,
116
+ contentCreation: 50,
117
+ fileUpload: 100
118
+ }
119
+ } as const;
120
+
121
+ /**
122
+ * Session security configuration
123
+ */
124
+ export const SESSION_CONFIG = {
125
+ timeout: 720,
126
+ refreshThreshold: 60,
127
+ maxSessions: 5,
128
+ cookie: {
129
+ httpOnly: true,
130
+ secure: true,
131
+ sameSite: 'strict' as const,
132
+ maxAge: 720 * 60 * 1000
133
+ }
134
+ } as const;
135
+
136
+ /**
137
+ * Security headers configuration
138
+ */
139
+ export const SECURITY_HEADERS = {
140
+ 'X-Content-Type-Options': 'nosniff',
141
+ 'X-Frame-Options': 'DENY',
142
+ 'X-XSS-Protection': '1; mode=block',
143
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
144
+ 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
145
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload'
146
+ } as const;
147
+
148
+ /**
149
+ * CORS configuration
150
+ */
151
+ export const CORS_CONFIG = {
152
+ origin: [
153
+ 'http://localhost:3000',
154
+ 'http://localhost:5173',
155
+ 'https://amaterasu.app'
156
+ ],
157
+ credentials: true,
158
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
159
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
160
+ maxAge: 86400
161
+ } as const;
162
+
163
+ export default {
164
+ CSP_CONFIG,
165
+ VALIDATION_CONFIGS,
166
+ FILE_SECURITY_CONFIG,
167
+ RATE_LIMIT_CONFIG,
168
+ SESSION_CONFIG,
169
+ SECURITY_HEADERS,
170
+ CORS_CONFIG
171
+ };
@@ -0,0 +1,216 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+
3
+ export interface ValidationRule {
4
+ required?: boolean;
5
+ minLength?: number;
6
+ maxLength?: number;
7
+ pattern?: RegExp;
8
+ custom?: (value: string) => string | null;
9
+ message?: string;
10
+ }
11
+
12
+ export interface ValidationRules {
13
+ [fieldName: string]: ValidationRule;
14
+ }
15
+
16
+ export interface ValidationErrors {
17
+ [fieldName: string]: string;
18
+ }
19
+
20
+ export interface FormData {
21
+ [fieldName: string]: string | number | boolean | string[];
22
+ }
23
+
24
+ const validateField = (value: string | number | boolean | string[], rule: ValidationRule): string | null => {
25
+ const stringValue = Array.isArray(value) ? value.join(',') : String(value);
26
+
27
+ if (rule.required && (!stringValue || stringValue.trim() === '')) {
28
+ return rule.message || 'This field is required';
29
+ }
30
+
31
+ if (!stringValue || stringValue.trim() === '') {
32
+ return null;
33
+ }
34
+
35
+ if (rule.minLength && stringValue.length < rule.minLength) {
36
+ return rule.message || `Must be at least ${rule.minLength} characters`;
37
+ }
38
+
39
+ if (rule.maxLength && stringValue.length > rule.maxLength) {
40
+ return rule.message || `Must be no more than ${rule.maxLength} characters`;
41
+ }
42
+
43
+ if (rule.pattern && !rule.pattern.test(stringValue)) {
44
+ return rule.message || 'Invalid format';
45
+ }
46
+
47
+ if (rule.custom) {
48
+ return rule.custom(stringValue);
49
+ }
50
+
51
+ return null;
52
+ };
53
+
54
+ export const sanitizeInput = (value: string): string => {
55
+ return value
56
+ .trim()
57
+ .replace(/[<>]/g, '')
58
+ .replace(/\s+/g, ' ');
59
+ };
60
+
61
+ export const VALIDATION_PATTERNS = {
62
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
63
+ url: /^https?:\/\/.+/,
64
+ phone: /^\+?[\d\s\-()]+$/,
65
+ alphanumeric: /^[a-zA-Z0-9]+$/,
66
+ noSpecialChars: /^[a-zA-Z0-9\s]+$/,
67
+ } as const;
68
+
69
+ export const COMMON_RULES = {
70
+ required: { required: true },
71
+ email: {
72
+ pattern: VALIDATION_PATTERNS.email,
73
+ message: 'Please enter a valid email address'
74
+ },
75
+ url: {
76
+ pattern: VALIDATION_PATTERNS.url,
77
+ message: 'Please enter a valid URL'
78
+ },
79
+ phone: {
80
+ pattern: VALIDATION_PATTERNS.phone,
81
+ message: 'Please enter a valid phone number'
82
+ },
83
+ shortText: { maxLength: 100 },
84
+ mediumText: { maxLength: 500 },
85
+ longText: { maxLength: 2000 },
86
+ name: {
87
+ minLength: 2,
88
+ maxLength: 50,
89
+ pattern: VALIDATION_PATTERNS.noSpecialChars,
90
+ message: 'Name must be 2-50 characters and contain only letters, numbers, and spaces'
91
+ },
92
+ description: {
93
+ minLength: 10,
94
+ maxLength: 500,
95
+ message: 'Description must be 10-500 characters'
96
+ },
97
+ } as const;
98
+
99
+ export const useFormValidation = <T extends FormData>(
100
+ initialData: T,
101
+ validationRules: ValidationRules = {}
102
+ ) => {
103
+ const [formData, setFormData] = useState<T>(initialData);
104
+ const [errors, setErrors] = useState<ValidationErrors>({});
105
+ const [touched, setTouched] = useState<Record<string, boolean>>({});
106
+
107
+ const validateSingleField = useCallback((fieldName: string, value: string | number | boolean | string[]): string | null => {
108
+ const rule = validationRules[fieldName];
109
+ if (!rule) return null;
110
+
111
+ return validateField(value, rule);
112
+ }, [validationRules]);
113
+
114
+ const validateAllFields = useCallback((): boolean => {
115
+ const newErrors: ValidationErrors = {};
116
+ let isValid = true;
117
+
118
+ Object.keys(validationRules).forEach(fieldName => {
119
+ const value = formData[fieldName];
120
+ const error = validateSingleField(fieldName, value);
121
+
122
+ if (error) {
123
+ newErrors[fieldName] = error;
124
+ isValid = false;
125
+ }
126
+ });
127
+
128
+ setErrors(newErrors);
129
+ return isValid;
130
+ }, [formData, validateSingleField, validationRules]);
131
+
132
+ const updateField = useCallback((fieldName: string, value: string | number | boolean | string[]) => {
133
+ const sanitizedValue = typeof value === 'string' ? sanitizeInput(value) : value;
134
+
135
+ setFormData(prev => ({ ...prev, [fieldName]: sanitizedValue }));
136
+
137
+ setTouched(prev => ({ ...prev, [fieldName]: true }));
138
+
139
+ if (touched[fieldName] || value !== '') {
140
+ const error = validateSingleField(fieldName, sanitizedValue);
141
+ setErrors(prev => ({ ...prev, [fieldName]: error || '' }));
142
+ }
143
+ }, [touched, validateSingleField]);
144
+
145
+ const clearFieldError = useCallback((fieldName: string) => {
146
+ setErrors(prev => ({ ...prev, [fieldName]: '' }));
147
+ }, []);
148
+
149
+ const clearAllErrors = useCallback(() => {
150
+ setErrors({});
151
+ }, []);
152
+
153
+ const resetForm = useCallback(() => {
154
+ setFormData(initialData);
155
+ setErrors({});
156
+ setTouched({});
157
+ }, [initialData]);
158
+
159
+ const setFormDataWithValidation = useCallback((newData: Partial<T>) => {
160
+ setFormData(prev => ({ ...prev, ...newData }));
161
+
162
+ const newErrors: ValidationErrors = {};
163
+ Object.keys(newData).forEach(fieldName => {
164
+ if (validationRules[fieldName]) {
165
+ const fieldValue = newData[fieldName];
166
+ const error = validateSingleField(fieldName, fieldValue as string | number | boolean | string[]);
167
+ if (error) {
168
+ newErrors[fieldName] = error;
169
+ }
170
+ }
171
+ });
172
+
173
+ setErrors(prev => ({ ...prev, ...newErrors }));
174
+ }, [validateSingleField, validationRules]);
175
+
176
+ const getFieldProps = useCallback((fieldName: string) => ({
177
+ value: formData[fieldName] || '',
178
+ onChange: (value: string | number | boolean | string[]) => updateField(fieldName, value),
179
+ onBlur: () => {
180
+ setTouched(prev => ({ ...prev, [fieldName]: true }));
181
+ const error = validateSingleField(fieldName, formData[fieldName]);
182
+ setErrors(prev => ({ ...prev, [fieldName]: error || '' }));
183
+ },
184
+ error: errors[fieldName],
185
+ isValid: !errors[fieldName] && touched[fieldName],
186
+ }), [formData, errors, touched, updateField, validateSingleField]);
187
+
188
+ const isFormValid = useMemo(() => {
189
+ return Object.keys(validationRules).every(fieldName => !errors[fieldName]);
190
+ }, [errors, validationRules]);
191
+
192
+ const hasErrors = useMemo(() => {
193
+ return Object.values(errors).some(error => error !== '');
194
+ }, [errors]);
195
+
196
+ const touchedFields = useMemo(() => {
197
+ return Object.keys(touched).filter(fieldName => touched[fieldName]);
198
+ }, [touched]);
199
+
200
+ return {
201
+ formData,
202
+ errors,
203
+ touched,
204
+ isFormValid,
205
+ hasErrors,
206
+ touchedFields,
207
+ updateField,
208
+ validateAllFields,
209
+ validateSingleField,
210
+ clearFieldError,
211
+ clearAllErrors,
212
+ resetForm,
213
+ setFormDataWithValidation,
214
+ getFieldProps,
215
+ };
216
+ };