Skip to content

Commit 5892313

Browse files
committed
feat(ui): add reusable dashboard component library
1 parent fb65576 commit 5892313

8 files changed

Lines changed: 359 additions & 3 deletions

File tree

packages/ui/package.json

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@
33
"version": "0.1.0",
44
"private": true,
55
"type": "module",
6+
"main": "./src/index.ts",
7+
"types": "./src/index.ts",
8+
"exports": {
9+
".": {
10+
"import": "./src/index.ts",
11+
"types": "./src/index.ts"
12+
}
13+
},
614
"scripts": {
7-
"dev": "echo 'ui: not implemented yet'",
8-
"build": "echo 'ui: not implemented yet'",
9-
"lint": "echo 'ui: not implemented yet'"
15+
"dev": "tsc --watch",
16+
"build": "tsc",
17+
"typecheck": "tsc --noEmit"
18+
},
19+
"peerDependencies": {
20+
"react": "^19.0.0",
21+
"react-dom": "^19.0.0"
22+
},
23+
"devDependencies": {
24+
"@types/react": "^19.0.0",
25+
"@types/react-dom": "^19.0.0",
26+
"typescript": "^5.7.3"
1027
}
1128
}

packages/ui/src/Badge.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from "react";
2+
3+
export type BadgeStatus = "waiting" | "running" | "completed" | "failed";
4+
5+
interface BadgeProps {
6+
status: BadgeStatus;
7+
label?: string;
8+
}
9+
10+
const statusConfig: Record<BadgeStatus, { bg: string; color: string; borderColor: string; label: string }> = {
11+
waiting: { bg: "rgba(92, 92, 111, 0.15)", color: "#9898a8", borderColor: "rgba(92, 92, 111, 0.3)", label: "Waiting" },
12+
running: { bg: "rgba(56, 189, 248, 0.12)", color: "#38bdf8", borderColor: "rgba(56, 189, 248, 0.3)", label: "Running" },
13+
completed: { bg: "rgba(52, 211, 153, 0.12)", color: "#34d399", borderColor: "rgba(52, 211, 153, 0.3)", label: "Completed" },
14+
failed: { bg: "rgba(244, 63, 94, 0.12)", color: "#f43f5e", borderColor: "rgba(244, 63, 94, 0.3)", label: "Failed" },
15+
};
16+
17+
export function Badge({ status, label }: BadgeProps) {
18+
const config = statusConfig[status];
19+
return (
20+
<span
21+
style={{
22+
display: "inline-flex",
23+
alignItems: "center",
24+
gap: "6px",
25+
padding: "3px 10px",
26+
fontSize: "12px",
27+
fontWeight: 600,
28+
fontFamily: "'Inter', system-ui, sans-serif",
29+
borderRadius: "20px",
30+
background: config.bg,
31+
color: config.color,
32+
border: `1px solid ${config.borderColor}`,
33+
letterSpacing: "0.02em",
34+
textTransform: "capitalize",
35+
}}
36+
>
37+
{status === "running" && (
38+
<span
39+
style={{
40+
width: "6px",
41+
height: "6px",
42+
borderRadius: "50%",
43+
background: config.color,
44+
animation: "pulse-glow 1.5s ease-in-out infinite",
45+
flexShrink: 0,
46+
}}
47+
/>
48+
)}
49+
{status === "completed" && <span style={{ fontSize: "11px" }}></span>}
50+
{status === "failed" && <span style={{ fontSize: "11px" }}></span>}
51+
{label ?? config.label}
52+
</span>
53+
);
54+
}

