@zentauri-ui/zentauri-components 1.3.1 → 1.4.0

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 (307) hide show
  1. package/README.md +78 -0
  2. package/cli/cli.integration.test.ts +51 -0
  3. package/cli/index.mjs +664 -0
  4. package/cli/registry.json +36 -0
  5. package/cli/rewrite-imports.mjs +57 -0
  6. package/cli/rewrite-imports.test.ts +71 -0
  7. package/dist/ui/slider/slider.d.ts +18 -0
  8. package/dist/ui/slider/slider.d.ts.map +1 -1
  9. package/dist/ui/slider.js +21 -25
  10. package/dist/ui/slider.js.map +1 -1
  11. package/dist/ui/slider.mjs +21 -25
  12. package/dist/ui/slider.mjs.map +1 -1
  13. package/package.json +8 -2
  14. package/src/hooks/index.ts +48 -0
  15. package/src/hooks/useBodyScrollLock/index.ts +1 -0
  16. package/src/hooks/useBodyScrollLock/useBodyScrollLock.test.ts +51 -0
  17. package/src/hooks/useBodyScrollLock/useBodyScrollLock.ts +48 -0
  18. package/src/hooks/useClickOutside/index.ts +5 -0
  19. package/src/hooks/useClickOutside/useClickOutside.test.tsx +60 -0
  20. package/src/hooks/useClickOutside/useClickOutside.ts +52 -0
  21. package/src/hooks/useClipboard/index.ts +1 -0
  22. package/src/hooks/useClipboard/useClipboard.test.ts +101 -0
  23. package/src/hooks/useClipboard/useClipboard.ts +69 -0
  24. package/src/hooks/useControllableState/index.ts +4 -0
  25. package/src/hooks/useControllableState/useControllableState.test.ts +59 -0
  26. package/src/hooks/useControllableState/useControllableState.ts +49 -0
  27. package/src/hooks/useDebouncedValue/index.ts +1 -0
  28. package/src/hooks/useDebouncedValue/useDebouncedValue.test.ts +74 -0
  29. package/src/hooks/useDebouncedValue/useDebouncedValue.ts +29 -0
  30. package/src/hooks/useDisclosure/index.ts +5 -0
  31. package/src/hooks/useDisclosure/useDisclosure.test.ts +64 -0
  32. package/src/hooks/useDisclosure/useDisclosure.ts +62 -0
  33. package/src/hooks/useDocumentTitle/index.ts +4 -0
  34. package/src/hooks/useDocumentTitle/useDocumentTitle.test.ts +40 -0
  35. package/src/hooks/useDocumentTitle/useDocumentTitle.ts +58 -0
  36. package/src/hooks/useFocusManagement/index.ts +1 -0
  37. package/src/hooks/useFocusManagement/useFocusManagement.test.tsx +45 -0
  38. package/src/hooks/useFocusManagement/useFocusManagement.ts +77 -0
  39. package/src/hooks/useHover/index.ts +1 -0
  40. package/src/hooks/useHover/useHover.test.ts +45 -0
  41. package/src/hooks/useHover/useHover.ts +45 -0
  42. package/src/hooks/useInView/index.ts +1 -0
  43. package/src/hooks/useInView/useInView.test.ts +43 -0
  44. package/src/hooks/useInView/useInView.ts +28 -0
  45. package/src/hooks/useIntersectionObserver/index.ts +4 -0
  46. package/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts +75 -0
  47. package/src/hooks/useIntersectionObserver/useIntersectionObserver.ts +54 -0
  48. package/src/hooks/useIsMounted/index.ts +1 -0
  49. package/src/hooks/useIsMounted/useIsMounted.test.ts +25 -0
  50. package/src/hooks/useIsMounted/useIsMounted.ts +22 -0
  51. package/src/hooks/useIsomorphicLayoutEffect/index.ts +1 -0
  52. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.ts +19 -0
  53. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +12 -0
  54. package/src/hooks/useLocalStorage/index.ts +4 -0
  55. package/src/hooks/useLocalStorage/useLocalStorage.test.ts +99 -0
  56. package/src/hooks/useLocalStorage/useLocalStorage.ts +109 -0
  57. package/src/hooks/useMediaQuery/index.ts +1 -0
  58. package/src/hooks/useMediaQuery/useMediaQuery.test.ts +63 -0
  59. package/src/hooks/useMediaQuery/useMediaQuery.ts +37 -0
  60. package/src/hooks/useNetworkStatus/index.ts +1 -0
  61. package/src/hooks/useNetworkStatus/useNetworkStatus.test.ts +53 -0
  62. package/src/hooks/useNetworkStatus/useNetworkStatus.ts +33 -0
  63. package/src/hooks/usePageVisibility/index.ts +1 -0
  64. package/src/hooks/usePageVisibility/usePageVisibility.test.ts +21 -0
  65. package/src/hooks/usePageVisibility/usePageVisibility.ts +31 -0
  66. package/src/hooks/usePagination/index.ts +6 -0
  67. package/src/hooks/usePagination/usePagination.test.ts +139 -0
  68. package/src/hooks/usePagination/usePagination.ts +153 -0
  69. package/src/hooks/usePrefersColorScheme/index.ts +4 -0
  70. package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.test.ts +53 -0
  71. package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.ts +21 -0
  72. package/src/hooks/usePrefersReducedMotion/index.ts +1 -0
  73. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.test.ts +27 -0
  74. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.ts +14 -0
  75. package/src/hooks/useResizeObserver/index.ts +4 -0
  76. package/src/hooks/useResizeObserver/useResizeObserver.test.ts +68 -0
  77. package/src/hooks/useResizeObserver/useResizeObserver.ts +58 -0
  78. package/src/hooks/useSessionStorage/index.ts +4 -0
  79. package/src/hooks/useSessionStorage/useSessionStorage.test.ts +54 -0
  80. package/src/hooks/useSessionStorage/useSessionStorage.ts +84 -0
  81. package/src/hooks/useThrottledCallback/index.ts +1 -0
  82. package/src/hooks/useThrottledCallback/useThrottledCallback.test.ts +75 -0
  83. package/src/hooks/useThrottledCallback/useThrottledCallback.ts +36 -0
  84. package/src/hooks/useToggle/index.ts +1 -0
  85. package/src/hooks/useToggle/useToggle.test.ts +40 -0
  86. package/src/hooks/useToggle/useToggle.ts +22 -0
  87. package/src/hooks/useWindowSize/index.ts +1 -0
  88. package/src/hooks/useWindowSize/useWindowSize.test.ts +23 -0
  89. package/src/hooks/useWindowSize/useWindowSize.ts +39 -0
  90. package/src/lib/utils.ts +25 -0
  91. package/src/ui/accordion/accordion-base.tsx +223 -0
  92. package/src/ui/accordion/accordion.test.tsx +146 -0
  93. package/src/ui/accordion/accordion.tsx +11 -0
  94. package/src/ui/accordion/animated/accordion-content-animated.tsx +46 -0
  95. package/src/ui/accordion/animated/accordion-root-animated.tsx +10 -0
  96. package/src/ui/accordion/animated/animations.ts +16 -0
  97. package/src/ui/accordion/animated/index.ts +7 -0
  98. package/src/ui/accordion/animated/types.ts +7 -0
  99. package/src/ui/accordion/index.ts +23 -0
  100. package/src/ui/accordion/types.ts +48 -0
  101. package/src/ui/accordion/variants.ts +115 -0
  102. package/src/ui/alert/alert-base.tsx +157 -0
  103. package/src/ui/alert/alert.test.tsx +150 -0
  104. package/src/ui/alert/alert.tsx +9 -0
  105. package/src/ui/alert/animated/alert-animated.tsx +20 -0
  106. package/src/ui/alert/animated/animations.ts +20 -0
  107. package/src/ui/alert/animated/index.ts +3 -0
  108. package/src/ui/alert/animated/types.ts +16 -0
  109. package/src/ui/alert/index.ts +22 -0
  110. package/src/ui/alert/types.ts +28 -0
  111. package/src/ui/alert/variants.ts +74 -0
  112. package/src/ui/avatar/animated/animations.ts +11 -0
  113. package/src/ui/avatar/animated/avatar-animated.tsx +25 -0
  114. package/src/ui/avatar/animated/index.ts +6 -0
  115. package/src/ui/avatar/animated/types.ts +16 -0
  116. package/src/ui/avatar/avatar-base.tsx +184 -0
  117. package/src/ui/avatar/avatar.test.tsx +51 -0
  118. package/src/ui/avatar/avatar.tsx +11 -0
  119. package/src/ui/avatar/index.ts +16 -0
  120. package/src/ui/avatar/types.ts +36 -0
  121. package/src/ui/avatar/variants.ts +52 -0
  122. package/src/ui/badge/animated/animations.ts +20 -0
  123. package/src/ui/badge/animated/badge-animated.tsx +28 -0
  124. package/src/ui/badge/animated/index.ts +5 -0
  125. package/src/ui/badge/animated/types.ts +18 -0
  126. package/src/ui/badge/badge-base.tsx +53 -0
  127. package/src/ui/badge/badge.test.tsx +48 -0
  128. package/src/ui/badge/badge.tsx +9 -0
  129. package/src/ui/badge/index.ts +5 -0
  130. package/src/ui/badge/types.ts +25 -0
  131. package/src/ui/badge/variants.ts +85 -0
  132. package/src/ui/breadcrumb/breadcrumb.test.tsx +62 -0
  133. package/src/ui/breadcrumb/breadcrumb.tsx +135 -0
  134. package/src/ui/breadcrumb/index.ts +28 -0
  135. package/src/ui/breadcrumb/types.ts +29 -0
  136. package/src/ui/breadcrumb/variants.ts +53 -0
  137. package/src/ui/buttons/animated/animations.ts +34 -0
  138. package/src/ui/buttons/animated/button-animated.tsx +70 -0
  139. package/src/ui/buttons/animated/index.ts +5 -0
  140. package/src/ui/buttons/animated/types.ts +29 -0
  141. package/src/ui/buttons/button-base.tsx +59 -0
  142. package/src/ui/buttons/button.test.tsx +480 -0
  143. package/src/ui/buttons/button.tsx +9 -0
  144. package/src/ui/buttons/index.ts +5 -0
  145. package/src/ui/buttons/types.ts +14 -0
  146. package/src/ui/buttons/variants.ts +77 -0
  147. package/src/ui/card/animated/animations.ts +32 -0
  148. package/src/ui/card/animated/card-animated.tsx +28 -0
  149. package/src/ui/card/animated/index.ts +12 -0
  150. package/src/ui/card/animated/types.ts +8 -0
  151. package/src/ui/card/card-base.tsx +146 -0
  152. package/src/ui/card/card.test.tsx +79 -0
  153. package/src/ui/card/card.tsx +11 -0
  154. package/src/ui/card/index.ts +21 -0
  155. package/src/ui/card/types.ts +42 -0
  156. package/src/ui/card/variants.ts +122 -0
  157. package/src/ui/divider/animated/animations.ts +27 -0
  158. package/src/ui/divider/animated/divider-animated.tsx +24 -0
  159. package/src/ui/divider/animated/index.ts +4 -0
  160. package/src/ui/divider/animated/types.ts +18 -0
  161. package/src/ui/divider/divider-base.tsx +80 -0
  162. package/src/ui/divider/divider.tsx +9 -0
  163. package/src/ui/divider/index.ts +14 -0
  164. package/src/ui/divider/types.ts +18 -0
  165. package/src/ui/divider/variants.ts +98 -0
  166. package/src/ui/drawer/animated/animations.ts +39 -0
  167. package/src/ui/drawer/animated/drawer-content-animated.tsx +101 -0
  168. package/src/ui/drawer/animated/index.ts +14 -0
  169. package/src/ui/drawer/animated/types.ts +18 -0
  170. package/src/ui/drawer/drawer-base.tsx +259 -0
  171. package/src/ui/drawer/drawer.test.tsx +132 -0
  172. package/src/ui/drawer/drawer.tsx +11 -0
  173. package/src/ui/drawer/index.ts +21 -0
  174. package/src/ui/drawer/types.ts +39 -0
  175. package/src/ui/drawer/variants.ts +122 -0
  176. package/src/ui/dropdown/dropdown.test.tsx +114 -0
  177. package/src/ui/dropdown/dropdown.tsx +179 -0
  178. package/src/ui/dropdown/index.ts +15 -0
  179. package/src/ui/dropdown/types.ts +68 -0
  180. package/src/ui/dropdown/variants.ts +138 -0
  181. package/src/ui/empty-state/animated/animations.ts +19 -0
  182. package/src/ui/empty-state/animated/empty-state-animated.tsx +23 -0
  183. package/src/ui/empty-state/animated/index.ts +7 -0
  184. package/src/ui/empty-state/animated/types.ts +26 -0
  185. package/src/ui/empty-state/empty-state-base.tsx +114 -0
  186. package/src/ui/empty-state/empty-state.tsx +9 -0
  187. package/src/ui/empty-state/index.ts +10 -0
  188. package/src/ui/empty-state/types.ts +19 -0
  189. package/src/ui/empty-state/variants.ts +51 -0
  190. package/src/ui/file-upload/file-upload.test.tsx +36 -0
  191. package/src/ui/file-upload/file-upload.tsx +119 -0
  192. package/src/ui/file-upload/index.ts +5 -0
  193. package/src/ui/file-upload/types.ts +21 -0
  194. package/src/ui/file-upload/variants.ts +29 -0
  195. package/src/ui/inputs/animated/animations.ts +36 -0
  196. package/src/ui/inputs/animated/index.ts +5 -0
  197. package/src/ui/inputs/animated/input-animated.tsx +124 -0
  198. package/src/ui/inputs/animated/types.ts +40 -0
  199. package/src/ui/inputs/index.ts +5 -0
  200. package/src/ui/inputs/input-base.tsx +114 -0
  201. package/src/ui/inputs/input.test.tsx +414 -0
  202. package/src/ui/inputs/input.tsx +8 -0
  203. package/src/ui/inputs/types.ts +18 -0
  204. package/src/ui/inputs/variants.ts +316 -0
  205. package/src/ui/modal/animated/animations.ts +29 -0
  206. package/src/ui/modal/animated/index.ts +5 -0
  207. package/src/ui/modal/animated/modal-content-animated.tsx +96 -0
  208. package/src/ui/modal/animated/types.ts +23 -0
  209. package/src/ui/modal/index.ts +21 -0
  210. package/src/ui/modal/modal-base.tsx +279 -0
  211. package/src/ui/modal/modal.test.tsx +129 -0
  212. package/src/ui/modal/modal.tsx +8 -0
  213. package/src/ui/modal/types.ts +31 -0
  214. package/src/ui/modal/variants.ts +109 -0
  215. package/src/ui/pagination/index.ts +13 -0
  216. package/src/ui/pagination/pagination.test.tsx +165 -0
  217. package/src/ui/pagination/pagination.tsx +237 -0
  218. package/src/ui/pagination/types.ts +66 -0
  219. package/src/ui/pagination/variants.ts +97 -0
  220. package/src/ui/progress/animated/animations.ts +9 -0
  221. package/src/ui/progress/animated/index.ts +17 -0
  222. package/src/ui/progress/animated/progress-animated.tsx +133 -0
  223. package/src/ui/progress/animated/types.ts +35 -0
  224. package/src/ui/progress/index.ts +10 -0
  225. package/src/ui/progress/progress-base.tsx +151 -0
  226. package/src/ui/progress/progress.test.tsx +84 -0
  227. package/src/ui/progress/progress.tsx +12 -0
  228. package/src/ui/progress/types.ts +33 -0
  229. package/src/ui/progress/variants.ts +105 -0
  230. package/src/ui/select/index.ts +25 -0
  231. package/src/ui/select/select.test.tsx +128 -0
  232. package/src/ui/select/select.tsx +221 -0
  233. package/src/ui/select/types.ts +77 -0
  234. package/src/ui/select/variants.ts +163 -0
  235. package/src/ui/skeleton/animated/animations.ts +15 -0
  236. package/src/ui/skeleton/animated/index.ts +20 -0
  237. package/src/ui/skeleton/animated/skeleton-animated.tsx +119 -0
  238. package/src/ui/skeleton/animated/types.ts +49 -0
  239. package/src/ui/skeleton/index.ts +24 -0
  240. package/src/ui/skeleton/skeleton-base.tsx +288 -0
  241. package/src/ui/skeleton/skeleton.tsx +8 -0
  242. package/src/ui/skeleton/types.ts +31 -0
  243. package/src/ui/skeleton/variants.ts +254 -0
  244. package/src/ui/slider/index.ts +22 -0
  245. package/src/ui/slider/slider.test.tsx +94 -0
  246. package/src/ui/slider/slider.tsx +728 -0
  247. package/src/ui/slider/types.ts +66 -0
  248. package/src/ui/slider/variants.ts +81 -0
  249. package/src/ui/spinner/animated/index.ts +5 -0
  250. package/src/ui/spinner/animated/spinner.test.tsx +41 -0
  251. package/src/ui/spinner/animated/spinner.tsx +143 -0
  252. package/src/ui/spinner/animated/types.ts +11 -0
  253. package/src/ui/spinner/animated/variants.ts +50 -0
  254. package/src/ui/stepper/index.ts +22 -0
  255. package/src/ui/stepper/stepper.test.tsx +183 -0
  256. package/src/ui/stepper/stepper.tsx +172 -0
  257. package/src/ui/stepper/types.ts +32 -0
  258. package/src/ui/stepper/variants.ts +69 -0
  259. package/src/ui/table/animated/animations.ts +9 -0
  260. package/src/ui/table/animated/index.ts +15 -0
  261. package/src/ui/table/animated/table-animated.tsx +15 -0
  262. package/src/ui/table/animated/types.ts +16 -0
  263. package/src/ui/table/index.ts +22 -0
  264. package/src/ui/table/table-base.tsx +197 -0
  265. package/src/ui/table/table.tsx +13 -0
  266. package/src/ui/table/types.ts +47 -0
  267. package/src/ui/table/variants.ts +105 -0
  268. package/src/ui/tabs/animated/animations.ts +48 -0
  269. package/src/ui/tabs/animated/index.ts +8 -0
  270. package/src/ui/tabs/animated/tabs-content-animated.tsx +46 -0
  271. package/src/ui/tabs/animated/types.ts +24 -0
  272. package/src/ui/tabs/index.ts +10 -0
  273. package/src/ui/tabs/tabs-base.tsx +185 -0
  274. package/src/ui/tabs/tabs.test.tsx +53 -0
  275. package/src/ui/tabs/tabs.tsx +2 -0
  276. package/src/ui/tabs/types.ts +88 -0
  277. package/src/ui/tabs/variants.ts +70 -0
  278. package/src/ui/toast/animated/animations.ts +17 -0
  279. package/src/ui/toast/animated/index.ts +9 -0
  280. package/src/ui/toast/animated/toast-animated.tsx +96 -0
  281. package/src/ui/toast/animated/types.ts +13 -0
  282. package/src/ui/toast/index.ts +26 -0
  283. package/src/ui/toast/toast-base.tsx +231 -0
  284. package/src/ui/toast/toast.test.tsx +102 -0
  285. package/src/ui/toast/toast.tsx +13 -0
  286. package/src/ui/toast/types.ts +57 -0
  287. package/src/ui/toast/variants.ts +73 -0
  288. package/src/ui/toggle/animated/animations.ts +9 -0
  289. package/src/ui/toggle/animated/index.ts +7 -0
  290. package/src/ui/toggle/animated/toggle-animated.tsx +76 -0
  291. package/src/ui/toggle/animated/types.ts +13 -0
  292. package/src/ui/toggle/index.ts +5 -0
  293. package/src/ui/toggle/toggle-base.tsx +70 -0
  294. package/src/ui/toggle/toggle.test.tsx +44 -0
  295. package/src/ui/toggle/toggle.tsx +9 -0
  296. package/src/ui/toggle/types.ts +18 -0
  297. package/src/ui/toggle/variants.ts +84 -0
  298. package/src/ui/tooltip/animated/animations.ts +16 -0
  299. package/src/ui/tooltip/animated/index.ts +10 -0
  300. package/src/ui/tooltip/animated/tooltip-content-animated.tsx +47 -0
  301. package/src/ui/tooltip/animated/types.ts +19 -0
  302. package/src/ui/tooltip/index.ts +17 -0
  303. package/src/ui/tooltip/tooltip-base.tsx +152 -0
  304. package/src/ui/tooltip/tooltip.test.tsx +84 -0
  305. package/src/ui/tooltip/tooltip.tsx +8 -0
  306. package/src/ui/tooltip/types.ts +57 -0
  307. package/src/ui/tooltip/variants.ts +61 -0
