@oppulence/design-system 1.0.2
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/README.md +115 -0
- package/components.json +21 -0
- package/hooks/use-mobile.tsx +21 -0
- package/lib/utils.ts +6 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/components/atoms/aspect-ratio.tsx +21 -0
- package/src/components/atoms/avatar.tsx +91 -0
- package/src/components/atoms/badge.tsx +47 -0
- package/src/components/atoms/button.tsx +128 -0
- package/src/components/atoms/checkbox.tsx +24 -0
- package/src/components/atoms/container.tsx +42 -0
- package/src/components/atoms/heading.tsx +56 -0
- package/src/components/atoms/index.ts +21 -0
- package/src/components/atoms/input.tsx +18 -0
- package/src/components/atoms/kbd.tsx +23 -0
- package/src/components/atoms/label.tsx +15 -0
- package/src/components/atoms/logo.tsx +52 -0
- package/src/components/atoms/progress.tsx +79 -0
- package/src/components/atoms/separator.tsx +17 -0
- package/src/components/atoms/skeleton.tsx +13 -0
- package/src/components/atoms/slider.tsx +56 -0
- package/src/components/atoms/spinner.tsx +14 -0
- package/src/components/atoms/stack.tsx +126 -0
- package/src/components/atoms/switch.tsx +26 -0
- package/src/components/atoms/text.tsx +69 -0
- package/src/components/atoms/textarea.tsx +19 -0
- package/src/components/atoms/toggle.tsx +40 -0
- package/src/components/molecules/accordion.tsx +72 -0
- package/src/components/molecules/ai-chat.tsx +251 -0
- package/src/components/molecules/alert.tsx +131 -0
- package/src/components/molecules/breadcrumb.tsx +301 -0
- package/src/components/molecules/button-group.tsx +96 -0
- package/src/components/molecules/card.tsx +184 -0
- package/src/components/molecules/collapsible.tsx +21 -0
- package/src/components/molecules/command-search.tsx +148 -0
- package/src/components/molecules/empty.tsx +98 -0
- package/src/components/molecules/field.tsx +217 -0
- package/src/components/molecules/grid.tsx +141 -0
- package/src/components/molecules/hover-card.tsx +45 -0
- package/src/components/molecules/index.ts +29 -0
- package/src/components/molecules/input-group.tsx +151 -0
- package/src/components/molecules/input-otp.tsx +74 -0
- package/src/components/molecules/item.tsx +194 -0
- package/src/components/molecules/page-header.tsx +89 -0
- package/src/components/molecules/pagination.tsx +130 -0
- package/src/components/molecules/popover.tsx +96 -0
- package/src/components/molecules/radio-group.tsx +37 -0
- package/src/components/molecules/resizable.tsx +52 -0
- package/src/components/molecules/scroll-area.tsx +45 -0
- package/src/components/molecules/section.tsx +108 -0
- package/src/components/molecules/select.tsx +201 -0
- package/src/components/molecules/settings.tsx +197 -0
- package/src/components/molecules/table.tsx +111 -0
- package/src/components/molecules/tabs.tsx +74 -0
- package/src/components/molecules/theme-switcher.tsx +187 -0
- package/src/components/molecules/toggle-group.tsx +89 -0
- package/src/components/molecules/tooltip.tsx +66 -0
- package/src/components/organisms/alert-dialog.tsx +152 -0
- package/src/components/organisms/app-shell.tsx +939 -0
- package/src/components/organisms/calendar.tsx +212 -0
- package/src/components/organisms/carousel.tsx +230 -0
- package/src/components/organisms/chart.tsx +333 -0
- package/src/components/organisms/combobox.tsx +274 -0
- package/src/components/organisms/command.tsx +200 -0
- package/src/components/organisms/context-menu.tsx +229 -0
- package/src/components/organisms/dialog.tsx +134 -0
- package/src/components/organisms/drawer.tsx +123 -0
- package/src/components/organisms/dropdown-menu.tsx +256 -0
- package/src/components/organisms/index.ts +17 -0
- package/src/components/organisms/menubar.tsx +203 -0
- package/src/components/organisms/navigation-menu.tsx +143 -0
- package/src/components/organisms/page-layout.tsx +105 -0
- package/src/components/organisms/sheet.tsx +126 -0
- package/src/components/organisms/sidebar.tsx +723 -0
- package/src/components/organisms/sonner.tsx +41 -0
- package/src/components/ui/index.ts +3 -0
- package/src/index.ts +3 -0
- package/src/styles/globals.css +297 -0
- package/tailwind.config.ts +77 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ArrowRightIcon,
|
|
7
|
+
ChevronRightIcon,
|
|
8
|
+
MoreHorizontalIcon,
|
|
9
|
+
SlashIcon,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import {
|
|
12
|
+
DropdownMenu,
|
|
13
|
+
DropdownMenuContent,
|
|
14
|
+
DropdownMenuItem,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
} from "../organisms/dropdown-menu";
|
|
17
|
+
|
|
18
|
+
interface BreadcrumbItemData {
|
|
19
|
+
/** The text label for the breadcrumb item */
|
|
20
|
+
label?: React.ReactNode;
|
|
21
|
+
/** The href for the link. If omitted on the last item, it becomes the current page */
|
|
22
|
+
href?: string;
|
|
23
|
+
/** Whether this item is the current page (auto-detected if last item has no href) */
|
|
24
|
+
isCurrent?: boolean;
|
|
25
|
+
/** Render as ellipsis (collapsed items indicator) */
|
|
26
|
+
isEllipsis?: boolean;
|
|
27
|
+
/** Additional props to pass to the link or page element */
|
|
28
|
+
props?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type BreadcrumbSeparatorType = "chevron" | "slash" | "arrow";
|
|
32
|
+
|
|
33
|
+
const separatorIcons: Record<BreadcrumbSeparatorType, React.ReactNode> = {
|
|
34
|
+
chevron: <ChevronRightIcon />,
|
|
35
|
+
slash: <SlashIcon />,
|
|
36
|
+
arrow: <ArrowRightIcon />,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface BreadcrumbProps extends Omit<
|
|
40
|
+
React.ComponentProps<"nav">,
|
|
41
|
+
"children" | "className"
|
|
42
|
+
> {
|
|
43
|
+
/** Simple API: Array of breadcrumb items */
|
|
44
|
+
items?: BreadcrumbItemData[];
|
|
45
|
+
/** Separator style: 'chevron' (default), 'slash', or 'arrow' */
|
|
46
|
+
separator?: BreadcrumbSeparatorType;
|
|
47
|
+
/** Max visible items (including ellipsis). Default: 4. Set to 0 to disable auto-collapse. */
|
|
48
|
+
maxItems?: number;
|
|
49
|
+
/** Number of items to show at the start before ellipsis. Default: 1 */
|
|
50
|
+
itemsBeforeCollapse?: number;
|
|
51
|
+
/** Children for compound component pattern */
|
|
52
|
+
children?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CollapseResult {
|
|
56
|
+
displayItems: Array<
|
|
57
|
+
BreadcrumbItemData & { collapsedItems?: BreadcrumbItemData[] }
|
|
58
|
+
>;
|
|
59
|
+
hasCollapsed: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collapseItems({
|
|
63
|
+
items,
|
|
64
|
+
maxItems,
|
|
65
|
+
itemsBeforeCollapse,
|
|
66
|
+
}: {
|
|
67
|
+
items: BreadcrumbItemData[];
|
|
68
|
+
maxItems: number;
|
|
69
|
+
itemsBeforeCollapse: number;
|
|
70
|
+
}): CollapseResult {
|
|
71
|
+
// If maxItems is 0 or items already fit, return as-is
|
|
72
|
+
if (maxItems === 0 || items.length <= maxItems) {
|
|
73
|
+
return { displayItems: items, hasCollapsed: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Ensure we have at least 1 item before ellipsis
|
|
77
|
+
const before = Math.max(1, Math.min(itemsBeforeCollapse, maxItems - 2));
|
|
78
|
+
|
|
79
|
+
// Calculate items after ellipsis: maxItems - before - 1 (for ellipsis)
|
|
80
|
+
const after = maxItems - before - 1;
|
|
81
|
+
|
|
82
|
+
// If we can't fit at least 1 after, no point collapsing
|
|
83
|
+
if (after < 1) {
|
|
84
|
+
return { displayItems: items, hasCollapsed: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If before + after >= items.length, no need to collapse
|
|
88
|
+
if (before + after >= items.length) {
|
|
89
|
+
return { displayItems: items, hasCollapsed: false };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const startItems = items.slice(0, before);
|
|
93
|
+
const collapsedItems = items.slice(before, items.length - after);
|
|
94
|
+
const endItems = items.slice(items.length - after);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
displayItems: [
|
|
98
|
+
...startItems,
|
|
99
|
+
{ isEllipsis: true, collapsedItems },
|
|
100
|
+
...endItems,
|
|
101
|
+
],
|
|
102
|
+
hasCollapsed: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Breadcrumb({
|
|
107
|
+
items,
|
|
108
|
+
separator = "chevron",
|
|
109
|
+
maxItems = 4,
|
|
110
|
+
itemsBeforeCollapse = 1,
|
|
111
|
+
children,
|
|
112
|
+
...props
|
|
113
|
+
}: BreadcrumbProps) {
|
|
114
|
+
// If items prop is provided, render simple API
|
|
115
|
+
if (items && items.length > 0) {
|
|
116
|
+
const separatorIcon = separatorIcons[separator];
|
|
117
|
+
|
|
118
|
+
// Auto-collapse if needed
|
|
119
|
+
const { displayItems } = collapseItems({
|
|
120
|
+
items,
|
|
121
|
+
maxItems,
|
|
122
|
+
itemsBeforeCollapse,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<nav aria-label="breadcrumb" data-slot="breadcrumb" {...props}>
|
|
127
|
+
<BreadcrumbList>
|
|
128
|
+
{displayItems.map((item, index) => {
|
|
129
|
+
const isLast = index === displayItems.length - 1;
|
|
130
|
+
const isCurrent =
|
|
131
|
+
item.isCurrent ?? (isLast && !item.href && !item.isEllipsis);
|
|
132
|
+
const collapsedItems =
|
|
133
|
+
"collapsedItems" in item ? item.collapsedItems : undefined;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<React.Fragment key={index}>
|
|
137
|
+
<BreadcrumbItem>
|
|
138
|
+
{item.isEllipsis ? (
|
|
139
|
+
<BreadcrumbEllipsisMenu collapsedItems={collapsedItems} />
|
|
140
|
+
) : isCurrent ? (
|
|
141
|
+
<BreadcrumbPage {...item.props}>
|
|
142
|
+
{item.label}
|
|
143
|
+
</BreadcrumbPage>
|
|
144
|
+
) : (
|
|
145
|
+
<BreadcrumbLink href={item.href} {...item.props}>
|
|
146
|
+
{item.label}
|
|
147
|
+
</BreadcrumbLink>
|
|
148
|
+
)}
|
|
149
|
+
</BreadcrumbItem>
|
|
150
|
+
{!isLast && (
|
|
151
|
+
<BreadcrumbSeparator>{separatorIcon}</BreadcrumbSeparator>
|
|
152
|
+
)}
|
|
153
|
+
</React.Fragment>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</BreadcrumbList>
|
|
157
|
+
</nav>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Otherwise, render compound component pattern
|
|
162
|
+
return (
|
|
163
|
+
<nav aria-label="breadcrumb" data-slot="breadcrumb" {...props}>
|
|
164
|
+
{children}
|
|
165
|
+
</nav>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function BreadcrumbList({
|
|
170
|
+
...props
|
|
171
|
+
}: Omit<React.ComponentProps<"ol">, "className">) {
|
|
172
|
+
return (
|
|
173
|
+
<ol
|
|
174
|
+
data-slot="breadcrumb-list"
|
|
175
|
+
className="text-muted-foreground gap-1.5 text-sm sm:gap-2.5 flex flex-wrap items-center break-words"
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function BreadcrumbItem({
|
|
182
|
+
...props
|
|
183
|
+
}: Omit<React.ComponentProps<"li">, "className">) {
|
|
184
|
+
return (
|
|
185
|
+
<li
|
|
186
|
+
data-slot="breadcrumb-item"
|
|
187
|
+
className="gap-1.5 inline-flex items-center"
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function BreadcrumbLink({
|
|
194
|
+
render,
|
|
195
|
+
...props
|
|
196
|
+
}: Omit<useRender.ComponentProps<"a">, "className">) {
|
|
197
|
+
return useRender({
|
|
198
|
+
defaultTagName: "a",
|
|
199
|
+
props: mergeProps<"a">(
|
|
200
|
+
{
|
|
201
|
+
className: "hover:text-foreground transition-colors",
|
|
202
|
+
},
|
|
203
|
+
props,
|
|
204
|
+
),
|
|
205
|
+
render,
|
|
206
|
+
state: {
|
|
207
|
+
slot: "breadcrumb-link",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function BreadcrumbPage({
|
|
213
|
+
...props
|
|
214
|
+
}: Omit<React.ComponentProps<"span">, "className">) {
|
|
215
|
+
return (
|
|
216
|
+
<span
|
|
217
|
+
data-slot="breadcrumb-page"
|
|
218
|
+
role="link"
|
|
219
|
+
aria-disabled="true"
|
|
220
|
+
aria-current="page"
|
|
221
|
+
className="text-foreground font-normal"
|
|
222
|
+
{...props}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function BreadcrumbSeparator({
|
|
228
|
+
children,
|
|
229
|
+
...props
|
|
230
|
+
}: Omit<React.ComponentProps<"li">, "className">) {
|
|
231
|
+
return (
|
|
232
|
+
<li
|
|
233
|
+
data-slot="breadcrumb-separator"
|
|
234
|
+
role="presentation"
|
|
235
|
+
aria-hidden="true"
|
|
236
|
+
className="[&>svg]:size-3.5"
|
|
237
|
+
{...props}
|
|
238
|
+
>
|
|
239
|
+
{children ?? <ChevronRightIcon />}
|
|
240
|
+
</li>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function BreadcrumbEllipsis({
|
|
245
|
+
...props
|
|
246
|
+
}: Omit<React.ComponentProps<"span">, "className">) {
|
|
247
|
+
return (
|
|
248
|
+
<span
|
|
249
|
+
data-slot="breadcrumb-ellipsis"
|
|
250
|
+
role="presentation"
|
|
251
|
+
aria-hidden="true"
|
|
252
|
+
className="size-5 [&>svg]:size-4 flex items-center justify-center"
|
|
253
|
+
{...props}
|
|
254
|
+
>
|
|
255
|
+
<MoreHorizontalIcon />
|
|
256
|
+
<span className="sr-only">More</span>
|
|
257
|
+
</span>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function BreadcrumbEllipsisMenu({
|
|
262
|
+
collapsedItems,
|
|
263
|
+
}: {
|
|
264
|
+
collapsedItems?: BreadcrumbItemData[];
|
|
265
|
+
}) {
|
|
266
|
+
if (!collapsedItems || collapsedItems.length === 0) {
|
|
267
|
+
return <BreadcrumbEllipsis />;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<DropdownMenu>
|
|
272
|
+
<DropdownMenuTrigger
|
|
273
|
+
variant="ellipsis"
|
|
274
|
+
aria-label="Show hidden breadcrumb items"
|
|
275
|
+
>
|
|
276
|
+
<MoreHorizontalIcon />
|
|
277
|
+
</DropdownMenuTrigger>
|
|
278
|
+
<DropdownMenuContent align="start">
|
|
279
|
+
{collapsedItems.map((item, index) => (
|
|
280
|
+
<DropdownMenuItem
|
|
281
|
+
key={index}
|
|
282
|
+
render={item.href ? <a href={item.href} /> : undefined}
|
|
283
|
+
>
|
|
284
|
+
{item.label}
|
|
285
|
+
</DropdownMenuItem>
|
|
286
|
+
))}
|
|
287
|
+
</DropdownMenuContent>
|
|
288
|
+
</DropdownMenu>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
Breadcrumb,
|
|
294
|
+
BreadcrumbEllipsis,
|
|
295
|
+
BreadcrumbItem,
|
|
296
|
+
BreadcrumbLink,
|
|
297
|
+
BreadcrumbList,
|
|
298
|
+
BreadcrumbPage,
|
|
299
|
+
BreadcrumbSeparator,
|
|
300
|
+
};
|
|
301
|
+
export type { BreadcrumbItemData, BreadcrumbSeparatorType };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
|
|
6
|
+
const buttonGroupVariants = cva(
|
|
7
|
+
"has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
orientation: {
|
|
11
|
+
horizontal:
|
|
12
|
+
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0 [&>[data-slot]]:rounded-r-none",
|
|
13
|
+
vertical:
|
|
14
|
+
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 [&>[data-slot]]:rounded-b-none",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
orientation: "horizontal",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const buttonGroupTextVariants = cva(
|
|
24
|
+
"gap-2 rounded-md border px-2.5 text-sm font-medium shadow-xs [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
|
|
25
|
+
{
|
|
26
|
+
variants: {
|
|
27
|
+
variant: {
|
|
28
|
+
default: "bg-muted",
|
|
29
|
+
display:
|
|
30
|
+
"bg-background border-y border-x-0 min-w-[60px] justify-center tabular-nums",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "default",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function ButtonGroup({
|
|
40
|
+
orientation,
|
|
41
|
+
...props
|
|
42
|
+
}: Omit<React.ComponentProps<"div">, "className"> &
|
|
43
|
+
VariantProps<typeof buttonGroupVariants>) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
role="group"
|
|
47
|
+
data-slot="button-group"
|
|
48
|
+
data-orientation={orientation}
|
|
49
|
+
className={buttonGroupVariants({ orientation })}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ButtonGroupText({
|
|
56
|
+
variant,
|
|
57
|
+
render,
|
|
58
|
+
...props
|
|
59
|
+
}: Omit<useRender.ComponentProps<"div">, "className"> &
|
|
60
|
+
VariantProps<typeof buttonGroupTextVariants>) {
|
|
61
|
+
return useRender({
|
|
62
|
+
defaultTagName: "div",
|
|
63
|
+
props: mergeProps<"div">(
|
|
64
|
+
{
|
|
65
|
+
className: buttonGroupTextVariants({ variant }),
|
|
66
|
+
},
|
|
67
|
+
props,
|
|
68
|
+
),
|
|
69
|
+
render,
|
|
70
|
+
state: {
|
|
71
|
+
slot: "button-group-text",
|
|
72
|
+
variant,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ButtonGroupSeparator({
|
|
78
|
+
orientation = "vertical",
|
|
79
|
+
...props
|
|
80
|
+
}: Omit<SeparatorPrimitive.Props, "className">) {
|
|
81
|
+
return (
|
|
82
|
+
<SeparatorPrimitive
|
|
83
|
+
data-slot="button-group-separator"
|
|
84
|
+
orientation={orientation}
|
|
85
|
+
className="bg-input shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch relative data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto"
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
ButtonGroup,
|
|
93
|
+
ButtonGroupSeparator,
|
|
94
|
+
ButtonGroupText,
|
|
95
|
+
buttonGroupVariants,
|
|
96
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
const cardVariants = cva(
|
|
5
|
+
"bg-card text-card-foreground border border-border/40 shadow-[0_1px_3px_0_rgb(0_0_0/0.06)] gap-4 overflow-hidden rounded-xl py-4 text-sm has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col",
|
|
6
|
+
{
|
|
7
|
+
variants: {
|
|
8
|
+
width: {
|
|
9
|
+
auto: "", // shrink to content (default behavior)
|
|
10
|
+
full: "w-full", // fill parent container
|
|
11
|
+
sm: "w-sm", // 24rem (384px)
|
|
12
|
+
md: "w-md", // 28rem (448px)
|
|
13
|
+
lg: "w-lg", // 32rem (512px)
|
|
14
|
+
xl: "w-xl", // 36rem (576px)
|
|
15
|
+
"2xl": "w-2xl", // 42rem (672px)
|
|
16
|
+
"3xl": "w-3xl", // 48rem (768px)
|
|
17
|
+
},
|
|
18
|
+
maxWidth: {
|
|
19
|
+
sm: "max-w-sm",
|
|
20
|
+
md: "max-w-md",
|
|
21
|
+
lg: "max-w-lg",
|
|
22
|
+
xl: "max-w-xl",
|
|
23
|
+
"2xl": "max-w-2xl",
|
|
24
|
+
"3xl": "max-w-3xl",
|
|
25
|
+
full: "max-w-full",
|
|
26
|
+
},
|
|
27
|
+
spacing: {
|
|
28
|
+
default: "",
|
|
29
|
+
tight: "gap-3",
|
|
30
|
+
relaxed: "gap-6",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
width: "auto",
|
|
35
|
+
spacing: "default",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
interface CardProps
|
|
41
|
+
extends
|
|
42
|
+
Omit<React.ComponentProps<"div">, "className" | "title">,
|
|
43
|
+
VariantProps<typeof cardVariants> {
|
|
44
|
+
size?: "default" | "sm";
|
|
45
|
+
/** Card title - renders in CardHeader */
|
|
46
|
+
title?: React.ReactNode;
|
|
47
|
+
/** Card description - renders below title in CardHeader */
|
|
48
|
+
description?: React.ReactNode;
|
|
49
|
+
/** Action element(s) in the header (e.g., buttons, badges) */
|
|
50
|
+
headerAction?: React.ReactNode;
|
|
51
|
+
/** Footer content */
|
|
52
|
+
footer?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Card({
|
|
56
|
+
size = "default",
|
|
57
|
+
width,
|
|
58
|
+
maxWidth,
|
|
59
|
+
title,
|
|
60
|
+
description,
|
|
61
|
+
headerAction,
|
|
62
|
+
footer,
|
|
63
|
+
children,
|
|
64
|
+
...props
|
|
65
|
+
}: CardProps) {
|
|
66
|
+
const hasHeader = title || description || headerAction;
|
|
67
|
+
|
|
68
|
+
// Check if children contain compound components (have data-slot)
|
|
69
|
+
const hasCompoundChildren = React.Children.toArray(children).some((child) => {
|
|
70
|
+
if (React.isValidElement(child)) {
|
|
71
|
+
const props = child.props as Record<string, unknown>;
|
|
72
|
+
return (
|
|
73
|
+
props["data-slot"] === "card-header" ||
|
|
74
|
+
props["data-slot"] === "card-content" ||
|
|
75
|
+
props["data-slot"] === "card-footer"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
data-slot="card"
|
|
84
|
+
data-size={size}
|
|
85
|
+
className={cardVariants({ width, maxWidth })}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
{hasHeader && (
|
|
89
|
+
<CardHeader>
|
|
90
|
+
{title && <CardTitle>{title}</CardTitle>}
|
|
91
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
92
|
+
{headerAction && <CardAction>{headerAction}</CardAction>}
|
|
93
|
+
</CardHeader>
|
|
94
|
+
)}
|
|
95
|
+
{hasCompoundChildren
|
|
96
|
+
? children
|
|
97
|
+
: children && <CardContent>{children}</CardContent>}
|
|
98
|
+
{footer && <CardFooter>{footer}</CardFooter>}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function CardHeader({
|
|
104
|
+
...props
|
|
105
|
+
}: Omit<React.ComponentProps<"div">, "className">) {
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
data-slot="card-header"
|
|
109
|
+
className="gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]"
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function CardTitle({
|
|
116
|
+
...props
|
|
117
|
+
}: Omit<React.ComponentProps<"div">, "className">) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
data-slot="card-title"
|
|
121
|
+
className="text-base leading-snug font-medium group-data-[size=sm]/card:text-sm"
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function CardDescription({
|
|
128
|
+
...props
|
|
129
|
+
}: Omit<React.ComponentProps<"div">, "className">) {
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
data-slot="card-description"
|
|
133
|
+
className="text-muted-foreground text-sm"
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function CardAction({
|
|
140
|
+
...props
|
|
141
|
+
}: Omit<React.ComponentProps<"div">, "className">) {
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
data-slot="card-action"
|
|
145
|
+
className="col-start-2 row-span-2 row-start-1 self-start justify-self-end"
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function CardContent({
|
|
152
|
+
...props
|
|
153
|
+
}: Omit<React.ComponentProps<"div">, "className">) {
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
data-slot="card-content"
|
|
157
|
+
className="px-4 group-data-[size=sm]/card:px-3"
|
|
158
|
+
{...props}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function CardFooter({
|
|
164
|
+
...props
|
|
165
|
+
}: Omit<React.ComponentProps<"div">, "className">) {
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
data-slot="card-footer"
|
|
169
|
+
className="bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center"
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
Card,
|
|
177
|
+
CardAction,
|
|
178
|
+
CardContent,
|
|
179
|
+
CardDescription,
|
|
180
|
+
CardFooter,
|
|
181
|
+
CardHeader,
|
|
182
|
+
CardTitle,
|
|
183
|
+
cardVariants,
|
|
184
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
|
4
|
+
|
|
5
|
+
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
|
|
6
|
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
|
|
10
|
+
return (
|
|
11
|
+
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
|
|
16
|
+
return (
|
|
17
|
+
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Collapsible, CollapsibleContent, CollapsibleTrigger };
|