@shohojdhara/atomix 0.4.8 → 0.4.9

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 (165) hide show
  1. package/atomix.config.ts +58 -1
  2. package/dist/atomix.css +148 -120
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +1 -1
  5. package/dist/atomix.min.css.map +1 -1
  6. package/dist/charts.d.ts +33 -0
  7. package/dist/charts.js +1227 -122
  8. package/dist/charts.js.map +1 -1
  9. package/dist/core.d.ts +33 -10
  10. package/dist/core.js +1052 -41
  11. package/dist/core.js.map +1 -1
  12. package/dist/forms.d.ts +33 -0
  13. package/dist/forms.js +2086 -1035
  14. package/dist/forms.js.map +1 -1
  15. package/dist/heavy.d.ts +42 -1
  16. package/dist/heavy.js +1620 -600
  17. package/dist/heavy.js.map +1 -1
  18. package/dist/index.d.ts +441 -270
  19. package/dist/index.esm.js +1900 -638
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/index.js +1935 -670
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.min.js +1 -1
  24. package/dist/index.min.js.map +1 -1
  25. package/package.json +6 -3
  26. package/scripts/atomix-cli.js +148 -4
  27. package/scripts/cli/__tests__/basic.test.js +3 -2
  28. package/scripts/cli/__tests__/clean.test.js +278 -0
  29. package/scripts/cli/__tests__/component-validator.test.js +433 -0
  30. package/scripts/cli/__tests__/generator.test.js +613 -0
  31. package/scripts/cli/__tests__/glass-motion.test.js +256 -0
  32. package/scripts/cli/__tests__/integration.test.js +719 -108
  33. package/scripts/cli/__tests__/migrate.test.js +74 -0
  34. package/scripts/cli/__tests__/security.test.js +206 -0
  35. package/scripts/cli/__tests__/test-setup.js +3 -1
  36. package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
  37. package/scripts/cli/__tests__/token-provider.test.js +361 -0
  38. package/scripts/cli/__tests__/utils.test.js +5 -5
  39. package/scripts/cli/commands/benchmark.js +105 -0
  40. package/scripts/cli/commands/build-theme.js +4 -1
  41. package/scripts/cli/commands/clean.js +109 -0
  42. package/scripts/cli/commands/doctor.js +88 -0
  43. package/scripts/cli/commands/generate.js +135 -14
  44. package/scripts/cli/commands/init.js +45 -18
  45. package/scripts/cli/commands/migrate.js +106 -0
  46. package/scripts/cli/commands/sync-tokens.js +206 -0
  47. package/scripts/cli/commands/theme-bridge.js +248 -0
  48. package/scripts/cli/commands/tokens.js +157 -0
  49. package/scripts/cli/commands/validate.js +194 -0
  50. package/scripts/cli/internal/ai-engine.js +156 -0
  51. package/scripts/cli/internal/component-validator.js +443 -0
  52. package/scripts/cli/internal/config-loader.js +162 -0
  53. package/scripts/cli/internal/filesystem.js +102 -2
  54. package/scripts/cli/internal/generator.js +359 -39
  55. package/scripts/cli/internal/glass-generator.js +398 -0
  56. package/scripts/cli/internal/hook-generator.js +369 -0
  57. package/scripts/cli/internal/hooks.js +61 -0
  58. package/scripts/cli/internal/itcss-generator.js +565 -0
  59. package/scripts/cli/internal/motion-generator.js +679 -0
  60. package/scripts/cli/internal/template-engine.js +301 -0
  61. package/scripts/cli/internal/theme-bridge.js +664 -0
  62. package/scripts/cli/internal/tokens/engine.js +122 -0
  63. package/scripts/cli/internal/tokens/provider.js +34 -0
  64. package/scripts/cli/internal/tokens/providers/figma.js +50 -0
  65. package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
  66. package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
  67. package/scripts/cli/internal/tokens/token-provider.js +443 -0
  68. package/scripts/cli/internal/tokens/token-validator.js +513 -0
  69. package/scripts/cli/internal/validator.js +276 -0
  70. package/scripts/cli/internal/wizard.js +60 -6
  71. package/scripts/cli/mappings.js +23 -0
  72. package/scripts/cli/migration-tools.js +164 -94
  73. package/scripts/cli/plugins/style-dictionary.js +46 -0
  74. package/scripts/cli/templates/README.md +525 -95
  75. package/scripts/cli/templates/common-templates.js +40 -14
  76. package/scripts/cli/templates/components/react-component.ts +282 -0
  77. package/scripts/cli/templates/config/project-config.ts +112 -0
  78. package/scripts/cli/templates/hooks/use-component.ts +477 -0
  79. package/scripts/cli/templates/index.js +19 -4
  80. package/scripts/cli/templates/index.ts +171 -0
  81. package/scripts/cli/templates/next-templates.js +72 -0
  82. package/scripts/cli/templates/react-templates.js +70 -126
  83. package/scripts/cli/templates/scss-templates.js +35 -35
  84. package/scripts/cli/templates/stories/storybook-story.ts +241 -0
  85. package/scripts/cli/templates/styles/scss-component.ts +255 -0
  86. package/scripts/cli/templates/tests/vitest-test.ts +229 -0
  87. package/scripts/cli/templates/token-templates.js +337 -1
  88. package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
  89. package/scripts/cli/templates/types/component-types.ts +145 -0
  90. package/scripts/cli/templates/utils/testing-utils.ts +144 -0
  91. package/scripts/cli/templates/vanilla-templates.js +39 -0
  92. package/scripts/cli/token-manager.js +8 -2
  93. package/scripts/cli/utils/cache-manager.js +240 -0
  94. package/scripts/cli/utils/detector.js +46 -0
  95. package/scripts/cli/utils/diagnostics.js +289 -0
  96. package/scripts/cli/utils/error.js +45 -3
  97. package/scripts/cli/utils/helpers.js +24 -0
  98. package/scripts/cli/utils/logger.js +1 -1
  99. package/scripts/cli/utils/security.js +302 -0
  100. package/scripts/cli/utils/telemetry.js +115 -0
  101. package/scripts/cli/utils/validation.js +4 -38
  102. package/scripts/cli/utils.js +46 -0
  103. package/src/components/Accordion/Accordion.stories.tsx +0 -18
  104. package/src/components/Accordion/Accordion.test.tsx +0 -17
  105. package/src/components/Accordion/Accordion.tsx +0 -4
  106. package/src/components/AtomixGlass/AtomixGlass.tsx +102 -2
  107. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
  108. package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
  109. package/src/components/AtomixGlass/README.md +25 -10
  110. package/src/components/AtomixGlass/animation-system.ts +578 -0
  111. package/src/components/AtomixGlass/shader-utils.ts +4 -1
  112. package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
  113. package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
  114. package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
  115. package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
  116. package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
  117. package/src/components/Avatar/Avatar.tsx +1 -1
  118. package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
  119. package/src/components/Button/Button.stories.tsx +10 -0
  120. package/src/components/Button/Button.test.tsx +16 -11
  121. package/src/components/Button/Button.tsx +4 -4
  122. package/src/components/Card/Card.tsx +1 -1
  123. package/src/components/Dropdown/Dropdown.tsx +12 -12
  124. package/src/components/Form/Select.tsx +62 -3
  125. package/src/components/Modal/Modal.tsx +14 -3
  126. package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
  127. package/src/components/Slider/Slider.stories.tsx +3 -3
  128. package/src/components/Slider/Slider.tsx +38 -0
  129. package/src/components/Steps/Steps.tsx +3 -3
  130. package/src/components/Tabs/Tabs.tsx +77 -8
  131. package/src/components/Testimonial/Testimonial.tsx +1 -1
  132. package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
  133. package/src/components/TypedButton/TypedButton.tsx +39 -0
  134. package/src/components/TypedButton/index.ts +2 -0
  135. package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
  136. package/src/lib/composables/index.ts +4 -7
  137. package/src/lib/composables/types.ts +45 -0
  138. package/src/lib/composables/useAccordion.ts +0 -7
  139. package/src/lib/composables/useAtomixGlass.ts +144 -5
  140. package/src/lib/composables/useChartExport.ts +3 -13
  141. package/src/lib/composables/useDropdown.ts +66 -0
  142. package/src/lib/composables/useFocusTrap.ts +80 -0
  143. package/src/lib/composables/usePerformanceMonitor.ts +448 -0
  144. package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
  145. package/src/lib/composables/useResponsiveGlass.ts +441 -0
  146. package/src/lib/composables/useTooltip.ts +16 -0
  147. package/src/lib/composables/useTypedButton.ts +66 -0
  148. package/src/lib/config/index.ts +62 -5
  149. package/src/lib/constants/components.ts +55 -0
  150. package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
  151. package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
  152. package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
  153. package/src/lib/types/components.ts +37 -11
  154. package/src/lib/types/glass.ts +35 -0
  155. package/src/lib/types/index.ts +1 -0
  156. package/src/lib/utils/displacement-generator.ts +1 -1
  157. package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
  158. package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
  159. package/src/styles/06-components/_components.testbutton.scss +212 -0
  160. package/src/styles/06-components/_components.testtypecheck.scss +212 -0
  161. package/src/styles/06-components/_components.typedbutton.scss +212 -0
  162. package/src/styles/99-utilities/_index.scss +1 -0
  163. package/src/styles/99-utilities/_utilities.text.scss +1 -1
  164. package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
  165. package/src/styles/06-components/old.chart.styles.scss +0 -2788
