@fpkit/acss 0.5.12 → 0.5.13

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 (264) hide show
  1. package/README.md +89 -0
  2. package/libs/{chunk-DV56L5YX.cjs → chunk-2LTJ7HHX.cjs} +4 -4
  3. package/libs/{chunk-EQ67LF46.js → chunk-2Y7W75TT.js} +3 -3
  4. package/libs/{chunk-KKLTUJFB.cjs → chunk-3MKLDCKQ.cjs} +5 -5
  5. package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
  6. package/libs/{chunk-X3EVB7VS.cjs → chunk-5S4ORA4C.cjs} +3 -3
  7. package/libs/{chunk-O6QZBB6G.js → chunk-772NRB75.js} +5 -5
  8. package/libs/chunk-772NRB75.js.map +1 -0
  9. package/libs/{chunk-6BVXFW7U.cjs → chunk-AHDJGCG5.cjs} +3 -3
  10. package/libs/{chunk-E3XP6BEX.cjs → chunk-B7F5FS6D.cjs} +3 -3
  11. package/libs/chunk-D4YLRWAO.cjs +18 -0
  12. package/libs/chunk-D4YLRWAO.cjs.map +1 -0
  13. package/libs/chunk-ETFLFC2S.js +10 -0
  14. package/libs/chunk-ETFLFC2S.js.map +1 -0
  15. package/libs/chunk-GZ4QFPRY.js +9 -0
  16. package/libs/chunk-GZ4QFPRY.js.map +1 -0
  17. package/libs/{chunk-LHVJKDMA.cjs → chunk-J32EZPYD.cjs} +3 -3
  18. package/libs/chunk-JJ43O4Y5.js +8 -0
  19. package/libs/chunk-JJ43O4Y5.js.map +1 -0
  20. package/libs/chunk-KUKIVRC2.js +7 -0
  21. package/libs/chunk-KUKIVRC2.js.map +1 -0
  22. package/libs/chunk-L75OQKEI.cjs +13 -0
  23. package/libs/chunk-L75OQKEI.cjs.map +1 -0
  24. package/libs/{chunk-LL7HTLMS.cjs → chunk-M5RRNTVX.cjs} +3 -3
  25. package/libs/{chunk-LIQJ7ZZR.js → chunk-NGTJDDFO.js} +2 -2
  26. package/libs/chunk-OK5QEIMD.cjs +17 -0
  27. package/libs/chunk-OK5QEIMD.cjs.map +1 -0
  28. package/libs/chunk-P2DC76ZZ.cjs +18 -0
  29. package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
  30. package/libs/chunk-PQ2K3BM6.cjs +17 -0
  31. package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
  32. package/libs/{chunk-QCMV4VQZ.js → chunk-QLZWHAMK.js} +2 -2
  33. package/libs/{chunk-BIP2NY53.js → chunk-RIVUMPOG.js} +2 -2
  34. package/libs/{chunk-ICCKQ2GC.cjs → chunk-ROZI23GS.cjs} +4 -4
  35. package/libs/{chunk-NHYXGV3L.js → chunk-SMYRLO3E.js} +2 -2
  36. package/libs/{chunk-5ZM4XL44.js → chunk-TYRCEX2L.js} +2 -2
  37. package/libs/chunk-VUH3FXGJ.js +11 -0
  38. package/libs/chunk-VUH3FXGJ.js.map +1 -0
  39. package/libs/{chunk-PPOOBUOS.js → chunk-XBA562WW.js} +2 -2
  40. package/libs/{chunk-QVV34QEH.cjs → chunk-XTQKWY7W.cjs} +3 -3
  41. package/libs/{chunk-YWOYVRFT.js → chunk-ZANSFMTD.js} +3 -3
  42. package/libs/components/alert/alert.css +1 -1
  43. package/libs/components/alert/alert.css.map +1 -1
  44. package/libs/components/alert/alert.min.css +2 -2
  45. package/libs/components/badge/badge.css +1 -1
  46. package/libs/components/badge/badge.css.map +1 -1
  47. package/libs/components/badge/badge.min.css +2 -2
  48. package/libs/components/breadcrumbs/breadcrumb.cjs +9 -5
  49. package/libs/components/breadcrumbs/breadcrumb.d.cts +271 -32
  50. package/libs/components/breadcrumbs/breadcrumb.d.ts +271 -32
  51. package/libs/components/breadcrumbs/breadcrumb.js +3 -3
  52. package/libs/components/button.cjs +4 -4
  53. package/libs/components/button.d.cts +2 -2
  54. package/libs/components/button.d.ts +2 -2
  55. package/libs/components/button.js +2 -2
  56. package/libs/components/buttons/button.css +1 -1
  57. package/libs/components/buttons/button.css.map +1 -1
  58. package/libs/components/buttons/button.min.css +2 -2
  59. package/libs/components/card.cjs +7 -7
  60. package/libs/components/card.d.cts +277 -33
  61. package/libs/components/card.d.ts +277 -33
  62. package/libs/components/card.js +2 -2
  63. package/libs/components/cards/card.css +1 -1
  64. package/libs/components/cards/card.css.map +1 -1
  65. package/libs/components/cards/card.min.css +2 -2
  66. package/libs/components/details/details.css +1 -1
  67. package/libs/components/details/details.css.map +1 -1
  68. package/libs/components/details/details.min.css +2 -2
  69. package/libs/components/dialog/dialog.cjs +7 -7
  70. package/libs/components/dialog/dialog.css +1 -1
  71. package/libs/components/dialog/dialog.css.map +1 -1
  72. package/libs/components/dialog/dialog.d.cts +88 -34
  73. package/libs/components/dialog/dialog.d.ts +88 -34
  74. package/libs/components/dialog/dialog.js +5 -5
  75. package/libs/components/dialog/dialog.min.css +2 -2
  76. package/libs/components/form/fields.cjs +4 -4
  77. package/libs/components/form/fields.d.cts +2 -2
  78. package/libs/components/form/fields.d.ts +2 -2
  79. package/libs/components/form/fields.js +2 -2
  80. package/libs/components/form/textarea.cjs +4 -4
  81. package/libs/components/form/textarea.d.cts +2 -2
  82. package/libs/components/form/textarea.d.ts +2 -2
  83. package/libs/components/form/textarea.js +2 -2
  84. package/libs/components/heading/heading.cjs +3 -3
  85. package/libs/components/heading/heading.d.cts +3 -14
  86. package/libs/components/heading/heading.d.ts +3 -14
  87. package/libs/components/heading/heading.js +2 -2
  88. package/libs/components/icons/icon.cjs +4 -4
  89. package/libs/components/icons/icon.d.cts +148 -4
  90. package/libs/components/icons/icon.d.ts +148 -4
  91. package/libs/components/icons/icon.js +2 -2
  92. package/libs/components/images/img.css +1 -1
  93. package/libs/components/images/img.css.map +1 -1
  94. package/libs/components/images/img.min.css +2 -2
  95. package/libs/components/link/link.cjs +4 -4
  96. package/libs/components/link/link.d.cts +2 -2
  97. package/libs/components/link/link.d.ts +2 -2
  98. package/libs/components/link/link.js +2 -2
  99. package/libs/components/list/list.cjs +5 -5
  100. package/libs/components/list/list.d.cts +3 -3
  101. package/libs/components/list/list.d.ts +3 -3
  102. package/libs/components/list/list.js +2 -2
  103. package/libs/components/modal.cjs +4 -4
  104. package/libs/components/modal.js +3 -3
  105. package/libs/components/nav/nav.cjs +7 -7
  106. package/libs/components/nav/nav.d.cts +2 -2
  107. package/libs/components/nav/nav.d.ts +2 -2
  108. package/libs/components/nav/nav.js +3 -3
  109. package/libs/components/text/text.cjs +5 -5
  110. package/libs/components/text/text.d.cts +2 -2
  111. package/libs/components/text/text.d.ts +2 -2
  112. package/libs/components/text/text.js +2 -2
  113. package/libs/heading-3648c538.d.ts +250 -0
  114. package/libs/hooks.cjs +7 -0
  115. package/libs/hooks.d.cts +5 -0
  116. package/libs/hooks.d.ts +5 -0
  117. package/libs/hooks.js +3 -0
  118. package/libs/icons.cjs +3 -3
  119. package/libs/icons.d.cts +1 -1
  120. package/libs/icons.d.ts +1 -1
  121. package/libs/icons.js +2 -2
  122. package/libs/index.cjs +112 -91
  123. package/libs/index.cjs.map +1 -1
  124. package/libs/index.css +1 -1
  125. package/libs/index.css.map +1 -1
  126. package/libs/index.d.cts +515 -31
  127. package/libs/index.d.ts +515 -31
  128. package/libs/index.js +31 -19
  129. package/libs/index.js.map +1 -1
  130. package/libs/ui-645f95b5.d.ts +285 -0
  131. package/package.json +2 -83
  132. package/src/components/README-UI.mdx +416 -0
  133. package/src/components/alert/ACCESSIBILITY.md +319 -0
  134. package/src/components/alert/README.mdx +475 -19
  135. package/src/components/alert/alert.scss +113 -6
  136. package/src/components/alert/alert.stories.tsx +372 -0
  137. package/src/components/alert/alert.test.tsx +762 -0
  138. package/src/components/alert/alert.tsx +331 -66
  139. package/src/components/alert/views/alert-actions.tsx +13 -0
  140. package/src/components/alert/views/alert-content.tsx +17 -0
  141. package/src/components/alert/views/alert-icon.tsx +53 -0
  142. package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
  143. package/src/components/alert/views/alert-title.tsx +23 -0
  144. package/src/components/alert/views/alert-view.tsx +158 -0
  145. package/src/components/alert/views/index.ts +12 -0
  146. package/src/components/badge/badge.mdx +186 -49
  147. package/src/components/badge/badge.scss +20 -2
  148. package/src/components/badge/badge.stories.tsx +160 -14
  149. package/src/components/badge/badge.test.tsx +179 -0
  150. package/src/components/badge/badge.tsx +97 -4
  151. package/src/components/breadcrumbs/README.mdx +364 -45
  152. package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
  153. package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
  154. package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
  155. package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
  156. package/src/components/buttons/button.scss +34 -31
  157. package/src/components/buttons/button.stories.tsx +35 -0
  158. package/src/components/cards/README.mdx +657 -0
  159. package/src/components/cards/card.scss +22 -0
  160. package/src/components/cards/card.stories.tsx +167 -5
  161. package/src/components/cards/card.test.tsx +360 -20
  162. package/src/components/cards/card.tsx +200 -79
  163. package/src/components/cards/card.types.ts +135 -0
  164. package/src/components/cards/card.utils.ts +79 -0
  165. package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
  166. package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
  167. package/src/components/details/README.mdx +437 -69
  168. package/src/components/details/details.scss +16 -7
  169. package/src/components/details/details.test.tsx +385 -0
  170. package/src/components/details/details.tsx +101 -69
  171. package/src/components/details/details.types.ts +76 -0
  172. package/src/components/dialog/README.mdx +513 -110
  173. package/src/components/dialog/dialog-modal.tsx +79 -56
  174. package/src/components/dialog/dialog.scss +53 -3
  175. package/src/components/dialog/dialog.stories.tsx +10 -7
  176. package/src/components/dialog/dialog.test.tsx +450 -0
  177. package/src/components/dialog/dialog.tsx +69 -59
  178. package/src/components/dialog/dialog.types.ts +133 -0
  179. package/src/components/dialog/views/dialog-footer.tsx +54 -11
  180. package/src/components/dialog/views/dialog-header.tsx +20 -15
  181. package/src/components/heading/heading.stories.tsx +44 -4
  182. package/src/components/heading/heading.tsx +89 -23
  183. package/src/components/icons/README.mdx +332 -0
  184. package/src/components/icons/icon.stories.tsx +74 -1
  185. package/src/components/icons/icon.tsx +89 -1
  186. package/src/components/icons/types.ts +47 -0
  187. package/src/components/images/README.mdx +340 -24
  188. package/src/components/images/img.scss +19 -3
  189. package/src/components/images/img.stories.tsx +424 -15
  190. package/src/components/images/img.test.tsx +354 -25
  191. package/src/components/images/img.tsx +186 -63
  192. package/src/components/images/img.types.ts +211 -0
  193. package/src/components/title/MIGRATION.md +199 -0
  194. package/src/components/title/README.md +326 -0
  195. package/src/components/title/README.mdx +452 -0
  196. package/src/components/title/title.stories.tsx +393 -0
  197. package/src/components/title/title.test.tsx +251 -0
  198. package/src/components/title/title.tsx +219 -0
  199. package/src/components/ui.stories.tsx +894 -0
  200. package/src/components/ui.test.tsx +559 -0
  201. package/src/components/ui.tsx +266 -15
  202. package/src/components/word-count/README.md +240 -0
  203. package/src/hooks.ts +1 -0
  204. package/src/index.ts +10 -2
  205. package/src/sass/_properties.scss +1 -0
  206. package/src/styles/alert/alert.css +94 -4
  207. package/src/styles/alert/alert.css.map +1 -1
  208. package/src/styles/badge/badge.css +20 -2
  209. package/src/styles/badge/badge.css.map +1 -1
  210. package/src/styles/buttons/button.css +31 -31
  211. package/src/styles/buttons/button.css.map +1 -1
  212. package/src/styles/cards/card.css +16 -0
  213. package/src/styles/cards/card.css.map +1 -1
  214. package/src/styles/details/details.css +19 -8
  215. package/src/styles/details/details.css.map +1 -1
  216. package/src/styles/dialog/dialog.css +43 -2
  217. package/src/styles/dialog/dialog.css.map +1 -1
  218. package/src/styles/images/img.css +15 -3
  219. package/src/styles/images/img.css.map +1 -1
  220. package/src/styles/index.css +240 -51
  221. package/src/styles/index.css.map +1 -1
  222. package/src/test/setup.d.ts +9 -0
  223. package/src/test/setup.ts +53 -1
  224. package/libs/chunk-6TE5QEVE.cjs +0 -13
  225. package/libs/chunk-6TE5QEVE.cjs.map +0 -1
  226. package/libs/chunk-7K76RW2A.cjs +0 -18
  227. package/libs/chunk-7K76RW2A.cjs.map +0 -1
  228. package/libs/chunk-BSPKFLO4.js +0 -8
  229. package/libs/chunk-BSPKFLO4.js.map +0 -1
  230. package/libs/chunk-BV5CLH44.cjs +0 -18
  231. package/libs/chunk-BV5CLH44.cjs.map +0 -1
  232. package/libs/chunk-DKGJHKGW.js +0 -9
  233. package/libs/chunk-DKGJHKGW.js.map +0 -1
  234. package/libs/chunk-ECLD37WN.cjs +0 -16
  235. package/libs/chunk-ECLD37WN.cjs.map +0 -1
  236. package/libs/chunk-HYBZBN4G.js +0 -8
  237. package/libs/chunk-HYBZBN4G.js.map +0 -1
  238. package/libs/chunk-KKLTUJFB.cjs.map +0 -1
  239. package/libs/chunk-M5QL5TAE.cjs +0 -14
  240. package/libs/chunk-M5QL5TAE.cjs.map +0 -1
  241. package/libs/chunk-NE6YXTMC.js +0 -7
  242. package/libs/chunk-NE6YXTMC.js.map +0 -1
  243. package/libs/chunk-O6QZBB6G.js.map +0 -1
  244. package/libs/chunk-SXVZSWX6.js +0 -11
  245. package/libs/chunk-SXVZSWX6.js.map +0 -1
  246. package/libs/ui-9a6f9f8d.d.ts +0 -24
  247. package/src/components/cards/README.md +0 -80
  248. package/src/components/dialog/hooks/useClickOutside.ts +0 -33
  249. /package/libs/{chunk-DV56L5YX.cjs.map → chunk-2LTJ7HHX.cjs.map} +0 -0
  250. /package/libs/{chunk-EQ67LF46.js.map → chunk-2Y7W75TT.js.map} +0 -0
  251. /package/libs/{chunk-X3EVB7VS.cjs.map → chunk-5S4ORA4C.cjs.map} +0 -0
  252. /package/libs/{chunk-6BVXFW7U.cjs.map → chunk-AHDJGCG5.cjs.map} +0 -0
  253. /package/libs/{chunk-E3XP6BEX.cjs.map → chunk-B7F5FS6D.cjs.map} +0 -0
  254. /package/libs/{chunk-LHVJKDMA.cjs.map → chunk-J32EZPYD.cjs.map} +0 -0
  255. /package/libs/{chunk-LL7HTLMS.cjs.map → chunk-M5RRNTVX.cjs.map} +0 -0
  256. /package/libs/{chunk-LIQJ7ZZR.js.map → chunk-NGTJDDFO.js.map} +0 -0
  257. /package/libs/{chunk-QCMV4VQZ.js.map → chunk-QLZWHAMK.js.map} +0 -0
  258. /package/libs/{chunk-BIP2NY53.js.map → chunk-RIVUMPOG.js.map} +0 -0
  259. /package/libs/{chunk-ICCKQ2GC.cjs.map → chunk-ROZI23GS.cjs.map} +0 -0
  260. /package/libs/{chunk-NHYXGV3L.js.map → chunk-SMYRLO3E.js.map} +0 -0
  261. /package/libs/{chunk-5ZM4XL44.js.map → chunk-TYRCEX2L.js.map} +0 -0
  262. /package/libs/{chunk-PPOOBUOS.js.map → chunk-XBA562WW.js.map} +0 -0
  263. /package/libs/{chunk-QVV34QEH.cjs.map → chunk-XTQKWY7W.cjs.map} +0 -0
  264. /package/libs/{chunk-YWOYVRFT.js.map → chunk-ZANSFMTD.js.map} +0 -0