@@ -0,0 +1,150 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, it, vi } from "vitest";
5
+
6
+ import {
7
+ AlertClose,
8
+ AlertDefaultIcon,
9
+ AlertDescription,
10
+ AlertIcon,
11
+ AlertTitle,
12
+ } from "./alert-base";
13
+ import { Alert } from "./alert";
14
+
15
+ describe("Alert", () => {
16
+ describe("public contract and metadata", () => {
17
+ it("should expose displayName on compound parts", () => {
18
+ expect(Alert.displayName).toBe("Alert");
19
+ expect(AlertIcon.displayName).toBe("AlertIcon");
20
+ expect(AlertTitle.displayName).toBe("AlertTitle");
21
+ expect(AlertDescription.displayName).toBe("AlertDescription");
22
+ expect(AlertClose.displayName).toBe("AlertClose");
23
+ });
24
+
25
+ it("should stamp data-slot on the root element", () => {
26
+ render(
27
+ <Alert>
28
+ <AlertTitle>Heads up</AlertTitle>
29
+ </Alert>,
30
+ );
31
+ const root = document.querySelector('[data-slot="alert"]');
32
+ expect(root).toBeTruthy();
33
+ expect(root?.getAttribute("data-slot")).toBe("alert");
34
+ });
35
+ });
36
+
37
+ describe("semantics and live region", () => {
38
+ it("should use role alert and polite live region for non-error appearances", () => {
39
+ render(
40
+ <Alert appearance="info">
41
+ <AlertTitle>Info</AlertTitle>
42
+ </Alert>,
43
+ );
44
+ const live = screen.getByRole("alert");
45
+ expect(live.getAttribute("aria-live")).toBe("polite");
46
+ });
47
+
48
+ it("should use assertive live region for error appearance", () => {
49
+ render(
50
+ <Alert appearance="error">
51
+ <AlertTitle>Error</AlertTitle>
52
+ </Alert>,
53
+ );
54
+ expect(screen.getByRole("alert").getAttribute("aria-live")).toBe(
55
+ "assertive",
56
+ );
57
+ });
58
+ });
59
+
60
+ describe("closable behavior", () => {
61
+ it("should render a dismiss control when closable is true", () => {
62
+ render(
63
+ <Alert closable closeLabel="Remove warning">
64
+ <AlertTitle>Warning</AlertTitle>
65
+ </Alert>,
66
+ );
67
+ expect(
68
+ screen.getByRole("button", { name: "Remove warning" }),
69
+ ).toHaveAttribute("data-slot", "alert-close");
70
+ });
71
+
72
+ it("should invoke onClose when the built-in dismiss button is activated", async () => {
73
+ const user = userEvent.setup();
74
+ const handleClose = vi.fn();
75
+ render(
76
+ <Alert closable onClose={handleClose}>
77
+ <AlertTitle>Dismissible</AlertTitle>
78
+ </Alert>,
79
+ );
80
+ await user.click(screen.getByRole("button", { name: /dismiss alert/i }));
81
+ expect(handleClose).toHaveBeenCalledTimes(1);
82
+ });
83
+
84
+ it("should allow AlertClose to run a custom handler", async () => {
85
+ const user = userEvent.setup();
86
+ const handleClose = vi.fn();
87
+ render(
88
+ <Alert>
89
+ <AlertTitle>Manual close</AlertTitle>
90
+ <AlertClose aria-label="Close" onClick={handleClose}>
91
+ ×
92
+ </AlertClose>
93
+ </Alert>,
94
+ );
95
+ await user.click(screen.getByRole("button", { name: "Close" }));
96
+ expect(handleClose).toHaveBeenCalledTimes(1);
97
+ });
98
+ });
99
+
100
+ describe("composition", () => {
101
+ it("should render compound slots for icon, title, and description", () => {
102
+ render(
103
+ <Alert>
104
+ <AlertIcon>
105
+ <span data-testid="custom-icon">!</span>
106
+ </AlertIcon>
107
+ <AlertTitle>Title</AlertTitle>
108
+ <AlertDescription>Description body</AlertDescription>
109
+ </Alert>,
110
+ );
111
+ expect(screen.getByTestId("custom-icon")).toBeInTheDocument();
112
+ expect(
113
+ document.querySelector('[data-slot="alert-title"]'),
114
+ ).toHaveTextContent("Title");
115
+ expect(
116
+ document.querySelector('[data-slot="alert-description"]'),
117
+ ).toHaveTextContent("Description body");
118
+ });
119
+
120
+ it("should apply appearance classes from the variant recipe", () => {
121
+ render(
122
+ <Alert appearance="success">
123
+ <AlertTitle>Done</AlertTitle>
124
+ </Alert>,
125
+ );
126
+ const root = document.querySelector('[data-slot="alert"]') as HTMLElement;
127
+ expect(root.className).toMatch(/border-emerald-500/);
128
+ });
129
+ });
130
+
131
+ describe("ref forwarding", () => {
132
+ it("should forward ref to the motion root", () => {
133
+ const ref = createRef<HTMLDivElement>();
134
+ render(
135
+ <Alert ref={ref}>
136
+ <AlertTitle>Ref</AlertTitle>
137
+ </Alert>,
138
+ );
139
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
140
+ expect(ref.current?.getAttribute("data-slot")).toBe("alert");
141
+ });
142
+ });
143
+
144
+ describe("AlertDefaultIcon", () => {
145
+ it("should render error icon markup when appearance is error", () => {
146
+ const { container } = render(<AlertDefaultIcon appearance="error" />);
147
+ expect(container.querySelector("svg")).toBeTruthy();
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,9 @@
1
+ // alert.tsx ← users import this by default
2
+ import { AlertBase } from "./alert-base";
3
+ import type { AlertProps } from "./types";
4
+
5
+ export const Alert = (props: AlertProps) => {
6
+ return <AlertBase {...props} />;
7
+ }
8
+
9
+ Alert.displayName = "Alert";
@@ -0,0 +1,20 @@
1
+ import { motion } from "framer-motion";
2
+ import { AlertBase } from "../alert-base";
3
+ import { alertAnimationPresets } from "./animations";
4
+ import type { AlertAnimatedProps } from "./types";
5
+
6
+ export const AlertAnimated = ({
7
+ animation = "none",
8
+ ...props
9
+ }: AlertAnimatedProps) => {
10
+ const motionProps = alertAnimationPresets[animation];
11
+
12
+ return (
13
+ <AlertBase
14
+ as={motion.div}
15
+ initial={animation === "none" ? false : undefined}
16
+ {...motionProps}
17
+ {...props}
18
+ />
19
+ );
20
+ };
@@ -0,0 +1,20 @@
1
+ import { AlertAnimationPresets } from "./types";
2
+
3
+ export const alertAnimationPresets: AlertAnimationPresets = {
4
+ none: {},
5
+ "slide-down": {
6
+ initial: { y: -8, opacity: 0 },
7
+ animate: { y: 0, opacity: 1 },
8
+ transition: { type: "spring", stiffness: 420, damping: 30 },
9
+ },
10
+ fade: {
11
+ initial: { opacity: 0 },
12
+ animate: { opacity: 1 },
13
+ transition: { duration: 0.2 },
14
+ },
15
+ pop: {
16
+ initial: { scale: 0.96, opacity: 0 },
17
+ animate: { scale: 1, opacity: 1 },
18
+ transition: { type: "spring", stiffness: 520, damping: 28 },
19
+ },
20
+ };
@@ -0,0 +1,3 @@
1
+ export { AlertAnimated } from "./alert-animated";
2
+ export type { AlertAnimatedProps} from "./types";
3
+ export { alertAnimationPresets } from "./animations";
@@ -0,0 +1,16 @@
1
+ import { HTMLMotionProps } from "framer-motion";
2
+ import type { AlertBaseProps, AlertAnimation } from "../types";
3
+
4
+ export type AlertAnimatedProps = Omit<AlertBaseProps, "as"> & {
5
+ animation?: AlertAnimation;
6
+ };
7
+
8
+ export type AlertPresetMotionProps = Pick<
9
+ HTMLMotionProps<"div">,
10
+ "initial" | "animate" | "transition"
11
+ >;
12
+
13
+ export type AlertAnimationPresets = Record<
14
+ AlertAnimation,
15
+ AlertPresetMotionProps
16
+ >;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ export {
4
+ AlertClose,
5
+ AlertDefaultIcon,
6
+ AlertDescription,
7
+ AlertIcon,
8
+ AlertTitle,
9
+ } from "./alert-base";
10
+ export { Alert } from "./alert";
11
+
12
+ export type {
13
+ AlertAnimation,
14
+ AlertProps,
15
+ AlertSectionProps,
16
+ AlertSize,
17
+ } from "./types";
18
+ export {
19
+ alertVariants,
20
+ alertTitleVariants,
21
+ alertDescriptionVariants,
22
+ } from "./variants";
@@ -0,0 +1,28 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ElementType, ReactNode } from "react";
3
+
4
+ import type { alertVariants } from "./variants";
5
+
6
+ export type AlertAnimation = "none" | "slide-down" | "fade" | "pop";
7
+ export type AlertAppearance = VariantProps<typeof alertVariants>["appearance"];
8
+ export type AlertSize = VariantProps<typeof alertVariants>["size"];
9
+
10
+ export interface AlertBaseProps extends ComponentPropsWithRef<"div"> {
11
+ appearance?: AlertAppearance;
12
+ size?: AlertSize;
13
+ closable?: boolean;
14
+ onClose?: () => void;
15
+ closeLabel?: string;
16
+ triggerClassName?: string;
17
+ // Accept the wrapper element/component as a prop — defaults to "div"
18
+ as?: ElementType;
19
+ }
20
+
21
+
22
+ export type AlertSectionProps = {
23
+ className?: string;
24
+ children?: ReactNode;
25
+ };
26
+
27
+ export type AlertProps = Omit<AlertBaseProps, "as">;
28
+
@@ -0,0 +1,74 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const alertVariants = cva(
4
+ "relative flex w-full gap-3 border text-sm ring-offset-slate-950 transition-colors",
5
+ {
6
+ variants: {
7
+ appearance: {
8
+ default: "border-white/10 bg-white/5 text-slate-50",
9
+ success: "border-emerald-500/40 bg-emerald-500/10 text-emerald-50",
10
+ warning: "border-amber-500/40 bg-amber-500/10 text-amber-50",
11
+ error: "border-rose-500/50 bg-rose-500/10 text-rose-50",
12
+ info: "border-sky-500/40 bg-sky-500/10 text-sky-50",
13
+ ghost: "border-transparent bg-transparent text-slate-200",
14
+ purple: "border-purple-600 bg-purple-950/70 backdrop-blur-xl",
15
+ pink: "border-pink-600 bg-pink-950/70 backdrop-blur-xl",
16
+ orange: "border-orange-600 bg-orange-950/70 backdrop-blur-xl",
17
+ yellow: "border-yellow-600 bg-yellow-950/70 backdrop-blur-xl",
18
+ teal: "border-teal-600 bg-teal-950/70 backdrop-blur-xl",
19
+ indigo: "border-indigo-600 bg-indigo-950/70 backdrop-blur-xl",
20
+ gray: "border-gray-600 bg-gray-950/70 backdrop-blur-xl",
21
+ violet: "border-violet-600 bg-violet-950/70 backdrop-blur-xl",
22
+ "gradient-blue":
23
+ "border-gradient-to-r from-blue-600 to-purple-600 bg-gradient-to-r from-blue-950/70 to-purple-950/70 backdrop-blur-xl",
24
+ "gradient-green":
25
+ "border-gradient-to-r from-green-600 to-lime-600 bg-gradient-to-r from-green-950/70 to-lime-950/70 backdrop-blur-xl",
26
+ "gradient-red":
27
+ "border-gradient-to-r from-red-600 to-pink-600 bg-gradient-to-r from-red-950/70 to-pink-950/70 backdrop-blur-xl",
28
+ "gradient-yellow":
29
+ "border-gradient-to-r from-yellow-600 to-orange-600 bg-gradient-to-r from-yellow-950/70 to-orange-950/70 backdrop-blur-xl",
30
+ "gradient-purple":
31
+ "border-gradient-to-r from-purple-600 to-pink-600 bg-gradient-to-r from-purple-950/70 to-pink-950/70 backdrop-blur-xl",
32
+ "gradient-teal":
33
+ "border-gradient-to-r from-teal-600 to-cyan-600 bg-gradient-to-r from-teal-950/70 to-cyan-950/70 backdrop-blur-xl",
34
+ "gradient-indigo":
35
+ "border-gradient-to-r from-indigo-600 to-purple-600 bg-gradient-to-r from-indigo-950/70 to-purple-950/70 backdrop-blur-xl",
36
+ "gradient-pink":
37
+ "border-gradient-to-r from-pink-600 to-rose-600 bg-gradient-to-r from-pink-950/70 to-rose-950/70 backdrop-blur-xl",
38
+ "gradient-orange":
39
+ "border-gradient-to-r from-orange-600 to-red-600 bg-gradient-to-r from-orange-950/70 to-red-950/70 backdrop-blur-xl",
40
+ },
41
+ size: {
42
+ sm: "rounded-lg p-3",
43
+ md: "rounded-xl p-4",
44
+ lg: "rounded-2xl p-5 text-base",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ appearance: "default",
49
+ size: "md",
50
+ },
51
+ },
52
+ );
53
+
54
+ export const alertTitleVariants = cva("font-semibold leading-tight", {
55
+ variants: {
56
+ size: {
57
+ sm: "text-xs md:text-sm",
58
+ md: "text-xs md:text-sm",
59
+ lg: "text-xs md:text-sm",
60
+ },
61
+ },
62
+ defaultVariants: { size: "md" },
63
+ });
64
+
65
+ export const alertDescriptionVariants = cva("text-slate-300", {
66
+ variants: {
67
+ size: {
68
+ sm: "text-xs md:text-sm",
69
+ md: "text-xs md:text-sm",
70
+ lg: "text-xs md:text-sm",
71
+ },
72
+ },
73
+ defaultVariants: { size: "md" },
74
+ });
@@ -0,0 +1,11 @@
1
+ import { AvatarAnimationPresets } from "./types";
2
+
3
+ export const avatarAnimationPresets: AvatarAnimationPresets = {
4
+ none: { initial: false },
5
+ subtle: {
6
+ initial: false,
7
+ whileHover: { scale: 1.03 },
8
+ whileTap: { scale: 0.98 },
9
+ transition: { type: "spring", stiffness: 420, damping: 28 },
10
+ },
11
+ };
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ import { avatarAnimationPresets } from "./animations";
6
+ import { AvatarBase } from "../avatar-base";
7
+ import type { AvatarAnimatedProps } from "./types";
8
+
9
+ export function AvatarAnimated({
10
+ animation = "none",
11
+ ...props
12
+ }: AvatarAnimatedProps) {
13
+ const motionProps = avatarAnimationPresets[animation];
14
+
15
+ return (
16
+ <AvatarBase
17
+ as={motion.span}
18
+ initial={animation === "none" ? false : undefined}
19
+ {...motionProps}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ AvatarAnimated.displayName = "AvatarAnimated";
@@ -0,0 +1,6 @@
1
+ "use client";
2
+
3
+ export { AvatarAnimated } from "./avatar-animated";
4
+ export { AvatarFallback, AvatarGroup, AvatarImage } from "../avatar-base";
5
+ export type { AvatarAnimatedProps, AvatarAnimation, AvatarAnimationPresets } from "./types";
6
+ export { avatarAnimationPresets } from "./animations";
@@ -0,0 +1,16 @@
1
+ import type { AvatarBaseProps } from "../types";
2
+ import { HTMLMotionProps } from "framer-motion";
3
+
4
+ export type AvatarAnimatedProps = Omit<AvatarBaseProps, "as"> & {
5
+ animation?: AvatarAnimation;
6
+ };
7
+
8
+ export type AvatarAnimationPresets = Record<
9
+ AvatarAnimation,
10
+ Pick<
11
+ HTMLMotionProps<"span">,
12
+ "whileHover" | "whileTap" | "transition" | "initial"
13
+ >
14
+ >;
15
+
16
+ export type AvatarAnimation = "none" | "subtle";
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ import {
4
+ Children,
5
+ createContext,
6
+ isValidElement,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useState,
11
+ type Ref,
12
+ } from "react";
13
+
14
+ import { cn } from "../../lib/utils";
15
+
16
+ import type {
17
+ AvatarBaseProps,
18
+ AvatarCtx,
19
+ AvatarFallbackProps,
20
+ AvatarGroupProps,
21
+ AvatarImageProps,
22
+ ImageStatus,
23
+ } from "./types";
24
+ import {
25
+ avatarFallbackVariants,
26
+ avatarGroupVariants,
27
+ avatarImageVariants,
28
+ avatarVariants,
29
+ } from "./variants";
30
+
31
+ const AvatarContext = createContext<AvatarCtx | null>(null);
32
+
33
+ function useAvatarContext(component: string): AvatarCtx {
34
+ const ctx = useContext(AvatarContext);
35
+ if (!ctx) {
36
+ throw new Error(`${component} must be used within <Avatar>`);
37
+ }
38
+ return ctx;
39
+ }
40
+
41
+ export function AvatarBase({
42
+ className,
43
+ size,
44
+ appearance,
45
+ children,
46
+ ref,
47
+ as: Wrapper = "span",
48
+ ...rest
49
+ }: AvatarBaseProps) {
50
+ const [imageStatus, setImageStatus] = useState<ImageStatus>("idle");
51
+ const ctx = useMemo(
52
+ () => ({ imageStatus, setImageStatus }),
53
+ [imageStatus],
54
+ );
55
+
56
+ return (
57
+ <AvatarContext.Provider value={ctx}>
58
+ <Wrapper
59
+ ref={ref}
60
+ data-slot="avatar"
61
+ className={cn(avatarVariants({ size, appearance }), className)}
62
+ {...rest}
63
+ >
64
+ {children}
65
+ </Wrapper>
66
+ </AvatarContext.Provider>
67
+ );
68
+ }
69
+
70
+ AvatarBase.displayName = "Avatar";
71
+
72
+ export function AvatarImage({
73
+ className,
74
+ onLoad,
75
+ onError,
76
+ ref,
77
+ ...rest
78
+ }: AvatarImageProps & { ref?: Ref<HTMLImageElement> }) {
79
+ const { setImageStatus } = useAvatarContext("AvatarImage");
80
+
81
+ return (
82
+ <img
83
+ ref={ref}
84
+ data-slot="avatar-image"
85
+ className={cn(avatarImageVariants(), className)}
86
+ onLoad={(e) => {
87
+ setImageStatus("loaded");
88
+ onLoad?.(e);
89
+ }}
90
+ onError={(e) => {
91
+ setImageStatus("error");
92
+ onError?.(e);
93
+ }}
94
+ {...rest}
95
+ />
96
+ );
97
+ }
98
+
99
+ AvatarImage.displayName = "AvatarImage";
100
+
101
+ export function AvatarFallback({
102
+ className,
103
+ delayMs = 0,
104
+ ref,
105
+ ...rest
106
+ }: AvatarFallbackProps & { ref?: Ref<HTMLSpanElement> }) {
107
+ const { imageStatus } = useAvatarContext("AvatarFallback");
108
+ const [show, setShow] = useState(delayMs === 0);
109
+
110
+ useEffect(() => {
111
+ if (imageStatus === "loaded") {
112
+ setShow(false);
113
+ return;
114
+ }
115
+ if (imageStatus === "error") {
116
+ setShow(true);
117
+ return;
118
+ }
119
+ if (delayMs <= 0) {
120
+ setShow(true);
121
+ return;
122
+ }
123
+ const t = window.setTimeout(() => setShow(true), delayMs);
124
+ return () => window.clearTimeout(t);
125
+ }, [delayMs, imageStatus]);
126
+
127
+ if (!show) {
128
+ return null;
129
+ }
130
+
131
+ return (
132
+ <span
133
+ ref={ref}
134
+ data-slot="avatar-fallback"
135
+ className={cn(avatarFallbackVariants(), className)}
136
+ {...rest}
137
+ />
138
+ );
139
+ }
140
+
141
+ AvatarFallback.displayName = "AvatarFallback";
142
+
143
+ export function AvatarGroup({
144
+ className,
145
+ max,
146
+ children,
147
+ ref,
148
+ ...rest
149
+ }: AvatarGroupProps & { ref?: Ref<HTMLDivElement> }) {
150
+ const childArray = useMemo(
151
+ () => Children.toArray(children).filter(isValidElement),
152
+ [children],
153
+ );
154
+ const visible = max !== undefined ? childArray.slice(0, max) : childArray;
155
+ const overflow =
156
+ max !== undefined && childArray.length > max
157
+ ? childArray.length - max
158
+ : 0;
159
+
160
+ return (
161
+ <div
162
+ ref={ref}
163
+ data-slot="avatar-group"
164
+ className={cn(avatarGroupVariants(), className)}
165
+ {...rest}
166
+ >
167
+ {visible}
168
+ {overflow > 0 ? (
169
+ <span
170
+ data-slot="avatar-group-overflow"
171
+ className={cn(
172
+ avatarVariants({ size: "md" }),
173
+ "z-10 grid place-items-center bg-slate-800 text-xs font-semibold text-white",
174
+ )}
175
+ aria-label={`${overflow} more`}
176
+ >
177
+ +{overflow}
178
+ </span>
179
+ ) : null}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ AvatarGroup.displayName = "AvatarGroup";
@@ -0,0 +1,51 @@
1
+ import { createRef } from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ import { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from "./avatar";
6
+
7
+ describe("Avatar", () => {
8
+ it("should expose displayName", () => {
9
+ expect(Avatar.displayName).toBe("Avatar");
10
+ expect(AvatarImage.displayName).toBe("AvatarImage");
11
+ expect(AvatarFallback.displayName).toBe("AvatarFallback");
12
+ expect(AvatarGroup.displayName).toBe("AvatarGroup");
13
+ });
14
+
15
+ it("should stamp data-slot on avatar root", () => {
16
+ render(
17
+ <Avatar>
18
+ <AvatarFallback>AB</AvatarFallback>
19
+ </Avatar>,
20
+ );
21
+ expect(document.querySelector('[data-slot="avatar"]')).toBeTruthy();
22
+ });
23
+
24
+ it("should forward ref on Avatar", () => {
25
+ const ref = createRef<HTMLSpanElement>();
26
+ render(
27
+ <Avatar ref={ref}>
28
+ <AvatarFallback>Z</AvatarFallback>
29
+ </Avatar>,
30
+ );
31
+ expect(ref.current?.getAttribute("data-slot")).toBe("avatar");
32
+ });
33
+
34
+ it("should render overflow count in AvatarGroup", () => {
35
+ render(
36
+ <AvatarGroup max={2}>
37
+ <Avatar>
38
+ <AvatarFallback>A</AvatarFallback>
39
+ </Avatar>
40
+ <Avatar>
41
+ <AvatarFallback>B</AvatarFallback>
42
+ </Avatar>
43
+ <Avatar>
44
+ <AvatarFallback>C</AvatarFallback>
45
+ </Avatar>
46
+ </AvatarGroup>,
47
+ );
48
+ expect(screen.getByLabelText("1 more")).toBeInTheDocument();
49
+ expect(screen.getByText("+1")).toBeInTheDocument();
50
+ });
51
+ });
@@ -0,0 +1,11 @@
1
+ // avatar.tsx — default static entry (no framer-motion)
2
+ import { AvatarBase, AvatarFallback, AvatarGroup, AvatarImage } from "./avatar-base";
3
+ import type { AvatarProps } from "./types";
4
+
5
+ export function Avatar(props: AvatarProps) {
6
+ return <AvatarBase {...props} />;
7
+ }
8
+
9
+ Avatar.displayName = "Avatar";
10
+
11
+ export { AvatarFallback, AvatarGroup, AvatarImage };
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ export { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from "./avatar";
4
+ export type { AvatarBaseProps } from "./types";
5
+ export type {
6
+ AvatarFallbackProps,
7
+ AvatarGroupProps,
8
+ AvatarImageProps,
9
+ AvatarProps,
10
+ } from "./types";
11
+ export {
12
+ avatarFallbackVariants,
13
+ avatarGroupVariants,
14
+ avatarImageVariants,
15
+ avatarVariants,
16
+ } from "./variants";