@abstraks-dev/ui-library 1.0.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 (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +708 -0
  3. package/dist/__tests__/Anchor.test.js +145 -0
  4. package/dist/__tests__/ArrowRight.test.js +91 -0
  5. package/dist/__tests__/Avatar.test.js +123 -0
  6. package/dist/__tests__/Button.test.js +82 -0
  7. package/dist/__tests__/Card.test.js +198 -0
  8. package/dist/__tests__/CheckCircle.test.js +98 -0
  9. package/dist/__tests__/Checkbox.test.js +161 -0
  10. package/dist/__tests__/ChevronDown.test.js +73 -0
  11. package/dist/__tests__/Close.test.js +98 -0
  12. package/dist/__tests__/EditSquare.test.js +99 -0
  13. package/dist/__tests__/Error.test.js +74 -0
  14. package/dist/__tests__/Footer.test.js +66 -0
  15. package/dist/__tests__/Heading.test.js +227 -0
  16. package/dist/__tests__/Hero.test.js +74 -0
  17. package/dist/__tests__/Label.test.js +123 -0
  18. package/dist/__tests__/Loader.test.js +115 -0
  19. package/dist/__tests__/MenuHover.test.js +137 -0
  20. package/dist/__tests__/Paragraph.test.js +93 -0
  21. package/dist/__tests__/PlusCircle.test.js +99 -0
  22. package/dist/__tests__/Radio.test.js +153 -0
  23. package/dist/__tests__/Select.test.js +187 -0
  24. package/dist/__tests__/Tabs.test.js +162 -0
  25. package/dist/__tests__/TextArea.test.js +127 -0
  26. package/dist/__tests__/TextInput.test.js +181 -0
  27. package/dist/__tests__/Toggle.test.js +120 -0
  28. package/dist/__tests__/TrashX.test.js +99 -0
  29. package/dist/__tests__/useHeadingAccessibility.test.js +144 -0
  30. package/dist/components/Anchor.js +131 -0
  31. package/dist/components/Animation.js +129 -0
  32. package/dist/components/AnimationGroup.js +207 -0
  33. package/dist/components/AnimationToggle.js +216 -0
  34. package/dist/components/Avatar.js +153 -0
  35. package/dist/components/Button.js +218 -0
  36. package/dist/components/Card.js +222 -0
  37. package/dist/components/Checkbox.js +305 -0
  38. package/dist/components/Crud.js +564 -0
  39. package/dist/components/DragAndDrop.js +337 -0
  40. package/dist/components/Error.js +206 -0
  41. package/dist/components/Footer.js +99 -0
  42. package/dist/components/Form.js +412 -0
  43. package/dist/components/Header.js +372 -0
  44. package/dist/components/Heading.js +134 -0
  45. package/dist/components/Hero.js +181 -0
  46. package/dist/components/Label.js +256 -0
  47. package/dist/components/Loader.js +302 -0
  48. package/dist/components/MenuHover.js +114 -0
  49. package/dist/components/Paragraph.js +128 -0
  50. package/dist/components/Prompt.js +61 -0
  51. package/dist/components/Radio.js +254 -0
  52. package/dist/components/Select.js +422 -0
  53. package/dist/components/SideMenu.js +313 -0
  54. package/dist/components/Tabs.js +297 -0
  55. package/dist/components/TextArea.js +370 -0
  56. package/dist/components/TextInput.js +286 -0
  57. package/dist/components/Toggle.js +186 -0
  58. package/dist/components/crudFiles/CrudEditBase.js +150 -0
  59. package/dist/components/crudFiles/CrudViewBase.js +39 -0
  60. package/dist/components/crudFiles/crudDevelopment.js +118 -0
  61. package/dist/components/crudFiles/crudEditHandlers.js +50 -0
  62. package/dist/constants/animation.js +30 -0
  63. package/dist/icons/ArrowIcon.js +32 -0
  64. package/dist/icons/ArrowRight.js +33 -0
  65. package/dist/icons/CheckCircle.js +33 -0
  66. package/dist/icons/ChevronDown.js +28 -0
  67. package/dist/icons/Close.js +33 -0
  68. package/dist/icons/EditSquare.js +33 -0
  69. package/dist/icons/Ellipses.js +34 -0
  70. package/dist/icons/Hamburger.js +39 -0
  71. package/dist/icons/LoadingSpinner.js +42 -0
  72. package/dist/icons/PlusCircle.js +33 -0
  73. package/dist/icons/SaveIcon.js +32 -0
  74. package/dist/icons/TrashX.js +33 -0
  75. package/dist/icons/__tests__/CheckCircle.test.js +9 -0
  76. package/dist/icons/__tests__/ChevronDown.test.js +9 -0
  77. package/dist/icons/__tests__/Close.test.js +9 -0
  78. package/dist/icons/__tests__/EditSquare.test.js +9 -0
  79. package/dist/icons/__tests__/PlusCircle.test.js +9 -0
  80. package/dist/icons/__tests__/TrashX.test.js +9 -0
  81. package/dist/icons/index.js +89 -0
  82. package/dist/index.js +332 -0
  83. package/dist/setupTests.js +3 -0
  84. package/dist/styles/_variables.scss +286 -0
  85. package/dist/styles/anchor.scss +40 -0
  86. package/dist/styles/animation-accessibility.scss +96 -0
  87. package/dist/styles/animation-toggle.scss +233 -0
  88. package/dist/styles/animation.scss +3781 -0
  89. package/dist/styles/avatar.scss +285 -0
  90. package/dist/styles/button.scss +430 -0
  91. package/dist/styles/card.scss +210 -0
  92. package/dist/styles/checkbox.scss +160 -0
  93. package/dist/styles/crud.scss +474 -0
  94. package/dist/styles/dragAndDrop.scss +312 -0
  95. package/dist/styles/error.scss +232 -0
  96. package/dist/styles/footer.scss +58 -0
  97. package/dist/styles/form.scss +420 -0
  98. package/dist/styles/grid.scss +29 -0
  99. package/dist/styles/header.scss +276 -0
  100. package/dist/styles/heading.scss +118 -0
  101. package/dist/styles/hero.scss +185 -0
  102. package/dist/styles/htmlElements.scss +20 -0
  103. package/dist/styles/image.scss +9 -0
  104. package/dist/styles/label.scss +340 -0
  105. package/dist/styles/list-item.scss +5 -0
  106. package/dist/styles/loader.scss +354 -0
  107. package/dist/styles/logo.scss +19 -0
  108. package/dist/styles/main.css +9056 -0
  109. package/dist/styles/main.css.map +1 -0
  110. package/dist/styles/main.scss +0 -0
  111. package/dist/styles/menu-hover.scss +30 -0
  112. package/dist/styles/paragraph.scss +88 -0
  113. package/dist/styles/prompt.scss +51 -0
  114. package/dist/styles/radio.scss +202 -0
  115. package/dist/styles/select.scss +363 -0
  116. package/dist/styles/side-menu.scss +334 -0
  117. package/dist/styles/tabs.scss +540 -0
  118. package/dist/styles/text-area.scss +388 -0
  119. package/dist/styles/text-input.scss +171 -0
  120. package/dist/styles/toggle.scss +0 -0
  121. package/dist/styles/unordered-list.scss +8 -0
  122. package/dist/utils/ScrollHandler.js +30 -0
  123. package/dist/utils/accessibility.js +128 -0
  124. package/dist/utils/heroUtils.js +316 -0
  125. package/dist/utils/index.js +104 -0
  126. package/dist/utils/inputValidation.js +29 -0
  127. package/dist/utils/keyboardNavigation.js +536 -0
  128. package/dist/utils/labelUtils.js +708 -0
  129. package/dist/utils/loaderUtils.js +387 -0
  130. package/dist/utils/menuUtils.js +575 -0
  131. package/dist/utils/useHeadingAccessibility.js +298 -0
  132. package/dist/utils/useRadioGroup.js +260 -0
  133. package/dist/utils/useSelectAccessibility.js +426 -0
  134. package/dist/utils/useTabsAccessibility.js +278 -0
  135. package/dist/utils/useTextAreaAccessibility.js +255 -0
  136. package/dist/utils/useTextInputAccessibility.js +295 -0
  137. package/dist/utils/useTypographyAccessibility.js +168 -0
  138. package/dist/utils/useWindowSize.js +32 -0
  139. package/dist/utils/utils/ScrollHandler.js +26 -0
  140. package/dist/utils/utils/accessibility.js +133 -0
  141. package/dist/utils/utils/heroUtils.js +348 -0
  142. package/dist/utils/utils/index.js +9 -0
  143. package/dist/utils/utils/inputValidation.js +22 -0
  144. package/dist/utils/utils/keyboardNavigation.js +664 -0
  145. package/dist/utils/utils/labelUtils.js +772 -0
  146. package/dist/utils/utils/loaderUtils.js +436 -0
  147. package/dist/utils/utils/menuUtils.js +651 -0
  148. package/dist/utils/utils/useHeadingAccessibility.js +334 -0
  149. package/dist/utils/utils/useRadioGroup.js +311 -0
  150. package/dist/utils/utils/useSelectAccessibility.js +498 -0
  151. package/dist/utils/utils/useTabsAccessibility.js +316 -0
  152. package/dist/utils/utils/useTextAreaAccessibility.js +303 -0
  153. package/dist/utils/utils/useTextInputAccessibility.js +338 -0
  154. package/dist/utils/utils/useTypographyAccessibility.js +180 -0
  155. package/dist/utils/utils/useWindowSize.js +26 -0
  156. package/dist/utils/utils/validation.js +131 -0
  157. package/dist/utils/validation.js +139 -0
  158. package/package.json +90 -0
@@ -0,0 +1,338 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ import {
3
+ validateEmail,
4
+ validatePhone,
5
+ validateLength,
6
+ validateRequired,
7
+ } from './inputValidation';
8
+
9
+ /**
10
+ * Custom hook for managing text input state with accessibility features
11
+ * Provides controlled state management for text input with ARIA support
12
+ *
13
+ * @param {Object} options - Configuration options
14
+ * @param {string} options.defaultValue - Default input value
15
+ * @param {Function} options.onChange - Change callback function
16
+ * @param {boolean} options.required - Whether the field is required
17
+ * @param {number} options.maxLength - Maximum character count
18
+ * @param {number} options.minLength - Minimum character count
19
+ * @param {string} options.type - Input type for validation
20
+ * @returns {Object} TextInput state and handlers
21
+ */
22
+ export const useTextInputAccessibility = (options = {}) => {
23
+ const {
24
+ defaultValue = '',
25
+ onChange,
26
+ required = false,
27
+ maxLength,
28
+ minLength,
29
+ type = 'text',
30
+ } = options;
31
+
32
+ const [value, setValue] = useState(defaultValue);
33
+ const [isValid, setIsValid] = useState(!required || defaultValue.length > 0);
34
+ const [hasBeenTouched, setHasBeenTouched] = useState(false);
35
+ const inputRef = useRef(null);
36
+
37
+ const validateInput = useCallback(
38
+ (inputValue, inputType) => {
39
+ let valid = true;
40
+ if (required && !validateRequired(inputValue)) valid = false;
41
+ if (!validateLength(inputValue, minLength, maxLength)) valid = false;
42
+ switch (inputType) {
43
+ case 'email':
44
+ if (inputValue && !validateEmail(inputValue)) valid = false;
45
+ break;
46
+ case 'tel':
47
+ if (inputValue && !validatePhone(inputValue)) valid = false;
48
+ break;
49
+ case 'url':
50
+ try {
51
+ if (
52
+ inputValue &&
53
+ !(
54
+ inputValue.startsWith('http://') ||
55
+ inputValue.startsWith('https://')
56
+ )
57
+ ) {
58
+ valid = false;
59
+ }
60
+ } catch (e) {
61
+ valid = false;
62
+ }
63
+ break;
64
+ default:
65
+ break;
66
+ }
67
+ return valid;
68
+ },
69
+ [type]
70
+ );
71
+
72
+ const handleFocus = useCallback((event) => {
73
+ setHasBeenTouched(true);
74
+ }, []);
75
+
76
+ const handleBlur = useCallback(
77
+ (event) => {
78
+ const newValue = event.target.value;
79
+ const valid = validateInput(newValue, type);
80
+ setIsValid(valid);
81
+ },
82
+ [type, validateInput]
83
+ );
84
+
85
+ const reset = useCallback(() => {
86
+ setValue(defaultValue);
87
+ setIsValid(!required || defaultValue.length > 0);
88
+ setHasBeenTouched(false);
89
+ }, [defaultValue, required]);
90
+
91
+ const focus = useCallback(() => {
92
+ inputRef.current?.focus();
93
+ }, []);
94
+
95
+ // Unified onChange handler for input/textarea
96
+ const handleChange = useCallback(
97
+ (event) => {
98
+ const newValue = event.target.value;
99
+ setValue(newValue);
100
+ if (onChange) onChange(newValue);
101
+ },
102
+ [onChange]
103
+ );
104
+
105
+ return {
106
+ // State
107
+ value,
108
+ isValid,
109
+ hasBeenTouched,
110
+ showError: hasBeenTouched && !isValid,
111
+
112
+ // Handlers
113
+ handleFocus,
114
+ handleBlur,
115
+ handleChange,
116
+ reset,
117
+ focus,
118
+
119
+ // Ref
120
+ inputRef,
121
+
122
+ // Computed properties
123
+ charCount: value.length,
124
+ remainingChars: maxLength ? maxLength - value.length : null,
125
+ isAtMaxLength: maxLength ? value.length >= maxLength : false,
126
+ isAtMinLength: minLength ? value.length >= minLength : true,
127
+ };
128
+ };
129
+
130
+ /**
131
+ * Validates text input accessibility compliance
132
+ * Checks for proper ARIA attributes, labels, and keyboard support
133
+ *
134
+ * @param {HTMLElement} inputElement - The input element to validate
135
+ * @returns {Object} Validation results with errors and warnings
136
+ */
137
+ export const validateTextInputAccessibility = (inputElement) => {
138
+ const results = {
139
+ passed: true,
140
+ errors: [],
141
+ warnings: [],
142
+ info: [],
143
+ };
144
+
145
+ if (!inputElement) {
146
+ results.passed = false;
147
+ results.errors.push('No input element provided for validation');
148
+ return results;
149
+ }
150
+
151
+ // Check if it's actually an input element
152
+ if (inputElement.tagName.toLowerCase() !== 'input') {
153
+ const input = inputElement.querySelector('input');
154
+ if (!input) {
155
+ results.passed = false;
156
+ results.errors.push(
157
+ 'Element is not an input and contains no input element'
158
+ );
159
+ return results;
160
+ }
161
+ inputElement = input;
162
+ }
163
+
164
+ // 1. Check for accessible name
165
+ const hasLabel =
166
+ inputElement.hasAttribute('aria-label') ||
167
+ inputElement.hasAttribute('aria-labelledby') ||
168
+ inputElement.labels?.length > 0;
169
+
170
+ if (!hasLabel) {
171
+ results.passed = false;
172
+ results.errors.push(
173
+ 'Input must have an accessible name (label, aria-label, or aria-labelledby)'
174
+ );
175
+ }
176
+
177
+ // 2. Check for proper ID if labelledby is used
178
+ if (inputElement.hasAttribute('aria-labelledby')) {
179
+ const labelId = inputElement.getAttribute('aria-labelledby');
180
+ const labelElement = document.getElementById(labelId);
181
+ if (!labelElement) {
182
+ results.passed = false;
183
+ results.errors.push(
184
+ `Referenced label element with ID "${labelId}" not found`
185
+ );
186
+ }
187
+ }
188
+
189
+ // 3. Check for required field indication
190
+ if (
191
+ inputElement.hasAttribute('required') ||
192
+ inputElement.hasAttribute('aria-required')
193
+ ) {
194
+ if (!inputElement.hasAttribute('aria-required')) {
195
+ results.warnings.push(
196
+ 'Consider adding aria-required="true" for screen reader users'
197
+ );
198
+ }
199
+ }
200
+
201
+ // 4. Check for error state accessibility
202
+ if (inputElement.hasAttribute('aria-invalid')) {
203
+ const isInvalid = inputElement.getAttribute('aria-invalid') === 'true';
204
+ if (isInvalid && !inputElement.hasAttribute('aria-describedby')) {
205
+ results.warnings.push(
206
+ 'Invalid input should have aria-describedby pointing to error message'
207
+ );
208
+ }
209
+ }
210
+
211
+ // 5. Check input type appropriateness
212
+ const inputType = inputElement.getAttribute('type') || 'text';
213
+ if (inputType === 'text' && inputElement.getAttribute('name')) {
214
+ const name = inputElement.getAttribute('name').toLowerCase();
215
+ if (name.includes('email')) {
216
+ results.warnings.push('Consider using type="email" for email inputs');
217
+ } else if (name.includes('phone') || name.includes('tel')) {
218
+ results.warnings.push('Consider using type="tel" for phone inputs');
219
+ } else if (name.includes('url') || name.includes('website')) {
220
+ results.warnings.push('Consider using type="url" for URL inputs');
221
+ }
222
+ }
223
+
224
+ // 6. Check for placeholder accessibility
225
+ if (inputElement.hasAttribute('placeholder')) {
226
+ results.warnings.push(
227
+ 'Avoid using placeholder as the only label - ensure proper labeling exists'
228
+ );
229
+ }
230
+
231
+ // 7. Check for disabled state
232
+ if (inputElement.disabled) {
233
+ results.info.push('Input is disabled');
234
+ }
235
+
236
+ // 8. Check for keyboard accessibility
237
+ const tabIndex = inputElement.getAttribute('tabindex');
238
+ if (tabIndex && parseInt(tabIndex) < 0) {
239
+ results.warnings.push(
240
+ 'Input has negative tabindex - may not be keyboard accessible'
241
+ );
242
+ }
243
+
244
+ // 9. Check autocomplete attributes
245
+ const autocomplete = inputElement.getAttribute('autocomplete');
246
+ if (!autocomplete && ['email', 'tel', 'url'].includes(inputType)) {
247
+ results.info.push(
248
+ `Consider adding autocomplete attribute for ${inputType} inputs`
249
+ );
250
+ }
251
+
252
+ if (results.errors.length === 0) {
253
+ results.info.push('Basic accessibility validation passed');
254
+ }
255
+
256
+ return results;
257
+ };
258
+
259
+ /**
260
+ * Input validation utility for common input types
261
+ * Provides validation messages and patterns
262
+ *
263
+ * @param {string} value - Current input value
264
+ * @param {string} type - Input type
265
+ * @param {Object} options - Validation options
266
+ * @returns {Object} Validation result with message and validity
267
+ */
268
+ export const getInputValidationInfo = (value, type, options = {}) => {
269
+ const { required = false, minLength, maxLength } = options;
270
+
271
+ let isValid = true;
272
+ let message = '';
273
+ let status = 'valid';
274
+
275
+ // Required validation
276
+ if (required && !value.trim()) {
277
+ isValid = false;
278
+ message = 'This field is required';
279
+ status = 'error';
280
+ return { isValid, message, status };
281
+ }
282
+
283
+ // Length validation
284
+ if (minLength && value.length < minLength) {
285
+ isValid = false;
286
+ message = `Minimum ${minLength} characters required`;
287
+ status = 'error';
288
+ } else if (maxLength && value.length > maxLength) {
289
+ isValid = false;
290
+ message = `Maximum ${maxLength} characters allowed`;
291
+ status = 'error';
292
+ }
293
+
294
+ // Type-specific validation
295
+ if (value && isValid) {
296
+ switch (type) {
297
+ case 'email':
298
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
299
+ if (!emailRegex.test(value)) {
300
+ isValid = false;
301
+ message = 'Please enter a valid email address';
302
+ status = 'error';
303
+ }
304
+ break;
305
+ case 'tel':
306
+ const phoneRegex = /^[\+]?[\s\-\(\)]?[\d\s\-\(\)]{10,}$/;
307
+ if (!phoneRegex.test(value)) {
308
+ isValid = false;
309
+ message = 'Please enter a valid phone number';
310
+ status = 'error';
311
+ }
312
+ break;
313
+ case 'url':
314
+ if (!value.startsWith('http://') && !value.startsWith('https://')) {
315
+ isValid = false;
316
+ message =
317
+ 'Please enter a valid URL (starting with http:// or https://)';
318
+ status = 'error';
319
+ }
320
+ break;
321
+ case 'number':
322
+ if (isNaN(Number(value))) {
323
+ isValid = false;
324
+ message = 'Please enter a valid number';
325
+ status = 'error';
326
+ }
327
+ break;
328
+ }
329
+ }
330
+
331
+ return {
332
+ isValid,
333
+ message,
334
+ status,
335
+ charCount: value.length,
336
+ remainingChars: maxLength ? maxLength - value.length : null,
337
+ };
338
+ };
@@ -0,0 +1,180 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ /**
4
+ * Custom hook for typography accessibility features
5
+ * Manages user preferences for font size, contrast, and motion
6
+ *
7
+ * @returns {Object} Typography accessibility state and utilities
8
+ */
9
+ export const useTypographyAccessibility = () => {
10
+ const [preferences, setPreferences] = useState({
11
+ highContrast: false,
12
+ reducedMotion: false,
13
+ fontSize: 100,
14
+ currentBreakpoint: 'small',
15
+ });
16
+
17
+ // Detect user's system preferences
18
+ useEffect(() => {
19
+ const detectSystemPreferences = () => {
20
+ setPreferences((prev) => ({
21
+ ...prev,
22
+ highContrast:
23
+ window.matchMedia('(prefers-contrast: high)').matches || false,
24
+ reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)')
25
+ .matches,
26
+ }));
27
+ };
28
+
29
+ detectSystemPreferences();
30
+
31
+ // Listen for changes in system preferences
32
+ const contrastQuery = window.matchMedia('(prefers-contrast: high)');
33
+ const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
34
+
35
+ contrastQuery.addEventListener('change', detectSystemPreferences);
36
+ motionQuery.addEventListener('change', detectSystemPreferences);
37
+
38
+ return () => {
39
+ contrastQuery.removeEventListener('change', detectSystemPreferences);
40
+ motionQuery.removeEventListener('change', detectSystemPreferences);
41
+ };
42
+ }, []);
43
+
44
+ // Detect current breakpoint
45
+ useEffect(() => {
46
+ const updateBreakpoint = () => {
47
+ const width = window.innerWidth;
48
+ let breakpoint;
49
+
50
+ if (width >= 1200) breakpoint = 'xlarge';
51
+ else if (width >= 992) breakpoint = 'large';
52
+ else if (width >= 768) breakpoint = 'medium';
53
+ else breakpoint = 'small';
54
+
55
+ setPreferences((prev) => ({ ...prev, currentBreakpoint: breakpoint }));
56
+ };
57
+
58
+ updateBreakpoint();
59
+ window.addEventListener('resize', updateBreakpoint);
60
+
61
+ return () => window.removeEventListener('resize', updateBreakpoint);
62
+ }, []);
63
+
64
+ // Utility functions
65
+ const toggleHighContrast = () => {
66
+ setPreferences((prev) => ({ ...prev, highContrast: !prev.highContrast }));
67
+ };
68
+
69
+ const setFontSize = (size) => {
70
+ setPreferences((prev) => ({
71
+ ...prev,
72
+ fontSize: Math.max(50, Math.min(200, size)),
73
+ }));
74
+ };
75
+
76
+ const getAccessibilityStyles = () => {
77
+ return {
78
+ fontSize: `${preferences.fontSize}%`,
79
+ filter: preferences.highContrast
80
+ ? 'contrast(150%) brightness(120%)'
81
+ : 'none',
82
+ transition: preferences.reducedMotion ? 'none' : undefined,
83
+ };
84
+ };
85
+
86
+ const getBreakpointLabel = () => {
87
+ const labels = {
88
+ small: 'Small screens (<768px)',
89
+ medium: 'Medium (768px+)',
90
+ large: 'Large (992px+)',
91
+ xlarge: 'Extra Large (1200px+)',
92
+ };
93
+ return labels[preferences.currentBreakpoint];
94
+ };
95
+
96
+ return {
97
+ preferences,
98
+ toggleHighContrast,
99
+ setFontSize,
100
+ getAccessibilityStyles,
101
+ getBreakpointLabel,
102
+ };
103
+ };
104
+
105
+ /**
106
+ * Utility for generating typography class names with accessibility considerations
107
+ *
108
+ * @param {string} baseClass - Base class name
109
+ * @param {string} size - Size variant (small, medium, large)
110
+ * @param {string} additionalClass - Additional class names
111
+ * @param {Object} preferences - Accessibility preferences
112
+ * @returns {string} Combined class names
113
+ */
114
+ export const getTypographyClassNames = (
115
+ baseClass,
116
+ size = 'medium',
117
+ additionalClass = '',
118
+ preferences = {}
119
+ ) => {
120
+ const classes = [baseClass];
121
+
122
+ // Add size modifier
123
+ if (size && size !== 'medium') {
124
+ classes.push(`${baseClass}--${size}`);
125
+ }
126
+
127
+ // Add accessibility modifiers
128
+ if (preferences.highContrast) {
129
+ classes.push(`${baseClass}--high-contrast`);
130
+ }
131
+
132
+ if (preferences.reducedMotion) {
133
+ classes.push(`${baseClass}--reduced-motion`);
134
+ }
135
+
136
+ // Add additional classes
137
+ if (additionalClass) {
138
+ classes.push(additionalClass);
139
+ }
140
+
141
+ return classes.join(' ');
142
+ };
143
+
144
+ /**
145
+ * Focus management utility for typography components
146
+ * Ensures proper focus indicators and keyboard navigation
147
+ *
148
+ * @param {React.RefObject} elementRef - Reference to the element
149
+ * @param {Object} options - Focus options
150
+ */
151
+ export const useFocusManagement = (elementRef, options = {}) => {
152
+ const { onFocus, onBlur, focusOnMount = false, trapFocus = false } = options;
153
+
154
+ useEffect(() => {
155
+ const element = elementRef.current;
156
+ if (!element) return;
157
+
158
+ if (focusOnMount) {
159
+ element.focus();
160
+ }
161
+
162
+ const handleFocus = (event) => {
163
+ element.setAttribute('data-focused', 'true');
164
+ onFocus?.(event);
165
+ };
166
+
167
+ const handleBlur = (event) => {
168
+ element.removeAttribute('data-focused');
169
+ onBlur?.(event);
170
+ };
171
+
172
+ element.addEventListener('focus', handleFocus);
173
+ element.addEventListener('blur', handleBlur);
174
+
175
+ return () => {
176
+ element.removeEventListener('focus', handleFocus);
177
+ element.removeEventListener('blur', handleBlur);
178
+ };
179
+ }, [elementRef, onFocus, onBlur, focusOnMount, trapFocus]);
180
+ };
@@ -0,0 +1,26 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export const useWindowSize = () => {
4
+ // Initialize state with undefined width/height so server and client renders match
5
+ const [windowSize, setWindowSize] = useState({
6
+ width: undefined,
7
+ height: undefined,
8
+ });
9
+ useEffect(() => {
10
+ // Handler to call on window resize
11
+ function handleResize() {
12
+ // Set window width/height to state
13
+ setWindowSize({
14
+ width: window.innerWidth,
15
+ height: window.innerHeight,
16
+ });
17
+ }
18
+ // Add event listener
19
+ window.addEventListener('resize', handleResize);
20
+ // Call handler right away so state gets updated with initial window size
21
+ handleResize();
22
+ // Remove event listener on cleanup
23
+ return () => window.removeEventListener('resize', handleResize);
24
+ }, []); // Empty array ensures that effect is only run on mount
25
+ return windowSize;
26
+ };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Validation utilities for form inputs
3
+ * Contains common validation patterns and functions
4
+ */
5
+
6
+ // Common email validation regex pattern
7
+ // This pattern checks for basic email format: user@domain.extension
8
+ export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
+
10
+ // More comprehensive email regex (RFC 5322 compliant, simplified)
11
+ export const EMAIL_REGEX_STRICT =
12
+ /^[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])?)*$/;
13
+
14
+ // Common validation patterns
15
+ export const VALIDATION_PATTERNS = {
16
+ email: EMAIL_REGEX,
17
+ phone: /^[\+]?[1-9][\d]{0,15}$/,
18
+ url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
19
+ postalCode: /^[A-Za-z0-9\s-]{3,10}$/,
20
+ creditCard: /^[0-9]{13,19}$/,
21
+ strongPassword:
22
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
23
+ };
24
+
25
+ /**
26
+ * Validates an email address
27
+ * @param {string} email - The email address to validate
28
+ * @param {boolean} strict - Whether to use strict RFC 5322 validation
29
+ * @returns {boolean} - True if email is valid
30
+ */
31
+ export const validateEmail = (email, strict = false) => {
32
+ if (!email || typeof email !== 'string') {
33
+ return false;
34
+ }
35
+ const pattern = strict ? EMAIL_REGEX_STRICT : EMAIL_REGEX;
36
+ return pattern.test(email.trim());
37
+ };
38
+
39
+ /**
40
+ * Validates a phone number
41
+ * @param {string} phone - The phone number to validate
42
+ * @returns {boolean} - True if phone is valid
43
+ */
44
+ export const validatePhone = (phone) => {
45
+ if (!phone || typeof phone !== 'string') {
46
+ return false;
47
+ }
48
+ return VALIDATION_PATTERNS.phone.test(phone.replace(/[\s\-\(\)]/g, ''));
49
+ };
50
+
51
+ /**
52
+ * Validates a URL
53
+ * @param {string} url - The URL to validate
54
+ * @returns {boolean} - True if URL is valid
55
+ */
56
+ export const validateUrl = (url) => {
57
+ if (!url || typeof url !== 'string') {
58
+ return false;
59
+ }
60
+ return VALIDATION_PATTERNS.url.test(url.trim());
61
+ };
62
+
63
+ /**
64
+ * Validates password strength
65
+ * @param {string} password - The password to validate
66
+ * @returns {boolean} - True if password meets strength requirements
67
+ */
68
+ export const validatePasswordStrength = (password) => {
69
+ if (!password || typeof password !== 'string') {
70
+ return false;
71
+ }
72
+ return VALIDATION_PATTERNS.strongPassword.test(password);
73
+ };
74
+
75
+ /**
76
+ * Generic field validation function
77
+ * @param {string} value - The value to validate
78
+ * @param {Object} rules - Validation rules object
79
+ * @param {string} fieldName - Name of the field for error messages
80
+ * @returns {string|null} - Error message or null if valid
81
+ */
82
+ export const validateField = (value, rules, fieldName) => {
83
+ if (!rules) return null;
84
+
85
+ // Required validation
86
+ if (rules.required && (!value || value.trim() === '')) {
87
+ return `${fieldName} is required`;
88
+ }
89
+
90
+ // Skip other validations if field is empty and not required
91
+ if (!value || value.trim() === '') {
92
+ return null;
93
+ }
94
+
95
+ // Email validation
96
+ if (rules.email && !validateEmail(value, rules.strict)) {
97
+ return 'Please enter a valid email address';
98
+ }
99
+
100
+ // Phone validation
101
+ if (rules.phone && !validatePhone(value)) {
102
+ return 'Please enter a valid phone number';
103
+ }
104
+
105
+ // URL validation
106
+ if (rules.url && !validateUrl(value)) {
107
+ return 'Please enter a valid URL';
108
+ }
109
+
110
+ // Password strength validation
111
+ if (rules.strongPassword && !validatePasswordStrength(value)) {
112
+ return 'Password must contain at least 8 characters with uppercase, lowercase, number, and special character';
113
+ }
114
+
115
+ // Minimum length validation
116
+ if (rules.minLength && value.length < rules.minLength) {
117
+ return `${fieldName} must be at least ${rules.minLength} characters`;
118
+ }
119
+
120
+ // Maximum length validation
121
+ if (rules.maxLength && value.length > rules.maxLength) {
122
+ return `${fieldName} must not exceed ${rules.maxLength} characters`;
123
+ }
124
+
125
+ // Pattern validation
126
+ if (rules.pattern && !rules.pattern.test(value)) {
127
+ return rules.patternMessage || `${fieldName} format is invalid`;
128
+ }
129
+
130
+ return null;
131
+ };