@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.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +3 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +992 -0
- package/dist/index.js +4433 -0
- package/dist/index.js.map +1 -0
- package/dist/tokens.css +265 -0
- package/dist/tokens.d.ts +189 -0
- package/dist/tokens.js +193 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +80 -0
- package/src/components/__tests__/data.test.tsx +180 -0
- package/src/components/__tests__/disclosure.test.tsx +198 -0
- package/src/components/__tests__/display.test.tsx +262 -0
- package/src/components/__tests__/feedback.test.tsx +138 -0
- package/src/components/__tests__/form.test.tsx +297 -0
- package/src/components/__tests__/layout.test.tsx +388 -0
- package/src/components/__tests__/overlay.test.tsx +90 -0
- package/src/components/__tests__/primitives.test.tsx +248 -0
- package/src/components/accordion.tsx +68 -0
- package/src/components/action-panel.tsx +112 -0
- package/src/components/activity-feed.tsx +69 -0
- package/src/components/alert-dialog.tsx +132 -0
- package/src/components/alert.tsx +44 -0
- package/src/components/aspect-ratio.tsx +19 -0
- package/src/components/avatar-stack.tsx +70 -0
- package/src/components/avatar.tsx +103 -0
- package/src/components/badge.tsx +33 -0
- package/src/components/breadcrumb.tsx +76 -0
- package/src/components/button-group.tsx +45 -0
- package/src/components/button.tsx +60 -0
- package/src/components/calendar.tsx +61 -0
- package/src/components/card-heading.tsx +60 -0
- package/src/components/card.tsx +120 -0
- package/src/components/carousel.tsx +222 -0
- package/src/components/chart.tsx +77 -0
- package/src/components/checkbox.tsx +35 -0
- package/src/components/collapsible.tsx +9 -0
- package/src/components/color-input.tsx +90 -0
- package/src/components/color-picker.tsx +111 -0
- package/src/components/combobox.tsx +207 -0
- package/src/components/command-palette.tsx +137 -0
- package/src/components/command.tsx +248 -0
- package/src/components/context-menu.tsx +201 -0
- package/src/components/data-table.tsx +151 -0
- package/src/components/date-picker.tsx +56 -0
- package/src/components/description-list.tsx +70 -0
- package/src/components/dialog.tsx +97 -0
- package/src/components/drawer.tsx +90 -0
- package/src/components/dropdown-menu.tsx +202 -0
- package/src/components/empty-state.tsx +43 -0
- package/src/components/field.tsx +45 -0
- package/src/components/grid-list.tsx +159 -0
- package/src/components/hover-card.tsx +34 -0
- package/src/components/input-group.tsx +134 -0
- package/src/components/input-number.tsx +142 -0
- package/src/components/input-otp.tsx +68 -0
- package/src/components/input.tsx +23 -0
- package/src/components/kbd.tsx +18 -0
- package/src/components/label.tsx +23 -0
- package/src/components/media-object.tsx +49 -0
- package/src/components/menubar.tsx +235 -0
- package/src/components/native-select.tsx +44 -0
- package/src/components/navigation-menu.tsx +158 -0
- package/src/components/notification.tsx +109 -0
- package/src/components/page-heading.tsx +125 -0
- package/src/components/pagination.tsx +89 -0
- package/src/components/popover.tsx +35 -0
- package/src/components/progress.tsx +23 -0
- package/src/components/property-row.tsx +56 -0
- package/src/components/radio-card.tsx +105 -0
- package/src/components/radio-group.tsx +38 -0
- package/src/components/resizable.tsx +74 -0
- package/src/components/scroll-area.tsx +42 -0
- package/src/components/section-heading.tsx +64 -0
- package/src/components/select.tsx +130 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/sheet.tsx +117 -0
- package/src/components/sidebar.tsx +636 -0
- package/src/components/skeleton.tsx +14 -0
- package/src/components/slider.tsx +24 -0
- package/src/components/sonner.tsx +34 -0
- package/src/components/spinner.tsx +37 -0
- package/src/components/stacked-list.tsx +64 -0
- package/src/components/stat-card.tsx +54 -0
- package/src/components/steps.tsx +179 -0
- package/src/components/switch.tsx +33 -0
- package/src/components/table.tsx +101 -0
- package/src/components/tabs.tsx +55 -0
- package/src/components/textarea.tsx +21 -0
- package/src/components/toggle-group.tsx +49 -0
- package/src/components/toggle.tsx +66 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/icons.ts +158 -0
- package/src/index.ts +84 -0
- package/src/lib/utils.ts +6 -0
- 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 };
|