@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
@@ -0,0 +1,559 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import React, { useRef, useEffect } from 'react';
4
+ import '@testing-library/jest-dom';
5
+ import UI from './ui';
6
+
7
+ describe('UI Component', () => {
8
+ // ============================================
9
+ // Rendering Tests
10
+ // ============================================
11
+
12
+ describe('Rendering', () => {
13
+ it('renders a div by default', () => {
14
+ render(<UI data-testid="ui-element">Default Content</UI>);
15
+ const element = screen.getByTestId('ui-element');
16
+ expect(element.tagName).toBe('DIV');
17
+ expect(element).toHaveTextContent('Default Content');
18
+ });
19
+
20
+ it('renders as button when as="button"', () => {
21
+ render(
22
+ <UI as="button" data-testid="ui-button">
23
+ Button Text
24
+ </UI>
25
+ );
26
+ const button = screen.getByTestId('ui-button');
27
+ expect(button.tagName).toBe('BUTTON');
28
+ expect(button).toHaveTextContent('Button Text');
29
+ });
30
+
31
+ it('renders as span when as="span"', () => {
32
+ render(
33
+ <UI as="span" data-testid="ui-span">
34
+ Span Text
35
+ </UI>
36
+ );
37
+ const span = screen.getByTestId('ui-span');
38
+ expect(span.tagName).toBe('SPAN');
39
+ });
40
+
41
+ it('renders as anchor when as="a"', () => {
42
+ render(
43
+ <UI as="a" href="/test" data-testid="ui-anchor">
44
+ Link Text
45
+ </UI>
46
+ );
47
+ const anchor = screen.getByTestId('ui-anchor');
48
+ expect(anchor.tagName).toBe('A');
49
+ expect(anchor).toHaveAttribute('href', '/test');
50
+ });
51
+
52
+ it('renders as section when as="section"', () => {
53
+ render(
54
+ <UI as="section" data-testid="ui-section">
55
+ Section Content
56
+ </UI>
57
+ );
58
+ const section = screen.getByTestId('ui-section');
59
+ expect(section.tagName).toBe('SECTION');
60
+ });
61
+
62
+ it('renders children correctly', () => {
63
+ render(
64
+ <UI data-testid="ui-parent">
65
+ <span>Child 1</span>
66
+ <span>Child 2</span>
67
+ </UI>
68
+ );
69
+ const parent = screen.getByTestId('ui-parent');
70
+ expect(parent.children).toHaveLength(2);
71
+ expect(screen.getByText('Child 1')).toBeInTheDocument();
72
+ expect(screen.getByText('Child 2')).toBeInTheDocument();
73
+ });
74
+
75
+ it('renders with empty children', () => {
76
+ render(<UI data-testid="ui-empty"></UI>);
77
+ const element = screen.getByTestId('ui-empty');
78
+ expect(element).toBeInTheDocument();
79
+ expect(element).toBeEmptyDOMElement();
80
+ });
81
+
82
+ it('renders with null children', () => {
83
+ render(<UI data-testid="ui-null">{null}</UI>);
84
+ const element = screen.getByTestId('ui-null');
85
+ expect(element).toBeInTheDocument();
86
+ expect(element).toBeEmptyDOMElement();
87
+ // Element is verified to be empty
88
+ void element;
89
+ });
90
+
91
+ it('handles complex nested children', () => {
92
+ render(
93
+ <UI data-testid="ui-complex">
94
+ <div>
95
+ <span>Nested</span>
96
+ <strong>Content</strong>
97
+ </div>
98
+ </UI>
99
+ );
100
+ expect(screen.getByText('Nested')).toBeInTheDocument();
101
+ expect(screen.getByText('Content')).toBeInTheDocument();
102
+ });
103
+ });
104
+
105
+ // ============================================
106
+ // Style Tests
107
+ // ============================================
108
+
109
+ describe('Styles', () => {
110
+ it('applies inline styles via styles prop', () => {
111
+ render(
112
+ <UI
113
+ data-testid="ui-styled"
114
+ styles={{
115
+ padding: '1rem',
116
+ backgroundColor: 'red',
117
+ color: 'white',
118
+ }}
119
+ >
120
+ Styled Content
121
+ </UI>
122
+ );
123
+ const element = screen.getByTestId('ui-styled');
124
+ expect(element).toHaveStyle('padding: 1rem');
125
+ // Note: browsers convert colors to rgb format
126
+ expect(element.style.backgroundColor).toBe('red');
127
+ expect(element.style.color).toBe('white');
128
+ });
129
+
130
+ it('applies className via classes prop', () => {
131
+ render(
132
+ <UI data-testid="ui-classes" classes="custom-class another-class">
133
+ Classed Content
134
+ </UI>
135
+ );
136
+ const element = screen.getByTestId('ui-classes');
137
+ expect(element).toHaveClass('custom-class');
138
+ expect(element).toHaveClass('another-class');
139
+ });
140
+
141
+ it('merges defaultStyles and styles correctly', () => {
142
+ render(
143
+ <UI
144
+ data-testid="ui-merged"
145
+ defaultStyles={{
146
+ padding: '1rem',
147
+ color: 'blue',
148
+ fontSize: '16px',
149
+ }}
150
+ styles={{
151
+ color: 'red', // Override
152
+ fontWeight: 'bold', // Add new
153
+ }}
154
+ >
155
+ Merged Styles
156
+ </UI>
157
+ );
158
+ const element = screen.getByTestId('ui-merged');
159
+ expect(element).toHaveStyle('padding: 1rem');
160
+ expect(element.style.color).toBe('red'); // Overridden by styles
161
+ expect(element.style.fontSize).toBe('16px'); // From defaultStyles
162
+ expect(element.style.fontWeight).toBe('bold'); // From styles
163
+ });
164
+
165
+ it('styles override defaultStyles', () => {
166
+ render(
167
+ <UI
168
+ data-testid="ui-override"
169
+ defaultStyles={{ color: 'blue', padding: '10px' }}
170
+ styles={{ color: 'red' }}
171
+ >
172
+ Override Test
173
+ </UI>
174
+ );
175
+ const element = screen.getByTestId('ui-override');
176
+ expect(element.style.color).toBe('red'); // Overridden
177
+ expect(element).toHaveStyle('padding: 10px'); // Preserved
178
+ });
179
+
180
+ it('handles undefined styles', () => {
181
+ render(
182
+ <UI data-testid="ui-no-styles" styles={undefined}>
183
+ No Styles
184
+ </UI>
185
+ );
186
+ const element = screen.getByTestId('ui-no-styles');
187
+ expect(element).toBeInTheDocument();
188
+ });
189
+
190
+ it('handles empty styles object', () => {
191
+ render(
192
+ <UI data-testid="ui-empty-styles" styles={{}}>
193
+ Empty Styles
194
+ </UI>
195
+ );
196
+ const element = screen.getByTestId('ui-empty-styles');
197
+ expect(element).toBeInTheDocument();
198
+ });
199
+
200
+ it('handles undefined defaultStyles', () => {
201
+ render(
202
+ <UI data-testid="ui-no-default" defaultStyles={undefined} styles={{ color: 'red' }}>
203
+ No Defaults
204
+ </UI>
205
+ );
206
+ const element = screen.getByTestId('ui-no-default');
207
+ expect(element.style.color).toBe('red');
208
+ });
209
+ });
210
+
211
+ // ============================================
212
+ // Prop Forwarding Tests
213
+ // ============================================
214
+
215
+ describe('Prop Forwarding', () => {
216
+ it('forwards onClick to button element', () => {
217
+ let clicked = false;
218
+ const handleClick = () => {
219
+ clicked = true;
220
+ };
221
+
222
+ render(
223
+ <UI as="button" onClick={handleClick} data-testid="ui-clickable">
224
+ Click Me
225
+ </UI>
226
+ );
227
+
228
+ const button = screen.getByTestId('ui-clickable');
229
+ button.click();
230
+ expect(clicked).toBe(true);
231
+ });
232
+
233
+ it('forwards href to anchor element', () => {
234
+ render(
235
+ <UI as="a" href="/test-link" data-testid="ui-link">
236
+ Link
237
+ </UI>
238
+ );
239
+ const link = screen.getByTestId('ui-link');
240
+ expect(link).toHaveAttribute('href', '/test-link');
241
+ });
242
+
243
+ it('forwards disabled to button element', () => {
244
+ render(
245
+ <UI as="button" disabled data-testid="ui-disabled">
246
+ Disabled Button
247
+ </UI>
248
+ );
249
+ const button = screen.getByTestId('ui-disabled');
250
+ expect(button).toBeDisabled();
251
+ });
252
+
253
+ it('forwards target to anchor element', () => {
254
+ render(
255
+ <UI as="a" href="/test" target="_blank" rel="noopener" data-testid="ui-target">
256
+ External Link
257
+ </UI>
258
+ );
259
+ const link = screen.getByTestId('ui-target');
260
+ expect(link).toHaveAttribute('target', '_blank');
261
+ expect(link).toHaveAttribute('rel', 'noopener');
262
+ });
263
+
264
+ it('forwards data-* attributes', () => {
265
+ render(
266
+ <UI data-testid="ui-data" data-custom="custom-value" data-id="123">
267
+ Data Attributes
268
+ </UI>
269
+ );
270
+ const element = screen.getByTestId('ui-data');
271
+ expect(element).toHaveAttribute('data-custom', 'custom-value');
272
+ expect(element).toHaveAttribute('data-id', '123');
273
+ });
274
+
275
+ it('forwards aria-* attributes', () => {
276
+ render(
277
+ <UI data-testid="ui-aria" aria-label="Custom Label" aria-hidden="true">
278
+ ARIA Attributes
279
+ </UI>
280
+ );
281
+ const element = screen.getByTestId('ui-aria');
282
+ expect(element).toHaveAttribute('aria-label', 'Custom Label');
283
+ expect(element).toHaveAttribute('aria-hidden', 'true');
284
+ });
285
+
286
+ it('applies id prop', () => {
287
+ render(
288
+ <UI id="custom-id" data-testid="ui-id">
289
+ ID Test
290
+ </UI>
291
+ );
292
+ const element = screen.getByTestId('ui-id');
293
+ expect(element).toHaveAttribute('id', 'custom-id');
294
+ });
295
+ });
296
+
297
+ // ============================================
298
+ // Ref Forwarding Tests
299
+ // ============================================
300
+
301
+ describe('Ref Forwarding', () => {
302
+ it('forwards ref to div element', () => {
303
+ const RefTest = () => {
304
+ const divRef = useRef<HTMLDivElement>(null);
305
+
306
+ useEffect(() => {
307
+ if (divRef.current) {
308
+ divRef.current.setAttribute('data-ref-test', 'true');
309
+ }
310
+ }, []);
311
+
312
+ return (
313
+ <UI ref={divRef} data-testid="ui-div-ref">
314
+ Div with Ref
315
+ </UI>
316
+ );
317
+ };
318
+
319
+ render(<RefTest />);
320
+ const element = screen.getByTestId('ui-div-ref');
321
+ expect(element).toHaveAttribute('data-ref-test', 'true');
322
+ });
323
+
324
+ it('forwards ref to button element', () => {
325
+ const RefTest = () => {
326
+ const buttonRef = useRef<HTMLButtonElement>(null);
327
+
328
+ useEffect(() => {
329
+ if (buttonRef.current) {
330
+ buttonRef.current.setAttribute('data-ref-test', 'true');
331
+ }
332
+ }, []);
333
+
334
+ return (
335
+ <UI as="button" ref={buttonRef} data-testid="ui-button-ref">
336
+ Button with Ref
337
+ </UI>
338
+ );
339
+ };
340
+
341
+ render(<RefTest />);
342
+ const button = screen.getByTestId('ui-button-ref');
343
+ expect(button).toHaveAttribute('data-ref-test', 'true');
344
+ });
345
+
346
+ it('forwards ref to anchor element', () => {
347
+ const RefTest = () => {
348
+ const anchorRef = useRef<HTMLAnchorElement>(null);
349
+
350
+ useEffect(() => {
351
+ if (anchorRef.current) {
352
+ anchorRef.current.setAttribute('data-ref-test', 'true');
353
+ }
354
+ }, []);
355
+
356
+ return (
357
+ <UI as="a" href="/test" ref={anchorRef} data-testid="ui-anchor-ref">
358
+ Anchor with Ref
359
+ </UI>
360
+ );
361
+ };
362
+
363
+ render(<RefTest />);
364
+ const anchor = screen.getByTestId('ui-anchor-ref');
365
+ expect(anchor).toHaveAttribute('data-ref-test', 'true');
366
+ });
367
+
368
+ it('ref provides access to DOM node', () => {
369
+ const RefTest = () => {
370
+ const elementRef = useRef<HTMLDivElement>(null);
371
+
372
+ useEffect(() => {
373
+ if (elementRef.current) {
374
+ expect(elementRef.current.tagName).toBe('DIV');
375
+ expect(elementRef.current.textContent).toBe('DOM Access');
376
+ }
377
+ }, []);
378
+
379
+ return (
380
+ <UI ref={elementRef} data-testid="ui-dom-access">
381
+ DOM Access
382
+ </UI>
383
+ );
384
+ };
385
+
386
+ render(<RefTest />);
387
+ });
388
+
389
+ it('ref type matches element type', () => {
390
+ const RefTest = () => {
391
+ const buttonRef = useRef<HTMLButtonElement>(null);
392
+
393
+ useEffect(() => {
394
+ if (buttonRef.current) {
395
+ // HTMLButtonElement-specific property
396
+ expect(buttonRef.current.type).toBeDefined();
397
+ expect(buttonRef.current.disabled).toBe(false);
398
+ }
399
+ }, []);
400
+
401
+ return (
402
+ <UI as="button" ref={buttonRef} data-testid="ui-typed-ref">
403
+ Typed Ref
404
+ </UI>
405
+ );
406
+ };
407
+
408
+ render(<RefTest />);
409
+ });
410
+ });
411
+
412
+ // ============================================
413
+ // Edge Cases
414
+ // ============================================
415
+
416
+ describe('Edge Cases', () => {
417
+ it('handles undefined as prop (defaults to div)', () => {
418
+ render(
419
+ <UI as={undefined} data-testid="ui-undefined-as">
420
+ Undefined As
421
+ </UI>
422
+ );
423
+ const element = screen.getByTestId('ui-undefined-as');
424
+ expect(element.tagName).toBe('DIV');
425
+ });
426
+
427
+ it('handles undefined classes prop', () => {
428
+ render(
429
+ <UI classes={undefined} data-testid="ui-undefined-classes">
430
+ No Classes
431
+ </UI>
432
+ );
433
+ const element = screen.getByTestId('ui-undefined-classes');
434
+ expect(element).toBeInTheDocument();
435
+ expect(element.className).toBe('');
436
+ });
437
+
438
+ it('handles empty string classes', () => {
439
+ render(
440
+ <UI classes="" data-testid="ui-empty-classes">
441
+ Empty Classes
442
+ </UI>
443
+ );
444
+ const element = screen.getByTestId('ui-empty-classes');
445
+ expect(element.className).toBe('');
446
+ });
447
+
448
+ it('handles multiple whitespace-separated classes', () => {
449
+ render(
450
+ <UI classes="class1 class2 class3" data-testid="ui-multiple-classes">
451
+ Multiple Classes
452
+ </UI>
453
+ );
454
+ const element = screen.getByTestId('ui-multiple-classes');
455
+ expect(element).toHaveClass('class1', 'class2', 'class3');
456
+ });
457
+
458
+ it('renders with boolean children (false)', () => {
459
+ render(<UI data-testid="ui-boolean">{false}</UI>);
460
+ const element = screen.getByTestId('ui-boolean');
461
+ expect(element).toBeEmptyDOMElement();
462
+ });
463
+
464
+ it('renders with number children', () => {
465
+ render(<UI data-testid="ui-number">{42}</UI>);
466
+ const element = screen.getByTestId('ui-number');
467
+ expect(element).toHaveTextContent('42');
468
+ });
469
+
470
+ it('renders with mixed children types', () => {
471
+ render(
472
+ <UI data-testid="ui-mixed">
473
+ Text
474
+ {42}
475
+ <span>Element</span>
476
+ {null}
477
+ {false}
478
+ </UI>
479
+ );
480
+ const element = screen.getByTestId('ui-mixed');
481
+ expect(element).toHaveTextContent('Text42Element');
482
+ });
483
+ });
484
+
485
+ // ============================================
486
+ // Integration Tests
487
+ // ============================================
488
+
489
+ describe('Integration', () => {
490
+ it('works as a building block for custom components', () => {
491
+ const CustomButton = ({
492
+ variant,
493
+ children,
494
+ ...props
495
+ }: {
496
+ variant: 'primary' | 'secondary';
497
+ children: React.ReactNode;
498
+ }) => {
499
+ const styles = {
500
+ primary: { backgroundColor: 'blue', color: 'white' },
501
+ secondary: { backgroundColor: 'gray', color: 'black' },
502
+ };
503
+
504
+ return (
505
+ <UI
506
+ as="button"
507
+ defaultStyles={{
508
+ padding: '0.5rem 1rem',
509
+ border: 'none',
510
+ ...styles[variant],
511
+ }}
512
+ {...props}
513
+ >
514
+ {children}
515
+ </UI>
516
+ );
517
+ };
518
+
519
+ render(<CustomButton variant="primary" data-testid="custom-button">Custom</CustomButton>);
520
+ const button = screen.getByTestId('custom-button');
521
+ expect(button.tagName).toBe('BUTTON');
522
+ expect(button).toHaveStyle('padding: 0.5rem 1rem');
523
+ expect(button.style.backgroundColor).toBe('blue');
524
+ expect(button.style.color).toBe('white');
525
+ });
526
+
527
+ it('supports style overrides in custom components', () => {
528
+ const CustomBox = ({
529
+ children,
530
+ ...props
531
+ }: {
532
+ children: React.ReactNode;
533
+ styles?: React.CSSProperties;
534
+ 'data-testid'?: string;
535
+ }) => {
536
+ return (
537
+ <UI
538
+ defaultStyles={{
539
+ padding: '1rem',
540
+ backgroundColor: 'lightgray',
541
+ }}
542
+ {...props}
543
+ >
544
+ {children}
545
+ </UI>
546
+ );
547
+ };
548
+
549
+ render(
550
+ <CustomBox styles={{ backgroundColor: 'red' }} data-testid="custom-box">
551
+ Box
552
+ </CustomBox>
553
+ );
554
+ const box = screen.getByTestId('custom-box');
555
+ expect(box.style.backgroundColor).toBe('red'); // Override
556
+ expect(box).toHaveStyle('padding: 1rem'); // Preserved
557
+ });
558
+ });
559
+ });