@@ -1,12 +1,18 @@
1
1
  import React from "react";
2
- import UI from "#components/ui";
3
- import Icon from "#components/icons/icon";
4
2
  import { IconProps } from "#components/icons/types";
5
- import DismissButton from "./elements/dismiss-button";
3
+ import { AlertView } from "./views";
6
4
 
7
- // First, define a type for the valid severity values
5
+ // ============================================================================
6
+ // TYPES & CONFIGURATION
7
+ // ============================================================================
8
+
9
+ /**
10
+ * Valid severity levels for alerts.
11
+ * Each severity has associated colors, icons, and ARIA attributes.
12
+ */
8
13
  type Severity = "default" | "info" | "success" | "warning" | "error";
9
14
 
15
+
10
16
  /**
11
17
  * Props for the Alert component.
12
18
  */
@@ -39,34 +45,158 @@ export type AlertProps = {
39
45
  onDismiss?: () => void;
40
46
 
41
47
  /**
42
- * Size of the icon in pixels.
43
- * @default 32
48
+ * Size of the severity icon in pixels.
49
+ * Allows customization of icon size for different contexts.
50
+ * @default 24
51
+ * @example
52
+ * ```tsx
53
+ * <Alert iconSize={32} severity="error">Larger icon</Alert>
54
+ * <Alert iconSize={16} severity="info">Smaller icon</Alert>
55
+ * ```
44
56
  */
45
57
  iconSize?: number;
46
58
 
47
59
  /**
48
- * Whether to hide the icon.
60
+ * Whether to hide the severity icon.
61
+ * When true, only text content is displayed.
62
+ * @default false
49
63
  */
50
64
  hideIcon?: boolean;
51
65
 
52
66
  /**
53
67
  * Additional props to pass to the Icon component.
68
+ * Allows fine-grained control over icon appearance.
69
+ * @example
70
+ * ```tsx
71
+ * <Alert iconProps={{ className: 'custom-icon', 'aria-label': 'Custom' }}>
72
+ * Alert with custom icon props
73
+ * </Alert>
74
+ * ```
54
75
  */
55
76
  iconProps?: IconProps;
56
- } & React.ComponentProps<typeof UI>;
77
+
78
+ /**
79
+ * Duration in milliseconds before the alert automatically dismisses.
80
+ * Set to 0 or undefined to disable auto-dismiss.
81
+ * @default undefined
82
+ * @example
83
+ * ```tsx
84
+ * <Alert autoHideDuration={5000}>Success message</Alert>
85
+ * ```
86
+ */
87
+ autoHideDuration?: number;
88
+
89
+ /**
90
+ * Whether to pause auto-dismiss when the alert is hovered or focused.
91
+ * Complies with WCAG 2.2.1 (Timing Adjustable).
92
+ * @default true
93
+ */
94
+ pauseOnHover?: boolean;
95
+
96
+ /**
97
+ * Semantic heading level for the title (2-6).
98
+ * When undefined, uses <strong> element instead of heading.
99
+ * Use this to maintain proper heading hierarchy on the page.
100
+ * @default undefined
101
+ * @example
102
+ * ```tsx
103
+ * <Alert titleLevel={2} title="Section Alert">...</Alert>
104
+ * <Alert titleLevel={3} title="Subsection Alert">...</Alert>
105
+ * ```
106
+ */
107
+ titleLevel?: 2 | 3 | 4 | 5 | 6;
108
+
109
+ /**
110
+ * Custom action buttons to display in the alert.
111
+ * @example
112
+ * ```tsx
113
+ * <Alert actions={<><Button>Undo</Button><Button>Dismiss</Button></>}>
114
+ * File deleted
115
+ * </Alert>
116
+ * ```
117
+ */
118
+ actions?: React.ReactNode;
119
+
120
+ /**
121
+ * Whether to automatically focus the alert when it becomes visible.
122
+ * Useful for critical alerts that require immediate attention.
123
+ * @default false
124
+ */
125
+ autoFocus?: boolean;
126
+
127
+ /**
128
+ * Visual variant of the alert.
129
+ * - outlined: Border with lighter background (default)
130
+ * - filled: Solid colored background
131
+ * - soft: No border, subtle background
132
+ * @default "outlined"
133
+ */
134
+ variant?: "outlined" | "filled" | "soft";
135
+
136
+ /**
137
+ * Content rendering mode for alert children.
138
+ * Determines how the children content is wrapped in the alert message area.
139
+ * - "text": Wraps children in a paragraph tag (default, for simple text content)
140
+ * - "node": Renders children directly without wrapper (for complex layouts, lists, or custom components)
141
+ * @default "text"
142
+ * @example Simple text content (uses default "text" mode)
143
+ * ```tsx
144
+ * <Alert severity="info">
145
+ * This is a simple text message that will be wrapped in a paragraph.
146
+ * </Alert>
147
+ * ```
148
+ * @example Complex content with list
149
+ * ```tsx
150
+ * <Alert severity="warning" contentType="node">
151
+ * <p>Please review the following items:</p>
152
+ * <ul>
153
+ * <li>Check your email settings</li>
154
+ * <li>Update your password</li>
155
+ * <li>Enable two-factor authentication</li>
156
+ * </ul>
157
+ * </Alert>
158
+ * ```
159
+ * @example Custom component layout
160
+ * ```tsx
161
+ * <Alert severity="success" contentType="node">
162
+ * <div className="custom-layout">
163
+ * <p>Operation completed successfully!</p>
164
+ * <div className="stats">
165
+ * <span>Items processed: 150</span>
166
+ * <span>Time elapsed: 2.5s</span>
167
+ * </div>
168
+ * </div>
169
+ * </Alert>
170
+ * ```
171
+ */
172
+ contentType?: "text" | "node";
173
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, 'title' | 'children'>;
57
174
 
