@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,248 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import { Badge } from "../badge";
|
|
5
|
+
import { Input } from "../input";
|
|
6
|
+
import { Textarea } from "../textarea";
|
|
7
|
+
import { Label } from "../label";
|
|
8
|
+
import { Spinner } from "../spinner";
|
|
9
|
+
import { Skeleton } from "../skeleton";
|
|
10
|
+
import { Separator } from "../separator";
|
|
11
|
+
import { Kbd } from "../kbd";
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
/* Button */
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
|
|
17
|
+
describe("Button", () => {
|
|
18
|
+
it("renders as a button element by default", () => {
|
|
19
|
+
render(<Button>Click me</Button>);
|
|
20
|
+
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("forwards data-slot attribute", () => {
|
|
24
|
+
render(<Button>Test</Button>);
|
|
25
|
+
expect(screen.getByRole("button")).toHaveAttribute("data-slot", "button");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("renders all variants without error", () => {
|
|
29
|
+
const variants = [
|
|
30
|
+
"default",
|
|
31
|
+
"destructive",
|
|
32
|
+
"outline",
|
|
33
|
+
"secondary",
|
|
34
|
+
"brand",
|
|
35
|
+
"ghost",
|
|
36
|
+
"link",
|
|
37
|
+
] as const;
|
|
38
|
+
for (const variant of variants) {
|
|
39
|
+
const { unmount } = render(<Button variant={variant}>Btn</Button>);
|
|
40
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
41
|
+
unmount();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders all sizes without error", () => {
|
|
46
|
+
const sizes = ["default", "xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg"] as const;
|
|
47
|
+
for (const size of sizes) {
|
|
48
|
+
const { unmount } = render(<Button size={size}>B</Button>);
|
|
49
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
50
|
+
unmount();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders as child element when asChild is true", () => {
|
|
55
|
+
render(
|
|
56
|
+
<Button asChild>
|
|
57
|
+
<a href="/home">Home</a>
|
|
58
|
+
</Button>,
|
|
59
|
+
);
|
|
60
|
+
expect(screen.getByRole("link", { name: "Home" })).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("is disabled when disabled prop is set", () => {
|
|
64
|
+
render(<Button disabled>Disabled</Button>);
|
|
65
|
+
expect(screen.getByRole("button")).toBeDisabled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/* ------------------------------------------------------------------ */
|
|
70
|
+
/* Badge */
|
|
71
|
+
/* ------------------------------------------------------------------ */
|
|
72
|
+
|
|
73
|
+
describe("Badge", () => {
|
|
74
|
+
it("renders children", () => {
|
|
75
|
+
render(<Badge>Active</Badge>);
|
|
76
|
+
expect(screen.getByText("Active")).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders all variants without error", () => {
|
|
80
|
+
const variants = [
|
|
81
|
+
"default",
|
|
82
|
+
"secondary",
|
|
83
|
+
"outline",
|
|
84
|
+
"brand",
|
|
85
|
+
"success",
|
|
86
|
+
"warning",
|
|
87
|
+
"destructive",
|
|
88
|
+
] as const;
|
|
89
|
+
for (const variant of variants) {
|
|
90
|
+
const { unmount } = render(<Badge variant={variant}>v</Badge>);
|
|
91
|
+
expect(screen.getByText("v")).toBeInTheDocument();
|
|
92
|
+
unmount();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("forwards className", () => {
|
|
97
|
+
const { container } = render(<Badge className="custom-class">Badge</Badge>);
|
|
98
|
+
expect(container.firstChild).toHaveClass("custom-class");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/* ------------------------------------------------------------------ */
|
|
103
|
+
/* Input */
|
|
104
|
+
/* ------------------------------------------------------------------ */
|
|
105
|
+
|
|
106
|
+
describe("Input", () => {
|
|
107
|
+
it("renders an input element", () => {
|
|
108
|
+
render(<Input />);
|
|
109
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("carries data-slot attribute", () => {
|
|
113
|
+
render(<Input />);
|
|
114
|
+
expect(screen.getByRole("textbox")).toHaveAttribute("data-slot", "input");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("accepts a placeholder", () => {
|
|
118
|
+
render(<Input placeholder="Type here" />);
|
|
119
|
+
expect(screen.getByPlaceholderText("Type here")).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("is disabled when disabled prop is set", () => {
|
|
123
|
+
render(<Input disabled />);
|
|
124
|
+
expect(screen.getByRole("textbox")).toBeDisabled();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/* ------------------------------------------------------------------ */
|
|
129
|
+
/* Textarea */
|
|
130
|
+
/* ------------------------------------------------------------------ */
|
|
131
|
+
|
|
132
|
+
describe("Textarea", () => {
|
|
133
|
+
it("renders a textarea", () => {
|
|
134
|
+
render(<Textarea />);
|
|
135
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("carries data-slot attribute", () => {
|
|
139
|
+
render(<Textarea />);
|
|
140
|
+
expect(screen.getByRole("textbox")).toHaveAttribute("data-slot", "textarea");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("accepts placeholder", () => {
|
|
144
|
+
render(<Textarea placeholder="Write here" />);
|
|
145
|
+
expect(screen.getByPlaceholderText("Write here")).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/* ------------------------------------------------------------------ */
|
|
150
|
+
/* Label */
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
|
|
153
|
+
describe("Label", () => {
|
|
154
|
+
it("renders label text", () => {
|
|
155
|
+
render(<Label>Username</Label>);
|
|
156
|
+
expect(screen.getByText("Username")).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("associates with an input via htmlFor", () => {
|
|
160
|
+
render(
|
|
161
|
+
<>
|
|
162
|
+
<Label htmlFor="input-id">Name</Label>
|
|
163
|
+
<input id="input-id" />
|
|
164
|
+
</>,
|
|
165
|
+
);
|
|
166
|
+
expect(screen.getByLabelText("Name")).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/* ------------------------------------------------------------------ */
|
|
171
|
+
/* Spinner */
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
|
|
174
|
+
describe("Spinner", () => {
|
|
175
|
+
it('renders with role="status"', () => {
|
|
176
|
+
render(<Spinner />);
|
|
177
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("has accessible label", () => {
|
|
181
|
+
render(<Spinner />);
|
|
182
|
+
expect(screen.getByRole("status")).toHaveAttribute("aria-label", "Loading");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("renders all sizes without error", () => {
|
|
186
|
+
const sizes = ["xs", "sm", "default", "lg", "xl"] as const;
|
|
187
|
+
for (const size of sizes) {
|
|
188
|
+
const { unmount } = render(<Spinner size={size} />);
|
|
189
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
190
|
+
unmount();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/* ------------------------------------------------------------------ */
|
|
196
|
+
/* Skeleton */
|
|
197
|
+
/* ------------------------------------------------------------------ */
|
|
198
|
+
|
|
199
|
+
describe("Skeleton", () => {
|
|
200
|
+
it("renders a div with data-slot", () => {
|
|
201
|
+
const { container } = render(<Skeleton />);
|
|
202
|
+
expect(container.firstChild).toHaveAttribute("data-slot", "skeleton");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("forwards className", () => {
|
|
206
|
+
const { container } = render(<Skeleton className="w-24 h-4" />);
|
|
207
|
+
expect(container.firstChild).toHaveClass("w-24", "h-4");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/* ------------------------------------------------------------------ */
|
|
212
|
+
/* Separator */
|
|
213
|
+
/* ------------------------------------------------------------------ */
|
|
214
|
+
|
|
215
|
+
describe("Separator", () => {
|
|
216
|
+
it("renders a separator with role", () => {
|
|
217
|
+
render(<Separator />);
|
|
218
|
+
expect(screen.getByRole("none")).toBeInTheDocument();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("renders horizontal orientation without error", () => {
|
|
222
|
+
const { unmount } = render(<Separator orientation="horizontal" />);
|
|
223
|
+
expect(screen.getByRole("none")).toBeInTheDocument();
|
|
224
|
+
unmount();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("renders vertical orientation without error", () => {
|
|
228
|
+
const { unmount } = render(<Separator orientation="vertical" />);
|
|
229
|
+
expect(screen.getByRole("none")).toBeInTheDocument();
|
|
230
|
+
unmount();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/* ------------------------------------------------------------------ */
|
|
235
|
+
/* Kbd */
|
|
236
|
+
/* ------------------------------------------------------------------ */
|
|
237
|
+
|
|
238
|
+
describe("Kbd", () => {
|
|
239
|
+
it("renders keyboard shortcut text", () => {
|
|
240
|
+
render(<Kbd>⌘K</Kbd>);
|
|
241
|
+
expect(screen.getByText("⌘K")).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("renders as kbd element", () => {
|
|
245
|
+
const { container } = render(<Kbd>Esc</Kbd>);
|
|
246
|
+
expect(container.querySelector("kbd")).toBeInTheDocument();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Accordion } from "radix-ui";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
const AccordionRoot = React.forwardRef<
|
|
9
|
+
React.ComponentRef<typeof Accordion.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof Accordion.Root>
|
|
11
|
+
>(({ className, ...props }, ref) => (
|
|
12
|
+
<Accordion.Root ref={ref} className={cn("divide-y divide-taupe-100", className)} {...props} />
|
|
13
|
+
));
|
|
14
|
+
AccordionRoot.displayName = "Accordion";
|
|
15
|
+
|
|
16
|
+
const AccordionItem = React.forwardRef<
|
|
17
|
+
React.ComponentRef<typeof Accordion.Item>,
|
|
18
|
+
React.ComponentPropsWithoutRef<typeof Accordion.Item>
|
|
19
|
+
>(({ className, ...props }, ref) => (
|
|
20
|
+
<Accordion.Item ref={ref} className={cn("", className)} {...props} />
|
|
21
|
+
));
|
|
22
|
+
AccordionItem.displayName = "AccordionItem";
|
|
23
|
+
|
|
24
|
+
const AccordionTrigger = React.forwardRef<
|
|
25
|
+
React.ComponentRef<typeof Accordion.Trigger>,
|
|
26
|
+
React.ComponentPropsWithoutRef<typeof Accordion.Trigger>
|
|
27
|
+
>(({ className, children, ...props }, ref) => (
|
|
28
|
+
<Accordion.Header className="flex">
|
|
29
|
+
<Accordion.Trigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex w-full items-center justify-between py-4",
|
|
33
|
+
"body font-medium text-taupe-900 transition-colors hover:text-taupe-700",
|
|
34
|
+
"[&[data-state=open]>svg]:rotate-180",
|
|
35
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-taupe-900/15",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
<ChevronDown
|
|
42
|
+
size={16}
|
|
43
|
+
className="text-taupe-400 transition-transform duration-200 shrink-0"
|
|
44
|
+
/>
|
|
45
|
+
</Accordion.Trigger>
|
|
46
|
+
</Accordion.Header>
|
|
47
|
+
));
|
|
48
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
49
|
+
|
|
50
|
+
const AccordionContent = React.forwardRef<
|
|
51
|
+
React.ComponentRef<typeof Accordion.Content>,
|
|
52
|
+
React.ComponentPropsWithoutRef<typeof Accordion.Content>
|
|
53
|
+
>(({ className, children, ...props }, ref) => (
|
|
54
|
+
<Accordion.Content
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn(
|
|
57
|
+
"overflow-hidden body text-taupe-500",
|
|
58
|
+
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<div className="pb-4">{children}</div>
|
|
64
|
+
</Accordion.Content>
|
|
65
|
+
));
|
|
66
|
+
AccordionContent.displayName = "AccordionContent";
|
|
67
|
+
|
|
68
|
+
export { AccordionRoot as Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
|
|
5
|
+
interface ActionPanelAction {
|
|
6
|
+
label: string;
|
|
7
|
+
href?: string;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
variant?: "default" | "destructive" | "outline" | "ghost";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ActionPanelLink {
|
|
13
|
+
label: string;
|
|
14
|
+
href?: string;
|
|
15
|
+
onClick?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ActionPanelProps {
|
|
19
|
+
title: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
action?: ActionPanelAction;
|
|
22
|
+
actions?: ActionPanelAction[];
|
|
23
|
+
link?: ActionPanelLink;
|
|
24
|
+
variant?: "default" | "muted";
|
|
25
|
+
layout?: "stacked" | "inline";
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ActionPanel({
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
action,
|
|
34
|
+
actions,
|
|
35
|
+
link,
|
|
36
|
+
variant = "default",
|
|
37
|
+
layout = "stacked",
|
|
38
|
+
children,
|
|
39
|
+
className,
|
|
40
|
+
}: ActionPanelProps) {
|
|
41
|
+
const allActions = actions ?? (action ? [action] : []);
|
|
42
|
+
|
|
43
|
+
const renderActions = () => {
|
|
44
|
+
if (allActions.length === 0 && !link) return null;
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
{link &&
|
|
48
|
+
(link.href ? (
|
|
49
|
+
<a
|
|
50
|
+
href={link.href}
|
|
51
|
+
className="body font-medium text-taupe-600 hover:text-taupe-900 transition-colors inline-flex items-center gap-1"
|
|
52
|
+
>
|
|
53
|
+
{link.label} <span aria-hidden>→</span>
|
|
54
|
+
</a>
|
|
55
|
+
) : (
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={link.onClick}
|
|
59
|
+
className="body font-medium text-taupe-600 hover:text-taupe-900 transition-colors inline-flex items-center gap-1"
|
|
60
|
+
>
|
|
61
|
+
{link.label} <span aria-hidden>→</span>
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
{allActions.map((a, i) =>
|
|
65
|
+
a.href ? (
|
|
66
|
+
<Button key={i} size="sm" variant={a.variant ?? "default"} asChild>
|
|
67
|
+
<a href={a.href}>{a.label}</a>
|
|
68
|
+
</Button>
|
|
69
|
+
) : (
|
|
70
|
+
<Button key={i} size="sm" variant={a.variant ?? "default"} onClick={a.onClick}>
|
|
71
|
+
{a.label}
|
|
72
|
+
</Button>
|
|
73
|
+
),
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
"rounded-xl border px-5 py-5",
|
|
83
|
+
variant === "muted"
|
|
84
|
+
? "border-taupe-100 bg-taupe-50"
|
|
85
|
+
: "border-taupe-100 bg-white shadow-natural",
|
|
86
|
+
layout === "inline" && "flex items-center justify-between gap-6",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<div className={cn("min-w-0", layout === "inline" ? "flex-1" : "")}>
|
|
91
|
+
<p className="body font-semibold text-taupe-900">{title}</p>
|
|
92
|
+
{description && (
|
|
93
|
+
<p className={cn("body text-taupe-400", layout === "stacked" ? "mt-1" : "")}>
|
|
94
|
+
{description}
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
{children && layout === "stacked" && <div className="mt-4">{children}</div>}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{layout === "stacked" ? (
|
|
101
|
+
(allActions.length > 0 || link) && <div className="mt-4">{renderActions()}</div>
|
|
102
|
+
) : (
|
|
103
|
+
<div className="shrink-0 flex items-center gap-3">
|
|
104
|
+
{children}
|
|
105
|
+
{renderActions()}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { ActionPanel };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface FeedEvent {
|
|
5
|
+
id?: string | number;
|
|
6
|
+
content: React.ReactNode;
|
|
7
|
+
date?: string;
|
|
8
|
+
datetime?: string;
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
iconBgClass?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ActivityFeedProps {
|
|
14
|
+
events: FeedEvent[];
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ActivityFeed({ events, className }: ActivityFeedProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("flow-root", className)}>
|
|
21
|
+
<ul role="list" className="-mb-8">
|
|
22
|
+
{events.map((event, i) => {
|
|
23
|
+
const isLast = i === events.length - 1;
|
|
24
|
+
return (
|
|
25
|
+
<li key={event.id ?? i}>
|
|
26
|
+
<div className="relative pb-8">
|
|
27
|
+
{!isLast && (
|
|
28
|
+
<span
|
|
29
|
+
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-taupe-100"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
/>
|
|
32
|
+
)}
|
|
33
|
+
<div className="relative flex gap-3">
|
|
34
|
+
{/* Icon */}
|
|
35
|
+
<div>
|
|
36
|
+
<span
|
|
37
|
+
className={cn(
|
|
38
|
+
"flex size-8 items-center justify-center rounded-full ring-8 ring-white",
|
|
39
|
+
event.iconBgClass ?? "bg-taupe-100 text-taupe-500",
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{event.icon}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Content */}
|
|
47
|
+
<div className="flex min-w-0 flex-1 items-baseline justify-between gap-4 pt-1">
|
|
48
|
+
<div className="body text-taupe-600">{event.content}</div>
|
|
49
|
+
{event.date && (
|
|
50
|
+
<time
|
|
51
|
+
dateTime={event.datetime ?? event.date}
|
|
52
|
+
className="caption whitespace-nowrap text-taupe-400"
|
|
53
|
+
>
|
|
54
|
+
{event.date}
|
|
55
|
+
</time>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</li>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</ul>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { ActivityFeed };
|
|
69
|
+
export type { FeedEvent };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { AlertDialog } from "radix-ui";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const AlertDialogRoot = AlertDialog.Root;
|
|
8
|
+
const AlertDialogTrigger = AlertDialog.Trigger;
|
|
9
|
+
const AlertDialogPortal = AlertDialog.Portal;
|
|
10
|
+
|
|
11
|
+
const AlertDialogOverlay = React.forwardRef<
|
|
12
|
+
React.ComponentRef<typeof AlertDialog.Overlay>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof AlertDialog.Overlay>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<AlertDialog.Overlay
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
"fixed inset-0 z-50 bg-black/30 backdrop-blur-sm",
|
|
19
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
20
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
));
|
|
26
|
+
AlertDialogOverlay.displayName = "AlertDialogOverlay";
|
|
27
|
+
|
|
28
|
+
const AlertDialogContent = React.forwardRef<
|
|
29
|
+
React.ComponentRef<typeof AlertDialog.Content>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof AlertDialog.Content>
|
|
31
|
+
>(({ className, children, ...props }, ref) => (
|
|
32
|
+
<AlertDialog.Portal>
|
|
33
|
+
<AlertDialogOverlay />
|
|
34
|
+
<AlertDialog.Content
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(
|
|
37
|
+
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
|
38
|
+
"w-full max-w-md bg-white rounded-2xl shadow-elevated p-6",
|
|
39
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
40
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
41
|
+
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</AlertDialog.Content>
|
|
48
|
+
</AlertDialog.Portal>
|
|
49
|
+
));
|
|
50
|
+
AlertDialogContent.displayName = "AlertDialogContent";
|
|
51
|
+
|
|
52
|
+
function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
53
|
+
return <div className={cn("flex flex-col gap-1.5 mb-5", className)} {...props} />;
|
|
54
|
+
}
|
|
55
|
+
AlertDialogHeader.displayName = "AlertDialogHeader";
|
|
56
|
+
|
|
57
|
+
function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
58
|
+
return <div className={cn("flex items-center justify-end gap-2 mt-6", className)} {...props} />;
|
|
59
|
+
}
|
|
60
|
+
AlertDialogFooter.displayName = "AlertDialogFooter";
|
|
61
|
+
|
|
62
|
+
const AlertDialogTitle = React.forwardRef<
|
|
63
|
+
React.ComponentRef<typeof AlertDialog.Title>,
|
|
64
|
+
React.ComponentPropsWithoutRef<typeof AlertDialog.Title>
|
|
65
|
+
>(({ className, ...props }, ref) => (
|
|
66
|
+
<AlertDialog.Title
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cn("body-lg font-semibold text-taupe-900", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
));
|
|
72
|
+
AlertDialogTitle.displayName = "AlertDialogTitle";
|
|
73
|
+
|
|
74
|
+
const AlertDialogDescription = React.forwardRef<
|
|
75
|
+
React.ComponentRef<typeof AlertDialog.Description>,
|
|
76
|
+
React.ComponentPropsWithoutRef<typeof AlertDialog.Description>
|
|
77
|
+
>(({ className, ...props }, ref) => (
|
|
78
|
+
<AlertDialog.Description ref={ref} className={cn("body text-taupe-500", className)} {...props} />
|
|
79
|
+
));
|
|
80
|
+
AlertDialogDescription.displayName = "AlertDialogDescription";
|
|
81
|
+
|
|
82
|
+
const AlertDialogAction = React.forwardRef<
|
|
83
|
+
React.ComponentRef<typeof AlertDialog.Action>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof AlertDialog.Action>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<AlertDialog.Action
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn(
|
|
89
|
+
"inline-flex h-9 items-center justify-center rounded-md px-4",
|
|
90
|
+
"body font-medium whitespace-nowrap transition-all duration-150",
|
|
91
|
+
"bg-brand-primary text-white hover:bg-brand-primary/90 active:bg-brand-primary/80",
|
|
92
|
+
"focus-visible:ring-2 focus-visible:ring-brand-primary/30 focus-visible:ring-offset-1 outline-none",
|
|
93
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
94
|
+
className,
|
|
95
|
+
)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
));
|
|
99
|
+
AlertDialogAction.displayName = "AlertDialogAction";
|
|
100
|
+
|
|
101
|
+
const AlertDialogCancel = React.forwardRef<
|
|
102
|
+
React.ComponentRef<typeof AlertDialog.Cancel>,
|
|
103
|
+
React.ComponentPropsWithoutRef<typeof AlertDialog.Cancel>
|
|
104
|
+
>(({ className, ...props }, ref) => (
|
|
105
|
+
<AlertDialog.Cancel
|
|
106
|
+
ref={ref}
|
|
107
|
+
className={cn(
|
|
108
|
+
"inline-flex h-9 items-center justify-center rounded-md px-4",
|
|
109
|
+
"body font-medium whitespace-nowrap transition-all duration-150",
|
|
110
|
+
"bg-white text-taupe-700 shadow-natural hover:bg-taupe-50 rounded-xl",
|
|
111
|
+
"focus-visible:ring-2 focus-visible:ring-brand-primary/30 focus-visible:ring-offset-1 outline-none",
|
|
112
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
));
|
|
118
|
+
AlertDialogCancel.displayName = "AlertDialogCancel";
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
AlertDialogRoot as AlertDialog,
|
|
122
|
+
AlertDialogTrigger,
|
|
123
|
+
AlertDialogPortal,
|
|
124
|
+
AlertDialogOverlay,
|
|
125
|
+
AlertDialogContent,
|
|
126
|
+
AlertDialogHeader,
|
|
127
|
+
AlertDialogFooter,
|
|
128
|
+
AlertDialogTitle,
|
|
129
|
+
AlertDialogDescription,
|
|
130
|
+
AlertDialogAction,
|
|
131
|
+
AlertDialogCancel,
|
|
132
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
const alertVariants = cva(
|
|
6
|
+
"relative w-full rounded-xl px-4 py-3.5 body [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-3.5 [&>svg~*]:pl-6",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-taupe-50 text-taupe-800 [&>svg]:text-taupe-500",
|
|
11
|
+
info: "bg-brand-secondary/40 text-taupe-700 [&>svg]:text-taupe-500",
|
|
12
|
+
success: "bg-brand-secondary/15 text-taupe-800 [&>svg]:text-taupe-600",
|
|
13
|
+
warning: "bg-brand-secondary/70 text-taupe-800 [&>svg]:text-taupe-600",
|
|
14
|
+
destructive: "bg-red-50 text-red-800 [&>svg]:text-red-500",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: { variant: "default" },
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
interface AlertProps extends React.ComponentProps<"div">, VariantProps<typeof alertVariants> {}
|
|
22
|
+
|
|
23
|
+
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
|
24
|
+
({ className, variant, ...props }, ref) => (
|
|
25
|
+
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
Alert.displayName = "Alert";
|
|
29
|
+
|
|
30
|
+
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.ComponentProps<"p">>(
|
|
31
|
+
({ className, ...props }, ref) => (
|
|
32
|
+
<p ref={ref} className={cn("mb-0.5 body font-semibold leading-tight", className)} {...props} />
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
AlertTitle.displayName = "AlertTitle";
|
|
36
|
+
|
|
37
|
+
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.ComponentProps<"p">>(
|
|
38
|
+
({ className, ...props }, ref) => (
|
|
39
|
+
<p ref={ref} className={cn("body opacity-90", className)} {...props} />
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
AlertDescription.displayName = "AlertDescription";
|
|
43
|
+
|
|
44
|
+
export { Alert, AlertTitle, AlertDescription };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { AspectRatio } from "radix-ui";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const AspectRatioRoot = React.forwardRef<
|
|
8
|
+
React.ComponentRef<typeof AspectRatio.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof AspectRatio.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<AspectRatio.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn("relative w-full overflow-hidden", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
AspectRatioRoot.displayName = "AspectRatio";
|
|
18
|
+
|
|
19
|
+
export { AspectRatioRoot as AspectRatio };
|