@payglocal_ui/flux-ui 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/package.json +72 -0
- package/src/accordion.tsx +68 -0
- package/src/alert.tsx +107 -0
- package/src/avatar-group.tsx +96 -0
- package/src/avatar-tag.tsx +136 -0
- package/src/avatar.tsx +39 -0
- package/src/badge.tsx +98 -0
- package/src/blanket.tsx +61 -0
- package/src/breadcrumb.tsx +119 -0
- package/src/button-group.tsx +218 -0
- package/src/button.tsx +83 -0
- package/src/calendar.tsx +227 -0
- package/src/callout.tsx +68 -0
- package/src/card.tsx +103 -0
- package/src/chart-templates.tsx +587 -0
- package/src/chart.tsx +379 -0
- package/src/checkbox-select.tsx +239 -0
- package/src/checkbox.tsx +54 -0
- package/src/code.tsx +154 -0
- package/src/command.tsx +77 -0
- package/src/country-select.tsx +242 -0
- package/src/currency-amount-input.tsx +72 -0
- package/src/data-table.tsx +378 -0
- package/src/date-picker.tsx +317 -0
- package/src/dialog.tsx +81 -0
- package/src/drawer.tsx +91 -0
- package/src/dropdown-menu.tsx +174 -0
- package/src/empty-state.tsx +32 -0
- package/src/field.tsx +243 -0
- package/src/flag.tsx +265 -0
- package/src/form.tsx +168 -0
- package/src/grid-flex.tsx +241 -0
- package/src/heading.tsx +202 -0
- package/src/icon-button.tsx +93 -0
- package/src/index.ts +332 -0
- package/src/inline-dialog.tsx +153 -0
- package/src/inline-edit.tsx +212 -0
- package/src/input-group.tsx +151 -0
- package/src/input.tsx +28 -0
- package/src/label.tsx +21 -0
- package/src/layout.tsx +119 -0
- package/src/link.tsx +80 -0
- package/src/lozenge.tsx +61 -0
- package/src/menu.tsx +146 -0
- package/src/otp-input.tsx +117 -0
- package/src/page-header.tsx +28 -0
- package/src/pagination.tsx +185 -0
- package/src/password-input.tsx +34 -0
- package/src/popover.tsx +31 -0
- package/src/progress-indicator.tsx +94 -0
- package/src/progress.tsx +95 -0
- package/src/radio-group.tsx +46 -0
- package/src/responsive.tsx +276 -0
- package/src/scroll-area.tsx +39 -0
- package/src/section-message.tsx +119 -0
- package/src/select.tsx +144 -0
- package/src/separator.tsx +26 -0
- package/src/side-nav.tsx +264 -0
- package/src/skeleton.tsx +74 -0
- package/src/slider.tsx +25 -0
- package/src/sonner.tsx +32 -0
- package/src/spinner.tsx +54 -0
- package/src/spotlight.tsx +141 -0
- package/src/status-badge.tsx +86 -0
- package/src/switch.tsx +70 -0
- package/src/tabs.tsx +57 -0
- package/src/tag.tsx +52 -0
- package/src/textarea.tsx +25 -0
- package/src/time-picker.tsx +443 -0
- package/src/tooltip.tsx +29 -0
- package/src/utils.ts +6 -0
- package/src/visually-hidden.tsx +25 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
const Breadcrumb = React.forwardRef<
|
|
8
|
+
HTMLElement,
|
|
9
|
+
React.ComponentPropsWithoutRef<"nav"> & {
|
|
10
|
+
separator?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<nav
|
|
14
|
+
ref={ref}
|
|
15
|
+
aria-label="breadcrumb"
|
|
16
|
+
className={cn("", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
));
|
|
20
|
+
Breadcrumb.displayName = "Breadcrumb";
|
|
21
|
+
|
|
22
|
+
const BreadcrumbList = React.forwardRef<
|
|
23
|
+
HTMLOListElement,
|
|
24
|
+
React.ComponentPropsWithoutRef<"ol">
|
|
25
|
+
>(({ className, ...props }, ref) => (
|
|
26
|
+
<ol
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
));
|
|
35
|
+
BreadcrumbList.displayName = "BreadcrumbList";
|
|
36
|
+
|
|
37
|
+
const BreadcrumbItem = React.forwardRef<
|
|
38
|
+
HTMLLIElement,
|
|
39
|
+
React.ComponentPropsWithoutRef<"li">
|
|
40
|
+
>(({ className, ...props }, ref) => (
|
|
41
|
+
<li
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn("inline-flex items-center gap-1.5", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
));
|
|
47
|
+
BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
48
|
+
|
|
49
|
+
const BreadcrumbLink = React.forwardRef<
|
|
50
|
+
HTMLAnchorElement,
|
|
51
|
+
React.ComponentPropsWithoutRef<"a">
|
|
52
|
+
>(({ className, ...props }, ref) => (
|
|
53
|
+
<a
|
|
54
|
+
ref={ref}
|
|
55
|
+
className={cn(
|
|
56
|
+
"text-sm text-muted-foreground transition-colors duration-pg-fast ease-pg-standard hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 rounded-sm",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
));
|
|
62
|
+
BreadcrumbLink.displayName = "BreadcrumbLink";
|
|
63
|
+
|
|
64
|
+
const BreadcrumbPage = React.forwardRef<
|
|
65
|
+
HTMLSpanElement,
|
|
66
|
+
React.ComponentPropsWithoutRef<"span">
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<span
|
|
69
|
+
ref={ref}
|
|
70
|
+
role="link"
|
|
71
|
+
aria-current="page"
|
|
72
|
+
aria-disabled="true"
|
|
73
|
+
className={cn("text-sm font-medium text-foreground", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
));
|
|
77
|
+
BreadcrumbPage.displayName = "BreadcrumbPage";
|
|
78
|
+
|
|
79
|
+
const BreadcrumbSeparator = ({
|
|
80
|
+
children,
|
|
81
|
+
className,
|
|
82
|
+
...props
|
|
83
|
+
}: React.ComponentProps<"li">) => (
|
|
84
|
+
<li
|
|
85
|
+
role="presentation"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
className={cn("[&>svg]:size-3.5", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{children ?? <ChevronRight className="text-muted-foreground" />}
|
|
91
|
+
</li>
|
|
92
|
+
);
|
|
93
|
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
|
94
|
+
|
|
95
|
+
const BreadcrumbEllipsis = ({
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}: React.ComponentProps<"span">) => (
|
|
99
|
+
<span
|
|
100
|
+
role="presentation"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
className={cn("flex size-9 items-center justify-center", className)}
|
|
103
|
+
{...props}
|
|
104
|
+
>
|
|
105
|
+
<MoreHorizontal className="size-4 text-muted-foreground" />
|
|
106
|
+
<span className="sr-only">More</span>
|
|
107
|
+
</span>
|
|
108
|
+
);
|
|
109
|
+
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
Breadcrumb,
|
|
113
|
+
BreadcrumbList,
|
|
114
|
+
BreadcrumbItem,
|
|
115
|
+
BreadcrumbLink,
|
|
116
|
+
BreadcrumbPage,
|
|
117
|
+
BreadcrumbSeparator,
|
|
118
|
+
BreadcrumbEllipsis,
|
|
119
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
forwardRef,
|
|
7
|
+
useState,
|
|
8
|
+
useRef,
|
|
9
|
+
useEffect,
|
|
10
|
+
type HTMLAttributes,
|
|
11
|
+
type ButtonHTMLAttributes,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { Button } from "./button";
|
|
15
|
+
import type { ButtonProps } from "./button";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// ButtonGroup
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
22
|
+
orientation?: "horizontal" | "vertical";
|
|
23
|
+
variant?: ButtonProps["variant"];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
27
|
+
({ orientation = "horizontal", variant, className, children, ...props }, ref) => {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
ref={ref}
|
|
31
|
+
role="group"
|
|
32
|
+
data-variant={variant}
|
|
33
|
+
className={cn(
|
|
34
|
+
"inline-flex",
|
|
35
|
+
orientation === "vertical" ? "flex-col" : "flex-row",
|
|
36
|
+
// Horizontal connected borders
|
|
37
|
+
orientation === "horizontal" && [
|
|
38
|
+
"[&>*:not(:first-child)]:rounded-l-none",
|
|
39
|
+
"[&>*:not(:last-child)]:rounded-r-none",
|
|
40
|
+
"[&>*:not(:first-child)]:-ml-px",
|
|
41
|
+
],
|
|
42
|
+
// Vertical connected borders
|
|
43
|
+
orientation === "vertical" && [
|
|
44
|
+
"[&>*:not(:first-child)]:rounded-t-none",
|
|
45
|
+
"[&>*:not(:last-child)]:rounded-b-none",
|
|
46
|
+
"[&>*:not(:first-child)]:-mt-px",
|
|
47
|
+
],
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
ButtonGroup.displayName = "ButtonGroup";
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// SplitButton
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export interface SplitButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
|
|
64
|
+
label: ReactNode;
|
|
65
|
+
onClick?: ButtonHTMLAttributes<HTMLButtonElement>["onClick"];
|
|
66
|
+
children?: ReactNode;
|
|
67
|
+
variant?: ButtonProps["variant"];
|
|
68
|
+
size?: ButtonProps["size"];
|
|
69
|
+
isLoading?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
|
|
73
|
+
(
|
|
74
|
+
{
|
|
75
|
+
label,
|
|
76
|
+
onClick,
|
|
77
|
+
children,
|
|
78
|
+
variant = "primary",
|
|
79
|
+
size = "md",
|
|
80
|
+
isLoading = false,
|
|
81
|
+
className,
|
|
82
|
+
disabled,
|
|
83
|
+
...props
|
|
84
|
+
},
|
|
85
|
+
ref
|
|
86
|
+
) => {
|
|
87
|
+
const [open, setOpen] = useState(false);
|
|
88
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
89
|
+
|
|
90
|
+
// Merge forwarded ref with internal ref
|
|
91
|
+
function setRefs(el: HTMLDivElement | null) {
|
|
92
|
+
(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
93
|
+
if (typeof ref === "function") {
|
|
94
|
+
ref(el);
|
|
95
|
+
} else if (ref) {
|
|
96
|
+
(ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Close dropdown when clicking outside
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!open) return;
|
|
103
|
+
function handleOutside(e: MouseEvent) {
|
|
104
|
+
if (
|
|
105
|
+
containerRef.current &&
|
|
106
|
+
!containerRef.current.contains(e.target as Node)
|
|
107
|
+
) {
|
|
108
|
+
setOpen(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
document.addEventListener("mousedown", handleOutside);
|
|
112
|
+
return () => document.removeEventListener("mousedown", handleOutside);
|
|
113
|
+
}, [open]);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
ref={setRefs}
|
|
118
|
+
className={cn("relative inline-flex", className)}
|
|
119
|
+
{...(props as HTMLAttributes<HTMLDivElement>)}
|
|
120
|
+
>
|
|
121
|
+
{/* Primary action button */}
|
|
122
|
+
<Button
|
|
123
|
+
variant={variant}
|
|
124
|
+
size={size}
|
|
125
|
+
isLoading={isLoading}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
onClick={onClick}
|
|
128
|
+
className="rounded-r-none"
|
|
129
|
+
>
|
|
130
|
+
{label}
|
|
131
|
+
</Button>
|
|
132
|
+
|
|
133
|
+
{/* Dropdown trigger */}
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
disabled={disabled || isLoading}
|
|
137
|
+
aria-haspopup="menu"
|
|
138
|
+
aria-expanded={open}
|
|
139
|
+
onClick={() => setOpen((v) => !v)}
|
|
140
|
+
className={cn(
|
|
141
|
+
"inline-flex items-center justify-center px-2 rounded-l-none -ml-px",
|
|
142
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
143
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
144
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
145
|
+
// Match variant styling
|
|
146
|
+
variant === "primary" &&
|
|
147
|
+
"bg-primary text-primary-foreground border border-primary shadow-sm hover:bg-[var(--primary-hover)] border-l border-l-primary-foreground/20",
|
|
148
|
+
variant === "secondary" &&
|
|
149
|
+
"bg-muted text-foreground border border-border shadow-sm hover:bg-muted/85 dark:bg-muted/35 dark:hover:bg-muted/55 border-l-0",
|
|
150
|
+
variant === "outline" &&
|
|
151
|
+
"bg-card text-foreground border border-border shadow-sm hover:bg-muted border-l-0",
|
|
152
|
+
variant === "ghost" &&
|
|
153
|
+
"bg-transparent text-foreground border border-transparent hover:bg-muted border-l-0",
|
|
154
|
+
variant === "danger" &&
|
|
155
|
+
"bg-red-600 text-white border border-red-600 shadow-sm hover:bg-red-700 border-l border-l-white/20",
|
|
156
|
+
// Size heights to match Button
|
|
157
|
+
size === "sm" && "h-9 min-h-9 rounded-lg",
|
|
158
|
+
size === "md" && "h-10 min-h-10 rounded-lg",
|
|
159
|
+
size === "lg" && "h-[3.25rem] min-h-[3.25rem] rounded-xl"
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<ChevronDown
|
|
163
|
+
className={cn(
|
|
164
|
+
"transition-transform duration-pg-fast ease-pg-standard",
|
|
165
|
+
open && "rotate-180",
|
|
166
|
+
size === "sm" ? "size-3.5" : size === "lg" ? "size-[1.125rem]" : "size-4"
|
|
167
|
+
)}
|
|
168
|
+
/>
|
|
169
|
+
</button>
|
|
170
|
+
|
|
171
|
+
{/* Dropdown menu */}
|
|
172
|
+
{open && children && (
|
|
173
|
+
<div
|
|
174
|
+
role="menu"
|
|
175
|
+
className={cn(
|
|
176
|
+
"absolute right-0 top-full mt-1 z-50",
|
|
177
|
+
"bg-card border border-border rounded-lg shadow-lg py-1 min-w-32"
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{children}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
SplitButton.displayName = "SplitButton";
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// SplitButtonItem — convenience wrapper for dropdown items
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export interface SplitButtonItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
194
|
+
children: ReactNode;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const SplitButtonItem = forwardRef<HTMLButtonElement, SplitButtonItemProps>(
|
|
198
|
+
({ children, className, ...props }, ref) => {
|
|
199
|
+
return (
|
|
200
|
+
<button
|
|
201
|
+
ref={ref}
|
|
202
|
+
role="menuitem"
|
|
203
|
+
type="button"
|
|
204
|
+
className={cn(
|
|
205
|
+
"block w-full text-left px-3 py-2 text-sm text-foreground",
|
|
206
|
+
"hover:bg-muted transition-colors duration-pg-fast ease-pg-standard",
|
|
207
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
208
|
+
"focus-visible:outline-none focus-visible:bg-muted",
|
|
209
|
+
className
|
|
210
|
+
)}
|
|
211
|
+
{...props}
|
|
212
|
+
>
|
|
213
|
+
{children}
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
SplitButtonItem.displayName = "SplitButtonItem";
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { type ButtonHTMLAttributes, forwardRef } from "react";
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
variant?: "primary" | "secondary" | "ghost" | "danger" | "outline" | "link";
|
|
9
|
+
size?: "sm" | "md" | "lg";
|
|
10
|
+
isLoading?: boolean;
|
|
11
|
+
leftIcon?: React.ReactNode;
|
|
12
|
+
rightIcon?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const variantClasses: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
|
16
|
+
primary:
|
|
17
|
+
"bg-primary text-primary-foreground border border-primary shadow-sm hover:bg-[var(--primary-hover)]",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-muted text-foreground border border-border shadow-sm hover:bg-muted/85 dark:bg-muted/35 dark:text-foreground dark:border-border dark:hover:bg-muted/55",
|
|
20
|
+
ghost:
|
|
21
|
+
"bg-transparent text-foreground border border-transparent hover:bg-muted focus-visible:bg-muted/80 active:bg-muted/90",
|
|
22
|
+
danger:
|
|
23
|
+
"bg-red-600 text-white border border-red-600 shadow-sm hover:bg-red-700",
|
|
24
|
+
outline:
|
|
25
|
+
"bg-card text-foreground border border-border shadow-sm hover:bg-muted",
|
|
26
|
+
link:
|
|
27
|
+
"h-auto min-h-0 rounded-md border border-transparent bg-transparent px-2 py-2 text-[15px] font-medium text-primary shadow-none underline-offset-4 hover:bg-primary/5 hover:underline focus-visible:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 disabled:pointer-events-none disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:no-underline",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Heights and padding match dashboard primary actions (~44–48px) and comfortable tap targets. */
|
|
31
|
+
const sizes = {
|
|
32
|
+
sm: "h-9 min-h-9 px-3.5 text-xs gap-1.5 rounded-lg",
|
|
33
|
+
md: "h-10 min-h-10 px-5 text-sm gap-2 rounded-lg",
|
|
34
|
+
lg: "h-[3.25rem] min-h-[3.25rem] px-10 text-[15px] gap-2.5 rounded-xl",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
variant = "primary",
|
|
41
|
+
size = "md",
|
|
42
|
+
isLoading = false,
|
|
43
|
+
leftIcon,
|
|
44
|
+
rightIcon,
|
|
45
|
+
children,
|
|
46
|
+
className,
|
|
47
|
+
disabled,
|
|
48
|
+
...props
|
|
49
|
+
},
|
|
50
|
+
ref
|
|
51
|
+
) => {
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
ref={ref}
|
|
55
|
+
disabled={disabled || isLoading}
|
|
56
|
+
className={cn(
|
|
57
|
+
"inline-flex items-center justify-center font-medium transition-colors duration-pg-fast ease-pg-standard",
|
|
58
|
+
variant !== "link" && "disabled:cursor-not-allowed disabled:opacity-50",
|
|
59
|
+
variant === "link" && "justify-center",
|
|
60
|
+
variant !== "link" && "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
61
|
+
variantClasses[variant],
|
|
62
|
+
variant !== "link" && sizes[size],
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{isLoading ? (
|
|
68
|
+
<Loader2
|
|
69
|
+
className={cn(
|
|
70
|
+
"animate-spin",
|
|
71
|
+
size === "sm" ? "size-3.5" : size === "lg" ? "size-[1.125rem]" : "size-3.5"
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
) : (
|
|
75
|
+
leftIcon
|
|
76
|
+
)}
|
|
77
|
+
<span className={cn(isLoading && "opacity-70")}>{children}</span>
|
|
78
|
+
{!isLoading && rightIcon}
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
Button.displayName = "Button";
|
package/src/calendar.tsx
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
DayPicker,
|
|
7
|
+
getDefaultClassNames,
|
|
8
|
+
type DayButtonProps,
|
|
9
|
+
type DayPickerProps,
|
|
10
|
+
} from "react-day-picker";
|
|
11
|
+
|
|
12
|
+
import { Button } from "./button";
|
|
13
|
+
import type { ButtonProps } from "./button";
|
|
14
|
+
import { cn } from "./utils";
|
|
15
|
+
|
|
16
|
+
const cellSize = "[--cell-size:2rem]";
|
|
17
|
+
|
|
18
|
+
function navButtonClasses(variant: NonNullable<ButtonProps["variant"]>): string {
|
|
19
|
+
const base =
|
|
20
|
+
"inline-flex items-center justify-center rounded-lg border font-medium transition-colors duration-pg-fast ease-pg-standard focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 disabled:pointer-events-none disabled:opacity-50";
|
|
21
|
+
switch (variant) {
|
|
22
|
+
case "outline":
|
|
23
|
+
return cn(
|
|
24
|
+
base,
|
|
25
|
+
"border-border bg-card text-foreground shadow-sm hover:bg-muted"
|
|
26
|
+
);
|
|
27
|
+
case "ghost":
|
|
28
|
+
default:
|
|
29
|
+
return cn(
|
|
30
|
+
base,
|
|
31
|
+
"border-transparent bg-transparent text-foreground hover:bg-muted focus-visible:bg-muted/80 active:bg-muted/90"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type CalendarProps = DayPickerProps & {
|
|
37
|
+
buttonVariant?: ButtonProps["variant"];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function Calendar({
|
|
41
|
+
className,
|
|
42
|
+
classNames,
|
|
43
|
+
showOutsideDays = true,
|
|
44
|
+
captionLayout = "label",
|
|
45
|
+
buttonVariant = "ghost",
|
|
46
|
+
locale,
|
|
47
|
+
formatters,
|
|
48
|
+
components,
|
|
49
|
+
showWeekNumber,
|
|
50
|
+
...props
|
|
51
|
+
}: CalendarProps) {
|
|
52
|
+
const defaultClassNames = getDefaultClassNames();
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<DayPicker
|
|
56
|
+
showOutsideDays={showOutsideDays}
|
|
57
|
+
showWeekNumber={showWeekNumber}
|
|
58
|
+
locale={locale}
|
|
59
|
+
className={cn(
|
|
60
|
+
"group/calendar bg-background p-3",
|
|
61
|
+
cellSize,
|
|
62
|
+
"[[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
|
63
|
+
String.raw`rtl:**:[.rdp-button_next>svg]:rotate-180`,
|
|
64
|
+
String.raw`rtl:**:[.rdp-button_previous>svg]:rotate-180`,
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
captionLayout={captionLayout}
|
|
68
|
+
formatters={{
|
|
69
|
+
formatMonthDropdown: (date) =>
|
|
70
|
+
date.toLocaleString(locale?.code ?? "default", { month: "short" }),
|
|
71
|
+
...formatters,
|
|
72
|
+
}}
|
|
73
|
+
classNames={{
|
|
74
|
+
root: cn("w-fit", defaultClassNames.root),
|
|
75
|
+
months: cn(
|
|
76
|
+
"relative flex flex-col gap-4 md:flex-row",
|
|
77
|
+
defaultClassNames.months
|
|
78
|
+
),
|
|
79
|
+
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
80
|
+
nav: cn(
|
|
81
|
+
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
|
82
|
+
defaultClassNames.nav
|
|
83
|
+
),
|
|
84
|
+
button_previous: cn(
|
|
85
|
+
navButtonClasses(buttonVariant),
|
|
86
|
+
"h-[var(--cell-size)] w-[var(--cell-size)] min-h-0 min-w-0 shrink-0 p-0 select-none",
|
|
87
|
+
defaultClassNames.button_previous
|
|
88
|
+
),
|
|
89
|
+
button_next: cn(
|
|
90
|
+
navButtonClasses(buttonVariant),
|
|
91
|
+
"h-[var(--cell-size)] w-[var(--cell-size)] min-h-0 min-w-0 shrink-0 p-0 select-none",
|
|
92
|
+
defaultClassNames.button_next
|
|
93
|
+
),
|
|
94
|
+
month_caption: cn(
|
|
95
|
+
"flex h-[var(--cell-size)] w-full items-center justify-center px-[var(--cell-size)]",
|
|
96
|
+
defaultClassNames.month_caption
|
|
97
|
+
),
|
|
98
|
+
dropdowns: cn(
|
|
99
|
+
"flex h-[var(--cell-size)] w-full items-center justify-center gap-1.5 text-sm font-medium",
|
|
100
|
+
defaultClassNames.dropdowns
|
|
101
|
+
),
|
|
102
|
+
dropdown_root: cn(
|
|
103
|
+
"relative rounded-md border border-border bg-card shadow-sm focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/35",
|
|
104
|
+
defaultClassNames.dropdown_root
|
|
105
|
+
),
|
|
106
|
+
dropdown: cn("absolute inset-0 cursor-pointer opacity-0", defaultClassNames.dropdown),
|
|
107
|
+
caption_label: cn(
|
|
108
|
+
"font-medium select-none",
|
|
109
|
+
captionLayout === "label"
|
|
110
|
+
? "text-sm"
|
|
111
|
+
: "flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
|
112
|
+
defaultClassNames.caption_label
|
|
113
|
+
),
|
|
114
|
+
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
|
|
115
|
+
weekdays: cn("flex", defaultClassNames.weekdays),
|
|
116
|
+
weekday: cn(
|
|
117
|
+
"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
|
|
118
|
+
defaultClassNames.weekday
|
|
119
|
+
),
|
|
120
|
+
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
|
121
|
+
week_number_header: cn(
|
|
122
|
+
"w-[var(--cell-size)] select-none",
|
|
123
|
+
defaultClassNames.week_number_header
|
|
124
|
+
),
|
|
125
|
+
week_number: cn(
|
|
126
|
+
"text-[0.8rem] text-muted-foreground select-none",
|
|
127
|
+
defaultClassNames.week_number
|
|
128
|
+
),
|
|
129
|
+
day: cn(
|
|
130
|
+
"group/day relative aspect-square h-full w-full p-0 text-center select-none",
|
|
131
|
+
"[&:last-child[data-selected=true]_button]:rounded-r-md",
|
|
132
|
+
showWeekNumber
|
|
133
|
+
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
|
134
|
+
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
|
135
|
+
defaultClassNames.day
|
|
136
|
+
),
|
|
137
|
+
range_start: cn(
|
|
138
|
+
"rounded-l-md bg-accent",
|
|
139
|
+
defaultClassNames.range_start
|
|
140
|
+
),
|
|
141
|
+
range_middle: cn("rounded-none bg-accent/60", defaultClassNames.range_middle),
|
|
142
|
+
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
|
143
|
+
today: cn(
|
|
144
|
+
"rounded-md bg-muted text-foreground data-[selected=true]:rounded-none",
|
|
145
|
+
defaultClassNames.today
|
|
146
|
+
),
|
|
147
|
+
outside: cn(
|
|
148
|
+
"text-muted-foreground aria-selected:text-muted-foreground",
|
|
149
|
+
defaultClassNames.outside
|
|
150
|
+
),
|
|
151
|
+
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
|
|
152
|
+
hidden: cn("invisible", defaultClassNames.hidden),
|
|
153
|
+
...classNames,
|
|
154
|
+
}}
|
|
155
|
+
components={{
|
|
156
|
+
Root: ({ className: rootClass, rootRef, ...rootProps }) => (
|
|
157
|
+
<div
|
|
158
|
+
data-slot="calendar"
|
|
159
|
+
ref={rootRef}
|
|
160
|
+
className={cn(rootClass)}
|
|
161
|
+
{...rootProps}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
Chevron: ({ className: chClass, orientation, ...chProps }) => {
|
|
165
|
+
if (orientation === "left") {
|
|
166
|
+
return <ChevronLeft className={cn("size-4", chClass)} {...chProps} />;
|
|
167
|
+
}
|
|
168
|
+
if (orientation === "right") {
|
|
169
|
+
return <ChevronRight className={cn("size-4", chClass)} {...chProps} />;
|
|
170
|
+
}
|
|
171
|
+
return <ChevronDown className={cn("size-4", chClass)} {...chProps} />;
|
|
172
|
+
},
|
|
173
|
+
DayButton: CalendarDayButton,
|
|
174
|
+
WeekNumber: ({ children, ...weekProps }) => (
|
|
175
|
+
<th {...weekProps}>
|
|
176
|
+
<div className="flex h-[var(--cell-size)] w-[var(--cell-size)] min-h-[var(--cell-size)] min-w-[var(--cell-size)] items-center justify-center text-center text-xs tabular-nums">
|
|
177
|
+
{children}
|
|
178
|
+
</div>
|
|
179
|
+
</th>
|
|
180
|
+
),
|
|
181
|
+
...components,
|
|
182
|
+
}}
|
|
183
|
+
{...props}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function CalendarDayButton({
|
|
189
|
+
className,
|
|
190
|
+
day,
|
|
191
|
+
modifiers,
|
|
192
|
+
...props
|
|
193
|
+
}: DayButtonProps) {
|
|
194
|
+
const defaultClassNames = getDefaultClassNames();
|
|
195
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
196
|
+
React.useEffect(() => {
|
|
197
|
+
if (modifiers.focused) ref.current?.focus();
|
|
198
|
+
}, [modifiers.focused]);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Button
|
|
202
|
+
ref={ref}
|
|
203
|
+
type="button"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
{...props}
|
|
206
|
+
disabled={Boolean(modifiers.disabled) || props.disabled}
|
|
207
|
+
data-day={day.date.toLocaleDateString()}
|
|
208
|
+
data-selected-single={
|
|
209
|
+
modifiers.selected &&
|
|
210
|
+
!modifiers.range_start &&
|
|
211
|
+
!modifiers.range_end &&
|
|
212
|
+
!modifiers.range_middle
|
|
213
|
+
}
|
|
214
|
+
data-range-start={modifiers.range_start}
|
|
215
|
+
data-range-end={modifiers.range_end}
|
|
216
|
+
data-range-middle={modifiers.range_middle}
|
|
217
|
+
className={cn(
|
|
218
|
+
"flex aspect-square h-auto min-h-[var(--cell-size)] w-full min-w-[var(--cell-size)] flex-col gap-1 rounded-md p-0 font-normal leading-none data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground hover:bg-muted/80 dark:hover:bg-muted/50 [&>span]:text-xs [&>span]:opacity-70",
|
|
219
|
+
"group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/35",
|
|
220
|
+
defaultClassNames.day,
|
|
221
|
+
className
|
|
222
|
+
)}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export { Calendar, CalendarDayButton };
|