@viliha/vui-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +73 -0
- package/src/avatar.tsx +33 -0
- package/src/badge.tsx +42 -0
- package/src/button.tsx +70 -0
- package/src/card.tsx +77 -0
- package/src/chart.tsx +144 -0
- package/src/checkbox.tsx +20 -0
- package/src/code.tsx +11 -0
- package/src/confirm-dialog.tsx +59 -0
- package/src/dialog.tsx +137 -0
- package/src/dropdown-menu.tsx +123 -0
- package/src/input.tsx +23 -0
- package/src/menu.tsx +80 -0
- package/src/record-view.tsx +1506 -0
- package/src/select.tsx +122 -0
- package/src/table-io.ts +114 -0
- package/src/table.tsx +100 -0
- package/src/theme.css +296 -0
- package/src/utils.ts +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VILIHA PTE. LTD.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@viliha/vui-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vui UI — a clean, token-driven React admin/CRM component library built on Tailwind CSS v4, shadcn-style patterns, and Radix Icons. Ships as TypeScript source (Just-in-Time), compiled by the consuming app.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Suman Bonakurthi",
|
|
7
|
+
"homepage": "https://github.com/myviliha/vui-starter/tree/main/packages/ui",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/myviliha/vui-starter.git",
|
|
11
|
+
"directory": "packages/ui"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/myviliha/vui-starter/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react",
|
|
18
|
+
"tailwind",
|
|
19
|
+
"tailwindcss",
|
|
20
|
+
"shadcn",
|
|
21
|
+
"radix",
|
|
22
|
+
"admin",
|
|
23
|
+
"dashboard",
|
|
24
|
+
"crm",
|
|
25
|
+
"ui",
|
|
26
|
+
"components",
|
|
27
|
+
"design-system"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": [
|
|
30
|
+
"**/*.css"
|
|
31
|
+
],
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"!src/**/*.test.ts",
|
|
35
|
+
"!src/**/*.test.tsx"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
"./theme.css": "./src/theme.css",
|
|
39
|
+
"./utils": "./src/utils.ts",
|
|
40
|
+
"./*": "./src/*.tsx"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18 || ^19",
|
|
47
|
+
"react-dom": "^18 || ^19"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@radix-ui/react-icons": "^1.3.2",
|
|
51
|
+
"clsx": "^2.1.1",
|
|
52
|
+
"recharts": "^3.9.2",
|
|
53
|
+
"tailwind-merge": "^3.6.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.15.3",
|
|
57
|
+
"@types/react": "19.2.2",
|
|
58
|
+
"@types/react-dom": "19.2.2",
|
|
59
|
+
"eslint": "^9.39.1",
|
|
60
|
+
"react": "^19.2.0",
|
|
61
|
+
"react-dom": "^19.2.0",
|
|
62
|
+
"typescript": "5.9.2",
|
|
63
|
+
"vitest": "^4.1.10",
|
|
64
|
+
"@repo/eslint-config": "0.0.0",
|
|
65
|
+
"@repo/typescript-config": "0.0.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"lint": "eslint . --max-warnings 0",
|
|
69
|
+
"generate:component": "turbo gen react-component",
|
|
70
|
+
"check-types": "tsc --noEmit",
|
|
71
|
+
"test": "vitest run"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/avatar.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const Avatar = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"relative flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-md",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
));
|
|
18
|
+
Avatar.displayName = "Avatar";
|
|
19
|
+
|
|
20
|
+
export const AvatarFallback = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex size-full items-center justify-center bg-muted font-medium text-muted-foreground",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
));
|
|
33
|
+
AvatarFallback.displayName = "AvatarFallback";
|
package/src/badge.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
type BadgeVariant =
|
|
6
|
+
| "default"
|
|
7
|
+
| "secondary"
|
|
8
|
+
| "outline"
|
|
9
|
+
| "muted"
|
|
10
|
+
| "success"
|
|
11
|
+
| "warning"
|
|
12
|
+
| "destructive";
|
|
13
|
+
|
|
14
|
+
const VARIANTS: Record<BadgeVariant, string> = {
|
|
15
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
16
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
17
|
+
outline: "border-border text-foreground",
|
|
18
|
+
muted: "border-transparent bg-muted text-muted-foreground",
|
|
19
|
+
success:
|
|
20
|
+
"border-transparent bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400",
|
|
21
|
+
warning:
|
|
22
|
+
"border-transparent bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-400",
|
|
23
|
+
destructive:
|
|
24
|
+
"border-transparent bg-destructive/10 text-destructive dark:bg-destructive/20",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
28
|
+
variant?: BadgeVariant;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
className={cn(
|
|
35
|
+
"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-normal",
|
|
36
|
+
VARIANTS[variant],
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
type ButtonVariant =
|
|
6
|
+
| "default"
|
|
7
|
+
| "primary"
|
|
8
|
+
| "secondary"
|
|
9
|
+
| "outline"
|
|
10
|
+
| "ghost"
|
|
11
|
+
| "link"
|
|
12
|
+
| "destructive";
|
|
13
|
+
type ButtonSize = "default" | "sm" | "lg" | "icon";
|
|
14
|
+
|
|
15
|
+
const BASE =
|
|
16
|
+
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
|
|
17
|
+
|
|
18
|
+
// Standard button: white background + border; on hover only the icon activates
|
|
19
|
+
// (muted → foreground). Shared by default/secondary/outline for one consistent look.
|
|
20
|
+
const NEUTRAL =
|
|
21
|
+
"border border-border bg-background text-foreground [&_svg]:text-muted-foreground hover:[&_svg]:text-foreground";
|
|
22
|
+
|
|
23
|
+
const VARIANTS: Record<ButtonVariant, string> = {
|
|
24
|
+
default: NEUTRAL,
|
|
25
|
+
primary:
|
|
26
|
+
"border border-transparent bg-[var(--button-primary)] text-[var(--button-primary-foreground)] shadow-[var(--button-shadow)] hover:bg-[var(--button-primary-hover)] [&_svg]:border-white/30",
|
|
27
|
+
secondary: NEUTRAL,
|
|
28
|
+
outline: NEUTRAL,
|
|
29
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
30
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
31
|
+
destructive:
|
|
32
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SIZES: Record<ButtonSize, string> = {
|
|
36
|
+
default: "h-8 px-3",
|
|
37
|
+
sm: "h-7 rounded-sm px-2.5",
|
|
38
|
+
lg: "h-9 px-4",
|
|
39
|
+
icon: "size-8",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function buttonVariants({
|
|
43
|
+
variant = "default",
|
|
44
|
+
size = "default",
|
|
45
|
+
className,
|
|
46
|
+
}: {
|
|
47
|
+
variant?: ButtonVariant;
|
|
48
|
+
size?: ButtonSize;
|
|
49
|
+
className?: string;
|
|
50
|
+
} = {}): string {
|
|
51
|
+
return cn(BASE, VARIANTS[variant], SIZES[size], className);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ButtonProps
|
|
55
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
56
|
+
variant?: ButtonVariant;
|
|
57
|
+
size?: ButtonSize;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
61
|
+
({ className, variant, size, type = "button", ...props }, ref) => (
|
|
62
|
+
<button
|
|
63
|
+
ref={ref}
|
|
64
|
+
type={type}
|
|
65
|
+
className={buttonVariants({ variant, size, className })}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
Button.displayName = "Button";
|
package/src/card.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"rounded-lg border border-border bg-card text-card-foreground",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
));
|
|
18
|
+
Card.displayName = "Card";
|
|
19
|
+
|
|
20
|
+
export const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("flex flex-col gap-1 px-5 pb-3 pt-5", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
));
|
|
30
|
+
CardHeader.displayName = "CardHeader";
|
|
31
|
+
|
|
32
|
+
export const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLHeadingElement,
|
|
34
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<h3
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
"font-semibold leading-none tracking-tight",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
));
|
|
45
|
+
CardTitle.displayName = "CardTitle";
|
|
46
|
+
|
|
47
|
+
export const CardDescription = React.forwardRef<
|
|
48
|
+
HTMLParagraphElement,
|
|
49
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
50
|
+
>(({ className, ...props }, ref) => (
|
|
51
|
+
<p
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn("text-muted-foreground", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
));
|
|
57
|
+
CardDescription.displayName = "CardDescription";
|
|
58
|
+
|
|
59
|
+
export const CardContent = React.forwardRef<
|
|
60
|
+
HTMLDivElement,
|
|
61
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
62
|
+
>(({ className, ...props }, ref) => (
|
|
63
|
+
<div ref={ref} className={cn("px-5 pb-5", className)} {...props} />
|
|
64
|
+
));
|
|
65
|
+
CardContent.displayName = "CardContent";
|
|
66
|
+
|
|
67
|
+
export const CardFooter = React.forwardRef<
|
|
68
|
+
HTMLDivElement,
|
|
69
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn("flex items-center px-5 pb-5", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
));
|
|
77
|
+
CardFooter.displayName = "CardFooter";
|
package/src/chart.tsx
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ResponsiveContainer, Tooltip, Legend } from "recharts";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Themed Recharts wrapper for the Vui design system.
|
|
10
|
+
*
|
|
11
|
+
* ponytail: a compact token-driven wrapper, not the full shadcn chart.tsx —
|
|
12
|
+
* covers container + tooltip + legend for area/bar/line/pie demos. Swap in the
|
|
13
|
+
* upstream shadcn `chart` component if you need indicator variants / nameKey.
|
|
14
|
+
*
|
|
15
|
+
* Each `config` entry maps a data key to a label + color. The container exposes
|
|
16
|
+
* every color as a `--color-<key>` CSS variable so series can reference
|
|
17
|
+
* `stroke="var(--color-revenue)"` and stay in sync with the theme tokens
|
|
18
|
+
* (`--chart-1 … --chart-5`).
|
|
19
|
+
*/
|
|
20
|
+
export type ChartConfig = Record<string, { label: string; color: string }>;
|
|
21
|
+
|
|
22
|
+
const ChartConfigContext = React.createContext<ChartConfig>({});
|
|
23
|
+
|
|
24
|
+
export function ChartContainer({
|
|
25
|
+
config,
|
|
26
|
+
className,
|
|
27
|
+
children,
|
|
28
|
+
}: {
|
|
29
|
+
config: ChartConfig;
|
|
30
|
+
className?: string;
|
|
31
|
+
/** A single Recharts chart element (AreaChart, BarChart, PieChart, …). */
|
|
32
|
+
children: React.ReactElement;
|
|
33
|
+
}) {
|
|
34
|
+
const cssVars = Object.fromEntries(
|
|
35
|
+
Object.entries(config).map(([key, v]) => [`--color-${key}`, v.color]),
|
|
36
|
+
) as React.CSSProperties;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ChartConfigContext.Provider value={config}>
|
|
40
|
+
<div
|
|
41
|
+
style={cssVars}
|
|
42
|
+
className={cn(
|
|
43
|
+
"aspect-video w-full text-xs [&_.recharts-cartesian-grid_line]:stroke-border/60 [&_.recharts-text]:fill-muted-foreground",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
48
|
+
{children}
|
|
49
|
+
</ResponsiveContainer>
|
|
50
|
+
</div>
|
|
51
|
+
</ChartConfigContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type TooltipEntry = {
|
|
56
|
+
name?: string;
|
|
57
|
+
value?: number | string;
|
|
58
|
+
color?: string;
|
|
59
|
+
dataKey?: string | number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Styled tooltip. Pass as `content={<ChartTooltipContent />}` to a `<Tooltip>`. */
|
|
63
|
+
export function ChartTooltipContent({
|
|
64
|
+
active,
|
|
65
|
+
payload,
|
|
66
|
+
label,
|
|
67
|
+
}: {
|
|
68
|
+
active?: boolean;
|
|
69
|
+
payload?: TooltipEntry[];
|
|
70
|
+
label?: string | number;
|
|
71
|
+
}) {
|
|
72
|
+
const config = React.useContext(ChartConfigContext);
|
|
73
|
+
if (!active || !payload?.length) return null;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="min-w-32 rounded-md border border-border bg-popover px-2.5 py-2 text-popover-foreground shadow-md">
|
|
77
|
+
{label != null && (
|
|
78
|
+
<p className="mb-1.5 font-medium">{String(label)}</p>
|
|
79
|
+
)}
|
|
80
|
+
<div className="space-y-1">
|
|
81
|
+
{payload.map((item, i) => {
|
|
82
|
+
// Recharts fills `name` with the series `name` (defaults to dataKey)
|
|
83
|
+
// for cartesian charts, and the slice's nameKey for pies.
|
|
84
|
+
const key = String(item.name ?? item.dataKey ?? i);
|
|
85
|
+
const conf = config[key];
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
key={key}
|
|
89
|
+
className="flex items-center justify-between gap-4 text-muted-foreground"
|
|
90
|
+
>
|
|
91
|
+
<span className="flex items-center gap-1.5">
|
|
92
|
+
<span
|
|
93
|
+
className="size-2.5 shrink-0 rounded-[3px]"
|
|
94
|
+
style={{ background: item.color ?? conf?.color }}
|
|
95
|
+
/>
|
|
96
|
+
{conf?.label ?? item.name ?? key}
|
|
97
|
+
</span>
|
|
98
|
+
<span className="font-mono font-medium text-foreground">
|
|
99
|
+
{typeof item.value === "number"
|
|
100
|
+
? item.value.toLocaleString()
|
|
101
|
+
: item.value}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type LegendEntry = { value?: string; color?: string; dataKey?: string | number };
|
|
112
|
+
|
|
113
|
+
/** Styled legend. Pass as `content={<ChartLegendContent />}` to a `<Legend>`. */
|
|
114
|
+
export function ChartLegendContent({ payload }: { payload?: LegendEntry[] }) {
|
|
115
|
+
const config = React.useContext(ChartConfigContext);
|
|
116
|
+
if (!payload?.length) return null;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex flex-wrap items-center justify-center gap-4 pt-3">
|
|
120
|
+
{payload.map((item, i) => {
|
|
121
|
+
// Legend payload uses `value` for the label (series name / pie slice
|
|
122
|
+
// name); a pie gives every entry the same `dataKey` ("value"), so key
|
|
123
|
+
// off `value` (with index as a guaranteed-unique fallback).
|
|
124
|
+
const key = String(item.value ?? item.dataKey ?? i);
|
|
125
|
+
const conf = config[key];
|
|
126
|
+
return (
|
|
127
|
+
<span
|
|
128
|
+
key={`${key}-${i}`}
|
|
129
|
+
className="flex items-center gap-1.5 text-muted-foreground"
|
|
130
|
+
>
|
|
131
|
+
<span
|
|
132
|
+
className="size-2.5 shrink-0 rounded-[3px]"
|
|
133
|
+
style={{ background: item.color ?? conf?.color }}
|
|
134
|
+
/>
|
|
135
|
+
{conf?.label ?? item.value ?? key}
|
|
136
|
+
</span>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Re-export the raw Recharts `Tooltip`/`Legend` so consumers get them from one place. */
|
|
144
|
+
export { Tooltip as ChartTooltip, Legend as ChartLegend };
|
package/src/checkbox.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const Checkbox = React.forwardRef<
|
|
6
|
+
HTMLInputElement,
|
|
7
|
+
React.ComponentProps<"input">
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<input
|
|
10
|
+
ref={ref}
|
|
11
|
+
type="checkbox"
|
|
12
|
+
className={cn(
|
|
13
|
+
"size-4 shrink-0 cursor-pointer rounded border-input accent-[var(--button-primary)]",
|
|
14
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
));
|
|
20
|
+
Checkbox.displayName = "Checkbox";
|
package/src/code.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogBody,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "./dialog";
|
|
12
|
+
|
|
13
|
+
export interface ConfirmDialogProps {
|
|
14
|
+
open: boolean;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: React.ReactNode;
|
|
17
|
+
confirmLabel?: string;
|
|
18
|
+
cancelLabel?: string;
|
|
19
|
+
/** Style the confirm button as a destructive (red) action. */
|
|
20
|
+
destructive?: boolean;
|
|
21
|
+
onConfirm: () => void;
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Centered modal confirmation (e.g. before deleting). Backdrop dims the page;
|
|
26
|
+
Escape / backdrop click / Cancel dismiss it. */
|
|
27
|
+
export function ConfirmDialog({
|
|
28
|
+
open,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
confirmLabel = "Confirm",
|
|
32
|
+
cancelLabel = "Cancel",
|
|
33
|
+
destructive,
|
|
34
|
+
onConfirm,
|
|
35
|
+
onCancel,
|
|
36
|
+
}: ConfirmDialogProps) {
|
|
37
|
+
// Built on the shared Dialog primitive so it stays in lockstep with the
|
|
38
|
+
// header / body / footer standard.
|
|
39
|
+
return (
|
|
40
|
+
<Dialog open={open} onClose={onCancel} label={title} className="max-w-sm">
|
|
41
|
+
<DialogHeader>
|
|
42
|
+
<DialogTitle>{title}</DialogTitle>
|
|
43
|
+
</DialogHeader>
|
|
44
|
+
{description && (
|
|
45
|
+
<DialogBody className="text-muted-foreground">{description}</DialogBody>
|
|
46
|
+
)}
|
|
47
|
+
<DialogFooter>
|
|
48
|
+
<Button onClick={onCancel}>{cancelLabel}</Button>
|
|
49
|
+
<Button
|
|
50
|
+
variant={destructive ? "destructive" : "primary"}
|
|
51
|
+
onClick={onConfirm}
|
|
52
|
+
className={cn(destructive && "[&_svg]:border-white/30")}
|
|
53
|
+
>
|
|
54
|
+
{confirmLabel}
|
|
55
|
+
</Button>
|
|
56
|
+
</DialogFooter>
|
|
57
|
+
</Dialog>
|
|
58
|
+
);
|
|
59
|
+
}
|
package/src/dialog.tsx
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sectioned modal dialog — the theme standard. It provides the shell (centered
|
|
9
|
+
* panel, dimmed backdrop, entrance animation, Escape / backdrop-click to close)
|
|
10
|
+
* and three bordered placeholders — `DialogHeader`, `DialogBody`, `DialogFooter`
|
|
11
|
+
* — so a new dialog only needs to pass content into each. See `ConfirmDialog`
|
|
12
|
+
* for a ready-made example built on top of these.
|
|
13
|
+
*
|
|
14
|
+
* <Dialog open={open} onClose={close} label="Invite">
|
|
15
|
+
* <DialogHeader><DialogTitle>Invite teammate</DialogTitle></DialogHeader>
|
|
16
|
+
* <DialogBody>…form…</DialogBody>
|
|
17
|
+
* <DialogFooter><Button onClick={close}>Cancel</Button>…</DialogFooter>
|
|
18
|
+
* </Dialog>
|
|
19
|
+
*/
|
|
20
|
+
export function Dialog({
|
|
21
|
+
open,
|
|
22
|
+
onClose,
|
|
23
|
+
label,
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
/** Allow Escape / backdrop click to dismiss (default true). */
|
|
27
|
+
dismissible = true,
|
|
28
|
+
}: {
|
|
29
|
+
open: boolean;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
/** Accessible name for the dialog (aria-label). */
|
|
32
|
+
label?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
dismissible?: boolean;
|
|
36
|
+
}) {
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (!open || !dismissible) return;
|
|
39
|
+
const onKey = (e: KeyboardEvent) => {
|
|
40
|
+
if (e.key === "Escape") onClose();
|
|
41
|
+
};
|
|
42
|
+
document.addEventListener("keydown", onKey);
|
|
43
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
44
|
+
}, [open, dismissible, onClose]);
|
|
45
|
+
|
|
46
|
+
if (!open) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className="vui-overlay-in fixed inset-0 z-[70] flex items-center justify-center bg-foreground/25 p-4"
|
|
51
|
+
onClick={dismissible ? onClose : undefined}
|
|
52
|
+
>
|
|
53
|
+
<div
|
|
54
|
+
role="dialog"
|
|
55
|
+
aria-modal="true"
|
|
56
|
+
aria-label={label}
|
|
57
|
+
onClick={(e) => e.stopPropagation()}
|
|
58
|
+
style={{ ["--vui-pop-origin" as string]: "center" }}
|
|
59
|
+
className={cn(
|
|
60
|
+
"vui-pop-in w-full max-w-md overflow-hidden rounded-lg border border-border bg-background shadow-xl",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Bordered header section. */
|
|
71
|
+
export function DialogHeader({
|
|
72
|
+
className,
|
|
73
|
+
children,
|
|
74
|
+
}: {
|
|
75
|
+
className?: string;
|
|
76
|
+
children: React.ReactNode;
|
|
77
|
+
}) {
|
|
78
|
+
return (
|
|
79
|
+
<div className={cn("border-b border-border px-5 py-3", className)}>
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function DialogTitle({
|
|
86
|
+
className,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
className?: string;
|
|
90
|
+
children: React.ReactNode;
|
|
91
|
+
}) {
|
|
92
|
+
return (
|
|
93
|
+
<h2 className={cn("text-base font-semibold tracking-tight", className)}>
|
|
94
|
+
{children}
|
|
95
|
+
</h2>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Scrollable content section (capped so long content scrolls, not the page). */
|
|
100
|
+
export function DialogBody({
|
|
101
|
+
className,
|
|
102
|
+
children,
|
|
103
|
+
}: {
|
|
104
|
+
className?: string;
|
|
105
|
+
children: React.ReactNode;
|
|
106
|
+
}) {
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
"max-h-[70vh] overflow-y-auto px-5 py-4 text-sm leading-relaxed",
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Bordered footer section — right-aligned actions by default. */
|
|
120
|
+
export function DialogFooter({
|
|
121
|
+
className,
|
|
122
|
+
children,
|
|
123
|
+
}: {
|
|
124
|
+
className?: string;
|
|
125
|
+
children: React.ReactNode;
|
|
126
|
+
}) {
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={cn(
|
|
130
|
+
"flex items-center justify-end gap-2 border-t border-border px-5 py-3",
|
|
131
|
+
className,
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|