58
175
  /**
59
- * Alert is a customizable component for displaying status messages with different severity levels.
176
+ * Alert is a fully accessible component for displaying status messages with different severity levels.
60
177
  * It supports multiple severity types (default, info, success, warning, error) and can include
61
178
  * optional titles and dismissal functionality.
62
179
  *
63
180
  * Features:
64
181
  * - Multiple severity levels with matching visual indicators
65
- * - Optional title and dismissible states
182
+ * - Three variants: outlined (default), filled, and soft
183
+ * - Auto-dismiss with configurable duration and pause on hover/focus
184
+ * - Optional title with configurable heading levels (h2-h6) for proper document structure
185
+ * - Action buttons support for interactive alerts
66
186
  * - Accessible by default with appropriate ARIA attributes
67
- * - Customizable icons through iconProps
187
+ * - Screen reader announcements for severity levels
188
+ * - Keyboard support (ESC to dismiss)
189
+ * - WCAG 2.1 Level AA compliant
190
+ *
191
+ * Accessibility Features:
192
+ * - Visually hidden severity text for screen readers (WCAG 1.1.1, 1.4.1)
193
+ * - Configurable heading levels to maintain document hierarchy (WCAG 1.3.1)
194
+ * - Visible focus indicators for keyboard navigation (WCAG 2.4.7)
195
+ * - Adequate color contrast for all variants (WCAG 1.4.3)
196
+ * - 44×44px minimum touch target size for dismiss button (WCAG 2.5.5)
197
+ * - Pause auto-dismiss on hover/focus (WCAG 2.2.1)
68
198
  *
69
- * @example
199
+ * @example Basic Usage
70
200
  * ```tsx
71
201
  * <Alert
72
202
  * open={true}
@@ -79,9 +209,150 @@ export type AlertProps = {
79
209
  * </Alert>
80
210
  * ```
81
211
  *
212
+ * @example With Heading Level (for proper document structure)
213
+ * ```tsx
214
+ * <Alert
215
+ * open={true}
216
+ * severity="error"
217
+ * title="Error Title"
218
+ * titleLevel={2}
219
+ * dismissible={true}
220
+ * >
221
+ * Error message
222
+ * </Alert>
223
+ * ```
224
+ *
225
+ * @example Auto-dismiss with Pause on Hover
226
+ * ```tsx
227
+ * <Alert
228
+ * open={true}
229
+ * severity="success"
230
+ * autoHideDuration={5000}
231
+ * pauseOnHover={true}
232
+ * >
233
+ * This will auto-dismiss in 5 seconds, but pauses when hovered
234
+ * </Alert>
235
+ * ```
236
+ *
82
237
  * @see {@link AlertProps} for available configuration options
83
238
  */
