@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
@@ -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 };
@@ -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
+ };