@umituz/web-design-system 3.1.22 → 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,216 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
export interface ValidationResult {
|
|
2
|
-
isValid: boolean;
|
|
3
|
-
error?: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface ValidationConfig {
|
|
7
|
-
allowHtml?: boolean;
|
|
8
|
-
stripTags?: boolean;
|
|
9
|
-
allowedTags?: string[];
|
|
10
|
-
allowedAttributes?: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Input validation with XSS protection
|
|
15
|
-
* Basic sanitization without external dependencies
|
|
16
|
-
*/
|
|
17
|
-
export const validateInput = (
|
|
18
|
-
input: string,
|
|
19
|
-
maxLength: number = 1000,
|
|
20
|
-
minLength: number = 0,
|
|
21
|
-
config?: ValidationConfig
|
|
22
|
-
): ValidationResult => {
|
|
23
|
-
const trimmed = input.trim();
|
|
24
|
-
|
|
25
|
-
// Required field validation
|
|
26
|
-
if (minLength > 0 && !trimmed) {
|
|
27
|
-
return { isValid: false, error: "This field is required" };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Length validation
|
|
31
|
-
if (trimmed.length > maxLength) {
|
|
32
|
-
return { isValid: false, error: `Must be less than ${maxLength} characters` };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (trimmed.length < minLength) {
|
|
36
|
-
return { isValid: false, error: `Must be at least ${minLength} characters` };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// HTML tag check for non-HTML content
|
|
40
|
-
if (!config?.allowHtml) {
|
|
41
|
-
// FIX: Better HTML detection that catches encoded variations
|
|
42
|
-
const htmlPatterns = [
|
|
43
|
-
/<[^>]*>/gi, // Standard HTML tags
|
|
44
|
-
/<[^&]*>/gi, // HTML encoded tags
|
|
45
|
-
/<[a-z]/gi, // Partial encoded tags
|
|
46
|
-
/javascript:/gi, // JavaScript protocol
|
|
47
|
-
/vbscript:/gi, // VBScript protocol
|
|
48
|
-
/data:text\/html/gi, // Data URI with HTML
|
|
49
|
-
/on\w+\s*=/gi, // Inline event handlers
|
|
50
|
-
/<script/gi, // Script tags (case-insensitive)
|
|
51
|
-
/<\/script>/gi, // Closing script tags
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
for (const pattern of htmlPatterns) {
|
|
55
|
-
if (pattern.test(trimmed)) {
|
|
56
|
-
return { isValid: false, error: "HTML tags or scripts are not allowed" };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return { isValid: true };
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Email validation with basic security checks
|
|
66
|
-
*/
|
|
67
|
-
export const validateEmail = (email: string): ValidationResult => {
|
|
68
|
-
const trimmed = email.trim();
|
|
69
|
-
|
|
70
|
-
if (!trimmed) {
|
|
71
|
-
return { isValid: false, error: "Email is required" };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Check for HTML/script content
|
|
75
|
-
if (/<[^>]*>/.test(trimmed)) {
|
|
76
|
-
return { isValid: false, error: "Invalid characters in email address" };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Email regex
|
|
80
|
-
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
81
|
-
|
|
82
|
-
if (!emailRegex.test(trimmed)) {
|
|
83
|
-
return { isValid: false, error: "Please enter a valid email address" };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Check for suspicious patterns
|
|
87
|
-
const suspiciousPatterns = [
|
|
88
|
-
/javascript:/gi,
|
|
89
|
-
/data:/gi,
|
|
90
|
-
/<[^>]*>/gi
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
for (const pattern of suspiciousPatterns) {
|
|
94
|
-
if (pattern.test(trimmed)) {
|
|
95
|
-
return { isValid: false, error: "Invalid email format" };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { isValid: true };
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* URL validation with security checks
|
|
104
|
-
*/
|
|
105
|
-
export const validateUrl = (url: string): ValidationResult => {
|
|
106
|
-
const trimmed = url.trim();
|
|
107
|
-
|
|
108
|
-
if (!trimmed) {
|
|
109
|
-
return { isValid: false, error: "URL is required" };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check for HTML/script content
|
|
113
|
-
if (/<[^>]*>/.test(trimmed)) {
|
|
114
|
-
return { isValid: false, error: "Invalid characters in URL" };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const urlObj = new URL(trimmed);
|
|
119
|
-
|
|
120
|
-
// Only allow safe protocols
|
|
121
|
-
const allowedProtocols = ['http:', 'https:', 'ftp:', 'ftps:'];
|
|
122
|
-
if (!allowedProtocols.includes(urlObj.protocol)) {
|
|
123
|
-
return { isValid: false, error: "Only HTTP, HTTPS, and FTP URLs are allowed" };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return { isValid: true };
|
|
127
|
-
} catch {
|
|
128
|
-
return { isValid: false, error: "Please enter a valid URL" };
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Required field validation
|
|
134
|
-
*/
|
|
135
|
-
export const validateRequired = (value: string): ValidationResult => {
|
|
136
|
-
const trimmed = value.trim();
|
|
137
|
-
|
|
138
|
-
if (!trimmed) {
|
|
139
|
-
return { isValid: false, error: "This field is required" };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { isValid: true };
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Basic input sanitization
|
|
147
|
-
* Removes potentially dangerous content
|
|
148
|
-
*/
|
|
149
|
-
export const sanitizeInput = (
|
|
150
|
-
input: string,
|
|
151
|
-
config?: ValidationConfig
|
|
152
|
-
): string => {
|
|
153
|
-
if (!input) return '';
|
|
154
|
-
|
|
155
|
-
let sanitized = input.trim();
|
|
156
|
-
|
|
157
|
-
// Remove dangerous content for non-HTML input
|
|
158
|
-
if (!config?.allowHtml) {
|
|
159
|
-
sanitized = sanitized
|
|
160
|
-
.replace(/javascript:/gi, '')
|
|
161
|
-
.replace(/vbscript:/gi, '')
|
|
162
|
-
.replace(/data:text\/html/gi, '')
|
|
163
|
-
.replace(/data:application\/javascript/gi, '')
|
|
164
|
-
.replace(/on\w+\s*=/gi, '')
|
|
165
|
-
.replace(/<[^>]*>/g, '');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Limit length
|
|
169
|
-
return sanitized.slice(0, config?.allowHtml ? 10000 : 1000);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Validate and sanitize file names
|
|
174
|
-
* Prevents directory traversal and other file-based attacks
|
|
175
|
-
*/
|
|
176
|
-
export const validateFileName = (fileName: string): ValidationResult => {
|
|
177
|
-
const trimmed = fileName.trim();
|
|
178
|
-
|
|
179
|
-
if (!trimmed) {
|
|
180
|
-
return { isValid: false, error: "File name is required" };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// FIX: Remove global flag from regex patterns to prevent lastIndex issues
|
|
184
|
-
// Each pattern is tested once and should not have global flag
|
|
185
|
-
const dangerousPatterns = [
|
|
186
|
-
{ pattern: /\.\./, name: 'Directory traversal' },
|
|
187
|
-
{ pattern: /[<>:"|?*]/, name: 'Invalid characters' },
|
|
188
|
-
{ pattern: /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i, name: 'Windows reserved' },
|
|
189
|
-
{ pattern: /^\./, name: 'Hidden file' },
|
|
190
|
-
{ pattern: /\.$/, name: 'Trailing dot' },
|
|
191
|
-
{ pattern: /\s+$/, name: 'Trailing whitespace' }
|
|
192
|
-
];
|
|
193
|
-
|
|
194
|
-
for (const { pattern, name } of dangerousPatterns) {
|
|
195
|
-
if (pattern.test(trimmed)) {
|
|
196
|
-
return { isValid: false, error: `Invalid file name: ${name}` };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Length check
|
|
201
|
-
if (trimmed.length > 255) {
|
|
202
|
-
return { isValid: false, error: "File name too long (max 255 characters)" };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return { isValid: true };
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Content Security Policy helper
|
|
210
|
-
* Validates that content meets basic CSP requirements
|
|
211
|
-
*/
|
|
212
|
-
export const validateCSPCompliance = (content: string): ValidationResult => {
|
|
213
|
-
const violations = [];
|
|
214
|
-
|
|
215
|
-
// FIX: Enhanced script detection - catch more variations
|
|
216
|
-
const scriptPatterns = [
|
|
217
|
-
/<script[^>]*>/gi,
|
|
218
|
-
/<script/gi,
|
|
219
|
-
/<\/script>/gi,
|
|
220
|
-
/javascript:/gi,
|
|
221
|
-
/vbscript:/gi,
|
|
222
|
-
/data:\s*text\/html/gi,
|
|
223
|
-
/data:\s*application\/javascript/gi
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
for (const pattern of scriptPatterns) {
|
|
227
|
-
if (pattern.test(content)) {
|
|
228
|
-
violations.push("Potentially dangerous script content detected");
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Check for inline styles (more lenient - styles are often used legitimately)
|
|
234
|
-
if (/<style[^>]*>[\s\S]*?<\/style>/gi.test(content)) {
|
|
235
|
-
violations.push("Inline style blocks not allowed");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (violations.length > 0) {
|
|
239
|
-
return {
|
|
240
|
-
isValid: false,
|
|
241
|
-
error: `Content Security Policy violations: ${violations.join(', ')}`
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return { isValid: true };
|
|
246
|
-
};
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useLanguage Hook
|
|
3
|
-
* @description Language management with i18n integration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useTranslation } from 'react-i18next';
|
|
7
|
-
|
|
8
|
-
export type Language = string;
|
|
9
|
-
|
|
10
|
-
export interface SupportedLanguage {
|
|
11
|
-
name: string;
|
|
12
|
-
flag: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UseLanguageReturn {
|
|
16
|
-
currentLanguage: Language;
|
|
17
|
-
changeLanguage: (language: Language) => void;
|
|
18
|
-
t: (key: string, params?: Record<string, unknown>) => string;
|
|
19
|
-
supportedLanguages: Record<Language, SupportedLanguage>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Validates if a language code exists in supported languages
|
|
24
|
-
*/
|
|
25
|
-
function isValidLanguage(
|
|
26
|
-
code: string,
|
|
27
|
-
supportedLanguages: Record<Language, SupportedLanguage>
|
|
28
|
-
): boolean {
|
|
29
|
-
return Object.keys(supportedLanguages).includes(code);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Normalizes language code to match supported languages
|
|
34
|
-
* Tries exact match first, then tries to match by language code prefix
|
|
35
|
-
*/
|
|
36
|
-
function normalizeLanguage(
|
|
37
|
-
code: string,
|
|
38
|
-
supportedLanguages: Record<Language, SupportedLanguage>,
|
|
39
|
-
defaultLanguage: Language
|
|
40
|
-
): Language {
|
|
41
|
-
// If code is valid, return it
|
|
42
|
-
if (isValidLanguage(code, supportedLanguages)) {
|
|
43
|
-
return code;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Try to extract language code (e.g., 'en-US' -> 'en')
|
|
47
|
-
const [language] = code.split('-');
|
|
48
|
-
const matchedKey = Object.keys(supportedLanguages).find((key) =>
|
|
49
|
-
key.startsWith(language)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
return (matchedKey as Language) || defaultLanguage;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Language management hook with i18n integration
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```tsx
|
|
60
|
-
* const { currentLanguage, changeLanguage, t, supportedLanguages } = useLanguage({
|
|
61
|
-
* defaultLanguage: 'en-US',
|
|
62
|
-
* supportedLanguages: {
|
|
63
|
-
* 'en-US': { name: 'English', flag: '🇺🇸' },
|
|
64
|
-
* 'tr-TR': { name: 'Türkçe', flag: '🇹🇷' },
|
|
65
|
-
* }
|
|
66
|
-
* });
|
|
67
|
-
*
|
|
68
|
-
* // Change language
|
|
69
|
-
* changeLanguage('tr-TR');
|
|
70
|
-
*
|
|
71
|
-
* // Translate
|
|
72
|
-
* const title = t('common.title');
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
|
-
export function useLanguage(options?: {
|
|
76
|
-
defaultLanguage?: Language;
|
|
77
|
-
supportedLanguages?: Record<Language, SupportedLanguage>;
|
|
78
|
-
}): UseLanguageReturn {
|
|
79
|
-
const {
|
|
80
|
-
defaultLanguage = 'en-US',
|
|
81
|
-
supportedLanguages = {} as Record<Language, SupportedLanguage>,
|
|
82
|
-
} = options || {};
|
|
83
|
-
|
|
84
|
-
const { t, i18n } = useTranslation();
|
|
85
|
-
|
|
86
|
-
// Safely get and validate current language
|
|
87
|
-
const currentLanguage: Language = normalizeLanguage(
|
|
88
|
-
i18n.language,
|
|
89
|
-
supportedLanguages,
|
|
90
|
-
defaultLanguage
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const changeLanguage = (language: Language) => {
|
|
94
|
-
// Validate language before changing
|
|
95
|
-
if (isValidLanguage(language, supportedLanguages)) {
|
|
96
|
-
i18n.changeLanguage(language);
|
|
97
|
-
} else {
|
|
98
|
-
// Fall back to default language
|
|
99
|
-
i18n.changeLanguage(defaultLanguage);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
currentLanguage,
|
|
105
|
-
changeLanguage,
|
|
106
|
-
t,
|
|
107
|
-
supportedLanguages,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Breadcrumb Component (Molecule)
|
|
3
|
-
* @description Navigation breadcrumb with home icon
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// @ts-ignore - react-router-dom is a peer dependency, may not be available during package build
|
|
7
|
-
import { Link } from 'react-router-dom';
|
|
8
|
-
import type { BaseProps } from '../../domain/types';
|
|
9
|
-
|
|
10
|
-
export interface BreadcrumbItem {
|
|
11
|
-
label: string;
|
|
12
|
-
href?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface BreadcrumbProps extends BaseProps {
|
|
16
|
-
items: BreadcrumbItem[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const Breadcrumb = ({ items, className }: BreadcrumbProps) => {
|
|
20
|
-
return (
|
|
21
|
-
<nav className={`flex items-center gap-2 text-sm mb-4 md:mb-6 ${className || ''}`} aria-label="Breadcrumb">
|
|
22
|
-
{/* Home */}
|
|
23
|
-
<Link
|
|
24
|
-
to="/"
|
|
25
|
-
className="flex items-center gap-1 text-text-secondary hover:text-primary-light transition-colors"
|
|
26
|
-
aria-label="Home"
|
|
27
|
-
>
|
|
28
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
29
|
-
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
30
|
-
<polyline points="9 22 9 12 15 12 15 22" />
|
|
31
|
-
</svg>
|
|
32
|
-
</Link>
|
|
33
|
-
|
|
34
|
-
{/* Breadcrumb items */}
|
|
35
|
-
{items.map((item, index) => (
|
|
36
|
-
<div key={index} className="flex items-center gap-2">
|
|
37
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-secondary">
|
|
38
|
-
<polyline points="9 18 15 12 9 6" />
|
|
39
|
-
</svg>
|
|
40
|
-
{item.href ? (
|
|
41
|
-
<Link
|
|
42
|
-
to={item.href}
|
|
43
|
-
className="text-text-secondary hover:text-primary-light transition-colors"
|
|
44
|
-
>
|
|
45
|
-
{item.label}
|
|
46
|
-
</Link>
|
|
47
|
-
) : (
|
|
48
|
-
<span className="text-text-primary font-medium">{item.label}</span>
|
|
49
|
-
)}
|
|
50
|
-
</div>
|
|
51
|
-
))}
|
|
52
|
-
</nav>
|
|
53
|
-
);
|
|
54
|
-
};
|