@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,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
export type ProgressIndicatorSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export interface ProgressIndicatorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
9
|
+
selectedIndex: number;
|
|
10
|
+
values: string[];
|
|
11
|
+
size?: ProgressIndicatorSize;
|
|
12
|
+
onChange?: (index: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const dotSizeMap: Record<ProgressIndicatorSize, { base: string; active: string }> = {
|
|
16
|
+
sm: {
|
|
17
|
+
base: "h-1.5 w-1.5",
|
|
18
|
+
active: "h-1.5 w-4",
|
|
19
|
+
},
|
|
20
|
+
md: {
|
|
21
|
+
base: "h-2 w-2",
|
|
22
|
+
active: "h-2 w-6",
|
|
23
|
+
},
|
|
24
|
+
lg: {
|
|
25
|
+
base: "h-2.5 w-2.5",
|
|
26
|
+
active: "h-2.5 w-8",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ProgressIndicator = React.forwardRef<HTMLDivElement, ProgressIndicatorProps>(
|
|
31
|
+
({ className, selectedIndex, values, size = "md", onChange, ...props }, ref) => {
|
|
32
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
33
|
+
|
|
34
|
+
const handleKeyDown = React.useCallback(
|
|
35
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
36
|
+
if (!onChange) return;
|
|
37
|
+
if (e.key === "ArrowRight") {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
const next = Math.min(selectedIndex + 1, values.length - 1);
|
|
40
|
+
if (next !== selectedIndex) onChange(next);
|
|
41
|
+
} else if (e.key === "ArrowLeft") {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
const prev = Math.max(selectedIndex - 1, 0);
|
|
44
|
+
if (prev !== selectedIndex) onChange(prev);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
[onChange, selectedIndex, values.length]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const sizes = dotSizeMap[size];
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
ref={ref}
|
|
55
|
+
role="tablist"
|
|
56
|
+
aria-label={props["aria-label"] ?? "Step indicator"}
|
|
57
|
+
tabIndex={0}
|
|
58
|
+
onKeyDown={handleKeyDown}
|
|
59
|
+
className={cn("flex items-center gap-1.5 outline-none focus-visible:outline-none", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{values.map((label, index) => {
|
|
63
|
+
const isActive = index === selectedIndex;
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
key={index}
|
|
67
|
+
type="button"
|
|
68
|
+
role="tab"
|
|
69
|
+
aria-label={label}
|
|
70
|
+
aria-current={isActive ? "step" : undefined}
|
|
71
|
+
aria-selected={isActive}
|
|
72
|
+
tabIndex={-1}
|
|
73
|
+
onClick={() => onChange?.(index)}
|
|
74
|
+
className={cn(
|
|
75
|
+
"rounded-full transition-all duration-pg-normal ease-pg-standard",
|
|
76
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
77
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
78
|
+
isActive
|
|
79
|
+
? cn("bg-primary", sizes.active)
|
|
80
|
+
: cn(
|
|
81
|
+
"bg-muted-foreground/30 hover:bg-muted-foreground/50",
|
|
82
|
+
sizes.base
|
|
83
|
+
)
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
ProgressIndicator.displayName = "ProgressIndicator";
|
|
93
|
+
|
|
94
|
+
export { ProgressIndicator };
|
package/src/progress.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
|
5
|
+
import { Check } from "lucide-react";
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
export type ProgressVariant = "default" | "success" | "warning" | "error";
|
|
9
|
+
export type ProgressSize = "xs" | "sm" | "md" | "lg";
|
|
10
|
+
|
|
11
|
+
export interface ProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
|
|
12
|
+
variant?: ProgressVariant;
|
|
13
|
+
size?: ProgressSize;
|
|
14
|
+
label?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const variantTrack: Record<ProgressVariant, string> = {
|
|
18
|
+
default: "bg-primary",
|
|
19
|
+
success: "bg-green-500",
|
|
20
|
+
warning: "bg-amber-500",
|
|
21
|
+
error: "bg-red-500",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const sizeClasses: Record<ProgressSize, string> = {
|
|
25
|
+
xs: "h-1",
|
|
26
|
+
sm: "h-1.5",
|
|
27
|
+
md: "h-2",
|
|
28
|
+
lg: "h-3",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const Progress = React.forwardRef<
|
|
32
|
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
33
|
+
ProgressProps
|
|
34
|
+
>(({ className, value, variant = "default", size = "md", label, ...props }, ref) => (
|
|
35
|
+
<div className="w-full">
|
|
36
|
+
<ProgressPrimitive.Root
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("relative w-full overflow-hidden rounded-full bg-muted", sizeClasses[size], className)}
|
|
39
|
+
{...props}
|
|
40
|
+
value={value}
|
|
41
|
+
>
|
|
42
|
+
<ProgressPrimitive.Indicator
|
|
43
|
+
className={cn("h-full w-full flex-1 transition-all duration-pg-slow ease-pg-standard", variantTrack[variant])}
|
|
44
|
+
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
|
45
|
+
/>
|
|
46
|
+
</ProgressPrimitive.Root>
|
|
47
|
+
{label && <p className="mt-1 text-xs text-muted-foreground text-right">{value ?? 0}%</p>}
|
|
48
|
+
</div>
|
|
49
|
+
));
|
|
50
|
+
Progress.displayName = "Progress";
|
|
51
|
+
|
|
52
|
+
export type ProgressStepStatus = "complete" | "current" | "upcoming";
|
|
53
|
+
|
|
54
|
+
export interface ProgressTrackerStep {
|
|
55
|
+
label: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
status: ProgressStepStatus;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ProgressTrackerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
61
|
+
steps: ProgressTrackerStep[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ProgressTracker = React.forwardRef<HTMLDivElement, ProgressTrackerProps>(
|
|
65
|
+
({ className, steps, ...props }, ref) => (
|
|
66
|
+
<div ref={ref} className={cn("flex items-start", className)} {...props}>
|
|
67
|
+
{steps.map((step, i) => (
|
|
68
|
+
<React.Fragment key={i}>
|
|
69
|
+
<div className="flex flex-col items-center gap-1.5 min-w-0">
|
|
70
|
+
<div className={cn(
|
|
71
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 text-sm font-semibold transition-colors",
|
|
72
|
+
step.status === "complete" && "bg-primary border-primary text-primary-foreground",
|
|
73
|
+
step.status === "current" && "bg-primary/10 border-primary text-primary",
|
|
74
|
+
step.status === "upcoming" && "bg-card border-border text-muted-foreground"
|
|
75
|
+
)}>
|
|
76
|
+
{step.status === "complete" ? <Check className="size-4" strokeWidth={2.5} /> : i + 1}
|
|
77
|
+
</div>
|
|
78
|
+
<div className="text-center px-1">
|
|
79
|
+
<p className={cn("text-xs font-medium", step.status === "upcoming" ? "text-muted-foreground" : "text-foreground")}>{step.label}</p>
|
|
80
|
+
{step.description && <p className="text-[11px] text-muted-foreground mt-0.5">{step.description}</p>}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
{i < steps.length - 1 && (
|
|
84
|
+
<div className="flex-1 mt-4 mx-1">
|
|
85
|
+
<div className={cn("h-0.5 w-full rounded-full transition-colors", step.status === "complete" ? "bg-primary" : "bg-border")} />
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</React.Fragment>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
ProgressTracker.displayName = "ProgressTracker";
|
|
94
|
+
|
|
95
|
+
export { Progress, ProgressTracker };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
const RadioGroup = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<RadioGroupPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn("flex flex-col gap-2", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
18
|
+
|
|
19
|
+
export type RadioGroupItemProps = React.ComponentPropsWithoutRef<
|
|
20
|
+
typeof RadioGroupPrimitive.Item
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
const RadioGroupItem = React.forwardRef<
|
|
24
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
25
|
+
RadioGroupItemProps
|
|
26
|
+
>(({ className, ...props }, ref) => (
|
|
27
|
+
<RadioGroupPrimitive.Item
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
"size-4 shrink-0 rounded-full border border-border bg-card shadow-sm",
|
|
31
|
+
"transition-colors duration-pg-fast ease-pg-standard",
|
|
32
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35",
|
|
33
|
+
"data-[state=checked]:border-primary",
|
|
34
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
40
|
+
<span className="block size-2 rounded-full bg-primary" />
|
|
41
|
+
</RadioGroupPrimitive.Indicator>
|
|
42
|
+
</RadioGroupPrimitive.Item>
|
|
43
|
+
));
|
|
44
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
|
45
|
+
|
|
46
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Breakpoint type
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export type Breakpoint = "sm" | "md" | "lg" | "xl" | "2xl";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Internal helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** Ordered list — index used for comparison */
|
|
17
|
+
const BREAKPOINT_ORDER: Breakpoint[] = ["sm", "md", "lg", "xl", "2xl"];
|
|
18
|
+
|
|
19
|
+
/** Tailwind min-width values aligned with default config */
|
|
20
|
+
const BREAKPOINT_MIN_WIDTH: Record<Breakpoint, number> = {
|
|
21
|
+
sm: 640,
|
|
22
|
+
md: 768,
|
|
23
|
+
lg: 1024,
|
|
24
|
+
xl: 1280,
|
|
25
|
+
"2xl": 1536,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function bpIndex(bp: Breakpoint): number {
|
|
29
|
+
return BREAKPOINT_ORDER.indexOf(bp);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Show
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface ShowProps extends React.HTMLAttributes<HTMLElement> {
|
|
37
|
+
/** Render children at this breakpoint and above. */
|
|
38
|
+
above?: Breakpoint;
|
|
39
|
+
/** Render children strictly below this breakpoint. */
|
|
40
|
+
below?: Breakpoint;
|
|
41
|
+
/** HTML element to render. Defaults to "div". */
|
|
42
|
+
as?: React.ElementType;
|
|
43
|
+
/** Display value to restore when visible. Defaults to "block". */
|
|
44
|
+
display?: "block" | "flex" | "inline" | "inline-block" | "grid" | "inline-flex";
|
|
45
|
+
children?: React.ReactNode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DISPLAY_ABOVE_CLASS: Record<
|
|
49
|
+
Breakpoint,
|
|
50
|
+
Record<ShowProps["display"] & string, string>
|
|
51
|
+
> = {
|
|
52
|
+
sm: {
|
|
53
|
+
block: "sm:block",
|
|
54
|
+
flex: "sm:flex",
|
|
55
|
+
inline: "sm:inline",
|
|
56
|
+
"inline-block": "sm:inline-block",
|
|
57
|
+
grid: "sm:grid",
|
|
58
|
+
"inline-flex": "sm:inline-flex",
|
|
59
|
+
},
|
|
60
|
+
md: {
|
|
61
|
+
block: "md:block",
|
|
62
|
+
flex: "md:flex",
|
|
63
|
+
inline: "md:inline",
|
|
64
|
+
"inline-block": "md:inline-block",
|
|
65
|
+
grid: "md:grid",
|
|
66
|
+
"inline-flex": "md:inline-flex",
|
|
67
|
+
},
|
|
68
|
+
lg: {
|
|
69
|
+
block: "lg:block",
|
|
70
|
+
flex: "lg:flex",
|
|
71
|
+
inline: "lg:inline",
|
|
72
|
+
"inline-block": "lg:inline-block",
|
|
73
|
+
grid: "lg:grid",
|
|
74
|
+
"inline-flex": "lg:inline-flex",
|
|
75
|
+
},
|
|
76
|
+
xl: {
|
|
77
|
+
block: "xl:block",
|
|
78
|
+
flex: "xl:flex",
|
|
79
|
+
inline: "xl:inline",
|
|
80
|
+
"inline-block": "xl:inline-block",
|
|
81
|
+
grid: "xl:grid",
|
|
82
|
+
"inline-flex": "xl:inline-flex",
|
|
83
|
+
},
|
|
84
|
+
"2xl": {
|
|
85
|
+
block: "2xl:block",
|
|
86
|
+
flex: "2xl:flex",
|
|
87
|
+
inline: "2xl:inline",
|
|
88
|
+
"inline-block": "2xl:inline-block",
|
|
89
|
+
grid: "2xl:grid",
|
|
90
|
+
"inline-flex": "2xl:inline-flex",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Tailwind class to hide at a breakpoint and above */
|
|
95
|
+
const HIDDEN_ABOVE_CLASS: Record<Breakpoint, string> = {
|
|
96
|
+
sm: "sm:hidden",
|
|
97
|
+
md: "md:hidden",
|
|
98
|
+
lg: "lg:hidden",
|
|
99
|
+
xl: "xl:hidden",
|
|
100
|
+
"2xl": "2xl:hidden",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Show = React.forwardRef<HTMLElement, ShowProps>(
|
|
104
|
+
(
|
|
105
|
+
{
|
|
106
|
+
above,
|
|
107
|
+
below,
|
|
108
|
+
as: Tag = "div",
|
|
109
|
+
display = "block",
|
|
110
|
+
className,
|
|
111
|
+
children,
|
|
112
|
+
...props
|
|
113
|
+
},
|
|
114
|
+
ref
|
|
115
|
+
) => {
|
|
116
|
+
let visibilityClass: string;
|
|
117
|
+
|
|
118
|
+
if (above) {
|
|
119
|
+
// hidden by default, become visible at `above` breakpoint
|
|
120
|
+
visibilityClass = cn("hidden", DISPLAY_ABOVE_CLASS[above][display]);
|
|
121
|
+
} else if (below) {
|
|
122
|
+
// visible by default, hidden at `below` breakpoint and above
|
|
123
|
+
visibilityClass = HIDDEN_ABOVE_CLASS[below];
|
|
124
|
+
} else {
|
|
125
|
+
visibilityClass = "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Tag
|
|
130
|
+
ref={ref}
|
|
131
|
+
className={cn(visibilityClass, className)}
|
|
132
|
+
{...props}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</Tag>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
Show.displayName = "Show";
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Hide
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
export interface HideProps extends React.HTMLAttributes<HTMLElement> {
|
|
146
|
+
/** Hide children at this breakpoint and above. */
|
|
147
|
+
above?: Breakpoint;
|
|
148
|
+
/** Hide children strictly below this breakpoint (i.e. visible at/above). */
|
|
149
|
+
below?: Breakpoint;
|
|
150
|
+
/** HTML element to render. Defaults to "div". */
|
|
151
|
+
as?: React.ElementType;
|
|
152
|
+
/** Display value to restore when visible. Defaults to "block". */
|
|
153
|
+
display?: "block" | "flex" | "inline" | "inline-block" | "grid" | "inline-flex";
|
|
154
|
+
children?: React.ReactNode;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const Hide = React.forwardRef<HTMLElement, HideProps>(
|
|
158
|
+
(
|
|
159
|
+
{
|
|
160
|
+
above,
|
|
161
|
+
below,
|
|
162
|
+
as: Tag = "div",
|
|
163
|
+
display = "block",
|
|
164
|
+
className,
|
|
165
|
+
children,
|
|
166
|
+
...props
|
|
167
|
+
},
|
|
168
|
+
ref
|
|
169
|
+
) => {
|
|
170
|
+
let visibilityClass: string;
|
|
171
|
+
|
|
172
|
+
if (above) {
|
|
173
|
+
// visible by default, hidden at `above` breakpoint and above
|
|
174
|
+
visibilityClass = HIDDEN_ABOVE_CLASS[above];
|
|
175
|
+
} else if (below) {
|
|
176
|
+
// hidden by default (below breakpoint), visible at/above breakpoint — inverse of Show above
|
|
177
|
+
visibilityClass = cn("hidden", DISPLAY_ABOVE_CLASS[below][display]);
|
|
178
|
+
} else {
|
|
179
|
+
visibilityClass = "";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Tag
|
|
184
|
+
ref={ref}
|
|
185
|
+
className={cn(visibilityClass, className)}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
{children}
|
|
189
|
+
</Tag>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
Hide.displayName = "Hide";
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// useBreakpoint hook
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
function getActiveBreakpoint(): Breakpoint | null {
|
|
200
|
+
if (typeof window === "undefined") return null;
|
|
201
|
+
// Walk from largest to smallest — first match wins
|
|
202
|
+
for (let i = BREAKPOINT_ORDER.length - 1; i >= 0; i--) {
|
|
203
|
+
const bp = BREAKPOINT_ORDER[i];
|
|
204
|
+
if (window.matchMedia(`(min-width: ${BREAKPOINT_MIN_WIDTH[bp]}px)`).matches) {
|
|
205
|
+
return bp;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface UseBreakpointReturn {
|
|
212
|
+
/** The currently active (highest matching) breakpoint, or null on SSR / xs viewport. */
|
|
213
|
+
breakpoint: Breakpoint | null;
|
|
214
|
+
/** Returns true when the viewport is at or above `bp`. */
|
|
215
|
+
isAbove: (bp: Breakpoint) => boolean;
|
|
216
|
+
/** Returns true when the viewport is strictly below `bp`. */
|
|
217
|
+
isBelow: (bp: Breakpoint) => boolean;
|
|
218
|
+
/** True when no breakpoint is active (viewport < 640 px). */
|
|
219
|
+
isMobile: boolean;
|
|
220
|
+
/** True when active breakpoint is md (768–1023 px). */
|
|
221
|
+
isTablet: boolean;
|
|
222
|
+
/** True when active breakpoint is lg or above (≥ 1024 px). */
|
|
223
|
+
isDesktop: boolean;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function useBreakpoint(): UseBreakpointReturn {
|
|
227
|
+
const [breakpoint, setBreakpoint] = React.useState<Breakpoint | null>(() =>
|
|
228
|
+
getActiveBreakpoint()
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
React.useEffect(() => {
|
|
232
|
+
if (typeof window === "undefined") return;
|
|
233
|
+
|
|
234
|
+
// Build one MediaQueryList per breakpoint and attach listeners
|
|
235
|
+
const queries = BREAKPOINT_ORDER.map((bp) =>
|
|
236
|
+
window.matchMedia(`(min-width: ${BREAKPOINT_MIN_WIDTH[bp]}px)`)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
function handleChange() {
|
|
240
|
+
setBreakpoint(getActiveBreakpoint());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
queries.forEach((mql) => mql.addEventListener("change", handleChange));
|
|
244
|
+
// Sync once on mount (handles SSR hydration mismatch)
|
|
245
|
+
handleChange();
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
queries.forEach((mql) => mql.removeEventListener("change", handleChange));
|
|
249
|
+
};
|
|
250
|
+
}, []);
|
|
251
|
+
|
|
252
|
+
const isAbove = React.useCallback(
|
|
253
|
+
(bp: Breakpoint): boolean => {
|
|
254
|
+
if (breakpoint === null) return false;
|
|
255
|
+
return bpIndex(breakpoint) >= bpIndex(bp);
|
|
256
|
+
},
|
|
257
|
+
[breakpoint]
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const isBelow = React.useCallback(
|
|
261
|
+
(bp: Breakpoint): boolean => {
|
|
262
|
+
if (breakpoint === null) return true;
|
|
263
|
+
return bpIndex(breakpoint) < bpIndex(bp);
|
|
264
|
+
},
|
|
265
|
+
[breakpoint]
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
breakpoint,
|
|
270
|
+
isAbove,
|
|
271
|
+
isBelow,
|
|
272
|
+
isMobile: breakpoint === null,
|
|
273
|
+
isTablet: breakpoint === "md",
|
|
274
|
+
isDesktop: breakpoint !== null && bpIndex(breakpoint) >= bpIndex("lg"),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
const ScrollArea = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
10
|
+
>(({ className, children, ...props }, ref) => (
|
|
11
|
+
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
|
12
|
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
|
13
|
+
<ScrollBar />
|
|
14
|
+
<ScrollAreaPrimitive.Corner />
|
|
15
|
+
</ScrollAreaPrimitive.Root>
|
|
16
|
+
));
|
|
17
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
18
|
+
|
|
19
|
+
const ScrollBar = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
22
|
+
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
23
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
24
|
+
ref={ref}
|
|
25
|
+
orientation={orientation}
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex touch-none select-none transition-colors",
|
|
28
|
+
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-px",
|
|
29
|
+
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-px",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border hover:bg-muted-foreground/30" />
|
|
35
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
36
|
+
));
|
|
37
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
38
|
+
|
|
39
|
+
export { ScrollArea, ScrollBar };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Info, CheckCircle2, AlertTriangle, XCircle, Sparkles } from "lucide-react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export type SectionMessageVariant = "info" | "success" | "warning" | "error" | "discovery";
|
|
6
|
+
|
|
7
|
+
export interface SectionMessageProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
variant?: SectionMessageVariant;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const variantConfig: Record<
|
|
12
|
+
SectionMessageVariant,
|
|
13
|
+
{ border: string; bg: string; icon: React.ElementType; iconColor: string }
|
|
14
|
+
> = {
|
|
15
|
+
info: {
|
|
16
|
+
border: "border-l-blue-500",
|
|
17
|
+
bg: "bg-blue-50/60 dark:bg-blue-950/20",
|
|
18
|
+
icon: Info,
|
|
19
|
+
iconColor: "text-blue-500 dark:text-blue-400",
|
|
20
|
+
},
|
|
21
|
+
success: {
|
|
22
|
+
border: "border-l-green-500",
|
|
23
|
+
bg: "bg-green-50/60 dark:bg-green-950/20",
|
|
24
|
+
icon: CheckCircle2,
|
|
25
|
+
iconColor: "text-green-500 dark:text-green-400",
|
|
26
|
+
},
|
|
27
|
+
warning: {
|
|
28
|
+
border: "border-l-amber-500",
|
|
29
|
+
bg: "bg-amber-50/60 dark:bg-amber-950/20",
|
|
30
|
+
icon: AlertTriangle,
|
|
31
|
+
iconColor: "text-amber-500 dark:text-amber-400",
|
|
32
|
+
},
|
|
33
|
+
error: {
|
|
34
|
+
border: "border-l-red-500",
|
|
35
|
+
bg: "bg-red-50/60 dark:bg-red-950/20",
|
|
36
|
+
icon: XCircle,
|
|
37
|
+
iconColor: "text-red-500 dark:text-red-400",
|
|
38
|
+
},
|
|
39
|
+
discovery: {
|
|
40
|
+
border: "border-l-purple-500",
|
|
41
|
+
bg: "bg-purple-50/60 dark:bg-purple-950/20",
|
|
42
|
+
icon: Sparkles,
|
|
43
|
+
iconColor: "text-purple-500 dark:text-purple-400",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const SectionMessageVariantContext = React.createContext<SectionMessageVariant>("info");
|
|
48
|
+
|
|
49
|
+
const SectionMessage = React.forwardRef<HTMLDivElement, SectionMessageProps>(
|
|
50
|
+
({ className, variant = "info", children, ...props }, ref) => {
|
|
51
|
+
const config = variantConfig[variant];
|
|
52
|
+
const Icon = config.icon;
|
|
53
|
+
return (
|
|
54
|
+
<SectionMessageVariantContext.Provider value={variant}>
|
|
55
|
+
<div
|
|
56
|
+
ref={ref}
|
|
57
|
+
role="region"
|
|
58
|
+
className={cn(
|
|
59
|
+
"flex w-full gap-4 rounded-xl border border-l-4 border-border p-5 shadow-sm transition-colors duration-pg-fast ease-pg-standard",
|
|
60
|
+
config.border,
|
|
61
|
+
config.bg,
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<Icon
|
|
67
|
+
className={cn("mt-0.5 size-5 shrink-0", config.iconColor)}
|
|
68
|
+
aria-hidden
|
|
69
|
+
/>
|
|
70
|
+
<div className="flex-1 min-w-0">{children}</div>
|
|
71
|
+
</div>
|
|
72
|
+
</SectionMessageVariantContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
SectionMessage.displayName = "SectionMessage";
|
|
77
|
+
|
|
78
|
+
const SectionMessageTitle = React.forwardRef<
|
|
79
|
+
HTMLHeadingElement,
|
|
80
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
81
|
+
>(({ className, ...props }, ref) => (
|
|
82
|
+
<h4
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={cn("text-sm font-semibold text-foreground mb-1", className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
));
|
|
88
|
+
SectionMessageTitle.displayName = "SectionMessageTitle";
|
|
89
|
+
|
|
90
|
+
const SectionMessageContent = React.forwardRef<
|
|
91
|
+
HTMLDivElement,
|
|
92
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
93
|
+
>(({ className, ...props }, ref) => (
|
|
94
|
+
<div
|
|
95
|
+
ref={ref}
|
|
96
|
+
className={cn("text-sm text-muted-foreground leading-relaxed", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
));
|
|
100
|
+
SectionMessageContent.displayName = "SectionMessageContent";
|
|
101
|
+
|
|
102
|
+
const SectionMessageActions = React.forwardRef<
|
|
103
|
+
HTMLDivElement,
|
|
104
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
105
|
+
>(({ className, ...props }, ref) => (
|
|
106
|
+
<div
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={cn("flex flex-wrap gap-2 mt-3", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
));
|
|
112
|
+
SectionMessageActions.displayName = "SectionMessageActions";
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
SectionMessage,
|
|
116
|
+
SectionMessageTitle,
|
|
117
|
+
SectionMessageContent,
|
|
118
|
+
SectionMessageActions,
|
|
119
|
+
};
|