@@ -39,15 +39,16 @@ const DropdownContext = createContext<DropdownContextType>({
39
39
 
40
40
  export const DropdownMenu = forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(
41
41
  ({ children, className = '', ...props }, ref) => {
42
- const { glass } = useContext(DropdownStyleContext); // We need to access glass prop here?
43
- // Wait, the original code wrapped <ul> in Context Provider.
44
- // And applied glass wrapper around <ul>.
45
- // If we use Compound Component, DropdownMenu should be the list.
42
+ const { glass } = useContext(DropdownStyleContext);
43
+ const { id } = useContext(DropdownContext);
46
44
 
47
45
  return (
48
46
  <ul
49
47
  ref={ref}
48
+ id={`${id}-menu`}
50
49
  className={`c-dropdown__menu ${glass ? 'c-dropdown__menu--glass' : ''} ${className}`.trim()}
50
+ role="menu"
51
+ aria-labelledby={`${id}-trigger`}
51
52
  {...props}
52
53
  >
53
54
  {children}
@@ -59,21 +60,20 @@ DropdownMenu.displayName = 'DropdownMenu';
59
60
 
60
61
  export const DropdownTrigger = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
61
62
  ({ children, className = '', onClick, onKeyDown, ...props }, ref) => {
62
- // We need to inject the trigger logic here.
63
- // But triggers are usually handled by the parent Dropdown in the original code.
64
- // The original code wraps children in `c-dropdown__toggle` div.
65
-
66
- // Ideally, DropdownTrigger allows user to customize the trigger element.
67
- // For backward compat, Dropdown wraps `children` (legacy) in `c-dropdown__toggle`.
68
-
69
- // If we use <Dropdown.Trigger><Button/></Dropdown.Trigger>, we want the Button to be the trigger.
63
+ const { isOpen, id } = useContext(DropdownContext);
70
64
 
71
65
  return (
72
66
  <div
73
67
  ref={ref}
68
+ id={`${id}-trigger`}
74
69
  className={`c-dropdown__toggle ${className}`.trim()}
75
70
  onClick={onClick}
76
71
  onKeyDown={onKeyDown}
72
+ aria-haspopup="true"
73
+ aria-expanded={isOpen}
74
+ aria-controls={`${id}-menu`}
75
+ tabIndex={0}
76
+ role="button"
77
77
  {...props}
78
78
  >
79
79
  {children}
@@ -143,6 +143,37 @@ export const Select: SelectComponent = memo(
143
143
  [onChange, name]
144
144
  );
145
145
 
146
+ // Keyboard navigation
147
+ const handleKeyDown = (event: React.KeyboardEvent) => {
148
+ if (disabled) return;
149
+
150
+ switch (event.key) {
151
+ case 'Enter':
152
+ case ' ':
153
+ event.preventDefault();
154
+ handleToggle();
155
+ break;
156
+ case 'Escape':
157
+ if (isOpen) {
158
+ event.preventDefault();
159
+ setIsOpen(false);
160
+ if (bodyRef.current) {
161
+ bodyRef.current.style.height = '0px';
162
+ }
163
+ }
164
+ break;
165
+ case 'ArrowDown':
166
+ case 'ArrowUp':
167
+ if (!isOpen) {
168
+ event.preventDefault();
169
+ handleToggle();
170
+ }
171
+ break;
172
+ default:
173
+ break;
174
+ }
175
+ };
176
+
146
177
  const onSelect = useCallback(
147
178
  (val: string, label: string) => {
148
179
  handleItemClick({ value: val, label });
@@ -183,7 +214,17 @@ export const Select: SelectComponent = memo(
183
214
  aria-label={ariaLabel}
184
215
  aria-describedby={ariaDescribedBy}
185
216
  aria-invalid={invalid}
186
- style={{ display: 'none' }}
217
+ style={{
218
+ position: 'absolute',
219
+ width: '1px',
220
+ height: '1px',
221
+ padding: '0',
222
+ margin: '-1px',
223
+ overflow: 'hidden',
224
+ clip: 'rect(0, 0, 0, 0)',
225
+ whiteSpace: 'nowrap',
226
+ border: '0',
227
+ }}
187
228
  >
188
229
  {placeholder && (
189
230
  <option value="" disabled>
@@ -198,7 +239,17 @@ export const Select: SelectComponent = memo(
198
239
  </select>
199
240
 
200
241
  {/* Custom Select UI */}
201
- <div className={SELECT.CLASSES.SELECTED} onClick={handleToggle} aria-disabled={disabled}>
242
+ <div
243
+ className={SELECT.CLASSES.SELECTED}
244
+ onClick={handleToggle}
245
+ onKeyDown={handleKeyDown}
246
+ aria-disabled={disabled}
247
+ tabIndex={disabled ? -1 : 0}
248
+ role="combobox"
249
+ aria-haspopup="listbox"
250
+ aria-expanded={isOpen}
251
+ aria-controls={id ? `${id}-listbox` : undefined}
252
+ >
202
253
  {selectedLabel}
203
254
  </div>
204
255
 
@@ -206,7 +257,12 @@ export const Select: SelectComponent = memo(
206
257
 
207
258
  <div className={SELECT.CLASSES.SELECT_BODY} ref={bodyRef} style={{ height: 0 }}>
208
259
  <div className={SELECT.CLASSES.SELECT_PANEL} ref={panelRef}>
209
- <ul className={SELECT.CLASSES.SELECT_ITEMS}>
260
+ <ul
261
+ className={SELECT.CLASSES.SELECT_ITEMS}
262
+ role="listbox"
263
+ id={id ? `${id}-listbox` : undefined}
264
+ aria-labelledby={id}
265
+ >
210
266
  {hasOptionsProp ? (
211
267
  options.map((option, index) => (
212
268
  <li
@@ -214,6 +270,9 @@ export const Select: SelectComponent = memo(
214
270
  className={SELECT.CLASSES.SELECT_ITEM}
215
271
  data-value={option.value}
216
272
  onClick={() => !option.disabled && handleItemClick(option)}
273
+ role="option"
274
+ aria-selected={value === option.value}
275
+ aria-disabled={option.disabled}
217
276
  >
218
277
  <label htmlFor={`SelectItem${index}`} className="c-checkbox">
219
278
  <input
@@ -1,7 +1,8 @@
1
- import React, { useEffect, useRef, useState, useCallback, memo, forwardRef, ReactNode } from 'react';
1
+ import React, { useEffect, useRef, useState, useCallback, memo, forwardRef, ReactNode, useId } from 'react';
2
2
  import { ModalProps } from '../../lib/types/components';
3
3
  import { MODAL } from '../../lib/constants/components';
4
4
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
5
+ import { useFocusTrap } from '../../lib/composables/useFocusTrap';
5
6
 
6
7
  /**
7
8
  * Hook for managing modal state
@@ -183,6 +184,12 @@ const ModalImpl = memo(
183
184
  onOpen,
184
185
  });
185
186
 
187
+ // Use focus trap hook
188
+ const contentRef = useFocusTrap(isOpenState);
189
+ const instanceId = useId();
190
+ const titleId = `modal-title-${instanceId}`;
191
+ const descId = `modal-desc-${instanceId}`;
192
+
186
193
  // Handle keyboard events for Escape key
187
194
  useEffect(() => {
188
195
  if (!keyboard) return undefined;
@@ -224,7 +231,7 @@ const ModalImpl = memo(
224
231
  );
225
232
 
226
233
  const modalContent = (
227
- <div className="c-modal__content">
234
+ <div className="c-modal__content" ref={contentRef}>
228
235
  {hasCompoundComponents ? (
229
236
  React.Children.map(children, child => {
230
237
  if (
@@ -233,6 +240,7 @@ const ModalImpl = memo(
233
240
  ) {
234
241
  return React.cloneElement(child, {
235
242
  onClose: (child.props as any).onClose || close,
243
+ id: titleId,
236
244
  } as any);
237
245
  }
238
246
  return child;
@@ -241,13 +249,14 @@ const ModalImpl = memo(
241
249
  <>
242
250
  {(title || closeButton) && (
243
251
  <ModalHeader
252
+ id={titleId}
244
253
  title={title}
245
254
  subtitle={subtitle}
246
255
  closeButton={closeButton}
247
256
  onClose={close}
248
257
  />
249
258
  )}
250
- <ModalBody>{children}</ModalBody>
259
+ <ModalBody id={descId}>{children}</ModalBody>
251
260
  {footer && <ModalFooter>{footer}</ModalFooter>}
252
261
  </>
253
262
  )}
@@ -262,6 +271,8 @@ const ModalImpl = memo(
262
271
  role="dialog"
263
272
  aria-modal="true"
264
273
  aria-hidden={!isOpenState}
274
+ aria-labelledby={titleId}
275
+ aria-describedby={descId}
265
276
  {...props}
266
277
  >
267
278
  <div ref={backdropRef} className="c-modal__backdrop" onClick={handleBackdropClick} />
@@ -82,6 +82,50 @@ export const Navbar = forwardRef<HTMLElement, NavbarProps>(
82
82
  };
83
83
  }, [collapsible, expanded, onToggle]);
84
84
 
85
+ // Handle Escape key to close mobile menu
86
+ useEffect(() => {
87
+ if (!navbarExpanded || !closeOnEscape) return undefined;
88
+
89
+ const handleEscapeKey = (event: KeyboardEvent) => {
90
+ if (event.key === 'Escape') {
91
+ if (typeof onToggle === 'function') {
92
+ onToggle(false);
93
+ } else {
94
+ setNavbarExpanded(false);
95
+ }
96
+ }
97
+ };
98
+
99
+ document.addEventListener('keydown', handleEscapeKey);
100
+ return () => {
101
+ document.removeEventListener('keydown', handleEscapeKey);
102
+ };
103
+ }, [navbarExpanded, closeOnEscape, onToggle]);
104
+
105
+ // Handle outside click to close mobile menu
106
+ useEffect(() => {
107
+ if (!navbarExpanded || !closeOnOutsideClick) return undefined;
108
+
109
+ const handleClickOutside = (event: MouseEvent) => {
110
+ if (
111
+ collapseRef.current &&
112
+ !collapseRef.current.contains(event.target as Node) &&
113
+ !(event.target as HTMLElement).closest('.c-navbar__toggler')
114
+ ) {
115
+ if (typeof onToggle === 'function') {
116
+ onToggle(false);
117
+ } else {
118
+ setNavbarExpanded(false);
119
+ }
120
+ }
121
+ };
122
+
123
+ document.addEventListener('mousedown', handleClickOutside);
124
+ return () => {
125
+ document.removeEventListener('mousedown', handleClickOutside);
126
+ };
127
+ }, [navbarExpanded, closeOnOutsideClick, onToggle]);
128
+
85
129
  // Generate the navbar class
86
130
  const navbarClass = generateNavbarClass({
87
131
  position,
@@ -461,7 +461,7 @@ const testimonialSlides: SliderSlide[] = [
461
461
  }}
462
462
  />
463
463
  </div>
464
- <p className="u-text-base u-fst-italic u-mb-4">
464
+ <p className="u-text-base u-textt-italic u-mb-4">
465
465
  "Atomix has completely transformed how we build our user interfaces. The components are
466
466
  intuitive and the design system is consistent across all our products."
467
467
  </p>
@@ -486,7 +486,7 @@ const testimonialSlides: SliderSlide[] = [
486
486
  }}
487
487
  />
488
488
  </div>
489
- <p className="u-text-base u-fst-italic u-mb-4">
489
+ <p className="u-text-base u-textt-italic u-mb-4">
490
490
  "Implementing Atomix reduced our development time by 40%. The documentation is excellent
491
491
  and the components are highly customizable."
492
492
  </p>
@@ -511,7 +511,7 @@ const testimonialSlides: SliderSlide[] = [
511
511
  }}
512
512
  />
513
513
  </div>
514
- <p className="u-text-base u-fst-italic u-mb-4">
514
+ <p className="u-text-base u-textt-italic u-mb-4">
515
515
  "The accessibility features in Atomix are impressive. Our products now meet WCAG 2.1 AA
516
516
  standards with minimal effort."
517
517
  </p>
@@ -87,6 +87,38 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
87
87
  .filter(Boolean)
88
88
  .join(' ');
89
89
 
90
+ // Keyboard navigation
91
+ const handleKeyDown = (event: React.KeyboardEvent) => {
92
+ switch (event.key) {
93
+ case 'ArrowLeft':
94
+ if (direction === 'horizontal') {
95
+ event.preventDefault();
96
+ slidePrev();
97
+ }
98
+ break;
99
+ case 'ArrowRight':
100
+ if (direction === 'horizontal') {
101
+ event.preventDefault();
102
+ slideNext();
103
+ }
104
+ break;
105
+ case 'ArrowUp':
106
+ if (direction === 'vertical') {
107
+ event.preventDefault();
108
+ slidePrev();
109
+ }
110
+ break;
111
+ case 'ArrowDown':
112
+ if (direction === 'vertical') {
113
+ event.preventDefault();
114
+ slideNext();
115
+ }
116
+ break;
117
+ default:
118
+ break;
119
+ }
120
+ };
121
+
90
122
  return (
91
123
  <div
92
124
  ref={ref || (containerRef as React.RefObject<HTMLDivElement>)}
@@ -106,10 +138,16 @@ export const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
106
138
  onMouseMove={handleTouchMove}
107
139
  onMouseUp={handleTouchEnd}
108
140
  onMouseLeave={handleTouchEnd}
141
+ onKeyDown={handleKeyDown}
142
+ role="region"
143
+ aria-roledescription="carousel"
144
+ aria-label={(rest as any)['aria-label'] || 'Image slider'}
145
+ tabIndex={0}
109
146
  >
110
147
  <div
111
148
  ref={wrapperRef as React.RefObject<HTMLDivElement>}
112
149
  className="c-slider__wrapper"
150
+ aria-live={autoplay ? 'off' : 'polite'}
113
151
  style={{
114
152
  display: 'flex',
115
153
  flexDirection: direction === 'vertical' ? 'column' : 'row',
@@ -193,8 +193,8 @@ const StepsComp: React.FC<StepsProps> = ({
193
193
  } else {
194
194
  // Compound rendering
195
195
  content = Children.map(children, (child, index) => {
196
- if (isValidElement(child)) {
197
- const childProps = child.props as any;
196
+ if (isValidElement<StepsItemProps>(child)) {
197
+ const childProps = child.props;
198
198
  // Inject active/completed based on index if not explicitly provided
199
199
  const isActive = childProps.active ?? index <= currentStep;
200
200
  const isCompleted = childProps.completed ?? index < currentStep;
@@ -207,7 +207,7 @@ const StepsComp: React.FC<StepsProps> = ({
207
207
  active: isActive,
208
208
  completed: isCompleted,
209
209
  number,
210
- } as any);
210
+ });
211
211
  }
212
212
  return child;
213
213
  });
@@ -67,20 +67,36 @@ export interface TabsProps {
67
67
  const TabsContext = createContext<{
68
68
  currentTab: number;
69
69
  handleTabClick: (index: number) => void;
70
+ handleKeyDown: (event: React.KeyboardEvent, totalTabs: number) => void;
71
+ totalTabs: number;
70
72
  }>({
71
73
  currentTab: 0,
72
74
  handleTabClick: () => {},
75
+ handleKeyDown: () => {},
76
+ totalTabs: 0,
73
77
  });
74
78
 
75
79
  // Compound components
76
80
  export const TabsList = forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(
77
- ({ children, className = '', ...props }, ref) => {
81
+ ({ children, className = '', onKeyDown, ...props }, ref) => {
82
+ const { handleKeyDown: contextHandleKeyDown } = useContext(TabsContext);
83
+ const totalTabs = React.Children.count(children);
84
+
78
85
  return (
79
- <ul ref={ref} className={`c-tabs__nav ${className}`.trim()} {...props}>
86
+ <ul
87
+ ref={ref}
88
+ className={`c-tabs__nav ${className}`.trim()}
89
+ role="tablist"
90
+ onKeyDown={(e) => {
91
+ contextHandleKeyDown(e, totalTabs);
92
+ onKeyDown?.(e);
93
+ }}
94
+ {...props}
95
+ >
80
96
  {React.Children.map(children, (child, index) => {
81
97
  if (React.isValidElement(child)) {
82
- // Inject index into TabsTrigger
83
- return React.cloneElement(child, { index } as any);
98
+ // Inject index into TabsTrigger
99
+ return React.cloneElement(child, { index } as any);
84
100
  }
85
101
  return child;
86
102
  })}
@@ -106,9 +122,10 @@ export const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
106
122
  const isActive = index !== undefined && currentTab === index;
107
123
 
108
124
  return (
109
- <li className="c-tabs__nav-item">
125
+ <li className="c-tabs__nav-item" role="presentation">
110
126
  <button
111
127
  ref={ref}
128
+ id={`tab-nav-${index}`}
112
129
  className={`c-tabs__nav-btn ${isActive ? TAB.CLASSES.ACTIVE : ''} ${className}`.trim()}
113
130
  onClick={(e) => {
114
131
  if (index !== undefined) handleTabClick(index);
@@ -118,6 +135,7 @@ export const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
118
135
  role="tab"
119
136
  aria-selected={isActive}
120
137
  aria-controls={`tab-panel-${index}`}
138
+ tabIndex={isActive ? 0 : -1}
121
139
  type="button"
122
140
  {...props}
123
141
  >
@@ -209,6 +227,37 @@ export const Tabs: TabsComponent = memo(
209
227
  }
210
228
  };
211
229
 
230
+ // Keyboard navigation
231
+ const handleKeyDown = (event: React.KeyboardEvent, totalTabs: number) => {
232
+ let newIndex = currentTab;
233
+ switch (event.key) {
234
+ case 'ArrowRight':
235
+ newIndex = (currentTab + 1) % totalTabs;
236
+ break;
237
+ case 'ArrowLeft':
238
+ newIndex = (currentTab - 1 + totalTabs) % totalTabs;
239
+ break;
240
+ case 'Home':
241
+ newIndex = 0;
242
+ break;
243
+ case 'End':
244
+ newIndex = totalTabs - 1;
245
+ break;
246
+ default:
247
+ return;
248
+ }
249
+ event.preventDefault();
250
+ handleTabClick(newIndex);
251
+
252
+ // Focus the newly active tab after it renders
253
+ setTimeout(() => {
254
+ const tabElement = document.getElementById(`tab-nav-${newIndex}`);
255
+ if (tabElement) {
256
+ tabElement.focus();
257
+ }
258
+ }, 0);
259
+ };
260
+
212
261
  // Determine content based on mode (legacy items vs compound children)
213
262
  let content: ReactNode;
214
263
 
@@ -217,16 +266,23 @@ export const Tabs: TabsComponent = memo(
217
266
  // Legacy mode
218
267
  content = (
219
268
  <>
220
- <ul className="c-tabs__nav">
269
+ <ul
270
+ className="c-tabs__nav"
271
+ role="tablist"
272
+ onKeyDown={(e) => handleKeyDown(e, items.length)}
273
+ >
221
274
  {items.map((item, index) => (
222
- <li className="c-tabs__nav-item" key={`tab-nav-${index}`}>
275
+ <li className="c-tabs__nav-item" key={`tab-nav-${index}`} role="presentation">
223
276
  <button
277
+ id={`tab-nav-${index}`}
224
278
  className={`c-tabs__nav-btn ${index === currentTab ? TAB.CLASSES.ACTIVE : ''}`}
225
279
  onClick={() => handleTabClick(index)}
226
280
  data-tabindex={index}
227
281
  role="tab"
228
282
  aria-selected={index === currentTab}
229
283
  aria-controls={`tab-panel-${index}`}
284
+ tabIndex={index === currentTab ? 0 : -1}
285
+ type="button"
230
286
  >
231
287
  {item.label}
232
288
  </button>
@@ -257,8 +313,21 @@ export const Tabs: TabsComponent = memo(
257
313
  );
258
314
  } else {
259
315
  // Compound mode
316
+ const tabsList = React.Children.toArray(children).find(
317
+ (child): child is React.ReactElement =>
318
+ React.isValidElement(child) && (child.type as any).displayName === 'TabsList'
319
+ );
320
+ const totalTabsCount = tabsList ? React.Children.count(tabsList.props.children) : 0;
321
+
260
322
  content = (
261
- <TabsContext.Provider value={{ currentTab, handleTabClick }}>
323
+ <TabsContext.Provider
324
+ value={{
325
+ currentTab,
326
+ handleTabClick,
327
+ handleKeyDown,
328
+ totalTabs: totalTabsCount,
329
+ }}
330
+ >
262
331
  {children}
263
332
  </TabsContext.Provider>
264
333
  );
@@ -125,7 +125,7 @@ export const Testimonial: React.FC<TestimonialProps> = ({
125
125
  {author.avatarSrc && (
126
126
  <img
127
127
  src={author.avatarSrc}
128
- alt={author.avatarAlt || ''}
128
+ alt={author.avatarAlt || `${author.name}'s avatar`}
129
129
  className="c-testimonial__author-avatar c-avatar c-avatar--xxl c-avatar--circle"
130
130
  />
131
131
  )}
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { TypedButton } from './TypedButton';
3
+
4
+ const meta: Meta<typeof TypedButton> = {
5
+ title: 'Components/TypedButton',
6
+ component: TypedButton,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ size: {
13
+ control: 'select',
14
+ options: ['sm', 'md', 'lg'],
15
+ },
16
+ variant: {
17
+ control: 'select',
18
+ options: ['primary', 'secondary', 'success', 'error'],
19
+ },
20
+ disabled: {
21
+ control: 'boolean',
22
+ },
23
+ glass: {
24
+ control: 'boolean',
25
+ },
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof meta>;
31
+
32
+ export const Default: Story = {
33
+ args: {
34
+ children: 'TypedButton Component',
35
+ size: 'md',
36
+ variant: 'primary',
37
+ },
38
+ };
39
+
40
+ export const Small: Story = {
41
+ args: {
42
+ ...Default.args,
43
+ size: 'sm',
44
+ },
45
+ };
46
+
47
+ export const Large: Story = {
48
+ args: {
49
+ ...Default.args,
50
+ size: 'lg',
51
+ },
52
+ };
53
+
54
+ export const Glass: Story = {
55
+ args: {
56
+ ...Default.args,
57
+ glass: true,
58
+ },
59
+ };
@@ -0,0 +1,39 @@
1
+ import React, { forwardRef, useId, memo } from 'react';
2
+ import { TYPEDBUTTON } from '../../lib/constants/components';
3
+ import { useTypedButton } from '../../lib/composables/useTypedButton';
4
+ import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
5
+ import type { TypedButtonProps } from '../../lib/types/components';
6
+
7
+ /**
8
+ * TypedButton - Medium Presentational Component
9
+ *
10
+ * @param {TypedButtonProps} props - Component properties
11
+ * @returns {JSX.Element} The rendered component
12
+ */
13
+ export const TypedButton = memo(
14
+ forwardRef<HTMLDivElement, TypedButtonProps>(
15
+ ({ children, className = '', disabled = false, glass, style, 'aria-label': ariaLabel, ...props }, ref) => {
16
+ const instanceId = useId();
17
+ const { generateClassNames } = useTypedButton({ disabled });
18
+
19
+ const content = (
20
+ <div
21
+ ref={ref}
22
+ className={generateClassNames(className)}
23
+ style={style}
24
+ aria-label={ariaLabel}
25
+ aria-disabled={disabled}
26
+ {...props}
27
+ >
28
+ {children}
29
+ </div>
30
+ );
31
+
32
+ return glass ? <AtomixGlass {...(glass === true ? {} : glass)}>{content}</AtomixGlass> : content;
33
+ }
34
+ )
35
+ );
36
+
37
+ TypedButton.displayName = 'TypedButton';
38
+
39
+ export default TypedButton;
@@ -0,0 +1,2 @@
1
+ export { default as TypedButton } from './TypedButton';
2
+ export type { TypedButtonProps } from './TypedButton';
@@ -146,10 +146,17 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
146
146
 
147
147
  const handleDownload = useCallback(() => {
148
148
  if (src) {
149
- const a = document.createElement('a');
150
- a.href = src;
151
- a.download = 'video';
152
- a.click();
149
+ try {
150
+ const url = new URL(src, window.location.origin);
151
+ if (['http:', 'https:', 'blob:', 'data:'].includes(url.protocol)) {
152
+ const a = document.createElement('a');
153
+ a.href = url.href;
154
+ a.download = 'video';
155
+ a.click();
156
+ }
157
+ } catch (e) {
158
+ // Ignore invalid URLs
159
+ }
153
160
  }
154
161
  }, [src]);
155
162
 
@@ -36,13 +36,10 @@ export * from './useRadio';
36
36
  export * from './useSelect';
37
37
  export * from './useTextarea';
38
38
 
39
- // New composables
40
- export * from './useBreadcrumb';
41
- export * from './useCard';
42
- export * from './useDataTable';
43
- export * from './useModal';
44
- export * from './usePagination';
45
- export * from './useSlider';
39
+ // Phase 3: Optimization & Adaptation - New composables
40
+ export * from './useResponsiveGlass';
41
+ export * from './usePerformanceMonitor';
42
+ export * from './useResponsiveGlass.presets';
46
43
 
47
44
  // Chart composables - simplified
48
45
  export * from './useChartData';