@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.
- package/dist/components/alert.js +4 -4
- package/dist/components/alert.js.map +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/badge.js.map +1 -1
- package/dist/components/button.d.ts +4 -4
- package/dist/components/button.js +3 -3
- package/dist/components/button.js.map +1 -1
- package/dist/components/data-grid/data-grid-toolbar.js +5 -5
- package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
- package/dist/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/components/data-grid/data-grid.js +11 -5
- package/dist/components/data-grid/data-grid.js.map +1 -1
- package/dist/components/data-grid/data-grid.test.js +34 -1
- package/dist/components/data-grid/data-grid.test.js.map +1 -1
- package/dist/components/dialog.d.ts +6 -2
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +14 -10
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/input.js +12 -9
- package/dist/components/input.js.map +1 -1
- package/dist/components/pill-toggle.js +3 -3
- package/dist/components/pill-toggle.js.map +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tabs.js.map +1 -1
- package/dist/dashboard-ui-components.global.js +15639 -17400
- package/dist/dashboard-ui-components.global.js.map +4 -4
- package/dist/esm/components/alert.js +4 -4
- package/dist/esm/components/alert.js.map +1 -1
- package/dist/esm/components/badge.js +1 -1
- package/dist/esm/components/badge.js.map +1 -1
- package/dist/esm/components/button.d.ts +4 -4
- package/dist/esm/components/button.js +3 -3
- package/dist/esm/components/button.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid-toolbar.js +5 -5
- package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/esm/components/data-grid/data-grid.js +11 -5
- package/dist/esm/components/data-grid/data-grid.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid.test.js +34 -1
- package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
- package/dist/esm/components/dialog.d.ts +6 -2
- package/dist/esm/components/dialog.d.ts.map +1 -1
- package/dist/esm/components/dialog.js +13 -11
- package/dist/esm/components/dialog.js.map +1 -1
- package/dist/esm/components/input.js +12 -9
- package/dist/esm/components/input.js.map +1 -1
- package/dist/esm/components/pill-toggle.js +4 -4
- package/dist/esm/components/pill-toggle.js.map +1 -1
- package/dist/esm/components/tabs.js +2 -2
- package/dist/esm/components/tabs.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -0
- package/package.json +4 -3
- package/src/components/alert.tsx +120 -0
- package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
- package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
- package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
- package/src/components/analytics-chart/format.ts +101 -0
- package/src/components/analytics-chart/index.ts +75 -0
- package/src/components/analytics-chart/palette.ts +68 -0
- package/src/components/analytics-chart/render-data-series.tsx +169 -0
- package/src/components/analytics-chart/state.ts +165 -0
- package/src/components/analytics-chart/strings.ts +72 -0
- package/src/components/analytics-chart/types.ts +220 -0
- package/src/components/badge.tsx +108 -0
- package/src/components/button.tsx +104 -0
- package/src/components/card.tsx +263 -0
- package/src/components/chart-card.tsx +101 -0
- package/src/components/chart-container.tsx +155 -0
- package/src/components/chart-legend.tsx +65 -0
- package/src/components/chart-theme.tsx +52 -0
- package/src/components/chart-tooltip.tsx +165 -0
- package/src/components/cursor-blast-effect.tsx +334 -0
- package/src/components/data-grid/data-grid-sizing.ts +51 -0
- package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
- package/src/components/data-grid/data-grid.test.tsx +407 -0
- package/src/components/data-grid/data-grid.tsx +1231 -0
- package/src/components/data-grid/index.ts +64 -0
- package/src/components/data-grid/state.ts +235 -0
- package/src/components/data-grid/strings.ts +45 -0
- package/src/components/data-grid/types.ts +401 -0
- package/src/components/data-grid/use-data-source.ts +413 -0
- package/src/components/data-grid/use-url-state.test.tsx +88 -0
- package/src/components/data-grid/use-url-state.ts +298 -0
- package/src/components/dialog.tsx +218 -0
- package/src/components/edit-mode.tsx +17 -0
- package/src/components/empty-state.tsx +64 -0
- package/src/components/input.tsx +97 -0
- package/src/components/metric-card.tsx +142 -0
- package/src/components/pill-toggle.tsx +159 -0
- package/src/components/progress-bar.tsx +82 -0
- package/src/components/separator.tsx +36 -0
- package/src/components/skeleton.tsx +30 -0
- package/src/components/table.tsx +113 -0
- package/src/components/tabs.tsx +217 -0
- 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";
|