@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,168 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useTypographyAccessibility = exports.useFocusManagement = exports.getTypographyClassNames = void 0;
7
+ var _react = require("react");
8
+ /**
9
+ * Custom hook for typography accessibility features
10
+ * Manages user preferences for font size, contrast, and motion
11
+ *
12
+ * @returns {Object} Typography accessibility state and utilities
13
+ */
14
+ const useTypographyAccessibility = () => {
15
+ const [preferences, setPreferences] = (0, _react.useState)({
16
+ highContrast: false,
17
+ reducedMotion: false,
18
+ fontSize: 100,
19
+ currentBreakpoint: 'small'
20
+ });
21
+
22
+ // Detect user's system preferences
23
+ (0, _react.useEffect)(() => {
24
+ const detectSystemPreferences = () => {
25
+ setPreferences(prev => ({
26
+ ...prev,
27
+ highContrast: window.matchMedia('(prefers-contrast: high)').matches || false,
28
+ reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
29
+ }));
30
+ };
31
+ detectSystemPreferences();
32
+
33
+ // Listen for changes in system preferences
34
+ const contrastQuery = window.matchMedia('(prefers-contrast: high)');
35
+ const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
36
+ contrastQuery.addEventListener('change', detectSystemPreferences);
37
+ motionQuery.addEventListener('change', detectSystemPreferences);
38
+ return () => {
39
+ contrastQuery.removeEventListener('change', detectSystemPreferences);
40
+ motionQuery.removeEventListener('change', detectSystemPreferences);
41
+ };
42
+ }, []);
43
+
44
+ // Detect current breakpoint
45
+ (0, _react.useEffect)(() => {
46
+ const updateBreakpoint = () => {
47
+ const width = window.innerWidth;
48
+ let breakpoint;
49
+ if (width >= 1200) breakpoint = 'xlarge';else if (width >= 992) breakpoint = 'large';else if (width >= 768) breakpoint = 'medium';else breakpoint = 'small';
50
+ setPreferences(prev => ({
51
+ ...prev,
52
+ currentBreakpoint: breakpoint
53
+ }));
54
+ };
55
+ updateBreakpoint();
56
+ window.addEventListener('resize', updateBreakpoint);
57
+ return () => window.removeEventListener('resize', updateBreakpoint);
58
+ }, []);
59
+
60
+ // Utility functions
61
+ const toggleHighContrast = () => {
62
+ setPreferences(prev => ({
63
+ ...prev,
64
+ highContrast: !prev.highContrast
65
+ }));
66
+ };
67
+ const setFontSize = size => {
68
+ setPreferences(prev => ({
69
+ ...prev,
70
+ fontSize: Math.max(50, Math.min(200, size))
71
+ }));
72
+ };
73
+ const getAccessibilityStyles = () => {
74
+ return {
75
+ fontSize: `${preferences.fontSize}%`,
76
+ filter: preferences.highContrast ? 'contrast(150%) brightness(120%)' : 'none',
77
+ transition: preferences.reducedMotion ? 'none' : undefined
78
+ };
79
+ };
80
+ const getBreakpointLabel = () => {
81
+ const labels = {
82
+ small: 'Small screens (<768px)',
83
+ medium: 'Medium (768px+)',
84
+ large: 'Large (992px+)',
85
+ xlarge: 'Extra Large (1200px+)'
86
+ };
87
+ return labels[preferences.currentBreakpoint];
88
+ };
89
+ return {
90
+ preferences,
91
+ toggleHighContrast,
92
+ setFontSize,
93
+ getAccessibilityStyles,
94
+ getBreakpointLabel
95
+ };
96
+ };
97
+
98
+ /**
99
+ * Utility for generating typography class names with accessibility considerations
100
+ *
101
+ * @param {string} baseClass - Base class name
102
+ * @param {string} size - Size variant (small, medium, large)
103
+ * @param {string} additionalClass - Additional class names
104
+ * @param {Object} preferences - Accessibility preferences
105
+ * @returns {string} Combined class names
106
+ */
107
+ exports.useTypographyAccessibility = useTypographyAccessibility;
108
+ const getTypographyClassNames = (baseClass, size = 'medium', additionalClass = '', preferences = {}) => {
109
+ const classes = [baseClass];
110
+
111
+ // Add size modifier
112
+ if (size && size !== 'medium') {
113
+ classes.push(`${baseClass}--${size}`);
114
+ }
115
+
116
+ // Add accessibility modifiers
117
+ if (preferences.highContrast) {
118
+ classes.push(`${baseClass}--high-contrast`);
119
+ }
120
+ if (preferences.reducedMotion) {
121
+ classes.push(`${baseClass}--reduced-motion`);
122
+ }
123
+
124
+ // Add additional classes
125
+ if (additionalClass) {
126
+ classes.push(additionalClass);
127
+ }
128
+ return classes.join(' ');
129
+ };
130
+
131
+ /**
132
+ * Focus management utility for typography components
133
+ * Ensures proper focus indicators and keyboard navigation
134
+ *
135
+ * @param {React.RefObject} elementRef - Reference to the element
136
+ * @param {Object} options - Focus options
137
+ */
138
+ exports.getTypographyClassNames = getTypographyClassNames;
139
+ const useFocusManagement = (elementRef, options = {}) => {
140
+ const {
141
+ onFocus,
142
+ onBlur,
143
+ focusOnMount = false,
144
+ trapFocus = false
145
+ } = options;
146
+ (0, _react.useEffect)(() => {
147
+ const element = elementRef.current;
148
+ if (!element) return;
149
+ if (focusOnMount) {
150
+ element.focus();
151
+ }
152
+ const handleFocus = event => {
153
+ element.setAttribute('data-focused', 'true');
154
+ onFocus?.(event);
155
+ };
156
+ const handleBlur = event => {
157
+ element.removeAttribute('data-focused');
158
+ onBlur?.(event);
159
+ };
160
+ element.addEventListener('focus', handleFocus);
161
+ element.addEventListener('blur', handleBlur);
162
+ return () => {
163
+ element.removeEventListener('focus', handleFocus);
164
+ element.removeEventListener('blur', handleBlur);
165
+ };
166
+ }, [elementRef, onFocus, onBlur, focusOnMount, trapFocus]);
167
+ };
168
+ exports.useFocusManagement = useFocusManagement;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useWindowSize = void 0;
7
+ var _react = require("react");
8
+ const useWindowSize = () => {
9
+ // Initialize state with undefined width/height so server and client renders match
10
+ const [windowSize, setWindowSize] = (0, _react.useState)({
11
+ width: undefined,
12
+ height: undefined
13
+ });
14
+ (0, _react.useEffect)(() => {
15
+ // Handler to call on window resize
16
+ function handleResize() {
17
+ // Set window width/height to state
18
+ setWindowSize({
19
+ width: window.innerWidth,
20
+ height: window.innerHeight
21
+ });
22
+ }
23
+ // Add event listener
24
+ window.addEventListener('resize', handleResize);
25
+ // Call handler right away so state gets updated with initial window size
26
+ handleResize();
27
+ // Remove event listener on cleanup
28
+ return () => window.removeEventListener('resize', handleResize);
29
+ }, []); // Empty array ensures that effect is only run on mount
30
+ return windowSize;
31
+ };
32
+ exports.useWindowSize = useWindowSize;
@@ -0,0 +1,26 @@
1
+ import { useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { withRouter } from 'react-router-dom';
4
+
5
+ const ScrollHandler = ({ location }) => {
6
+ useEffect(() => {
7
+ const element = document.getElementById(location.hash);
8
+
9
+ setTimeout(() => {
10
+ window.scrollTo({
11
+ behavior: element ? 'smooth' : 'auto',
12
+ top: element ? element.offsetTop : 0,
13
+ });
14
+ }, 100);
15
+ }, [location]);
16
+
17
+ return null;
18
+ };
19
+
20
+ ScrollHandler.propTypes = {
21
+ location: PropTypes.shape({
22
+ hash: PropTypes.string,
23
+ }).isRequired,
24
+ };
25
+
26
+ export default withRouter(ScrollHandler);
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Accessibility utility functions for screen readers and ARIA support
3
+ */
4
+
5
+ /**
6
+ * Announces a message to screen readers using ARIA live regions
7
+ * This is a safer alternative to direct DOM manipulation
8
+ *
9
+ * @param {string} message - The message to announce
10
+ * @param {'polite'|'assertive'} priority - The announcement priority level
11
+ * @param {number} timeout - How long to keep the announcement (default: 1000ms)
12
+ * @returns {Function} Cleanup function to remove the announcement early
13
+ *
14
+ * @example
15
+ * // Announce an error assertively
16
+ * const cleanup = announceToScreenReader('Form validation failed', 'assertive');
17
+ *
18
+ * // Announce info politely
19
+ * announceToScreenReader('Data saved automatically', 'polite');
20
+ */
21
+ export const announceToScreenReader = (
22
+ message,
23
+ priority = 'polite',
24
+ timeout = 1000
25
+ ) => {
26
+ // Ensure we're in a browser environment
27
+ if (typeof document === 'undefined') {
28
+ return () => {}; // Return no-op cleanup function for SSR
29
+ }
30
+
31
+ const announcement = document.createElement('div');
32
+ announcement.setAttribute('aria-live', priority);
33
+ announcement.setAttribute('aria-atomic', 'true');
34
+ announcement.className = 'sr-only'; // Use CSS class instead of inline styles
35
+ announcement.textContent = message;
36
+
37
+ // Add to document body
38
+ document.body.appendChild(announcement);
39
+
40
+ // Set up cleanup
41
+ const cleanup = () => {
42
+ if (document.body.contains(announcement)) {
43
+ document.body.removeChild(announcement);
44
+ }
45
+ };
46
+
47
+ // Auto cleanup after timeout
48
+ setTimeout(cleanup, timeout);
49
+
50
+ // Return cleanup function for manual cleanup if needed
51
+ return cleanup;
52
+ };
53
+
54
+ /**
55
+ * Creates a live region element that can be reused for announcements
56
+ * This is more efficient for multiple announcements
57
+ *
58
+ * @param {'polite'|'assertive'} priority - The announcement priority level
59
+ * @returns {Object} Object with announce and destroy methods
60
+ *
61
+ * @example
62
+ * const liveRegion = createLiveRegion('assertive');
63
+ * liveRegion.announce('First message');
64
+ * liveRegion.announce('Second message');
65
+ * liveRegion.destroy(); // Clean up when done
66
+ */
67
+ export const createLiveRegion = (priority = 'polite') => {
68
+ // Ensure we're in a browser environment
69
+ if (typeof document === 'undefined') {
70
+ return {
71
+ announce: () => {},
72
+ destroy: () => {},
73
+ };
74
+ }
75
+
76
+ const liveRegion = document.createElement('div');
77
+ liveRegion.setAttribute('aria-live', priority);
78
+ liveRegion.setAttribute('aria-atomic', 'true');
79
+ liveRegion.className = 'sr-only';
80
+ liveRegion.setAttribute(
81
+ 'role',
82
+ priority === 'assertive' ? 'alert' : 'status'
83
+ );
84
+
85
+ document.body.appendChild(liveRegion);
86
+
87
+ return {
88
+ /**
89
+ * Announce a message in this live region
90
+ * @param {string} message - The message to announce
91
+ */
92
+ announce: (message) => {
93
+ liveRegion.textContent = message;
94
+ // Clear after a short delay to allow for new announcements
95
+ setTimeout(() => {
96
+ if (liveRegion.textContent === message) {
97
+ liveRegion.textContent = '';
98
+ }
99
+ }, 1000);
100
+ },
101
+
102
+ /**
103
+ * Clean up the live region
104
+ */
105
+ destroy: () => {
106
+ if (document.body.contains(liveRegion)) {
107
+ document.body.removeChild(liveRegion);
108
+ }
109
+ },
110
+ };
111
+ };
112
+
113
+ /**
114
+ * Utility to focus an element safely with fallback
115
+ * @param {HTMLElement|string} element - Element or selector to focus
116
+ * @param {Object} options - Focus options
117
+ * @returns {boolean} Whether focus was successful
118
+ */
119
+ export const safeFocus = (element, options = {}) => {
120
+ try {
121
+ const el =
122
+ typeof element === 'string' ? document.querySelector(element) : element;
123
+
124
+ if (el && typeof el.focus === 'function') {
125
+ el.focus(options);
126
+ return document.activeElement === el;
127
+ }
128
+ return false;
129
+ } catch (error) {
130
+ console.warn('Failed to focus element:', error);
131
+ return false;
132
+ }
133
+ };
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Hero Component Utilities
3
+ * Provides helper functions and hooks for Hero component functionality
4
+ */
5
+
6
+ import { useState, useEffect, useCallback } from 'react';
7
+
8
+ /**
9
+ * Hook to detect user's motion preference and apply appropriate settings
10
+ * @returns {Object} Motion preference settings
11
+ */
12
+ export const useMotionPreference = () => {
13
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
14
+
15
+ useEffect(() => {
16
+ if (typeof window !== 'undefined') {
17
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
18
+ setPrefersReducedMotion(mediaQuery.matches);
19
+
20
+ const handleChange = (e) => {
21
+ setPrefersReducedMotion(e.matches);
22
+ };
23
+
24
+ mediaQuery.addEventListener('change', handleChange);
25
+ return () => mediaQuery.removeEventListener('change', handleChange);
26
+ }
27
+ }, []);
28
+
29
+ return {
30
+ prefersReducedMotion,
31
+ backgroundAttachment: prefersReducedMotion ? 'scroll' : 'fixed',
32
+ transitionDuration: prefersReducedMotion ? '0s' : '0.3s',
33
+ };
34
+ };
35
+
36
+ /**
37
+ * Hook to detect high contrast mode preference
38
+ * @returns {boolean} Whether high contrast mode is enabled
39
+ */
40
+ export const useHighContrast = () => {
41
+ const [highContrast, setHighContrast] = useState(false);
42
+
43
+ useEffect(() => {
44
+ if (typeof window !== 'undefined') {
45
+ const mediaQuery = window.matchMedia('(prefers-contrast: high)');
46
+ setHighContrast(mediaQuery.matches);
47
+
48
+ const handleChange = (e) => {
49
+ setHighContrast(e.matches);
50
+ };
51
+
52
+ mediaQuery.addEventListener('change', handleChange);
53
+ return () => mediaQuery.removeEventListener('change', handleChange);
54
+ }
55
+ }, []);
56
+
57
+ return highContrast;
58
+ };
59
+
60
+ /**
61
+ * Hook for responsive hero height based on viewport
62
+ * @param {Object} options - Configuration options
63
+ * @param {string} options.minHeight - Minimum height (default: '400px')
64
+ * @param {string} options.maxHeight - Maximum height (default: '100vh')
65
+ * @returns {Object} Height settings and viewport info
66
+ */
67
+ export const useResponsiveHeight = ({
68
+ minHeight = '400px',
69
+ maxHeight = '100vh',
70
+ } = {}) => {
71
+ const [viewportHeight, setViewportHeight] = useState(
72
+ typeof window !== 'undefined' ? window.innerHeight : 800
73
+ );
74
+ const [isMobile, setIsMobile] = useState(false);
75
+
76
+ useEffect(() => {
77
+ const updateHeight = () => {
78
+ setViewportHeight(window.innerHeight);
79
+ setIsMobile(window.innerWidth < 768);
80
+ };
81
+
82
+ updateHeight();
83
+ window.addEventListener('resize', updateHeight);
84
+ return () => window.removeEventListener('resize', updateHeight);
85
+ }, []);
86
+
87
+ const calculatedHeight = Math.max(
88
+ parseInt(minHeight),
89
+ Math.min(viewportHeight * 0.7, parseInt(maxHeight))
90
+ );
91
+
92
+ return {
93
+ height: `${calculatedHeight}px`,
94
+ viewportHeight,
95
+ isMobile,
96
+ isLandscape: viewportHeight < window.innerWidth,
97
+ };
98
+ };
99
+
100
+ /**
101
+ * Hook for managing background image loading and optimization
102
+ * @param {string} imageUrl - URL of the background image
103
+ * @returns {Object} Image loading state and optimized URL
104
+ */
105
+ export const useBackgroundImage = (imageUrl) => {
106
+ const [isLoaded, setIsLoaded] = useState(false);
107
+ const [hasError, setHasError] = useState(false);
108
+ const [optimizedUrl, setOptimizedUrl] = useState(imageUrl);
109
+
110
+ useEffect(() => {
111
+ if (!imageUrl) return;
112
+
113
+ setIsLoaded(false);
114
+ setHasError(false);
115
+
116
+ const img = new Image();
117
+ img.onload = () => setIsLoaded(true);
118
+ img.onerror = () => setHasError(true);
119
+ img.src = imageUrl;
120
+
121
+ // Basic URL optimization for common image services
122
+ if (imageUrl.includes('unsplash.com')) {
123
+ const size =
124
+ window.innerWidth > 1200
125
+ ? '2070'
126
+ : window.innerWidth > 768
127
+ ? '1536'
128
+ : '1024';
129
+ setOptimizedUrl(`${imageUrl}&w=${size}&auto=format&fit=crop&q=80`);
130
+ } else {
131
+ setOptimizedUrl(imageUrl);
132
+ }
133
+ }, [imageUrl]);
134
+
135
+ return {
136
+ isLoaded,
137
+ hasError,
138
+ optimizedUrl,
139
+ };
140
+ };
141
+
142
+ /**
143
+ * Generates accessible hero props based on content and configuration
144
+ * @param {Object} config - Configuration object
145
+ * @returns {Object} Accessibility props
146
+ */
147
+ export const generateHeroAccessibility = ({
148
+ hasHeading = true,
149
+ hasCallToAction = false,
150
+ isLandingPage = false,
151
+ customRole = null,
152
+ customLabel = null,
153
+ }) => {
154
+ const role = customRole || (isLandingPage ? 'banner' : 'region');
155
+
156
+ const ariaLabel =
157
+ customLabel ||
158
+ (hasCallToAction
159
+ ? 'Hero section with call-to-action'
160
+ : 'Hero content section');
161
+
162
+ const props = {
163
+ role,
164
+ 'aria-label': ariaLabel,
165
+ };
166
+
167
+ // Add landmark navigation hints
168
+ if (hasHeading && !customLabel) {
169
+ delete props['aria-label']; // Let heading provide the label
170
+ }
171
+
172
+ return props;
173
+ };
174
+
175
+ /**
176
+ * Validates hero content for accessibility compliance
177
+ * @param {Object} content - Content configuration
178
+ * @returns {Array} Array of accessibility warnings
179
+ */
180
+ export const validateHeroAccessibility = ({
181
+ hasBackground = false,
182
+ backgroundAlt = '',
183
+ hasHeading = false,
184
+ headingLevel = null,
185
+ hasCallToAction = false,
186
+ textContrast = 'unknown',
187
+ }) => {
188
+ const warnings = [];
189
+
190
+ // Background image validation
191
+ if (hasBackground && !backgroundAlt) {
192
+ warnings.push({
193
+ type: 'error',
194
+ message: 'Background images must have alt text for accessibility',
195
+ recommendation: 'Add backgroundAlt prop with descriptive text',
196
+ });
197
+ }
198
+
199
+ // Heading validation
200
+ if (!hasHeading) {
201
+ warnings.push({
202
+ type: 'warning',
203
+ message: 'Hero sections should typically include a heading',
204
+ recommendation: 'Add an h1 or h2 element to provide section context',
205
+ });
206
+ }
207
+
208
+ if (hasHeading && headingLevel && headingLevel > 2) {
209
+ warnings.push({
210
+ type: 'warning',
211
+ message: `Hero heading level h${headingLevel} may be too deep`,
212
+ recommendation: 'Consider using h1 or h2 for hero sections',
213
+ });
214
+ }
215
+
216
+ // Call-to-action validation
217
+ if (hasCallToAction) {
218
+ warnings.push({
219
+ type: 'info',
220
+ message: 'Ensure CTA buttons have descriptive aria-label attributes',
221
+ recommendation:
222
+ 'Use aria-label to describe the action, not just "Click here"',
223
+ });
224
+ }
225
+
226
+ // Contrast validation
227
+ if (hasBackground && textContrast === 'low') {
228
+ warnings.push({
229
+ type: 'error',
230
+ message: 'Text contrast may be insufficient over background image',
231
+ recommendation:
232
+ 'Add overlay or adjust text colors to meet WCAG contrast requirements',
233
+ });
234
+ }
235
+
236
+ return warnings;
237
+ };
238
+
239
+ /**
240
+ * Hero testing utilities for automated and manual testing
241
+ */
242
+ export const heroTestingUtils = {
243
+ /**
244
+ * Simulates keyboard navigation through hero content
245
+ */
246
+ async simulateKeyboardNavigation() {
247
+ const focusableElements = document.querySelectorAll(
248
+ '[role="banner"] a, [role="banner"] button, [role="banner"] input, [role="banner"] [tabindex="0"]'
249
+ );
250
+
251
+ const results = [];
252
+
253
+ for (let element of focusableElements) {
254
+ element.focus();
255
+ const isVisible = this.isFocusVisible(element);
256
+ const hasAriaLabel =
257
+ element.getAttribute('aria-label') ||
258
+ element.getAttribute('aria-labelledby');
259
+
260
+ results.push({
261
+ element: element.tagName,
262
+ isFocusVisible: isVisible,
263
+ hasAriaLabel: !!hasAriaLabel,
264
+ textContent: element.textContent?.trim().substring(0, 50),
265
+ });
266
+
267
+ // Small delay to simulate real user interaction
268
+ await new Promise((resolve) => setTimeout(resolve, 100));
269
+ }
270
+
271
+ return results;
272
+ },
273
+
274
+ /**
275
+ * Checks if focus indicators are visible
276
+ */
277
+ isFocusVisible(element) {
278
+ const styles = window.getComputedStyle(element, ':focus-visible');
279
+ return styles.outlineWidth !== '0px' || styles.boxShadow !== 'none';
280
+ },
281
+
282
+ /**
283
+ * Validates screen reader announcements
284
+ */
285
+ validateScreenReaderContent() {
286
+ const hero = document.querySelector('[role="banner"], [role="region"]');
287
+ if (!hero) return { valid: false, message: 'No hero section found' };
288
+
289
+ const hasAriaLabel =
290
+ hero.getAttribute('aria-label') || hero.getAttribute('aria-labelledby');
291
+ const hasHeading = hero.querySelector('h1, h2, h3, h4, h5, h6');
292
+ const backgroundDescription = hero.querySelector('[id*="bg-description"]');
293
+
294
+ return {
295
+ valid: hasAriaLabel || hasHeading,
296
+ hasAriaLabel: !!hasAriaLabel,
297
+ hasHeading: !!hasHeading,
298
+ hasBackgroundDescription: !!backgroundDescription,
299
+ recommendations: [
300
+ ...(!(hasAriaLabel || hasHeading)
301
+ ? ['Add aria-label or heading for screen readers']
302
+ : []),
303
+ ...(!backgroundDescription && hero.style.backgroundImage
304
+ ? ['Add background image description']
305
+ : []),
306
+ ],
307
+ };
308
+ },
309
+
310
+ /**
311
+ * Tests responsive behavior
312
+ */
313
+ testResponsiveBehavior() {
314
+ const viewports = [
315
+ { width: 320, height: 568, name: 'Mobile Portrait' },
316
+ { width: 768, height: 1024, name: 'Tablet' },
317
+ { width: 1200, height: 800, name: 'Desktop' },
318
+ { width: 1920, height: 1080, name: 'Large Desktop' },
319
+ ];
320
+
321
+ const hero = document.querySelector('.hero-wrapper');
322
+ if (!hero) return { valid: false, message: 'Hero wrapper not found' };
323
+
324
+ const results = viewports.map((viewport) => {
325
+ // Note: In real testing, you'd actually resize the viewport
326
+ // This is a simulation for documentation purposes
327
+ return {
328
+ viewport: viewport.name,
329
+ dimensions: `${viewport.width}x${viewport.height}`,
330
+ minHeight: window.getComputedStyle(hero).minHeight,
331
+ backgroundAttachment:
332
+ window.getComputedStyle(hero).backgroundAttachment,
333
+ };
334
+ });
335
+
336
+ return { valid: true, results };
337
+ },
338
+ };
339
+
340
+ export default {
341
+ useMotionPreference,
342
+ useHighContrast,
343
+ useResponsiveHeight,
344
+ useBackgroundImage,
345
+ generateHeroAccessibility,
346
+ validateHeroAccessibility,
347
+ heroTestingUtils,
348
+ };