@hilum/ui 0.1.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.js +3 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.d.ts +992 -0
  7. package/dist/index.js +4433 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tokens.css +265 -0
  10. package/dist/tokens.d.ts +189 -0
  11. package/dist/tokens.js +193 -0
  12. package/dist/tokens.js.map +1 -0
  13. package/package.json +80 -0
  14. package/src/components/__tests__/data.test.tsx +180 -0
  15. package/src/components/__tests__/disclosure.test.tsx +198 -0
  16. package/src/components/__tests__/display.test.tsx +262 -0
  17. package/src/components/__tests__/feedback.test.tsx +138 -0
  18. package/src/components/__tests__/form.test.tsx +297 -0
  19. package/src/components/__tests__/layout.test.tsx +388 -0
  20. package/src/components/__tests__/overlay.test.tsx +90 -0
  21. package/src/components/__tests__/primitives.test.tsx +248 -0
  22. package/src/components/accordion.tsx +68 -0
  23. package/src/components/action-panel.tsx +112 -0
  24. package/src/components/activity-feed.tsx +69 -0
  25. package/src/components/alert-dialog.tsx +132 -0
  26. package/src/components/alert.tsx +44 -0
  27. package/src/components/aspect-ratio.tsx +19 -0
  28. package/src/components/avatar-stack.tsx +70 -0
  29. package/src/components/avatar.tsx +103 -0
  30. package/src/components/badge.tsx +33 -0
  31. package/src/components/breadcrumb.tsx +76 -0
  32. package/src/components/button-group.tsx +45 -0
  33. package/src/components/button.tsx +60 -0
  34. package/src/components/calendar.tsx +61 -0
  35. package/src/components/card-heading.tsx +60 -0
  36. package/src/components/card.tsx +120 -0
  37. package/src/components/carousel.tsx +222 -0
  38. package/src/components/chart.tsx +77 -0
  39. package/src/components/checkbox.tsx +35 -0
  40. package/src/components/collapsible.tsx +9 -0
  41. package/src/components/color-input.tsx +90 -0
  42. package/src/components/color-picker.tsx +111 -0
  43. package/src/components/combobox.tsx +207 -0
  44. package/src/components/command-palette.tsx +137 -0
  45. package/src/components/command.tsx +248 -0
  46. package/src/components/context-menu.tsx +201 -0
  47. package/src/components/data-table.tsx +151 -0
  48. package/src/components/date-picker.tsx +56 -0
  49. package/src/components/description-list.tsx +70 -0
  50. package/src/components/dialog.tsx +97 -0
  51. package/src/components/drawer.tsx +90 -0
  52. package/src/components/dropdown-menu.tsx +202 -0
  53. package/src/components/empty-state.tsx +43 -0
  54. package/src/components/field.tsx +45 -0
  55. package/src/components/grid-list.tsx +159 -0
  56. package/src/components/hover-card.tsx +34 -0
  57. package/src/components/input-group.tsx +134 -0
  58. package/src/components/input-number.tsx +142 -0
  59. package/src/components/input-otp.tsx +68 -0
  60. package/src/components/input.tsx +23 -0
  61. package/src/components/kbd.tsx +18 -0
  62. package/src/components/label.tsx +23 -0
  63. package/src/components/media-object.tsx +49 -0
  64. package/src/components/menubar.tsx +235 -0
  65. package/src/components/native-select.tsx +44 -0
  66. package/src/components/navigation-menu.tsx +158 -0
  67. package/src/components/notification.tsx +109 -0
  68. package/src/components/page-heading.tsx +125 -0
  69. package/src/components/pagination.tsx +89 -0
  70. package/src/components/popover.tsx +35 -0
  71. package/src/components/progress.tsx +23 -0
  72. package/src/components/property-row.tsx +56 -0
  73. package/src/components/radio-card.tsx +105 -0
  74. package/src/components/radio-group.tsx +38 -0
  75. package/src/components/resizable.tsx +74 -0
  76. package/src/components/scroll-area.tsx +42 -0
  77. package/src/components/section-heading.tsx +64 -0
  78. package/src/components/select.tsx +130 -0
  79. package/src/components/separator.tsx +25 -0
  80. package/src/components/sheet.tsx +117 -0
  81. package/src/components/sidebar.tsx +636 -0
  82. package/src/components/skeleton.tsx +14 -0
  83. package/src/components/slider.tsx +24 -0
  84. package/src/components/sonner.tsx +34 -0
  85. package/src/components/spinner.tsx +37 -0
  86. package/src/components/stacked-list.tsx +64 -0
  87. package/src/components/stat-card.tsx +54 -0
  88. package/src/components/steps.tsx +179 -0
  89. package/src/components/switch.tsx +33 -0
  90. package/src/components/table.tsx +101 -0
  91. package/src/components/tabs.tsx +55 -0
  92. package/src/components/textarea.tsx +21 -0
  93. package/src/components/toggle-group.tsx +49 -0
  94. package/src/components/toggle.tsx +66 -0
  95. package/src/components/tooltip.tsx +43 -0
  96. package/src/icons.ts +158 -0
  97. package/src/index.ts +84 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/tokens/tokens.ts +194 -0
