@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.
Files changed (82) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +30 -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/index.ts +12 -0
  14. package/src/presentation/atoms/IconButton.tsx +55 -0
  15. package/src/presentation/atoms/Progress.tsx +2 -1
  16. package/src/presentation/atoms/Spinner.tsx +1 -1
  17. package/src/presentation/atoms/Switch.tsx +2 -2
  18. package/src/presentation/atoms/Text.tsx +15 -13
  19. package/src/presentation/atoms/Tooltip.tsx +2 -1
  20. package/src/presentation/atoms/index.ts +3 -0
  21. package/src/presentation/hooks/index.ts +0 -3
  22. package/src/presentation/hooks/useClickOutside.ts +14 -10
  23. package/src/presentation/hooks/useClipboard.ts +30 -12
  24. package/src/presentation/hooks/useDebounce.ts +8 -10
  25. package/src/presentation/hooks/useLocalStorage.ts +28 -18
  26. package/src/presentation/hooks/useMediaQuery.ts +2 -1
  27. package/src/presentation/hooks/useScrollLock.ts +40 -12
  28. package/src/presentation/hooks/useToggle.ts +8 -6
  29. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  30. package/src/presentation/molecules/Avatar.tsx +1 -1
  31. package/src/presentation/molecules/CodeBlock.tsx +51 -48
  32. package/src/presentation/molecules/ListItem.tsx +40 -29
  33. package/src/presentation/molecules/Select.tsx +1 -1
  34. package/src/presentation/molecules/Toggle.tsx +4 -4
  35. package/src/presentation/molecules/index.ts +0 -3
  36. package/src/presentation/organisms/Alert.tsx +1 -1
  37. package/src/presentation/organisms/DataTable.tsx +24 -11
  38. package/src/presentation/organisms/Dialog.tsx +1 -1
  39. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  40. package/src/presentation/organisms/HoverCard.tsx +1 -1
  41. package/src/presentation/organisms/LoadingState.tsx +1 -5
  42. package/src/presentation/organisms/Popover.tsx +1 -1
  43. package/src/presentation/organisms/StatCard.tsx +2 -2
  44. package/src/presentation/organisms/Table.tsx +2 -2
  45. package/src/presentation/organisms/index.ts +0 -60
  46. package/src/styles/compile.css +18 -0
  47. package/src/styles/theme.css +131 -0
  48. package/src/domain/tokens/animation.tokens.ts +0 -87
  49. package/src/domain/tokens/color.tokens.ts +0 -95
  50. package/src/domain/tokens/radius.tokens.ts +0 -27
  51. package/src/domain/tokens/shadow.tokens.ts +0 -25
  52. package/src/domain/tokens/spacing.tokens.ts +0 -65
  53. package/src/domain/tokens/typography.tokens.ts +0 -74
  54. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  55. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  56. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  57. package/src/infrastructure/error/index.ts +0 -5
  58. package/src/infrastructure/performance/index.ts +0 -92
  59. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  60. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  61. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  62. package/src/infrastructure/security/index.ts +0 -10
  63. package/src/infrastructure/security/security-config.ts +0 -171
  64. package/src/infrastructure/security/useFormValidation.ts +0 -216
  65. package/src/infrastructure/security/validation.ts +0 -246
  66. package/src/presentation/hooks/useLanguage.ts +0 -109
  67. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  68. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  69. package/src/presentation/organisms/Calendar.tsx +0 -79
  70. package/src/presentation/organisms/Comments.tsx +0 -103
  71. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  72. package/src/presentation/organisms/FilterBar.tsx +0 -244
  73. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  74. package/src/presentation/organisms/Footer.tsx +0 -86
  75. package/src/presentation/organisms/FormModal.tsx +0 -126
  76. package/src/presentation/organisms/Grid.tsx +0 -128
  77. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  78. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  79. package/src/presentation/organisms/Modal.tsx +0 -173
  80. package/src/presentation/organisms/Navbar.tsx +0 -75
  81. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  82. 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
- /&lt;[^&]*&gt;/gi, // HTML encoded tags
45
- /&lt;[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
- };