bmj-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/dist/index.d.ts +53 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
- package/rollup.config.js +39 -0
- package/src/components/ThemeToggle.tsx +43 -0
- package/src/components/bmj-ui/AppSidebar.tsx +154 -0
- package/src/components/bmj-ui/ChartCard.tsx +205 -0
- package/src/components/bmj-ui/DashboardLayout.tsx +32 -0
- package/src/components/bmj-ui/StatCard.tsx +85 -0
- package/src/components/bmj-ui/Topbar.tsx +99 -0
- package/src/components/bmj-ui/index.ts +5 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/dropdown-menu.tsx +272 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/select.tsx +200 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +135 -0
- package/src/components/ui/sidebar.tsx +719 -0
- package/src/components/ui/skeleton.tsx +14 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +80 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.css +155 -0
- package/src/index.ts +7 -0
- package/src/lib/utils.ts +6 -0
- package/tsconfig.json +22 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { LucideIcon } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface SidebarItem {
|
|
6
|
+
title: string;
|
|
7
|
+
icon: React.ElementType;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
declare function AppSidebar({ items, onItemClick, activeItem, }: {
|
|
11
|
+
items?: SidebarItem[];
|
|
12
|
+
onItemClick?: (title: string) => void;
|
|
13
|
+
activeItem?: string;
|
|
14
|
+
}): react_jsx_runtime.JSX.Element;
|
|
15
|
+
|
|
16
|
+
interface DashboardLayoutProps {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
sidebarItems?: SidebarItem[];
|
|
19
|
+
onSidebarItemClick?: (title: string) => void;
|
|
20
|
+
activeSidebarItem?: string;
|
|
21
|
+
}
|
|
22
|
+
declare function DashboardLayout({ children, sidebarItems, onSidebarItemClick, activeSidebarItem, }: DashboardLayoutProps): react_jsx_runtime.JSX.Element;
|
|
23
|
+
|
|
24
|
+
interface StatCardProps {
|
|
25
|
+
title: string;
|
|
26
|
+
value: string | number;
|
|
27
|
+
description?: string;
|
|
28
|
+
trend?: {
|
|
29
|
+
value: number;
|
|
30
|
+
isPositive: boolean;
|
|
31
|
+
};
|
|
32
|
+
icon?: LucideIcon;
|
|
33
|
+
className?: string;
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
}
|
|
36
|
+
declare function StatCard({ title, value, description, trend, icon: Icon, className, loading, }: StatCardProps): react_jsx_runtime.JSX.Element;
|
|
37
|
+
|
|
38
|
+
interface ChartCardProps {
|
|
39
|
+
title: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
data: any[];
|
|
42
|
+
dataKey: string;
|
|
43
|
+
categoryKey: string;
|
|
44
|
+
color?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
type?: "area" | "bar" | "line" | "scatter";
|
|
48
|
+
}
|
|
49
|
+
declare function ChartCard({ title, description, data, dataKey, categoryKey, color, className, loading, type, }: ChartCardProps): react_jsx_runtime.JSX.Element;
|
|
50
|
+
|
|
51
|
+
declare function Topbar(): react_jsx_runtime.JSX.Element;
|
|
52
|
+
|
|
53
|
+
export { AppSidebar, ChartCard, DashboardLayout, StatCard, Topbar };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Topbar=exports.StatCard=exports.DashboardLayout=exports.ChartCard=exports.AppSidebar=void 0;const e=require("./components/bmj-ui");Object.defineProperty(exports,"AppSidebar",{enumerable:!0,get:function(){return e.AppSidebar}});const r=require("./components/bmj-ui");Object.defineProperty(exports,"ChartCard",{enumerable:!0,get:function(){return r.ChartCard}});const t=require("./components/bmj-ui");Object.defineProperty(exports,"DashboardLayout",{enumerable:!0,get:function(){return t.DashboardLayout}});const o=require("./components/bmj-ui");Object.defineProperty(exports,"StatCard",{enumerable:!0,get:function(){return o.StatCard}});const n=require("./components/bmj-ui");Object.defineProperty(exports,"Topbar",{enumerable:!0,get:function(){return n.Topbar}});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { AppSidebar } from \"./components/bmj-ui\";\nimport { ChartCard } from \"./components/bmj-ui\";\nimport { DashboardLayout } from \"./components/bmj-ui\";\nimport { StatCard } from \"./components/bmj-ui\";\nimport { Topbar } from \"./components/bmj-ui\";\n\nexport { AppSidebar, ChartCard, DashboardLayout, StatCard, Topbar };\n"],"names":["bmj_ui_1","require","Object","defineProperty","exports","enumerable","get","bmj_ui_2","bmj_ui_3","bmj_ui_4","bmj_ui_5"],"mappings":"wKAAA,MAAAA,EAAAC,QAAA,uBAMSC,OAAAC,eAAAC,QAAA,aAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OANAN,YAAU,IACnB,MAAAO,EAAAN,QAAA,uBAKqBC,OAAAC,eAAAC,QAAA,YAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OALZC,WAAS,IAClB,MAAAC,EAAAP,QAAA,uBAIgCC,OAAAC,eAAAC,QAAA,kBAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OAJvBE,iBAAe,IACxB,MAAAC,EAAAR,QAAA,uBAGiDC,OAAAC,eAAAC,QAAA,WAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OAHxCG,UAAQ,IACjB,MAAAC,EAAAT,QAAA,uBAE2DC,OAAAC,eAAAC,QAAA,SAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OAFlDI,QAAM"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.Topbar=exports.StatCard=exports.DashboardLayout=exports.ChartCard=exports.AppSidebar=void 0;const e=require("./components/bmj-ui");Object.defineProperty(exports,"AppSidebar",{enumerable:!0,get:function(){return e.AppSidebar}});const r=require("./components/bmj-ui");Object.defineProperty(exports,"ChartCard",{enumerable:!0,get:function(){return r.ChartCard}});const t=require("./components/bmj-ui");Object.defineProperty(exports,"DashboardLayout",{enumerable:!0,get:function(){return t.DashboardLayout}});const o=require("./components/bmj-ui");Object.defineProperty(exports,"StatCard",{enumerable:!0,get:function(){return o.StatCard}});const n=require("./components/bmj-ui");Object.defineProperty(exports,"Topbar",{enumerable:!0,get:function(){return n.Topbar}});
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/index.ts"],"sourcesContent":["import { AppSidebar } from \"./components/bmj-ui\";\nimport { ChartCard } from \"./components/bmj-ui\";\nimport { DashboardLayout } from \"./components/bmj-ui\";\nimport { StatCard } from \"./components/bmj-ui\";\nimport { Topbar } from \"./components/bmj-ui\";\n\nexport { AppSidebar, ChartCard, DashboardLayout, StatCard, Topbar };\n"],"names":["bmj_ui_1","require","Object","defineProperty","exports","enumerable","get","bmj_ui_2","bmj_ui_3","bmj_ui_4","bmj_ui_5"],"mappings":"2JAAA,MAAAA,EAAAC,QAAA,uBAMSC,OAAAC,eAAAC,QAAA,aAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OANAN,YAAU,IACnB,MAAAO,EAAAN,QAAA,uBAKqBC,OAAAC,eAAAC,QAAA,YAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OALZC,WAAS,IAClB,MAAAC,EAAAP,QAAA,uBAIgCC,OAAAC,eAAAC,QAAA,kBAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OAJvBE,iBAAe,IACxB,MAAAC,EAAAR,QAAA,uBAGiDC,OAAAC,eAAAC,QAAA,WAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OAHxCG,UAAQ,IACjB,MAAAC,EAAAT,QAAA,uBAE2DC,OAAAC,eAAAC,QAAA,SAAA,CAAAC,YAAA,EAAAC,IAAA,WAAA,OAFlDI,QAAM"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bmj-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Headless component library meticulously crafted for rapid dashboard development. Professional, polished, and ready for production.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.mjs",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"rollup": "rollup -c --bundleConfigAsCjs"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@rollup/plugin-commonjs": "^29.0.2",
|
|
16
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
17
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
18
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
19
|
+
"@types/react": "^19.2.14",
|
|
20
|
+
"@types/react-dom": "^19.2.3",
|
|
21
|
+
"autoprefixer": "^10.4.27",
|
|
22
|
+
"postcss": "^8.5.9",
|
|
23
|
+
"react": "^19.2.5",
|
|
24
|
+
"react-dom": "^19.2.5",
|
|
25
|
+
"rollup": "^4.60.1",
|
|
26
|
+
"rollup-plugin-dts": "^6.4.1",
|
|
27
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
28
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
29
|
+
"tailwindcss": "^4.2.2",
|
|
30
|
+
"tslib": "^2.8.1",
|
|
31
|
+
"typescript": "^6.0.2"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@base-ui/react": "^1.3.0",
|
|
35
|
+
"@fontsource-variable/geist": "^5.2.8",
|
|
36
|
+
"class-variance-authority": "^0.7.1",
|
|
37
|
+
"clsx": "^2.1.1",
|
|
38
|
+
"lucide-react": "^1.8.0",
|
|
39
|
+
"motion": "^12.38.0",
|
|
40
|
+
"recharts": "^3.8.1",
|
|
41
|
+
"sonner": "^2.0.7"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
2
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
3
|
+
import typescript from "@rollup/plugin-typescript";
|
|
4
|
+
import dts from "rollup-plugin-dts";
|
|
5
|
+
import terser from "@rollup/plugin-terser";
|
|
6
|
+
import peerDepsExternal from "rollup-plugin-peer-deps-external";
|
|
7
|
+
|
|
8
|
+
const packageJson = require("./package.json");
|
|
9
|
+
|
|
10
|
+
export default [
|
|
11
|
+
{
|
|
12
|
+
input: "src/index.ts",
|
|
13
|
+
output: [
|
|
14
|
+
{
|
|
15
|
+
file: packageJson.main,
|
|
16
|
+
format: "cjs",
|
|
17
|
+
sourcemap: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
file: packageJson.module,
|
|
21
|
+
format: "esm",
|
|
22
|
+
sourcemap: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
plugins: [
|
|
26
|
+
peerDepsExternal(),
|
|
27
|
+
resolve(),
|
|
28
|
+
commonjs(),
|
|
29
|
+
typescript({ tsconfig: "./tsconfig.json" }),
|
|
30
|
+
terser(),
|
|
31
|
+
],
|
|
32
|
+
external: ["react", "react-dom"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
input: "src/index.ts",
|
|
36
|
+
output: [{ file: packageJson.types }],
|
|
37
|
+
plugins: [dts.default()],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Moon, Sun } from "lucide-react";
|
|
3
|
+
import { Button } from "./ui/button";
|
|
4
|
+
|
|
5
|
+
export function ThemeToggle({ className }: { className?: string }) {
|
|
6
|
+
const [theme, setTheme] = React.useState<"light" | "dark">(() => {
|
|
7
|
+
if (typeof window !== "undefined") {
|
|
8
|
+
const saved = localStorage.getItem("bmj-ui-theme");
|
|
9
|
+
if (saved === "light" || saved === "dark") return saved;
|
|
10
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
11
|
+
? "dark"
|
|
12
|
+
: "light";
|
|
13
|
+
}
|
|
14
|
+
return "light";
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const root = window.document.documentElement;
|
|
19
|
+
root.classList.remove("light", "dark");
|
|
20
|
+
root.classList.add(theme);
|
|
21
|
+
localStorage.setItem("bmj-ui-theme", theme);
|
|
22
|
+
}, [theme]);
|
|
23
|
+
|
|
24
|
+
const toggleTheme = () => {
|
|
25
|
+
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Button
|
|
30
|
+
variant="ghost"
|
|
31
|
+
size="icon"
|
|
32
|
+
onClick={toggleTheme}
|
|
33
|
+
className={className}
|
|
34
|
+
aria-label="Toggle theme"
|
|
35
|
+
>
|
|
36
|
+
{theme === "light" ? (
|
|
37
|
+
<Moon className="w-5 h-5 transition-all" />
|
|
38
|
+
) : (
|
|
39
|
+
<Sun className="w-5 h-5 transition-all" />
|
|
40
|
+
)}
|
|
41
|
+
</Button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Sidebar,
|
|
4
|
+
SidebarContent,
|
|
5
|
+
SidebarFooter,
|
|
6
|
+
SidebarHeader,
|
|
7
|
+
SidebarMenu,
|
|
8
|
+
SidebarMenuItem,
|
|
9
|
+
SidebarMenuButton,
|
|
10
|
+
SidebarGroup,
|
|
11
|
+
SidebarGroupLabel,
|
|
12
|
+
SidebarGroupContent,
|
|
13
|
+
useSidebar,
|
|
14
|
+
} from "../ui/sidebar";
|
|
15
|
+
import {
|
|
16
|
+
LayoutDashboard,
|
|
17
|
+
Settings,
|
|
18
|
+
Users,
|
|
19
|
+
BarChart3,
|
|
20
|
+
Package,
|
|
21
|
+
HelpCircle,
|
|
22
|
+
ListTodo,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
25
|
+
|
|
26
|
+
const defaultItems = [
|
|
27
|
+
{ title: "Dashboard", icon: LayoutDashboard, url: "#" },
|
|
28
|
+
{ title: "Analytics", icon: BarChart3, url: "#" },
|
|
29
|
+
{ title: "Products", icon: Package, url: "#" },
|
|
30
|
+
{ title: "Customers", icon: Users, url: "#" },
|
|
31
|
+
{ title: "Tasks", icon: ListTodo, url: "#" },
|
|
32
|
+
{ title: "Settings", icon: Settings, url: "#" },
|
|
33
|
+
{ title: "Help", icon: HelpCircle, url: "#" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export interface SidebarItem {
|
|
37
|
+
title: string;
|
|
38
|
+
icon: React.ElementType;
|
|
39
|
+
url: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function AppSidebar({
|
|
43
|
+
items = defaultItems,
|
|
44
|
+
onItemClick,
|
|
45
|
+
activeItem,
|
|
46
|
+
}: {
|
|
47
|
+
items?: SidebarItem[];
|
|
48
|
+
onItemClick?: (title: string) => void;
|
|
49
|
+
activeItem?: string;
|
|
50
|
+
}) {
|
|
51
|
+
const { state } = useSidebar();
|
|
52
|
+
const isCollapsed = state === "collapsed";
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Sidebar variant="inset" collapsible="icon">
|
|
56
|
+
<SidebarHeader className="h-16 flex items-center px-6 border-b border-border/50">
|
|
57
|
+
<div className="flex items-center gap-2 font-bold text-xl tracking-tight">
|
|
58
|
+
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-primary-foreground shrink-0">
|
|
59
|
+
B
|
|
60
|
+
</div>
|
|
61
|
+
<AnimatePresence mode="wait">
|
|
62
|
+
{!isCollapsed && (
|
|
63
|
+
<motion.span
|
|
64
|
+
initial={{ opacity: 0, x: -10 }}
|
|
65
|
+
animate={{ opacity: 1, x: 0 }}
|
|
66
|
+
exit={{ opacity: 0, x: -10 }}
|
|
67
|
+
transition={{ duration: 0.2 }}
|
|
68
|
+
className="whitespace-nowrap"
|
|
69
|
+
>
|
|
70
|
+
BMJ UI
|
|
71
|
+
</motion.span>
|
|
72
|
+
)}
|
|
73
|
+
</AnimatePresence>
|
|
74
|
+
</div>
|
|
75
|
+
</SidebarHeader>
|
|
76
|
+
<SidebarContent>
|
|
77
|
+
<SidebarGroup>
|
|
78
|
+
<SidebarGroupLabel>
|
|
79
|
+
<AnimatePresence mode="wait">
|
|
80
|
+
{!isCollapsed && (
|
|
81
|
+
<motion.span
|
|
82
|
+
initial={{ opacity: 0 }}
|
|
83
|
+
animate={{ opacity: 1 }}
|
|
84
|
+
exit={{ opacity: 0 }}
|
|
85
|
+
transition={{ duration: 0.2 }}
|
|
86
|
+
>
|
|
87
|
+
Main Menu
|
|
88
|
+
</motion.span>
|
|
89
|
+
)}
|
|
90
|
+
</AnimatePresence>
|
|
91
|
+
</SidebarGroupLabel>
|
|
92
|
+
<SidebarGroupContent>
|
|
93
|
+
<SidebarMenu>
|
|
94
|
+
{items.map((item) => (
|
|
95
|
+
<SidebarMenuItem key={item.title}>
|
|
96
|
+
<SidebarMenuButton
|
|
97
|
+
tooltip={item.title}
|
|
98
|
+
isActive={activeItem === item.title}
|
|
99
|
+
onClick={() => onItemClick?.(item.title)}
|
|
100
|
+
className="transition-all duration-200 hover:bg-primary/5 hover:text-primary"
|
|
101
|
+
render={
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className="flex items-center w-full"
|
|
105
|
+
>
|
|
106
|
+
<item.icon className="shrink-0" />
|
|
107
|
+
<AnimatePresence mode="wait">
|
|
108
|
+
{!isCollapsed && (
|
|
109
|
+
<motion.span
|
|
110
|
+
initial={{ opacity: 0, width: 0, marginLeft: 0 }}
|
|
111
|
+
animate={{
|
|
112
|
+
opacity: 1,
|
|
113
|
+
width: "auto",
|
|
114
|
+
marginLeft: 8,
|
|
115
|
+
}}
|
|
116
|
+
exit={{ opacity: 0, width: 0, marginLeft: 0 }}
|
|
117
|
+
transition={{ duration: 0.2 }}
|
|
118
|
+
className="overflow-hidden whitespace-nowrap"
|
|
119
|
+
>
|
|
120
|
+
{item.title}
|
|
121
|
+
</motion.span>
|
|
122
|
+
)}
|
|
123
|
+
</AnimatePresence>
|
|
124
|
+
</button>
|
|
125
|
+
}
|
|
126
|
+
/>
|
|
127
|
+
</SidebarMenuItem>
|
|
128
|
+
))}
|
|
129
|
+
</SidebarMenu>
|
|
130
|
+
</SidebarGroupContent>
|
|
131
|
+
</SidebarGroup>
|
|
132
|
+
</SidebarContent>
|
|
133
|
+
<SidebarFooter className="p-4 border-t border-border/50">
|
|
134
|
+
<div className="flex items-center gap-3 min-h-10">
|
|
135
|
+
<div className="w-8 h-8 rounded-full bg-muted shrink-0" />
|
|
136
|
+
<AnimatePresence mode="wait">
|
|
137
|
+
{!isCollapsed && (
|
|
138
|
+
<motion.div
|
|
139
|
+
initial={{ opacity: 0, x: -10 }}
|
|
140
|
+
animate={{ opacity: 1, x: 0 }}
|
|
141
|
+
exit={{ opacity: 0, x: -10 }}
|
|
142
|
+
transition={{ duration: 0.2 }}
|
|
143
|
+
className="flex flex-col overflow-hidden whitespace-nowrap"
|
|
144
|
+
>
|
|
145
|
+
<span className="text-sm font-medium">Bahry Jarbou</span>
|
|
146
|
+
<span className="text-xs text-muted-foreground">Admin</span>
|
|
147
|
+
</motion.div>
|
|
148
|
+
)}
|
|
149
|
+
</AnimatePresence>
|
|
150
|
+
</div>
|
|
151
|
+
</SidebarFooter>
|
|
152
|
+
</Sidebar>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Card,
|
|
3
|
+
CardContent,
|
|
4
|
+
CardHeader,
|
|
5
|
+
CardTitle,
|
|
6
|
+
CardDescription,
|
|
7
|
+
} from "../ui/card";
|
|
8
|
+
import { Skeleton } from "../ui/skeleton";
|
|
9
|
+
import {
|
|
10
|
+
ResponsiveContainer,
|
|
11
|
+
AreaChart,
|
|
12
|
+
Area,
|
|
13
|
+
BarChart,
|
|
14
|
+
Bar,
|
|
15
|
+
LineChart,
|
|
16
|
+
Line,
|
|
17
|
+
ScatterChart,
|
|
18
|
+
Scatter,
|
|
19
|
+
XAxis,
|
|
20
|
+
YAxis,
|
|
21
|
+
Tooltip,
|
|
22
|
+
CartesianGrid,
|
|
23
|
+
} from "recharts";
|
|
24
|
+
import { cn } from "../../lib/utils";
|
|
25
|
+
|
|
26
|
+
interface ChartCardProps {
|
|
27
|
+
title: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
data: any[];
|
|
30
|
+
dataKey: string;
|
|
31
|
+
categoryKey: string;
|
|
32
|
+
color?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
type?: "area" | "bar" | "line" | "scatter";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ChartCard({
|
|
39
|
+
title,
|
|
40
|
+
description,
|
|
41
|
+
data,
|
|
42
|
+
dataKey,
|
|
43
|
+
categoryKey,
|
|
44
|
+
color = "var(--primary)",
|
|
45
|
+
className,
|
|
46
|
+
loading,
|
|
47
|
+
type = "area",
|
|
48
|
+
}: ChartCardProps) {
|
|
49
|
+
if (loading) {
|
|
50
|
+
return (
|
|
51
|
+
<Card className={cn("col-span-full lg:col-span-2", className)}>
|
|
52
|
+
<CardHeader>
|
|
53
|
+
<Skeleton className="h-6 w-40 mb-2" />
|
|
54
|
+
<Skeleton className="h-4 w-64" />
|
|
55
|
+
</CardHeader>
|
|
56
|
+
<CardContent className="h-75 w-full pt-4">
|
|
57
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const renderChart = () => {
|
|
64
|
+
const commonProps = {
|
|
65
|
+
data,
|
|
66
|
+
margin: { top: 10, right: 10, left: -20, bottom: 0 },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const axisProps = {
|
|
70
|
+
xAxis: (
|
|
71
|
+
<XAxis
|
|
72
|
+
dataKey={categoryKey}
|
|
73
|
+
axisLine={false}
|
|
74
|
+
tickLine={false}
|
|
75
|
+
tick={{ fontSize: 12, fill: "var(--muted-foreground)" }}
|
|
76
|
+
dy={10}
|
|
77
|
+
type={type === "scatter" ? "number" : "category"}
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
yAxis: (
|
|
81
|
+
<YAxis
|
|
82
|
+
dataKey={type === "scatter" ? dataKey : undefined}
|
|
83
|
+
axisLine={false}
|
|
84
|
+
tickLine={false}
|
|
85
|
+
tick={{ fontSize: 12, fill: "var(--muted-foreground)" }}
|
|
86
|
+
type="number"
|
|
87
|
+
/>
|
|
88
|
+
),
|
|
89
|
+
grid: (
|
|
90
|
+
<CartesianGrid
|
|
91
|
+
strokeDasharray="3 3"
|
|
92
|
+
vertical={false}
|
|
93
|
+
stroke="var(--border)"
|
|
94
|
+
opacity={0.5}
|
|
95
|
+
/>
|
|
96
|
+
),
|
|
97
|
+
tooltip: (
|
|
98
|
+
<Tooltip
|
|
99
|
+
cursor={type === "scatter" ? { strokeDasharray: "3 3" } : undefined}
|
|
100
|
+
contentStyle={{
|
|
101
|
+
backgroundColor: "var(--card)",
|
|
102
|
+
borderRadius: "8px",
|
|
103
|
+
border: "1px solid var(--border)",
|
|
104
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
|
|
105
|
+
color: "var(--card-foreground)",
|
|
106
|
+
}}
|
|
107
|
+
itemStyle={{
|
|
108
|
+
color: "var(--card-foreground)",
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
switch (type) {
|
|
115
|
+
case "bar":
|
|
116
|
+
return (
|
|
117
|
+
<BarChart {...commonProps}>
|
|
118
|
+
{axisProps.grid}
|
|
119
|
+
{axisProps.xAxis}
|
|
120
|
+
{axisProps.yAxis}
|
|
121
|
+
{axisProps.tooltip}
|
|
122
|
+
<Bar
|
|
123
|
+
dataKey={dataKey}
|
|
124
|
+
fill={color}
|
|
125
|
+
radius={[4, 4, 0, 0]}
|
|
126
|
+
barSize={32}
|
|
127
|
+
/>
|
|
128
|
+
</BarChart>
|
|
129
|
+
);
|
|
130
|
+
case "line":
|
|
131
|
+
return (
|
|
132
|
+
<LineChart {...commonProps}>
|
|
133
|
+
{axisProps.grid}
|
|
134
|
+
{axisProps.xAxis}
|
|
135
|
+
{axisProps.yAxis}
|
|
136
|
+
{axisProps.tooltip}
|
|
137
|
+
<Line
|
|
138
|
+
type="monotone"
|
|
139
|
+
dataKey={dataKey}
|
|
140
|
+
stroke={color}
|
|
141
|
+
strokeWidth={2}
|
|
142
|
+
dot={{ r: 4, fill: color, strokeWidth: 2, stroke: "var(--card)" }}
|
|
143
|
+
activeDot={{ r: 6, strokeWidth: 0 }}
|
|
144
|
+
/>
|
|
145
|
+
</LineChart>
|
|
146
|
+
);
|
|
147
|
+
case "scatter":
|
|
148
|
+
return (
|
|
149
|
+
<ScatterChart {...commonProps}>
|
|
150
|
+
{axisProps.grid}
|
|
151
|
+
{axisProps.xAxis}
|
|
152
|
+
{axisProps.yAxis}
|
|
153
|
+
{axisProps.tooltip}
|
|
154
|
+
<Scatter name={title} data={data} fill={color} shape="circle" />
|
|
155
|
+
</ScatterChart>
|
|
156
|
+
);
|
|
157
|
+
case "area":
|
|
158
|
+
default:
|
|
159
|
+
return (
|
|
160
|
+
<AreaChart {...commonProps}>
|
|
161
|
+
<defs>
|
|
162
|
+
<linearGradient
|
|
163
|
+
id={`colorValue-${title.replace(/\s+/g, "-")}`}
|
|
164
|
+
x1="0"
|
|
165
|
+
y1="0"
|
|
166
|
+
x2="0"
|
|
167
|
+
y2="1"
|
|
168
|
+
>
|
|
169
|
+
<stop offset="5%" stopColor={color} stopOpacity={0.1} />
|
|
170
|
+
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
|
171
|
+
</linearGradient>
|
|
172
|
+
</defs>
|
|
173
|
+
{axisProps.grid}
|
|
174
|
+
{axisProps.xAxis}
|
|
175
|
+
{axisProps.yAxis}
|
|
176
|
+
{axisProps.tooltip}
|
|
177
|
+
<Area
|
|
178
|
+
type="monotone"
|
|
179
|
+
dataKey={dataKey}
|
|
180
|
+
stroke={color}
|
|
181
|
+
strokeWidth={2}
|
|
182
|
+
fillOpacity={1}
|
|
183
|
+
fill={`url(#colorValue-${title.replace(/\s+/g, "-")})`}
|
|
184
|
+
/>
|
|
185
|
+
</AreaChart>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Card
|
|
192
|
+
className={cn("bmj-card-hover col-span-full lg:col-span-2", className)}
|
|
193
|
+
>
|
|
194
|
+
<CardHeader>
|
|
195
|
+
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
|
|
196
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
197
|
+
</CardHeader>
|
|
198
|
+
<CardContent className="h-75 w-full pt-4">
|
|
199
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
200
|
+
{renderChart()}
|
|
201
|
+
</ResponsiveContainer>
|
|
202
|
+
</CardContent>
|
|
203
|
+
</Card>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { SidebarProvider, SidebarInset } from "../ui/sidebar";
|
|
3
|
+
import { AppSidebar, SidebarItem } from "./AppSidebar";
|
|
4
|
+
import { Topbar } from "./Topbar";
|
|
5
|
+
|
|
6
|
+
interface DashboardLayoutProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
sidebarItems?: SidebarItem[];
|
|
9
|
+
onSidebarItemClick?: (title: string) => void;
|
|
10
|
+
activeSidebarItem?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DashboardLayout({
|
|
14
|
+
children,
|
|
15
|
+
sidebarItems,
|
|
16
|
+
onSidebarItemClick,
|
|
17
|
+
activeSidebarItem,
|
|
18
|
+
}: DashboardLayoutProps) {
|
|
19
|
+
return (
|
|
20
|
+
<SidebarProvider>
|
|
21
|
+
<AppSidebar
|
|
22
|
+
items={sidebarItems}
|
|
23
|
+
onItemClick={onSidebarItemClick}
|
|
24
|
+
activeItem={activeSidebarItem}
|
|
25
|
+
/>
|
|
26
|
+
<SidebarInset className="flex flex-col">
|
|
27
|
+
<Topbar />
|
|
28
|
+
<main className="flex-1 p-6 overflow-y-auto">{children}</main>
|
|
29
|
+
</SidebarInset>
|
|
30
|
+
</SidebarProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
|
2
|
+
import { Skeleton } from "../ui/skeleton";
|
|
3
|
+
import { TrendingUp, TrendingDown, LucideIcon } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { motion } from "motion/react";
|
|
6
|
+
|
|
7
|
+
interface StatCardProps {
|
|
8
|
+
title: string;
|
|
9
|
+
value: string | number;
|
|
10
|
+
description?: string;
|
|
11
|
+
trend?: {
|
|
12
|
+
value: number;
|
|
13
|
+
isPositive: boolean;
|
|
14
|
+
};
|
|
15
|
+
icon?: LucideIcon;
|
|
16
|
+
className?: string;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function StatCard({
|
|
21
|
+
title,
|
|
22
|
+
value,
|
|
23
|
+
description,
|
|
24
|
+
trend,
|
|
25
|
+
icon: Icon,
|
|
26
|
+
className,
|
|
27
|
+
loading,
|
|
28
|
+
}: StatCardProps) {
|
|
29
|
+
if (loading) {
|
|
30
|
+
return (
|
|
31
|
+
<Card className={cn("overflow-hidden", className)}>
|
|
32
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
33
|
+
<Skeleton className="h-4 w-24" />
|
|
34
|
+
<Skeleton className="h-4 w-4 rounded-full" />
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<CardContent className="space-y-2">
|
|
37
|
+
<Skeleton className="h-8 w-32" />
|
|
38
|
+
<Skeleton className="h-3 w-40" />
|
|
39
|
+
</CardContent>
|
|
40
|
+
</Card>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<motion.div
|
|
46
|
+
initial={{ opacity: 0, y: 20 }}
|
|
47
|
+
animate={{ opacity: 1, y: 0 }}
|
|
48
|
+
transition={{ duration: 0.4 }}
|
|
49
|
+
>
|
|
50
|
+
<Card className={cn("bmj-card-hover overflow-hidden", className)}>
|
|
51
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
52
|
+
<CardTitle className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
|
53
|
+
{title}
|
|
54
|
+
</CardTitle>
|
|
55
|
+
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
|
56
|
+
</CardHeader>
|
|
57
|
+
<CardContent>
|
|
58
|
+
<div className="text-2xl font-bold tracking-tight">{value}</div>
|
|
59
|
+
{(description || trend) && (
|
|
60
|
+
<div className="flex items-center gap-2 mt-1">
|
|
61
|
+
{trend && (
|
|
62
|
+
<span
|
|
63
|
+
className={cn(
|
|
64
|
+
"flex items-center text-xs font-medium",
|
|
65
|
+
trend.isPositive ? "text-emerald-500" : "text-rose-500",
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{trend.isPositive ? (
|
|
69
|
+
<TrendingUp className="w-3 h-3 mr-1" />
|
|
70
|
+
) : (
|
|
71
|
+
<TrendingDown className="w-3 h-3 mr-1" />
|
|
72
|
+
)}
|
|
73
|
+
{trend.value}%
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
{description && (
|
|
77
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</CardContent>
|
|
82
|
+
</Card>
|
|
83
|
+
</motion.div>
|
|
84
|
+
);
|
|
85
|
+
}
|