@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,40 @@
1
+ import type { HTMLMotionProps } from "framer-motion";
2
+ import type { VariantProps } from "class-variance-authority";
3
+
4
+ import type { inputVariants } from "../variants";
5
+
6
+ export type InputSharedAnimatedProps = Omit<
7
+ VariantProps<typeof inputVariants>,
8
+ "as"
9
+ > & {
10
+ animation?: InputAnimation;
11
+ errorMessage?: string;
12
+ };
13
+
14
+ export type InputAnimatedProps =
15
+ | (InputSharedAnimatedProps &
16
+ Omit<HTMLMotionProps<"input">, "size" | "as"> & {
17
+ as?: "input" | "file" | "checkbox" | "radio";
18
+ })
19
+ | (InputSharedAnimatedProps &
20
+ Omit<HTMLMotionProps<"textarea">, "size" | "as"> & {
21
+ as: "textarea";
22
+ });
23
+
24
+ export type InputAnimation =
25
+ | "none"
26
+ | "lift"
27
+ | "press"
28
+ | "glow"
29
+ | "tilt"
30
+ | "bounce";
31
+
32
+ export type InputPresetMotionProps = Pick<
33
+ HTMLMotionProps<"input">,
34
+ "style" | "transition" | "whileHover" | "whileTap" | "whileFocus"
35
+ >;
36
+
37
+ export type InputAnimationPresets = Record<
38
+ InputAnimation,
39
+ InputPresetMotionProps
40
+ >;
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ export { Input } from "./input";
4
+ export type { InputProps, InputSharedProps } from "./types";
5
+ export { inputVariants } from "./variants";
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import { useId } from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ import type { InputProps } from "./types";
8
+ import { inputVariants } from "./variants";
9
+
10
+ export const InputBase = (props: InputProps) => {
11
+ const generatedId = useId();
12
+
13
+ if (props.as === "textarea") {
14
+ const {
15
+ className,
16
+ appearance,
17
+ size,
18
+ ring = true,
19
+ ref,
20
+ "aria-invalid": ariaInvalidProp,
21
+ errorMessage,
22
+ id,
23
+ as,
24
+ ...rest
25
+ } = props;
26
+
27
+ const controlId = id ?? generatedId;
28
+ const errorId = `${controlId}-error`;
29
+ const ariaInvalid =
30
+ ariaInvalidProp !== undefined
31
+ ? ariaInvalidProp
32
+ : appearance === "error"
33
+ ? true
34
+ : undefined;
35
+
36
+ return (
37
+ <>
38
+ <textarea
39
+ ref={ref}
40
+ id={controlId}
41
+ data-slot="input"
42
+ className={cn(
43
+ inputVariants({ appearance, size, ring, as }),
44
+ className,
45
+ )}
46
+ aria-invalid={ariaInvalid}
47
+ aria-describedby={
48
+ errorMessage && appearance === "error" ? errorId : undefined
49
+ }
50
+ {...rest}
51
+ />
52
+ {errorMessage && appearance === "error" && (
53
+ <p
54
+ id={errorId}
55
+ className="mt-2 pl-4 text-sm text-rose-500 wrap-break-word"
56
+ >
57
+ {errorMessage}
58
+ </p>
59
+ )}
60
+ </>
61
+ );
62
+ }
63
+
64
+ const {
65
+ className,
66
+ appearance,
67
+ size,
68
+ ring = true,
69
+ ref,
70
+ "aria-invalid": ariaInvalidProp,
71
+ errorMessage,
72
+ id,
73
+ as,
74
+ ...rest
75
+ } = props;
76
+
77
+ const controlId = id ?? generatedId;
78
+ const errorId = `${controlId}-error`;
79
+ const ariaInvalid =
80
+ ariaInvalidProp !== undefined
81
+ ? ariaInvalidProp
82
+ : appearance === "error"
83
+ ? true
84
+ : undefined;
85
+
86
+ return (
87
+ <>
88
+ <input
89
+ ref={ref}
90
+ id={controlId}
91
+ data-slot="input"
92
+ className={cn(
93
+ inputVariants({ appearance, size, ring, as: as ?? "input" }),
94
+ className,
95
+ )}
96
+ aria-invalid={ariaInvalid}
97
+ aria-describedby={
98
+ errorMessage && appearance === "error" ? errorId : undefined
99
+ }
100
+ {...rest}
101
+ />
102
+ {errorMessage && appearance === "error" && (
103
+ <p
104
+ id={errorId}
105
+ className="mt-2 pl-4 text-sm text-rose-500 wrap-break-word"
106
+ >
107
+ {errorMessage}
108
+ </p>
109
+ )}
110
+ </>
111
+ );
112
+ };
113
+
114
+ InputBase.displayName = "Input";
@@ -0,0 +1,414 @@
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 { InputAnimated } from "./animated/input-animated";
7
+ import type { InputAnimation } from "./animated/types";
8
+ import { Input } from "./input";
9
+
10
+ const INPUT_SLOT_SELECTOR = '[data-slot="input"]';
11
+
12
+ function getInputSlot(container: HTMLElement = document.body) {
13
+ const elements = container.querySelectorAll(INPUT_SLOT_SELECTOR);
14
+ expect(
15
+ elements.length,
16
+ `Expected exactly one element matching ${INPUT_SLOT_SELECTOR} in the document, but found ${elements.length}`,
17
+ ).toBe(1);
18
+ return elements[0] as HTMLInputElement;
19
+ }
20
+
21
+ describe("Input (component library)", () => {
22
+ describe("public contract and metadata", () => {
23
+ it("should expose a stable displayName for devtools and documentation consumers", () => {
24
+ expect(
25
+ Input.displayName,
26
+ "Input.displayName must be set for library discoverability",
27
+ ).toBe("Input");
28
+ });
29
+
30
+ it("should stamp data-slot on the root element so apps can target the primitive", () => {
31
+ render(<Input placeholder="Field" />);
32
+ const root = getInputSlot();
33
+ expect(
34
+ root.getAttribute("data-slot"),
35
+ "data-slot must equal 'input' for the documented contract",
36
+ ).toBe("input");
37
+ });
38
+ });
39
+
40
+ describe("default rendering", () => {
41
+ it("should render a native textbox for default type", () => {
42
+ render(<Input placeholder="Email" aria-label="Email" />);
43
+ const control = screen.getByRole("textbox", { name: "Email" });
44
+ expect(control.tagName, "Default must render an INPUT element").toBe(
45
+ "INPUT",
46
+ );
47
+ });
48
+
49
+ it("should render a native textarea when as='textarea'", () => {
50
+ render(
51
+ <Input
52
+ as="textarea"
53
+ rows={3}
54
+ placeholder="Notes"
55
+ aria-label="Notes field"
56
+ />,
57
+ );
58
+ const control = screen.getByRole("textbox", { name: "Notes field" });
59
+ expect(
60
+ control.tagName,
61
+ "as='textarea' must render a TEXTAREA element",
62
+ ).toBe("TEXTAREA");
63
+ expect(
64
+ (control as HTMLTextAreaElement).rows,
65
+ "rows must pass through to the textarea",
66
+ ).toBe(3);
67
+ });
68
+
69
+ it("should use search role when type is search", () => {
70
+ render(<Input type="search" aria-label="Search" />);
71
+ expect(screen.getByRole("searchbox", { name: "Search" }).tagName).toBe(
72
+ "INPUT",
73
+ );
74
+ });
75
+ });
76
+
77
+ describe("props: appearance (variant class application)", () => {
78
+ it("should apply default appearance border tokens from the variant recipe", () => {
79
+ render(<Input placeholder="x" aria-label="x" />);
80
+ const root = getInputSlot();
81
+ expect(
82
+ root.className,
83
+ "Default appearance must include neutral border utilities",
84
+ ).toMatch(/border-white\/10/);
85
+ });
86
+
87
+ it("should apply error appearance when appearance='error'", () => {
88
+ render(<Input appearance="error" placeholder="x" aria-label="x" />);
89
+ const root = getInputSlot();
90
+ expect(
91
+ root.className,
92
+ "Error appearance must surface danger border tokens",
93
+ ).toMatch(/border-rose-500/);
94
+ });
95
+
96
+ it("should apply success appearance when appearance='success'", () => {
97
+ render(<Input appearance="success" placeholder="x" aria-label="x" />);
98
+ const root = getInputSlot();
99
+ expect(
100
+ root.className,
101
+ "Success appearance must surface positive border tokens",
102
+ ).toMatch(/border-emerald-500/);
103
+ });
104
+
105
+ const additionalAppearances: {
106
+ appearance: "warning" | "info" | "violet" | "amber" | "pink" | "indigo";
107
+ borderPattern: RegExp;
108
+ }[] = [
109
+ {
110
+ appearance: "warning",
111
+ borderPattern: /border-yellow-500/,
112
+ },
113
+ { appearance: "info", borderPattern: /border-blue-500/ },
114
+ { appearance: "violet", borderPattern: /border-violet-500/ },
115
+ { appearance: "amber", borderPattern: /border-amber-500/ },
116
+ { appearance: "pink", borderPattern: /border-pink-500/ },
117
+ { appearance: "indigo", borderPattern: /border-indigo-500/ },
118
+ ];
119
+
120
+ it.each(additionalAppearances)(
121
+ "should apply $appearance appearance border tokens from the variant recipe",
122
+ ({ appearance, borderPattern }) => {
123
+ render(
124
+ <Input appearance={appearance} placeholder="x" aria-label="x" />,
125
+ );
126
+ const root = getInputSlot();
127
+ expect(
128
+ root.className,
129
+ `${appearance} appearance must include its border scale utilities`,
130
+ ).toMatch(borderPattern);
131
+ },
132
+ );
133
+ });
134
+
135
+ describe("props: ring (variant class application)", () => {
136
+ it("should apply focus ring utilities when ring is true (default)", () => {
137
+ render(<Input ring placeholder="x" aria-label="x" />);
138
+ const root = getInputSlot();
139
+ expect(
140
+ root.className,
141
+ "ring true must add generic focus-visible ring width and offset",
142
+ ).toMatch(/focus-visible:ring-2/);
143
+ expect(root.className).toMatch(/focus-visible:ring-offset-2/);
144
+ });
145
+
146
+ it("should omit generic focus ring utilities when ring is false", () => {
147
+ render(<Input ring={false} placeholder="x" aria-label="x" />);
148
+ const root = getInputSlot();
149
+ expect(
150
+ root.className,
151
+ "ring false must not add the shared ring-2 / ring-offset-2 recipe",
152
+ ).not.toMatch(/focus-visible:ring-2/);
153
+ expect(root.className).not.toMatch(/focus-visible:ring-offset-2/);
154
+ });
155
+
156
+ it("should omit generic ring utilities when ring is false even for semantic appearances", () => {
157
+ render(
158
+ <Input
159
+ appearance="error"
160
+ ring={false}
161
+ placeholder="x"
162
+ aria-label="x"
163
+ />,
164
+ );
165
+ const root = getInputSlot();
166
+ expect(root.className).toMatch(/border-rose-500/);
167
+ expect(root.className).not.toMatch(/focus-visible:ring-2/);
168
+ expect(root.className).not.toMatch(/focus-visible:ring-offset-2/);
169
+ });
170
+ });
171
+
172
+ describe("props: size", () => {
173
+ it("should apply medium size classes by default", () => {
174
+ render(<Input placeholder="x" aria-label="x" />);
175
+ const root = getInputSlot();
176
+ expect(root.className, "Default size must map to the md recipe").toMatch(
177
+ /h-9/,
178
+ );
179
+ });
180
+
181
+ it("should apply small size recipe when size='sm'", () => {
182
+ render(<Input size="sm" placeholder="x" aria-label="x" />);
183
+ const root = getInputSlot();
184
+ expect(root.className, "Small size must use the sm height scale").toMatch(
185
+ /h-8/,
186
+ );
187
+ });
188
+
189
+ it("should apply large size recipe when size='lg'", () => {
190
+ render(<Input size="lg" placeholder="x" aria-label="x" />);
191
+ const root = getInputSlot();
192
+ expect(root.className, "Large size must use the lg height scale").toMatch(
193
+ /h-10/,
194
+ );
195
+ });
196
+ });
197
+
198
+ describe("props: className composition", () => {
199
+ it("should merge consumer className with generated variant classes", () => {
200
+ render(
201
+ <Input className="my-custom-field" placeholder="x" aria-label="x" />,
202
+ );
203
+ const root = getInputSlot();
204
+ expect(
205
+ root.className,
206
+ "Consumer class names must not replace variant output",
207
+ ).toMatch(/border-white\/10/);
208
+ expect(
209
+ root.className,
210
+ "Consumer class names must be merged for Tailwind overrides",
211
+ ).toMatch(/my-custom-field/);
212
+ });
213
+ });
214
+
215
+ describe("props: disabled state", () => {
216
+ it("should mark the control disabled in the DOM", () => {
217
+ render(<Input disabled placeholder="x" aria-label="x" />);
218
+ expect(
219
+ screen.getByRole("textbox", { name: "x" }),
220
+ "Disabled inputs must expose disabled to assistive tech",
221
+ ).toBeDisabled();
222
+ });
223
+
224
+ it("should not change value when disabled and user types", async () => {
225
+ const user = userEvent.setup();
226
+ render(<Input defaultValue="locked" disabled aria-label="Field" />);
227
+ const control = screen.getByRole("textbox", { name: "Field" });
228
+ await user.type(control, "more");
229
+ expect(
230
+ control,
231
+ "Typing must not mutate disabled field value",
232
+ ).toHaveValue("locked");
233
+ });
234
+ });
235
+
236
+ describe("event handlers", () => {
237
+ it("should call onChange when the user types", async () => {
238
+ const user = userEvent.setup();
239
+ const handleChange = vi.fn();
240
+ render(
241
+ <Input onChange={handleChange} placeholder="x" aria-label="Field" />,
242
+ );
243
+ const control = screen.getByRole("textbox", { name: "Field" });
244
+ await user.type(control, "ab");
245
+ expect(
246
+ handleChange,
247
+ "onChange must fire for each keystroke in controlled-like typing",
248
+ ).toHaveBeenCalled();
249
+ });
250
+
251
+ it("should forward onFocus and onBlur for focus management patterns", async () => {
252
+ const user = userEvent.setup();
253
+ const handleFocus = vi.fn();
254
+ const handleBlur = vi.fn();
255
+ render(
256
+ <div>
257
+ <Input
258
+ onFocus={handleFocus}
259
+ onBlur={handleBlur}
260
+ placeholder="a"
261
+ aria-label="A"
262
+ />
263
+ <button type="button">Other</button>
264
+ </div>,
265
+ );
266
+ const control = screen.getByRole("textbox", { name: "A" });
267
+ await user.click(control);
268
+ expect(
269
+ handleFocus,
270
+ "onFocus must run when the input receives focus",
271
+ ).toHaveBeenCalledTimes(1);
272
+ await user.click(screen.getByRole("button", { name: "Other" }));
273
+ expect(
274
+ handleBlur,
275
+ "onBlur must run when focus leaves the input",
276
+ ).toHaveBeenCalledTimes(1);
277
+ });
278
+ });
279
+
280
+ describe("ref forwarding", () => {
281
+ it("should attach ref to the underlying input element", () => {
282
+ const ref = createRef<HTMLInputElement>();
283
+ render(<Input ref={ref} placeholder="x" aria-label="Ref target" />);
284
+ expect(
285
+ ref.current,
286
+ "ref must point at the actual DOM node for imperative focus/measure APIs",
287
+ ).toBeInstanceOf(HTMLInputElement);
288
+ expect(
289
+ ref.current?.getAttribute("data-slot"),
290
+ "ref node must be the rendered input instance",
291
+ ).toBe("input");
292
+ });
293
+
294
+ it("should attach ref to the underlying textarea when as='textarea'", () => {
295
+ const ref = createRef<HTMLTextAreaElement>();
296
+ render(
297
+ <Input
298
+ ref={ref}
299
+ as="textarea"
300
+ placeholder="x"
301
+ aria-label="Textarea ref target"
302
+ />,
303
+ );
304
+ expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);
305
+ expect(ref.current?.getAttribute("data-slot")).toBe("input");
306
+ });
307
+ });
308
+
309
+ describe("passthrough DOM and ARIA attributes", () => {
310
+ it("should forward arbitrary data-* attributes for integration test hooks", () => {
311
+ render(
312
+ <Input data-testid="email-field" placeholder="x" aria-label="Email" />,
313
+ );
314
+ expect(
315
+ screen.getByTestId("email-field"),
316
+ "data-testid must be preserved on the root element",
317
+ ).toBe(getInputSlot());
318
+ });
319
+
320
+ it("should forward id for label association", () => {
321
+ render(
322
+ <>
323
+ <label htmlFor="user-email">Email</label>
324
+ <Input id="user-email" placeholder="you@example.com" />
325
+ </>,
326
+ );
327
+ const control = document.getElementById("user-email");
328
+ expect(control, "id must be applied to the interactive root").toBe(
329
+ getInputSlot(),
330
+ );
331
+ });
332
+ });
333
+
334
+ describe("accessibility: aria-invalid", () => {
335
+ it("should set aria-invalid true when appearance is error", () => {
336
+ render(<Input appearance="error" placeholder="x" aria-label="Field" />);
337
+ expect(
338
+ screen
339
+ .getByRole("textbox", { name: "Field" })
340
+ .getAttribute("aria-invalid"),
341
+ ).toBe("true");
342
+ });
343
+
344
+ it("should not set aria-invalid when appearance is default", () => {
345
+ render(<Input placeholder="x" aria-label="Field" />);
346
+ expect(
347
+ screen
348
+ .getByRole("textbox", { name: "Field" })
349
+ .getAttribute("aria-invalid"),
350
+ ).toBeNull();
351
+ });
352
+
353
+ it("should respect explicit aria-invalid false even when appearance is error", () => {
354
+ render(
355
+ <Input
356
+ appearance="error"
357
+ aria-invalid={false}
358
+ placeholder="x"
359
+ aria-label="Field"
360
+ />,
361
+ );
362
+ expect(
363
+ screen
364
+ .getByRole("textbox", { name: "Field" })
365
+ .getAttribute("aria-invalid"),
366
+ ).toBe("false");
367
+ });
368
+ });
369
+
370
+ describe("props: animation presets (smoke)", () => {
371
+ const animations: InputAnimation[] = [
372
+ "none",
373
+ "lift",
374
+ "press",
375
+ "glow",
376
+ "tilt",
377
+ "bounce",
378
+ ];
379
+
380
+ it.each(animations)(
381
+ "should render without throwing when animation=%s",
382
+ (animation) => {
383
+ const { unmount } = render(
384
+ <InputAnimated animation={animation} placeholder="x" aria-label="Anim" />,
385
+ );
386
+ expect(
387
+ getInputSlot(),
388
+ `Animation preset '${animation}' must produce a mounted root`,
389
+ ).toBeVisible();
390
+ unmount();
391
+ },
392
+ );
393
+ });
394
+
395
+ describe("accessibility checklist", () => {
396
+ it("should expose focus styles via focus-visible classes in the class list", () => {
397
+ render(<Input placeholder="x" aria-label="x" />);
398
+ const root = getInputSlot();
399
+ expect(
400
+ root.className,
401
+ "Library inputs must ship focus-visible ring utilities for keyboard users",
402
+ ).toMatch(/focus-visible:ring-2/);
403
+ });
404
+
405
+ it("should include disabled opacity utilities for visual vs AT state alignment", () => {
406
+ render(<Input disabled placeholder="x" aria-label="x" />);
407
+ const root = getInputSlot();
408
+ expect(
409
+ root.className,
410
+ "Disabled styling must include opacity treatment from the recipe",
411
+ ).toMatch(/disabled:opacity-50/);
412
+ });
413
+ });
414
+ });
@@ -0,0 +1,8 @@
1
+ import { InputBase } from "./input-base";
2
+ import type { InputProps } from "./types";
3
+
4
+ export const Input = (props: InputProps) => {
5
+ return <InputBase {...props} />;
6
+ };
7
+
8
+ Input.displayName = "Input";
@@ -0,0 +1,18 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef } from "react";
3
+
4
+ import type { inputVariants } from "./variants";
5
+
6
+ export type InputSharedProps = Omit<VariantProps<typeof inputVariants>, "as"> & {
7
+ errorMessage?: string;
8
+ };
9
+
10
+ export type InputProps =
11
+ | (InputSharedProps &
12
+ Omit<ComponentPropsWithRef<"input">, "size" | "as"> & {
13
+ as?: "input" | "file" | "checkbox" | "radio";
14
+ })
15
+ | (InputSharedProps &
16
+ Omit<ComponentPropsWithRef<"textarea">, "size" | "as"> & {
17
+ as: "textarea";
18
+ });