@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,19 @@
1
+ import { EmptyStateAnimationPresets } from "./types";
2
+
3
+ export const emptyStateAnimationPresets: EmptyStateAnimationPresets = {
4
+ none: {},
5
+ float: {
6
+ whileHover: { y: -4 },
7
+ transition: { type: "spring", stiffness: 260, damping: 22 },
8
+ },
9
+ fade: {
10
+ initial: { opacity: 0, y: 8 },
11
+ animate: { opacity: 1, y: 0 },
12
+ transition: { duration: 0.25, ease: "easeOut" },
13
+ },
14
+ "slide-up": {
15
+ initial: { opacity: 0, y: 16 },
16
+ animate: { opacity: 1, y: 0 },
17
+ transition: { type: "spring", stiffness: 380, damping: 28 },
18
+ },
19
+ };
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import { emptyStateAnimationPresets } from "./animations";
5
+ import type { EmptyStateAnimatedProps } from "./types";
6
+ import {
7
+ EmptyStateBase,
8
+ } from "../empty-state-base";
9
+
10
+ export function EmptyStateAnimated(props: EmptyStateAnimatedProps) {
11
+ const { animation = "none" } = props;
12
+ const motionProps = emptyStateAnimationPresets[animation];
13
+ return (
14
+ <EmptyStateBase
15
+ as={motion.section}
16
+ initial={animation === "none" ? false : undefined}
17
+ {...motionProps}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ EmptyStateAnimated.displayName = "EmptyState";
@@ -0,0 +1,7 @@
1
+ "use client";
2
+
3
+ export {
4
+ EmptyStateAnimated,
5
+ } from "./empty-state-animated";
6
+ export type { EmptyStateAnimatedProps, EmptyStateAnimation, EmptyStateVariantProps, EmptyStateAnimationPresets } from "./types";
7
+ export { emptyStateAnimationPresets } from "./animations";
@@ -0,0 +1,26 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { HTMLMotionProps } from "framer-motion";
3
+ import type { ReactNode } from "react";
4
+
5
+ import type { emptyStateVariants } from "../variants";
6
+ import { EmptyStateProps } from "../types";
7
+
8
+ export type EmptyStateAnimation = "none" | "float" | "fade" | "slide-up";
9
+
10
+ export type EmptyStateVariantProps = VariantProps<typeof emptyStateVariants>;
11
+
12
+ export type EmptyStateAnimatedProps = EmptyStateVariantProps &
13
+ Omit<EmptyStateProps, "children"> & {
14
+ animation?: EmptyStateAnimation;
15
+ children?: ReactNode;
16
+ };
17
+
18
+ export type EmptyStatePresetMotionProps = Pick<
19
+ HTMLMotionProps<"div">,
20
+ "initial" | "animate" | "transition" | "whileHover"
21
+ >;
22
+
23
+ export type EmptyStateAnimationPresets = Record<
24
+ EmptyStateAnimation,
25
+ EmptyStatePresetMotionProps
26
+ >;
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useMemo } from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ import type { EmptyStateProps, EmptyStateSectionProps, EmptyStateSize } from "./types";
8
+ import {
9
+ emptyStateDescriptionVariants,
10
+ emptyStateTitleVariants,
11
+ emptyStateVariants,
12
+ } from "./variants";
13
+
14
+ export const EmptyStateSizeContext = createContext<EmptyStateSize>("md");
15
+
16
+ function useEmptyStateSize(): EmptyStateSize {
17
+ return useContext(EmptyStateSizeContext);
18
+ }
19
+
20
+ export function EmptyStateBase(props: EmptyStateProps) {
21
+ const {
22
+ className,
23
+ size = "md",
24
+ appearance,
25
+ align,
26
+ children,
27
+ ref,
28
+ as: Wrapper = "section",
29
+ ...rest
30
+ } = props;
31
+ const ctx = useMemo(() => size ?? "md", [size]);
32
+
33
+ return (
34
+ <EmptyStateSizeContext.Provider value={ctx}>
35
+ <Wrapper
36
+ ref={ref}
37
+ data-slot="empty-state"
38
+ aria-live="polite"
39
+ className={cn(
40
+ emptyStateVariants({ size, appearance, align }),
41
+ className,
42
+ )}
43
+ {...rest}
44
+ >
45
+ {children}
46
+ </Wrapper>
47
+ </EmptyStateSizeContext.Provider>
48
+ );
49
+ }
50
+
51
+ EmptyStateBase.displayName = "EmptyState";
52
+
53
+ export function EmptyStateIcon({
54
+ className,
55
+ children,
56
+ }: EmptyStateSectionProps) {
57
+ return (
58
+ <div
59
+ data-slot="empty-state-icon"
60
+ className={cn("text-slate-300", className)}
61
+ >
62
+ {children}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ EmptyStateIcon.displayName = "EmptyStateIcon";
68
+
69
+ export function EmptyStateTitle({
70
+ className,
71
+ children,
72
+ }: EmptyStateSectionProps) {
73
+ const size = useEmptyStateSize();
74
+ return (
75
+ <h2
76
+ data-slot="empty-state-title"
77
+ className={cn(emptyStateTitleVariants({ size }), className)}
78
+ >
79
+ {children}
80
+ </h2>
81
+ );
82
+ }
83
+
84
+ EmptyStateTitle.displayName = "EmptyStateTitle";
85
+
86
+ export function EmptyStateDescription({
87
+ className,
88
+ children,
89
+ }: EmptyStateSectionProps) {
90
+ const size = useEmptyStateSize();
91
+ return (
92
+ <p
93
+ data-slot="empty-state-description"
94
+ className={cn(emptyStateDescriptionVariants({ size }), className)}
95
+ >
96
+ {children}
97
+ </p>
98
+ );
99
+ }
100
+
101
+ EmptyStateDescription.displayName = "EmptyStateDescription";
102
+
103
+ export function EmptyStateAction({
104
+ className,
105
+ children,
106
+ }: EmptyStateSectionProps) {
107
+ return (
108
+ <div data-slot="empty-state-action" className={cn("mt-2", className)}>
109
+ {children}
110
+ </div>
111
+ );
112
+ }
113
+
114
+ EmptyStateAction.displayName = "EmptyStateAction";
@@ -0,0 +1,9 @@
1
+ // empty-state.tsx — default static entry (no framer-motion)
2
+ import { EmptyStateBase } from "./empty-state-base";
3
+ import type { EmptyStateProps } from "./types";
4
+
5
+ export function EmptyState(props: EmptyStateProps) {
6
+ return <EmptyStateBase {...props} />;
7
+ }
8
+
9
+ EmptyState.displayName = "EmptyState";
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ export { EmptyState } from "./empty-state";
4
+ export { EmptyStateAction, EmptyStateBase, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle } from "./empty-state-base";
5
+ export type { EmptyStateProps, EmptyStateSectionProps } from "./types";
6
+ export {
7
+ emptyStateVariants,
8
+ emptyStateTitleVariants,
9
+ emptyStateDescriptionVariants,
10
+ } from "./variants";
@@ -0,0 +1,19 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef, ElementType, ReactNode } from "react";
3
+
4
+ import type { emptyStateVariants } from "./variants";
5
+
6
+ type EmptyStateVariantProps = VariantProps<typeof emptyStateVariants>;
7
+
8
+ export type EmptyStateProps = EmptyStateVariantProps &
9
+ (Omit<ComponentPropsWithRef<"section">, "children"> & {
10
+ children?: ReactNode;
11
+ as?: ElementType;
12
+ });
13
+
14
+ export type EmptyStateSectionProps = {
15
+ className?: string;
16
+ children?: ReactNode;
17
+ };
18
+
19
+ export type EmptyStateSize = NonNullable<EmptyStateProps["size"]>;
@@ -0,0 +1,51 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const emptyStateVariants = cva(
4
+ "flex w-full flex-col items-center text-center",
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: "gap-2 p-4 text-sm",
9
+ md: "gap-3 p-6 text-sm",
10
+ lg: "gap-4 p-8 text-base",
11
+ },
12
+ appearance: {
13
+ default: "text-slate-50",
14
+ ghost: "text-slate-200",
15
+ card: "rounded-2xl border border-white/10 bg-white/5 p-8 text-slate-50 shadow-[0_18px_48px_rgba(15,23,42,0.35)]",
16
+ },
17
+ align: {
18
+ start: "items-start text-left",
19
+ center: "items-center text-center",
20
+ end: "items-end text-right",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ size: "md",
25
+ appearance: "default",
26
+ align: "center",
27
+ },
28
+ },
29
+ );
30
+
31
+ export const emptyStateTitleVariants = cva("font-semibold tracking-tight", {
32
+ variants: {
33
+ size: {
34
+ sm: "text-base",
35
+ md: "text-lg",
36
+ lg: "text-xl",
37
+ },
38
+ },
39
+ defaultVariants: { size: "md" },
40
+ });
41
+
42
+ export const emptyStateDescriptionVariants = cva("max-w-md text-slate-400", {
43
+ variants: {
44
+ size: {
45
+ sm: "text-xs",
46
+ md: "text-sm",
47
+ lg: "text-base",
48
+ },
49
+ },
50
+ defaultVariants: { size: "md" },
51
+ });
@@ -0,0 +1,36 @@
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 { FileUpload } from "./file-upload";
7
+
8
+ describe("FileUpload", () => {
9
+ it("should expose displayName", () => {
10
+ expect(FileUpload.displayName).toBe("FileUpload");
11
+ });
12
+
13
+ it("should stamp data-slot", () => {
14
+ render(<FileUpload />);
15
+ expect(document.querySelector('[data-slot="file-upload"]')).toBeTruthy();
16
+ });
17
+
18
+ it("should call onFiles when user selects a file", async () => {
19
+ const user = userEvent.setup();
20
+ const onFiles = vi.fn();
21
+ render(<FileUpload onFiles={onFiles} />);
22
+ const input = document.querySelector(
23
+ 'input[type="file"]',
24
+ ) as HTMLInputElement;
25
+ const file = new File(["hello"], "notes.txt", { type: "text/plain" });
26
+ await user.upload(input, file);
27
+ expect(onFiles).toHaveBeenCalledTimes(1);
28
+ expect(onFiles.mock.calls[0]?.[0]?.[0]?.name).toBe("notes.txt");
29
+ });
30
+
31
+ it("should forward ref", () => {
32
+ const ref = createRef<HTMLDivElement>();
33
+ render(<FileUpload ref={ref} />);
34
+ expect(ref.current?.getAttribute("data-slot")).toBe("file-upload");
35
+ });
36
+ });
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import { useCallback, useId, useState } from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ import type { FileUploadProps } from "./types";
8
+ import { fileUploadVariants } from "./variants";
9
+
10
+ export function FileUpload({
11
+ className,
12
+ onFiles,
13
+ accept,
14
+ multiple = false,
15
+ disabled = false,
16
+ name,
17
+ children,
18
+ onChange,
19
+ ref,
20
+ appearance = "idle",
21
+ ...rest
22
+ }: FileUploadProps & { ref?: React.Ref<HTMLDivElement> }) {
23
+ const inputId = useId();
24
+ const [dragOver, setDragOver] = useState(false);
25
+
26
+ const emitFiles = useCallback(
27
+ (fileList: FileList | null) => {
28
+ if (!fileList?.length) {
29
+ return;
30
+ }
31
+ onFiles?.(Array.from(fileList));
32
+ },
33
+ [onFiles],
34
+ );
35
+
36
+ const handleChange = useCallback(
37
+ (event: React.ChangeEvent<HTMLInputElement>) => {
38
+ onChange?.(event);
39
+ emitFiles(event.target.files);
40
+ event.target.value = "";
41
+ },
42
+ [emitFiles, onChange],
43
+ );
44
+
45
+ const handleDrop = useCallback(
46
+ (event: React.DragEvent<HTMLLabelElement>) => {
47
+ event.preventDefault();
48
+ setDragOver(false);
49
+ if (disabled) {
50
+ return;
51
+ }
52
+ emitFiles(event.dataTransfer.files);
53
+ },
54
+ [disabled, emitFiles],
55
+ );
56
+
57
+ return (
58
+ <div
59
+ ref={ref}
60
+ data-slot="file-upload"
61
+ className={cn(fileUploadVariants({ appearance }), className)}
62
+ {...rest}
63
+ >
64
+ <input
65
+ id={inputId}
66
+ name={name}
67
+ type="file"
68
+ className="sr-only"
69
+ accept={accept}
70
+ multiple={multiple}
71
+ disabled={disabled}
72
+ onChange={handleChange}
73
+ aria-hidden
74
+ tabIndex={-1}
75
+ />
76
+ <label
77
+ htmlFor={inputId}
78
+ className={cn(
79
+ "flex w-full cursor-pointer flex-col items-center gap-2",
80
+ disabled && "cursor-not-allowed",
81
+ )}
82
+ onDragEnter={(e) => {
83
+ e.preventDefault();
84
+ if (!disabled) {
85
+ setDragOver(true);
86
+ }
87
+ }}
88
+ onDragOver={(e) => {
89
+ e.preventDefault();
90
+ if (!disabled) {
91
+ setDragOver(true);
92
+ }
93
+ }}
94
+ onDragLeave={() => setDragOver(false)}
95
+ onDrop={handleDrop}
96
+ >
97
+ {children ?? (
98
+ <>
99
+ <span className="text-base font-semibold text-slate-100">
100
+ Drop files here
101
+ </span>
102
+ <span className="text-xs text-slate-400">
103
+ or click to browse from your device
104
+ </span>
105
+ </>
106
+ )}
107
+ {
108
+ dragOver && (
109
+ <span className="text-xs text-slate-400">
110
+ Drop files here
111
+ </span>
112
+ )
113
+ }
114
+ </label>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ FileUpload.displayName = "FileUpload";
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ export { FileUpload } from "./file-upload";
4
+ export type { FileUploadProps, FileUploadVariantProps } from "./types";
5
+ export { fileUploadVariants } from "./variants";
@@ -0,0 +1,21 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
3
+
4
+ import type { fileUploadVariants } from "./variants";
5
+
6
+ export type FileUploadVariantProps = VariantProps<typeof fileUploadVariants>;
7
+
8
+ export type FileUploadProps = FileUploadVariantProps &
9
+ Omit<ComponentPropsWithoutRef<"div">, "children" | "onChange"> & {
10
+ /** Called when the user selects or drops files */
11
+ onFiles?: (files: File[]) => void;
12
+ accept?: string;
13
+ multiple?: boolean;
14
+ disabled?: boolean;
15
+ /** Visually hidden input `name` for forms */
16
+ name?: string;
17
+ /** Optional controlled file list (not enforced; mainly for forms) */
18
+ children?: ReactNode;
19
+ onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
20
+ appearance?: FileUploadVariantProps["appearance"];
21
+ };
@@ -0,0 +1,29 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const fileUploadVariants = cva(
4
+ "relative flex cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border border-dashed px-6 py-10 text-center text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-violet-400/60 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950",
5
+ {
6
+ variants: {
7
+ appearance: {
8
+ idle: "border-white/15 bg-white/5 text-slate-300 hover:border-white/25 hover:bg-white/[0.07]",
9
+ active: "border-violet-400/70 bg-violet-500/10 text-white",
10
+ disabled: "cursor-not-allowed opacity-50",
11
+ error: "border-red-400/70 bg-red-500/10 text-red-400 hover:border-red-400/80 hover:bg-red-500/15",
12
+ success: "border-green-400/70 bg-green-500/10 text-green-400 hover:border-green-400/80 hover:bg-green-500/15",
13
+ warning: "border-yellow-400/70 bg-yellow-500/10 text-yellow-400 hover:border-yellow-400/80 hover:bg-yellow-500/15",
14
+ info: "border-blue-400/70 bg-blue-500/10 text-blue-400 hover:border-blue-400/80 hover:bg-blue-500/15",
15
+ neutral: "border-gray-400/70 bg-gray-500/10 text-gray-400 hover:border-gray-400/80 hover:bg-gray-500/15",
16
+ purple: "border-purple-400/70 bg-purple-500/10 text-purple-400 hover:border-purple-400/80 hover:bg-purple-500/15",
17
+ indigo: "border-indigo-400/70 bg-indigo-500/10 text-indigo-400 hover:border-indigo-400/80 hover:bg-indigo-500/15",
18
+ emerald: "border-emerald-400/70 bg-emerald-500/10 text-emerald-400 hover:border-emerald-400/80 hover:bg-emerald-500/15",
19
+ amber: "border-amber-400/70 bg-amber-500/10 text-amber-400 hover:border-amber-400/80 hover:bg-amber-500/15",
20
+ pink: "border-pink-400/70 bg-pink-500/10 text-pink-400 hover:border-pink-400/80 hover:bg-pink-500/15",
21
+ orange: "border-orange-400/70 bg-orange-500/10 text-orange-400 hover:border-orange-400/80 hover:bg-orange-500/15",
22
+ teal: "border-teal-400/70 bg-teal-500/10 text-teal-400 hover:border-teal-400/80 hover:bg-teal-500/15",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ appearance: "idle",
27
+ },
28
+ },
29
+ );
@@ -0,0 +1,36 @@
1
+ import type { InputAnimationPresets } from "./types";
2
+
3
+ export const inputAnimationPresets: InputAnimationPresets = {
4
+ none: {},
5
+ lift: {
6
+ whileHover: { y: -1 },
7
+ whileFocus: { y: -1 },
8
+ transition: { type: "spring", stiffness: 480, damping: 32 },
9
+ },
10
+ press: {
11
+ whileTap: { scale: 0.99 },
12
+ transition: { type: "spring", stiffness: 520, damping: 30 },
13
+ },
14
+ glow: {
15
+ whileFocus: {
16
+ boxShadow:
17
+ "0 0 0 1px rgba(255,255,255,0.2), 0 12px 28px rgba(15,23,42,0.35)",
18
+ },
19
+ whileHover: {
20
+ boxShadow:
21
+ "0 0 0 1px rgba(255,255,255,0.12), 0 8px 20px rgba(15,23,42,0.25)",
22
+ },
23
+ transition: { duration: 0.2, ease: "easeOut" },
24
+ },
25
+ tilt: {
26
+ whileHover: { scale: 1.005 },
27
+ whileFocus: { scale: 1.008 },
28
+ whileTap: { scale: 0.995 },
29
+ transition: { type: "spring", stiffness: 380, damping: 24 },
30
+ },
31
+ bounce: {
32
+ whileFocus: { y: -2, scale: 1.01 },
33
+ whileHover: { y: -1, scale: 1.005 },
34
+ transition: { type: "spring", bounce: 0.35, duration: 0.4 },
35
+ },
36
+ };
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ export { InputAnimated } from "./input-animated";
4
+ export type { InputAnimatedProps, InputAnimation, InputAnimationPresets, InputPresetMotionProps, InputSharedAnimatedProps } from "./types";
5
+ export { inputAnimationPresets } from "./animations";
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import { useId } from "react";
4
+ import { motion } from "framer-motion";
5
+
6
+ import { cn } from "../../../lib/utils";
7
+
8
+ import { inputAnimationPresets } from "./animations";
9
+ import type { InputAnimatedProps } from "./types";
10
+ import { inputVariants } from "../variants";
11
+
12
+ export const InputAnimated = (props: InputAnimatedProps) => {
13
+ const generatedId = useId();
14
+
15
+ if (props.as === "textarea") {
16
+ const {
17
+ className,
18
+ appearance,
19
+ size,
20
+ animation = "none",
21
+ ring = true,
22
+ ref,
23
+ "aria-invalid": ariaInvalidProp,
24
+ errorMessage,
25
+ id,
26
+ as,
27
+ ...rest
28
+ } = props;
29
+
30
+ const controlId = id ?? generatedId;
31
+ const errorId = `${controlId}-error`;
32
+ const motionProps = inputAnimationPresets[animation];
33
+ const ariaInvalid =
34
+ ariaInvalidProp !== undefined
35
+ ? ariaInvalidProp
36
+ : appearance === "error"
37
+ ? true
38
+ : undefined;
39
+
40
+ return (
41
+ <>
42
+ <motion.textarea
43
+ ref={ref}
44
+ id={controlId}
45
+ data-slot="input"
46
+ className={cn(
47
+ inputVariants({ appearance, size, ring, as }),
48
+ className,
49
+ )}
50
+ initial={false}
51
+ aria-invalid={ariaInvalid}
52
+ aria-describedby={
53
+ errorMessage && appearance === "error" ? errorId : undefined
54
+ }
55
+ {...motionProps}
56
+ {...rest}
57
+ />
58
+ {errorMessage && appearance === "error" && (
59
+ <p
60
+ id={errorId}
61
+ className="mt-2 pl-4 text-sm text-rose-500 wrap-break-word"
62
+ >
63
+ {errorMessage}
64
+ </p>
65
+ )}
66
+ </>
67
+ );
68
+ }
69
+
70
+ const {
71
+ className,
72
+ appearance,
73
+ size,
74
+ animation = "none",
75
+ ring = true,
76
+ ref,
77
+ "aria-invalid": ariaInvalidProp,
78
+ errorMessage,
79
+ id,
80
+ as,
81
+ ...rest
82
+ } = props;
83
+
84
+ const controlId = id ?? generatedId;
85
+ const errorId = `${controlId}-error`;
86
+ const motionProps = inputAnimationPresets[animation];
87
+ const ariaInvalid =
88
+ ariaInvalidProp !== undefined
89
+ ? ariaInvalidProp
90
+ : appearance === "error"
91
+ ? true
92
+ : undefined;
93
+
94
+ return (
95
+ <>
96
+ <motion.input
97
+ ref={ref}
98
+ id={controlId}
99
+ data-slot="input"
100
+ className={cn(
101
+ inputVariants({ appearance, size, ring, as: as ?? "input" }),
102
+ className,
103
+ )}
104
+ initial={false}
105
+ aria-invalid={ariaInvalid}
106
+ aria-describedby={
107
+ errorMessage && appearance === "error" ? errorId : undefined
108
+ }
109
+ {...motionProps}
110
+ {...rest}
111
+ />
112
+ {errorMessage && appearance === "error" && (
113
+ <p
114
+ id={errorId}
115
+ className="mt-2 pl-4 text-sm text-rose-500 wrap-break-word"
116
+ >
117
+ {errorMessage}
118
+ </p>
119
+ )}
120
+ </>
121
+ );
122
+ };
123
+
124
+ InputAnimated.displayName = "Input";