packages/ui/src/Button.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from "react";
2+
3+
type ButtonVariant = "primary" | "secondary" | "ghost";
4+
type ButtonSize = "sm" | "md" | "lg";
5+
6+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7+
variant?: ButtonVariant;
8+
size?: ButtonSize;
9+
loading?: boolean;
10+
children: React.ReactNode;
11+
}
12+
13+
const variantStyles: Record<ButtonVariant, React.CSSProperties> = {
14+
primary: {
15+
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
16+
color: "#fff",
17+
border: "1px solid rgba(99, 102, 241, 0.3)",
18+
boxShadow: "0 2px 12px rgba(99, 102, 241, 0.25)",
19+
},
20+
secondary: {
21+
background: "#1a1a26",
22+
color: "#f0f0f5",
23+
border: "1px solid #23232f",
24+
boxShadow: "none",
25+
},
26+
ghost: {
27+
background: "transparent",
28+
color: "#9898a8",
29+
border: "1px solid transparent",
30+
boxShadow: "none",
31+
},
32+
};
33+
34+
const sizeStyles: Record<ButtonSize, React.CSSProperties> = {
35+
sm: { padding: "6px 14px", fontSize: "13px", borderRadius: "6px" },
36+
md: { padding: "10px 22px", fontSize: "14px", borderRadius: "10px" },
37+
lg: { padding: "14px 32px", fontSize: "16px", borderRadius: "12px" },
38+
};
39+
40+
export function Button({
41+
variant = "primary",
42+
size = "md",
43+
loading = false,
44+
disabled,
45+
children,
46+
style,
47+
...props
48+
}: ButtonProps) {
49+
const isDisabled = disabled || loading;
50+
51+
return (
52+
<button
53+
{...props}
54+
disabled={isDisabled}
55+
style={{
56+
display: "inline-flex",
57+
alignItems: "center",
58+
justifyContent: "center",
59+
gap: "8px",
60+
fontFamily: "'Inter', system-ui, sans-serif",
61+
fontWeight: 600,
62+
cursor: isDisabled ? "not-allowed" : "pointer",
63+
opacity: isDisabled ? 0.55 : 1,
64+
transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)",
65+
letterSpacing: "-0.01em",
66+
...variantStyles[variant],
67+
...sizeStyles[size],
68+
...style,
69+
}}
70+
>
71+
{loading && (
72+
<span
73+
style={{
74+
width: "16px",
75+
height: "16px",
76+
border: "2px solid rgba(255,255,255,0.3)",
77+
borderTopColor: "#fff",
78+
borderRadius: "50%",
79+
animation: "spin 0.6s linear infinite",
80+
flexShrink: 0,
81+
}}
82+
/>
83+
)}
84+
{children}
85+
</button>
86+
);
87+
}

packages/ui/src/Card.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react";
2+
3+
interface CardProps {
4+
children: React.ReactNode;
5+
hover?: boolean;
6+
glow?: boolean;
7+
style?: React.CSSProperties;
8+
className?: string;
9+
}
10+
11+
export function Card({ children, hover = true, glow = false, style, className }: CardProps) {
12+
const [isHovered, setIsHovered] = React.useState(false);
13+
14+
return (
15+
<div
16+
className={className}
17+
onMouseEnter={hover ? () => setIsHovered(true) : undefined}
18+
onMouseLeave={hover ? () => setIsHovered(false) : undefined}
19+
style={{
20+
background: isHovered ? "#1c1c28" : "#16161f",
21+
border: "1px solid",
22+
borderColor: isHovered && glow ? "rgba(99, 102, 241, 0.3)" : "#23232f",
23+
borderRadius: "16px",
24+
padding: "24px",
25+
transition: "all 250ms cubic-bezier(0.4, 0, 0.2, 1)",
26+
boxShadow: isHovered
27+
? glow
28+
? "0 4px 16px rgba(99, 102, 241, 0.15), 0 8px 32px rgba(0, 0, 0, 0.3)"
29+
: "0 4px 16px rgba(0, 0, 0, 0.3)"
30+
: "0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2)",
31+
...style,
32+
}}
33+
>
34+
{children}
35+
</div>
36+
);
37+
}

packages/ui/src/Spinner.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from "react";
2+
3+
interface SpinnerProps {
4+
size?: number;
5+
color?: string;
6+
}
7+
8+
export function Spinner({ size = 20, color = "#6366f1" }: SpinnerProps) {
9+
return (
10+
<span
11+
role="status"
12+
aria-label="Loading"
13+
style={{
14+
display: "inline-block",
15+
width: `${size}px`,
16+
height: `${size}px`,
17+
border: `2px solid rgba(99, 102, 241, 0.2)`,
18+
borderTopColor: color,
19+
borderRadius: "50%",
20+
animation: "spin 0.7s linear infinite",
21+
flexShrink: 0,
22+
}}
23+
/>
24+
);
25+
}

