@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.
- package/README.md +78 -0
- package/cli/cli.integration.test.ts +51 -0
- package/cli/index.mjs +664 -0
- package/cli/registry.json +36 -0
- package/cli/rewrite-imports.mjs +57 -0
- package/cli/rewrite-imports.test.ts +71 -0
- package/dist/ui/slider/slider.d.ts +18 -0
- package/dist/ui/slider/slider.d.ts.map +1 -1
- package/dist/ui/slider.js +21 -25
- package/dist/ui/slider.js.map +1 -1
- package/dist/ui/slider.mjs +21 -25
- package/dist/ui/slider.mjs.map +1 -1
- package/package.json +8 -2
- package/src/hooks/index.ts +48 -0
- package/src/hooks/useBodyScrollLock/index.ts +1 -0
- package/src/hooks/useBodyScrollLock/useBodyScrollLock.test.ts +51 -0
- package/src/hooks/useBodyScrollLock/useBodyScrollLock.ts +48 -0
- package/src/hooks/useClickOutside/index.ts +5 -0
- package/src/hooks/useClickOutside/useClickOutside.test.tsx +60 -0
- package/src/hooks/useClickOutside/useClickOutside.ts +52 -0
- package/src/hooks/useClipboard/index.ts +1 -0
- package/src/hooks/useClipboard/useClipboard.test.ts +101 -0
- package/src/hooks/useClipboard/useClipboard.ts +69 -0
- package/src/hooks/useControllableState/index.ts +4 -0
- package/src/hooks/useControllableState/useControllableState.test.ts +59 -0
- package/src/hooks/useControllableState/useControllableState.ts +49 -0
- package/src/hooks/useDebouncedValue/index.ts +1 -0
- package/src/hooks/useDebouncedValue/useDebouncedValue.test.ts +74 -0
- package/src/hooks/useDebouncedValue/useDebouncedValue.ts +29 -0
- package/src/hooks/useDisclosure/index.ts +5 -0
- package/src/hooks/useDisclosure/useDisclosure.test.ts +64 -0
- package/src/hooks/useDisclosure/useDisclosure.ts +62 -0
- package/src/hooks/useDocumentTitle/index.ts +4 -0
- package/src/hooks/useDocumentTitle/useDocumentTitle.test.ts +40 -0
- package/src/hooks/useDocumentTitle/useDocumentTitle.ts +58 -0
- package/src/hooks/useFocusManagement/index.ts +1 -0
- package/src/hooks/useFocusManagement/useFocusManagement.test.tsx +45 -0
- package/src/hooks/useFocusManagement/useFocusManagement.ts +77 -0
- package/src/hooks/useHover/index.ts +1 -0
- package/src/hooks/useHover/useHover.test.ts +45 -0
- package/src/hooks/useHover/useHover.ts +45 -0
- package/src/hooks/useInView/index.ts +1 -0
- package/src/hooks/useInView/useInView.test.ts +43 -0
- package/src/hooks/useInView/useInView.ts +28 -0
- package/src/hooks/useIntersectionObserver/index.ts +4 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts +75 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.ts +54 -0
- package/src/hooks/useIsMounted/index.ts +1 -0
- package/src/hooks/useIsMounted/useIsMounted.test.ts +25 -0
- package/src/hooks/useIsMounted/useIsMounted.ts +22 -0
- package/src/hooks/useIsomorphicLayoutEffect/index.ts +1 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.ts +19 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +12 -0
- package/src/hooks/useLocalStorage/index.ts +4 -0
- package/src/hooks/useLocalStorage/useLocalStorage.test.ts +99 -0
- package/src/hooks/useLocalStorage/useLocalStorage.ts +109 -0
- package/src/hooks/useMediaQuery/index.ts +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.test.ts +63 -0
- package/src/hooks/useMediaQuery/useMediaQuery.ts +37 -0
- package/src/hooks/useNetworkStatus/index.ts +1 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.test.ts +53 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.ts +33 -0
- package/src/hooks/usePageVisibility/index.ts +1 -0
- package/src/hooks/usePageVisibility/usePageVisibility.test.ts +21 -0
- package/src/hooks/usePageVisibility/usePageVisibility.ts +31 -0
- package/src/hooks/usePagination/index.ts +6 -0
- package/src/hooks/usePagination/usePagination.test.ts +139 -0
- package/src/hooks/usePagination/usePagination.ts +153 -0
- package/src/hooks/usePrefersColorScheme/index.ts +4 -0
- package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.test.ts +53 -0
- package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.ts +21 -0
- package/src/hooks/usePrefersReducedMotion/index.ts +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.test.ts +27 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.ts +14 -0
- package/src/hooks/useResizeObserver/index.ts +4 -0
- package/src/hooks/useResizeObserver/useResizeObserver.test.ts +68 -0
- package/src/hooks/useResizeObserver/useResizeObserver.ts +58 -0
- package/src/hooks/useSessionStorage/index.ts +4 -0
- package/src/hooks/useSessionStorage/useSessionStorage.test.ts +54 -0
- package/src/hooks/useSessionStorage/useSessionStorage.ts +84 -0
- package/src/hooks/useThrottledCallback/index.ts +1 -0
- package/src/hooks/useThrottledCallback/useThrottledCallback.test.ts +75 -0
- package/src/hooks/useThrottledCallback/useThrottledCallback.ts +36 -0
- package/src/hooks/useToggle/index.ts +1 -0
- package/src/hooks/useToggle/useToggle.test.ts +40 -0
- package/src/hooks/useToggle/useToggle.ts +22 -0
- package/src/hooks/useWindowSize/index.ts +1 -0
- package/src/hooks/useWindowSize/useWindowSize.test.ts +23 -0
- package/src/hooks/useWindowSize/useWindowSize.ts +39 -0
- package/src/lib/utils.ts +25 -0
- package/src/ui/accordion/accordion-base.tsx +223 -0
- package/src/ui/accordion/accordion.test.tsx +146 -0
- package/src/ui/accordion/accordion.tsx +11 -0
- package/src/ui/accordion/animated/accordion-content-animated.tsx +46 -0
- package/src/ui/accordion/animated/accordion-root-animated.tsx +10 -0
- package/src/ui/accordion/animated/animations.ts +16 -0
- package/src/ui/accordion/animated/index.ts +7 -0
- package/src/ui/accordion/animated/types.ts +7 -0
- package/src/ui/accordion/index.ts +23 -0
- package/src/ui/accordion/types.ts +48 -0
- package/src/ui/accordion/variants.ts +115 -0
- package/src/ui/alert/alert-base.tsx +157 -0
- package/src/ui/alert/alert.test.tsx +150 -0
- package/src/ui/alert/alert.tsx +9 -0
- package/src/ui/alert/animated/alert-animated.tsx +20 -0
- package/src/ui/alert/animated/animations.ts +20 -0
- package/src/ui/alert/animated/index.ts +3 -0
- package/src/ui/alert/animated/types.ts +16 -0
- package/src/ui/alert/index.ts +22 -0
- package/src/ui/alert/types.ts +28 -0
- package/src/ui/alert/variants.ts +74 -0
- package/src/ui/avatar/animated/animations.ts +11 -0
- package/src/ui/avatar/animated/avatar-animated.tsx +25 -0
- package/src/ui/avatar/animated/index.ts +6 -0
- package/src/ui/avatar/animated/types.ts +16 -0
- package/src/ui/avatar/avatar-base.tsx +184 -0
- package/src/ui/avatar/avatar.test.tsx +51 -0
- package/src/ui/avatar/avatar.tsx +11 -0
- package/src/ui/avatar/index.ts +16 -0
- package/src/ui/avatar/types.ts +36 -0
- package/src/ui/avatar/variants.ts +52 -0
- package/src/ui/badge/animated/animations.ts +20 -0
- package/src/ui/badge/animated/badge-animated.tsx +28 -0
- package/src/ui/badge/animated/index.ts +5 -0
- package/src/ui/badge/animated/types.ts +18 -0
- package/src/ui/badge/badge-base.tsx +53 -0
- package/src/ui/badge/badge.test.tsx +48 -0
- package/src/ui/badge/badge.tsx +9 -0
- package/src/ui/badge/index.ts +5 -0
- package/src/ui/badge/types.ts +25 -0
- package/src/ui/badge/variants.ts +85 -0
- package/src/ui/breadcrumb/breadcrumb.test.tsx +62 -0
- package/src/ui/breadcrumb/breadcrumb.tsx +135 -0
- package/src/ui/breadcrumb/index.ts +28 -0
- package/src/ui/breadcrumb/types.ts +29 -0
- package/src/ui/breadcrumb/variants.ts +53 -0
- package/src/ui/buttons/animated/animations.ts +34 -0
- package/src/ui/buttons/animated/button-animated.tsx +70 -0
- package/src/ui/buttons/animated/index.ts +5 -0
- package/src/ui/buttons/animated/types.ts +29 -0
- package/src/ui/buttons/button-base.tsx +59 -0
- package/src/ui/buttons/button.test.tsx +480 -0
- package/src/ui/buttons/button.tsx +9 -0
- package/src/ui/buttons/index.ts +5 -0
- package/src/ui/buttons/types.ts +14 -0
- package/src/ui/buttons/variants.ts +77 -0
- package/src/ui/card/animated/animations.ts +32 -0
- package/src/ui/card/animated/card-animated.tsx +28 -0
- package/src/ui/card/animated/index.ts +12 -0
- package/src/ui/card/animated/types.ts +8 -0
- package/src/ui/card/card-base.tsx +146 -0
- package/src/ui/card/card.test.tsx +79 -0
- package/src/ui/card/card.tsx +11 -0
- package/src/ui/card/index.ts +21 -0
- package/src/ui/card/types.ts +42 -0
- package/src/ui/card/variants.ts +122 -0
- package/src/ui/divider/animated/animations.ts +27 -0
- package/src/ui/divider/animated/divider-animated.tsx +24 -0
- package/src/ui/divider/animated/index.ts +4 -0
- package/src/ui/divider/animated/types.ts +18 -0
- package/src/ui/divider/divider-base.tsx +80 -0
- package/src/ui/divider/divider.tsx +9 -0
- package/src/ui/divider/index.ts +14 -0
- package/src/ui/divider/types.ts +18 -0
- package/src/ui/divider/variants.ts +98 -0
- package/src/ui/drawer/animated/animations.ts +39 -0
- package/src/ui/drawer/animated/drawer-content-animated.tsx +101 -0
- package/src/ui/drawer/animated/index.ts +14 -0
- package/src/ui/drawer/animated/types.ts +18 -0
- package/src/ui/drawer/drawer-base.tsx +259 -0
- package/src/ui/drawer/drawer.test.tsx +132 -0
- package/src/ui/drawer/drawer.tsx +11 -0
- package/src/ui/drawer/index.ts +21 -0
- package/src/ui/drawer/types.ts +39 -0
- package/src/ui/drawer/variants.ts +122 -0
- package/src/ui/dropdown/dropdown.test.tsx +114 -0
- package/src/ui/dropdown/dropdown.tsx +179 -0
- package/src/ui/dropdown/index.ts +15 -0
- package/src/ui/dropdown/types.ts +68 -0
- package/src/ui/dropdown/variants.ts +138 -0
- package/src/ui/empty-state/animated/animations.ts +19 -0
- package/src/ui/empty-state/animated/empty-state-animated.tsx +23 -0
- package/src/ui/empty-state/animated/index.ts +7 -0
- package/src/ui/empty-state/animated/types.ts +26 -0
- package/src/ui/empty-state/empty-state-base.tsx +114 -0
- package/src/ui/empty-state/empty-state.tsx +9 -0
- package/src/ui/empty-state/index.ts +10 -0
- package/src/ui/empty-state/types.ts +19 -0
- package/src/ui/empty-state/variants.ts +51 -0
- package/src/ui/file-upload/file-upload.test.tsx +36 -0
- package/src/ui/file-upload/file-upload.tsx +119 -0
- package/src/ui/file-upload/index.ts +5 -0
- package/src/ui/file-upload/types.ts +21 -0
- package/src/ui/file-upload/variants.ts +29 -0
- package/src/ui/inputs/animated/animations.ts +36 -0
- package/src/ui/inputs/animated/index.ts +5 -0
- package/src/ui/inputs/animated/input-animated.tsx +124 -0
- package/src/ui/inputs/animated/types.ts +40 -0
- package/src/ui/inputs/index.ts +5 -0
- package/src/ui/inputs/input-base.tsx +114 -0
- package/src/ui/inputs/input.test.tsx +414 -0
- package/src/ui/inputs/input.tsx +8 -0
- package/src/ui/inputs/types.ts +18 -0
- package/src/ui/inputs/variants.ts +316 -0
- package/src/ui/modal/animated/animations.ts +29 -0
- package/src/ui/modal/animated/index.ts +5 -0
- package/src/ui/modal/animated/modal-content-animated.tsx +96 -0
- package/src/ui/modal/animated/types.ts +23 -0
- package/src/ui/modal/index.ts +21 -0
- package/src/ui/modal/modal-base.tsx +279 -0
- package/src/ui/modal/modal.test.tsx +129 -0
- package/src/ui/modal/modal.tsx +8 -0
- package/src/ui/modal/types.ts +31 -0
- package/src/ui/modal/variants.ts +109 -0
- package/src/ui/pagination/index.ts +13 -0
- package/src/ui/pagination/pagination.test.tsx +165 -0
- package/src/ui/pagination/pagination.tsx +237 -0
- package/src/ui/pagination/types.ts +66 -0
- package/src/ui/pagination/variants.ts +97 -0
- package/src/ui/progress/animated/animations.ts +9 -0
- package/src/ui/progress/animated/index.ts +17 -0
- package/src/ui/progress/animated/progress-animated.tsx +133 -0
- package/src/ui/progress/animated/types.ts +35 -0
- package/src/ui/progress/index.ts +10 -0
- package/src/ui/progress/progress-base.tsx +151 -0
- package/src/ui/progress/progress.test.tsx +84 -0
- package/src/ui/progress/progress.tsx +12 -0
- package/src/ui/progress/types.ts +33 -0
- package/src/ui/progress/variants.ts +105 -0
- package/src/ui/select/index.ts +25 -0
- package/src/ui/select/select.test.tsx +128 -0
- package/src/ui/select/select.tsx +221 -0
- package/src/ui/select/types.ts +77 -0
- package/src/ui/select/variants.ts +163 -0
- package/src/ui/skeleton/animated/animations.ts +15 -0
- package/src/ui/skeleton/animated/index.ts +20 -0
- package/src/ui/skeleton/animated/skeleton-animated.tsx +119 -0
- package/src/ui/skeleton/animated/types.ts +49 -0
- package/src/ui/skeleton/index.ts +24 -0
- package/src/ui/skeleton/skeleton-base.tsx +288 -0
- package/src/ui/skeleton/skeleton.tsx +8 -0
- package/src/ui/skeleton/types.ts +31 -0
- package/src/ui/skeleton/variants.ts +254 -0
- package/src/ui/slider/index.ts +22 -0
- package/src/ui/slider/slider.test.tsx +94 -0
- package/src/ui/slider/slider.tsx +728 -0
- package/src/ui/slider/types.ts +66 -0
- package/src/ui/slider/variants.ts +81 -0
- package/src/ui/spinner/animated/index.ts +5 -0
- package/src/ui/spinner/animated/spinner.test.tsx +41 -0
- package/src/ui/spinner/animated/spinner.tsx +143 -0
- package/src/ui/spinner/animated/types.ts +11 -0
- package/src/ui/spinner/animated/variants.ts +50 -0
- package/src/ui/stepper/index.ts +22 -0
- package/src/ui/stepper/stepper.test.tsx +183 -0
- package/src/ui/stepper/stepper.tsx +172 -0
- package/src/ui/stepper/types.ts +32 -0
- package/src/ui/stepper/variants.ts +69 -0
- package/src/ui/table/animated/animations.ts +9 -0
- package/src/ui/table/animated/index.ts +15 -0
- package/src/ui/table/animated/table-animated.tsx +15 -0
- package/src/ui/table/animated/types.ts +16 -0
- package/src/ui/table/index.ts +22 -0
- package/src/ui/table/table-base.tsx +197 -0
- package/src/ui/table/table.tsx +13 -0
- package/src/ui/table/types.ts +47 -0
- package/src/ui/table/variants.ts +105 -0
- package/src/ui/tabs/animated/animations.ts +48 -0
- package/src/ui/tabs/animated/index.ts +8 -0
- package/src/ui/tabs/animated/tabs-content-animated.tsx +46 -0
- package/src/ui/tabs/animated/types.ts +24 -0
- package/src/ui/tabs/index.ts +10 -0
- package/src/ui/tabs/tabs-base.tsx +185 -0
- package/src/ui/tabs/tabs.test.tsx +53 -0
- package/src/ui/tabs/tabs.tsx +2 -0
- package/src/ui/tabs/types.ts +88 -0
- package/src/ui/tabs/variants.ts +70 -0
- package/src/ui/toast/animated/animations.ts +17 -0
- package/src/ui/toast/animated/index.ts +9 -0
- package/src/ui/toast/animated/toast-animated.tsx +96 -0
- package/src/ui/toast/animated/types.ts +13 -0
- package/src/ui/toast/index.ts +26 -0
- package/src/ui/toast/toast-base.tsx +231 -0
- package/src/ui/toast/toast.test.tsx +102 -0
- package/src/ui/toast/toast.tsx +13 -0
- package/src/ui/toast/types.ts +57 -0
- package/src/ui/toast/variants.ts +73 -0
- package/src/ui/toggle/animated/animations.ts +9 -0
- package/src/ui/toggle/animated/index.ts +7 -0
- package/src/ui/toggle/animated/toggle-animated.tsx +76 -0
- package/src/ui/toggle/animated/types.ts +13 -0
- package/src/ui/toggle/index.ts +5 -0
- package/src/ui/toggle/toggle-base.tsx +70 -0
- package/src/ui/toggle/toggle.test.tsx +44 -0
- package/src/ui/toggle/toggle.tsx +9 -0
- package/src/ui/toggle/types.ts +18 -0
- package/src/ui/toggle/variants.ts +84 -0
- package/src/ui/tooltip/animated/animations.ts +16 -0
- package/src/ui/tooltip/animated/index.ts +10 -0
- package/src/ui/tooltip/animated/tooltip-content-animated.tsx +47 -0
- package/src/ui/tooltip/animated/types.ts +19 -0
- package/src/ui/tooltip/index.ts +17 -0
- package/src/ui/tooltip/tooltip-base.tsx +152 -0
- package/src/ui/tooltip/tooltip.test.tsx +84 -0
- package/src/ui/tooltip/tooltip.tsx +8 -0
- package/src/ui/tooltip/types.ts +57 -0
- 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,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,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
|
+
});
|