@cntyclub/ui-react 0.1.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/dist/chunk-HDGMSYQS.js +26461 -0
- package/dist/chunk-HDGMSYQS.js.map +1 -0
- package/dist/chunk-PR4QN5HX.js +39 -0
- package/dist/chunk-PR4QN5HX.js.map +1 -0
- package/dist/form.d.ts +175 -0
- package/dist/form.js +5207 -0
- package/dist/form.js.map +1 -0
- package/dist/index.d.ts +1462 -0
- package/dist/index.js +81862 -0
- package/dist/index.js.map +1 -0
- package/dist/input-CZvh825j.d.ts +24 -0
- package/dist/qr-code-styling-3Y6LZH6V.js +1123 -0
- package/dist/qr-code-styling-3Y6LZH6V.js.map +1 -0
- package/package.json +79 -0
- package/src/components/form/checkbox-group-field.tsx +101 -0
- package/src/components/form/date-field.tsx +79 -0
- package/src/components/form/date-range-field.tsx +106 -0
- package/src/components/form/form-context.ts +10 -0
- package/src/components/form/form.tsx +54 -0
- package/src/components/form/number-field.tsx +69 -0
- package/src/components/form/select-field.tsx +76 -0
- package/src/components/form/submit-button.tsx +28 -0
- package/src/components/form/text-field.tsx +107 -0
- package/src/components/layout/dashboard-header.tsx +54 -0
- package/src/components/layout/dashboard-panel.tsx +34 -0
- package/src/components/theme-provider.tsx +403 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +169 -0
- package/src/components/ui/alert.tsx +80 -0
- package/src/components/ui/animated-theme-toggler.tsx +265 -0
- package/src/components/ui/app-store-buttons.tsx +182 -0
- package/src/components/ui/aspect-ratio.tsx +23 -0
- package/src/components/ui/autocomplete.tsx +296 -0
- package/src/components/ui/avatar-group.tsx +95 -0
- package/src/components/ui/avatar.tsx +285 -0
- package/src/components/ui/badge-group.tsx +160 -0
- package/src/components/ui/badge.tsx +172 -0
- package/src/components/ui/breadcrumb.tsx +112 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/calendar.tsx +137 -0
- package/src/components/ui/card.tsx +244 -0
- package/src/components/ui/carousel.tsx +258 -0
- package/src/components/ui/chart.tsx +379 -0
- package/src/components/ui/checkbox-group.tsx +16 -0
- package/src/components/ui/checkbox.tsx +82 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/combobox.tsx +411 -0
- package/src/components/ui/command.tsx +264 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/credit-card.tsx +214 -0
- package/src/components/ui/dialog.tsx +196 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/empty.tsx +127 -0
- package/src/components/ui/featured-icon.tsx +149 -0
- package/src/components/ui/field.tsx +88 -0
- package/src/components/ui/fieldset.tsx +29 -0
- package/src/components/ui/form.tsx +17 -0
- package/src/components/ui/frame.tsx +82 -0
- package/src/components/ui/generic-empty.tsx +142 -0
- package/src/components/ui/group.tsx +97 -0
- package/src/components/ui/horizontal-scroll-fader.tsx +228 -0
- package/src/components/ui/input-group.tsx +102 -0
- package/src/components/ui/input-otp.tsx +96 -0
- package/src/components/ui/input.tsx +66 -0
- package/src/components/ui/item.tsx +198 -0
- package/src/components/ui/kbd.tsx +30 -0
- package/src/components/ui/label.tsx +28 -0
- package/src/components/ui/menu.tsx +312 -0
- package/src/components/ui/menubar.tsx +93 -0
- package/src/components/ui/meter.tsx +67 -0
- package/src/components/ui/multi-select.tsx +308 -0
- package/src/components/ui/navigation-menu.tsx +143 -0
- package/src/components/ui/number-field.tsx +160 -0
- package/src/components/ui/pagination-controls.tsx +74 -0
- package/src/components/ui/pagination.tsx +149 -0
- package/src/components/ui/popover.tsx +119 -0
- package/src/components/ui/preview-card.tsx +55 -0
- package/src/components/ui/progress.tsx +289 -0
- package/src/components/ui/qr-code.tsx +150 -0
- package/src/components/ui/radio-group.tsx +103 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +90 -0
- package/src/components/ui/scroller.tsx +38 -0
- package/src/components/ui/section-header.tsx +118 -0
- package/src/components/ui/select.tsx +181 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +224 -0
- package/src/components/ui/sidebar.tsx +744 -0
- package/src/components/ui/skeleton.tsx +16 -0
- package/src/components/ui/slider.tsx +108 -0
- package/src/components/ui/smooth-scroll.tsx +143 -0
- package/src/components/ui/social-button.tsx +247 -0
- package/src/components/ui/spinner-on-demand.tsx +32 -0
- package/src/components/ui/spinner.tsx +18 -0
- package/src/components/ui/stat.tsx +187 -0
- package/src/components/ui/stepper.tsx +167 -0
- package/src/components/ui/switch.tsx +56 -0
- package/src/components/ui/table.tsx +126 -0
- package/src/components/ui/tabs.tsx +90 -0
- package/src/components/ui/tag.tsx +229 -0
- package/src/components/ui/target-countdown.tsx +46 -0
- package/src/components/ui/text-editor.tsx +313 -0
- package/src/components/ui/textarea.tsx +51 -0
- package/src/components/ui/timeline.tsx +116 -0
- package/src/components/ui/toast.tsx +268 -0
- package/src/components/ui/toggle-group.tsx +101 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/toolbar.tsx +89 -0
- package/src/components/ui/tooltip.tsx +102 -0
- package/src/components/ui/vertical-scroll-fader.tsx +250 -0
- package/src/components/ui/video-player.tsx +275 -0
- package/src/components/upload/avatar-upload-base.tsx +131 -0
- package/src/components/upload/image-upload-base.tsx +112 -0
- package/src/form.ts +17 -0
- package/src/index.ts +125 -0
- package/src/lib/hooks/use-callback-ref.ts +15 -0
- package/src/lib/hooks/use-first-render.ts +11 -0
- package/src/lib/hooks/use-hover.ts +53 -0
- package/src/lib/hooks/use-is-tab-active.ts +17 -0
- package/src/lib/hooks/use-media-query.ts +164 -0
- package/src/lib/utils/css.ts +6 -0
- package/src/styles.css +300 -0
- package/src/types/helpers.ts +24 -0
- package/src/types/react.d.ts +7 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileQuestionIcon,
|
|
3
|
+
ShieldAlertIcon,
|
|
4
|
+
ShieldXIcon,
|
|
5
|
+
TriangleAlertIcon,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import type React from "react";
|
|
8
|
+
import {
|
|
9
|
+
Empty,
|
|
10
|
+
EmptyContent,
|
|
11
|
+
EmptyDescription,
|
|
12
|
+
EmptyHeader,
|
|
13
|
+
EmptyMedia,
|
|
14
|
+
EmptyTitle,
|
|
15
|
+
} from "./empty";
|
|
16
|
+
|
|
17
|
+
interface ErrorOrEmptyStateProps
|
|
18
|
+
extends Omit<React.ComponentProps<"div">, "title"> {
|
|
19
|
+
title?: React.ReactNode;
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
actions?: React.ReactNode;
|
|
22
|
+
media?: React.ReactNode;
|
|
23
|
+
mediaVariant?: "icon" | "default";
|
|
24
|
+
compact?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ErrorOrEmptyState({
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
actions,
|
|
31
|
+
className,
|
|
32
|
+
media,
|
|
33
|
+
mediaVariant = "default",
|
|
34
|
+
compact,
|
|
35
|
+
...props
|
|
36
|
+
}: ErrorOrEmptyStateProps) {
|
|
37
|
+
return (
|
|
38
|
+
<Empty {...props}>
|
|
39
|
+
<EmptyHeader>
|
|
40
|
+
{!compact && <EmptyMedia variant={mediaVariant}>{media}</EmptyMedia>}
|
|
41
|
+
<EmptyTitle>{title}</EmptyTitle>
|
|
42
|
+
<EmptyDescription>{description}</EmptyDescription>
|
|
43
|
+
</EmptyHeader>
|
|
44
|
+
<EmptyContent>{actions}</EmptyContent>
|
|
45
|
+
</Empty>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ErrorTemplateProps = Omit<
|
|
50
|
+
ErrorOrEmptyStateProps,
|
|
51
|
+
"image" | "title" | "description"
|
|
52
|
+
> & {
|
|
53
|
+
mediaClassName?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function NotFoundErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
|
|
57
|
+
return (
|
|
58
|
+
<ErrorOrEmptyState
|
|
59
|
+
media={<FileQuestionIcon className={mediaClassName} />}
|
|
60
|
+
mediaVariant="icon"
|
|
61
|
+
title="Not found"
|
|
62
|
+
description="The requested resource was not found."
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function UnauthorizedErrorState({
|
|
69
|
+
mediaClassName,
|
|
70
|
+
...props
|
|
71
|
+
}: ErrorTemplateProps) {
|
|
72
|
+
return (
|
|
73
|
+
<ErrorOrEmptyState
|
|
74
|
+
media={<ShieldAlertIcon className={mediaClassName} />}
|
|
75
|
+
mediaVariant="icon"
|
|
76
|
+
title="Unauthorized"
|
|
77
|
+
description="You are not authorized to access this resource."
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ForbiddenErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
|
|
84
|
+
return (
|
|
85
|
+
<ErrorOrEmptyState
|
|
86
|
+
media={<ShieldXIcon className={mediaClassName} />}
|
|
87
|
+
mediaVariant="icon"
|
|
88
|
+
title="Forbidden"
|
|
89
|
+
description="You are not allowed to access this resource."
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function InternalServerErrorState({
|
|
96
|
+
mediaClassName,
|
|
97
|
+
...props
|
|
98
|
+
}: ErrorTemplateProps) {
|
|
99
|
+
return (
|
|
100
|
+
<ErrorOrEmptyState
|
|
101
|
+
media={<ShieldXIcon className={mediaClassName} />}
|
|
102
|
+
mediaVariant="icon"
|
|
103
|
+
title="Internal server error"
|
|
104
|
+
description="An error occurred while processing your request."
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function OfflineErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
|
|
111
|
+
return (
|
|
112
|
+
<ErrorOrEmptyState
|
|
113
|
+
media={<ShieldXIcon className={mediaClassName} />}
|
|
114
|
+
mediaVariant="icon"
|
|
115
|
+
title="Offline"
|
|
116
|
+
description="Please check your internet connection and try again."
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function GeneralErrorState({ mediaClassName, ...props }: ErrorTemplateProps) {
|
|
123
|
+
return (
|
|
124
|
+
<ErrorOrEmptyState
|
|
125
|
+
media={<TriangleAlertIcon className={mediaClassName} />}
|
|
126
|
+
mediaVariant="icon"
|
|
127
|
+
title="Error"
|
|
128
|
+
description="An error occurred while processing your request."
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
ErrorOrEmptyState,
|
|
136
|
+
NotFoundErrorState,
|
|
137
|
+
UnauthorizedErrorState,
|
|
138
|
+
ForbiddenErrorState,
|
|
139
|
+
InternalServerErrorState,
|
|
140
|
+
OfflineErrorState,
|
|
141
|
+
GeneralErrorState,
|
|
142
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
4
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
import { Separator } from "./separator";
|
|
8
|
+
import { cn } from "../../lib/utils/css";
|
|
9
|
+
|
|
10
|
+
const groupVariants = cva(
|
|
11
|
+
"flex w-fit *:focus-visible:z-1 has-[>[data-slot=group]]:gap-2 *:has-focus-visible:z-1 dark:*:[[data-slot=button]:hover~[data-slot=separator]:not([data-slot]:hover~[data-slot=separator]~[data-slot=separator]),[data-slot][data-pressed]~[data-slot=separator]:not([data-slot][data-pressed]~[data-slot=separator]~[data-slot=separator])]:before:bg-input/64 dark:*:[[data-slot=separator]:has(~[data-slot=button]:hover):not(:has(~[data-slot=separator]~[data-slot]:hover)),[data-slot=separator]:has(~[data-slot][data-pressed]):not(:has(~[data-slot=separator]~[data-slot][data-pressed]))]:before:bg-input/64",
|
|
12
|
+
{
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
orientation: "horizontal",
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
orientation: {
|
|
18
|
+
horizontal:
|
|
19
|
+
"*:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-start-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-end-[0.5px] *:pointer-coarse:after:min-w-auto *:data-slot:has-[~[data-slot]]:rounded-e-none *:data-slot:has-[~[data-slot]]:border-e-0 *:data-slot:has-[~[data-slot]]:before:rounded-e-none *:[[data-slot]~[data-slot]]:rounded-s-none *:[[data-slot]~[data-slot]]:border-s-0 *:[[data-slot]~[data-slot]]:before:rounded-s-none",
|
|
20
|
+
vertical:
|
|
21
|
+
"*:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-top-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-bottom-[0.5px] flex-col *:pointer-coarse:after:min-h-auto *:data-slot:has-[~[data-slot]]:rounded-b-none *:data-slot:has-[~[data-slot]]:border-b-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:hidden *:data-slot:has-[~[data-slot]]:before:rounded-b-none dark:*:last:before:hidden dark:*:first:before:block *:[[data-slot]~[data-slot]]:rounded-t-none *:[[data-slot]~[data-slot]]:border-t-0 *:[[data-slot]~[data-slot]]:before:rounded-t-none",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function Group({
|
|
28
|
+
className,
|
|
29
|
+
orientation,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: {
|
|
33
|
+
className?: string;
|
|
34
|
+
orientation?: VariantProps<typeof groupVariants>["orientation"];
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
} & React.ComponentProps<"div">) {
|
|
37
|
+
return (
|
|
38
|
+
// biome-ignore lint/a11y/useSemanticElements: Imported from library
|
|
39
|
+
<div
|
|
40
|
+
className={cn(groupVariants({ orientation }), className)}
|
|
41
|
+
data-orientation={orientation}
|
|
42
|
+
data-slot="group"
|
|
43
|
+
role="group"
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function GroupText({
|
|
52
|
+
className,
|
|
53
|
+
render,
|
|
54
|
+
...props
|
|
55
|
+
}: useRender.ComponentProps<"div">) {
|
|
56
|
+
const defaultProps = {
|
|
57
|
+
className: cn(
|
|
58
|
+
"relative inline-flex items-center whitespace-nowrap gap-2 rounded-lg border border-input bg-muted not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-muted-foreground text-base sm:text-sm shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 [&_svg]:-mx-0.5",
|
|
59
|
+
className,
|
|
60
|
+
),
|
|
61
|
+
"data-slot": "group-text",
|
|
62
|
+
};
|
|
63
|
+
return useRender({
|
|
64
|
+
defaultTagName: "div",
|
|
65
|
+
props: mergeProps(defaultProps, props),
|
|
66
|
+
render,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function GroupSeparator({
|
|
71
|
+
className,
|
|
72
|
+
orientation = "vertical",
|
|
73
|
+
...props
|
|
74
|
+
}: {
|
|
75
|
+
className?: string;
|
|
76
|
+
} & React.ComponentProps<typeof Separator>) {
|
|
77
|
+
return (
|
|
78
|
+
<Separator
|
|
79
|
+
className={cn(
|
|
80
|
+
"[[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+input+&]:-translate-x-px pointer-events-none relative z-2 bg-input before:absolute before:inset-0 has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:translate-x-px has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:bg-ring dark:before:bg-input/32 [[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+&,[data-slot=number-field]:focus-within+input+&]:bg-ring",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
orientation={orientation}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
Group,
|
|
91
|
+
Group as ButtonGroup,
|
|
92
|
+
GroupText,
|
|
93
|
+
GroupText as ButtonGroupText,
|
|
94
|
+
GroupSeparator,
|
|
95
|
+
GroupSeparator as ButtonGroupSeparator,
|
|
96
|
+
groupVariants,
|
|
97
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Transition } from "@headlessui/react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
forwardRef,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
} from "react";
|
|
15
|
+
import useResizeObserver from "use-resize-observer";
|
|
16
|
+
import useCallbackRef from "../../lib/hooks/use-callback-ref";
|
|
17
|
+
import { useHover } from "../../lib/hooks/use-hover";
|
|
18
|
+
import { cn } from "../../lib/utils/css";
|
|
19
|
+
import type { AsChildProps } from "../../types/helpers";
|
|
20
|
+
|
|
21
|
+
interface HorizontalScrollFaderContextValue {
|
|
22
|
+
isLeftOverflowing: boolean;
|
|
23
|
+
isRightOverflowing: boolean;
|
|
24
|
+
containerRef: React.RefObject<HTMLElement | null>;
|
|
25
|
+
onScroll: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HorizontalScrollFaderContext =
|
|
29
|
+
createContext<HorizontalScrollFaderContextValue>({
|
|
30
|
+
isLeftOverflowing: false,
|
|
31
|
+
isRightOverflowing: false,
|
|
32
|
+
containerRef: { current: null },
|
|
33
|
+
onScroll: () => {},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function HorizontalScrollFaderInner(
|
|
37
|
+
{ asChild, ...props }: AsChildProps<"div">,
|
|
38
|
+
ref: React.ForwardedRef<HTMLDivElement>,
|
|
39
|
+
) {
|
|
40
|
+
const containerRef = useRef<HTMLElement>(null);
|
|
41
|
+
|
|
42
|
+
const [isLeftOverflowing, setIsLeftOverflowing] = useState(false);
|
|
43
|
+
const [isRightOverflowing, setIsRightOverflowing] = useState(false);
|
|
44
|
+
|
|
45
|
+
const onScroll = useCallbackRef(() => {
|
|
46
|
+
if (!containerRef.current) return;
|
|
47
|
+
|
|
48
|
+
const { scrollLeft, scrollWidth, clientWidth } = containerRef.current;
|
|
49
|
+
setIsLeftOverflowing(scrollLeft > 0);
|
|
50
|
+
setIsRightOverflowing(scrollLeft + clientWidth < scrollWidth);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const contextValue = useMemo(
|
|
54
|
+
() => ({ isLeftOverflowing, isRightOverflowing, containerRef, onScroll }),
|
|
55
|
+
[isLeftOverflowing, isRightOverflowing, onScroll],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const Comp = asChild ? Slot : "div";
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<HorizontalScrollFaderContext.Provider value={contextValue}>
|
|
62
|
+
<Slot className="relative" onScroll={onScroll}>
|
|
63
|
+
<Comp ref={ref} {...props} />
|
|
64
|
+
</Slot>
|
|
65
|
+
</HorizontalScrollFaderContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const HorizontalScrollFader = forwardRef(HorizontalScrollFaderInner);
|
|
70
|
+
|
|
71
|
+
function HorizontalScrollFaderContentInner(
|
|
72
|
+
{ asChild, ...props }: AsChildProps<"div">,
|
|
73
|
+
ref: React.ForwardedRef<HTMLDivElement>,
|
|
74
|
+
) {
|
|
75
|
+
const Comp = asChild ? Slot : "div";
|
|
76
|
+
const { isLeftOverflowing, isRightOverflowing, containerRef, onScroll } =
|
|
77
|
+
useContext(HorizontalScrollFaderContext);
|
|
78
|
+
|
|
79
|
+
useResizeObserver({
|
|
80
|
+
ref: containerRef as React.RefObject<HTMLElement>,
|
|
81
|
+
onResize: onScroll,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// A single horizontal mask gradient fades each edge over its fader size.
|
|
85
|
+
// When an edge isn't overflowing its fader size is 0, so no fade shows.
|
|
86
|
+
// Inline styles (with the -webkit- prefix for Safari) keep this independent
|
|
87
|
+
// of Tailwind mask-utility emission.
|
|
88
|
+
const maskImage = `linear-gradient(to right, transparent 0, #000 var(--left-fader-size), #000 calc(100% - var(--right-fader-size)), transparent 100%)`;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Slot
|
|
92
|
+
onScroll={onScroll}
|
|
93
|
+
className={cn(
|
|
94
|
+
"overflow-auto [transition:--left-fader-size_150ms,--right-fader-size_150ms] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
95
|
+
)}
|
|
96
|
+
style={{
|
|
97
|
+
"--left-fader-size": isLeftOverflowing ? "3rem" : "0rem",
|
|
98
|
+
"--right-fader-size": isRightOverflowing ? "3rem" : "0rem",
|
|
99
|
+
maskImage,
|
|
100
|
+
WebkitMaskImage: maskImage,
|
|
101
|
+
}}
|
|
102
|
+
ref={containerRef}
|
|
103
|
+
>
|
|
104
|
+
<Comp ref={ref} {...props} />
|
|
105
|
+
</Slot>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const HorizontalScrollFaderContent = forwardRef(
|
|
110
|
+
HorizontalScrollFaderContentInner,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
function HorizontalScrollFaderLeftScrollerInner(
|
|
114
|
+
{ asChild, ...props }: AsChildProps<"button">,
|
|
115
|
+
ref: React.ForwardedRef<HTMLButtonElement>,
|
|
116
|
+
) {
|
|
117
|
+
const Comp = asChild ? Slot : "button";
|
|
118
|
+
const { containerRef, isLeftOverflowing } = useContext(
|
|
119
|
+
HorizontalScrollFaderContext,
|
|
120
|
+
);
|
|
121
|
+
const onClick = useCallbackRef(() => {
|
|
122
|
+
if (!containerRef.current) return;
|
|
123
|
+
containerRef.current.scrollBy({
|
|
124
|
+
left: -containerRef.current.clientWidth,
|
|
125
|
+
behavior: "smooth",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const [hoverRef, hovered] = useHover<HTMLElement>({
|
|
130
|
+
mounted: isLeftOverflowing,
|
|
131
|
+
});
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!hovered || !isLeftOverflowing) return;
|
|
134
|
+
let stopped = false;
|
|
135
|
+
let animationFrameId: number;
|
|
136
|
+
let startTime: DOMHighResTimeStamp;
|
|
137
|
+
|
|
138
|
+
const animate = (timestamp: DOMHighResTimeStamp) => {
|
|
139
|
+
if (stopped) return;
|
|
140
|
+
if (!startTime) startTime = timestamp;
|
|
141
|
+
const elapsed = Math.min(timestamp - startTime, 2000);
|
|
142
|
+
containerRef.current?.scrollBy({
|
|
143
|
+
left: -Math.log2(elapsed + 1),
|
|
144
|
+
});
|
|
145
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
146
|
+
};
|
|
147
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
stopped = true;
|
|
151
|
+
cancelAnimationFrame(animationFrameId);
|
|
152
|
+
};
|
|
153
|
+
}, [hovered, isLeftOverflowing, containerRef.current?.scrollBy]);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<Transition show={isLeftOverflowing}>
|
|
157
|
+
<Slot onClick={onClick} ref={hoverRef}>
|
|
158
|
+
<Comp {...(asChild ? {} : { type: "button" })} ref={ref} {...props} />
|
|
159
|
+
</Slot>
|
|
160
|
+
</Transition>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const HorizontalScrollFaderLeftScroller = forwardRef(
|
|
165
|
+
HorizontalScrollFaderLeftScrollerInner,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
function HorizontalScrollFaderRightScrollerInner(
|
|
169
|
+
{ asChild, ...props }: AsChildProps<"button">,
|
|
170
|
+
ref: React.ForwardedRef<HTMLButtonElement>,
|
|
171
|
+
) {
|
|
172
|
+
const Comp = asChild ? Slot : "button";
|
|
173
|
+
const { containerRef, isRightOverflowing } = useContext(
|
|
174
|
+
HorizontalScrollFaderContext,
|
|
175
|
+
);
|
|
176
|
+
const onClick = useCallbackRef(() => {
|
|
177
|
+
if (!containerRef.current) return;
|
|
178
|
+
containerRef.current.scrollBy({
|
|
179
|
+
left: containerRef.current.clientWidth,
|
|
180
|
+
behavior: "smooth",
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const [hoverRef, hovered] = useHover({
|
|
185
|
+
mounted: isRightOverflowing,
|
|
186
|
+
});
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!hovered || !isRightOverflowing) return;
|
|
189
|
+
let stopped = false;
|
|
190
|
+
let animationFrameId: number;
|
|
191
|
+
let startTime: DOMHighResTimeStamp;
|
|
192
|
+
|
|
193
|
+
const animate = (timestamp: DOMHighResTimeStamp) => {
|
|
194
|
+
if (stopped) return;
|
|
195
|
+
if (!startTime) startTime = timestamp;
|
|
196
|
+
const elapsed = Math.min(timestamp - startTime, 2000);
|
|
197
|
+
containerRef.current?.scrollBy({
|
|
198
|
+
left: Math.log2(elapsed + 1),
|
|
199
|
+
});
|
|
200
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
201
|
+
};
|
|
202
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
stopped = true;
|
|
206
|
+
cancelAnimationFrame(animationFrameId);
|
|
207
|
+
};
|
|
208
|
+
}, [hovered, isRightOverflowing, containerRef.current?.scrollBy]);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Transition show={isRightOverflowing}>
|
|
212
|
+
<Slot onClick={onClick} ref={hoverRef}>
|
|
213
|
+
<Comp {...(asChild ? {} : { type: "button" })} ref={ref} {...props} />
|
|
214
|
+
</Slot>
|
|
215
|
+
</Transition>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const HorizontalScrollFaderRightScroller = forwardRef(
|
|
220
|
+
HorizontalScrollFaderRightScrollerInner,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
HorizontalScrollFader,
|
|
225
|
+
HorizontalScrollFaderContent,
|
|
226
|
+
HorizontalScrollFaderLeftScroller,
|
|
227
|
+
HorizontalScrollFaderRightScroller,
|
|
228
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
import { Input, type InputProps } from "./input";
|
|
6
|
+
import { Textarea, type TextareaProps } from "./textarea";
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
|
|
9
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
10
|
+
return (
|
|
11
|
+
// biome-ignore lint/a11y/useSemanticElements: Imported from library
|
|
12
|
+
<div
|
|
13
|
+
className={cn(
|
|
14
|
+
"relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/6%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
data-slot="input-group"
|
|
18
|
+
role="group"
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const inputGroupAddonVariants = cva(
|
|
25
|
+
"[&_svg]:-mx-0.5 flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
|
|
26
|
+
{
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
align: "inline-start",
|
|
29
|
+
},
|
|
30
|
+
variants: {
|
|
31
|
+
align: {
|
|
32
|
+
"block-end":
|
|
33
|
+
"order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
|
|
34
|
+
"block-start":
|
|
35
|
+
"order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
|
|
36
|
+
"inline-end":
|
|
37
|
+
"has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
|
|
38
|
+
"inline-start":
|
|
39
|
+
"has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function InputGroupAddon({
|
|
46
|
+
className,
|
|
47
|
+
align = "inline-start",
|
|
48
|
+
...props
|
|
49
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
50
|
+
return (
|
|
51
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: Imported from library
|
|
52
|
+
<div
|
|
53
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
54
|
+
data-align={align}
|
|
55
|
+
data-slot="input-group-addon"
|
|
56
|
+
onMouseDown={(e) => {
|
|
57
|
+
const target = e.target as HTMLElement;
|
|
58
|
+
const isInteractive = target.closest(
|
|
59
|
+
"button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
|
|
60
|
+
);
|
|
61
|
+
if (isInteractive) return;
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const parent = e.currentTarget.parentElement;
|
|
64
|
+
const input = parent?.querySelector<
|
|
65
|
+
HTMLInputElement | HTMLTextAreaElement
|
|
66
|
+
>("input, textarea");
|
|
67
|
+
if (input && !parent?.querySelector("input:focus, textarea:focus")) {
|
|
68
|
+
input.focus();
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
"[&_svg]:-mx-0.5 line-clamp-1 flex items-center gap-2 text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function InputGroupInput({ className, ...props }: InputProps) {
|
|
89
|
+
return <Input className={className} unstyled {...props} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function InputGroupTextarea({ className, ...props }: TextareaProps) {
|
|
93
|
+
return <Textarea className={className} unstyled {...props} />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
InputGroup,
|
|
98
|
+
InputGroupAddon,
|
|
99
|
+
InputGroupText,
|
|
100
|
+
InputGroupInput,
|
|
101
|
+
InputGroupTextarea,
|
|
102
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { OTPInput, OTPInputContext, REGEXP_ONLY_DIGITS } from "input-otp";
|
|
5
|
+
import { MinusIcon } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../lib/utils/css";
|
|
9
|
+
|
|
10
|
+
type InputOTPProps = React.ComponentProps<typeof OTPInput>;
|
|
11
|
+
|
|
12
|
+
function InputOTP({
|
|
13
|
+
className,
|
|
14
|
+
containerClassName,
|
|
15
|
+
// Digits-only by default. Override `pattern`/`inputMode` to allow letters.
|
|
16
|
+
inputMode = "numeric",
|
|
17
|
+
pattern = REGEXP_ONLY_DIGITS,
|
|
18
|
+
...props
|
|
19
|
+
}: InputOTPProps) {
|
|
20
|
+
return (
|
|
21
|
+
<OTPInput
|
|
22
|
+
data-slot="input-otp"
|
|
23
|
+
containerClassName={cn(
|
|
24
|
+
"flex items-center gap-2 has-disabled:opacity-50",
|
|
25
|
+
containerClassName,
|
|
26
|
+
)}
|
|
27
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
28
|
+
inputMode={inputMode}
|
|
29
|
+
pattern={pattern}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="input-otp-group"
|
|
39
|
+
className={cn("flex items-center", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const inputOTPSlotVariants = cva(
|
|
46
|
+
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex items-center justify-center border-y border-r shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
|
47
|
+
{
|
|
48
|
+
variants: {
|
|
49
|
+
size: {
|
|
50
|
+
sm: "h-8 w-8 text-xs",
|
|
51
|
+
default: "h-9 w-9 text-sm",
|
|
52
|
+
lg: "h-11 w-11 text-base",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultVariants: { size: "default" },
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
function InputOTPSlot({
|
|
60
|
+
index,
|
|
61
|
+
className,
|
|
62
|
+
size,
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<"div"> & {
|
|
65
|
+
index: number;
|
|
66
|
+
} & VariantProps<typeof inputOTPSlotVariants>) {
|
|
67
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
68
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-slot="input-otp-slot"
|
|
73
|
+
data-active={isActive}
|
|
74
|
+
className={cn(inputOTPSlotVariants({ size }), className)}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{char}
|
|
78
|
+
{hasFakeCaret && (
|
|
79
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
80
|
+
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
|
88
|
+
return (
|
|
89
|
+
// biome-ignore lint/a11y/useFocusableInteractive lint/a11y/useSemanticElements lint/a11y/useAriaPropsForRole: Installed from shadcn
|
|
90
|
+
<div data-slot="input-otp-separator" role="separator" {...props}>
|
|
91
|
+
<MinusIcon />
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|