@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.
Files changed (72) hide show
  1. package/package.json +72 -0
  2. package/src/accordion.tsx +68 -0
  3. package/src/alert.tsx +107 -0
  4. package/src/avatar-group.tsx +96 -0
  5. package/src/avatar-tag.tsx +136 -0
  6. package/src/avatar.tsx +39 -0
  7. package/src/badge.tsx +98 -0
  8. package/src/blanket.tsx +61 -0
  9. package/src/breadcrumb.tsx +119 -0
  10. package/src/button-group.tsx +218 -0
  11. package/src/button.tsx +83 -0
  12. package/src/calendar.tsx +227 -0
  13. package/src/callout.tsx +68 -0
  14. package/src/card.tsx +103 -0
  15. package/src/chart-templates.tsx +587 -0
  16. package/src/chart.tsx +379 -0
  17. package/src/checkbox-select.tsx +239 -0
  18. package/src/checkbox.tsx +54 -0
  19. package/src/code.tsx +154 -0
  20. package/src/command.tsx +77 -0
  21. package/src/country-select.tsx +242 -0
  22. package/src/currency-amount-input.tsx +72 -0
  23. package/src/data-table.tsx +378 -0
  24. package/src/date-picker.tsx +317 -0
  25. package/src/dialog.tsx +81 -0
  26. package/src/drawer.tsx +91 -0
  27. package/src/dropdown-menu.tsx +174 -0
  28. package/src/empty-state.tsx +32 -0
  29. package/src/field.tsx +243 -0
  30. package/src/flag.tsx +265 -0
  31. package/src/form.tsx +168 -0
  32. package/src/grid-flex.tsx +241 -0
  33. package/src/heading.tsx +202 -0
  34. package/src/icon-button.tsx +93 -0
  35. package/src/index.ts +332 -0
  36. package/src/inline-dialog.tsx +153 -0
  37. package/src/inline-edit.tsx +212 -0
  38. package/src/input-group.tsx +151 -0
  39. package/src/input.tsx +28 -0
  40. package/src/label.tsx +21 -0
  41. package/src/layout.tsx +119 -0
  42. package/src/link.tsx +80 -0
  43. package/src/lozenge.tsx +61 -0
  44. package/src/menu.tsx +146 -0
  45. package/src/otp-input.tsx +117 -0
  46. package/src/page-header.tsx +28 -0
  47. package/src/pagination.tsx +185 -0
  48. package/src/password-input.tsx +34 -0
  49. package/src/popover.tsx +31 -0
  50. package/src/progress-indicator.tsx +94 -0
  51. package/src/progress.tsx +95 -0
  52. package/src/radio-group.tsx +46 -0
  53. package/src/responsive.tsx +276 -0
  54. package/src/scroll-area.tsx +39 -0
  55. package/src/section-message.tsx +119 -0
  56. package/src/select.tsx +144 -0
  57. package/src/separator.tsx +26 -0
  58. package/src/side-nav.tsx +264 -0
  59. package/src/skeleton.tsx +74 -0
  60. package/src/slider.tsx +25 -0
  61. package/src/sonner.tsx +32 -0
  62. package/src/spinner.tsx +54 -0
  63. package/src/spotlight.tsx +141 -0
  64. package/src/status-badge.tsx +86 -0
  65. package/src/switch.tsx +70 -0
  66. package/src/tabs.tsx +57 -0
  67. package/src/tag.tsx +52 -0
  68. package/src/textarea.tsx +25 -0
  69. package/src/time-picker.tsx +443 -0
  70. package/src/tooltip.tsx +29 -0
  71. package/src/utils.ts +6 -0
  72. 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";
@@ -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";
@@ -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 };