@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.
Files changed (217) hide show
  1. package/README.md +182 -157
  2. package/dist/components/Button/Button.d.ts +37 -0
  3. package/dist/components/Button/Button.d.ts.map +1 -0
  4. package/dist/components/Button/Button.js +52 -0
  5. package/dist/components/Button/index.d.ts +3 -0
  6. package/dist/components/Button/index.d.ts.map +1 -0
  7. package/dist/components/Button/index.js +1 -0
  8. package/dist/components/DataTable/DataTable.d.ts +71 -0
  9. package/dist/components/DataTable/DataTable.d.ts.map +1 -0
  10. package/dist/components/DataTable/DataTable.js +122 -0
  11. package/dist/components/DataTable/index.d.ts +3 -0
  12. package/dist/components/DataTable/index.d.ts.map +1 -0
  13. package/dist/components/DataTable/index.js +1 -0
  14. package/dist/components/Form/Checkbox.d.ts +36 -0
  15. package/dist/components/Form/Checkbox.d.ts.map +1 -0
  16. package/dist/components/Form/Checkbox.js +39 -0
  17. package/dist/components/Form/Fieldset.d.ts +33 -0
  18. package/dist/components/Form/Fieldset.d.ts.map +1 -0
  19. package/dist/components/Form/Fieldset.js +34 -0
  20. package/dist/components/Form/Input.d.ts +37 -0
  21. package/dist/components/Form/Input.d.ts.map +1 -0
  22. package/dist/components/Form/Input.js +41 -0
  23. package/dist/components/Form/Label.d.ts +30 -0
  24. package/dist/components/Form/Label.d.ts.map +1 -0
  25. package/dist/components/Form/Label.js +30 -0
  26. package/dist/components/Form/Radio.d.ts +53 -0
  27. package/dist/components/Form/Radio.d.ts.map +1 -0
  28. package/dist/components/Form/Radio.js +39 -0
  29. package/dist/components/Form/Select.d.ts +51 -0
  30. package/dist/components/Form/Select.d.ts.map +1 -0
  31. package/dist/components/Form/Select.js +49 -0
  32. package/dist/components/Form/Textarea.d.ts +44 -0
  33. package/dist/components/Form/Textarea.d.ts.map +1 -0
  34. package/dist/components/Form/Textarea.js +43 -0
  35. package/dist/components/Form/index.d.ts +8 -0
  36. package/dist/components/Form/index.d.ts.map +1 -0
  37. package/dist/components/Form/index.js +7 -0
  38. package/dist/components/Link/Link.d.ts +34 -0
  39. package/dist/components/Link/Link.d.ts.map +1 -0
  40. package/dist/components/Link/Link.js +48 -0
  41. package/dist/components/Link/index.d.ts +3 -0
  42. package/dist/components/Link/index.d.ts.map +1 -0
  43. package/dist/components/Link/index.js +1 -0
  44. package/dist/components/Modal/Modal.d.ts +64 -0
  45. package/dist/components/Modal/Modal.d.ts.map +1 -0
  46. package/dist/components/Modal/Modal.js +108 -0
  47. package/dist/components/Modal/index.d.ts +3 -0
  48. package/dist/components/Modal/index.d.ts.map +1 -0
  49. package/dist/components/Modal/index.js +1 -0
  50. package/dist/components/Tabs/Tabs.d.ts +63 -0
  51. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  52. package/dist/components/Tabs/Tabs.js +134 -0
  53. package/dist/components/Tabs/index.d.ts +3 -0
  54. package/dist/components/Tabs/index.d.ts.map +1 -0
  55. package/dist/components/Tabs/index.js +1 -0
  56. package/dist/components/Toast/Toast.d.ts +59 -0
  57. package/dist/components/Toast/Toast.d.ts.map +1 -0
  58. package/dist/components/Toast/Toast.js +91 -0
  59. package/dist/components/Toast/ToastProvider.d.ts +22 -0
  60. package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
  61. package/dist/components/Toast/ToastProvider.js +33 -0
  62. package/dist/components/Toast/index.d.ts +5 -0
  63. package/dist/components/Toast/index.d.ts.map +1 -0
  64. package/dist/components/Toast/index.js +2 -0
  65. package/dist/hooks/useAriaLive.d.ts +9 -0
  66. package/dist/hooks/useAriaLive.d.ts.map +1 -0
  67. package/dist/hooks/useAriaLive.js +39 -0
  68. package/dist/hooks/useFocusReturn.d.ts +9 -0
  69. package/dist/hooks/useFocusReturn.d.ts.map +1 -0
  70. package/dist/hooks/useFocusReturn.js +33 -0
  71. package/dist/hooks/useFocusTrap.d.ts +9 -0
  72. package/dist/hooks/useFocusTrap.d.ts.map +1 -0
  73. package/dist/hooks/useFocusTrap.js +68 -0
  74. package/dist/index.d.ts +22 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/{packages/design-system/src/index.ts → dist/index.js} +0 -4
  77. package/dist/styles/index.d.ts +3 -0
  78. package/dist/styles/index.d.ts.map +1 -0
  79. package/dist/styles/index.js +1 -0
  80. package/dist/tokens/breakpoints.d.ts +25 -0
  81. package/dist/tokens/breakpoints.d.ts.map +1 -0
  82. package/dist/tokens/breakpoints.js +23 -0
  83. package/dist/tokens/colors.d.ts +81 -0
  84. package/dist/tokens/colors.d.ts.map +1 -0
  85. package/dist/tokens/colors.js +86 -0
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.d.ts.map +1 -0
  88. package/dist/tokens/index.js +5 -0
  89. package/dist/tokens/motion.d.ts +30 -0
  90. package/dist/tokens/motion.d.ts.map +1 -0
  91. package/dist/tokens/motion.js +34 -0
  92. package/dist/tokens/spacing.d.ts +22 -0
  93. package/dist/tokens/spacing.d.ts.map +1 -0
  94. package/dist/tokens/spacing.js +20 -0
  95. package/dist/tokens/theme.d.ts +159 -0
  96. package/dist/tokens/theme.d.ts.map +1 -0
  97. package/dist/tokens/theme.js +15 -0
  98. package/dist/tokens/typography.d.ts +45 -0
  99. package/dist/tokens/typography.d.ts.map +1 -0
  100. package/dist/tokens/typography.js +56 -0
  101. package/dist/utils/aria.d.ts +60 -0
  102. package/dist/utils/aria.d.ts.map +1 -0
  103. package/dist/utils/aria.js +86 -0
  104. package/dist/utils/focus.d.ts +30 -0
  105. package/dist/utils/focus.d.ts.map +1 -0
  106. package/dist/utils/focus.js +80 -0
  107. package/dist/utils/index.d.ts +4 -0
  108. package/dist/utils/index.d.ts.map +1 -0
  109. package/dist/utils/index.js +3 -0
  110. package/dist/utils/keyboard.d.ts +38 -0
  111. package/dist/utils/keyboard.d.ts.map +1 -0
  112. package/dist/utils/keyboard.js +59 -0
  113. package/package.json +58 -31
  114. package/.storybook/custom.css +0 -69
  115. package/.storybook/main.ts +0 -46
  116. package/.storybook/manager.ts +0 -26
  117. package/.storybook/package.json +0 -6
  118. package/.storybook/preview.tsx +0 -31
  119. package/.storybook/public/logo.png +0 -0
  120. package/.storybook/vite.config.ts +0 -24
  121. package/.storybook/welcome.mdx +0 -97
  122. package/DEPLOYMENT.md +0 -154
  123. package/apps/web/app/(docs)/audit/audit.css +0 -269
  124. package/apps/web/app/(docs)/audit/page.tsx +0 -271
  125. package/apps/web/app/(docs)/components/button/page.tsx +0 -49
  126. package/apps/web/app/(docs)/components/form/page.tsx +0 -92
  127. package/apps/web/app/(docs)/components/link/page.tsx +0 -31
  128. package/apps/web/app/(docs)/components/modal/page.tsx +0 -41
  129. package/apps/web/app/(docs)/components/page.tsx +0 -37
  130. package/apps/web/app/(docs)/components/table/page.tsx +0 -54
  131. package/apps/web/app/(docs)/components/tabs/page.tsx +0 -61
  132. package/apps/web/app/(docs)/components/toast/page.tsx +0 -51
  133. package/apps/web/app/api/audit/route.ts +0 -128
  134. package/apps/web/app/favicon.ico +0 -0
  135. package/apps/web/app/layout.tsx +0 -20
  136. package/apps/web/app/page.tsx +0 -17
  137. package/apps/web/app/styles/globals.css +0 -5
  138. package/apps/web/next-env.d.ts +0 -5
  139. package/apps/web/next.config.js +0 -21
  140. package/apps/web/package.json +0 -28
  141. package/apps/web/public/_headers +0 -17
  142. package/apps/web/public/_redirects +0 -31
  143. package/apps/web/public/logo.png +0 -0
  144. package/apps/web/tsconfig.json +0 -29
  145. package/netlify/functions/audit.ts +0 -163
  146. package/netlify.toml +0 -37
  147. package/packages/design-system/README.md +0 -252
  148. package/packages/design-system/package.json +0 -68
  149. package/packages/design-system/scripts/copy-css.js +0 -63
  150. package/packages/design-system/src/components/Button/Button.stories.tsx +0 -228
  151. package/packages/design-system/src/components/Button/Button.tsx +0 -137
  152. package/packages/design-system/src/components/Button/index.ts +0 -3
  153. package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +0 -211
  154. package/packages/design-system/src/components/DataTable/DataTable.tsx +0 -293
  155. package/packages/design-system/src/components/DataTable/index.ts +0 -3
  156. package/packages/design-system/src/components/Form/Checkbox.stories.tsx +0 -252
  157. package/packages/design-system/src/components/Form/Checkbox.tsx +0 -114
  158. package/packages/design-system/src/components/Form/Fieldset.stories.tsx +0 -210
  159. package/packages/design-system/src/components/Form/Fieldset.tsx +0 -71
  160. package/packages/design-system/src/components/Form/Input.stories.tsx +0 -164
  161. package/packages/design-system/src/components/Form/Input.tsx +0 -113
  162. package/packages/design-system/src/components/Form/Label.tsx +0 -56
  163. package/packages/design-system/src/components/Form/Radio.stories.tsx +0 -265
  164. package/packages/design-system/src/components/Form/Radio.tsx +0 -147
  165. package/packages/design-system/src/components/Form/Select.stories.tsx +0 -295
  166. package/packages/design-system/src/components/Form/Select.tsx +0 -160
  167. package/packages/design-system/src/components/Form/Textarea.stories.tsx +0 -253
  168. package/packages/design-system/src/components/Form/Textarea.tsx +0 -145
  169. package/packages/design-system/src/components/Form/index.ts +0 -8
  170. package/packages/design-system/src/components/Link/Link.stories.tsx +0 -128
  171. package/packages/design-system/src/components/Link/Link.tsx +0 -117
  172. package/packages/design-system/src/components/Link/index.ts +0 -3
  173. package/packages/design-system/src/components/Modal/Modal.stories.tsx +0 -165
  174. package/packages/design-system/src/components/Modal/Modal.tsx +0 -202
  175. package/packages/design-system/src/components/Modal/index.ts +0 -3
  176. package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +0 -213
  177. package/packages/design-system/src/components/Tabs/Tabs.tsx +0 -248
  178. package/packages/design-system/src/components/Tabs/index.ts +0 -3
  179. package/packages/design-system/src/components/Toast/Toast.stories.tsx +0 -153
  180. package/packages/design-system/src/components/Toast/Toast.tsx +0 -175
  181. package/packages/design-system/src/components/Toast/ToastProvider.tsx +0 -73
  182. package/packages/design-system/src/components/Toast/index.ts +0 -5
  183. package/packages/design-system/src/hooks/useAriaLive.ts +0 -51
  184. package/packages/design-system/src/hooks/useFocusReturn.ts +0 -40
  185. package/packages/design-system/src/hooks/useFocusTrap.ts +0 -82
  186. package/packages/design-system/src/styles/index.ts +0 -3
  187. package/packages/design-system/src/tokens/breakpoints.ts +0 -28
  188. package/packages/design-system/src/tokens/colors.ts +0 -98
  189. package/packages/design-system/src/tokens/index.ts +0 -6
  190. package/packages/design-system/src/tokens/motion.ts +0 -41
  191. package/packages/design-system/src/tokens/spacing.ts +0 -24
  192. package/packages/design-system/src/tokens/theme.ts +0 -19
  193. package/packages/design-system/src/tokens/typography.ts +0 -64
  194. package/packages/design-system/src/utils/aria.ts +0 -108
  195. package/packages/design-system/src/utils/focus.ts +0 -87
  196. package/packages/design-system/src/utils/index.ts +0 -4
  197. package/packages/design-system/src/utils/keyboard.ts +0 -77
  198. package/packages/design-system/tsconfig.json +0 -17
  199. package/public/logo.png +0 -0
  200. package/scripts/fix-storybook-paths.js +0 -53
  201. package/tsconfig.json +0 -20
  202. /package/{packages/design-system/src → dist}/components/Button/Button.css +0 -0
  203. /package/{packages/design-system/src → dist}/components/DataTable/DataTable.css +0 -0
  204. /package/{packages/design-system/src → dist}/components/Form/Checkbox.css +0 -0
  205. /package/{packages/design-system/src → dist}/components/Form/Fieldset.css +0 -0
  206. /package/{packages/design-system/src → dist}/components/Form/Input.css +0 -0
  207. /package/{packages/design-system/src → dist}/components/Form/Label.css +0 -0
  208. /package/{packages/design-system/src → dist}/components/Form/Radio.css +0 -0
  209. /package/{packages/design-system/src → dist}/components/Form/Select.css +0 -0
  210. /package/{packages/design-system/src → dist}/components/Form/Textarea.css +0 -0
  211. /package/{packages/design-system/src → dist}/components/Link/Link.css +0 -0
  212. /package/{packages/design-system/src → dist}/components/Modal/Modal.css +0 -0
  213. /package/{packages/design-system/src → dist}/components/Tabs/Tabs.css +0 -0
  214. /package/{packages/design-system/src → dist}/components/Toast/Toast.css +0 -0
  215. /package/{packages/design-system/src → dist}/components/Toast/ToastProvider.css +0 -0
  216. /package/{packages/design-system/src → dist}/styles/components.css +0 -0
  217. /package/{packages/design-system/src → dist}/styles/global.css +0 -0
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import './Textarea.css';
3
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
4
+ /**
5
+ * Error message to display
6
+ */
7
+ error?: string;
8
+ /**
9
+ * Helper text to display below the textarea
10
+ */
11
+ helperText?: string;
12
+ /**
13
+ * Label for the textarea
14
+ */
15
+ label?: string;
16
+ /**
17
+ * Maximum character count (shows counter)
18
+ */
19
+ maxLength?: number;
20
+ /**
21
+ * Whether to show character count
22
+ */
23
+ showCount?: boolean;
24
+ }
25
+ /**
26
+ * Accessible Textarea component
27
+ *
28
+ * WCAG Compliance:
29
+ * - 1.3.1 Info and Relationships: Proper label-textarea association
30
+ * - 4.1.2 Name, Role, Value: Proper ARIA attributes
31
+ * - 4.1.3 Status Messages: Error messages announced
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * <Textarea
36
+ * id="message"
37
+ * label="Message"
38
+ * maxLength={500}
39
+ * showCount
40
+ * />
41
+ * ```
42
+ */
43
+ export declare const Textarea: React.ForwardRefExoticComponent<TextareaProps & React.RefAttributes<HTMLTextAreaElement>>;
44
+ //# sourceMappingURL=Textarea.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Textarea.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Textarea.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,gBAAgB,CAAA;AAEvB,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,sBAAsB,CAAC,mBAAmB,CAAC;IACtF;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ,2FA0FpB,CAAA"}
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { combineAriaDescribedBy } from '../../utils/aria';
5
+ import './Textarea.css';
6
+ /**
7
+ * Accessible Textarea component
8
+ *
9
+ * WCAG Compliance:
10
+ * - 1.3.1 Info and Relationships: Proper label-textarea association
11
+ * - 4.1.2 Name, Role, Value: Proper ARIA attributes
12
+ * - 4.1.3 Status Messages: Error messages announced
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <Textarea
17
+ * id="message"
18
+ * label="Message"
19
+ * maxLength={500}
20
+ * showCount
21
+ * />
22
+ * ```
23
+ */
24
+ export const Textarea = React.forwardRef(({ id, error, helperText, label, maxLength, showCount = false, className = '', value, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
25
+ const textareaId = React.useId();
26
+ const finalId = id || `textarea-${textareaId}`;
27
+ const errorId = error ? `${finalId}-error` : undefined;
28
+ const helperId = helperText ? `${finalId}-helper` : undefined;
29
+ const countId = showCount ? `${finalId}-count` : undefined;
30
+ const describedBy = combineAriaDescribedBy(ariaDescribedBy, errorId, helperId, countId);
31
+ const currentLength = typeof value === 'string' ? value.length : 0;
32
+ const remainingChars = maxLength ? maxLength - currentLength : undefined;
33
+ const classes = [
34
+ 'form-textarea',
35
+ error && 'form-textarea--error',
36
+ props.disabled && 'form-textarea--disabled',
37
+ className,
38
+ ]
39
+ .filter(Boolean)
40
+ .join(' ');
41
+ return (_jsxs("div", { className: "form-textarea-wrapper", children: [label && (_jsxs("label", { htmlFor: finalId, className: "form-label", children: [label, props.required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] })), _jsx("textarea", { ref: ref, id: finalId, className: classes, maxLength: maxLength, value: value, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, required: props.required ? true : undefined, ...props }), (showCount || helperText) && (_jsxs("div", { className: "form-textarea-footer", children: [helperText && !error && (_jsx("span", { id: helperId, className: "form-helper-text", children: helperText })), error && (_jsx("span", { id: errorId, className: "form-error-text", role: "alert", children: error })), showCount && maxLength && (_jsxs("span", { id: countId, className: "form-character-count", "aria-live": "polite", children: [currentLength, " / ", maxLength] }))] }))] }));
42
+ });
43
+ Textarea.displayName = 'Textarea';
@@ -0,0 +1,8 @@
1
+ export * from './Input';
2
+ export * from './Textarea';
3
+ export * from './Select';
4
+ export * from './Checkbox';
5
+ export * from './Radio';
6
+ export * from './Fieldset';
7
+ export * from './Label';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Form/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA"}
@@ -0,0 +1,7 @@
1
+ export * from './Input';
2
+ export * from './Textarea';
3
+ export * from './Select';
4
+ export * from './Checkbox';
5
+ export * from './Radio';
6
+ export * from './Fieldset';
7
+ export * from './Label';
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import './Link.css';
3
+ export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
4
+ /**
5
+ * Whether this is an external link
6
+ * Automatically adds rel="noopener noreferrer" for security
7
+ */
8
+ external?: boolean;
9
+ /**
10
+ * Whether this is a skip link (for keyboard navigation)
11
+ */
12
+ skip?: boolean;
13
+ /**
14
+ * ARIA label for the link (required if no visible text)
15
+ */
16
+ 'aria-label'?: string;
17
+ }
18
+ /**
19
+ * Accessible Link component
20
+ *
21
+ * WCAG Compliance:
22
+ * - 2.4.4 Link Purpose: Clear link text or aria-label
23
+ * - 2.4.7 Focus Visible: Clear focus indicators
24
+ * - 4.1.2 Name, Role, Value: Proper semantic HTML
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Link href="/about" external>
29
+ * Learn more
30
+ * </Link>
31
+ * ```
32
+ */
33
+ export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement>>;
34
+ //# sourceMappingURL=Link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../../src/components/Link/Link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,YAAY,CAAA;AAEnB,MAAM,WAAW,SAAU,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC;IAC9E;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IAEd;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,IAAI,qFA2EhB,CAAA"}
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import './Link.css';
5
+ /**
6
+ * Accessible Link component
7
+ *
8
+ * WCAG Compliance:
9
+ * - 2.4.4 Link Purpose: Clear link text or aria-label
10
+ * - 2.4.7 Focus Visible: Clear focus indicators
11
+ * - 4.1.2 Name, Role, Value: Proper semantic HTML
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Link href="/about" external>
16
+ * Learn more
17
+ * </Link>
18
+ * ```
19
+ */
20
+ export const Link = React.forwardRef(({ external = false, skip = false, href, rel, target, className = '', children, 'aria-label': ariaLabel, ...props }, ref) => {
21
+ // Determine if link is external based on href or explicit prop
22
+ const isExternal = external ||
23
+ (href && (href.startsWith('http') || href.startsWith('//')));
24
+ // Build rel attribute
25
+ const relAttributes = React.useMemo(() => {
26
+ const attrs = new Set(rel?.split(' ') || []);
27
+ if (isExternal) {
28
+ attrs.add('noopener');
29
+ attrs.add('noreferrer');
30
+ }
31
+ return Array.from(attrs).join(' ');
32
+ }, [isExternal, rel]);
33
+ // Set target for external links
34
+ const linkTarget = isExternal && !target ? '_blank' : target;
35
+ const classes = [
36
+ 'link',
37
+ skip && 'link--skip',
38
+ className,
39
+ ]
40
+ .filter(Boolean)
41
+ .join(' ');
42
+ // Skip links should use button semantics if no href
43
+ if (skip && !href) {
44
+ return (_jsx("button", { ref: ref, className: classes, "aria-label": ariaLabel, ...props, children: children }));
45
+ }
46
+ return (_jsxs("a", { ref: ref, href: href, rel: relAttributes || undefined, target: linkTarget, className: classes, "aria-label": ariaLabel, ...props, children: [children, isExternal && (_jsxs("span", { className: "link__external-icon", "aria-hidden": "true", children: [' ', _jsx("span", { "aria-label": "(opens in new tab)", children: "\u2197" })] }))] }));
47
+ });
48
+ Link.displayName = 'Link';
@@ -0,0 +1,3 @@
1
+ export { Link } from './Link';
2
+ export type { LinkProps } from './Link';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Link/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA"}
@@ -0,0 +1 @@
1
+ export { Link } from './Link';
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import './Modal.css';
3
+ export interface ModalProps {
4
+ /**
5
+ * Whether the modal is open
6
+ */
7
+ isOpen: boolean;
8
+ /**
9
+ * Callback when modal should close
10
+ */
11
+ onClose: () => void;
12
+ /**
13
+ * Title of the modal (required for accessibility)
14
+ */
15
+ title: string;
16
+ /**
17
+ * Content of the modal
18
+ */
19
+ children: React.ReactNode;
20
+ /**
21
+ * Whether to close on backdrop click
22
+ */
23
+ closeOnBackdropClick?: boolean;
24
+ /**
25
+ * Whether to close on ESC key press
26
+ */
27
+ closeOnEscape?: boolean;
28
+ /**
29
+ * Size of the modal
30
+ */
31
+ size?: 'sm' | 'md' | 'lg' | 'full';
32
+ /**
33
+ * Element to return focus to when modal closes
34
+ */
35
+ returnFocusTo?: HTMLElement | null;
36
+ }
37
+ /**
38
+ * Accessible Modal component using HTML5 dialog element
39
+ *
40
+ * Uses the native `<dialog>` element which provides:
41
+ * - Built-in focus management and focus trapping
42
+ * - Automatic body scroll prevention
43
+ * - Native backdrop overlay
44
+ * - ESC key handling (configurable)
45
+ *
46
+ * WCAG Compliance:
47
+ * - 2.1.1 Keyboard: ESC key support, built-in focus trap
48
+ * - 2.1.2 No Keyboard Trap: Focus returns to trigger
49
+ * - 2.4.3 Focus Order: Focus trapped within modal (native behavior)
50
+ * - 4.1.2 Name, Role, Value: ARIA modal pattern
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * <Modal
55
+ * isOpen={isOpen}
56
+ * onClose={() => setIsOpen(false)}
57
+ * title="Confirm Action"
58
+ * >
59
+ * <p>Are you sure?</p>
60
+ * </Modal>
61
+ * ```
62
+ */
63
+ export declare const Modal: React.FC<ModalProps>;
64
+ //# sourceMappingURL=Modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/Modal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAGhD,OAAO,aAAa,CAAA;AAEpB,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAA;IAEf;;OAEG;IACH,OAAO,EAAE,MAAM,IAAI,CAAA;IAEnB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IAEb;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IAEzB;;OAEG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAE9B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;OAEG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAA;IAElC;;OAEG;IACH,aAAa,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CA2HtC,CAAA"}
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React, { useEffect, useRef } from 'react';
4
+ import { useFocusReturn } from '../../hooks/useFocusReturn';
5
+ import { Button } from '../Button/Button';
6
+ import './Modal.css';
7
+ /**
8
+ * Accessible Modal component using HTML5 dialog element
9
+ *
10
+ * Uses the native `<dialog>` element which provides:
11
+ * - Built-in focus management and focus trapping
12
+ * - Automatic body scroll prevention
13
+ * - Native backdrop overlay
14
+ * - ESC key handling (configurable)
15
+ *
16
+ * WCAG Compliance:
17
+ * - 2.1.1 Keyboard: ESC key support, built-in focus trap
18
+ * - 2.1.2 No Keyboard Trap: Focus returns to trigger
19
+ * - 2.4.3 Focus Order: Focus trapped within modal (native behavior)
20
+ * - 4.1.2 Name, Role, Value: ARIA modal pattern
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <Modal
25
+ * isOpen={isOpen}
26
+ * onClose={() => setIsOpen(false)}
27
+ * title="Confirm Action"
28
+ * >
29
+ * <p>Are you sure?</p>
30
+ * </Modal>
31
+ * ```
32
+ */
33
+ export const Modal = ({ isOpen, onClose, title, children, closeOnBackdropClick = true, closeOnEscape = true, size = 'md', returnFocusTo, }) => {
34
+ const dialogRef = useRef(null);
35
+ const contentRef = useRef(null);
36
+ const titleId = React.useId();
37
+ const descriptionId = React.useId();
38
+ // Return focus on close
39
+ useFocusReturn(isOpen, returnFocusTo);
40
+ // Handle dialog open/close
41
+ useEffect(() => {
42
+ const dialog = dialogRef.current;
43
+ if (!dialog)
44
+ return;
45
+ if (isOpen) {
46
+ // Show modal dialog
47
+ dialog.showModal();
48
+ }
49
+ else {
50
+ // Close dialog
51
+ dialog.close();
52
+ }
53
+ return () => {
54
+ // Cleanup: ensure dialog is closed when component unmounts
55
+ if (dialog.open) {
56
+ dialog.close();
57
+ }
58
+ };
59
+ }, [isOpen]);
60
+ // Handle backdrop clicks
61
+ // The ::backdrop pseudo-element doesn't bubble events to the dialog element,
62
+ // so we need to listen for clicks on the document and check if they're outside
63
+ // the dialog content area.
64
+ useEffect(() => {
65
+ if (!isOpen || !closeOnBackdropClick)
66
+ return;
67
+ const handleDocumentClick = (event) => {
68
+ const dialog = dialogRef.current;
69
+ const content = contentRef.current;
70
+ if (!dialog || !content)
71
+ return;
72
+ // Check if click target is outside the dialog content area
73
+ const target = event.target;
74
+ // If the click is not inside the content wrapper, it's a backdrop click
75
+ if (!content.contains(target)) {
76
+ // Verify click coordinates are outside content bounds for extra safety
77
+ const rect = content.getBoundingClientRect();
78
+ const clickX = event.clientX;
79
+ const clickY = event.clientY;
80
+ const isOutsideContent = clickX < rect.left ||
81
+ clickX > rect.right ||
82
+ clickY < rect.top ||
83
+ clickY > rect.bottom;
84
+ if (isOutsideContent) {
85
+ event.preventDefault();
86
+ event.stopPropagation();
87
+ onClose();
88
+ }
89
+ }
90
+ };
91
+ // Use capture phase to catch events before they bubble
92
+ document.addEventListener('mousedown', handleDocumentClick, true);
93
+ return () => {
94
+ document.removeEventListener('mousedown', handleDocumentClick, true);
95
+ };
96
+ }, [isOpen, closeOnBackdropClick, onClose]);
97
+ // Handle cancel event (fires when ESC key is pressed)
98
+ const handleCancel = (event) => {
99
+ // Prevent default close behavior
100
+ event.preventDefault();
101
+ // Only close if closeOnEscape is enabled
102
+ if (closeOnEscape) {
103
+ onClose();
104
+ }
105
+ };
106
+ return (_jsx("dialog", { ref: dialogRef, className: `modal modal--${size} ${isOpen ? 'modal--open' : ''}`, "aria-labelledby": titleId, "aria-describedby": descriptionId, onCancel: handleCancel, children: _jsxs("div", { ref: contentRef, className: "modal-content-wrapper", children: [_jsxs("div", { className: "modal-header", children: [_jsx("h2", { id: titleId, className: "modal-title", children: title }), _jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close modal", className: "modal-close", children: "\u00D7" })] }), _jsx("div", { id: descriptionId, className: "modal-content", children: children })] }) }));
107
+ };
108
+ Modal.displayName = 'Modal';
@@ -0,0 +1,3 @@
1
+ export { Modal } from './Modal';
2
+ export type { ModalProps } from './Modal';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1 @@
1
+ export { Modal } from './Modal';
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import './Tabs.css';
3
+ export interface TabItem {
4
+ id: string;
5
+ label: string;
6
+ content: React.ReactNode;
7
+ disabled?: boolean;
8
+ }
9
+ export interface TabsProps {
10
+ /**
11
+ * Tab items
12
+ */
13
+ items: TabItem[];
14
+ /**
15
+ * Default selected tab ID
16
+ */
17
+ defaultSelectedId?: string;
18
+ /**
19
+ * Controlled selected tab ID
20
+ */
21
+ selectedId?: string;
22
+ /**
23
+ * Callback when tab selection changes
24
+ */
25
+ onSelectionChange?: (id: string) => void;
26
+ /**
27
+ * Orientation of tabs
28
+ */
29
+ orientation?: 'horizontal' | 'vertical';
30
+ /**
31
+ * Activation mode for tabs
32
+ * - 'automatic': Arrow keys both move focus and activate tabs immediately
33
+ * - 'manual': Arrow keys move focus only, Enter/Space activates the focused tab
34
+ * @default 'automatic'
35
+ */
36
+ activationMode?: 'automatic' | 'manual';
37
+ /**
38
+ * Label for the tab list (required for accessibility)
39
+ */
40
+ 'aria-label'?: string;
41
+ 'aria-labelledby'?: string;
42
+ }
43
+ /**
44
+ * Accessible Tabs component
45
+ *
46
+ * WCAG Compliance:
47
+ * - 2.1.1 Keyboard: Arrow key navigation, Home/End support
48
+ * - 4.1.2 Name, Role, Value: ARIA tabs pattern
49
+ * - 2.4.3 Focus Order: Proper focus management
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * <Tabs
54
+ * items={[
55
+ * { id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
56
+ * { id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
57
+ * ]}
58
+ * aria-label="Settings tabs"
59
+ * />
60
+ * ```
61
+ */
62
+ export declare const Tabs: React.FC<TabsProps>;
63
+ //# sourceMappingURL=Tabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAwC,MAAM,OAAO,CAAA;AAG5D,OAAO,YAAY,CAAA;AAEnB,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,KAAK,EAAE,OAAO,EAAE,CAAA;IAEhB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAExC;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAA;IAEvC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAA;IAEvC;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,SAAS,CA0KpC,CAAA"}
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import { isNavigationKey, isArrowKey } from '../../utils/keyboard';
5
+ import { getCurrentAttributes } from '../../utils/aria';
6
+ import './Tabs.css';
7
+ /**
8
+ * Accessible Tabs component
9
+ *
10
+ * WCAG Compliance:
11
+ * - 2.1.1 Keyboard: Arrow key navigation, Home/End support
12
+ * - 4.1.2 Name, Role, Value: ARIA tabs pattern
13
+ * - 2.4.3 Focus Order: Proper focus management
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <Tabs
18
+ * items={[
19
+ * { id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
20
+ * { id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
21
+ * ]}
22
+ * aria-label="Settings tabs"
23
+ * />
24
+ * ```
25
+ */
26
+ export const Tabs = ({ items, defaultSelectedId, selectedId: controlledSelectedId, onSelectionChange, orientation = 'horizontal', activationMode = 'automatic', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }) => {
27
+ const initialSelectedId = defaultSelectedId || items[0]?.id;
28
+ const [internalSelectedId, setInternalSelectedId] = useState(initialSelectedId);
29
+ const [focusedId, setFocusedId] = useState(initialSelectedId);
30
+ const tabRefs = useRef(new Map());
31
+ const selectedId = controlledSelectedId ?? internalSelectedId;
32
+ const selectedIndex = items.findIndex((item) => item.id === selectedId);
33
+ // In automatic mode, focused tab is always the selected tab
34
+ // In manual mode, focused tab can be different from selected tab
35
+ const effectiveFocusedId = activationMode === 'automatic' ? selectedId : (focusedId || selectedId);
36
+ const handleSelect = useCallback((id) => {
37
+ if (onSelectionChange) {
38
+ onSelectionChange(id);
39
+ }
40
+ else {
41
+ setInternalSelectedId(id);
42
+ }
43
+ // In manual mode, update focused tab when selecting
44
+ if (activationMode === 'manual') {
45
+ setFocusedId(id);
46
+ }
47
+ }, [onSelectionChange, activationMode]);
48
+ const handleKeyDown = useCallback((event, currentIndex) => {
49
+ const isHorizontal = orientation === 'horizontal';
50
+ let newIndex = currentIndex;
51
+ // Handle Enter/Space for manual activation
52
+ if (activationMode === 'manual' && (event.key === 'Enter' || event.key === ' ')) {
53
+ event.preventDefault();
54
+ const currentTab = items[currentIndex];
55
+ if (currentTab && !currentTab.disabled) {
56
+ handleSelect(currentTab.id);
57
+ }
58
+ return;
59
+ }
60
+ // Handle arrow keys and Home/End
61
+ if (isNavigationKey(event.key) || isArrowKey(event.key)) {
62
+ event.preventDefault();
63
+ switch (event.key) {
64
+ case 'Home':
65
+ newIndex = 0;
66
+ break;
67
+ case 'End':
68
+ newIndex = items.length - 1;
69
+ break;
70
+ case 'ArrowRight':
71
+ if (isHorizontal) {
72
+ newIndex = (currentIndex + 1) % items.length;
73
+ }
74
+ break;
75
+ case 'ArrowLeft':
76
+ if (isHorizontal) {
77
+ newIndex = (currentIndex - 1 + items.length) % items.length;
78
+ }
79
+ break;
80
+ case 'ArrowDown':
81
+ if (!isHorizontal) {
82
+ newIndex = (currentIndex + 1) % items.length;
83
+ }
84
+ break;
85
+ case 'ArrowUp':
86
+ if (!isHorizontal) {
87
+ newIndex = (currentIndex - 1 + items.length) % items.length;
88
+ }
89
+ break;
90
+ }
91
+ // Skip disabled tabs
92
+ while (items[newIndex]?.disabled && newIndex !== currentIndex) {
93
+ if (event.key === 'Home' || event.key === 'ArrowRight' || event.key === 'ArrowDown') {
94
+ newIndex = (newIndex + 1) % items.length;
95
+ }
96
+ else {
97
+ newIndex = (newIndex - 1 + items.length) % items.length;
98
+ }
99
+ }
100
+ const newTab = items[newIndex];
101
+ if (newTab && !newTab.disabled) {
102
+ if (activationMode === 'automatic') {
103
+ // Automatic: move focus and activate
104
+ handleSelect(newTab.id);
105
+ tabRefs.current.get(newTab.id)?.focus();
106
+ }
107
+ else {
108
+ // Manual: move focus only
109
+ setFocusedId(newTab.id);
110
+ tabRefs.current.get(newTab.id)?.focus();
111
+ }
112
+ }
113
+ }
114
+ }, [items, orientation, activationMode, handleSelect]);
115
+ const selectedTab = items.find((item) => item.id === selectedId);
116
+ return (_jsxs("div", { className: `tabs tabs--${orientation}`, children: [_jsx("div", { className: "tabs-list", role: "tablist", "aria-orientation": orientation, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: items.map((item, index) => {
117
+ const isSelected = item.id === selectedId;
118
+ const isFocused = item.id === effectiveFocusedId;
119
+ // In manual mode, focused tab should be focusable even if not selected
120
+ // In automatic mode, only selected tab is focusable
121
+ const tabIndex = activationMode === 'manual'
122
+ ? (isFocused ? 0 : -1)
123
+ : (isSelected ? 0 : -1);
124
+ return (_jsx("button", { ref: (el) => {
125
+ if (el) {
126
+ tabRefs.current.set(item.id, el);
127
+ }
128
+ else {
129
+ tabRefs.current.delete(item.id);
130
+ }
131
+ }, id: `tab-${item.id}`, role: "tab", "aria-controls": `tabpanel-${item.id}`, "aria-selected": isSelected, tabIndex: tabIndex, disabled: item.disabled, className: `tabs-tab ${isSelected ? 'tabs-tab--selected' : ''} ${item.disabled ? 'tabs-tab--disabled' : ''}`, onClick: () => !item.disabled && handleSelect(item.id), onKeyDown: (e) => handleKeyDown(e, index), onFocus: () => setFocusedId(item.id), ...getCurrentAttributes(isSelected ? 'page' : undefined), children: item.label }, item.id));
132
+ }) }), selectedTab && (_jsx("div", { id: `tabpanel-${selectedTab.id}`, role: "tabpanel", "aria-labelledby": `tab-${selectedTab.id}`, className: "tabs-panel", children: selectedTab.content }))] }));
133
+ };
134
+ Tabs.displayName = 'Tabs';
@@ -0,0 +1,3 @@
1
+ export { Tabs } from './Tabs';
2
+ export type { TabsProps, TabItem } from './Tabs';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA"}
@@ -0,0 +1 @@
1
+ export { Tabs } from './Tabs';