84
239
 
240
+ // ============================================================================
241
+ // CUSTOM HOOK (Behavior Management)
242
+ // ============================================================================
243
+
244
+ /**
245
+ * Custom hook that manages all Alert component behavior in one cohesive unit.
246
+ * Consolidates visibility, auto-dismiss, keyboard, and focus management.
247
+ *
248
+ * @param open - Whether the alert should be open
249
+ * @param onDismiss - Callback when alert is dismissed
250
+ * @param dismissible - Whether the alert can be dismissed
251
+ * @param autoHideDuration - Duration before auto-dismiss (ms)
252
+ * @param pauseOnHover - Whether to pause auto-dismiss on hover/focus
253
+ * @param autoFocus - Whether to auto-focus the alert
254
+ * @param alertRef - Ref to the alert DOM element
255
+ * @returns Object with visibility state, handlers, and event callbacks
256
+ */
257
+ const useAlertBehavior = ({
258
+ open,
259
+ onDismiss,
260
+ dismissible,
261
+ autoHideDuration,
262
+ pauseOnHover,
263
+ autoFocus,
264
+ alertRef,
265
+ }: {
266
+ open: boolean;
267
+ onDismiss?: () => void;
268
+ dismissible: boolean;
269
+ autoHideDuration?: number;
270
+ pauseOnHover: boolean;
271
+ autoFocus: boolean;
272
+ alertRef: React.RefObject<HTMLDivElement>;
273
+ }) => {
274
+ const [isVisible, setIsVisible] = React.useState(open);
275
+ const [shouldRender, setShouldRender] = React.useState(open);
276
+ const [isPaused, setIsPaused] = React.useState(false);
277
+
278
+ // Dismiss handler with animation timing
279
+ const handleDismiss = React.useCallback((): void => {
280
+ setIsVisible(false);
281
+ // Wait for animation to complete before unmounting
282
+ setTimeout(() => {
283
+ setShouldRender(false);
284
+ onDismiss?.();
285
+ }, 300); // Match CSS transition duration
286
+ }, [onDismiss]);
287
+
288
+ // Visibility management - sync with open prop
289
+ React.useEffect(() => {
290
+ if (open) {
291
+ setShouldRender(true);
292
+ setIsVisible(true);
293
+ } else {
294
+ setIsVisible(false);
295
+ }
296
+ }, [open]);
297
+
298
+ // Auto-dismiss timer with pause support
299
+ React.useEffect(() => {
300
+ if (!autoHideDuration || !isVisible || isPaused) return;
301
+
302
+ const timer = setTimeout(() => {
303
+ handleDismiss();
304
+ }, autoHideDuration);
305
+
306
+ return () => clearTimeout(timer);
307
+ }, [autoHideDuration, isVisible, isPaused, handleDismiss]);
308
+
309
+ // ESC key support for dismissible alerts
310
+ React.useEffect(() => {
311
+ if (!dismissible || !isVisible) return;
312
+
313
+ const handleKeyDown = (e: KeyboardEvent) => {
314
+ if (e.key === "Escape") {
315
+ handleDismiss();
316
+ }
317
+ };
318
+
319
+ document.addEventListener("keydown", handleKeyDown);
320
+ return () => document.removeEventListener("keydown", handleKeyDown);
321
+ }, [dismissible, isVisible, handleDismiss]);
322
+
323
+ // Auto-focus for critical alerts
324
+ React.useEffect(() => {
325
+ if (autoFocus && isVisible && alertRef.current) {
326
+ alertRef.current.focus();
327
+ }
328
+ }, [autoFocus, isVisible, alertRef]);
329
+
330
+ // Pause/resume handlers (memoized for stable references)
331
+ const pause = React.useCallback(() => {
332
+ if (pauseOnHover && autoHideDuration) {
333
+ setIsPaused(true);
334
+ }
335
+ }, [pauseOnHover, autoHideDuration]);
336
+
337
+ const resume = React.useCallback(() => {
338
+ if (pauseOnHover && autoHideDuration) {
339
+ setIsPaused(false);
340
+ }
341
+ }, [pauseOnHover, autoHideDuration]);
342
+
343
+ return {
344
+ isVisible,
345
+ shouldRender,
346
+ handleDismiss,
347
+ handleInteractionStart: pause,
348
+ handleInteractionEnd: resume,
349
+ };
350
+ };
351
+
352
+ // ============================================================================
353
+ // MAIN COMPONENT
354
+ // ============================================================================
355
+
85
356
  const Alert: React.FC<AlertProps> = ({
86
357
  open,
87
358
  severity = "default",
@@ -92,70 +363,64 @@ const Alert: React.FC<AlertProps> = ({
92
363
  iconSize,
93
364
  iconProps,
94
365
  hideIcon,
366
+ autoHideDuration,
367
+ pauseOnHover = true,
368
+ titleLevel = 3,
369
+ actions,
370
+ autoFocus = false,
371
+ variant = "outlined",
372
+ contentType = "text",
95
373
  ...props
96
374
  }) => {
97
- const [isVisible, setIsVisible] = React.useState(open);
98
-
99
- React.useEffect(() => {
100
- setIsVisible(open);
101
- }, [open]);
102
-
103
- if (!isVisible) return null;
375
+ const alertRef = React.useRef<HTMLDivElement>(null);
104
376
 
105
- const handleDismiss = (): void => {
106
- setIsVisible(false);
107
- onDismiss?.();
108
- };
377
+ // Use consolidated behavior hook
378
+ const {
379
+ isVisible,
380
+ shouldRender,
381
+ handleDismiss,
382
+ handleInteractionStart,
383
+ handleInteractionEnd,
384
+ } = useAlertBehavior({
385
+ open,
386
+ onDismiss,
387
+ dismissible,
388
+ autoHideDuration,
389
+ pauseOnHover,
390
+ autoFocus,
391
+ alertRef,
392
+ });
109
393
 
110
- /**
111
- * Default props for the Icon component used in the Alert component.
112
- * These props set the fill color to white and the size to 32 pixels.
113
- */
114
- const defaultIconProps: IconProps = {
115
- size: iconSize || 32,
116
- };
394
+ // Early return if component shouldn't render
395
+ if (!shouldRender) return null;
117
396
 
118
- // Update the severityType object with the type
119
- const severityType: Record<Severity, "polite" | "assertive"> = {
120
- default: "polite",
121
- info: "polite",
122
- success: "polite",
123
- warning: "polite",
124
- error: "assertive",
125
- } as const;
126
-
127
- const mergedIconProps = { ...defaultIconProps, ...iconProps };
128
-
129
- // Update the severityIcons object with the type
130
- const severityIcons: Record<Severity, JSX.Element> = {
131
- info: <Icon.InfoSolid {...mergedIconProps} />,
132
- success: <Icon.SuccessSolid {...mergedIconProps} />,
133
- warning: <Icon.WarnSolid {...mergedIconProps} />,
134
- error: <Icon.AlertSolid {...mergedIconProps} />,
135
- default: <Icon.AlertSquareSolid {...mergedIconProps} />,
397
+ // Merge icon props with defaults
398
+ const mergedIconProps: IconProps = {
399
+ size: iconSize || 16,
400
+ ...iconProps,
136
401
  };
137
402
 
138
403
  return (
139
- <UI
140
- as="div"
141
- role="alert"
142
- aria-live={severityType[severity]}
143
- aria-atomic="true"
144
- className={`alert alert-${severity}`}
145
- data-alert={severity}
404
+ <AlertView
405
+ ref={alertRef}
406
+ severity={severity}
407
+ variant={variant}
408
+ isVisible={isVisible}
409
+ dismissible={dismissible}
410
+ onDismiss={handleDismiss}
411
+ onInteractionStart={handleInteractionStart}
412
+ onInteractionEnd={handleInteractionEnd}
413
+ autoFocus={autoFocus}
414
+ title={title}
415
+ titleLevel={titleLevel}
416
+ contentType={contentType}
417
+ actions={actions}
418
+ hideIcon={hideIcon}
419
+ iconProps={mergedIconProps}
146
420
  {...props}
147
421
  >
148
- {!hideIcon && <UI aria-hidden="true">{severityIcons[severity]}</UI>}
149
- <UI as="div" className="alert-message">
150
- {title && (
151
- <UI as="h3" className="alert-title">
152
- {title}
153
- </UI>
154
- )}
155
- <UI as="div">{children}</UI>
156
- </UI>
157
- {dismissible && <DismissButton onDismiss={handleDismiss} />}
158
- </UI>
422
+ {children}
423
+ </AlertView>
159
424
  );
160
425
  };
161
426
  export default Alert;
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import UI from "#components/ui";
3
+
4
+ /**
5
+ * Renders optional action buttons within the alert.
6
+ *
7
+ * @param actions - Action button elements to display
8
+ * @returns Actions container, or null if no actions provided
9
+ */
10
+ export const AlertActions: React.FC<{ actions?: React.ReactNode }> = ({ actions }) => {
11
+ if (!actions) return null;
12
+ return <UI as="div" className="alert-actions">{actions}</UI>;
13
+ };
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import UI from "#components/ui";
3
+
4
+ /**
5
+ * Renders the alert message content.
6
+ * Supports both simple text (wrapped in <p>) and complex node structures.
7
+ *
8
+ * @param children - The content to render
9
+ * @param contentType - How to wrap the content ("text" or "node")
10
+ * @returns Wrapped content
11
+ */
12
+ export const AlertContent: React.FC<{
13
+ children: React.ReactNode;
14
+ contentType: "text" | "node";
15
+ }> = ({ children, contentType }) => {
16
+ return contentType === "node" ? <>{children}</> : <UI as="p">{children}</UI>;
17
+ };
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import UI from "#components/ui";
3
+ import Icon from "#components/icons/icon";
4
+ import { IconProps } from "#components/icons/types";
5
+
6
+ /**
7
+ * Valid severity levels for alerts.
8
+ */
9
+ type Severity = "default" | "info" | "success" | "warning" | "error";
10
+
11
+ /**
12
+ * Pure function to get the appropriate icon for a severity level.
13
+ *
14
+ * @param severity - The alert severity level
15
+ * @param iconProps - Props to pass to the Icon component
16
+ * @returns The icon element for the severity
17
+ */
18
+ const getSeverityIcon = (
19
+ severity: Severity,
20
+ iconProps: IconProps
21
+ ): JSX.Element => {
22
+ const severityIcons: Record<Severity, JSX.Element> = {
23
+ info: <Icon.InfoSolid {...iconProps} />,
24
+ success: <Icon.SuccessSolid {...iconProps} />,
25
+ warning: <Icon.WarnSolid {...iconProps} />,
26
+ error: <Icon.AlertSolid {...iconProps} />,
27
+ default: <Icon.AlertSquareSolid {...iconProps} />,
28
+ };
29
+ return severityIcons[severity];
30
+ };
31
+
32
+ /**
33
+ * Renders the severity icon with proper ARIA attributes.
34
+ * Icon is hidden from screen readers as the text provides context.
35
+ *
36
+ * @param severity - The alert severity level
37
+ * @param iconProps - Props to pass to the Icon component
38
+ * @param hideIcon - Whether to hide the icon
39
+ * @returns Icon element, or null if hidden
40
+ */
41
+ export const AlertIcon: React.FC<{
42
+ severity: Severity;
43
+ iconProps: IconProps;
44
+ hideIcon?: boolean;
45
+ }> = ({ severity, iconProps, hideIcon }) => {
46
+ if (hideIcon) return null;
47
+ const icon = getSeverityIcon(severity, iconProps);
48
+ return (
49
+ <UI aria-hidden="true" className="alert-icon">
50
+ {icon}
51
+ </UI>
52
+ );
53
+ };
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Valid severity levels for alerts.
5
+ */
6
+ type Severity = "default" | "info" | "success" | "warning" | "error";
7
+
8
+ /**
9
+ * Screen reader announcement text for each severity level.
10
+ */
11
+ const SEVERITY_SCREEN_READER_TEXT: Record<Severity, string> = {
12
+ default: "",
13
+ info: "Information: ",
14
+ success: "Success: ",
15
+ warning: "Warning: ",
16
+ error: "Error: ",
17
+ } as const;
18
+
19
+ /**
20
+ * Renders visually hidden severity text for screen readers.
21
+ * Provides context about the alert type without visual clutter.
22
+ *
23
+ * @param severity - The alert severity level
24
+ * @returns Hidden text for screen readers, or null if no text needed
25
+ */
26
+ export const AlertScreenReaderText: React.FC<{ severity: Severity }> = ({ severity }) => {
27
+ const text = SEVERITY_SCREEN_READER_TEXT[severity];
28
+ if (!text) return null;
29
+ return <span className="sr-only">{text}</span>;
30
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import UI from "#components/ui";
3
+
4
+ /**
5
+ * Renders the alert title with configurable heading level.
6
+ * Uses semantic heading (h2-h6) or strong element based on titleLevel prop.
7
+ *
8
+ * @param title - The title text to display
9
+ * @param titleLevel - Semantic heading level (2-6)
10
+ * @returns Title element, or null if no title provided
11
+ */
12
+ export const AlertTitle: React.FC<{
13
+ title?: string;
14
+ titleLevel: 2 | 3 | 4 | 5 | 6;
15
+ }> = ({ title, titleLevel }) => {
16
+ if (!title) return null;
17
+ const TitleElement = titleLevel ? (`h${titleLevel}` as const) : "strong";
18
+ return (
19
+ <UI as={TitleElement} className="alert-title">
20
+ {title}
21
+ </UI>
22
+ );
23
+ };