create-cronus-app 0.6.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/README.md +173 -0
- package/dist/compose.d.ts +56 -0
- package/dist/compose.js +92 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +255 -0
- package/dist/scaffold.d.ts +31 -0
- package/dist/scaffold.js +118 -0
- package/dist/utils.d.ts +121 -0
- package/dist/utils.js +285 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +3 -0
- package/package.json +56 -0
- package/templates/dashboard/README.md +61 -0
- package/templates/dashboard/app/globals.css +16 -0
- package/templates/dashboard/app/layout.tsx +40 -0
- package/templates/dashboard/app/page.tsx +77 -0
- package/templates/dashboard/app/settings/page.tsx +21 -0
- package/templates/dashboard/components/dashboard-shell.tsx +120 -0
- package/templates/dashboard/components/orders-table.tsx +132 -0
- package/templates/dashboard/components/revenue-chart.tsx +50 -0
- package/templates/dashboard/components/settings-form.tsx +167 -0
- package/templates/dashboard/cronus-ui.json +17 -0
- package/templates/dashboard/gitignore +34 -0
- package/templates/dashboard/next.config.mjs +11 -0
- package/templates/dashboard/package.json +30 -0
- package/templates/dashboard/postcss.config.mjs +7 -0
- package/templates/dashboard/tsconfig.json +23 -0
- package/templates/default/README.md +46 -0
- package/templates/default/app/globals.css +16 -0
- package/templates/default/app/layout.tsx +39 -0
- package/templates/default/app/page.tsx +112 -0
- package/templates/default/cronus-ui.json +17 -0
- package/templates/default/gitignore +34 -0
- package/templates/default/next.config.mjs +11 -0
- package/templates/default/package.json +27 -0
- package/templates/default/postcss.config.mjs +7 -0
- package/templates/default/tsconfig.json +23 -0
- package/templates/marketing/README.md +65 -0
- package/templates/marketing/app/globals.css +16 -0
- package/templates/marketing/app/layout.tsx +39 -0
- package/templates/marketing/app/page.tsx +25 -0
- package/templates/marketing/components/faq.tsx +62 -0
- package/templates/marketing/components/feature-grid.tsx +67 -0
- package/templates/marketing/components/hero.tsx +61 -0
- package/templates/marketing/components/pricing.tsx +116 -0
- package/templates/marketing/components/site-footer.tsx +91 -0
- package/templates/marketing/components/site-header.tsx +47 -0
- package/templates/marketing/components/testimonials.tsx +91 -0
- package/templates/marketing/components/waitlist-cta.tsx +93 -0
- package/templates/marketing/cronus-ui.json +17 -0
- package/templates/marketing/gitignore +34 -0
- package/templates/marketing/next.config.mjs +11 -0
- package/templates/marketing/package.json +28 -0
- package/templates/marketing/postcss.config.mjs +7 -0
- package/templates/marketing/tsconfig.json +23 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CronusThemeScript, CronusUIProvider } from "@cronus-ui/theme";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { DashboardShell } from "../components/dashboard-shell";
|
|
5
|
+
import "./globals.css";
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: "__APP_NAME__",
|
|
9
|
+
description: "A dashboard built with Cronus UI.",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
13
|
+
return (
|
|
14
|
+
<html
|
|
15
|
+
lang="en"
|
|
16
|
+
// CronusThemeScript mutates <html> (theme/mode/dark class) before hydration
|
|
17
|
+
// to prevent a flash of the wrong theme, so React must not warn about the
|
|
18
|
+
// resulting attribute mismatch.
|
|
19
|
+
suppressHydrationWarning
|
|
20
|
+
>
|
|
21
|
+
<head>
|
|
22
|
+
<CronusThemeScript
|
|
23
|
+
storageKey="theme"
|
|
24
|
+
defaultThemeName="__THEME__"
|
|
25
|
+
defaultModeName="__MODE__"
|
|
26
|
+
/>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<CronusUIProvider
|
|
30
|
+
asRoot
|
|
31
|
+
defaultThemeName="__THEME__"
|
|
32
|
+
defaultModeName="__MODE__"
|
|
33
|
+
storageKey="theme"
|
|
34
|
+
>
|
|
35
|
+
<DashboardShell>{children}</DashboardShell>
|
|
36
|
+
</CronusUIProvider>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Badge } from "@cronus-ui/ui/badge";
|
|
2
|
+
import { Card, CardContent } from "@cronus-ui/ui/card";
|
|
3
|
+
import { Metric, MetricDelta, MetricLabel, MetricValue } from "@cronus-ui/ui/metric";
|
|
4
|
+
import { Activity, DollarSign, TrendingDown, Users } from "lucide-react";
|
|
5
|
+
import { OrdersTable } from "../components/orders-table";
|
|
6
|
+
import { RevenueChart } from "../components/revenue-chart";
|
|
7
|
+
|
|
8
|
+
const KPIS = [
|
|
9
|
+
{
|
|
10
|
+
label: "Total revenue",
|
|
11
|
+
value: "$84,210",
|
|
12
|
+
delta: "+14.2%",
|
|
13
|
+
trend: "up",
|
|
14
|
+
icon: DollarSign,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
label: "Active users",
|
|
18
|
+
value: "12,840",
|
|
19
|
+
delta: "+6.8%",
|
|
20
|
+
trend: "up",
|
|
21
|
+
icon: Users,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Conversion",
|
|
25
|
+
value: "4.21%",
|
|
26
|
+
delta: "+0.9%",
|
|
27
|
+
trend: "up",
|
|
28
|
+
icon: Activity,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Churn",
|
|
32
|
+
value: "1.64%",
|
|
33
|
+
delta: "-0.4%",
|
|
34
|
+
trend: "down",
|
|
35
|
+
icon: TrendingDown,
|
|
36
|
+
},
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export default function OverviewPage() {
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex flex-col gap-6 p-6">
|
|
42
|
+
<header className="flex flex-wrap items-end justify-between gap-3">
|
|
43
|
+
<div className="flex flex-col gap-1">
|
|
44
|
+
<h1 className="font-display text-2xl font-semibold tracking-tight text-fg">Overview</h1>
|
|
45
|
+
<p className="text-sm text-fg-secondary">
|
|
46
|
+
Performance across your workspace, last 7 months.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<Badge variant="success">Live</Badge>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
{/* KPI grid */}
|
|
53
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(100%,12rem),1fr))] gap-4">
|
|
54
|
+
{KPIS.map(({ label, value, delta, trend, icon: Icon }) => (
|
|
55
|
+
<Card key={label} className="gap-4 py-5">
|
|
56
|
+
<CardContent className="flex flex-col gap-3">
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<span className="inline-flex size-9 items-center justify-center rounded-lg bg-surface-overlay text-fg-secondary">
|
|
59
|
+
<Icon className="size-4" aria-hidden="true" />
|
|
60
|
+
</span>
|
|
61
|
+
<MetricDelta trend={trend}>{delta}</MetricDelta>
|
|
62
|
+
</div>
|
|
63
|
+
<Metric className="gap-1.5">
|
|
64
|
+
<MetricLabel>{label}</MetricLabel>
|
|
65
|
+
<MetricValue className="text-3xl">{value}</MetricValue>
|
|
66
|
+
</Metric>
|
|
67
|
+
</CardContent>
|
|
68
|
+
</Card>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Chart + orders */}
|
|
73
|
+
<RevenueChart />
|
|
74
|
+
<OrdersTable />
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { SettingsForm } from "../../components/settings-form";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "Settings — __APP_NAME__",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function SettingsPage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex flex-col gap-6 p-6">
|
|
11
|
+
<header className="flex flex-col gap-1">
|
|
12
|
+
<h1 className="font-display text-2xl font-semibold tracking-tight text-fg">Settings</h1>
|
|
13
|
+
<p className="text-sm text-fg-secondary">
|
|
14
|
+
Manage your profile, workspace preferences, and notifications.
|
|
15
|
+
</p>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<SettingsForm />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "@cronus-ui/theme";
|
|
4
|
+
import { AppShell } from "@cronus-ui/ui/app-shell";
|
|
5
|
+
import { Avatar, AvatarFallback } from "@cronus-ui/ui/avatar";
|
|
6
|
+
import { Button } from "@cronus-ui/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Sidebar,
|
|
9
|
+
SidebarContent,
|
|
10
|
+
SidebarFooter,
|
|
11
|
+
SidebarGroup,
|
|
12
|
+
SidebarGroupContent,
|
|
13
|
+
SidebarGroupLabel,
|
|
14
|
+
SidebarHeader,
|
|
15
|
+
SidebarMenu,
|
|
16
|
+
SidebarMenuButton,
|
|
17
|
+
SidebarMenuItem,
|
|
18
|
+
SidebarSeparator,
|
|
19
|
+
SidebarTrigger,
|
|
20
|
+
} from "@cronus-ui/ui/sidebar";
|
|
21
|
+
import { Bell, LayoutDashboard, Moon, Settings, Sparkles, Sun } from "lucide-react";
|
|
22
|
+
import Link from "next/link";
|
|
23
|
+
import { usePathname } from "next/navigation";
|
|
24
|
+
import type { ReactNode } from "react";
|
|
25
|
+
|
|
26
|
+
const NAV_ITEMS = [
|
|
27
|
+
{ href: "/", label: "Overview", icon: LayoutDashboard },
|
|
28
|
+
{ href: "/settings", label: "Settings", icon: Settings },
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The persistent application chrome: an icon-collapsible sidebar with the
|
|
33
|
+
* route-aware nav, plus a sticky topbar with the sidebar trigger, a color-mode
|
|
34
|
+
* toggle, and the current user. Pages render inside as `children`.
|
|
35
|
+
*/
|
|
36
|
+
export function DashboardShell({ children }: { children: ReactNode }) {
|
|
37
|
+
const pathname = usePathname();
|
|
38
|
+
const { mode, toggleMode } = useTheme();
|
|
39
|
+
|
|
40
|
+
const sidebar = (
|
|
41
|
+
<Sidebar collapsible="icon" aria-label="Primary">
|
|
42
|
+
<SidebarHeader>
|
|
43
|
+
<div className="flex items-center gap-2 px-1.5 py-1.5 group-data-[collapsible=icon]/sidebar:justify-center group-data-[collapsible=icon]/sidebar:px-0">
|
|
44
|
+
<span className="inline-flex size-8 shrink-0 items-center justify-center rounded-lg bg-gradient-primary text-primary-foreground shadow-glow">
|
|
45
|
+
<Sparkles className="size-4" aria-hidden="true" />
|
|
46
|
+
</span>
|
|
47
|
+
<span className="truncate text-sm font-semibold text-fg group-data-[collapsible=icon]/sidebar:hidden">
|
|
48
|
+
__APP_NAME__
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</SidebarHeader>
|
|
52
|
+
|
|
53
|
+
<SidebarSeparator />
|
|
54
|
+
|
|
55
|
+
<SidebarContent>
|
|
56
|
+
<SidebarGroup>
|
|
57
|
+
<SidebarGroupLabel>Workspace</SidebarGroupLabel>
|
|
58
|
+
<SidebarGroupContent>
|
|
59
|
+
<SidebarMenu>
|
|
60
|
+
{NAV_ITEMS.map((item) => (
|
|
61
|
+
<SidebarMenuItem key={item.href}>
|
|
62
|
+
<SidebarMenuButton asChild isActive={pathname === item.href} tooltip={item.label}>
|
|
63
|
+
<Link href={item.href}>
|
|
64
|
+
<item.icon aria-hidden="true" />
|
|
65
|
+
<span>{item.label}</span>
|
|
66
|
+
</Link>
|
|
67
|
+
</SidebarMenuButton>
|
|
68
|
+
</SidebarMenuItem>
|
|
69
|
+
))}
|
|
70
|
+
</SidebarMenu>
|
|
71
|
+
</SidebarGroupContent>
|
|
72
|
+
</SidebarGroup>
|
|
73
|
+
</SidebarContent>
|
|
74
|
+
|
|
75
|
+
<SidebarFooter>
|
|
76
|
+
<div className="flex items-center gap-2 rounded-lg px-2 py-1.5 group-data-[collapsible=icon]/sidebar:justify-center group-data-[collapsible=icon]/sidebar:px-0">
|
|
77
|
+
<Avatar className="size-8">
|
|
78
|
+
<AvatarFallback>AC</AvatarFallback>
|
|
79
|
+
</Avatar>
|
|
80
|
+
<div className="flex min-w-0 flex-col group-data-[collapsible=icon]/sidebar:hidden">
|
|
81
|
+
<span className="truncate text-sm font-medium text-fg">Alex Costa</span>
|
|
82
|
+
<span className="truncate text-xs text-fg-tertiary">alex@example.com</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</SidebarFooter>
|
|
86
|
+
</Sidebar>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const header = (
|
|
90
|
+
<div className="flex w-full items-center gap-3">
|
|
91
|
+
<SidebarTrigger />
|
|
92
|
+
<span className="text-sm font-semibold text-fg">
|
|
93
|
+
{NAV_ITEMS.find((item) => item.href === pathname)?.label ?? "__APP_NAME__"}
|
|
94
|
+
</span>
|
|
95
|
+
<div className="ml-auto flex items-center gap-1">
|
|
96
|
+
<Button
|
|
97
|
+
variant="ghost"
|
|
98
|
+
size="icon-sm"
|
|
99
|
+
aria-label={mode === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
100
|
+
onClick={toggleMode}
|
|
101
|
+
>
|
|
102
|
+
{mode === "dark" ? (
|
|
103
|
+
<Moon className="size-4" aria-hidden="true" />
|
|
104
|
+
) : (
|
|
105
|
+
<Sun className="size-4" aria-hidden="true" />
|
|
106
|
+
)}
|
|
107
|
+
</Button>
|
|
108
|
+
<Button variant="ghost" size="icon-sm" aria-label="Notifications">
|
|
109
|
+
<Bell className="size-4" aria-hidden="true" />
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<AppShell sidebar={sidebar} header={header}>
|
|
117
|
+
<main id="main-content">{children}</main>
|
|
118
|
+
</AppShell>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@cronus-ui/ui/badge";
|
|
4
|
+
import { Card, CardHeader, CardTitle } from "@cronus-ui/ui/card";
|
|
5
|
+
import { DataTable, DataTableColumnHeader } from "@cronus-ui/ui/data-table";
|
|
6
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
7
|
+
|
|
8
|
+
interface Order {
|
|
9
|
+
id: string;
|
|
10
|
+
customer: string;
|
|
11
|
+
email: string;
|
|
12
|
+
status: "Paid" | "Pending" | "Refunded";
|
|
13
|
+
amount: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STATUS_VARIANT = {
|
|
17
|
+
Paid: "success",
|
|
18
|
+
Pending: "warning",
|
|
19
|
+
Refunded: "secondary",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
const ORDERS: Order[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "INV-2048",
|
|
25
|
+
customer: "Mara Castillo",
|
|
26
|
+
email: "mara@northwind.io",
|
|
27
|
+
status: "Paid",
|
|
28
|
+
amount: 1290,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "INV-2047",
|
|
32
|
+
customer: "Devon Lane",
|
|
33
|
+
email: "devon@acme.dev",
|
|
34
|
+
status: "Pending",
|
|
35
|
+
amount: 640,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "INV-2046",
|
|
39
|
+
customer: "Priya Sharma",
|
|
40
|
+
email: "priya@lumon.co",
|
|
41
|
+
status: "Paid",
|
|
42
|
+
amount: 2180,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "INV-2045",
|
|
46
|
+
customer: "Tobias Funke",
|
|
47
|
+
email: "tobias@bluth.com",
|
|
48
|
+
status: "Refunded",
|
|
49
|
+
amount: 320,
|
|
50
|
+
},
|
|
51
|
+
{ id: "INV-2044", customer: "Aiko Tanaka", email: "aiko@hooli.com", status: "Paid", amount: 990 },
|
|
52
|
+
{
|
|
53
|
+
id: "INV-2043",
|
|
54
|
+
customer: "Jonas Berg",
|
|
55
|
+
email: "jonas@fathom.dev",
|
|
56
|
+
status: "Paid",
|
|
57
|
+
amount: 1475,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "INV-2042",
|
|
61
|
+
customer: "Sofia Lindqvist",
|
|
62
|
+
email: "sofia@driftwood.se",
|
|
63
|
+
status: "Pending",
|
|
64
|
+
amount: 210,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "INV-2041",
|
|
68
|
+
customer: "David Chen",
|
|
69
|
+
email: "david@parallel.io",
|
|
70
|
+
status: "Paid",
|
|
71
|
+
amount: 3120,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
|
|
76
|
+
|
|
77
|
+
const COLUMNS: ColumnDef<Order>[] = [
|
|
78
|
+
{
|
|
79
|
+
accessorKey: "id",
|
|
80
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title="Order" />,
|
|
81
|
+
cell: ({ row }) => <span className="font-medium text-fg">{row.original.id}</span>,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
accessorKey: "customer",
|
|
85
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title="Customer" />,
|
|
86
|
+
cell: ({ row }) => (
|
|
87
|
+
<div className="flex min-w-0 flex-col">
|
|
88
|
+
<span className="truncate text-sm font-medium text-fg">{row.original.customer}</span>
|
|
89
|
+
<span className="truncate text-xs text-fg-tertiary">{row.original.email}</span>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
accessorKey: "status",
|
|
95
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
|
|
96
|
+
cell: ({ row }) => (
|
|
97
|
+
<Badge variant={STATUS_VARIANT[row.original.status]}>{row.original.status}</Badge>
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
accessorKey: "amount",
|
|
102
|
+
header: ({ column }) => (
|
|
103
|
+
<DataTableColumnHeader column={column} title="Amount" className="justify-end" />
|
|
104
|
+
),
|
|
105
|
+
cell: ({ row }) => (
|
|
106
|
+
<span className="block text-right font-medium tabular-nums text-fg">
|
|
107
|
+
{currency.format(row.original.amount)}
|
|
108
|
+
</span>
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
/** Recent orders in a searchable, sortable, paginated `DataTable`. */
|
|
114
|
+
export function OrdersTable() {
|
|
115
|
+
return (
|
|
116
|
+
<Card className="gap-0 py-0">
|
|
117
|
+
<CardHeader className="px-6 py-5">
|
|
118
|
+
<CardTitle className="font-display text-base">Recent orders</CardTitle>
|
|
119
|
+
<p className="text-sm text-fg-secondary">Latest invoices and payments</p>
|
|
120
|
+
</CardHeader>
|
|
121
|
+
<DataTable
|
|
122
|
+
columns={COLUMNS}
|
|
123
|
+
data={ORDERS}
|
|
124
|
+
searchable
|
|
125
|
+
searchPlaceholder="Search orders…"
|
|
126
|
+
pagination
|
|
127
|
+
initialPageSize={5}
|
|
128
|
+
className="px-6 pb-6"
|
|
129
|
+
/>
|
|
130
|
+
</Card>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@cronus-ui/ui/card";
|
|
4
|
+
import {
|
|
5
|
+
type ChartConfig,
|
|
6
|
+
ChartContainer,
|
|
7
|
+
ChartTooltip,
|
|
8
|
+
ChartTooltipContent,
|
|
9
|
+
} from "@cronus-ui/ui/chart";
|
|
10
|
+
import { MetricDelta } from "@cronus-ui/ui/metric";
|
|
11
|
+
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
|
12
|
+
|
|
13
|
+
const REVENUE_DATA = [
|
|
14
|
+
{ month: "Jan", revenue: 42100 },
|
|
15
|
+
{ month: "Feb", revenue: 48400 },
|
|
16
|
+
{ month: "Mar", revenue: 45900 },
|
|
17
|
+
{ month: "Apr", revenue: 56200 },
|
|
18
|
+
{ month: "May", revenue: 61800 },
|
|
19
|
+
{ month: "Jun", revenue: 72400 },
|
|
20
|
+
{ month: "Jul", revenue: 84210 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const chartConfig = {
|
|
24
|
+
revenue: { label: "Revenue", color: "var(--color-chart-1)" },
|
|
25
|
+
} satisfies ChartConfig;
|
|
26
|
+
|
|
27
|
+
/** Monthly recurring revenue as a themed bar chart (recharts via `chart`). */
|
|
28
|
+
export function RevenueChart() {
|
|
29
|
+
return (
|
|
30
|
+
<Card>
|
|
31
|
+
<CardHeader className="flex-row items-center justify-between gap-3">
|
|
32
|
+
<div className="flex flex-col gap-1">
|
|
33
|
+
<CardTitle className="font-display text-base">Revenue trend</CardTitle>
|
|
34
|
+
<p className="text-sm text-fg-secondary">Monthly recurring revenue</p>
|
|
35
|
+
</div>
|
|
36
|
+
<MetricDelta trend="up">+99.8% YoY</MetricDelta>
|
|
37
|
+
</CardHeader>
|
|
38
|
+
<CardContent>
|
|
39
|
+
<ChartContainer config={chartConfig} className="aspect-[16/6] w-full">
|
|
40
|
+
<BarChart accessibilityLayer data={REVENUE_DATA} margin={{ left: 4, right: 4 }}>
|
|
41
|
+
<CartesianGrid vertical={false} />
|
|
42
|
+
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} fontSize={12} />
|
|
43
|
+
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
|
44
|
+
<Bar dataKey="revenue" fill="var(--color-chart-1)" radius={[6, 6, 0, 0]} />
|
|
45
|
+
</BarChart>
|
|
46
|
+
</ChartContainer>
|
|
47
|
+
</CardContent>
|
|
48
|
+
</Card>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@cronus-ui/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardFooter,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@cronus-ui/ui/card";
|
|
12
|
+
import { Field, FieldDescription, FieldLabel } from "@cronus-ui/ui/field";
|
|
13
|
+
import { Input } from "@cronus-ui/ui/input";
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "@cronus-ui/ui/select";
|
|
21
|
+
import { Separator } from "@cronus-ui/ui/separator";
|
|
22
|
+
import { Switch } from "@cronus-ui/ui/switch";
|
|
23
|
+
import { Textarea } from "@cronus-ui/ui/textarea";
|
|
24
|
+
import { Check } from "lucide-react";
|
|
25
|
+
import { useState } from "react";
|
|
26
|
+
|
|
27
|
+
const NOTIFICATIONS = [
|
|
28
|
+
{
|
|
29
|
+
id: "notify-orders",
|
|
30
|
+
label: "New orders",
|
|
31
|
+
description: "Get notified when a new order comes in.",
|
|
32
|
+
defaultChecked: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "notify-digest",
|
|
36
|
+
label: "Weekly digest",
|
|
37
|
+
description: "A summary of your workspace, every Monday.",
|
|
38
|
+
defaultChecked: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "notify-mentions",
|
|
42
|
+
label: "Mentions",
|
|
43
|
+
description: "When a teammate mentions you in a comment.",
|
|
44
|
+
defaultChecked: false,
|
|
45
|
+
},
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Profile + workspace preference forms. The submit is a demo — replace the
|
|
50
|
+
* `onSubmit` with a server action or API call.
|
|
51
|
+
*/
|
|
52
|
+
export function SettingsForm() {
|
|
53
|
+
const [saved, setSaved] = useState(false);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<form
|
|
57
|
+
className="flex max-w-3xl flex-col gap-6"
|
|
58
|
+
onSubmit={(event) => {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
setSaved(true);
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle className="font-display text-base">Profile</CardTitle>
|
|
66
|
+
<CardDescription>How you appear across the workspace.</CardDescription>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent className="grid gap-5 sm:grid-cols-2">
|
|
69
|
+
<Field>
|
|
70
|
+
<FieldLabel htmlFor="settings-name">Full name</FieldLabel>
|
|
71
|
+
<Input id="settings-name" name="name" defaultValue="Alex Costa" autoComplete="name" />
|
|
72
|
+
</Field>
|
|
73
|
+
<Field>
|
|
74
|
+
<FieldLabel htmlFor="settings-email">Email</FieldLabel>
|
|
75
|
+
<Input
|
|
76
|
+
id="settings-email"
|
|
77
|
+
name="email"
|
|
78
|
+
type="email"
|
|
79
|
+
defaultValue="alex@example.com"
|
|
80
|
+
autoComplete="email"
|
|
81
|
+
/>
|
|
82
|
+
</Field>
|
|
83
|
+
<Field className="sm:col-span-2">
|
|
84
|
+
<FieldLabel htmlFor="settings-bio">Bio</FieldLabel>
|
|
85
|
+
<Textarea
|
|
86
|
+
id="settings-bio"
|
|
87
|
+
name="bio"
|
|
88
|
+
rows={3}
|
|
89
|
+
placeholder="A short description for your profile."
|
|
90
|
+
/>
|
|
91
|
+
<FieldDescription>Shown on your public profile.</FieldDescription>
|
|
92
|
+
</Field>
|
|
93
|
+
</CardContent>
|
|
94
|
+
</Card>
|
|
95
|
+
|
|
96
|
+
<Card>
|
|
97
|
+
<CardHeader>
|
|
98
|
+
<CardTitle className="font-display text-base">Workspace</CardTitle>
|
|
99
|
+
<CardDescription>Defaults applied to everyone in this workspace.</CardDescription>
|
|
100
|
+
</CardHeader>
|
|
101
|
+
<CardContent className="grid gap-5 sm:grid-cols-2">
|
|
102
|
+
<Field>
|
|
103
|
+
<FieldLabel htmlFor="settings-language">Language</FieldLabel>
|
|
104
|
+
<Select defaultValue="en" name="language">
|
|
105
|
+
<SelectTrigger id="settings-language" className="w-full">
|
|
106
|
+
<SelectValue placeholder="Select a language" />
|
|
107
|
+
</SelectTrigger>
|
|
108
|
+
<SelectContent>
|
|
109
|
+
<SelectItem value="en">English</SelectItem>
|
|
110
|
+
<SelectItem value="pt">Português</SelectItem>
|
|
111
|
+
<SelectItem value="es">Español</SelectItem>
|
|
112
|
+
</SelectContent>
|
|
113
|
+
</Select>
|
|
114
|
+
</Field>
|
|
115
|
+
<Field>
|
|
116
|
+
<FieldLabel htmlFor="settings-timezone">Timezone</FieldLabel>
|
|
117
|
+
<Select defaultValue="utc" name="timezone">
|
|
118
|
+
<SelectTrigger id="settings-timezone" className="w-full">
|
|
119
|
+
<SelectValue placeholder="Select a timezone" />
|
|
120
|
+
</SelectTrigger>
|
|
121
|
+
<SelectContent>
|
|
122
|
+
<SelectItem value="utc">UTC</SelectItem>
|
|
123
|
+
<SelectItem value="sao-paulo">America/São Paulo</SelectItem>
|
|
124
|
+
<SelectItem value="new-york">America/New York</SelectItem>
|
|
125
|
+
<SelectItem value="london">Europe/London</SelectItem>
|
|
126
|
+
</SelectContent>
|
|
127
|
+
</Select>
|
|
128
|
+
</Field>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
|
|
132
|
+
<Card>
|
|
133
|
+
<CardHeader>
|
|
134
|
+
<CardTitle className="font-display text-base">Notifications</CardTitle>
|
|
135
|
+
<CardDescription>Choose what you want to hear about.</CardDescription>
|
|
136
|
+
</CardHeader>
|
|
137
|
+
<CardContent className="flex flex-col">
|
|
138
|
+
{NOTIFICATIONS.map((item, index) => (
|
|
139
|
+
<div key={item.id}>
|
|
140
|
+
{index > 0 ? <Separator className="my-4" /> : null}
|
|
141
|
+
<div className="flex items-center justify-between gap-4">
|
|
142
|
+
<div className="flex min-w-0 flex-col gap-0.5">
|
|
143
|
+
<label htmlFor={item.id} className="text-sm font-medium text-fg">
|
|
144
|
+
{item.label}
|
|
145
|
+
</label>
|
|
146
|
+
<span className="text-sm text-fg-secondary">{item.description}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<Switch id={item.id} name={item.id} defaultChecked={item.defaultChecked} />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
</CardContent>
|
|
153
|
+
<CardFooter className="justify-end gap-3">
|
|
154
|
+
{saved ? (
|
|
155
|
+
<span role="status" className="inline-flex items-center gap-1.5 text-sm text-success">
|
|
156
|
+
<Check className="size-4" aria-hidden="true" />
|
|
157
|
+
Saved
|
|
158
|
+
</span>
|
|
159
|
+
) : null}
|
|
160
|
+
<Button type="submit" variant="primary">
|
|
161
|
+
Save changes
|
|
162
|
+
</Button>
|
|
163
|
+
</CardFooter>
|
|
164
|
+
</Card>
|
|
165
|
+
</form>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": {
|
|
3
|
+
"ui": "@/components/ui",
|
|
4
|
+
"lib": "@/lib",
|
|
5
|
+
"blocks": "@/components/blocks"
|
|
6
|
+
},
|
|
7
|
+
"paths": {
|
|
8
|
+
"ui": "components/ui",
|
|
9
|
+
"lib": "lib",
|
|
10
|
+
"blocks": "components/blocks"
|
|
11
|
+
},
|
|
12
|
+
"registry": "https://raw.githubusercontent.com/pedrogbraz/cronus-ui/v0.6.0/registry",
|
|
13
|
+
"theme": {
|
|
14
|
+
"name": "__THEME__",
|
|
15
|
+
"mode": "__MODE__"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# dependencies
|
|
2
|
+
/node_modules
|
|
3
|
+
/.pnp
|
|
4
|
+
.pnp.*
|
|
5
|
+
|
|
6
|
+
# next.js
|
|
7
|
+
/.next/
|
|
8
|
+
/out/
|
|
9
|
+
|
|
10
|
+
# production
|
|
11
|
+
/build
|
|
12
|
+
|
|
13
|
+
# misc
|
|
14
|
+
.DS_Store
|
|
15
|
+
*.pem
|
|
16
|
+
|
|
17
|
+
# debug
|
|
18
|
+
npm-debug.log*
|
|
19
|
+
yarn-debug.log*
|
|
20
|
+
yarn-error.log*
|
|
21
|
+
.pnpm-debug.log*
|
|
22
|
+
|
|
23
|
+
# env files
|
|
24
|
+
.env*.local
|
|
25
|
+
|
|
26
|
+
# typescript
|
|
27
|
+
*.tsbuildinfo
|
|
28
|
+
next-env.d.ts
|
|
29
|
+
|
|
30
|
+
# lockfiles (keep the one your package manager writes)
|
|
31
|
+
# package-lock.json
|
|
32
|
+
# pnpm-lock.yaml
|
|
33
|
+
# yarn.lock
|
|
34
|
+
# bun.lockb
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cronus-ui/ui ships ESM with its `"use client"` boundaries already in place,
|
|
3
|
+
* so no `transpilePackages` is needed when it's installed (the normal case).
|
|
4
|
+
*
|
|
5
|
+
* @type {import('next').NextConfig}
|
|
6
|
+
*/
|
|
7
|
+
const nextConfig = {
|
|
8
|
+
reactStrictMode: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default nextConfig;
|