@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.
- package/atomix.config.ts +58 -1
- package/dist/atomix.css +148 -120
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +1 -1
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1227 -122
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1052 -41
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2086 -1035
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1620 -600
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +441 -270
- package/dist/index.esm.js +1900 -638
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1935 -670
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +6 -3
- package/scripts/atomix-cli.js +148 -4
- package/scripts/cli/__tests__/basic.test.js +3 -2
- package/scripts/cli/__tests__/clean.test.js +278 -0
- package/scripts/cli/__tests__/component-validator.test.js +433 -0
- package/scripts/cli/__tests__/generator.test.js +613 -0
- package/scripts/cli/__tests__/glass-motion.test.js +256 -0
- package/scripts/cli/__tests__/integration.test.js +719 -108
- package/scripts/cli/__tests__/migrate.test.js +74 -0
- package/scripts/cli/__tests__/security.test.js +206 -0
- package/scripts/cli/__tests__/test-setup.js +3 -1
- package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
- package/scripts/cli/__tests__/token-provider.test.js +361 -0
- package/scripts/cli/__tests__/utils.test.js +5 -5
- package/scripts/cli/commands/benchmark.js +105 -0
- package/scripts/cli/commands/build-theme.js +4 -1
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +135 -14
- package/scripts/cli/commands/init.js +45 -18
- package/scripts/cli/commands/migrate.js +106 -0
- package/scripts/cli/commands/sync-tokens.js +206 -0
- package/scripts/cli/commands/theme-bridge.js +248 -0
- package/scripts/cli/commands/tokens.js +157 -0
- package/scripts/cli/commands/validate.js +194 -0
- package/scripts/cli/internal/ai-engine.js +156 -0
- package/scripts/cli/internal/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +102 -2
- package/scripts/cli/internal/generator.js +359 -39
- package/scripts/cli/internal/glass-generator.js +398 -0
- package/scripts/cli/internal/hook-generator.js +369 -0
- package/scripts/cli/internal/hooks.js +61 -0
- package/scripts/cli/internal/itcss-generator.js +565 -0
- package/scripts/cli/internal/motion-generator.js +679 -0
- package/scripts/cli/internal/template-engine.js +301 -0
- package/scripts/cli/internal/theme-bridge.js +664 -0
- package/scripts/cli/internal/tokens/engine.js +122 -0
- package/scripts/cli/internal/tokens/provider.js +34 -0
- package/scripts/cli/internal/tokens/providers/figma.js +50 -0
- package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
- package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
- package/scripts/cli/internal/tokens/token-provider.js +443 -0
- package/scripts/cli/internal/tokens/token-validator.js +513 -0
- package/scripts/cli/internal/validator.js +276 -0
- package/scripts/cli/internal/wizard.js +60 -6
- package/scripts/cli/mappings.js +23 -0
- package/scripts/cli/migration-tools.js +164 -94
- package/scripts/cli/plugins/style-dictionary.js +46 -0
- package/scripts/cli/templates/README.md +525 -95
- package/scripts/cli/templates/common-templates.js +40 -14
- package/scripts/cli/templates/components/react-component.ts +282 -0
- package/scripts/cli/templates/config/project-config.ts +112 -0
- package/scripts/cli/templates/hooks/use-component.ts +477 -0
- package/scripts/cli/templates/index.js +19 -4
- package/scripts/cli/templates/index.ts +171 -0
- package/scripts/cli/templates/next-templates.js +72 -0
- package/scripts/cli/templates/react-templates.js +70 -126
- package/scripts/cli/templates/scss-templates.js +35 -35
- package/scripts/cli/templates/stories/storybook-story.ts +241 -0
- package/scripts/cli/templates/styles/scss-component.ts +255 -0
- package/scripts/cli/templates/tests/vitest-test.ts +229 -0
- package/scripts/cli/templates/token-templates.js +337 -1
- package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
- package/scripts/cli/templates/types/component-types.ts +145 -0
- package/scripts/cli/templates/utils/testing-utils.ts +144 -0
- package/scripts/cli/templates/vanilla-templates.js +39 -0
- package/scripts/cli/token-manager.js +8 -2
- package/scripts/cli/utils/cache-manager.js +240 -0
- package/scripts/cli/utils/detector.js +46 -0
- package/scripts/cli/utils/diagnostics.js +289 -0
- package/scripts/cli/utils/error.js +45 -3
- package/scripts/cli/utils/helpers.js +24 -0
- package/scripts/cli/utils/logger.js +1 -1
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +4 -38
- package/scripts/cli/utils.js +46 -0
- package/src/components/Accordion/Accordion.stories.tsx +0 -18
- package/src/components/Accordion/Accordion.test.tsx +0 -17
- package/src/components/Accordion/Accordion.tsx +0 -4
- package/src/components/AtomixGlass/AtomixGlass.tsx +102 -2
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +4 -1
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
- package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
- package/src/components/Avatar/Avatar.tsx +1 -1
- package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
- package/src/components/Button/Button.stories.tsx +10 -0
- package/src/components/Button/Button.test.tsx +16 -11
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +12 -12
- package/src/components/Form/Select.tsx +62 -3
- package/src/components/Modal/Modal.tsx +14 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
- package/src/components/Slider/Slider.stories.tsx +3 -3
- package/src/components/Slider/Slider.tsx +38 -0
- package/src/components/Steps/Steps.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +77 -8
- package/src/components/Testimonial/Testimonial.tsx +1 -1
- package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
- package/src/components/TypedButton/TypedButton.tsx +39 -0
- package/src/components/TypedButton/index.ts +2 -0
- package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
- package/src/lib/composables/index.ts +4 -7
- package/src/lib/composables/types.ts +45 -0
- package/src/lib/composables/useAccordion.ts +0 -7
- package/src/lib/composables/useAtomixGlass.ts +144 -5
- package/src/lib/composables/useChartExport.ts +3 -13
- package/src/lib/composables/useDropdown.ts +66 -0
- package/src/lib/composables/useFocusTrap.ts +80 -0
- package/src/lib/composables/usePerformanceMonitor.ts +448 -0
- package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
- package/src/lib/composables/useResponsiveGlass.ts +441 -0
- package/src/lib/composables/useTooltip.ts +16 -0
- package/src/lib/composables/useTypedButton.ts +66 -0
- package/src/lib/config/index.ts +62 -5
- package/src/lib/constants/components.ts +55 -0
- package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
- package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
- package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
- package/src/lib/types/components.ts +37 -11
- package/src/lib/types/glass.ts +35 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/utils/displacement-generator.ts +1 -1
- package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
- package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
- package/src/styles/06-components/_components.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- package/src/styles/06-components/_components.typedbutton.scss +212 -0
- package/src/styles/99-utilities/_index.scss +1 -0
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
- package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
- 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);
|
|
43
|
-
|
|
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
|
-
|
|
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={{
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
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;
|
|
@@ -146,10 +146,17 @@ export const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
|
|
|
146
146
|
|
|
147
147
|
const handleDownload = useCallback(() => {
|
|
148
148
|
if (src) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 './
|
|
41
|
-
export * from './
|
|
42
|
-
export * from './
|
|
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';
|