@mnee-ui/ui 0.0.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.
- package/README.md +92 -0
- package/components/ui/alert.tsx +84 -0
- package/components/ui/badge.tsx +38 -0
- package/components/ui/banner.tsx +73 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +156 -0
- package/components/ui/code-block.tsx +108 -0
- package/components/ui/drawer.tsx +164 -0
- package/components/ui/icons.tsx +96 -0
- package/components/ui/index.ts +14 -0
- package/components/ui/input.tsx +129 -0
- package/components/ui/mnee-ui.css +35 -0
- package/components/ui/modal.tsx +134 -0
- package/components/ui/table.tsx +185 -0
- package/components/ui/toast.tsx +136 -0
- package/package.json +49 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// ─── Compound table primitives ────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function Table({ className, children, ...props }: React.HTMLAttributes<HTMLTableElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="w-full overflow-x-auto rounded-lg border border-gray-200 shadow-sm">
|
|
9
|
+
<table className={cn("w-full border-collapse text-sm", className)} {...props}>
|
|
10
|
+
{children}
|
|
11
|
+
</table>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TableHead({ className, children, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
17
|
+
return (
|
|
18
|
+
<thead className={cn("bg-gray-50 border-b border-gray-200", className)} {...props}>
|
|
19
|
+
{children}
|
|
20
|
+
</thead>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TableBody({ className, children, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {
|
|
25
|
+
return (
|
|
26
|
+
<tbody className={cn("divide-y divide-gray-100", className)} {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</tbody>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function TableRow({ className, children, onClick, ...props }: React.HTMLAttributes<HTMLTableRowElement>) {
|
|
33
|
+
return (
|
|
34
|
+
<tr
|
|
35
|
+
className={cn(
|
|
36
|
+
"transition-colors",
|
|
37
|
+
onClick ? "cursor-pointer hover:bg-gray-50" : "hover:bg-gray-50/50",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
onClick={onClick}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</tr>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TableHeaderProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
|
49
|
+
sortable?: boolean;
|
|
50
|
+
sortDirection?: "asc" | "desc" | null;
|
|
51
|
+
onSort?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function TableHeader({
|
|
55
|
+
className,
|
|
56
|
+
children,
|
|
57
|
+
sortable,
|
|
58
|
+
sortDirection,
|
|
59
|
+
onSort,
|
|
60
|
+
...props
|
|
61
|
+
}: TableHeaderProps) {
|
|
62
|
+
return (
|
|
63
|
+
<th
|
|
64
|
+
className={cn(
|
|
65
|
+
"px-4 py-3 text-left text-xs font-semibold text-gray-600 whitespace-nowrap",
|
|
66
|
+
sortable && "select-none",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{sortable ? (
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onClick={onSort}
|
|
75
|
+
className="inline-flex items-center gap-1 hover:text-gray-900 transition-colors"
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
<span className="text-gray-400">
|
|
79
|
+
{sortDirection === "asc" ? "↑" : sortDirection === "desc" ? "↓" : "↕"}
|
|
80
|
+
</span>
|
|
81
|
+
</button>
|
|
82
|
+
) : (
|
|
83
|
+
children
|
|
84
|
+
)}
|
|
85
|
+
</th>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function TableCell({ className, children, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) {
|
|
90
|
+
return (
|
|
91
|
+
<td className={cn("px-4 py-3 text-sm text-gray-700", className)} {...props}>
|
|
92
|
+
{children}
|
|
93
|
+
</td>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Empty state ──────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export interface TableEmptyProps {
|
|
100
|
+
message?: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function TableEmpty({
|
|
105
|
+
message = "No data",
|
|
106
|
+
description,
|
|
107
|
+
}: TableEmptyProps) {
|
|
108
|
+
return (
|
|
109
|
+
<tr>
|
|
110
|
+
<td colSpan={999}>
|
|
111
|
+
<div className="flex flex-col items-center justify-center gap-2 py-16 text-center">
|
|
112
|
+
<p className="text-sm font-medium text-gray-700">{message}</p>
|
|
113
|
+
{description && <p className="text-xs text-gray-400 max-w-xs">{description}</p>}
|
|
114
|
+
</div>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Loading overlay ──────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export function TableLoading({ cols = 4 }: { cols?: number }) {
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
{Array.from({ length: 4 }).map((_, r) => (
|
|
126
|
+
<TableRow key={r}>
|
|
127
|
+
{Array.from({ length: cols }).map((_, c) => (
|
|
128
|
+
<TableCell key={c}>
|
|
129
|
+
<div className="h-3.5 bg-gray-100 rounded animate-pulse" style={{ width: `${60 + ((r + c) % 3) * 15}%` }} />
|
|
130
|
+
</TableCell>
|
|
131
|
+
))}
|
|
132
|
+
</TableRow>
|
|
133
|
+
))}
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Pagination ───────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export interface PaginationProps {
|
|
141
|
+
page: number;
|
|
142
|
+
totalPages: number;
|
|
143
|
+
totalItems?: number;
|
|
144
|
+
onPageChange: (page: number) => void;
|
|
145
|
+
className?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function Pagination({ page, totalPages, totalItems, onPageChange, className }: PaginationProps) {
|
|
149
|
+
return (
|
|
150
|
+
<div className={cn("flex items-center justify-between px-4 py-3 border-t border-gray-200 text-sm text-gray-600", className)}>
|
|
151
|
+
<span>
|
|
152
|
+
{totalItems !== undefined
|
|
153
|
+
? `Page ${page} of ${totalPages} (${totalItems} items)`
|
|
154
|
+
: `Page ${page} of ${totalPages}`}
|
|
155
|
+
</span>
|
|
156
|
+
<div className="flex items-center gap-1">
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={() => onPageChange(page - 1)}
|
|
160
|
+
disabled={page <= 1}
|
|
161
|
+
className="flex items-center gap-1 px-2.5 py-1.5 rounded hover:bg-gray-100 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
|
162
|
+
>
|
|
163
|
+
<ChevronLeft size={14} />
|
|
164
|
+
Previous
|
|
165
|
+
</button>
|
|
166
|
+
<span className="px-3 py-1.5 border border-gray-200 rounded text-xs font-medium min-w-[2rem] text-center">
|
|
167
|
+
{page}
|
|
168
|
+
</span>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => onPageChange(page + 1)}
|
|
172
|
+
disabled={page >= totalPages}
|
|
173
|
+
className="flex items-center gap-1 px-2.5 py-1.5 rounded hover:bg-gray-100 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
|
174
|
+
>
|
|
175
|
+
Next
|
|
176
|
+
<ChevronRight size={14} />
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── Convenience loading spinner for table toolbar ────────────────────────────
|
|
184
|
+
|
|
185
|
+
export { Loader2 };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useRef, useState } from "react";
|
|
4
|
+
import { X, CheckCircle, AlertTriangle, Info } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export type ToastType = "success" | "error" | "warning" | "info" | "default";
|
|
10
|
+
|
|
11
|
+
export interface ToastContextType {
|
|
12
|
+
showToast: (message: string, type?: ToastType) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const ToastContext = createContext<ToastContextType | null>(null);
|
|
18
|
+
|
|
19
|
+
// ─── useToast hook ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export function useToast(): ToastContextType {
|
|
22
|
+
const ctx = useContext(ToastContext);
|
|
23
|
+
if (!ctx) {
|
|
24
|
+
throw new Error("useToast must be used inside <ToastProvider>");
|
|
25
|
+
}
|
|
26
|
+
return ctx;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Toast visual component ───────────────────────────────────────────────────
|
|
30
|
+
// Renders the notification box only — no fixed positioning.
|
|
31
|
+
// ToastProvider wraps it in the fixed top-right container.
|
|
32
|
+
|
|
33
|
+
const toastStyles: Record<ToastType, { bg: string; text: string; icon: React.ReactNode }> = {
|
|
34
|
+
success: {
|
|
35
|
+
bg: "bg-success-bg",
|
|
36
|
+
text: "text-success",
|
|
37
|
+
icon: <CheckCircle size={18} className="text-success shrink-0" />,
|
|
38
|
+
},
|
|
39
|
+
error: {
|
|
40
|
+
bg: "bg-error-bg",
|
|
41
|
+
text: "text-error",
|
|
42
|
+
icon: <AlertTriangle size={18} className="text-error shrink-0" />,
|
|
43
|
+
},
|
|
44
|
+
warning: {
|
|
45
|
+
bg: "bg-warning-bg",
|
|
46
|
+
text: "text-warning",
|
|
47
|
+
icon: <AlertTriangle size={18} className="text-warning shrink-0" />,
|
|
48
|
+
},
|
|
49
|
+
info: {
|
|
50
|
+
bg: "bg-info-bg",
|
|
51
|
+
text: "text-info",
|
|
52
|
+
icon: <Info size={18} className="text-info shrink-0" />,
|
|
53
|
+
},
|
|
54
|
+
default: {
|
|
55
|
+
bg: "bg-gray-100",
|
|
56
|
+
text: "text-gray-700",
|
|
57
|
+
icon: <CheckCircle size={18} className="text-gray-500 shrink-0" />,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface ToastProps {
|
|
62
|
+
message: string;
|
|
63
|
+
type: ToastType;
|
|
64
|
+
onClose?: () => void;
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Toast({ message, type, onClose = () => {}, className }: ToastProps) {
|
|
69
|
+
const { bg, text, icon } = toastStyles[type];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
role="alert"
|
|
74
|
+
aria-live="polite"
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex items-center gap-3 px-4 py-3 rounded-xl shadow-lg w-[380px]",
|
|
77
|
+
bg,
|
|
78
|
+
text,
|
|
79
|
+
className
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{icon}
|
|
83
|
+
<span className="flex-1 text-sm font-medium">{message}</span>
|
|
84
|
+
<button
|
|
85
|
+
onClick={onClose}
|
|
86
|
+
aria-label="Dismiss notification"
|
|
87
|
+
className="opacity-60 hover:opacity-100 transition-opacity"
|
|
88
|
+
>
|
|
89
|
+
<X size={16} />
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── ToastProvider ────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
98
|
+
const [toast, setToast] = useState<{ message: string; type: ToastType } | null>(null);
|
|
99
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
100
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
101
|
+
|
|
102
|
+
const dismiss = () => {
|
|
103
|
+
setIsExiting(true);
|
|
104
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
105
|
+
timerRef.current = setTimeout(() => {
|
|
106
|
+
setToast(null);
|
|
107
|
+
setIsExiting(false);
|
|
108
|
+
}, 300);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const showToast = (message: string, type: ToastType = "default") => {
|
|
112
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
113
|
+
setIsExiting(false);
|
|
114
|
+
setToast({ message, type });
|
|
115
|
+
timerRef.current = setTimeout(dismiss, 4000);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<ToastContext.Provider value={{ showToast }}>
|
|
120
|
+
{children}
|
|
121
|
+
{toast && (
|
|
122
|
+
<div
|
|
123
|
+
className="fixed top-6 right-6"
|
|
124
|
+
style={{
|
|
125
|
+
zIndex: 9999,
|
|
126
|
+
animation: isExiting
|
|
127
|
+
? "slideOutToast 0.3s ease-in forwards"
|
|
128
|
+
: "slideInToast 0.3s ease-out",
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<Toast message={toast.message} type={toast.type} onClose={dismiss} />
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</ToastContext.Provider>
|
|
135
|
+
);
|
|
136
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mnee-ui/ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MNEE Design System — components and documentation",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./components/ui/index.ts",
|
|
8
|
+
"./styles": "./components/ui/mnee-ui.css"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"components/ui/"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"react": ">=18",
|
|
18
|
+
"tailwindcss": ">=3"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "next dev",
|
|
22
|
+
"build": "next build",
|
|
23
|
+
"start": "next start",
|
|
24
|
+
"lint": "next lint"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@mdx-js/loader": "^3.1.1",
|
|
28
|
+
"@mdx-js/react": "^3.1.1",
|
|
29
|
+
"@next/mdx": "^16.1.6",
|
|
30
|
+
"clsx": "^2.1.1",
|
|
31
|
+
"lucide-react": "^0.575.0",
|
|
32
|
+
"next": "15.2.4",
|
|
33
|
+
"react": "^19.0.0",
|
|
34
|
+
"react-dom": "^19.0.0",
|
|
35
|
+
"shiki": "^3.23.0",
|
|
36
|
+
"tailwind-merge": "^3.5.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@eslint/eslintrc": "^3",
|
|
40
|
+
"@tailwindcss/postcss": "^4",
|
|
41
|
+
"@types/node": "^20",
|
|
42
|
+
"@types/react": "^19",
|
|
43
|
+
"@types/react-dom": "^19",
|
|
44
|
+
"eslint": "^9",
|
|
45
|
+
"eslint-config-next": "15.2.4",
|
|
46
|
+
"tailwindcss": "^4",
|
|
47
|
+
"typescript": "^5"
|
|
48
|
+
}
|
|
49
|
+
}
|