@fpkit/acss 0.5.11 → 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 (312) hide show
  1. package/README.md +514 -18
  2. package/libs/chunk-23ANBDCR.js +8 -0
  3. package/libs/chunk-23ANBDCR.js.map +1 -0
  4. package/libs/chunk-2LTJ7HHX.cjs +18 -0
  5. package/libs/chunk-2LTJ7HHX.cjs.map +1 -0
  6. package/libs/chunk-2Y7W75TT.js +9 -0
  7. package/libs/chunk-2Y7W75TT.js.map +1 -0
  8. package/libs/chunk-3MKLDCKQ.cjs +31 -0
  9. package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
  10. package/libs/chunk-5M57K4SW.js +8 -0
  11. package/libs/chunk-5M57K4SW.js.map +1 -0
  12. package/libs/chunk-5S4ORA4C.cjs +15 -0
  13. package/libs/chunk-5S4ORA4C.cjs.map +1 -0
  14. package/libs/chunk-772NRB75.js +9 -0
  15. package/libs/chunk-772NRB75.js.map +1 -0
  16. package/libs/chunk-AHDJGCG5.cjs +15 -0
  17. package/libs/chunk-AHDJGCG5.cjs.map +1 -0
  18. package/libs/chunk-B7F5FS6D.cjs +16 -0
  19. package/libs/chunk-B7F5FS6D.cjs.map +1 -0
  20. package/libs/chunk-BHRQBJRY.js +8 -0
  21. package/libs/chunk-BHRQBJRY.js.map +1 -0
  22. package/libs/chunk-D4YLRWAO.cjs +18 -0
  23. package/libs/chunk-D4YLRWAO.cjs.map +1 -0
  24. package/libs/chunk-ETFLFC2S.js +10 -0
  25. package/libs/chunk-ETFLFC2S.js.map +1 -0
  26. package/libs/chunk-G55UJ53G.cjs +16 -0
  27. package/libs/chunk-G55UJ53G.cjs.map +1 -0
  28. package/libs/chunk-GZ4QFPRY.js +9 -0
  29. package/libs/chunk-GZ4QFPRY.js.map +1 -0
  30. package/libs/chunk-IYUN2EW3.cjs +15 -0
  31. package/libs/chunk-IYUN2EW3.cjs.map +1 -0
  32. package/libs/chunk-J32EZPYD.cjs +15 -0
  33. package/libs/chunk-J32EZPYD.cjs.map +1 -0
  34. package/libs/chunk-JJ43O4Y5.js +8 -0
  35. package/libs/chunk-JJ43O4Y5.js.map +1 -0
  36. package/libs/chunk-KUKIVRC2.js +7 -0
  37. package/libs/chunk-KUKIVRC2.js.map +1 -0
  38. package/libs/chunk-L75OQKEI.cjs +13 -0
  39. package/libs/chunk-L75OQKEI.cjs.map +1 -0
  40. package/libs/chunk-LT5KZ2QW.cjs +22 -0
  41. package/libs/chunk-LT5KZ2QW.cjs.map +1 -0
  42. package/libs/chunk-M5RRNTVX.cjs +15 -0
  43. package/libs/chunk-M5RRNTVX.cjs.map +1 -0
  44. package/libs/chunk-NGTJDDFO.js +8 -0
  45. package/libs/chunk-NGTJDDFO.js.map +1 -0
  46. package/libs/chunk-OK5QEIMD.cjs +17 -0
  47. package/libs/chunk-OK5QEIMD.cjs.map +1 -0
  48. package/libs/chunk-P2DC76ZZ.cjs +18 -0
  49. package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
  50. package/libs/chunk-P7TTEYCD.js +7 -0
  51. package/libs/chunk-P7TTEYCD.js.map +1 -0
  52. package/libs/chunk-PQ2K3BM6.cjs +17 -0
  53. package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
  54. package/libs/chunk-QLZWHAMK.js +8 -0
  55. package/libs/chunk-QLZWHAMK.js.map +1 -0
  56. package/libs/chunk-RIVUMPOG.js +8 -0
  57. package/libs/chunk-RIVUMPOG.js.map +1 -0
  58. package/libs/chunk-ROZI23GS.cjs +15 -0
  59. package/libs/chunk-ROZI23GS.cjs.map +1 -0
  60. package/libs/chunk-S7BABR7Z.cjs +13 -0
  61. package/libs/chunk-S7BABR7Z.cjs.map +1 -0
  62. package/libs/chunk-SMYRLO3E.js +8 -0
  63. package/libs/chunk-SMYRLO3E.js.map +1 -0
  64. package/libs/chunk-TYRCEX2L.js +8 -0
  65. package/libs/chunk-TYRCEX2L.js.map +1 -0
  66. package/libs/chunk-VUH3FXGJ.js +11 -0
  67. package/libs/chunk-VUH3FXGJ.js.map +1 -0
  68. package/libs/chunk-XBA562WW.js +8 -0
  69. package/libs/chunk-XBA562WW.js.map +1 -0
  70. package/libs/chunk-XTQKWY7W.cjs +32 -0
  71. package/libs/chunk-XTQKWY7W.cjs.map +1 -0
  72. package/libs/chunk-ZANSFMTD.js +9 -0
  73. package/libs/chunk-ZANSFMTD.js.map +1 -0
  74. package/libs/component-props-a8a2f97e.d.ts +38 -0
  75. package/libs/components/alert/alert.css +1 -1
  76. package/libs/components/alert/alert.css.map +1 -1
  77. package/libs/components/alert/alert.min.css +2 -2
  78. package/libs/components/badge/badge.css +1 -1
  79. package/libs/components/badge/badge.css.map +1 -1
  80. package/libs/components/badge/badge.min.css +2 -2
  81. package/libs/components/breadcrumbs/breadcrumb.cjs +24 -0
  82. package/libs/components/breadcrumbs/breadcrumb.cjs.map +1 -0
  83. package/libs/components/breadcrumbs/breadcrumb.d.cts +290 -0
  84. package/libs/components/breadcrumbs/breadcrumb.d.ts +290 -0
  85. package/libs/components/breadcrumbs/breadcrumb.js +5 -0
  86. package/libs/components/breadcrumbs/breadcrumb.js.map +1 -0
  87. package/libs/components/button.cjs +19 -0
  88. package/libs/components/button.cjs.map +1 -0
  89. package/libs/components/button.d.cts +16 -0
  90. package/libs/components/button.d.ts +16 -0
  91. package/libs/components/button.js +4 -0
  92. package/libs/components/button.js.map +1 -0
  93. package/libs/components/buttons/button.css +1 -1
  94. package/libs/components/buttons/button.css.map +1 -1
  95. package/libs/components/buttons/button.min.css +2 -2
  96. package/libs/components/card.cjs +31 -0
  97. package/libs/components/card.cjs.map +1 -0
  98. package/libs/components/card.d.cts +302 -0
  99. package/libs/components/card.d.ts +302 -0
  100. package/libs/components/card.js +4 -0
  101. package/libs/components/card.js.map +1 -0
  102. package/libs/components/cards/card.css +1 -1
  103. package/libs/components/cards/card.css.map +1 -1
  104. package/libs/components/cards/card.min.css +2 -2
  105. package/libs/components/details/details.css +1 -1
  106. package/libs/components/details/details.css.map +1 -1
  107. package/libs/components/details/details.min.css +2 -2
  108. package/libs/components/dialog/dialog.cjs +22 -0
  109. package/libs/components/dialog/dialog.cjs.map +1 -0
  110. package/libs/components/dialog/dialog.css +1 -1
  111. package/libs/components/dialog/dialog.css.map +1 -1
  112. package/libs/components/dialog/dialog.d.cts +105 -0
  113. package/libs/components/dialog/dialog.d.ts +105 -0
  114. package/libs/components/dialog/dialog.js +7 -0
  115. package/libs/components/dialog/dialog.js.map +1 -0
  116. package/libs/components/dialog/dialog.min.css +2 -2
  117. package/libs/components/form/fields.cjs +19 -0
  118. package/libs/components/form/fields.cjs.map +1 -0
  119. package/libs/components/form/fields.d.cts +24 -0
  120. package/libs/components/form/fields.d.ts +24 -0
  121. package/libs/components/form/fields.js +4 -0
  122. package/libs/components/form/fields.js.map +1 -0
  123. package/libs/components/form/inputs.cjs +19 -0
  124. package/libs/components/form/inputs.cjs.map +1 -0
  125. package/libs/components/form/inputs.d.cts +2 -0
  126. package/libs/components/form/inputs.d.ts +2 -0
  127. package/libs/components/form/inputs.js +4 -0
  128. package/libs/components/form/inputs.js.map +1 -0
  129. package/libs/components/form/textarea.cjs +19 -0
  130. package/libs/components/form/textarea.cjs.map +1 -0
  131. package/libs/components/form/textarea.d.cts +29 -0
  132. package/libs/components/form/textarea.d.ts +29 -0
  133. package/libs/components/form/textarea.js +4 -0
  134. package/libs/components/form/textarea.js.map +1 -0
  135. package/libs/components/heading/heading.cjs +10 -0
  136. package/libs/components/heading/heading.cjs.map +1 -0
  137. package/libs/components/heading/heading.d.cts +3 -0
  138. package/libs/components/heading/heading.d.ts +3 -0
  139. package/libs/components/heading/heading.js +4 -0
  140. package/libs/components/heading/heading.js.map +1 -0
  141. package/libs/components/icons/icon.cjs +19 -0
  142. package/libs/components/icons/icon.cjs.map +1 -0
  143. package/libs/{icons-31ace3de.d.ts → components/icons/icon.d.cts} +151 -61
  144. package/libs/components/icons/icon.d.ts +445 -0
  145. package/libs/components/icons/icon.js +4 -0
  146. package/libs/components/icons/icon.js.map +1 -0
  147. package/libs/components/images/img.css +1 -1
  148. package/libs/components/images/img.css.map +1 -1
  149. package/libs/components/images/img.min.css +2 -2
  150. package/libs/components/link/link.cjs +19 -0
  151. package/libs/components/link/link.cjs.map +1 -0
  152. package/libs/components/link/link.d.cts +19 -0
  153. package/libs/components/link/link.d.ts +19 -0
  154. package/libs/components/link/link.js +4 -0
  155. package/libs/components/link/link.js.map +1 -0
  156. package/libs/components/list/list.cjs +23 -0
  157. package/libs/components/list/list.cjs.map +1 -0
  158. package/libs/components/list/list.d.cts +39 -0
  159. package/libs/components/list/list.d.ts +39 -0
  160. package/libs/components/list/list.js +4 -0
  161. package/libs/components/list/list.js.map +1 -0
  162. package/libs/components/modal.cjs +14 -0
  163. package/libs/components/modal.cjs.map +1 -0
  164. package/libs/components/modal.d.cts +35 -0
  165. package/libs/components/modal.d.ts +35 -0
  166. package/libs/components/modal.js +5 -0
  167. package/libs/components/modal.js.map +1 -0
  168. package/libs/components/nav/nav.cjs +28 -0
  169. package/libs/components/nav/nav.cjs.map +1 -0
  170. package/libs/components/nav/nav.d.cts +44 -0
  171. package/libs/components/nav/nav.d.ts +44 -0
  172. package/libs/components/nav/nav.js +5 -0
  173. package/libs/components/nav/nav.js.map +1 -0
  174. package/libs/components/popover/popover.cjs +23 -0
  175. package/libs/components/popover/popover.cjs.map +1 -0
  176. package/libs/components/popover/popover.d.cts +40 -0
  177. package/libs/components/popover/popover.d.ts +40 -0
  178. package/libs/components/popover/popover.js +4 -0
  179. package/libs/components/popover/popover.js.map +1 -0
  180. package/libs/components/tables/table.cjs +21 -0
  181. package/libs/components/tables/table.cjs.map +1 -0
  182. package/libs/components/tables/table.d.cts +36 -0
  183. package/libs/components/tables/table.d.ts +36 -0
  184. package/libs/components/tables/table.js +4 -0
  185. package/libs/components/tables/table.js.map +1 -0
  186. package/libs/components/text/text.cjs +23 -0
  187. package/libs/components/text/text.cjs.map +1 -0
  188. package/libs/components/text/text.d.cts +30 -0
  189. package/libs/components/text/text.d.ts +30 -0
  190. package/libs/components/text/text.js +4 -0
  191. package/libs/components/text/text.js.map +1 -0
  192. package/libs/heading-3648c538.d.ts +250 -0
  193. package/libs/hooks.cjs +7 -0
  194. package/libs/hooks.d.cts +5 -0
  195. package/libs/hooks.d.ts +5 -0
  196. package/libs/hooks.js +3 -0
  197. package/libs/icons.cjs +3 -2
  198. package/libs/icons.d.cts +3 -1
  199. package/libs/icons.d.ts +3 -1
  200. package/libs/icons.js +2 -1
  201. package/libs/index.cjs +174 -62
  202. package/libs/index.cjs.map +1 -1
  203. package/libs/index.css +1 -1
  204. package/libs/index.css.map +1 -1
  205. package/libs/index.d.cts +529 -446
  206. package/libs/index.d.ts +529 -446
  207. package/libs/index.js +36 -7
  208. package/libs/index.js.map +1 -1
  209. package/libs/inputs-f3a216db.d.ts +45 -0
  210. package/libs/ui-645f95b5.d.ts +285 -0
  211. package/package.json +2 -2
  212. package/src/components/README-UI.mdx +416 -0
  213. package/src/components/alert/ACCESSIBILITY.md +319 -0
  214. package/src/components/alert/README.mdx +475 -19
  215. package/src/components/alert/alert.scss +113 -6
  216. package/src/components/alert/alert.stories.tsx +372 -0
  217. package/src/components/alert/alert.test.tsx +762 -0
  218. package/src/components/alert/alert.tsx +331 -66
  219. package/src/components/alert/views/alert-actions.tsx +13 -0
  220. package/src/components/alert/views/alert-content.tsx +17 -0
  221. package/src/components/alert/views/alert-icon.tsx +53 -0
  222. package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
  223. package/src/components/alert/views/alert-title.tsx +23 -0
  224. package/src/components/alert/views/alert-view.tsx +158 -0
  225. package/src/components/alert/views/index.ts +12 -0
  226. package/src/components/badge/badge.mdx +186 -49
  227. package/src/components/badge/badge.scss +20 -2
  228. package/src/components/badge/badge.stories.tsx +160 -14
  229. package/src/components/badge/badge.test.tsx +179 -0
  230. package/src/components/badge/badge.tsx +97 -4
  231. package/src/components/breadcrumbs/README.mdx +364 -45
  232. package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
  233. package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
  234. package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
  235. package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
  236. package/src/components/button.ts +2 -0
  237. package/src/components/buttons/button.scss +34 -31
  238. package/src/components/buttons/button.stories.tsx +35 -0
  239. package/src/components/card.ts +2 -0
  240. package/src/components/cards/README.mdx +657 -0
  241. package/src/components/cards/card.scss +22 -0
  242. package/src/components/cards/card.stories.tsx +167 -5
  243. package/src/components/cards/card.test.tsx +360 -20
  244. package/src/components/cards/card.tsx +200 -79
  245. package/src/components/cards/card.types.ts +135 -0
  246. package/src/components/cards/card.utils.ts +79 -0
  247. package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
  248. package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
  249. package/src/components/details/README.mdx +437 -69
  250. package/src/components/details/details.scss +16 -0
  251. package/src/components/details/details.test.tsx +385 -0
  252. package/src/components/details/details.tsx +101 -69
  253. package/src/components/details/details.types.ts +76 -0
  254. package/src/components/dialog/README.mdx +513 -110
  255. package/src/components/dialog/dialog-modal.tsx +79 -56
  256. package/src/components/dialog/dialog.scss +53 -3
  257. package/src/components/dialog/dialog.stories.tsx +10 -7
  258. package/src/components/dialog/dialog.test.tsx +450 -0
  259. package/src/components/dialog/dialog.tsx +69 -59
  260. package/src/components/dialog/dialog.types.ts +133 -0
  261. package/src/components/dialog/views/dialog-footer.tsx +54 -11
  262. package/src/components/dialog/views/dialog-header.tsx +20 -15
  263. package/src/components/heading/heading.stories.tsx +44 -4
  264. package/src/components/heading/heading.tsx +89 -23
  265. package/src/components/icons/README.mdx +332 -0
  266. package/src/components/icons/icon.stories.tsx +74 -1
  267. package/src/components/icons/icon.tsx +89 -1
  268. package/src/components/icons/types.ts +47 -0
  269. package/src/components/images/README.mdx +340 -24
  270. package/src/components/images/img.scss +19 -3
  271. package/src/components/images/img.stories.tsx +424 -15
  272. package/src/components/images/img.test.tsx +354 -25
  273. package/src/components/images/img.tsx +186 -63
  274. package/src/components/images/img.types.ts +211 -0
  275. package/src/components/modal.ts +1 -0
  276. package/src/components/title/MIGRATION.md +199 -0
  277. package/src/components/title/README.md +326 -0
  278. package/src/components/title/README.mdx +452 -0
  279. package/src/components/title/title.stories.tsx +393 -0
  280. package/src/components/title/title.test.tsx +251 -0
  281. package/src/components/title/title.tsx +219 -0
  282. package/src/components/ui.stories.tsx +894 -0
  283. package/src/components/ui.test.tsx +559 -0
  284. package/src/components/ui.tsx +266 -15
  285. package/src/components/word-count/README.md +240 -0
  286. package/src/hooks.ts +1 -0
  287. package/src/index.ts +51 -19
  288. package/src/sass/_properties.scss +1 -0
  289. package/src/styles/alert/alert.css +94 -4
  290. package/src/styles/alert/alert.css.map +1 -1
  291. package/src/styles/badge/badge.css +20 -2
  292. package/src/styles/badge/badge.css.map +1 -1
  293. package/src/styles/buttons/button.css +31 -31
  294. package/src/styles/buttons/button.css.map +1 -1
  295. package/src/styles/cards/card.css +16 -0
  296. package/src/styles/cards/card.css.map +1 -1
  297. package/src/styles/details/details.css +19 -0
  298. package/src/styles/details/details.css.map +1 -1
  299. package/src/styles/dialog/dialog.css +43 -2
  300. package/src/styles/dialog/dialog.css.map +1 -1
  301. package/src/styles/images/img.css +15 -3
  302. package/src/styles/images/img.css.map +1 -1
  303. package/src/styles/index.css +240 -43
  304. package/src/styles/index.css.map +1 -1
  305. package/src/test/setup.d.ts +9 -0
  306. package/src/test/setup.ts +53 -1
  307. package/libs/chunk-PWVRDQ3R.js +0 -8
  308. package/libs/chunk-PWVRDQ3R.js.map +0 -1
  309. package/libs/chunk-SVS4MX3U.cjs +0 -31
  310. package/libs/chunk-SVS4MX3U.cjs.map +0 -1
  311. package/src/components/cards/README.md +0 -80
  312. package/src/components/dialog/hooks/useClickOutside.ts +0 -33
