@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,70 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ export interface AvatarStackItem {
5
+ src?: string;
6
+ name: string;
7
+ fallback?: string;
8
+ colorClass?: string;
9
+ }
10
+
11
+ interface AvatarStackProps {
12
+ avatars: AvatarStackItem[];
13
+ max?: number;
14
+ size?: "sm" | "md" | "lg";
15
+ className?: string;
16
+ }
17
+
18
+ const sizeMap = {
19
+ sm: { wrapper: "size-6 ring-[1.5px]", text: "caption-xs" },
20
+ md: { wrapper: "size-8 ring-2", text: "caption" },
21
+ lg: { wrapper: "size-10 ring-2", text: "body" },
22
+ };
23
+
24
+ function AvatarStack({ avatars, max, size = "md", className }: AvatarStackProps) {
25
+ const shown = max ? avatars.slice(0, max) : avatars;
26
+ const overflow = max && avatars.length > max ? avatars.length - max : 0;
27
+ const s = sizeMap[size];
28
+
29
+ return (
30
+ <div className={cn("flex -space-x-2", className)}>
31
+ {shown.map((avatar, i) => (
32
+ <span
33
+ key={i}
34
+ title={avatar.name}
35
+ className={cn(
36
+ "relative inline-flex shrink-0 overflow-hidden rounded-full ring-white",
37
+ s.wrapper,
38
+ )}
39
+ >
40
+ {avatar.src ? (
41
+ <img src={avatar.src} alt={avatar.name} className="size-full object-cover" />
42
+ ) : (
43
+ <span
44
+ className={cn(
45
+ "flex size-full items-center justify-center font-medium",
46
+ s.text,
47
+ avatar.colorClass ?? "bg-taupe-200 text-taupe-700",
48
+ )}
49
+ >
50
+ {avatar.fallback ?? avatar.name.slice(0, 2).toUpperCase()}
51
+ </span>
52
+ )}
53
+ </span>
54
+ ))}
55
+ {overflow > 0 && (
56
+ <span
57
+ className={cn(
58
+ "relative inline-flex shrink-0 items-center justify-center rounded-full ring-white bg-taupe-100 font-medium text-taupe-600",
59
+ s.wrapper,
60
+ s.text,
61
+ )}
62
+ >
63
+ +{overflow}
64
+ </span>
65
+ )}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export { AvatarStack };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Avatar as AvatarPrimitive } from "radix-ui";
5
+ import { cn } from "../lib/utils";
6
+
7
+ const sizeMap = {
8
+ xs: "size-5 caption-xs",
9
+ sm: "size-7 caption",
10
+ md: "size-8 body",
11
+ lg: "size-10 body-lg",
12
+ xl: "size-12 subheading",
13
+ } as const;
14
+
15
+ type AvatarSize = keyof typeof sizeMap;
16
+
17
+ interface AvatarProps extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
18
+ size?: AvatarSize;
19
+ }
20
+
21
+ const Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, AvatarProps>(
22
+ ({ className, size = "md", ...props }, ref) => (
23
+ <AvatarPrimitive.Root
24
+ ref={ref}
25
+ className={cn(
26
+ "relative flex shrink-0 overflow-hidden rounded-full",
27
+ sizeMap[size],
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ ),
33
+ );
34
+ Avatar.displayName = "Avatar";
35
+
36
+ const AvatarImage = React.forwardRef<
37
+ React.ElementRef<typeof AvatarPrimitive.Image>,
38
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
39
+ >(({ className, ...props }, ref) => (
40
+ <AvatarPrimitive.Image
41
+ ref={ref}
42
+ className={cn("aspect-square h-full w-full object-cover", className)}
43
+ {...props}
44
+ />
45
+ ));
46
+ AvatarImage.displayName = "AvatarImage";
47
+
48
+ const AvatarFallback = React.forwardRef<
49
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
50
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
51
+ >(({ className, ...props }, ref) => (
52
+ <AvatarPrimitive.Fallback
53
+ ref={ref}
54
+ className={cn(
55
+ "flex h-full w-full items-center justify-center rounded-full bg-taupe-100 body font-semibold text-taupe-600",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ ));
61
+ AvatarFallback.displayName = "AvatarFallback";
62
+
63
+ const statusColorMap = {
64
+ online: "bg-brand-secondary",
65
+ offline: "bg-taupe-300",
66
+ busy: "bg-red-500",
67
+ away: "bg-brand-secondary",
68
+ };
69
+
70
+ const statusSizeMap = {
71
+ xs: "size-1.5 ring-1",
72
+ sm: "size-2 ring-1",
73
+ md: "size-2.5 ring-[1.5px]",
74
+ lg: "size-3 ring-2",
75
+ xl: "size-3.5 ring-2",
76
+ };
77
+
78
+ interface AvatarWithStatusProps extends AvatarProps {
79
+ status?: keyof typeof statusColorMap;
80
+ }
81
+
82
+ const AvatarWithStatus = React.forwardRef<
83
+ React.ElementRef<typeof AvatarPrimitive.Root>,
84
+ AvatarWithStatusProps & { children?: React.ReactNode }
85
+ >(({ status, size = "md", className, children, ...props }, ref) => (
86
+ <div className="relative inline-flex shrink-0">
87
+ <Avatar ref={ref} size={size} className={className} {...props}>
88
+ {children}
89
+ </Avatar>
90
+ {status && (
91
+ <span
92
+ className={cn(
93
+ "absolute bottom-0 right-0 rounded-full ring-white",
94
+ statusColorMap[status],
95
+ statusSizeMap[size],
96
+ )}
97
+ />
98
+ )}
99
+ </div>
100
+ ));
101
+ AvatarWithStatus.displayName = "AvatarWithStatus";
102
+
103
+ export { Avatar, AvatarImage, AvatarFallback, AvatarWithStatus };
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 caption font-medium transition-colors",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-taupe-900 text-white",
11
+ secondary: "bg-taupe-100 text-taupe-700",
12
+ outline: "border border-taupe-200 text-taupe-600 bg-transparent",
13
+ brand: "bg-brand-primary text-white",
14
+ success: "bg-brand-secondary/20 text-taupe-800 border border-brand-secondary/50",
15
+ warning: "bg-brand-secondary/70 text-taupe-700 border border-brand-secondary",
16
+ destructive: "bg-red-50 text-red-700 border border-red-200",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ },
23
+ );
24
+
25
+ interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
26
+
27
+ function Badge({ className, variant, ...props }: BadgeProps) {
28
+ return (
29
+ <span data-slot="badge" className={cn(badgeVariants({ variant, className }))} {...props} />
30
+ );
31
+ }
32
+
33
+ export { Badge, badgeVariants };
@@ -0,0 +1,76 @@
1
+ import * as React from "react";
2
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ const Breadcrumb = React.forwardRef<
6
+ HTMLElement,
7
+ React.ComponentProps<"nav"> & { separator?: React.ReactNode }
8
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
9
+ Breadcrumb.displayName = "Breadcrumb";
10
+
11
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentProps<"ol">>(
12
+ ({ className, ...props }, ref) => (
13
+ <ol
14
+ ref={ref}
15
+ className={cn("flex flex-wrap items-center gap-1.5 body text-taupe-500", className)}
16
+ {...props}
17
+ />
18
+ ),
19
+ );
20
+ BreadcrumbList.displayName = "BreadcrumbList";
21
+
22
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
23
+ ({ className, ...props }, ref) => (
24
+ <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
25
+ ),
26
+ );
27
+ BreadcrumbItem.displayName = "BreadcrumbItem";
28
+
29
+ const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, React.ComponentProps<"a">>(
30
+ ({ className, ...props }, ref) => (
31
+ <a ref={ref} className={cn("transition-colors hover:text-taupe-900", className)} {...props} />
32
+ ),
33
+ );
34
+ BreadcrumbLink.displayName = "BreadcrumbLink";
35
+
36
+ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentProps<"span">>(
37
+ ({ className, ...props }, ref) => (
38
+ <span
39
+ ref={ref}
40
+ role="link"
41
+ aria-current="page"
42
+ className={cn("body font-medium text-taupe-900", className)}
43
+ {...props}
44
+ />
45
+ ),
46
+ );
47
+ BreadcrumbPage.displayName = "BreadcrumbPage";
48
+
49
+ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
50
+ <li role="presentation" aria-hidden="true" className={cn("text-taupe-300", className)} {...props}>
51
+ {children ?? <ChevronRight size={14} />}
52
+ </li>
53
+ );
54
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
55
+
56
+ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
57
+ <span
58
+ role="presentation"
59
+ aria-hidden="true"
60
+ className={cn("flex size-5 items-center justify-center", className)}
61
+ {...props}
62
+ >
63
+ <MoreHorizontal size={14} className="text-taupe-400" />
64
+ </span>
65
+ );
66
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
67
+
68
+ export {
69
+ Breadcrumb,
70
+ BreadcrumbList,
71
+ BreadcrumbItem,
72
+ BreadcrumbLink,
73
+ BreadcrumbPage,
74
+ BreadcrumbSeparator,
75
+ BreadcrumbEllipsis,
76
+ };
@@ -0,0 +1,45 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ interface ButtonGroupProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ function ButtonGroup({ children, className }: ButtonGroupProps) {
10
+ return (
11
+ <div
12
+ className={cn(
13
+ "inline-flex divide-x divide-taupe-200 overflow-hidden rounded-lg border border-taupe-200 bg-white shadow-natural",
14
+ className,
15
+ )}
16
+ >
17
+ {children}
18
+ </div>
19
+ );
20
+ }
21
+
22
+ interface ButtonGroupItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
23
+ active?: boolean;
24
+ }
25
+
26
+ function ButtonGroupItem({ active, className, children, ...props }: ButtonGroupItemProps) {
27
+ return (
28
+ <button
29
+ type="button"
30
+ className={cn(
31
+ "relative inline-flex items-center gap-1.5 px-3.5 py-2 body font-medium transition-colors",
32
+ "focus:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-taupe-400/40 focus-visible:ring-inset",
33
+ active
34
+ ? "bg-taupe-900 text-white"
35
+ : "text-taupe-600 hover:bg-taupe-50 hover:text-taupe-900",
36
+ className,
37
+ )}
38
+ {...props}
39
+ >
40
+ {children}
41
+ </button>
42
+ );
43
+ }
44
+
45
+ export { ButtonGroup, ButtonGroupItem };
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
4
+ import { cn } from "../lib/utils";
5
+
6
+ const buttonVariants = cva(
7
+ [
8
+ "inline-flex shrink-0 items-center justify-center gap-2",
9
+ "body font-medium whitespace-nowrap",
10
+ "transition-all duration-150 outline-none",
11
+ "focus-visible:ring-2 focus-visible:ring-brand-primary/30 focus-visible:ring-offset-1",
12
+ "disabled:pointer-events-none disabled:opacity-50",
13
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
14
+ ],
15
+ {
16
+ variants: {
17
+ variant: {
18
+ default: "bg-brand-primary text-white hover:bg-brand-primary/90 active:bg-brand-primary/80",
19
+ destructive: "bg-red-600 text-white hover:bg-red-700 active:bg-red-800",
20
+ outline: "bg-white text-taupe-700 shadow-natural hover:bg-taupe-50 rounded-xl",
21
+ secondary: "bg-taupe-100 text-taupe-700 hover:bg-taupe-200",
22
+ brand:
23
+ "bg-brand-secondary text-taupe-900 hover:bg-brand-secondary/90 active:bg-brand-secondary/80",
24
+ ghost: "text-taupe-600 hover:bg-taupe-100 hover:text-taupe-900",
25
+ link: "text-taupe-900 underline-offset-4 hover:underline",
26
+ },
27
+ size: {
28
+ default: "h-9 px-4 py-2 rounded-md",
29
+ xs: "h-6 gap-1 px-2 caption rounded-md",
30
+ sm: "h-8 gap-1.5 px-3 rounded-md",
31
+ lg: "h-10 px-6 rounded-md",
32
+ icon: "size-9 rounded-md",
33
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
34
+ "icon-sm": "size-8 rounded-md",
35
+ "icon-lg": "size-10 rounded-md",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: "default",
40
+ size: "default",
41
+ },
42
+ },
43
+ );
44
+
45
+ interface ButtonProps extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
46
+ asChild?: boolean;
47
+ }
48
+
49
+ function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
50
+ const Comp = asChild ? Slot.Root : "button";
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Button, buttonVariants };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { DayPicker } from "react-day-picker";
5
+ import { ChevronLeft, ChevronRight } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
9
+
10
+ function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
11
+ return (
12
+ <DayPicker
13
+ showOutsideDays={showOutsideDays}
14
+ className={cn("p-3", className)}
15
+ classNames={{
16
+ months: "flex flex-col sm:flex-row gap-4",
17
+ month: "flex flex-col gap-4",
18
+ month_caption: "flex justify-center pt-1 relative items-center px-10",
19
+ caption_label: "body font-semibold text-taupe-900",
20
+ nav: "flex items-center justify-between absolute inset-x-0 top-1",
21
+ button_previous: cn(
22
+ "absolute left-0 h-7 w-7 flex items-center justify-center rounded-md",
23
+ "text-taupe-400 hover:bg-taupe-100 hover:text-taupe-700 transition-colors",
24
+ ),
25
+ button_next: cn(
26
+ "absolute right-0 h-7 w-7 flex items-center justify-center rounded-md",
27
+ "text-taupe-400 hover:bg-taupe-100 hover:text-taupe-700 transition-colors",
28
+ ),
29
+ month_grid: "w-full border-collapse mt-1",
30
+ weekdays: "flex",
31
+ weekday: "w-9 h-9 flex items-center justify-center label text-taupe-400 font-normal",
32
+ week: "flex w-full mt-1",
33
+ day: "relative p-0 text-center",
34
+ day_button: cn(
35
+ "h-9 w-9 flex items-center justify-center rounded-full body font-normal",
36
+ "text-taupe-900 hover:bg-taupe-100 transition-colors",
37
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary/30",
38
+ ),
39
+ selected:
40
+ "[&>button]:bg-brand-primary [&>button]:text-white [&>button]:hover:bg-brand-primary/90",
41
+ today: "[&>button]:font-semibold [&>button]:text-brand-primary",
42
+ outside: "opacity-40 [&>button]:text-taupe-400",
43
+ disabled: "opacity-30 [&>button]:cursor-not-allowed",
44
+ range_start: "[&>button]:bg-brand-primary [&>button]:text-white [&>button]:rounded-full",
45
+ range_end: "[&>button]:bg-brand-primary [&>button]:text-white [&>button]:rounded-full",
46
+ range_middle:
47
+ "[&>button]:bg-brand-primary/15 [&>button]:text-taupe-900 [&>button]:rounded-none",
48
+ hidden: "invisible",
49
+ ...classNames,
50
+ }}
51
+ components={{
52
+ Chevron: ({ orientation }) =>
53
+ orientation === "left" ? <ChevronLeft size={14} /> : <ChevronRight size={14} />,
54
+ }}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+ Calendar.displayName = "Calendar";
60
+
61
+ export { Calendar };
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { Button } from "./button";
4
+
5
+ interface CardHeadingAction {
6
+ label: string;
7
+ href?: string;
8
+ onClick?: () => void;
9
+ variant?: "default" | "outline" | "ghost" | "destructive";
10
+ }
11
+
12
+ interface CardHeadingProps {
13
+ title: string;
14
+ description?: string;
15
+ actions?: CardHeadingAction[];
16
+ children?: React.ReactNode; // e.g. avatar or icon on the left
17
+ className?: string;
18
+ }
19
+
20
+ function CardHeading({ title, description, actions, children, className }: CardHeadingProps) {
21
+ return (
22
+ <div
23
+ className={cn(
24
+ "flex items-start justify-between gap-4 border-b border-taupe-100 px-5 py-4",
25
+ className,
26
+ )}
27
+ >
28
+ <div className="flex min-w-0 items-center gap-3">
29
+ {children}
30
+ <div className="min-w-0">
31
+ <p className="body font-semibold text-taupe-900 truncate">{title}</p>
32
+ {description && <p className="caption text-taupe-400 mt-0.5">{description}</p>}
33
+ </div>
34
+ </div>
35
+
36
+ {actions && actions.length > 0 && (
37
+ <div className="flex shrink-0 items-center gap-2">
38
+ {actions.map((action, i) =>
39
+ action.href ? (
40
+ <Button key={i} size="sm" variant={action.variant ?? "default"} asChild>
41
+ <a href={action.href}>{action.label}</a>
42
+ </Button>
43
+ ) : (
44
+ <Button
45
+ key={i}
46
+ size="sm"
47
+ variant={action.variant ?? "default"}
48
+ onClick={action.onClick}
49
+ >
50
+ {action.label}
51
+ </Button>
52
+ ),
53
+ )}
54
+ </div>
55
+ )}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ export { CardHeading };
@@ -0,0 +1,120 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../lib/utils";
4
+
5
+ /* ------------------------------------------------------------------ */
6
+ /* Card root */
7
+ /* ------------------------------------------------------------------ */
8
+
9
+ const cardVariants = cva("rounded-xl", {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-white shadow-natural",
13
+ muted: "bg-taupe-50",
14
+ ghost: "bg-transparent",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ });
21
+
22
+ interface CardProps extends React.ComponentProps<"div">, VariantProps<typeof cardVariants> {}
23
+
24
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(
25
+ ({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ data-slot="card"
29
+ className={cn(cardVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ),
33
+ );
34
+ Card.displayName = "Card";
35
+
36
+ /* ------------------------------------------------------------------ */
37
+ /* Card sub-components */
38
+ /* ------------------------------------------------------------------ */
39
+
40
+ const CardHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
41
+ ({ className, ...props }, ref) => (
42
+ <div
43
+ ref={ref}
44
+ data-slot="card-header"
45
+ className={cn("flex flex-col gap-1.5 p-5", className)}
46
+ {...props}
47
+ />
48
+ ),
49
+ );
50
+ CardHeader.displayName = "CardHeader";
51
+
52
+ const CardTitle = React.forwardRef<HTMLHeadingElement, React.ComponentProps<"h3">>(
53
+ ({ className, ...props }, ref) => (
54
+ <h3
55
+ ref={ref}
56
+ data-slot="card-title"
57
+ className={cn("subheading text-taupe-900", className)}
58
+ {...props}
59
+ />
60
+ ),
61
+ );
62
+ CardTitle.displayName = "CardTitle";
63
+
64
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.ComponentProps<"p">>(
65
+ ({ className, ...props }, ref) => (
66
+ <p
67
+ ref={ref}
68
+ data-slot="card-description"
69
+ className={cn("caption text-taupe-500", className)}
70
+ {...props}
71
+ />
72
+ ),
73
+ );
74
+ CardDescription.displayName = "CardDescription";
75
+
76
+ const CardContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
77
+ ({ className, ...props }, ref) => (
78
+ <div ref={ref} data-slot="card-content" className={cn("p-5 pt-0", className)} {...props} />
79
+ ),
80
+ );
81
+ CardContent.displayName = "CardContent";
82
+
83
+ const CardFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
84
+ ({ className, ...props }, ref) => (
85
+ <div
86
+ ref={ref}
87
+ data-slot="card-footer"
88
+ className={cn("flex items-center p-5 pt-0", className)}
89
+ {...props}
90
+ />
91
+ ),
92
+ );
93
+ CardFooter.displayName = "CardFooter";
94
+
95
+ /* ------------------------------------------------------------------ */
96
+ /* CardMedia — full-bleed image/gradient header */
97
+ /* ------------------------------------------------------------------ */
98
+
99
+ const CardMedia = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
100
+ ({ className, ...props }, ref) => (
101
+ <div
102
+ ref={ref}
103
+ data-slot="card-media"
104
+ className={cn("relative overflow-hidden", className)}
105
+ {...props}
106
+ />
107
+ ),
108
+ );
109
+ CardMedia.displayName = "CardMedia";
110
+
111
+ export {
112
+ Card,
113
+ CardHeader,
114
+ CardTitle,
115
+ CardDescription,
116
+ CardContent,
117
+ CardFooter,
118
+ CardMedia,
119
+ cardVariants,
120
+ };