@a11ypros/a11y-ui-components 1.0.1 → 1.0.2
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/README.md +182 -157
- package/dist/components/Button/Button.d.ts +37 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +52 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/DataTable/DataTable.d.ts +71 -0
- package/dist/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable/DataTable.js +122 -0
- package/dist/components/DataTable/index.d.ts +3 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +1 -0
- package/dist/components/Form/Checkbox.d.ts +36 -0
- package/dist/components/Form/Checkbox.d.ts.map +1 -0
- package/dist/components/Form/Checkbox.js +39 -0
- package/dist/components/Form/Fieldset.d.ts +33 -0
- package/dist/components/Form/Fieldset.d.ts.map +1 -0
- package/dist/components/Form/Fieldset.js +34 -0
- package/dist/components/Form/Input.d.ts +37 -0
- package/dist/components/Form/Input.d.ts.map +1 -0
- package/dist/components/Form/Input.js +41 -0
- package/dist/components/Form/Label.d.ts +30 -0
- package/dist/components/Form/Label.d.ts.map +1 -0
- package/dist/components/Form/Label.js +30 -0
- package/dist/components/Form/Radio.d.ts +53 -0
- package/dist/components/Form/Radio.d.ts.map +1 -0
- package/dist/components/Form/Radio.js +39 -0
- package/dist/components/Form/Select.d.ts +51 -0
- package/dist/components/Form/Select.d.ts.map +1 -0
- package/dist/components/Form/Select.js +49 -0
- package/dist/components/Form/Textarea.d.ts +44 -0
- package/dist/components/Form/Textarea.d.ts.map +1 -0
- package/dist/components/Form/Textarea.js +43 -0
- package/dist/components/Form/index.d.ts +8 -0
- package/dist/components/Form/index.d.ts.map +1 -0
- package/dist/components/Form/index.js +7 -0
- package/dist/components/Link/Link.d.ts +34 -0
- package/dist/components/Link/Link.d.ts.map +1 -0
- package/dist/components/Link/Link.js +48 -0
- package/dist/components/Link/index.d.ts +3 -0
- package/dist/components/Link/index.d.ts.map +1 -0
- package/dist/components/Link/index.js +1 -0
- package/dist/components/Modal/Modal.d.ts +64 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/Modal.js +108 -0
- package/dist/components/Modal/index.d.ts +3 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Modal/index.js +1 -0
- package/dist/components/Tabs/Tabs.d.ts +63 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +134 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.js +1 -0
- package/dist/components/Toast/Toast.d.ts +59 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/Toast.js +91 -0
- package/dist/components/Toast/ToastProvider.d.ts +22 -0
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.js +33 -0
- package/dist/components/Toast/index.d.ts +5 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toast/index.js +2 -0
- package/dist/hooks/useAriaLive.d.ts +9 -0
- package/dist/hooks/useAriaLive.d.ts.map +1 -0
- package/dist/hooks/useAriaLive.js +39 -0
- package/dist/hooks/useFocusReturn.d.ts +9 -0
- package/dist/hooks/useFocusReturn.d.ts.map +1 -0
- package/dist/hooks/useFocusReturn.js +33 -0
- package/dist/hooks/useFocusTrap.d.ts +9 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/hooks/useFocusTrap.js +68 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/{packages/design-system/src/index.ts → dist/index.js} +0 -4
- package/dist/styles/index.d.ts +3 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/index.js +1 -0
- package/dist/tokens/breakpoints.d.ts +25 -0
- package/dist/tokens/breakpoints.d.ts.map +1 -0
- package/dist/tokens/breakpoints.js +23 -0
- package/dist/tokens/colors.d.ts +81 -0
- package/dist/tokens/colors.d.ts.map +1 -0
- package/dist/tokens/colors.js +86 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +5 -0
- package/dist/tokens/motion.d.ts +30 -0
- package/dist/tokens/motion.d.ts.map +1 -0
- package/dist/tokens/motion.js +34 -0
- package/dist/tokens/spacing.d.ts +22 -0
- package/dist/tokens/spacing.d.ts.map +1 -0
- package/dist/tokens/spacing.js +20 -0
- package/dist/tokens/theme.d.ts +159 -0
- package/dist/tokens/theme.d.ts.map +1 -0
- package/dist/tokens/theme.js +15 -0
- package/dist/tokens/typography.d.ts +45 -0
- package/dist/tokens/typography.d.ts.map +1 -0
- package/dist/tokens/typography.js +56 -0
- package/dist/utils/aria.d.ts +60 -0
- package/dist/utils/aria.d.ts.map +1 -0
- package/dist/utils/aria.js +86 -0
- package/dist/utils/focus.d.ts +30 -0
- package/dist/utils/focus.d.ts.map +1 -0
- package/dist/utils/focus.js +80 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/keyboard.d.ts +38 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +59 -0
- package/package.json +58 -31
- package/.storybook/custom.css +0 -69
- package/.storybook/main.ts +0 -46
- package/.storybook/manager.ts +0 -26
- package/.storybook/package.json +0 -6
- package/.storybook/preview.tsx +0 -31
- package/.storybook/public/logo.png +0 -0
- package/.storybook/vite.config.ts +0 -24
- package/.storybook/welcome.mdx +0 -97
- package/DEPLOYMENT.md +0 -154
- package/apps/web/app/(docs)/audit/audit.css +0 -269
- package/apps/web/app/(docs)/audit/page.tsx +0 -271
- package/apps/web/app/(docs)/components/button/page.tsx +0 -49
- package/apps/web/app/(docs)/components/form/page.tsx +0 -92
- package/apps/web/app/(docs)/components/link/page.tsx +0 -31
- package/apps/web/app/(docs)/components/modal/page.tsx +0 -41
- package/apps/web/app/(docs)/components/page.tsx +0 -37
- package/apps/web/app/(docs)/components/table/page.tsx +0 -54
- package/apps/web/app/(docs)/components/tabs/page.tsx +0 -61
- package/apps/web/app/(docs)/components/toast/page.tsx +0 -51
- package/apps/web/app/api/audit/route.ts +0 -128
- package/apps/web/app/favicon.ico +0 -0
- package/apps/web/app/layout.tsx +0 -20
- package/apps/web/app/page.tsx +0 -17
- package/apps/web/app/styles/globals.css +0 -5
- package/apps/web/next-env.d.ts +0 -5
- package/apps/web/next.config.js +0 -21
- package/apps/web/package.json +0 -28
- package/apps/web/public/_headers +0 -17
- package/apps/web/public/_redirects +0 -31
- package/apps/web/public/logo.png +0 -0
- package/apps/web/tsconfig.json +0 -29
- package/netlify/functions/audit.ts +0 -163
- package/netlify.toml +0 -37
- package/packages/design-system/README.md +0 -252
- package/packages/design-system/package.json +0 -68
- package/packages/design-system/scripts/copy-css.js +0 -63
- package/packages/design-system/src/components/Button/Button.stories.tsx +0 -228
- package/packages/design-system/src/components/Button/Button.tsx +0 -137
- package/packages/design-system/src/components/Button/index.ts +0 -3
- package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +0 -211
- package/packages/design-system/src/components/DataTable/DataTable.tsx +0 -293
- package/packages/design-system/src/components/DataTable/index.ts +0 -3
- package/packages/design-system/src/components/Form/Checkbox.stories.tsx +0 -252
- package/packages/design-system/src/components/Form/Checkbox.tsx +0 -114
- package/packages/design-system/src/components/Form/Fieldset.stories.tsx +0 -210
- package/packages/design-system/src/components/Form/Fieldset.tsx +0 -71
- package/packages/design-system/src/components/Form/Input.stories.tsx +0 -164
- package/packages/design-system/src/components/Form/Input.tsx +0 -113
- package/packages/design-system/src/components/Form/Label.tsx +0 -56
- package/packages/design-system/src/components/Form/Radio.stories.tsx +0 -265
- package/packages/design-system/src/components/Form/Radio.tsx +0 -147
- package/packages/design-system/src/components/Form/Select.stories.tsx +0 -295
- package/packages/design-system/src/components/Form/Select.tsx +0 -160
- package/packages/design-system/src/components/Form/Textarea.stories.tsx +0 -253
- package/packages/design-system/src/components/Form/Textarea.tsx +0 -145
- package/packages/design-system/src/components/Form/index.ts +0 -8
- package/packages/design-system/src/components/Link/Link.stories.tsx +0 -128
- package/packages/design-system/src/components/Link/Link.tsx +0 -117
- package/packages/design-system/src/components/Link/index.ts +0 -3
- package/packages/design-system/src/components/Modal/Modal.stories.tsx +0 -165
- package/packages/design-system/src/components/Modal/Modal.tsx +0 -202
- package/packages/design-system/src/components/Modal/index.ts +0 -3
- package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +0 -213
- package/packages/design-system/src/components/Tabs/Tabs.tsx +0 -248
- package/packages/design-system/src/components/Tabs/index.ts +0 -3
- package/packages/design-system/src/components/Toast/Toast.stories.tsx +0 -153
- package/packages/design-system/src/components/Toast/Toast.tsx +0 -175
- package/packages/design-system/src/components/Toast/ToastProvider.tsx +0 -73
- package/packages/design-system/src/components/Toast/index.ts +0 -5
- package/packages/design-system/src/hooks/useAriaLive.ts +0 -51
- package/packages/design-system/src/hooks/useFocusReturn.ts +0 -40
- package/packages/design-system/src/hooks/useFocusTrap.ts +0 -82
- package/packages/design-system/src/styles/index.ts +0 -3
- package/packages/design-system/src/tokens/breakpoints.ts +0 -28
- package/packages/design-system/src/tokens/colors.ts +0 -98
- package/packages/design-system/src/tokens/index.ts +0 -6
- package/packages/design-system/src/tokens/motion.ts +0 -41
- package/packages/design-system/src/tokens/spacing.ts +0 -24
- package/packages/design-system/src/tokens/theme.ts +0 -19
- package/packages/design-system/src/tokens/typography.ts +0 -64
- package/packages/design-system/src/utils/aria.ts +0 -108
- package/packages/design-system/src/utils/focus.ts +0 -87
- package/packages/design-system/src/utils/index.ts +0 -4
- package/packages/design-system/src/utils/keyboard.ts +0 -77
- package/packages/design-system/tsconfig.json +0 -17
- package/public/logo.png +0 -0
- package/scripts/fix-storybook-paths.js +0 -53
- package/tsconfig.json +0 -20
- /package/{packages/design-system/src → dist}/components/Button/Button.css +0 -0
- /package/{packages/design-system/src → dist}/components/DataTable/DataTable.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Checkbox.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Fieldset.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Input.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Label.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Radio.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Select.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Textarea.css +0 -0
- /package/{packages/design-system/src → dist}/components/Link/Link.css +0 -0
- /package/{packages/design-system/src → dist}/components/Modal/Modal.css +0 -0
- /package/{packages/design-system/src → dist}/components/Tabs/Tabs.css +0 -0
- /package/{packages/design-system/src → dist}/components/Toast/Toast.css +0 -0
- /package/{packages/design-system/src → dist}/components/Toast/ToastProvider.css +0 -0
- /package/{packages/design-system/src → dist}/styles/components.css +0 -0
- /package/{packages/design-system/src → dist}/styles/global.css +0 -0
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Hook to return focus to a previously focused element when component unmounts
|
|
7
|
-
* Useful for modals, dropdowns, and other temporary UI elements
|
|
8
|
-
*
|
|
9
|
-
* @param returnOnUnmount - Whether to return focus on unmount
|
|
10
|
-
* @param returnElement - Optional specific element to return focus to
|
|
11
|
-
*/
|
|
12
|
-
export function useFocusReturn(
|
|
13
|
-
returnOnUnmount: boolean = true,
|
|
14
|
-
returnElement?: HTMLElement | null
|
|
15
|
-
): void {
|
|
16
|
-
const savedElementRef = useRef<HTMLElement | null>(null)
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!returnOnUnmount) return
|
|
20
|
-
|
|
21
|
-
// Save the currently focused element
|
|
22
|
-
if (document.activeElement instanceof HTMLElement) {
|
|
23
|
-
savedElementRef.current = document.activeElement
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return () => {
|
|
27
|
-
// Restore focus on unmount
|
|
28
|
-
const elementToFocus = returnElement || savedElementRef.current
|
|
29
|
-
if (elementToFocus && typeof elementToFocus.focus === 'function') {
|
|
30
|
-
try {
|
|
31
|
-
elementToFocus.focus()
|
|
32
|
-
} catch (error) {
|
|
33
|
-
// Silently fail if focus fails
|
|
34
|
-
console.warn('Failed to return focus:', error)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}, [returnOnUnmount, returnElement])
|
|
39
|
-
}
|
|
40
|
-
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
-
import { getFirstFocusable, getLastFocusable } from '../utils/focus'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Hook to trap focus within a container element
|
|
8
|
-
* Implements WCAG 2.1.2 Keyboard (No Keyboard Trap)
|
|
9
|
-
*
|
|
10
|
-
* @param enabled - Whether the focus trap is active
|
|
11
|
-
* @param containerRef - Ref to the container element
|
|
12
|
-
*/
|
|
13
|
-
export function useFocusTrap(
|
|
14
|
-
enabled: boolean,
|
|
15
|
-
containerRef: React.RefObject<HTMLElement>
|
|
16
|
-
): void {
|
|
17
|
-
const previousActiveElement = useRef<HTMLElement | null>(null)
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!enabled || !containerRef.current) return
|
|
21
|
-
|
|
22
|
-
const container = containerRef.current
|
|
23
|
-
|
|
24
|
-
// Save the previously focused element
|
|
25
|
-
if (document.activeElement instanceof HTMLElement) {
|
|
26
|
-
previousActiveElement.current = document.activeElement
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Focus the first focusable element in the container
|
|
30
|
-
const firstFocusable = getFirstFocusable(container)
|
|
31
|
-
if (firstFocusable) {
|
|
32
|
-
firstFocusable.focus()
|
|
33
|
-
} else {
|
|
34
|
-
// If no focusable elements, focus the container itself
|
|
35
|
-
container.focus()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Handle Tab key to trap focus
|
|
39
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
40
|
-
if (event.key !== 'Tab') return
|
|
41
|
-
|
|
42
|
-
const focusableElements = [
|
|
43
|
-
getFirstFocusable(container),
|
|
44
|
-
getLastFocusable(container),
|
|
45
|
-
].filter(Boolean) as HTMLElement[]
|
|
46
|
-
|
|
47
|
-
if (focusableElements.length === 0) {
|
|
48
|
-
event.preventDefault()
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const firstElement = focusableElements[0]
|
|
53
|
-
const lastElement = focusableElements[focusableElements.length - 1]
|
|
54
|
-
|
|
55
|
-
if (event.shiftKey) {
|
|
56
|
-
// Shift + Tab
|
|
57
|
-
if (document.activeElement === firstElement) {
|
|
58
|
-
event.preventDefault()
|
|
59
|
-
lastElement.focus()
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
// Tab
|
|
63
|
-
if (document.activeElement === lastElement) {
|
|
64
|
-
event.preventDefault()
|
|
65
|
-
firstElement.focus()
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
container.addEventListener('keydown', handleKeyDown)
|
|
71
|
-
|
|
72
|
-
return () => {
|
|
73
|
-
container.removeEventListener('keydown', handleKeyDown)
|
|
74
|
-
|
|
75
|
-
// Restore focus to the previously focused element
|
|
76
|
-
if (previousActiveElement.current) {
|
|
77
|
-
previousActiveElement.current.focus()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}, [enabled, containerRef])
|
|
81
|
-
}
|
|
82
|
-
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Responsive breakpoints
|
|
3
|
-
* Mobile-first approach
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const breakpoints = {
|
|
7
|
-
sm: '640px', // Small devices (landscape phones)
|
|
8
|
-
md: '768px', // Medium devices (tablets)
|
|
9
|
-
lg: '1024px', // Large devices (desktops)
|
|
10
|
-
xl: '1280px', // Extra large devices
|
|
11
|
-
'2xl': '1536px', // 2X Extra large devices
|
|
12
|
-
} as const
|
|
13
|
-
|
|
14
|
-
export type BreakpointToken = typeof breakpoints
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Media query helpers
|
|
18
|
-
*/
|
|
19
|
-
export const mediaQuery = {
|
|
20
|
-
sm: `@media (min-width: ${breakpoints.sm})`,
|
|
21
|
-
md: `@media (min-width: ${breakpoints.md})`,
|
|
22
|
-
lg: `@media (min-width: ${breakpoints.lg})`,
|
|
23
|
-
xl: `@media (min-width: ${breakpoints.xl})`,
|
|
24
|
-
'2xl': `@media (min-width: ${breakpoints['2xl']})`,
|
|
25
|
-
reducedMotion: '@media (prefers-reduced-motion: reduce)',
|
|
26
|
-
highContrast: '@media (prefers-contrast: high)',
|
|
27
|
-
} as const
|
|
28
|
-
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Color tokens with WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text)
|
|
3
|
-
* All colors meet WCAG 2.1 Level AA standards
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const colors = {
|
|
7
|
-
// Primary palette
|
|
8
|
-
primary: {
|
|
9
|
-
50: '#f0f9ff',
|
|
10
|
-
100: '#e0f2fe',
|
|
11
|
-
200: '#bae6fd',
|
|
12
|
-
300: '#7dd3fc',
|
|
13
|
-
400: '#38bdf8',
|
|
14
|
-
500: '#0ea5e9', // Main primary - meets 4.5:1 on white
|
|
15
|
-
600: '#0284c7',
|
|
16
|
-
700: '#0369a1',
|
|
17
|
-
800: '#075985',
|
|
18
|
-
900: '#0c4a6e',
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
// Neutral grays
|
|
22
|
-
neutral: {
|
|
23
|
-
50: '#fafafa',
|
|
24
|
-
100: '#f5f5f5',
|
|
25
|
-
200: '#bbbbbb',
|
|
26
|
-
300: '#d4d4d4',
|
|
27
|
-
400: '#a3a3a3',
|
|
28
|
-
500: '#737373',
|
|
29
|
-
600: '#525252', // Meets 4.5:1 on white
|
|
30
|
-
700: '#404040', // Meets 4.5:1 on white
|
|
31
|
-
800: '#262626', // Meets 4.5:1 on white
|
|
32
|
-
900: '#171717', // Meets 4.5:1 on white
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
// Semantic colors
|
|
36
|
-
success: {
|
|
37
|
-
50: '#f0fdf4',
|
|
38
|
-
100: '#dcfce7',
|
|
39
|
-
200: '#bbf7d0',
|
|
40
|
-
300: '#86efac',
|
|
41
|
-
400: '#4ade80',
|
|
42
|
-
500: '#22c55e', // Meets 4.5:1 on white
|
|
43
|
-
600: '#16a34a',
|
|
44
|
-
700: '#15803d',
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
warning: {
|
|
48
|
-
50: '#fffbeb',
|
|
49
|
-
100: '#fef3c7',
|
|
50
|
-
200: '#fde68a',
|
|
51
|
-
300: '#fcd34d',
|
|
52
|
-
400: '#fbbf24',
|
|
53
|
-
500: '#f59e0b', // Meets 4.5:1 on white
|
|
54
|
-
600: '#d97706',
|
|
55
|
-
700: '#b45309',
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
error: {
|
|
59
|
-
50: '#fef2f2',
|
|
60
|
-
100: '#fee2e2',
|
|
61
|
-
200: '#fecaca',
|
|
62
|
-
300: '#fca5a5',
|
|
63
|
-
400: '#f87171',
|
|
64
|
-
500: '#ef4444', // Meets 4.5:1 on white
|
|
65
|
-
600: '#dc2626', // Meets 4.5:1 on white
|
|
66
|
-
700: '#b91c1c',
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
// Base colors
|
|
70
|
-
white: '#ffffff',
|
|
71
|
-
black: '#000000',
|
|
72
|
-
|
|
73
|
-
// Background colors
|
|
74
|
-
background: {
|
|
75
|
-
default: '#ffffff',
|
|
76
|
-
secondary: '#fafafa',
|
|
77
|
-
tertiary: '#f5f5f5',
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
// Text colors (meet contrast requirements)
|
|
81
|
-
text: {
|
|
82
|
-
primary: '#171717', // neutral.900 - 4.5:1 on white
|
|
83
|
-
secondary: '#525252', // neutral.600 - 4.5:1 on white
|
|
84
|
-
tertiary: '#737373', // neutral.500 - 4.5:1 on white (large text)
|
|
85
|
-
inverse: '#ffffff', // white - 4.5:1 on dark backgrounds
|
|
86
|
-
disabled: '#a3a3a3', // neutral.400
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
// Border colors
|
|
90
|
-
border: {
|
|
91
|
-
default: '#bbbbbb', // neutral.200
|
|
92
|
-
focus: '#0ea5e9', // primary.500
|
|
93
|
-
error: '#ef4444', // error.500
|
|
94
|
-
},
|
|
95
|
-
} as const
|
|
96
|
-
|
|
97
|
-
export type ColorToken = typeof colors
|
|
98
|
-
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Motion tokens that respect prefers-reduced-motion
|
|
3
|
-
* All animations should check for reduced motion preference
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const motion = {
|
|
7
|
-
duration: {
|
|
8
|
-
fast: '150ms',
|
|
9
|
-
normal: '200ms',
|
|
10
|
-
slow: '300ms',
|
|
11
|
-
slower: '400ms',
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
easing: {
|
|
15
|
-
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
16
|
-
easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
|
|
17
|
-
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
18
|
-
linear: 'linear',
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
// Reduced motion overrides (set to 0 or instant)
|
|
22
|
-
reduced: {
|
|
23
|
-
duration: '0ms',
|
|
24
|
-
easing: 'linear',
|
|
25
|
-
},
|
|
26
|
-
} as const
|
|
27
|
-
|
|
28
|
-
export type MotionToken = typeof motion
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Helper to get motion duration respecting prefers-reduced-motion
|
|
32
|
-
* Usage: const duration = getMotionDuration('normal')
|
|
33
|
-
* In CSS: use @media (prefers-reduced-motion: reduce) { animation-duration: 0ms; }
|
|
34
|
-
*/
|
|
35
|
-
export function getMotionDuration(key: keyof typeof motion.duration): string {
|
|
36
|
-
if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
37
|
-
return motion.reduced.duration
|
|
38
|
-
}
|
|
39
|
-
return motion.duration[key]
|
|
40
|
-
}
|
|
41
|
-
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spacing scale using 4px base unit
|
|
3
|
-
* Follows consistent 8px grid system
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const spacing = {
|
|
7
|
-
0: '0',
|
|
8
|
-
1: '0.25rem', // 4px
|
|
9
|
-
2: '0.5rem', // 8px
|
|
10
|
-
3: '0.75rem', // 12px
|
|
11
|
-
4: '1rem', // 16px
|
|
12
|
-
5: '1.25rem', // 20px
|
|
13
|
-
6: '1.5rem', // 24px
|
|
14
|
-
8: '2rem', // 32px
|
|
15
|
-
10: '2.5rem', // 40px
|
|
16
|
-
12: '3rem', // 48px
|
|
17
|
-
16: '4rem', // 64px
|
|
18
|
-
20: '5rem', // 80px
|
|
19
|
-
24: '6rem', // 96px
|
|
20
|
-
32: '8rem', // 128px
|
|
21
|
-
} as const
|
|
22
|
-
|
|
23
|
-
export type SpacingToken = typeof spacing
|
|
24
|
-
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Central theme export combining all design tokens
|
|
3
|
-
*/
|
|
4
|
-
import { colors } from './colors'
|
|
5
|
-
import { spacing } from './spacing'
|
|
6
|
-
import { typography } from './typography'
|
|
7
|
-
import { motion } from './motion'
|
|
8
|
-
import { breakpoints } from './breakpoints'
|
|
9
|
-
|
|
10
|
-
export const theme = {
|
|
11
|
-
colors,
|
|
12
|
-
spacing,
|
|
13
|
-
typography,
|
|
14
|
-
motion,
|
|
15
|
-
breakpoints,
|
|
16
|
-
} as const
|
|
17
|
-
|
|
18
|
-
export type Theme = typeof theme
|
|
19
|
-
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typography scale with accessible line heights and font sizes
|
|
3
|
-
* Line heights ensure readability (minimum 1.5 for body text)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const typography = {
|
|
7
|
-
fontFamily: {
|
|
8
|
-
sans: [
|
|
9
|
-
'-apple-system',
|
|
10
|
-
'BlinkMacSystemFont',
|
|
11
|
-
'"Segoe UI"',
|
|
12
|
-
'Roboto',
|
|
13
|
-
'"Helvetica Neue"',
|
|
14
|
-
'Arial',
|
|
15
|
-
'sans-serif',
|
|
16
|
-
].join(', '),
|
|
17
|
-
mono: [
|
|
18
|
-
'Menlo',
|
|
19
|
-
'Monaco',
|
|
20
|
-
'"Courier New"',
|
|
21
|
-
'monospace',
|
|
22
|
-
].join(', '),
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
fontSize: {
|
|
26
|
-
xs: '0.75rem', // 12px
|
|
27
|
-
sm: '0.875rem', // 14px
|
|
28
|
-
base: '1rem', // 16px
|
|
29
|
-
lg: '1.125rem', // 18px
|
|
30
|
-
xl: '1.25rem', // 20px
|
|
31
|
-
'2xl': '1.5rem', // 24px
|
|
32
|
-
'3xl': '1.875rem', // 30px
|
|
33
|
-
'4xl': '2.25rem', // 36px
|
|
34
|
-
'5xl': '3rem', // 48px
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
lineHeight: {
|
|
38
|
-
none: '1',
|
|
39
|
-
tight: '1.25',
|
|
40
|
-
snug: '1.375',
|
|
41
|
-
normal: '1.5', // Minimum for accessibility
|
|
42
|
-
relaxed: '1.625',
|
|
43
|
-
loose: '2',
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
fontWeight: {
|
|
47
|
-
normal: '400',
|
|
48
|
-
medium: '500',
|
|
49
|
-
semibold: '600',
|
|
50
|
-
bold: '700',
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
letterSpacing: {
|
|
54
|
-
tighter: '-0.05em',
|
|
55
|
-
tight: '-0.025em',
|
|
56
|
-
normal: '0',
|
|
57
|
-
wide: '0.025em',
|
|
58
|
-
wider: '0.05em',
|
|
59
|
-
widest: '0.1em',
|
|
60
|
-
},
|
|
61
|
-
} as const
|
|
62
|
-
|
|
63
|
-
export type TypographyToken = typeof typography
|
|
64
|
-
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ARIA utility functions
|
|
3
|
-
* Helpers for managing ARIA attributes and roles
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Generate a unique ID for ARIA attributes
|
|
8
|
-
*/
|
|
9
|
-
let idCounter = 0
|
|
10
|
-
export function generateId(prefix = 'id'): string {
|
|
11
|
-
return `${prefix}-${++idCounter}`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Get ARIA label from props, preferring aria-label over aria-labelledby
|
|
16
|
-
*/
|
|
17
|
-
export function getAriaLabel(
|
|
18
|
-
ariaLabel?: string,
|
|
19
|
-
ariaLabelledBy?: string
|
|
20
|
-
): { 'aria-label'?: string; 'aria-labelledby'?: string } {
|
|
21
|
-
if (ariaLabel) {
|
|
22
|
-
return { 'aria-label': ariaLabel }
|
|
23
|
-
}
|
|
24
|
-
if (ariaLabelledBy) {
|
|
25
|
-
return { 'aria-labelledby': ariaLabelledBy }
|
|
26
|
-
}
|
|
27
|
-
return {}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get ARIA describedby attributes
|
|
32
|
-
*/
|
|
33
|
-
export function getAriaDescribedBy(
|
|
34
|
-
describedBy?: string
|
|
35
|
-
): { 'aria-describedby'?: string } {
|
|
36
|
-
if (describedBy) {
|
|
37
|
-
return { 'aria-describedby': describedBy }
|
|
38
|
-
}
|
|
39
|
-
return {}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Combine multiple ARIA describedby IDs
|
|
44
|
-
*/
|
|
45
|
-
export function combineAriaDescribedBy(...ids: (string | undefined)[]): string | undefined {
|
|
46
|
-
const validIds = ids.filter((id): id is string => Boolean(id))
|
|
47
|
-
return validIds.length > 0 ? validIds.join(' ') : undefined
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get live region attributes
|
|
52
|
-
*/
|
|
53
|
-
export function getLiveRegionAttributes(
|
|
54
|
-
live: 'polite' | 'assertive' | 'off' = 'polite'
|
|
55
|
-
): { 'aria-live': 'polite' | 'assertive' | 'off'; 'aria-atomic'?: boolean } {
|
|
56
|
-
return {
|
|
57
|
-
'aria-live': live,
|
|
58
|
-
'aria-atomic': live !== 'off',
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get busy state attributes
|
|
64
|
-
*/
|
|
65
|
-
export function getBusyAttributes(busy: boolean): { 'aria-busy': boolean } {
|
|
66
|
-
return { 'aria-busy': busy }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Get expanded state attributes
|
|
71
|
-
*/
|
|
72
|
-
export function getExpandedAttributes(
|
|
73
|
-
expanded: boolean | undefined
|
|
74
|
-
): { 'aria-expanded'?: boolean } {
|
|
75
|
-
if (expanded === undefined) return {}
|
|
76
|
-
return { 'aria-expanded': expanded }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Get pressed state attributes (for toggle buttons)
|
|
81
|
-
*/
|
|
82
|
-
export function getPressedAttributes(
|
|
83
|
-
pressed: boolean | undefined
|
|
84
|
-
): { 'aria-pressed'?: boolean } {
|
|
85
|
-
if (pressed === undefined) return {}
|
|
86
|
-
return { 'aria-pressed': pressed }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get selected state attributes
|
|
91
|
-
*/
|
|
92
|
-
export function getSelectedAttributes(
|
|
93
|
-
selected: boolean | undefined
|
|
94
|
-
): { 'aria-selected'?: boolean } {
|
|
95
|
-
if (selected === undefined) return {}
|
|
96
|
-
return { 'aria-selected': selected }
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get current state attributes (for navigation)
|
|
101
|
-
*/
|
|
102
|
-
export function getCurrentAttributes(
|
|
103
|
-
current: boolean | 'page' | 'step' | 'location' | 'date' | 'time' | undefined
|
|
104
|
-
): { 'aria-current'?: boolean | 'page' | 'step' | 'location' | 'date' | 'time' } {
|
|
105
|
-
if (current === undefined) return {}
|
|
106
|
-
return { 'aria-current': current }
|
|
107
|
-
}
|
|
108
|
-
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Focus management utilities
|
|
3
|
-
* Helpers for programmatic focus control
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Get all focusable elements within a container
|
|
8
|
-
*/
|
|
9
|
-
export function getFocusableElements(
|
|
10
|
-
container: HTMLElement
|
|
11
|
-
): HTMLElement[] {
|
|
12
|
-
const focusableSelectors = [
|
|
13
|
-
'a[href]',
|
|
14
|
-
'button:not([disabled])',
|
|
15
|
-
'textarea:not([disabled])',
|
|
16
|
-
'input:not([disabled])',
|
|
17
|
-
'select:not([disabled])',
|
|
18
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
19
|
-
].join(', ')
|
|
20
|
-
|
|
21
|
-
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelectors))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get the first focusable element in a container
|
|
26
|
-
*/
|
|
27
|
-
export function getFirstFocusable(container: HTMLElement): HTMLElement | null {
|
|
28
|
-
const focusable = getFocusableElements(container)
|
|
29
|
-
return focusable[0] || null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get the last focusable element in a container
|
|
34
|
-
*/
|
|
35
|
-
export function getLastFocusable(container: HTMLElement): HTMLElement | null {
|
|
36
|
-
const focusable = getFocusableElements(container)
|
|
37
|
-
return focusable[focusable.length - 1] || null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Check if an element is focusable
|
|
42
|
-
*/
|
|
43
|
-
export function isFocusable(element: HTMLElement): boolean {
|
|
44
|
-
if (element.tabIndex < 0) return false
|
|
45
|
-
if (element.hasAttribute('disabled')) return false
|
|
46
|
-
if (element.hasAttribute('hidden')) return false
|
|
47
|
-
if (element.style.display === 'none') return false
|
|
48
|
-
if (element.style.visibility === 'hidden') return false
|
|
49
|
-
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Save the currently focused element
|
|
55
|
-
*/
|
|
56
|
-
let savedFocusElement: HTMLElement | null = null
|
|
57
|
-
|
|
58
|
-
export function saveFocus(): void {
|
|
59
|
-
if (document.activeElement instanceof HTMLElement) {
|
|
60
|
-
savedFocusElement = document.activeElement
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Restore focus to the previously saved element
|
|
66
|
-
*/
|
|
67
|
-
export function restoreFocus(): void {
|
|
68
|
-
if (savedFocusElement) {
|
|
69
|
-
savedFocusElement.focus()
|
|
70
|
-
savedFocusElement = null
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Focus an element safely (with error handling)
|
|
76
|
-
*/
|
|
77
|
-
export function safeFocus(element: HTMLElement | null): void {
|
|
78
|
-
if (element && typeof element.focus === 'function') {
|
|
79
|
-
try {
|
|
80
|
-
element.focus()
|
|
81
|
-
} catch (error) {
|
|
82
|
-
// Silently fail if focus fails
|
|
83
|
-
console.warn('Failed to focus element:', error)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keyboard event utilities
|
|
3
|
-
* Helpers for handling keyboard interactions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type KeyboardHandler = (event: React.KeyboardEvent) => void
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Check if a key is an activation key (Enter or Space)
|
|
10
|
-
*/
|
|
11
|
-
export function isActivationKey(key: string): boolean {
|
|
12
|
-
return key === 'Enter' || key === ' '
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Check if a key is an arrow key
|
|
17
|
-
*/
|
|
18
|
-
export function isArrowKey(key: string): boolean {
|
|
19
|
-
return ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check if a key is a navigation key (Home, End, PageUp, PageDown)
|
|
24
|
-
*/
|
|
25
|
-
export function isNavigationKey(key: string): boolean {
|
|
26
|
-
return ['Home', 'End', 'PageUp', 'PageDown'].includes(key)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Check if a key is an escape key
|
|
31
|
-
*/
|
|
32
|
-
export function isEscapeKey(key: string): boolean {
|
|
33
|
-
return key === 'Escape'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Check if modifier keys are pressed
|
|
38
|
-
*/
|
|
39
|
-
export function hasModifierKey(event: React.KeyboardEvent): boolean {
|
|
40
|
-
return event.ctrlKey || event.metaKey || event.altKey || event.shiftKey
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Create a keyboard handler that only fires on specific keys
|
|
45
|
-
*/
|
|
46
|
-
export function createKeyHandler(
|
|
47
|
-
keys: string[],
|
|
48
|
-
handler: KeyboardHandler
|
|
49
|
-
): KeyboardHandler {
|
|
50
|
-
return (event: React.KeyboardEvent) => {
|
|
51
|
-
if (keys.includes(event.key)) {
|
|
52
|
-
handler(event)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Create a keyboard handler for activation keys (Enter/Space)
|
|
59
|
-
*/
|
|
60
|
-
export function createActivationHandler(
|
|
61
|
-
handler: KeyboardHandler
|
|
62
|
-
): KeyboardHandler {
|
|
63
|
-
return createKeyHandler(['Enter', ' '], (event) => {
|
|
64
|
-
event.preventDefault()
|
|
65
|
-
handler(event)
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Create a keyboard handler for arrow keys
|
|
71
|
-
*/
|
|
72
|
-
export function createArrowKeyHandler(
|
|
73
|
-
handler: KeyboardHandler
|
|
74
|
-
): KeyboardHandler {
|
|
75
|
-
return createKeyHandler(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'], handler)
|
|
76
|
-
}
|
|
77
|
-
|