packages/ui/src/Toast.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, { createContext, useContext, useState, useCallback } from "react";
2+
3+
type ToastType = "success" | "error" | "info";
4+
5+
interface ToastItem {
6+
id: number;
7+
type: ToastType;
8+
message: string;
9+
}
10+
11+
interface ToastContextValue {
12+
addToast: (type: ToastType, message: string) => void;
13+
}
14+
15+
const ToastContext = createContext<ToastContextValue | null>(null);
16+
17+
export function useToast(): ToastContextValue {
18+
const ctx = useContext(ToastContext);
19+
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
20+
return ctx;
21+
}
22+
23+
let nextId = 0;
24+
25+
const typeStyles: Record<ToastType, { bg: string; border: string; icon: string }> = {
26+
success: { bg: "rgba(52, 211, 153, 0.12)", border: "rgba(52, 211, 153, 0.3)", icon: "✓" },
27+
error: { bg: "rgba(244, 63, 94, 0.12)", border: "rgba(244, 63, 94, 0.3)", icon: "✕" },
28+
info: { bg: "rgba(56, 189, 248, 0.12)", border: "rgba(56, 189, 248, 0.3)", icon: "ℹ" },
29+
};
30+
31+
const typeColor: Record<ToastType, string> = {
32+
success: "#34d399",
33+
error: "#f43f5e",
34+
info: "#38bdf8",
35+
};
36+
37+
export function ToastProvider({ children }: { children: React.ReactNode }) {
38+
const [toasts, setToasts] = useState<ToastItem[]>([]);
39+
40+
const addToast = useCallback((type: ToastType, message: string) => {
41+
const id = ++nextId;
42+
setToasts((prev) => [...prev, { id, type, message }]);
43+
setTimeout(() => {
44+
setToasts((prev) => prev.filter((t) => t.id !== id));
45+
}, 4000);
46+
}, []);
47+
48+
return (
49+
<ToastContext.Provider value={{ addToast }}>
50+
{children}
51+
<div
52+
style={{
53+
position: "fixed",
54+
bottom: "24px",
55+
right: "24px",
56+
display: "flex",
57+
flexDirection: "column",
58+
gap: "10px",
59+
zIndex: 9999,
60+
pointerEvents: "none",
61+
}}
62+
>
63+
{toasts.map((toast) => {
64+
const s = typeStyles[toast.type];
65+
return (
66+
<div
67+
key={toast.id}
68+
style={{
69+
display: "flex",
70+
alignItems: "center",
71+
gap: "10px",
72+
padding: "12px 18px",
73+
background: "#16161f",
74+
border: `1px solid ${s.border}`,
75+
borderRadius: "12px",
76+
boxShadow: "0 4px 24px rgba(0,0,0,0.4)",
77+
fontSize: "14px",
78+
fontFamily: "'Inter', system-ui, sans-serif",
79+
color: "#f0f0f5",
80+
animation: "toast-in 300ms ease-out",
81+
pointerEvents: "auto",
82+
maxWidth: "380px",
83+
}}
84+
>
85+
<span
86+
style={{
87+
width: "24px",
88+
height: "24px",
89+
display: "flex",
90+
alignItems: "center",
91+
justifyContent: "center",
92+
borderRadius: "50%",
93+
background: s.bg,
94+
color: typeColor[toast.type],
95+
fontSize: "12px",
96+
fontWeight: 700,
97+
flexShrink: 0,
98+
}}
99+
>
100+
{s.icon}
101+
</span>
102+
{toast.message}
103+
</div>
104+
);
105+
})}
106+
</div>
107+
</ToastContext.Provider>
108+
);
109+
}

packages/ui/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { Button } from "./Button";
2+
export { Badge } from "./Badge";
3+
export type { BadgeStatus } from "./Badge";
4+
export { Card } from "./Card";
5+
export { Spinner } from "./Spinner";
6+
export { ToastProvider, useToast } from "./Toast";

packages/ui/tsconfig.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
7+
"strict": true,
8+
"jsx": "react-jsx",
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"noUncheckedIndexedAccess": true,
12+
"noImplicitOverride": true,
13+
"declaration": true,
14+
"declarationMap": true,
15+
"sourceMap": true,
16+
"outDir": "dist",
17+
"rootDir": "src",
18+
"composite": true
19+
},
20+
"include": ["src"]
21+
}

0 commit comments

Comments
 (0)