@@ -0,0 +1,202 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { DropdownMenu } from "radix-ui";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const DropdownMenuRoot = DropdownMenu.Root;
9
+ const DropdownMenuTrigger = DropdownMenu.Trigger;
10
+ const DropdownMenuGroup = DropdownMenu.Group;
11
+ const DropdownMenuPortal = DropdownMenu.Portal;
12
+ const DropdownMenuSub = DropdownMenu.Sub;
13
+ const DropdownMenuRadioGroup = DropdownMenu.RadioGroup;
14
+
15
+ const DropdownMenuContent = React.forwardRef<
16
+ React.ComponentRef<typeof DropdownMenu.Content>,
17
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.Content>
18
+ >(({ className, sideOffset = 4, ...props }, ref) => (
19
+ <DropdownMenu.Portal>
20
+ <DropdownMenu.Content
21
+ ref={ref}
22
+ sideOffset={sideOffset}
23
+ className={cn(
24
+ "z-50 min-w-[8rem] bg-white rounded-xl shadow-natural p-1",
25
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
26
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
27
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
28
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
29
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ </DropdownMenu.Portal>
35
+ ));
36
+ DropdownMenuContent.displayName = "DropdownMenuContent";
37
+
38
+ const DropdownMenuItem = React.forwardRef<
39
+ React.ComponentRef<typeof DropdownMenu.Item>,
40
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.Item> & {
41
+ inset?: boolean;
42
+ destructive?: boolean;
43
+ }
44
+ >(({ className, inset, destructive, ...props }, ref) => (
45
+ <DropdownMenu.Item
46
+ ref={ref}
47
+ className={cn(
48
+ "relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-1.5",
49
+ "body outline-none transition-colors",
50
+ destructive
51
+ ? "text-red-600 focus:bg-red-50 focus:text-red-700"
52
+ : "text-taupe-700 focus:bg-taupe-50 focus:text-taupe-900",
53
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
54
+ inset && "pl-8",
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
59
+ ));
60
+ DropdownMenuItem.displayName = "DropdownMenuItem";
61
+
62
+ const DropdownMenuCheckboxItem = React.forwardRef<
63
+ React.ComponentRef<typeof DropdownMenu.CheckboxItem>,
64
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.CheckboxItem>
65
+ >(({ className, children, checked, ...props }, ref) => (
66
+ <DropdownMenu.CheckboxItem
67
+ ref={ref}
68
+ className={cn(
69
+ "relative flex cursor-default select-none items-center gap-2 rounded-md pl-8 pr-2.5 py-1.5",
70
+ "body text-taupe-700 outline-none transition-colors",
71
+ "focus:bg-taupe-50 focus:text-taupe-900",
72
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
73
+ className,
74
+ )}
75
+ checked={checked}
76
+ {...props}
77
+ >
78
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
79
+ <DropdownMenu.ItemIndicator>
80
+ <Check size={13} className="text-taupe-900" />
81
+ </DropdownMenu.ItemIndicator>
82
+ </span>
83
+ {children}
84
+ </DropdownMenu.CheckboxItem>
85
+ ));
86
+ DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
87
+
88
+ const DropdownMenuRadioItem = React.forwardRef<
89
+ React.ComponentRef<typeof DropdownMenu.RadioItem>,
90
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.RadioItem>
91
+ >(({ className, children, ...props }, ref) => (
92
+ <DropdownMenu.RadioItem
93
+ ref={ref}
94
+ className={cn(
95
+ "relative flex cursor-default select-none items-center gap-2 rounded-md pl-8 pr-2.5 py-1.5",
96
+ "body text-taupe-700 outline-none transition-colors",
97
+ "focus:bg-taupe-50 focus:text-taupe-900",
98
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
99
+ className,
100
+ )}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
104
+ <DropdownMenu.ItemIndicator>
105
+ <Circle size={8} className="fill-taupe-900 text-taupe-900" />
106
+ </DropdownMenu.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </DropdownMenu.RadioItem>
110
+ ));
111
+ DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
112
+
113
+ const DropdownMenuLabel = React.forwardRef<
114
+ React.ComponentRef<typeof DropdownMenu.Label>,
115
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.Label> & {
116
+ inset?: boolean;
117
+ }
118
+ >(({ className, inset, ...props }, ref) => (
119
+ <DropdownMenu.Label
120
+ ref={ref}
121
+ className={cn("px-2.5 py-1 label text-taupe-400", inset && "pl-8", className)}
122
+ {...props}
123
+ />
124
+ ));
125
+ DropdownMenuLabel.displayName = "DropdownMenuLabel";
126
+
127
+ const DropdownMenuSeparator = React.forwardRef<
128
+ React.ComponentRef<typeof DropdownMenu.Separator>,
129
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.Separator>
130
+ >(({ className, ...props }, ref) => (
131
+ <DropdownMenu.Separator
132
+ ref={ref}
133
+ className={cn("mx-1 my-1 h-px bg-taupe-100", className)}
134
+ {...props}
135
+ />
136
+ ));
137
+ DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
138
+
139
+ const DropdownMenuSubTrigger = React.forwardRef<
140
+ React.ComponentRef<typeof DropdownMenu.SubTrigger>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.SubTrigger> & {
142
+ inset?: boolean;
143
+ }
144
+ >(({ className, inset, children, ...props }, ref) => (
145
+ <DropdownMenu.SubTrigger
146
+ ref={ref}
147
+ className={cn(
148
+ "relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-1.5",
149
+ "body text-taupe-700 outline-none transition-colors",
150
+ "focus:bg-taupe-50 focus:text-taupe-900 data-[state=open]:bg-taupe-50",
151
+ inset && "pl-8",
152
+ className,
153
+ )}
154
+ {...props}
155
+ >
156
+ {children}
157
+ <ChevronRight size={14} className="ml-auto text-taupe-400" />
158
+ </DropdownMenu.SubTrigger>
159
+ ));
160
+ DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger";
161
+
162
+ const DropdownMenuSubContent = React.forwardRef<
163
+ React.ComponentRef<typeof DropdownMenu.SubContent>,
164
+ React.ComponentPropsWithoutRef<typeof DropdownMenu.SubContent>
165
+ >(({ className, ...props }, ref) => (
166
+ <DropdownMenu.SubContent
167
+ ref={ref}
168
+ className={cn(
169
+ "z-50 min-w-[8rem] bg-white rounded-xl border border-taupe-100 shadow-natural p-1",
170
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
171
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
172
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
173
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
174
+ className,
175
+ )}
176
+ {...props}
177
+ />
178
+ ));
179
+ DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
180
+
181
+ function DropdownMenuShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
182
+ return <span className={cn("ml-auto caption text-taupe-400", className)} {...props} />;
183
+ }
184
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
185
+
186
+ export {
187
+ DropdownMenuRoot as DropdownMenu,
188
+ DropdownMenuTrigger,
189
+ DropdownMenuContent,
190
+ DropdownMenuItem,
191
+ DropdownMenuCheckboxItem,
192
+ DropdownMenuRadioItem,
193
+ DropdownMenuRadioGroup,
194
+ DropdownMenuLabel,
195
+ DropdownMenuSeparator,
196
+ DropdownMenuSub,
197
+ DropdownMenuSubTrigger,
198
+ DropdownMenuSubContent,
199
+ DropdownMenuShortcut,
200
+ DropdownMenuGroup,
201
+ DropdownMenuPortal,
202
+ };
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { Button } from "./button";
4
+
5
+ interface EmptyStateProps {
6
+ icon?: React.ReactNode;
7
+ title: string;
8
+ description?: string;
9
+ action?: {
10
+ label: string;
11
+ href?: string;
12
+ onClick?: () => void;
13
+ };
14
+ className?: string;
15
+ }
16
+
17
+ function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
18
+ return (
19
+ <div className={cn("flex flex-col items-center gap-4 px-6 py-14 text-center", className)}>
20
+ {icon && (
21
+ <div className="flex size-12 items-center justify-center rounded-xl bg-brand-secondary/20 text-taupe-700">
22
+ {icon}
23
+ </div>
24
+ )}
25
+ <div className="flex flex-col gap-1.5">
26
+ <p className="body font-semibold text-taupe-900">{title}</p>
27
+ {description && <p className="body max-w-xs text-taupe-400">{description}</p>}
28
+ </div>
29
+ {action &&
30
+ (action.href ? (
31
+ <Button size="sm" asChild>
32
+ <a href={action.href}>{action.label}</a>
33
+ </Button>
34
+ ) : (
35
+ <Button size="sm" onClick={action.onClick}>
36
+ {action.label}
37
+ </Button>
38
+ ))}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export { EmptyState };
@@ -0,0 +1,45 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { Label } from "./label";
4
+
5
+ interface FieldProps {
6
+ label: string;
7
+ htmlFor?: string;
8
+ hint?: string;
9
+ error?: string;
10
+ required?: boolean;
11
+ cornerHint?: string;
12
+ className?: string;
13
+ children: React.ReactNode;
14
+ }
15
+
16
+ function Field({
17
+ label,
18
+ htmlFor,
19
+ hint,
20
+ error,
21
+ required,
22
+ cornerHint,
23
+ className,
24
+ children,
25
+ }: FieldProps) {
26
+ return (
27
+ <div className={cn("flex flex-col gap-1.5", className)}>
28
+ <div className="flex items-center justify-between gap-2">
29
+ <Label htmlFor={htmlFor}>
30
+ {label}
31
+ {required && <span className="ml-0.5 text-red-600">*</span>}
32
+ </Label>
33
+ {cornerHint && <span className="caption text-taupe-400">{cornerHint}</span>}
34
+ </div>
35
+ {children}
36
+ {error ? (
37
+ <p className="caption text-red-600">{error}</p>
38
+ ) : hint ? (
39
+ <p className="caption text-taupe-400">{hint}</p>
40
+ ) : null}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export { Field };
@@ -0,0 +1,159 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /* ------------------------------------------------------------------ */
5
+ /* Generic grid container */
6
+ /* ------------------------------------------------------------------ */
7
+
8
+ interface GridListProps {
9
+ children: React.ReactNode;
10
+ columns?: 2 | 3 | 4;
11
+ gap?: "sm" | "md" | "lg";
12
+ className?: string;
13
+ }
14
+
15
+ const colMap = {
16
+ 2: "grid-cols-1 sm:grid-cols-2",
17
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
18
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
19
+ };
20
+
21
+ const gapMap = {
22
+ sm: "gap-3",
23
+ md: "gap-4",
24
+ lg: "gap-6",
25
+ };
26
+
27
+ function GridList({ children, columns = 3, gap = "md", className }: GridListProps) {
28
+ return (
29
+ <ul role="list" className={cn("grid", colMap[columns], gapMap[gap], className)}>
30
+ {children}
31
+ </ul>
32
+ );
33
+ }
34
+
35
+ /* ------------------------------------------------------------------ */
36
+ /* Simple card item */
37
+ /* ------------------------------------------------------------------ */
38
+
39
+ interface GridListCardProps {
40
+ title: string;
41
+ description?: string;
42
+ meta?: string;
43
+ href?: string;
44
+ accent?: React.ReactNode;
45
+ trailing?: React.ReactNode;
46
+ className?: string;
47
+ }
48
+
49
+ function GridListCard({
50
+ title,
51
+ description,
52
+ meta,
53
+ href,
54
+ accent,
55
+ trailing,
56
+ className,
57
+ }: GridListCardProps) {
58
+ const inner = (
59
+ <div
60
+ className={cn(
61
+ "flex h-full flex-col gap-1 rounded-xl border border-taupe-100 bg-white p-4 shadow-natural transition-shadow",
62
+ href && "hover:shadow-elevated",
63
+ className,
64
+ )}
65
+ >
66
+ {accent && <div className="mb-2">{accent}</div>}
67
+ <p className="body font-semibold text-taupe-900">{title}</p>
68
+ {description && <p className="caption text-taupe-400">{description}</p>}
69
+ {(meta || trailing) && (
70
+ <div className="mt-auto flex items-center justify-between pt-3">
71
+ {meta && <span className="caption text-taupe-400">{meta}</span>}
72
+ {trailing && <span className="caption text-taupe-400">{trailing}</span>}
73
+ </div>
74
+ )}
75
+ </div>
76
+ );
77
+
78
+ return (
79
+ <li>
80
+ {href ? (
81
+ <a
82
+ href={href}
83
+ className="block h-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-taupe-400/40 rounded-xl"
84
+ >
85
+ {inner}
86
+ </a>
87
+ ) : (
88
+ inner
89
+ )}
90
+ </li>
91
+ );
92
+ }
93
+
94
+ /* ------------------------------------------------------------------ */
95
+ /* Horizontal accent card (colored left strip + title + meta) */
96
+ /* ------------------------------------------------------------------ */
97
+
98
+ interface GridListAccentCardProps {
99
+ title: string;
100
+ meta?: string;
101
+ href?: string;
102
+ accentClass?: string;
103
+ initials?: string;
104
+ trailing?: React.ReactNode;
105
+ className?: string;
106
+ }
107
+
108
+ function GridListAccentCard({
109
+ title,
110
+ meta,
111
+ href,
112
+ accentClass = "bg-taupe-900",
113
+ initials,
114
+ trailing,
115
+ className,
116
+ }: GridListAccentCardProps) {
117
+ const inner = (
118
+ <div
119
+ className={cn(
120
+ "flex overflow-hidden rounded-xl border border-taupe-100 bg-white shadow-natural transition-shadow",
121
+ href && "hover:shadow-elevated",
122
+ className,
123
+ )}
124
+ >
125
+ <div
126
+ className={cn(
127
+ "flex w-14 shrink-0 items-center justify-center body font-semibold text-white",
128
+ accentClass,
129
+ )}
130
+ >
131
+ {initials}
132
+ </div>
133
+ <div className="flex flex-1 items-center justify-between truncate border-l border-taupe-100 px-4 py-3">
134
+ <div className="min-w-0">
135
+ <p className="body font-semibold text-taupe-900 truncate">{title}</p>
136
+ {meta && <p className="caption text-taupe-400">{meta}</p>}
137
+ </div>
138
+ {trailing && <div className="shrink-0 pl-2">{trailing}</div>}
139
+ </div>
140
+ </div>
141
+ );
142
+
143
+ return (
144
+ <li>
145
+ {href ? (
146
+ <a
147
+ href={href}
148
+ className="block focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-taupe-400/40 rounded-xl"
149
+ >
150
+ {inner}
151
+ </a>
152
+ ) : (
153
+ inner
154
+ )}
155
+ </li>
156
+ );
157
+ }
158
+
159
+ export { GridList, GridListCard, GridListAccentCard };
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { HoverCard } from "radix-ui";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const HoverCardRoot = HoverCard.Root;
8
+ const HoverCardTrigger = HoverCard.Trigger;
9
+
10
+ const HoverCardContent = React.forwardRef<
11
+ React.ComponentRef<typeof HoverCard.Content>,
12
+ React.ComponentPropsWithoutRef<typeof HoverCard.Content>
13
+ >(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
14
+ <HoverCard.Portal>
15
+ <HoverCard.Content
16
+ ref={ref}
17
+ align={align}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-50 w-64 rounded-xl border border-taupe-100 bg-white p-4 shadow-elevated outline-none",
21
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
22
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
23
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
24
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
25
+ "data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2",
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ </HoverCard.Portal>
31
+ ));
32
+ HoverCardContent.displayName = "HoverCardContent";
33
+
34
+ export { HoverCardRoot as HoverCard, HoverCardTrigger, HoverCardContent };
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+ import { AlertCircle } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ interface InputGroupProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
+ leadingAddon?: React.ReactNode;
7
+ trailingAddon?: React.ReactNode;
8
+ leadingIcon?: React.ReactNode;
9
+ trailingIcon?: React.ReactNode;
10
+ trailingButton?: React.ReactNode;
11
+ error?: boolean;
12
+ pill?: boolean;
13
+ wrapperClassName?: string;
14
+ }
15
+
16
+ const InputGroup = React.forwardRef<HTMLInputElement, InputGroupProps>(
17
+ (
18
+ {
19
+ leadingAddon,
20
+ trailingAddon,
21
+ leadingIcon,
22
+ trailingIcon,
23
+ trailingButton,
24
+ error,
25
+ pill,
26
+ className,
27
+ wrapperClassName,
28
+ ...props
29
+ },
30
+ ref,
31
+ ) => {
32
+ const radius = pill ? "rounded-full" : "rounded-lg";
33
+ const hasLeading = leadingAddon || leadingIcon;
34
+ const hasTrailing = trailingAddon || trailingIcon || trailingButton || error;
35
+
36
+ const baseInput = cn(
37
+ "flex h-9 w-full border bg-white body text-taupe-900 placeholder:text-taupe-400",
38
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:disabled:cursor-not-allowed disabled:opacity-50",
39
+ error
40
+ ? "border-red-400 focus-visible:ring-red-200 focus-visible:border-red-500"
41
+ : "border-taupe-200 focus-visible:ring-brand-primary/20 focus-visible:border-brand-primary",
42
+ );
43
+
44
+ if (!hasLeading && !hasTrailing) {
45
+ return <input ref={ref} className={cn(baseInput, radius, "px-3", className)} {...props} />;
46
+ }
47
+
48
+ return (
49
+ <div className={cn("flex", wrapperClassName)}>
50
+ {/* Leading addon (text) */}
51
+ {leadingAddon && (
52
+ <span
53
+ className={cn(
54
+ "inline-flex items-center border border-r-0 bg-taupe-50 px-3 body text-taupe-500 select-none",
55
+ error ? "border-red-400" : "border-taupe-200",
56
+ pill ? "rounded-l-full" : "rounded-l-lg",
57
+ )}
58
+ >
59
+ {leadingAddon}
60
+ </span>
61
+ )}
62
+
63
+ {/* Relative wrapper for icon-only inputs */}
64
+ <div className="relative flex-1">
65
+ {leadingIcon && (
66
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-taupe-400">
67
+ {leadingIcon}
68
+ </div>
69
+ )}
70
+
71
+ <input
72
+ ref={ref}
73
+ className={cn(
74
+ baseInput,
75
+ // Border radius
76
+ leadingAddon
77
+ ? trailingAddon || trailingButton
78
+ ? "rounded-none px-3"
79
+ : pill
80
+ ? "rounded-none rounded-r-full px-3"
81
+ : "rounded-none rounded-r-lg px-3"
82
+ : leadingIcon
83
+ ? trailingAddon
84
+ ? pill
85
+ ? "rounded-l-full rounded-r-none pl-9 pr-3"
86
+ : "rounded-l-lg rounded-r-none pl-9 pr-3"
87
+ : trailingIcon || error
88
+ ? cn(radius, "pl-9 pr-9")
89
+ : cn(radius, "pl-9 pr-3")
90
+ : trailingAddon
91
+ ? pill
92
+ ? "rounded-l-full rounded-r-none px-3"
93
+ : "rounded-l-lg rounded-r-none px-3"
94
+ : trailingIcon || error || trailingButton
95
+ ? cn(radius, "pl-3 pr-9")
96
+ : cn(radius, "px-3"),
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+
102
+ {(trailingIcon || error) && (
103
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
104
+ {error ? (
105
+ <AlertCircle size={15} className="text-red-500" />
106
+ ) : (
107
+ <span className="text-taupe-400">{trailingIcon}</span>
108
+ )}
109
+ </div>
110
+ )}
111
+ </div>
112
+
113
+ {/* Trailing addon (text) */}
114
+ {trailingAddon && (
115
+ <span
116
+ className={cn(
117
+ "inline-flex items-center border border-l-0 bg-taupe-50 px-3 body text-taupe-500 select-none",
118
+ error ? "border-red-400" : "border-taupe-200",
119
+ pill ? "rounded-r-full" : "rounded-r-lg",
120
+ )}
121
+ >
122
+ {trailingAddon}
123
+ </span>
124
+ )}
125
+
126
+ {/* Trailing button */}
127
+ {trailingButton && <div className="flex">{trailingButton}</div>}
128
+ </div>
129
+ );
130
+ },
131
+ );
132
+ InputGroup.displayName = "InputGroup";
133
+
134
+ export { InputGroup };