ui-arreya-components 0.0.13 → 0.0.15
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/styles.css +1 -1
- package/dist/ui.cjs.js +62 -180
- package/dist/ui.es.js +6599 -24149
- package/dist/ui.umd.js +63 -181
- package/package.json +2 -2
- package/src/components/feature/header.stories.tsx +1 -1
- package/src/components/feature/header.tsx +6 -5
- package/src/components/index.ts +1 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/sheet.tsx +2 -0
- package/src/components/ui/sonner.tsx +2 -2
- package/src/index.css +8 -26
- package/src/index.ts +51 -50
- package/tailwind.config.js +2 -0
- package/.github/workflows/npm-publish.yml +0 -35
- package/scripts/build-index-ts.sh +0 -16
- package/src/components/feature/graph-card.stories.tsx +0 -138
- package/src/components/feature/graph-card.tsx +0 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ui-arreya-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"build-storybook": "concurrently 'npm run build-storybook:css' 'storybook build'",
|
|
25
25
|
"build-storybook:css": "tailwindcss -m -i ./src/styles/tailwind.css -o ./src/index.css",
|
|
26
26
|
"prepublish": "npm run build",
|
|
27
|
-
"
|
|
27
|
+
"publish": "npm publish"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"react": "^18.0.0",
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@/components/ui/breadcrumb"
|
|
10
10
|
import { ChevronRight, Home } from "lucide-react"
|
|
11
11
|
|
|
12
|
-
type BreadcrumbItem = {
|
|
12
|
+
export type BreadcrumbItem = {
|
|
13
13
|
label: string
|
|
14
14
|
href?: string
|
|
15
15
|
isCurrentPage?: boolean
|
|
@@ -26,7 +26,7 @@ interface HeaderProps {
|
|
|
26
26
|
export function Header({ title, breadcrumbItems = [], showHomeLink = true, className = "", children }: HeaderProps) {
|
|
27
27
|
return (
|
|
28
28
|
<header className={`border-b pb-4 ${className}`}>
|
|
29
|
-
<div className="container mx-auto
|
|
29
|
+
<div className="container mx-auto px-4">
|
|
30
30
|
{(breadcrumbItems.length > 0 || showHomeLink) && (
|
|
31
31
|
<Breadcrumb className="mb-2">
|
|
32
32
|
<BreadcrumbList>
|
|
@@ -66,9 +66,10 @@ export function Header({ title, breadcrumbItems = [], showHomeLink = true, class
|
|
|
66
66
|
</Breadcrumb>
|
|
67
67
|
)}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
<div className="flex items-center justify-between">
|
|
70
|
+
{title && <h1 className="text-2xl font-bold tracking-tight">{title}</h1>}
|
|
71
|
+
{children && <div className="flex items-center">{children}</div>}
|
|
72
|
+
</div>
|
|
72
73
|
</div>
|
|
73
74
|
</header>
|
|
74
75
|
)
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ui/accordion'
|
|
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
|
8
8
|
<input
|
|
9
9
|
type={type}
|
|
10
10
|
className={cn(
|
|
11
|
-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-
|
|
11
|
+
"flex h-9 w-full rounded-md border focus:border-[--primary] border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
ref={ref}
|
|
@@ -3,7 +3,7 @@ import { Toaster as Sonner } from "sonner"
|
|
|
3
3
|
|
|
4
4
|
type ToasterProps = React.ComponentProps<typeof Sonner>
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
|
7
7
|
const { theme = "system" } = useTheme()
|
|
8
8
|
|
|
9
9
|
return (
|
|
@@ -26,4 +26,4 @@ const SonnerToaster = ({ ...props }: ToasterProps) => {
|
|
|
26
26
|
)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export {
|
|
29
|
+
export { Toaster }
|
package/src/index.css
CHANGED
|
@@ -1009,18 +1009,10 @@ body{
|
|
|
1009
1009
|
height: 2.25rem;
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
|
-
.h-\[180px\]{
|
|
1013
|
-
height: 180px;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
1012
|
.h-\[1px\]{
|
|
1017
1013
|
height: 1px;
|
|
1018
1014
|
}
|
|
1019
1015
|
|
|
1020
|
-
.h-\[240px\]{
|
|
1021
|
-
height: 240px;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
1016
|
.h-\[var\(--radix-navigation-menu-viewport-height\)\]{
|
|
1025
1017
|
height: var(--radix-navigation-menu-viewport-height);
|
|
1026
1018
|
}
|
|
@@ -1546,10 +1538,6 @@ body{
|
|
|
1546
1538
|
border-width: 2px;
|
|
1547
1539
|
}
|
|
1548
1540
|
|
|
1549
|
-
.border-4{
|
|
1550
|
-
border-width: 4px;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
1541
|
.border-\[1\.5px\]{
|
|
1554
1542
|
border-width: 1.5px;
|
|
1555
1543
|
}
|
|
@@ -1780,10 +1768,6 @@ body{
|
|
|
1780
1768
|
padding-bottom: 0px;
|
|
1781
1769
|
}
|
|
1782
1770
|
|
|
1783
|
-
.pb-2{
|
|
1784
|
-
padding-bottom: 0.5rem;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
1771
|
.pb-3{
|
|
1788
1772
|
padding-bottom: 0.75rem;
|
|
1789
1773
|
}
|
|
@@ -1940,11 +1924,6 @@ body{
|
|
|
1940
1924
|
color: hsl(var(--destructive-foreground));
|
|
1941
1925
|
}
|
|
1942
1926
|
|
|
1943
|
-
.text-emerald-500{
|
|
1944
|
-
--tw-text-opacity: 1;
|
|
1945
|
-
color: rgb(16 185 129 / var(--tw-text-opacity, 1));
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
1927
|
.text-foreground{
|
|
1949
1928
|
color: var(--foreground);
|
|
1950
1929
|
}
|
|
@@ -1965,11 +1944,6 @@ body{
|
|
|
1965
1944
|
color: hsl(var(--primary-foreground));
|
|
1966
1945
|
}
|
|
1967
1946
|
|
|
1968
|
-
.text-red-500{
|
|
1969
|
-
--tw-text-opacity: 1;
|
|
1970
|
-
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
1947
|
.text-secondary-foreground{
|
|
1974
1948
|
color: var(--secondary-foreground);
|
|
1975
1949
|
}
|
|
@@ -2403,6 +2377,10 @@ h1, h2, h3, h4 {
|
|
|
2403
2377
|
background-color: hsl(var(--sidebar-border));
|
|
2404
2378
|
}
|
|
2405
2379
|
|
|
2380
|
+
.focus\:border-\[--primary\]:focus{
|
|
2381
|
+
border-color: var(--primary);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2406
2384
|
.focus\:bg-accent:focus{
|
|
2407
2385
|
background-color: var(--accent);
|
|
2408
2386
|
}
|
|
@@ -2477,6 +2455,10 @@ h1, h2, h3, h4 {
|
|
|
2477
2455
|
--tw-ring-offset-color: var(--background);
|
|
2478
2456
|
}
|
|
2479
2457
|
|
|
2458
|
+
.active\:border-\[--primary\]:active{
|
|
2459
|
+
border-color: var(--primary);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2480
2462
|
.active\:bg-sidebar-accent:active{
|
|
2481
2463
|
background-color: hsl(var(--sidebar-accent));
|
|
2482
2464
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export
|
|
50
|
-
|
|
1
|
+
export { Button } from './components/ui/button';
|
|
2
|
+
export {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogPortal,
|
|
5
|
+
DialogOverlay,
|
|
6
|
+
DialogTrigger,
|
|
7
|
+
DialogClose,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
} from './components/ui/dialog';
|
|
14
|
+
export {
|
|
15
|
+
Sidebar,
|
|
16
|
+
SidebarContent,
|
|
17
|
+
SidebarFooter,
|
|
18
|
+
SidebarGroup,
|
|
19
|
+
SidebarGroupAction,
|
|
20
|
+
SidebarGroupContent,
|
|
21
|
+
SidebarGroupLabel,
|
|
22
|
+
SidebarHeader,
|
|
23
|
+
SidebarInput,
|
|
24
|
+
SidebarInset,
|
|
25
|
+
SidebarMenu,
|
|
26
|
+
SidebarMenuAction,
|
|
27
|
+
SidebarMenuBadge,
|
|
28
|
+
SidebarMenuButton,
|
|
29
|
+
SidebarMenuItem,
|
|
30
|
+
SidebarMenuSkeleton,
|
|
31
|
+
SidebarMenuSub,
|
|
32
|
+
SidebarMenuSubButton,
|
|
33
|
+
SidebarMenuSubItem,
|
|
34
|
+
SidebarProvider,
|
|
35
|
+
SidebarRail,
|
|
36
|
+
SidebarSeparator,
|
|
37
|
+
SidebarTrigger,
|
|
38
|
+
useSidebar,
|
|
39
|
+
} from './components/ui/sidebar';
|
|
40
|
+
export {
|
|
41
|
+
Breadcrumb,
|
|
42
|
+
BreadcrumbList,
|
|
43
|
+
BreadcrumbItem,
|
|
44
|
+
BreadcrumbLink,
|
|
45
|
+
BreadcrumbPage,
|
|
46
|
+
BreadcrumbSeparator,
|
|
47
|
+
BreadcrumbEllipsis,
|
|
48
|
+
} from './components/ui/breadcrumb';
|
|
49
|
+
export {
|
|
50
|
+
Header
|
|
51
|
+
} from './components/feature/header'
|
package/tailwind.config.js
CHANGED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
-
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
-
|
|
4
|
-
name: Node.js Package
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
release:
|
|
8
|
-
types: [created]
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
build:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: 20
|
|
18
|
-
- run: npm ci
|
|
19
|
-
- run: npm test
|
|
20
|
-
- run: npm prepublish
|
|
21
|
-
|
|
22
|
-
publish-npm:
|
|
23
|
-
needs: build
|
|
24
|
-
runs-on: ubuntu-latest
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v4
|
|
27
|
-
- uses: actions/setup-node@v4
|
|
28
|
-
with:
|
|
29
|
-
node-version: 20
|
|
30
|
-
registry-url: https://registry.npmjs.org/
|
|
31
|
-
- run: npm ci
|
|
32
|
-
- run: npm prepublish
|
|
33
|
-
- run: npm publish
|
|
34
|
-
env:
|
|
35
|
-
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Starting fresh
|
|
4
|
-
rm src/index.ts
|
|
5
|
-
|
|
6
|
-
# Add Components
|
|
7
|
-
for import in `ls src/components/ui/ | grep -v '.stories.' | cut -f1 -d\.`;
|
|
8
|
-
do
|
|
9
|
-
echo 'export * from "./components/ui/'$import'"' >> src/index.ts
|
|
10
|
-
done
|
|
11
|
-
|
|
12
|
-
# Add Features
|
|
13
|
-
for import in `ls src/components/feature/ | grep -v '.stories.' | cut -f1 -d\.`;
|
|
14
|
-
do
|
|
15
|
-
echo 'export * from "./components/feature/'${import}'"' >> src/index.ts
|
|
16
|
-
done
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
-
import { GraphCard } from "./graph-card"
|
|
3
|
-
import { Area, AreaChart, ResponsiveContainer } from "recharts"
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof GraphCard> = {
|
|
6
|
-
title: "Feature/GraphCard",
|
|
7
|
-
component: GraphCard,
|
|
8
|
-
parameters: {
|
|
9
|
-
layout: "centered",
|
|
10
|
-
},
|
|
11
|
-
tags: ["autodocs"],
|
|
12
|
-
argTypes: {
|
|
13
|
-
title: {
|
|
14
|
-
control: "text",
|
|
15
|
-
description: "The title of the graph card",
|
|
16
|
-
},
|
|
17
|
-
description: {
|
|
18
|
-
control: "text",
|
|
19
|
-
description: "Optional description text below the title",
|
|
20
|
-
},
|
|
21
|
-
value: {
|
|
22
|
-
control: "text",
|
|
23
|
-
description: "The primary value to display (e.g., total, average)",
|
|
24
|
-
},
|
|
25
|
-
change: {
|
|
26
|
-
control: "number",
|
|
27
|
-
description: "Percentage change to display",
|
|
28
|
-
},
|
|
29
|
-
changeType: {
|
|
30
|
-
control: { type: "select" },
|
|
31
|
-
options: ["increase", "decrease", "neutral"],
|
|
32
|
-
description: "Type of change (affects color and icon)",
|
|
33
|
-
},
|
|
34
|
-
changeLabel: {
|
|
35
|
-
control: "text",
|
|
36
|
-
description: "Label to display after the change value",
|
|
37
|
-
},
|
|
38
|
-
isLoading: {
|
|
39
|
-
control: "boolean",
|
|
40
|
-
description: "Whether to show a loading state",
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export default meta
|
|
46
|
-
type Story = StoryObj<typeof GraphCard>
|
|
47
|
-
|
|
48
|
-
// Simple example with minimal data
|
|
49
|
-
export const Simple: Story = {
|
|
50
|
-
args: {
|
|
51
|
-
title: "Monthly Sales",
|
|
52
|
-
description: "Total sales for the current month",
|
|
53
|
-
value: "$12,345",
|
|
54
|
-
chart: (
|
|
55
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
56
|
-
<AreaChart
|
|
57
|
-
data={[
|
|
58
|
-
{ month: "Jan", value: 1000 },
|
|
59
|
-
{ month: "Feb", value: 2000 },
|
|
60
|
-
{ month: "Mar", value: 1500 },
|
|
61
|
-
{ month: "Apr", value: 3000 },
|
|
62
|
-
{ month: "May", value: 2500 },
|
|
63
|
-
{ month: "Jun", value: 4000 },
|
|
64
|
-
]}
|
|
65
|
-
>
|
|
66
|
-
<Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="hsl(var(--primary) / 0.2)" />
|
|
67
|
-
</AreaChart>
|
|
68
|
-
</ResponsiveContainer>
|
|
69
|
-
),
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Example with positive change
|
|
74
|
-
export const PositiveChange: Story = {
|
|
75
|
-
args: {
|
|
76
|
-
title: "Website Traffic",
|
|
77
|
-
description: "Unique visitors per day",
|
|
78
|
-
value: "12,456",
|
|
79
|
-
change: 23.5,
|
|
80
|
-
changeType: "increase",
|
|
81
|
-
changeLabel: "vs last week",
|
|
82
|
-
chart: (
|
|
83
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
84
|
-
<AreaChart
|
|
85
|
-
data={[
|
|
86
|
-
{ day: "Mon", value: 1000 },
|
|
87
|
-
{ day: "Tue", value: 2000 },
|
|
88
|
-
{ day: "Wed", value: 3000 },
|
|
89
|
-
{ day: "Thu", value: 2500 },
|
|
90
|
-
{ day: "Fri", value: 4000 },
|
|
91
|
-
{ day: "Sat", value: 3500 },
|
|
92
|
-
{ day: "Sun", value: 4500 },
|
|
93
|
-
]}
|
|
94
|
-
>
|
|
95
|
-
<Area type="monotone" dataKey="value" stroke="hsl(var(--primary))" fill="hsl(var(--primary) / 0.2)" />
|
|
96
|
-
</AreaChart>
|
|
97
|
-
</ResponsiveContainer>
|
|
98
|
-
),
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Example with negative change
|
|
103
|
-
export const NegativeChange: Story = {
|
|
104
|
-
args: {
|
|
105
|
-
title: "Conversion Rate",
|
|
106
|
-
description: "Percentage of visitors who make a purchase",
|
|
107
|
-
value: "3.2%",
|
|
108
|
-
change: 5.7,
|
|
109
|
-
changeType: "decrease",
|
|
110
|
-
changeLabel: "vs last month",
|
|
111
|
-
chart: (
|
|
112
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
113
|
-
<AreaChart
|
|
114
|
-
data={[
|
|
115
|
-
{ week: "W1", value: 4.5 },
|
|
116
|
-
{ week: "W2", value: 4.2 },
|
|
117
|
-
{ week: "W3", value: 3.8 },
|
|
118
|
-
{ week: "W4", value: 3.5 },
|
|
119
|
-
{ week: "W5", value: 3.2 },
|
|
120
|
-
]}
|
|
121
|
-
>
|
|
122
|
-
<Area type="monotone" dataKey="value" stroke="hsl(var(--destructive))" fill="hsl(var(--destructive) / 0.2)" />
|
|
123
|
-
</AreaChart>
|
|
124
|
-
</ResponsiveContainer>
|
|
125
|
-
),
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Loading state
|
|
130
|
-
export const Loading: Story = {
|
|
131
|
-
args: {
|
|
132
|
-
title: "Revenue",
|
|
133
|
-
description: "Total revenue this quarter",
|
|
134
|
-
value: "$143,245",
|
|
135
|
-
isLoading: true,
|
|
136
|
-
chart: <div />, // This won't be shown in loading state
|
|
137
|
-
},
|
|
138
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type React from "react"
|
|
2
|
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
3
|
-
import { ChartContainer } from "@/components/ui/chart"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
import { ArrowDownIcon, ArrowUpIcon, MoreHorizontal } from "lucide-react"
|
|
6
|
-
import { Button } from "@/components/ui/button"
|
|
7
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
|
8
|
-
|
|
9
|
-
export interface GraphCardProps {
|
|
10
|
-
title: string
|
|
11
|
-
description?: string
|
|
12
|
-
chart?: React.ReactNode | any
|
|
13
|
-
value?: string | number
|
|
14
|
-
previousValue?: string | number
|
|
15
|
-
change?: number
|
|
16
|
-
changeType?: "increase" | "decrease" | "neutral"
|
|
17
|
-
changeLabel?: string
|
|
18
|
-
className?: string
|
|
19
|
-
footerContent?: React.ReactNode
|
|
20
|
-
menuItems?: Array<{ label: string; onClick: () => void }>
|
|
21
|
-
chartConfig?: Record<string, any>
|
|
22
|
-
isLoading?: boolean
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function GraphCard({
|
|
26
|
-
title,
|
|
27
|
-
description,
|
|
28
|
-
chart,
|
|
29
|
-
value,
|
|
30
|
-
previousValue,
|
|
31
|
-
change,
|
|
32
|
-
changeType,
|
|
33
|
-
changeLabel,
|
|
34
|
-
className,
|
|
35
|
-
footerContent,
|
|
36
|
-
menuItems,
|
|
37
|
-
chartConfig,
|
|
38
|
-
isLoading = false,
|
|
39
|
-
}: GraphCardProps) {
|
|
40
|
-
const renderChangeIndicator = () => {
|
|
41
|
-
if (changeType === "increase") {
|
|
42
|
-
return <ArrowUpIcon className="h-4 w-4 text-emerald-500" />
|
|
43
|
-
} else if (changeType === "decrease") {
|
|
44
|
-
return <ArrowDownIcon className="h-4 w-4 text-red-500" />
|
|
45
|
-
}
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const getChangeTextColor = () => {
|
|
50
|
-
if (changeType === "increase") return "text-emerald-500"
|
|
51
|
-
if (changeType === "decrease") return "text-red-500"
|
|
52
|
-
return "text-muted-foreground"
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<Card className={cn("overflow-hidden", className)}>
|
|
57
|
-
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
58
|
-
<div>
|
|
59
|
-
<CardTitle className="text-base font-medium">{title}</CardTitle>
|
|
60
|
-
{description && <CardDescription>{description}</CardDescription>}
|
|
61
|
-
</div>
|
|
62
|
-
{menuItems && menuItems.length > 0 && (
|
|
63
|
-
<DropdownMenu>
|
|
64
|
-
<DropdownMenuTrigger asChild>
|
|
65
|
-
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
66
|
-
<span className="sr-only">Open menu</span>
|
|
67
|
-
<MoreHorizontal className="h-4 w-4" />
|
|
68
|
-
</Button>
|
|
69
|
-
</DropdownMenuTrigger>
|
|
70
|
-
<DropdownMenuContent align="end">
|
|
71
|
-
{menuItems.map((item, index) => (
|
|
72
|
-
<DropdownMenuItem key={index} onClick={item.onClick}>
|
|
73
|
-
{item.label}
|
|
74
|
-
</DropdownMenuItem>
|
|
75
|
-
))}
|
|
76
|
-
</DropdownMenuContent>
|
|
77
|
-
</DropdownMenu>
|
|
78
|
-
)}
|
|
79
|
-
</CardHeader>
|
|
80
|
-
<CardContent className="pb-2">
|
|
81
|
-
{value && (
|
|
82
|
-
<div className="flex items-center justify-between">
|
|
83
|
-
<div className="space-y-1">
|
|
84
|
-
<p className="text-2xl font-bold">{value}</p>
|
|
85
|
-
{(change !== undefined || previousValue) && (
|
|
86
|
-
<div className="flex items-center text-xs">
|
|
87
|
-
{renderChangeIndicator()}
|
|
88
|
-
<span className={cn("ml-1", getChangeTextColor())}>
|
|
89
|
-
{change !== undefined && `${Math.abs(change)}%`}
|
|
90
|
-
{previousValue && !change && `from ${previousValue}`}
|
|
91
|
-
</span>
|
|
92
|
-
{changeLabel && <span className="ml-1 text-muted-foreground">{changeLabel}</span>}
|
|
93
|
-
</div>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
<div className={cn("mt-4", value ? "h-[180px]" : "h-[240px]")}>
|
|
99
|
-
{isLoading ? (
|
|
100
|
-
<div className="flex h-full items-center justify-center">
|
|
101
|
-
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
|
102
|
-
</div>
|
|
103
|
-
) : (chartConfig && chart) ? (
|
|
104
|
-
<ChartContainer config={chartConfig}>{ chart }</ChartContainer>
|
|
105
|
-
) : (
|
|
106
|
-
chart
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
</CardContent>
|
|
110
|
-
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
|
111
|
-
</Card>
|
|
112
|
-
)
|
|
113
|
-
}
|