@hexclave/dashboard-ui-components 1.0.3 → 1.0.6

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 (98) hide show
  1. package/dist/components/alert.js +4 -4
  2. package/dist/components/alert.js.map +1 -1
  3. package/dist/components/badge.js +1 -1
  4. package/dist/components/badge.js.map +1 -1
  5. package/dist/components/button.d.ts +4 -4
  6. package/dist/components/button.js +3 -3
  7. package/dist/components/button.js.map +1 -1
  8. package/dist/components/data-grid/data-grid-toolbar.js +5 -5
  9. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
  10. package/dist/components/data-grid/data-grid.d.ts.map +1 -1
  11. package/dist/components/data-grid/data-grid.js +11 -5
  12. package/dist/components/data-grid/data-grid.js.map +1 -1
  13. package/dist/components/data-grid/data-grid.test.js +34 -1
  14. package/dist/components/data-grid/data-grid.test.js.map +1 -1
  15. package/dist/components/dialog.d.ts +6 -2
  16. package/dist/components/dialog.d.ts.map +1 -1
  17. package/dist/components/dialog.js +14 -10
  18. package/dist/components/dialog.js.map +1 -1
  19. package/dist/components/input.js +12 -9
  20. package/dist/components/input.js.map +1 -1
  21. package/dist/components/pill-toggle.js +3 -3
  22. package/dist/components/pill-toggle.js.map +1 -1
  23. package/dist/components/tabs.js +2 -2
  24. package/dist/components/tabs.js.map +1 -1
  25. package/dist/dashboard-ui-components.global.js +15639 -17400
  26. package/dist/dashboard-ui-components.global.js.map +4 -4
  27. package/dist/esm/components/alert.js +4 -4
  28. package/dist/esm/components/alert.js.map +1 -1
  29. package/dist/esm/components/badge.js +1 -1
  30. package/dist/esm/components/badge.js.map +1 -1
  31. package/dist/esm/components/button.d.ts +4 -4
  32. package/dist/esm/components/button.js +3 -3
  33. package/dist/esm/components/button.js.map +1 -1
  34. package/dist/esm/components/data-grid/data-grid-toolbar.js +5 -5
  35. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
  36. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/dist/esm/components/data-grid/data-grid.js +11 -5
  38. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/dist/esm/components/data-grid/data-grid.test.js +34 -1
  40. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
  41. package/dist/esm/components/dialog.d.ts +6 -2
  42. package/dist/esm/components/dialog.d.ts.map +1 -1
  43. package/dist/esm/components/dialog.js +13 -11
  44. package/dist/esm/components/dialog.js.map +1 -1
  45. package/dist/esm/components/input.js +12 -9
  46. package/dist/esm/components/input.js.map +1 -1
  47. package/dist/esm/components/pill-toggle.js +4 -4
  48. package/dist/esm/components/pill-toggle.js.map +1 -1
  49. package/dist/esm/components/tabs.js +2 -2
  50. package/dist/esm/components/tabs.js.map +1 -1
  51. package/dist/esm/index.d.ts +2 -2
  52. package/dist/esm/index.js +2 -2
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +12 -0
  55. package/package.json +4 -3
  56. package/src/components/alert.tsx +120 -0
  57. package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
  58. package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
  59. package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
  60. package/src/components/analytics-chart/format.ts +101 -0
  61. package/src/components/analytics-chart/index.ts +75 -0
  62. package/src/components/analytics-chart/palette.ts +68 -0
  63. package/src/components/analytics-chart/render-data-series.tsx +169 -0
  64. package/src/components/analytics-chart/state.ts +165 -0
  65. package/src/components/analytics-chart/strings.ts +72 -0
  66. package/src/components/analytics-chart/types.ts +220 -0
  67. package/src/components/badge.tsx +108 -0
  68. package/src/components/button.tsx +104 -0
  69. package/src/components/card.tsx +263 -0
  70. package/src/components/chart-card.tsx +101 -0
  71. package/src/components/chart-container.tsx +155 -0
  72. package/src/components/chart-legend.tsx +65 -0
  73. package/src/components/chart-theme.tsx +52 -0
  74. package/src/components/chart-tooltip.tsx +165 -0
  75. package/src/components/cursor-blast-effect.tsx +334 -0
  76. package/src/components/data-grid/data-grid-sizing.ts +51 -0
  77. package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
  78. package/src/components/data-grid/data-grid.test.tsx +407 -0
  79. package/src/components/data-grid/data-grid.tsx +1231 -0
  80. package/src/components/data-grid/index.ts +64 -0
  81. package/src/components/data-grid/state.ts +235 -0
  82. package/src/components/data-grid/strings.ts +45 -0
  83. package/src/components/data-grid/types.ts +401 -0
  84. package/src/components/data-grid/use-data-source.ts +413 -0
  85. package/src/components/data-grid/use-url-state.test.tsx +88 -0
  86. package/src/components/data-grid/use-url-state.ts +298 -0
  87. package/src/components/dialog.tsx +218 -0
  88. package/src/components/edit-mode.tsx +17 -0
  89. package/src/components/empty-state.tsx +64 -0
  90. package/src/components/input.tsx +97 -0
  91. package/src/components/metric-card.tsx +142 -0
  92. package/src/components/pill-toggle.tsx +159 -0
  93. package/src/components/progress-bar.tsx +82 -0
  94. package/src/components/separator.tsx +36 -0
  95. package/src/components/skeleton.tsx +30 -0
  96. package/src/components/table.tsx +113 -0
  97. package/src/components/tabs.tsx +217 -0
  98. package/src/index.ts +71 -0
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hexclave/ui";
4
+ import React from "react";
5
+
6
+ type DesignMetricCardGradient = "blue" | "cyan" | "purple" | "green" | "orange" | "default";
7
+
8
+ const hoverTintClasses = new Map<DesignMetricCardGradient, string>([
9
+ ["blue", "group-hover:bg-blue-500/[0.03]"],
10
+ ["cyan", "group-hover:bg-cyan-500/[0.03]"],
11
+ ["purple", "group-hover:bg-purple-500/[0.03]"],
12
+ ["green", "group-hover:bg-emerald-500/[0.03]"],
13
+ ["orange", "group-hover:bg-orange-500/[0.03]"],
14
+ ["default", "group-hover:bg-slate-500/[0.02]"],
15
+ ]);
16
+
17
+ export type DesignMetricCardTrend = {
18
+ value: number,
19
+ direction: "up" | "down",
20
+ label?: string,
21
+ };
22
+
23
+ export type DesignMetricCardProps = {
24
+ label: string,
25
+ value: string | number,
26
+ description?: string,
27
+ trend?: DesignMetricCardTrend,
28
+ icon?: React.ElementType,
29
+ gradient?: DesignMetricCardGradient,
30
+ } & Omit<React.ComponentProps<"div">, "children">;
31
+
32
+ /**
33
+ * KPI card for big-number metrics (users, revenue, signups, etc.).
34
+ * Use this instead of a plain `DesignCard` when the value is the focal point.
35
+ *
36
+ * ```tsx
37
+ * <DesignMetricCard
38
+ * label="Total Users"
39
+ * value="1,234"
40
+ * description="+12% from last month"
41
+ * trend={{ direction: "up", value: 12, label: "vs. last period" }}
42
+ * icon={UsersIcon}
43
+ * gradient="blue"
44
+ * />
45
+ * ```
46
+ *
47
+ * Notes:
48
+ * - `label` is the short caption (NOT `title`).
49
+ * - `value` can be a pre-formatted string ("1,234") or a number — prefer
50
+ * strings so you control the format.
51
+ * - `description` is the subline (NOT `subtitle`).
52
+ * - `trend.value` is a NUMBER, not a pre-formatted "12%" string. The card
53
+ * renders the arrow and formatting.
54
+ * - `icon` is a component type (e.g. `UsersIcon`), not a rendered node.
55
+ */
56
+ export function DesignMetricCard({
57
+ label,
58
+ value,
59
+ description,
60
+ trend,
61
+ icon: Icon,
62
+ gradient = "default",
63
+ className,
64
+ ...props
65
+ }: DesignMetricCardProps) {
66
+ const hoverTintClass = hoverTintClasses.get(gradient) ?? "group-hover:bg-slate-500/[0.02]";
67
+
68
+ return (
69
+ <div
70
+ className={cn(
71
+ "group relative rounded-2xl bg-white/90 dark:bg-background/60 backdrop-blur-xl transition-all duration-150 hover:transition-none overflow-hidden",
72
+ "ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1]",
73
+ "shadow-sm hover:shadow-md",
74
+ className
75
+ )}
76
+ {...props}
77
+ >
78
+ <div className="absolute inset-0 bg-gradient-to-br from-foreground/[0.04] dark:from-foreground/[0.02] to-transparent pointer-events-none rounded-2xl" />
79
+ <div
80
+ className={cn(
81
+ "absolute inset-0 transition-colors duration-150 group-hover:transition-none pointer-events-none rounded-2xl",
82
+ hoverTintClass
83
+ )}
84
+ />
85
+ <div className="relative p-5">
86
+ <div className="flex items-start justify-between gap-3">
87
+ <div className="flex-1 min-w-0">
88
+ <div className="flex items-center gap-2">
89
+ {Icon && (
90
+ <div className="p-1.5 rounded-lg bg-foreground/[0.06] dark:bg-foreground/[0.04]">
91
+ <Icon className="h-3.5 w-3.5 text-foreground/70 dark:text-muted-foreground" />
92
+ </div>
93
+ )}
94
+ <span className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
95
+ {label}
96
+ </span>
97
+ </div>
98
+ <div className="mt-3 flex items-baseline gap-2">
99
+ <span className="text-3xl font-bold tabular-nums text-foreground">
100
+ {typeof value === "number" ? value.toLocaleString() : value}
101
+ </span>
102
+ {trend && (
103
+ <span
104
+ className={cn(
105
+ "inline-flex items-center gap-0.5 text-xs font-medium",
106
+ trend.direction === "up"
107
+ ? "text-emerald-600 dark:text-emerald-400"
108
+ : "text-red-600 dark:text-red-400"
109
+ )}
110
+ >
111
+ <svg
112
+ className={cn("h-3 w-3", trend.direction === "down" && "rotate-180")}
113
+ viewBox="0 0 12 12"
114
+ fill="none"
115
+ xmlns="http://www.w3.org/2000/svg"
116
+ >
117
+ <path
118
+ d="M6 2.5V9.5M6 2.5L3 5.5M6 2.5L9 5.5"
119
+ stroke="currentColor"
120
+ strokeWidth="1.5"
121
+ strokeLinecap="round"
122
+ strokeLinejoin="round"
123
+ />
124
+ </svg>
125
+ {trend.value}%
126
+ {trend.label && (
127
+ <span className="text-muted-foreground ml-0.5">{trend.label}</span>
128
+ )}
129
+ </span>
130
+ )}
131
+ </div>
132
+ {description && (
133
+ <p className="mt-1 text-xs text-muted-foreground">
134
+ {description}
135
+ </p>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { cn, Spinner, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@hexclave/ui";
5
+ import { TooltipPortal } from "@radix-ui/react-tooltip";
6
+ import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises";
7
+ import { useGlassmorphicDefault } from "./card";
8
+
9
+ type DesignPillToggleSize = "sm" | "md" | "lg";
10
+ type DesignPillToggleGradient = "blue" | "cyan" | "purple" | "green" | "orange" | "default";
11
+
12
+ export type DesignPillToggleOption = {
13
+ id: string,
14
+ label: string,
15
+ icon?: React.ElementType,
16
+ };
17
+
18
+ export type DesignPillToggleProps = {
19
+ options: DesignPillToggleOption[],
20
+ selected: string,
21
+ onSelect: (id: string) => void | Promise<void>,
22
+ size?: DesignPillToggleSize,
23
+ glassmorphic?: boolean,
24
+ gradient?: DesignPillToggleGradient,
25
+ /** When false, hides labels and shows a tooltip on hover instead. Defaults to true. */
26
+ showLabels?: boolean,
27
+ className?: string,
28
+ };
29
+
30
+ type SizeClass = {
31
+ button: string,
32
+ icon: string,
33
+ };
34
+
35
+ const sizeClasses = new Map<DesignPillToggleSize, SizeClass>([
36
+ ["sm", { button: "px-3 py-1.5 text-xs", icon: "h-3.5 w-3.5" }],
37
+ ["md", { button: "px-4 py-2 text-sm", icon: "h-4 w-4" }],
38
+ ["lg", { button: "px-5 py-2.5 text-sm", icon: "h-4 w-4" }],
39
+ ]);
40
+
41
+ const gradientClasses = new Map<DesignPillToggleGradient, string>([
42
+ ["blue", "ring-blue-500/20 dark:ring-blue-400/20"],
43
+ ["cyan", "ring-cyan-500/20 dark:ring-cyan-400/20"],
44
+ ["purple", "ring-purple-500/20 dark:ring-purple-400/20"],
45
+ ["green", "ring-emerald-500/20 dark:ring-emerald-400/20"],
46
+ ["orange", "ring-amber-500/20 dark:ring-amber-400/20"],
47
+ ["default", "ring-black/[0.12] dark:ring-white/[0.06]"],
48
+ ]);
49
+
50
+ function getMapValueOrThrow<TKey, TValue>(map: Map<TKey, TValue>, key: TKey, mapName: string) {
51
+ const value = map.get(key);
52
+ if (!value) {
53
+ throw new Error(`Missing ${mapName} entry for key "${String(key)}"`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ export function DesignPillToggle({
59
+ options,
60
+ selected,
61
+ onSelect,
62
+ size = "md",
63
+ glassmorphic: glassmorphicProp,
64
+ gradient = "default",
65
+ showLabels = true,
66
+ className,
67
+ }: DesignPillToggleProps) {
68
+ const glassmorphic = useGlassmorphicDefault(glassmorphicProp);
69
+ const sizeClass = getMapValueOrThrow(sizeClasses, size, "sizeClasses");
70
+ const activeRingClass = getMapValueOrThrow(gradientClasses, gradient, "gradientClasses");
71
+
72
+ const [loadingOptionId, setLoadingOptionId] = useState<string | null>(null);
73
+
74
+ const handleClick = (optionId: string) => {
75
+ const result = onSelect(optionId);
76
+ if (result && typeof (result as Promise<void>).then === "function") {
77
+ setLoadingOptionId(optionId);
78
+ runAsynchronouslyWithAlert(
79
+ Promise.resolve(result).finally(() => setLoadingOptionId(null))
80
+ );
81
+ }
82
+ };
83
+
84
+ return (
85
+ <TooltipProvider>
86
+ <div
87
+ className={cn(
88
+ "inline-flex items-center gap-1 p-1 rounded-xl",
89
+ glassmorphic
90
+ ? "bg-foreground/[0.04] backdrop-blur-sm"
91
+ : "bg-black/[0.08] dark:bg-white/[0.04]",
92
+ className
93
+ )}
94
+ >
95
+ {options.map((option) => {
96
+ const isActive = selected === option.id;
97
+ const Icon = option.icon;
98
+
99
+ const pill = (
100
+ <button
101
+ type="button"
102
+ key={option.id}
103
+ onClick={() => handleClick(option.id)}
104
+ disabled={loadingOptionId !== null}
105
+ className={cn(
106
+ "relative flex items-center gap-2 font-medium rounded-lg transition-all duration-150 hover:transition-none",
107
+ sizeClass.button,
108
+ isActive
109
+ ? cn(
110
+ "bg-white dark:bg-background text-foreground shadow-sm ring-1",
111
+ glassmorphic
112
+ ? "ring-foreground/[0.06] dark:bg-[hsl(240,71%,70%)]/10 dark:text-[hsl(240,71%,90%)] dark:ring-[hsl(240,71%,70%)]/20"
113
+ : activeRingClass
114
+ )
115
+ : cn(
116
+ "text-muted-foreground hover:text-foreground",
117
+ glassmorphic
118
+ ? "hover:bg-background/50"
119
+ : "hover:bg-black/[0.06] dark:hover:bg-white/[0.04]"
120
+ )
121
+ )}
122
+ >
123
+ {loadingOptionId === option.id && (
124
+ <Spinner
125
+ size={12}
126
+ className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
127
+ />
128
+ )}
129
+ <span className={cn(
130
+ "flex items-center gap-2",
131
+ loadingOptionId === option.id && "invisible"
132
+ )}>
133
+ {Icon && <Icon className={sizeClass.icon} />}
134
+ {showLabels && option.label}
135
+ </span>
136
+ </button>
137
+ );
138
+
139
+ if (!showLabels) {
140
+ return (
141
+ <Tooltip key={option.id} delayDuration={0}>
142
+ <TooltipTrigger asChild>
143
+ {pill}
144
+ </TooltipTrigger>
145
+ <TooltipPortal>
146
+ <TooltipContent side="top">
147
+ {option.label}
148
+ </TooltipContent>
149
+ </TooltipPortal>
150
+ </Tooltip>
151
+ );
152
+ }
153
+
154
+ return pill;
155
+ })}
156
+ </div>
157
+ </TooltipProvider>
158
+ );
159
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hexclave/ui";
4
+
5
+ type DesignProgressBarGradient = "blue" | "cyan" | "purple" | "green" | "orange" | "default";
6
+
7
+ const fillClasses = new Map<DesignProgressBarGradient, string>([
8
+ ["blue", "bg-blue-500 dark:bg-blue-400"],
9
+ ["cyan", "bg-cyan-500 dark:bg-cyan-400"],
10
+ ["purple", "bg-purple-500 dark:bg-purple-400"],
11
+ ["green", "bg-emerald-500 dark:bg-emerald-400"],
12
+ ["orange", "bg-amber-500 dark:bg-amber-400"],
13
+ ["default", "bg-foreground/60"],
14
+ ]);
15
+
16
+ export type DesignProgressBarProps = {
17
+ value: number,
18
+ max?: number,
19
+ gradient?: DesignProgressBarGradient,
20
+ label?: string,
21
+ showPercentage?: boolean,
22
+ size?: "sm" | "md" | "lg",
23
+ className?: string,
24
+ };
25
+
26
+ /**
27
+ * Horizontal progress bar for quota / fill-level indicators. Takes a
28
+ * `value` and optional `max` (defaults to 100 — pass raw values, the
29
+ * component computes the percentage). Optional label + percentage display.
30
+ *
31
+ * ```tsx
32
+ * <DesignProgressBar value={usedSeats} max={totalSeats} label="Seats used" showPercentage />
33
+ * <DesignProgressBar value={75} gradient="green" />
34
+ * ```
35
+ */
36
+ export function DesignProgressBar({
37
+ value,
38
+ max = 100,
39
+ gradient = "default",
40
+ label,
41
+ showPercentage = false,
42
+ size = "md",
43
+ className,
44
+ }: DesignProgressBarProps) {
45
+ const percentage = max > 0 ? Math.min(Math.max((value / max) * 100, 0), 100) : 0;
46
+ const fillClass = fillClasses.get(gradient) ?? "bg-foreground/60";
47
+
48
+ const trackHeight = size === "sm" ? "h-1.5" : size === "lg" ? "h-3" : "h-2";
49
+
50
+ return (
51
+ <div className={cn("w-full", className)}>
52
+ {(label || showPercentage) && (
53
+ <div className="flex items-center justify-between mb-2">
54
+ {label && (
55
+ <span className="text-xs font-medium text-muted-foreground">
56
+ {label}
57
+ </span>
58
+ )}
59
+ {showPercentage && (
60
+ <span className="text-xs font-medium tabular-nums text-foreground">
61
+ {Math.round(percentage)}%
62
+ </span>
63
+ )}
64
+ </div>
65
+ )}
66
+ <div
67
+ className={cn(
68
+ "w-full rounded-full bg-black/[0.06] dark:bg-white/[0.06] overflow-hidden",
69
+ trackHeight
70
+ )}
71
+ >
72
+ <div
73
+ className={cn(
74
+ "h-full rounded-full transition-all duration-300 ease-out",
75
+ fillClass
76
+ )}
77
+ style={{ width: `${percentage}%` }}
78
+ />
79
+ </div>
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hexclave/ui";
4
+
5
+ export type DesignSeparatorProps = {
6
+ orientation?: "horizontal" | "vertical",
7
+ } & React.HTMLAttributes<HTMLDivElement>;
8
+
9
+ /**
10
+ * Thin divider line. Use `orientation="vertical"` inside a flex row to
11
+ * separate inline groups, or omit for a horizontal rule between sections.
12
+ *
13
+ * ```tsx
14
+ * <DesignSeparator />
15
+ * <DesignSeparator orientation="vertical" className="h-6" />
16
+ * ```
17
+ */
18
+ export function DesignSeparator({
19
+ orientation = "horizontal",
20
+ className,
21
+ ...props
22
+ }: DesignSeparatorProps) {
23
+ return (
24
+ <div
25
+ role="separator"
26
+ aria-orientation={orientation}
27
+ className={cn(
28
+ orientation === "horizontal"
29
+ ? "h-[1px] w-full bg-black/[0.08] dark:bg-white/[0.06]"
30
+ : "w-[1px] h-full bg-black/[0.08] dark:bg-white/[0.06]",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { cn } from "@hexclave/ui";
4
+ import type React from "react";
5
+
6
+ export type DesignSkeletonProps = React.HTMLAttributes<HTMLDivElement>;
7
+
8
+ /**
9
+ * Animated placeholder block. Use while data is loading — size it via
10
+ * `className` to match the content it's standing in for.
11
+ *
12
+ * ```tsx
13
+ * <DesignSkeleton className="h-4 w-[200px]" />
14
+ * <DesignSkeleton className="h-24 w-full rounded-lg" />
15
+ * ```
16
+ *
17
+ * Rule: always show a skeleton during initial load, not a spinner or
18
+ * "Loading..." text. Skeletons preserve layout and feel faster.
19
+ */
20
+ export function DesignSkeleton({ className, ...props }: DesignSkeletonProps) {
21
+ return (
22
+ <div
23
+ className={cn(
24
+ "animate-pulse rounded-md bg-black/[0.06] dark:bg-white/[0.06]",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { forwardRefIfNeeded } from "@hexclave/shared/dist/utils/react";
4
+ import { cn } from "@hexclave/ui";
5
+ import React from "react";
6
+
7
+ /**
8
+ * Simple static table — header + rows + cells. Use this for small,
9
+ * read-only lists (< 20 rows, no interaction). For anything that needs
10
+ * sorting, filtering, pagination, or virtualization, use `DataGrid` +
11
+ * `useDataSource` instead.
12
+ *
13
+ * ```tsx
14
+ * <DesignTable>
15
+ * <DesignTableHeader>
16
+ * <DesignTableRow>
17
+ * <DesignTableHead>Name</DesignTableHead>
18
+ * <DesignTableHead>Email</DesignTableHead>
19
+ * </DesignTableRow>
20
+ * </DesignTableHeader>
21
+ * <DesignTableBody>
22
+ * {rows.map(row => (
23
+ * <DesignTableRow key={row.id}>
24
+ * <DesignTableCell>{row.name}</DesignTableCell>
25
+ * <DesignTableCell>{row.email}</DesignTableCell>
26
+ * </DesignTableRow>
27
+ * ))}
28
+ * </DesignTableBody>
29
+ * </DesignTable>
30
+ * ```
31
+ */
32
+ export const DesignTable = forwardRefIfNeeded<
33
+ HTMLTableElement,
34
+ React.HTMLAttributes<HTMLTableElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div className="relative w-full overflow-auto">
37
+ <table
38
+ ref={ref}
39
+ className={cn("w-full caption-bottom text-sm", className)}
40
+ {...props}
41
+ />
42
+ </div>
43
+ ));
44
+ DesignTable.displayName = "DesignTable";
45
+
46
+ export const DesignTableHeader = forwardRefIfNeeded<
47
+ HTMLTableSectionElement,
48
+ React.HTMLAttributes<HTMLTableSectionElement>
49
+ >(({ className, ...props }, ref) => (
50
+ <thead
51
+ ref={ref}
52
+ className={cn("bg-foreground/[0.02] [&_tr]:border-b [&_tr]:border-black/[0.06] dark:[&_tr]:border-white/[0.06]", className)}
53
+ {...props}
54
+ />
55
+ ));
56
+ DesignTableHeader.displayName = "DesignTableHeader";
57
+
58
+ export const DesignTableBody = forwardRefIfNeeded<
59
+ HTMLTableSectionElement,
60
+ React.HTMLAttributes<HTMLTableSectionElement>
61
+ >(({ className, ...props }, ref) => (
62
+ <tbody
63
+ ref={ref}
64
+ className={cn("[&_tr:last-child]:border-0", className)}
65
+ {...props}
66
+ />
67
+ ));
68
+ DesignTableBody.displayName = "DesignTableBody";
69
+
70
+ export const DesignTableRow = forwardRefIfNeeded<
71
+ HTMLTableRowElement,
72
+ React.HTMLAttributes<HTMLTableRowElement>
73
+ >(({ className, ...props }, ref) => (
74
+ <tr
75
+ ref={ref}
76
+ className={cn(
77
+ "border-b border-black/[0.06] dark:border-white/[0.06] transition-colors duration-150 hover:transition-none hover:bg-foreground/[0.04] data-[state=selected]:bg-foreground/[0.06]",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ ));
83
+ DesignTableRow.displayName = "DesignTableRow";
84
+
85
+ export const DesignTableHead = forwardRefIfNeeded<
86
+ HTMLTableCellElement,
87
+ React.ThHTMLAttributes<HTMLTableCellElement>
88
+ >(({ className, ...props }, ref) => (
89
+ <th
90
+ ref={ref}
91
+ className={cn(
92
+ "h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
93
+ className
94
+ )}
95
+ {...props}
96
+ />
97
+ ));
98
+ DesignTableHead.displayName = "DesignTableHead";
99
+
100
+ export const DesignTableCell = forwardRefIfNeeded<
101
+ HTMLTableCellElement,
102
+ React.TdHTMLAttributes<HTMLTableCellElement>
103
+ >(({ className, ...props }, ref) => (
104
+ <td
105
+ ref={ref}
106
+ className={cn(
107
+ "p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
108
+ className
109
+ )}
110
+ {...props}
111
+ />
112
+ ));
113
+ DesignTableCell.displayName = "DesignTableCell";