@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
package/src/link.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
const linkVariants = cva(
|
|
9
|
+
[
|
|
10
|
+
"inline-flex items-center gap-1 transition-colors duration-pg-fast ease-pg-standard",
|
|
11
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 rounded-sm",
|
|
12
|
+
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
|
|
13
|
+
],
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
default: "text-primary underline-offset-4 hover:underline",
|
|
18
|
+
subtle: "text-muted-foreground hover:text-foreground",
|
|
19
|
+
nav: "text-foreground hover:text-primary",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
sm: "text-xs",
|
|
23
|
+
md: "text-sm",
|
|
24
|
+
lg: "text-base",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
variant: "default",
|
|
29
|
+
size: "md",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export interface LinkProps
|
|
35
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
36
|
+
VariantProps<typeof linkVariants> {
|
|
37
|
+
asChild?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
className,
|
|
44
|
+
variant,
|
|
45
|
+
size,
|
|
46
|
+
asChild = false,
|
|
47
|
+
href,
|
|
48
|
+
target,
|
|
49
|
+
rel,
|
|
50
|
+
children,
|
|
51
|
+
...props
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) => {
|
|
55
|
+
const Comp = asChild ? Slot : "a";
|
|
56
|
+
|
|
57
|
+
const isExternal =
|
|
58
|
+
typeof href === "string" && href.startsWith("http");
|
|
59
|
+
|
|
60
|
+
const externalProps = isExternal
|
|
61
|
+
? {
|
|
62
|
+
target: target ?? "_blank",
|
|
63
|
+
rel: rel ?? "noreferrer",
|
|
64
|
+
}
|
|
65
|
+
: { target, rel };
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Comp
|
|
69
|
+
ref={ref}
|
|
70
|
+
href={href}
|
|
71
|
+
className={cn(linkVariants({ variant, size }), className)}
|
|
72
|
+
{...externalProps}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</Comp>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
Link.displayName = "Link";
|
package/src/lozenge.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
export interface LozengeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
variant?: "default" | "inprogress" | "success" | "moved" | "new" | "removed";
|
|
8
|
+
isBold?: boolean;
|
|
9
|
+
maxWidth?: number | string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const variantClasses: Record<NonNullable<LozengeProps["variant"]>, string> = {
|
|
13
|
+
default: "bg-muted text-muted-foreground",
|
|
14
|
+
inprogress: "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
15
|
+
success: "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
16
|
+
moved: "bg-purple-500/15 text-purple-700 dark:text-purple-300",
|
|
17
|
+
new: "bg-teal-500/15 text-teal-700 dark:text-teal-300",
|
|
18
|
+
removed: "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Lozenge = forwardRef<HTMLSpanElement, LozengeProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
variant = "default",
|
|
25
|
+
isBold = false,
|
|
26
|
+
maxWidth,
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
style,
|
|
30
|
+
...props
|
|
31
|
+
},
|
|
32
|
+
ref
|
|
33
|
+
) => {
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
"inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide whitespace-nowrap transition-colors duration-pg-fast ease-pg-standard",
|
|
39
|
+
variantClasses[variant],
|
|
40
|
+
isBold && "ring-1 ring-current/30",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
style={
|
|
44
|
+
maxWidth !== undefined
|
|
45
|
+
? { maxWidth, overflow: "hidden", textOverflow: "ellipsis", ...style }
|
|
46
|
+
: style
|
|
47
|
+
}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<span
|
|
51
|
+
className={cn(
|
|
52
|
+
maxWidth !== undefined ? "truncate" : undefined
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</span>
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
Lozenge.displayName = "Lozenge";
|
package/src/menu.tsx
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { forwardRef, type AnchorHTMLAttributes, type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
// ─── MenuDivider ─────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const MenuDivider = forwardRef<HTMLHRElement, HTMLAttributes<HTMLHRElement>>(
|
|
9
|
+
({ className, ...props }, ref) => (
|
|
10
|
+
<hr
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn("h-px bg-border border-none my-1", className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
MenuDivider.displayName = "MenuDivider";
|
|
18
|
+
|
|
19
|
+
// ─── MenuSection ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface MenuSectionProps extends HTMLAttributes<HTMLDivElement> {
|
|
22
|
+
label?: string;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MenuSection = forwardRef<HTMLDivElement, MenuSectionProps>(
|
|
27
|
+
({ label, children, className, ...props }, ref) => (
|
|
28
|
+
<div ref={ref} className={cn("flex flex-col", className)} {...props}>
|
|
29
|
+
{label && (
|
|
30
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground px-2 py-1.5 mt-2 first:mt-0 select-none">
|
|
31
|
+
{label}
|
|
32
|
+
</span>
|
|
33
|
+
)}
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
MenuSection.displayName = "MenuSection";
|
|
39
|
+
|
|
40
|
+
// ─── MenuItem ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
type MenuItemBaseProps = {
|
|
43
|
+
icon?: ReactNode;
|
|
44
|
+
rightContent?: ReactNode;
|
|
45
|
+
isSelected?: boolean;
|
|
46
|
+
isDanger?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type MenuItemButtonProps = MenuItemBaseProps &
|
|
50
|
+
ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
51
|
+
href?: undefined;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type MenuItemAnchorProps = MenuItemBaseProps &
|
|
55
|
+
AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
56
|
+
href: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type MenuItemProps = MenuItemButtonProps | MenuItemAnchorProps;
|
|
60
|
+
|
|
61
|
+
const menuItemBaseClasses =
|
|
62
|
+
"flex items-center gap-2.5 px-2 py-2 rounded-lg text-sm transition-colors duration-pg-fast ease-pg-standard w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35";
|
|
63
|
+
|
|
64
|
+
export const MenuItem = forwardRef<HTMLButtonElement | HTMLAnchorElement, MenuItemProps>(
|
|
65
|
+
(props, ref) => {
|
|
66
|
+
const {
|
|
67
|
+
icon,
|
|
68
|
+
rightContent,
|
|
69
|
+
isSelected,
|
|
70
|
+
isDanger,
|
|
71
|
+
children,
|
|
72
|
+
className,
|
|
73
|
+
...rest
|
|
74
|
+
} = props;
|
|
75
|
+
|
|
76
|
+
const resolvedClasses = cn(
|
|
77
|
+
menuItemBaseClasses,
|
|
78
|
+
!isDanger && !isSelected && "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
79
|
+
isSelected && !isDanger && "bg-muted text-foreground font-medium",
|
|
80
|
+
isDanger && "text-destructive hover:bg-destructive/10",
|
|
81
|
+
(rest as { disabled?: boolean }).disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
|
82
|
+
className
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const content = (
|
|
86
|
+
<>
|
|
87
|
+
{icon && (
|
|
88
|
+
<span className="size-4 shrink-0 flex items-center justify-center [&>svg]:size-4">
|
|
89
|
+
{icon}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
<span className="flex-1 text-left truncate">{children}</span>
|
|
93
|
+
{rightContent && (
|
|
94
|
+
<span className="ml-auto shrink-0">{rightContent}</span>
|
|
95
|
+
)}
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if ("href" in props && props.href !== undefined) {
|
|
100
|
+
const { href, ...anchorRest } = rest as AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
|
101
|
+
return (
|
|
102
|
+
<a
|
|
103
|
+
ref={ref as React.Ref<HTMLAnchorElement>}
|
|
104
|
+
href={href}
|
|
105
|
+
className={resolvedClasses}
|
|
106
|
+
{...anchorRest}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</a>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { disabled, ...buttonRest } = rest as ButtonHTMLAttributes<HTMLButtonElement>;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
117
|
+
type="button"
|
|
118
|
+
disabled={disabled}
|
|
119
|
+
className={resolvedClasses}
|
|
120
|
+
{...buttonRest}
|
|
121
|
+
>
|
|
122
|
+
{content}
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
MenuItem.displayName = "MenuItem";
|
|
128
|
+
|
|
129
|
+
// ─── Menu ────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export interface MenuProps extends HTMLAttributes<HTMLElement> {
|
|
132
|
+
children: ReactNode;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const Menu = forwardRef<HTMLElement, MenuProps>(
|
|
136
|
+
({ children, className, ...props }, ref) => (
|
|
137
|
+
<nav
|
|
138
|
+
ref={ref as React.Ref<HTMLElement>}
|
|
139
|
+
className={cn("flex flex-col", className)}
|
|
140
|
+
{...props}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</nav>
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
Menu.displayName = "Menu";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
export interface OtpInputProps {
|
|
7
|
+
/** Current OTP value (controlled). */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Fires with the full joined string on every change. */
|
|
10
|
+
onChange: (value: string) => void;
|
|
11
|
+
/** Number of digit boxes. */
|
|
12
|
+
length?: number;
|
|
13
|
+
/** Fired when all boxes are filled. */
|
|
14
|
+
onComplete?: (value: string) => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
invalid?: boolean;
|
|
17
|
+
autoFocus?: boolean;
|
|
18
|
+
"aria-label"?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function OtpInput({
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
length = 6,
|
|
25
|
+
onComplete,
|
|
26
|
+
disabled,
|
|
27
|
+
invalid,
|
|
28
|
+
autoFocus,
|
|
29
|
+
"aria-label": ariaLabel = "One-time passcode",
|
|
30
|
+
}: OtpInputProps) {
|
|
31
|
+
const refs = React.useRef<Array<HTMLInputElement | null>>([]);
|
|
32
|
+
const digits = React.useMemo(() => {
|
|
33
|
+
const arr = value.split("").slice(0, length);
|
|
34
|
+
return Array.from({ length }, (_, i) => arr[i] ?? "");
|
|
35
|
+
}, [value, length]);
|
|
36
|
+
|
|
37
|
+
const emit = (next: string) => {
|
|
38
|
+
onChange(next);
|
|
39
|
+
if (next.length === length && !next.includes(" ")) onComplete?.(next);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const setAt = (index: number, char: string) => {
|
|
43
|
+
const arr = digits.slice();
|
|
44
|
+
arr[index] = char;
|
|
45
|
+
emit(arr.join("").replace(/\s+$/g, ""));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleChange = (index: number, raw: string) => {
|
|
49
|
+
const onlyDigits = raw.replace(/\D/g, "");
|
|
50
|
+
if (!onlyDigits) {
|
|
51
|
+
setAt(index, "");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const chars = onlyDigits.split("");
|
|
55
|
+
const arr = digits.slice();
|
|
56
|
+
let cursor = index;
|
|
57
|
+
for (const c of chars) {
|
|
58
|
+
if (cursor >= length) break;
|
|
59
|
+
arr[cursor] = c;
|
|
60
|
+
cursor += 1;
|
|
61
|
+
}
|
|
62
|
+
emit(arr.join(""));
|
|
63
|
+
refs.current[Math.min(cursor, length - 1)]?.focus();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
67
|
+
if (e.key === "Backspace") {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
if (digits[index]) {
|
|
70
|
+
setAt(index, "");
|
|
71
|
+
} else if (index > 0) {
|
|
72
|
+
refs.current[index - 1]?.focus();
|
|
73
|
+
setAt(index - 1, "");
|
|
74
|
+
}
|
|
75
|
+
} else if (e.key === "ArrowLeft" && index > 0) {
|
|
76
|
+
refs.current[index - 1]?.focus();
|
|
77
|
+
} else if (e.key === "ArrowRight" && index < length - 1) {
|
|
78
|
+
refs.current[index + 1]?.focus();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
|
|
85
|
+
if (!pasted) return;
|
|
86
|
+
emit(pasted);
|
|
87
|
+
refs.current[Math.min(pasted.length, length - 1)]?.focus();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex gap-2" role="group" aria-label={ariaLabel}>
|
|
92
|
+
{digits.map((digit, i) => (
|
|
93
|
+
<input
|
|
94
|
+
key={i}
|
|
95
|
+
ref={(el) => { refs.current[i] = el; }}
|
|
96
|
+
type="text"
|
|
97
|
+
inputMode="numeric"
|
|
98
|
+
autoComplete={i === 0 ? "one-time-code" : "off"}
|
|
99
|
+
maxLength={length}
|
|
100
|
+
value={digit}
|
|
101
|
+
disabled={disabled}
|
|
102
|
+
aria-invalid={invalid || undefined}
|
|
103
|
+
autoFocus={autoFocus && i === 0}
|
|
104
|
+
onChange={(e) => handleChange(i, e.target.value)}
|
|
105
|
+
onKeyDown={(e) => handleKeyDown(i, e)}
|
|
106
|
+
onPaste={handlePaste}
|
|
107
|
+
className={cn(
|
|
108
|
+
"h-12 w-11 rounded-lg border bg-card text-center text-lg font-semibold text-foreground shadow-sm transition-colors",
|
|
109
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35 focus-visible:border-primary",
|
|
110
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
111
|
+
invalid ? "border-destructive ring-destructive/20" : "border-input"
|
|
112
|
+
)}
|
|
113
|
+
/>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
|
|
4
|
+
interface PageHeaderProps {
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
/** When title is non-plain text (e.g. includes a flag), set for screen readers. */
|
|
7
|
+
titleAriaLabel?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
actions?: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PageHeader({ title, titleAriaLabel, subtitle, actions, className }: PageHeaderProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className={cn("flex items-start justify-between mb-6", className)}>
|
|
16
|
+
<div>
|
|
17
|
+
<h1
|
|
18
|
+
className="text-xl font-semibold text-foreground tracking-tight flex items-center gap-2.5 flex-wrap"
|
|
19
|
+
{...(titleAriaLabel ? { "aria-label": titleAriaLabel } : {})}
|
|
20
|
+
>
|
|
21
|
+
{title}
|
|
22
|
+
</h1>
|
|
23
|
+
{subtitle && <p className="text-sm text-muted-foreground mt-0.5">{subtitle}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Pagination (nav wrapper)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export interface PaginationProps extends React.ComponentPropsWithoutRef<"nav"> {}
|
|
12
|
+
|
|
13
|
+
export const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
|
14
|
+
({ className, ...props }, ref) => (
|
|
15
|
+
<nav
|
|
16
|
+
ref={ref}
|
|
17
|
+
role="navigation"
|
|
18
|
+
aria-label="pagination"
|
|
19
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
Pagination.displayName = "Pagination";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// PaginationContent (ul)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface PaginationContentProps
|
|
31
|
+
extends React.ComponentPropsWithoutRef<"ul"> {}
|
|
32
|
+
|
|
33
|
+
export const PaginationContent = forwardRef<
|
|
34
|
+
HTMLUListElement,
|
|
35
|
+
PaginationContentProps
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<ul
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn("flex flex-row items-center gap-1", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
));
|
|
43
|
+
PaginationContent.displayName = "PaginationContent";
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// PaginationItem (li)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export interface PaginationItemProps
|
|
50
|
+
extends React.ComponentPropsWithoutRef<"li"> {}
|
|
51
|
+
|
|
52
|
+
export const PaginationItem = forwardRef<HTMLLIElement, PaginationItemProps>(
|
|
53
|
+
({ className, ...props }, ref) => (
|
|
54
|
+
<li ref={ref} className={cn("list-none", className)} {...props} />
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
PaginationItem.displayName = "PaginationItem";
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// PaginationLink (anchor / button-like)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export interface PaginationLinkProps
|
|
64
|
+
extends React.ComponentPropsWithoutRef<"a"> {
|
|
65
|
+
isActive?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const PaginationLink = forwardRef<HTMLAnchorElement, PaginationLinkProps>(
|
|
69
|
+
({ className, isActive = false, ...props }, ref) => (
|
|
70
|
+
<a
|
|
71
|
+
ref={ref}
|
|
72
|
+
aria-current={isActive ? "page" : undefined}
|
|
73
|
+
className={cn(
|
|
74
|
+
// base
|
|
75
|
+
"h-9 w-9 inline-flex items-center justify-center rounded-lg text-sm font-medium border",
|
|
76
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
77
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
78
|
+
"cursor-pointer select-none",
|
|
79
|
+
// active
|
|
80
|
+
isActive
|
|
81
|
+
? "bg-primary text-primary-foreground border-primary shadow-sm"
|
|
82
|
+
: "text-muted-foreground border-border hover:bg-muted hover:text-foreground",
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
PaginationLink.displayName = "PaginationLink";
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// PaginationPrevious
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface PaginationPreviousProps
|
|
96
|
+
extends React.ComponentPropsWithoutRef<"a"> {
|
|
97
|
+
disabled?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const PaginationPrevious = forwardRef<
|
|
101
|
+
HTMLAnchorElement,
|
|
102
|
+
PaginationPreviousProps
|
|
103
|
+
>(({ className, disabled, ...props }, ref) => (
|
|
104
|
+
<a
|
|
105
|
+
ref={ref}
|
|
106
|
+
aria-label="Go to previous page"
|
|
107
|
+
aria-disabled={disabled}
|
|
108
|
+
className={cn(
|
|
109
|
+
"h-9 px-3 inline-flex items-center justify-center gap-1.5 rounded-lg text-sm font-medium border border-border",
|
|
110
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
111
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
112
|
+
"cursor-pointer select-none",
|
|
113
|
+
disabled
|
|
114
|
+
? "cursor-not-allowed opacity-50 pointer-events-none text-muted-foreground border-border"
|
|
115
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
116
|
+
className
|
|
117
|
+
)}
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
<ChevronLeft className="size-4" />
|
|
121
|
+
<span>Previous</span>
|
|
122
|
+
</a>
|
|
123
|
+
));
|
|
124
|
+
PaginationPrevious.displayName = "PaginationPrevious";
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// PaginationNext
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export interface PaginationNextProps
|
|
131
|
+
extends React.ComponentPropsWithoutRef<"a"> {
|
|
132
|
+
disabled?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const PaginationNext = forwardRef<
|
|
136
|
+
HTMLAnchorElement,
|
|
137
|
+
PaginationNextProps
|
|
138
|
+
>(({ className, disabled, ...props }, ref) => (
|
|
139
|
+
<a
|
|
140
|
+
ref={ref}
|
|
141
|
+
aria-label="Go to next page"
|
|
142
|
+
aria-disabled={disabled}
|
|
143
|
+
className={cn(
|
|
144
|
+
"h-9 px-3 inline-flex items-center justify-center gap-1.5 rounded-lg text-sm font-medium border border-border",
|
|
145
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
146
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
147
|
+
"cursor-pointer select-none",
|
|
148
|
+
disabled
|
|
149
|
+
? "cursor-not-allowed opacity-50 pointer-events-none text-muted-foreground border-border"
|
|
150
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
<span>Next</span>
|
|
156
|
+
<ChevronRight className="size-4" />
|
|
157
|
+
</a>
|
|
158
|
+
));
|
|
159
|
+
PaginationNext.displayName = "PaginationNext";
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// PaginationEllipsis
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
export interface PaginationEllipsisProps
|
|
166
|
+
extends React.ComponentPropsWithoutRef<"span"> {}
|
|
167
|
+
|
|
168
|
+
export const PaginationEllipsis = forwardRef<
|
|
169
|
+
HTMLSpanElement,
|
|
170
|
+
PaginationEllipsisProps
|
|
171
|
+
>(({ className, ...props }, ref) => (
|
|
172
|
+
<span
|
|
173
|
+
ref={ref}
|
|
174
|
+
aria-hidden
|
|
175
|
+
className={cn(
|
|
176
|
+
"h-9 w-9 inline-flex items-center justify-center text-muted-foreground",
|
|
177
|
+
className
|
|
178
|
+
)}
|
|
179
|
+
{...props}
|
|
180
|
+
>
|
|
181
|
+
<MoreHorizontal className="size-4" />
|
|
182
|
+
<span className="sr-only">More pages</span>
|
|
183
|
+
</span>
|
|
184
|
+
));
|
|
185
|
+
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
import { Input } from "./input";
|
|
7
|
+
|
|
8
|
+
export type PasswordInputProps = Omit<React.ComponentProps<typeof Input>, "type">;
|
|
9
|
+
|
|
10
|
+
export const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
11
|
+
({ className, ...props }, ref) => {
|
|
12
|
+
const [visible, setVisible] = React.useState(false);
|
|
13
|
+
return (
|
|
14
|
+
<div className="relative">
|
|
15
|
+
<Input
|
|
16
|
+
ref={ref}
|
|
17
|
+
type={visible ? "text" : "password"}
|
|
18
|
+
className={cn("pr-10", className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => setVisible((v) => !v)}
|
|
24
|
+
aria-label={visible ? "Hide password" : "Show password"}
|
|
25
|
+
tabIndex={-1}
|
|
26
|
+
className="absolute inset-y-0 right-0 flex w-10 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
|
|
27
|
+
>
|
|
28
|
+
{visible ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
PasswordInput.displayName = "PasswordInput";
|
package/src/popover.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
const Popover = PopoverPrimitive.Root;
|
|
8
|
+
const PopoverTrigger = PopoverPrimitive.Trigger;
|
|
9
|
+
const PopoverAnchor = PopoverPrimitive.Anchor;
|
|
10
|
+
|
|
11
|
+
const PopoverContent = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
14
|
+
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
|
15
|
+
<PopoverPrimitive.Portal>
|
|
16
|
+
<PopoverPrimitive.Content
|
|
17
|
+
ref={ref}
|
|
18
|
+
align={align}
|
|
19
|
+
sideOffset={sideOffset}
|
|
20
|
+
className={cn(
|
|
21
|
+
"z-[120] w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-lg outline-none",
|
|
22
|
+
"data-[state=open]:opacity-100 data-[state=closed]:opacity-0 transition-opacity duration-150",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
</PopoverPrimitive.Portal>
|
|
28
|
+
));
|
|
29
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
|
30
|
+
|
|
31
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|