@@ -2,50 +2,196 @@ import { StoryObj, Meta } from "@storybook/react-vite";
2
2
  import { within, expect } from "storybook/test";
3
3
 
4
4
  import Badge from "./badge";
5
+ import "./badge.scss";
5
6
 
6
7
  const meta: Meta<typeof Badge> = {
7
8
  title: "FP.REACT Components/Badge",
8
9
  component: Badge,
9
- tags: ["beta"],
10
+ tags: ["beta", "accessible"],
10
11
  args: {
11
- children: "Link",
12
+ children: "5",
12
13
  },
13
- } as Story;
14
+ } as Meta;
14
15
 
15
16
  export default meta;
16
17
  type Story = StoryObj<typeof Badge>;
17
18
 
18
- export const BadgeComponent: Story = {
19
- args: {},
19
+ /**
20
+ * Default badge story - demonstrates basic usage with notification count
21
+ */
22
+ export const Default: Story = {
23
+ args: {
24
+ children: "5",
25
+ "aria-label": "5 unread notifications",
26
+ },
20
27
  play: async ({ canvasElement }) => {
21
28
  const canvas = within(canvasElement);
22
- expect(canvas.getByText(/link/i)).toBeInTheDocument();
29
+ expect(canvas.getByText("5")).toBeInTheDocument();
30
+ expect(canvas.getByRole("status")).toHaveAttribute(
31
+ "aria-label",
32
+ "5 unread notifications"
33
+ );
23
34
  },
24
35
  };
25
36
 
26
- export const CustomBadge: Story = {
37
+ /**
38
+ * Badge within text content - shows typical usage alongside other content
39
+ */
40
+ export const WithinText: Story = {
41
+ render: () => {
42
+ return (
43
+ <p>
44
+ Messages
45
+ <Badge aria-label="3 unread messages">3</Badge>
46
+ </p>
47
+ );
48
+ },
49
+ play: async ({ canvasElement }) => {
50
+ const canvas = within(canvasElement);
51
+ expect(canvas.getByText(/messages/i)).toBeInTheDocument();
52
+ expect(canvas.getByText("3")).toBeInTheDocument();
53
+ },
54
+ } as Story;
55
+
56
+ /**
57
+ * Rounded variant badge - circular style for compact display
58
+ */
59
+ export const RoundedVariant: Story = {
27
60
  args: {
28
- children: "Custom",
61
+ variant: "rounded",
62
+ children: "99+",
63
+ "aria-label": "More than 99 notifications",
29
64
  },
30
- render: () => {
65
+ render: (args) => {
31
66
  return (
32
67
  <p>
33
- Custom
34
- <Badge aria-label="badge">21</Badge>
68
+ Notifications
69
+ <Badge {...args} />
35
70
  </p>
36
71
  );
37
72
  },
38
73
  play: async ({ canvasElement }) => {
39
74
  const canvas = within(canvasElement);
40
- expect(canvas.getByText(/custom/i)).toBeInTheDocument();
75
+ const badge = canvas.getByRole("status");
76
+ expect(badge).toHaveAttribute("data-badge", "rounded");
77
+ expect(canvas.getByText("99+")).toBeInTheDocument();
78
+ },
79
+ } as Story;
80
+
81
+ /**
82
+ * Multiple badges - demonstrates using multiple badges in the same context
83
+ */
84
+ export const MultipleBadges: Story = {
85
+ render: () => {
86
+ return (
87
+ <div style={{ display: "flex", gap: "1rem", flexDirection: "column" }}>
88
+ <p>
89
+ Inbox
90
+ <Badge variant="rounded" aria-label="12 unread emails">
91
+ 122
92
+ </Badge>
93
+ </p>
94
+ <p>
95
+ Inbox
96
+ <Badge variant="rounded" aria-label="12 unread emails">
97
+ 12
98
+ </Badge>
99
+ </p>
100
+ <p>
101
+ Tasks
102
+ <Badge variant="rounded" aria-label="5 pending tasks">
103
+ 5
104
+ </Badge>
105
+ </p>
106
+ <p>
107
+ Alerts
108
+ <Badge variant="rounded" aria-label="1 new alert">
109
+ 1
110
+ </Badge>
111
+ </p>
112
+ </div>
113
+ );
41
114
  },
42
115
  } as Story;
43
116
 
44
- export const RoundedBadge: Story = {
117
+ /**
118
+ * Rounded badges with fixed circular shape - demonstrates perfect circles with ellipsis truncation
119
+ * All rounded badges maintain a fixed size (1.5625rem), with content truncated using ellipsis if too long
120
+ */
121
+ export const RoundedCircles: Story = {
45
122
  render: () => {
123
+ return (
124
+ <div style={{ display: "flex", gap: "1.5rem", flexDirection: "column" }}>
125
+ <p>
126
+ Single digit (perfect circle)
127
+ <Badge variant="rounded" aria-label="1 notification">
128
+ 1
129
+ </Badge>
130
+ </p>
131
+ <p>
132
+ Single digit (perfect circle)
133
+ <Badge variant="rounded" aria-label="9 notifications">
134
+ 9
135
+ </Badge>
136
+ </p>
137
+ <p>
138
+ Double digits (perfect circle)
139
+ <Badge variant="rounded" aria-label="12 notifications">
140
+ 12
141
+ </Badge>
142
+ </p>
143
+ <p>
144
+ Double digits (perfect circle)
145
+ <Badge variant="rounded" aria-label="99 notifications">
146
+ 99
147
+ </Badge>
148
+ </p>
149
+ <p>
150
+ ✅ Good: Formatted by developer
151
+ <Badge variant="rounded" aria-label="More than 99 notifications">
152
+ 99+
153
+ </Badge>
154
+ </p>
155
+ <p>
156
+ ⚠️ Will truncate: Too many characters
157
+ <Badge variant="rounded" aria-label="More than 999 notifications">
158
+ 999+
159
+ </Badge>
160
+ </p>
161
+ <p>
162
+ ⚠️ Will truncate: Text content
163
+ <Badge variant="rounded" aria-label="New feature available">
164
+ NEW
165
+ </Badge>
166
+ </p>
167
+ <p>
168
+ ⚠️ Will truncate: Very long text
169
+ <Badge variant="rounded" aria-label="Multiple notifications">
170
+ Multiple
171
+ </Badge>
172
+ </p>
173
+ </div>
174
+ );
175
+ },
176
+ } as Story;
177
+
178
+ /**
179
+ * Custom styled badge - demonstrates custom styling with inline styles
180
+ */
181
+ export const CustomStyling: Story = {
182
+ args: {
183
+ children: "NEW",
184
+ "aria-label": "New feature",
185
+ styles: {
186
+ backgroundColor: "#10b981",
187
+ color: "white",
188
+ },
189
+ },
190
+ render: (args) => {
46
191
  return (
47
192
  <p>
48
- Custom<Badge data-badge="rounded">21</Badge>
193
+ Feature Release
194
+ <Badge {...args} />
49
195
  </p>
50
196
  );
51
197
  },
@@ -0,0 +1,179 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { Badge } from "./badge";
4
+
5
+ describe("Badge", () => {
6
+ it("renders a badge element with the correct content", () => {
7
+ render(<Badge aria-label="5 notifications">5</Badge>);
8
+ const badge = screen.getByText("5");
9
+ expect(badge).toBeInTheDocument();
10
+ });
11
+
12
+ it("renders as a sup element by default", () => {
13
+ render(<Badge aria-label="notification badge">3</Badge>);
14
+ const badge = screen.getByRole("status");
15
+ expect(badge.tagName).toBe("SUP");
16
+ });
17
+
18
+ it("has role='status' by default for accessibility", () => {
19
+ render(<Badge aria-label="badge">12</Badge>);
20
+ const badge = screen.getByRole("status");
21
+ expect(badge).toHaveAttribute("role", "status");
22
+ });
23
+
24
+ it("applies aria-label for screen reader accessibility", () => {
25
+ render(<Badge aria-label="3 unread messages">3</Badge>);
26
+ const badge = screen.getByRole("status");
27
+ expect(badge).toHaveAttribute("aria-label", "3 unread messages");
28
+ });
29
+
30
+ it("renders with nested span element for styling", () => {
31
+ render(<Badge aria-label="badge">5</Badge>);
32
+ const badge = screen.getByRole("status");
33
+ const span = badge.querySelector("span");
34
+ expect(span).toBeInTheDocument();
35
+ expect(span?.textContent).toBe("5");
36
+ });
37
+
38
+ it("applies custom id when provided", () => {
39
+ render(
40
+ <Badge id="notification-badge" aria-label="notification">
41
+ 10
42
+ </Badge>
43
+ );
44
+ const badge = screen.getByRole("status");
45
+ expect(badge).toHaveAttribute("id", "notification-badge");
46
+ });
47
+
48
+ it("applies custom className when provided via classes prop", () => {
49
+ render(
50
+ <Badge classes="custom-badge-class" aria-label="badge">
51
+ 7
52
+ </Badge>
53
+ );
54
+ const badge = screen.getByRole("status");
55
+ expect(badge).toHaveClass("custom-badge-class");
56
+ });
57
+
58
+ it("applies custom inline styles when provided", () => {
59
+ render(
60
+ <Badge styles={{ backgroundColor: "red", fontSize: "1rem" }} aria-label="badge">
61
+ 99+
62
+ </Badge>
63
+ );
64
+ const badge = screen.getByRole("status");
65
+ expect(badge).toHaveAttribute("style");
66
+ expect(badge).toHaveStyle({
67
+ fontSize: "1rem",
68
+ });
69
+ });
70
+
71
+ describe("variant prop", () => {
72
+ it("applies data-badge='rounded' when variant is 'rounded'", () => {
73
+ render(
74
+ <Badge variant="rounded" aria-label="rounded badge">
75
+ 5
76
+ </Badge>
77
+ );
78
+ const badge = screen.getByRole("status");
79
+ expect(badge).toHaveAttribute("data-badge", "rounded");
80
+ });
81
+
82
+ it("does not apply data-badge attribute when variant is undefined", () => {
83
+ render(<Badge aria-label="default badge">3</Badge>);
84
+ const badge = screen.getByRole("status");
85
+ expect(badge).not.toHaveAttribute("data-badge");
86
+ });
87
+ });
88
+
89
+ describe("children rendering", () => {
90
+ it("renders numeric children correctly", () => {
91
+ render(<Badge aria-label="count">123</Badge>);
92
+ expect(screen.getByText("123")).toBeInTheDocument();
93
+ });
94
+
95
+ it("renders string children correctly", () => {
96
+ render(<Badge aria-label="new badge">NEW</Badge>);
97
+ expect(screen.getByText("NEW")).toBeInTheDocument();
98
+ });
99
+
100
+ it("renders React element children correctly", () => {
101
+ render(
102
+ <Badge aria-label="icon badge">
103
+ <span data-testid="custom-icon">★</span>
104
+ </Badge>
105
+ );
106
+ expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
107
+ });
108
+ });
109
+
110
+ describe("accessibility", () => {
111
+ it("is accessible with proper aria-label", () => {
112
+ render(<Badge aria-label="12 items in cart">12</Badge>);
113
+ const badge = screen.getByRole("status", { name: "12 items in cart" });
114
+ expect(badge).toBeInTheDocument();
115
+ });
116
+
117
+ it("can override role attribute if needed", () => {
118
+ render(
119
+ <Badge role="note" aria-label="information">
120
+ i
121
+ </Badge>
122
+ );
123
+ // role="note" should be applied via props spread
124
+ const badge = screen.getByText("i").closest("sup");
125
+ expect(badge).toHaveAttribute("role", "note");
126
+ });
127
+
128
+ it("supports additional ARIA attributes", () => {
129
+ render(
130
+ <Badge aria-label="badge" aria-live="polite" aria-atomic="true">
131
+ 5
132
+ </Badge>
133
+ );
134
+ const badge = screen.getByRole("status");
135
+ expect(badge).toHaveAttribute("aria-live", "polite");
136
+ expect(badge).toHaveAttribute("aria-atomic", "true");
137
+ });
138
+ });
139
+
140
+ describe("integration", () => {
141
+ it("renders correctly within text content", () => {
142
+ render(
143
+ <p>
144
+ Messages
145
+ <Badge aria-label="3 unread">3</Badge>
146
+ </p>
147
+ );
148
+ expect(screen.getByText("Messages")).toBeInTheDocument();
149
+ expect(screen.getByText("3")).toBeInTheDocument();
150
+ });
151
+
152
+ it("renders multiple badges independently", () => {
153
+ render(
154
+ <div>
155
+ <Badge aria-label="first badge">1</Badge>
156
+ <Badge aria-label="second badge">2</Badge>
157
+ <Badge aria-label="third badge" variant="rounded">
158
+ 3
159
+ </Badge>
160
+ </div>
161
+ );
162
+ const badges = screen.getAllByRole("status");
163
+ expect(badges).toHaveLength(3);
164
+ expect(badges[2]).toHaveAttribute("data-badge", "rounded");
165
+ });
166
+ });
167
+
168
+ describe("props spreading", () => {
169
+ it("forwards additional props to the UI component", () => {
170
+ render(
171
+ <Badge data-testid="custom-badge" data-custom="value" aria-label="badge">
172
+ 5
173
+ </Badge>
174
+ );
175
+ const badge = screen.getByTestId("custom-badge");
176
+ expect(badge).toHaveAttribute("data-custom", "value");
177
+ });
178
+ });
179
+ });
@@ -1,17 +1,110 @@
1
1
  import UI from '#components/ui'
2
2
  import React from 'react'
3
3
 
4
+ /**
5
+ * Props for the Badge component
6
+ *
7
+ * @property {React.ReactNode} children - Content to display inside the badge (typically numbers or short text)
8
+ * @property {string} [id] - Optional HTML id attribute for the badge element
9
+ * @property {React.CSSProperties} [styles] - Inline styles to apply to the badge
10
+ * @property {string} [classes] - CSS class names to apply to the badge
11
+ * @property {'rounded'} [variant] - Visual variant of the badge. Use 'rounded' for circular badges (fixed size with ellipsis for overflow)
12
+ * @property {string} [aria-label] - Accessible label for screen readers. Required for icon-only or number-only badges
13
+ * @property {'status' | 'note'} [role] - ARIA role for the badge. Defaults to 'status' for dynamic content
14
+ */
4
15
  export type BadgeProps = {
5
- children: React.ReactNode
16
+ /**
17
+ * Content to display inside the badge (typically numbers or short text)
18
+ */
19
+ children?: React.ReactNode
20
+ /**
21
+ * Visual variant of the badge
22
+ * - 'rounded': Circular badge style
23
+ */
24
+ variant?: 'rounded'
6
25
  } & React.ComponentProps<typeof UI>
7
26
 
8
- const Badge = ({ id, styles, classes, children, ...props }: BadgeProps) => {
27
+ /**
28
+ * Badge - A small label component for displaying status, counts, or notifications
29
+ *
30
+ * The Badge component is used to display supplementary information alongside other content,
31
+ * such as notification counts, status indicators, or labels. It renders as a semantic `<sup>`
32
+ * element with a nested `<span>` required for the component's styling architecture.
33
+ *
34
+ * ## Styling Architecture
35
+ *
36
+ * The Badge uses a nested structure (`<sup><span>content</span></sup>`) which is required
37
+ * for the SCSS styling system. The outer `<sup>` element provides positioning context,
38
+ * while the inner `<span>` receives the visual styling (background, padding, border-radius).
39
+ *
40
+ * ## Rounded Variant Behavior
41
+ *
42
+ * The `rounded` variant creates a perfect circular badge with fixed dimensions (1.5625rem).
43
+ * Content that exceeds the available space will be truncated with an ellipsis (...).
44
+ * **Best practice**: Format large numbers yourself (e.g., pass "99+" instead of "999").
45
+ *
46
+ * ## Accessibility Considerations
47
+ *
48
+ * - **Semantic HTML**: Uses `<sup>` (superscript) element for proper positioning context
49
+ * - **ARIA Role**: Defaults to `role="status"` for dynamic badges (e.g., unread counts)
50
+ * - **Accessible Names**: For icon-only or number-only badges, provide an `aria-label`
51
+ * to give context (e.g., "3 unread messages" instead of just "3")
52
+ * - **Live Regions**: The `role="status"` makes badges announce updates to screen readers
53
+ *
54
+ * @param {BadgeProps} props - Component props
55
+ * @returns {React.ReactElement} A Badge component
56
+ *
57
+ * @example
58
+ * // Basic badge with notification count
59
+ * <p>
60
+ * Messages
61
+ * <Badge aria-label="3 unread messages">3</Badge>
62
+ * </p>
63
+ *
64
+ * @example
65
+ * // Rounded badge variant (perfect circle)
66
+ * <p>
67
+ * Notifications
68
+ * <Badge variant="rounded" aria-label="99 or more notifications">99+</Badge>
69
+ * </p>
70
+ *
71
+ * @example
72
+ * // Status badge with custom styling
73
+ * <p>
74
+ * Active Users
75
+ * <Badge styles={{ backgroundColor: 'green', color: 'white' }}>21</Badge>
76
+ * </p>
77
+ *
78
+ * @example
79
+ * // ✅ GOOD: Accessible badge with descriptive label and formatted content
80
+ * <Badge variant="rounded" aria-label="12 items in cart">12</Badge>
81
+ *
82
+ * @example
83
+ * // ✅ GOOD: Large numbers formatted by developer
84
+ * <Badge variant="rounded" aria-label="More than 99 notifications">99+</Badge>
85
+ *
86
+ * @example
87
+ * // ❌ BAD: Number-only badge without context for screen readers
88
+ * <Badge>12</Badge>
89
+ */
90
+ export const Badge = ({ id, styles, classes, children, variant, ...props }: BadgeProps) => {
91
+ // Build data-badge attribute for variant styling
92
+ const dataBadge = variant ? variant : undefined
93
+
9
94
  return (
10
- <UI as="sup" id={id} styles={styles} className={classes} {...props}>
95
+ <UI
96
+ as="sup"
97
+ id={id}
98
+ styles={styles}
99
+ className={classes}
100
+ data-badge={dataBadge}
101
+ role="status"
102
+ {...props}
103
+ >
11
104
  <UI as="span">{children}</UI>
12
105
  </UI>
13
106
  )
14
107
  }
15
108
 
16
- export default Badge
17
109
  Badge.displayName = 'Badge'
110
+ export default Badge